mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
The jobindex and linkedin detail parsers matched description containers with a non-greedy regex that stops at the first inner </div>, so any posting whose description contains nested divs was silently truncated (jobindex dropped later sections; linkedin dropped everything after the first block). Replaces the regex with a depth-tracked extractDivContent scanner that walks div open/close markers to the matching close. Verified: truncation bug reproduced against real markup fixtures, depth arithmetic correct (no off-by-one/infinite-loop), 28 tests pass network-free, no regression on non-nested divs. Malformed-HTML over-grabs rather than truncates - the safer failure, cleaned by downstream stripTags/decode. By @oscarbol09.
99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import { parseJobCards, extractDivContent } from "../src/helpers";
|
|
|
|
// Minimal jobad-wrapper markup: parseJobCards splits on `jobad-wrapper-<id>`
|
|
// and reads the title from the <h4><a href>…</a></h4> and the company from the
|
|
// jix-toolbar-top__company link. We inject HTML entities to exercise decoding.
|
|
function card(id: string, title: string, company = "Acme A/S"): string {
|
|
return `<div id="jobad-wrapper-${id}" class="PaidJob">
|
|
<h4><a href="https://www.jobindex.dk/jobannonce/${id}">${title}</a></h4>
|
|
<div class="jix-toolbar-top__company">
|
|
<a href="https://www.jobindex.dk/virksomhed/acme">${company}</a>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
describe("decodeHtmlEntities (via parseJobCards)", () => {
|
|
test("decodes hexadecimal numeric entities (ø -> ø)", () => {
|
|
const [c] = parseJobCards(card("h1", "Smørrebro Chef"));
|
|
expect(c.title).toBe("Smørrebro Chef");
|
|
});
|
|
|
|
test("decodes uppercase-X hexadecimal entities (æ -> æ)", () => {
|
|
const [c] = parseJobCards(card("h2", "Kære Kollega"));
|
|
expect(c.title).toBe("Kære Kollega");
|
|
});
|
|
|
|
test("still decodes decimal numeric entities (å -> å) — regression", () => {
|
|
const [c] = parseJobCards(card("h3", "århus Lead"));
|
|
expect(c.title).toBe("århus Lead");
|
|
});
|
|
|
|
test("decodes supplementary-plane code points with fromCodePoint (😀)", () => {
|
|
const [c] = parseJobCards(card("h4", "Growth 😀"));
|
|
expect(c.title).toBe("Growth 😀");
|
|
});
|
|
|
|
test("decodes hex supplementary-plane code points (😀)", () => {
|
|
const [c] = parseJobCards(card("h5", "Growth 😀"));
|
|
expect(c.title).toBe("Growth 😀");
|
|
});
|
|
|
|
test("decodes hex entities in the company name too", () => {
|
|
const [c] = parseJobCards(card("h6", "Engineer", "Nørrebro ApS"));
|
|
expect(c.company).toBe("Nørrebro ApS");
|
|
});
|
|
});
|
|
|
|
describe("extractDivContent", () => {
|
|
test("extracts content from simple div", () => {
|
|
const html = '<div class="job-text">Simple text</div>';
|
|
expect(extractDivContent(html, "job-text")).toBe("Simple text");
|
|
});
|
|
|
|
test("extracts content with nested divs — the regression case", () => {
|
|
const html = `<div class="job-text">
|
|
<div class="highlight">First section</div>
|
|
<div class="details">Second section with important info</div>
|
|
</div>`;
|
|
expect(extractDivContent(html, "job-text")).toBe(
|
|
'\n <div class="highlight">First section</div>\n <div class="details">Second section with important info</div>\n ',
|
|
);
|
|
});
|
|
|
|
test("returns null when class not found", () => {
|
|
expect(extractDivContent("<div>no class</div>", "nonexistent")).toBeNull();
|
|
});
|
|
|
|
test("works with extra attributes on the div", () => {
|
|
const html = '<div id="desc" class="job-text" data-x="1">Content</div>';
|
|
expect(extractDivContent(html, "job-text")).toBe("Content");
|
|
});
|
|
|
|
test("handles deeply nested divs (3 levels)", () => {
|
|
const html = `<div class="job-text">
|
|
<div>
|
|
<div>Deep content</div>
|
|
</div>
|
|
</div>`;
|
|
expect(extractDivContent(html, "job-text")).toBe(
|
|
'\n <div>\n <div>Deep content</div>\n </div>\n ',
|
|
);
|
|
});
|
|
|
|
test("handles empty content", () => {
|
|
const html = '<div class="job-text"></div>';
|
|
expect(extractDivContent(html, "job-text")).toBe("");
|
|
});
|
|
|
|
test("handles br and other non-div tags", () => {
|
|
const html = '<div class="job-text">Line1<br>Line2<br>Line3</div>';
|
|
expect(extractDivContent(html, "job-text")).toBe("Line1<br>Line2<br>Line3");
|
|
});
|
|
|
|
test("escapes special regex characters in class name", () => {
|
|
const html = '<div class="job-text (special)">Content</div>';
|
|
expect(extractDivContent(html, "job-text (special)")).toBe("Content");
|
|
});
|
|
});
|