mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
A live page wrote its Jobtype item as "Fuldtid </ li >". node-html-parser consumes well-formed markup but does not recognise a closing tag with a space after the "<" or the slash, so the fragment became a text node and overviewValue kept it: employmentType: ["Fuldtid </ li >"], matching none of the documented job-type vocabulary. The same cleanText feeds every text field on both branches. Strip only the closing-tag shape in cleanText before collapsing whitespace. The parser decodes entities before cleanText runs, so author text the page encoded as <div> or <T> arrives as a literal <div> / <T> and must stay; a probe of what the parser leaks shows only malformed closing shapes (</ li >, </ li>, < /li>). Two fixtures: the stray tag in both the Jobtype item and a description paragraph (fails on master), and an entity-encoded <div> that survives. Observed by the maintainer in the #516 merge note.
131 lines
5.7 KiB
TypeScript
131 lines
5.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { parseJobPostingFromHtml } from "../src/commands/detail";
|
|
|
|
const HTML_WITHOUT_JSON_LD = `<!doctype html>
|
|
<html lang="da">
|
|
<head>
|
|
<title>Journalistisk udvikler søges | jobdanmark</title>
|
|
</head>
|
|
<body>
|
|
<div class="job-list-details">
|
|
<div class="job-details-head row mx-0">
|
|
<div class="company-logo col-auto">
|
|
<img src="/media/jfm-logo.png?width=100" alt="JFM">
|
|
</div>
|
|
<h3 class="title">Journalistisk udvikler søges</h3>
|
|
<a href="/virksomheder/jfm">JFM</a>
|
|
<span>Banegårdspladsen 1, 5000 Odense C</span>
|
|
</div>
|
|
<p>Hvad nu hvis du med få klik kunne undersøge dit lokalområde?</p>
|
|
<ul>
|
|
<li>identificere relevante datasæt og muligheder</li>
|
|
</ul>
|
|
</div>
|
|
<a href="https://jfm.career.emply.com/da/apply/example" class="action primary count-click">Ansøg nu</a>
|
|
<ul class="job-overview list-unstyled">
|
|
<li><strong>Udgivet:</strong> 03-07-2026</li>
|
|
<li><strong>Jobtype:</strong> Fuldtid</li>
|
|
<li><strong>Arbejdssted:</strong> Banegårdspladsen 1, 5000 Odense C</li>
|
|
<li><strong>Ansøgningsfrist:</strong> 02-08-2026 23.59</li>
|
|
</ul>
|
|
</body>
|
|
</html>`;
|
|
|
|
describe("parseJobPostingFromHtml", () => {
|
|
test("falls back to rendered Jobdanmark HTML when JSON-LD is absent", () => {
|
|
const parsed = parseJobPostingFromHtml(
|
|
HTML_WITHOUT_JSON_LD,
|
|
"journalistisk-udvikler",
|
|
"https://jobdanmark.dk/job/journalistisk-udvikler",
|
|
);
|
|
|
|
expect(parsed.title).toBe("Journalistisk udvikler søges");
|
|
// The fallback must emit the same shapes as the JSON-LD branch: contract
|
|
// dates, not the page's raw DD-MM-YYYY text (review finding F25, 2026-08-19).
|
|
expect(parsed.datePosted).toBe("2026-07-03");
|
|
expect(parsed.validThrough).toBe("2026-08-02");
|
|
expect(parsed.employmentType).toEqual(["Fuldtid"]);
|
|
expect(parsed.hiringOrganization.name).toBe("JFM");
|
|
expect(parsed.hiringOrganization.logo).toBe("https://jobdanmark.dk/media/jfm-logo.png?width=100");
|
|
expect(parsed.jobLocation.streetAddress).toBe("Banegårdspladsen 1, 5000 Odense C");
|
|
expect(parsed.jobLocation.addressLocality).toBe("Odense C");
|
|
expect(parsed.description).toContain("identificere relevante datasæt");
|
|
expect(parsed.applyUrl).toBe("https://jfm.career.emply.com/da/apply/example");
|
|
});
|
|
|
|
test("maps a rolling deadline (Løbende) to null in the HTML fallback", () => {
|
|
// "Løbende" is free text meaning rolling/ongoing - jobbank's parser maps
|
|
// its equivalent to null, and a stored "Løbende" deadline would hit every
|
|
// date-arithmetic consumer (review finding F25, 2026-08-19).
|
|
const html = HTML_WITHOUT_JSON_LD.replace(
|
|
"<li><strong>Ansøgningsfrist:</strong> 02-08-2026 23.59</li>",
|
|
"<li><strong>Ansøgningsfrist:</strong> Løbende</li>",
|
|
);
|
|
const parsed = parseJobPostingFromHtml(html, "s", "https://jobdanmark.dk/job/s");
|
|
expect(parsed.validThrough).toBeNull();
|
|
});
|
|
|
|
test("drops a malformed closing tag that the parser left as text (#525)", () => {
|
|
// A live page (konsulent-til-renoveringsprogram) wrote its Jobtype item as
|
|
// `Fuldtid </ li >`; node-html-parser does not recognise `</ li >` as a tag,
|
|
// so it reached employmentType verbatim. The same cleanText feeds the title,
|
|
// company, workplace and description on this branch.
|
|
const html = HTML_WITHOUT_JSON_LD.replace(
|
|
"<li><strong>Jobtype:</strong> Fuldtid</li>",
|
|
"<li><strong>Jobtype:</strong> Fuldtid </ li ></li>",
|
|
).replace(
|
|
"<p>Hvad nu hvis du med få klik kunne undersøge dit lokalområde?</p>",
|
|
"<p>Hvad nu hvis du med få klik kunne undersøge dit lokalområde?</ p ></p>",
|
|
);
|
|
|
|
const parsed = parseJobPostingFromHtml(
|
|
html,
|
|
"journalistisk-udvikler",
|
|
"https://jobdanmark.dk/job/journalistisk-udvikler",
|
|
);
|
|
|
|
expect(parsed.employmentType).toEqual(["Fuldtid"]);
|
|
expect(parsed.description.split("\n")[0]).toBe("Hvad nu hvis du med få klik kunne undersøge dit lokalområde?");
|
|
expect(JSON.stringify(parsed)).not.toMatch(/<\s*\/?\s*[a-z]+\s*>/i);
|
|
});
|
|
|
|
test("keeps author text the page entity-encoded as a literal tag (#525)", () => {
|
|
// node-html-parser decodes entities before cleanText runs, so `<div>`
|
|
// in a description arrives as a literal `<div>`. Only the malformed
|
|
// closing-tag shape is stripped; an opening tag or a type parameter stays.
|
|
const html = HTML_WITHOUT_JSON_LD.replace(
|
|
"<p>Hvad nu hvis du med få klik kunne undersøge dit lokalområde?</p>",
|
|
"<p>Erfaring med <div> og <span> i HTML, og <T> generics; x < y > z.</p>",
|
|
);
|
|
|
|
const parsed = parseJobPostingFromHtml(
|
|
html,
|
|
"journalistisk-udvikler",
|
|
"https://jobdanmark.dk/job/journalistisk-udvikler",
|
|
);
|
|
|
|
expect(parsed.description.split("\n")[0]).toBe(
|
|
"Erfaring med <div> og <span> i HTML, og <T> generics; x < y > z.",
|
|
);
|
|
});
|
|
|
|
test("does not reject titles containing '404' mid-phrase", () => {
|
|
const htmlWith404InTitle = HTML_WITHOUT_JSON_LD.replace(
|
|
"<title>Journalistisk udvikler søges | jobdanmark</title>",
|
|
"<title>HTTP 404 Page Designer | jobdanmark</title>",
|
|
).replace(
|
|
'<h3 class="title">Journalistisk udvikler søges</h3>',
|
|
'<h3 class="title">HTTP 404 Page Designer</h3>',
|
|
);
|
|
|
|
const parsed = parseJobPostingFromHtml(
|
|
htmlWith404InTitle,
|
|
"http-404-designer",
|
|
"https://jobdanmark.dk/job/http-404-designer",
|
|
);
|
|
|
|
expect(parsed.title).toBe("HTTP 404 Page Designer");
|
|
expect(parsed.hiringOrganization.name).toBe("JFM");
|
|
});
|
|
});
|