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 `
`;
}
// The /scrape contract fields beyond title/company. The original fixture had
// no 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 `
Data Engineer
Copenhagen, Denmark
3 days ago
`;
}
describe("parseJobCards contract fields", () => {
test("extracts date from the listdate 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 (é)", () => {
const [card] = parseJobCards(searchCard("123", "Café Manager"));
expect(card.title).toBe("Café Manager");
});
test("decodes uppercase-X hexadecimal entities (...;)", () => {
const [card] = parseJobCards(searchCard("124", "Debüt Role")); // ü = ü
expect(card.title).toBe("Debüt Role");
});
test("still decodes decimal numeric entities (é) — regression", () => {
const [card] = parseJobCards(searchCard("125", "Café Lead"));
expect(card.title).toBe("Café Lead");
});
test("decodes supplementary-plane code points with fromCodePoint (😀)", () => {
const [card] = parseJobCards(searchCard("126", "Growth 😀"));
expect(card.title).toBe("Growth 😀");
});
test("decodes hex supplementary-plane code points (😀)", () => {
const [card] = parseJobCards(searchCard("127", "Growth 😀"));
expect(card.title).toBe("Growth 😀");
});
test("decodes hex entities in the company subtitle too", () => {
const [card] = parseJobCards(searchCard("128", "Engineer", "Nørrebro ApS"));
expect(card.company).toBe("Nørrebro ApS");
});
});
describe("parseJobDetail dropped fields", () => {
test("emits no applyUrl field", () => {
// The extraction regex assumed class-before-href and never matched
// LinkedIn's real markup (null on every live posting), and a fixed
// version would only capture the job-view URL - a duplicate of `url`.
// The field is dropped rather than fixed (review finding F19,
// 2026-08-19). This test pins the removal so it does not quietly
// return as a broken or redundant field.
const job = parseJobDetail("", "1");
expect("applyUrl" in job).toBe(false);
});
});
describe("decodeHtmlEntities (via parseJobDetail)", () => {
test("decodes hex entities inside the job title", () => {
const html = `Señor Engineer `;
const job = parseJobDetail(html, "999");
expect(job.title).toBe("Señor Engineer");
});
});
describe("extractDivContent", () => {
test("extracts content from simple div", () => {
const html = 'Simple text
';
expect(extractDivContent(html, "description__text")).toBe("Simple text");
});
test("extracts content with nested divs — the regression case", () => {
const html = `
Requirements:
About Us:
We are...
`;
expect(extractDivContent(html, "description__text")).toBe(
'\n Requirements:
\n \n About Us:
\n We are...
\n ',
);
});
test("returns null when class not found", () => {
expect(extractDivContent("no class
", "nonexistent")).toBeNull();
});
test("works with show-more-less-html__markup class", () => {
const html = 'LinkedIn content
';
expect(extractDivContent(html, "show-more-less-html__markup")).toBe("LinkedIn content");
});
test("handles deeply nested divs (3 levels)", () => {
const html = ``;
expect(extractDivContent(html, "description__text")).toBe(
'\n \n ',
);
});
test("handles empty content", () => {
const html = '
';
expect(extractDivContent(html, "description__text")).toBe("");
});
test("parseJobDetail uses extractDivContent and preserves full description", () => {
const html = `
Requirements:
About Us:
We are hiring!
`;
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();
});
});