Files
ai-job-search/.agents/skills/linkedin-search/cli/tests/parsing.test.ts
T
Mads LorentzenandClaude Opus 5 2edf8c41f1 test(portals): cover linkedin card date/location and jobindex parseSearchPage
The linkedin fixture was purpose-built for entity decoding and had no
<time> or location element, so removing the date extraction - a /scrape
contract field on a default-ON portal - survived the suite. jobindex's
parseSearchPage (the Stash parser behind every search) had zero tests,
so meta.total silently dropping hitcount survived too. Both mutations
now fail exactly the new tests. The ASAP deadline branch is deliberately
left to the F12 fix, which changes its behaviour to null. Review finding
F35 (2026-08-19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:03:00 +02:00

167 lines
6.5 KiB
TypeScript

import { describe, test, expect } from "bun:test";
import { parseJobCards, parseJobDetail, extractDivContent, minutesToTPR } from "../src/helpers";
// Minimal search-card markup: parseJobCards splits on the job-posting URN and
// needs an id, a base-search-card__title, and a full-link. Everything else is
// optional. We inject HTML entities into the title/company to exercise decoding.
function searchCard(id: string, title: string, company = "Acme"): string {
return `<li>
<div data-entity-urn="urn:li:jobPosting:${id}">
<a class="base-card__full-link" href="https://www.linkedin.com/jobs/view/${id}"></a>
<h3 class="base-search-card__title">${title}</h3>
<h4 class="base-search-card__subtitle"><a href="https://www.linkedin.com/company/acme">${company}</a></h4>
</div>
</li>`;
}
// The /scrape contract fields beyond title/company. The original fixture had
// no <time> or location element at all, so deleting the date extraction from
// parseJobCards left every test green (review finding F35, 2026-08-19).
function searchCardWithMeta(id: string, datetimeAttr: string, listdateClass = "job-search-card__listdate"): string {
return `<li>
<div data-entity-urn="urn:li:jobPosting:${id}">
<a class="base-card__full-link" href="https://www.linkedin.com/jobs/view/${id}"></a>
<h3 class="base-search-card__title">Data Engineer</h3>
<h4 class="base-search-card__subtitle"><a href="https://www.linkedin.com/company/acme">Acme</a></h4>
<span class="job-search-card__location">Copenhagen, Denmark</span>
<time class="${listdateClass}" datetime="${datetimeAttr}">3 days ago</time>
</div>
</li>`;
}
describe("parseJobCards contract fields", () => {
test("extracts date from the listdate <time> element", () => {
const [card] = parseJobCards(searchCardWithMeta("200", "2026-08-10"));
expect(card.date).toBe("2026-08-10");
});
test("extracts date from the listdate--new variant class", () => {
const [card] = parseJobCards(
searchCardWithMeta("201", "2026-08-15", "job-search-card__listdate--new"),
);
expect(card.date).toBe("2026-08-15");
});
test("extracts location from the location span", () => {
const [card] = parseJobCards(searchCardWithMeta("202", "2026-08-10"));
expect(card.location).toBe("Copenhagen, Denmark");
});
test("date and location are null when the elements are absent", () => {
const [card] = parseJobCards(searchCard("203", "Bare Card"));
expect(card.date).toBeNull();
expect(card.location).toBeNull();
});
});
describe("decodeHtmlEntities (via parseJobCards)", () => {
test("decodes hexadecimal numeric entities (&#xE9;)", () => {
const [card] = parseJobCards(searchCard("123", "Caf&#xE9; Manager"));
expect(card.title).toBe("Café Manager");
});
test("decodes uppercase-X hexadecimal entities (&#X...;)", () => {
const [card] = parseJobCards(searchCard("124", "Deb&#XFC;t Role")); // &#XFC; = ü
expect(card.title).toBe("Debüt Role");
});
test("still decodes decimal numeric entities (&#233;) — regression", () => {
const [card] = parseJobCards(searchCard("125", "Caf&#233; Lead"));
expect(card.title).toBe("Café Lead");
});
test("decodes supplementary-plane code points with fromCodePoint (&#128512;)", () => {
const [card] = parseJobCards(searchCard("126", "Growth &#128512;"));
expect(card.title).toBe("Growth 😀");
});
test("decodes hex supplementary-plane code points (&#x1F600;)", () => {
const [card] = parseJobCards(searchCard("127", "Growth &#x1F600;"));
expect(card.title).toBe("Growth 😀");
});
test("decodes hex entities in the company subtitle too", () => {
const [card] = parseJobCards(searchCard("128", "Engineer", "N&#xF8;rrebro ApS"));
expect(card.company).toBe("Nørrebro ApS");
});
});
describe("decodeHtmlEntities (via parseJobDetail)", () => {
test("decodes hex entities inside the job title", () => {
const html = `<h1 class="topcard__title">Se&#xF1;or Engineer</h1>`;
const job = parseJobDetail(html, "999");
expect(job.title).toBe("Señor Engineer");
});
});
describe("extractDivContent", () => {
test("extracts content from simple div", () => {
const html = '<div class="description__text">Simple text</div>';
expect(extractDivContent(html, "description__text")).toBe("Simple text");
});
test("extracts content with nested divs — the regression case", () => {
const html = `<div class="description__text">
<div>Requirements:</div>
<ul><li>Skill A</li></ul>
<div>About Us:</div>
<p>We are...</p>
</div>`;
expect(extractDivContent(html, "description__text")).toBe(
'\n <div>Requirements:</div>\n <ul><li>Skill A</li></ul>\n <div>About Us:</div>\n <p>We are...</p>\n ',
);
});
test("returns null when class not found", () => {
expect(extractDivContent("<div>no class</div>", "nonexistent")).toBeNull();
});
test("works with show-more-less-html__markup class", () => {
const html = '<div class="show-more-less-html__markup">LinkedIn content</div>';
expect(extractDivContent(html, "show-more-less-html__markup")).toBe("LinkedIn content");
});
test("handles deeply nested divs (3 levels)", () => {
const html = `<div class="description__text">
<div>
<div>Deep content</div>
</div>
</div>`;
expect(extractDivContent(html, "description__text")).toBe(
'\n <div>\n <div>Deep content</div>\n </div>\n ',
);
});
test("handles empty content", () => {
const html = '<div class="description__text"></div>';
expect(extractDivContent(html, "description__text")).toBe("");
});
test("parseJobDetail uses extractDivContent and preserves full description", () => {
const html = `<div class="description__text">
<div>Requirements:</div>
<ul><li>5 years Python</li></ul>
<div>About Us:</div>
<p>We are hiring!</p>
</div>`;
const job = parseJobDetail(html, "999");
expect(job.description).toContain("Requirements:");
expect(job.description).toContain("5 years Python");
expect(job.description).toContain("About Us:");
expect(job.description).toContain("We are hiring!");
});
});
describe("minutesToTPR", () => {
test("converts minutes to an f_TPR seconds window", () => {
expect(minutesToTPR(30)).toBe("r1800");
expect(minutesToTPR(1)).toBe("r60");
expect(minutesToTPR(1440)).toBe("r86400"); // matches jobageToTPR(1)
});
test("returns null for non-positive input", () => {
expect(minutesToTPR(0)).toBeNull();
expect(minutesToTPR(-5)).toBeNull();
});
});