Files
ai-job-search/.agents/skills/jobdanmark-search/cli/tests/detail-parsing.test.ts
T
Ayobami Adegoke a644df0625 fix(jobdanmark-search): drop a malformed closing tag the parser left as text (#526)
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 &lt;div&gt; or &lt;T&gt; 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.
2026-10-15 11:53:54 +02:00

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&#xF8;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&#xF8;ges</h3>
<a href="/virksomheder/jfm">JFM</a>
<span>Baneg&#xE5;rdspladsen 1, 5000 Odense C</span>
</div>
<p>Hvad nu hvis du med f&#xE5; klik kunne unders&#xF8;ge dit lokalomr&#xE5;de?</p>
<ul>
<li>identificere relevante datas&#xE6;t og muligheder</li>
</ul>
</div>
<a href="https://jfm.career.emply.com/da/apply/example" class="action primary count-click">Ans&#xF8;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&#xE5;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&#xE5; klik kunne unders&#xF8;ge dit lokalomr&#xE5;de?</p>",
"<p>Hvad nu hvis du med f&#xE5; klik kunne unders&#xF8;ge dit lokalomr&#xE5;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 `&lt;div&gt;`
// 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&#xE5; klik kunne unders&#xF8;ge dit lokalomr&#xE5;de?</p>",
"<p>Erfaring med &lt;div&gt; og &lt;span&gt; i HTML, og &lt;T&gt; generics; x &lt; y &gt; 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&#xF8;ges | jobdanmark</title>",
"<title>HTTP 404 Page Designer | jobdanmark</title>",
).replace(
'<h3 class="title">Journalistisk udvikler s&#xF8;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");
});
});