mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-08 16:07:40 +00:00
toResult passed posted_at through verbatim, so search and detail JSON carried "date": "2026-07-06T00:00:00Z" while every other portal CLI emits the date part only and /scrape stores it as posted_date: "YYYY-MM-DD". The table and plain renderers already cut the value to ten characters, so only JSON consumers - the /scrape path - saw the raw timestamp. Add contractDate(): the timestamp's date part, null for an absent or non-date value. The two tests that pinned the raw timestamp now expect the date and fail on master, plus three cases on the helper. SKILL.md documents the shape.
151 lines
5.5 KiB
TypeScript
151 lines
5.5 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
||
import { cleanHtml, contractDate, normalizeSlug, toResult, toDetail, type FreehireJob } from "../src/helpers";
|
||
|
||
function job(overrides: Partial<FreehireJob> = {}): FreehireJob {
|
||
return {
|
||
public_slug: "backend-engineer-acme-ab12cd34",
|
||
source: "greenhouse",
|
||
external_id: "acme:1",
|
||
url: "https://boards.greenhouse.io/acme/jobs/1",
|
||
title: "Backend Engineer",
|
||
company: "Acme",
|
||
company_slug: "acme",
|
||
location: "Berlin, Germany",
|
||
description: "<ul><li>Build APIs</li><li>Ship & iterate</li></ul>",
|
||
skills: ["go", "kubernetes"],
|
||
work_mode: "remote",
|
||
regions: ["eu"],
|
||
countries: ["de"],
|
||
cities: ["Berlin"],
|
||
posted_at: "2026-07-06T00:00:00Z",
|
||
created_at: "2026-07-06T15:00:00Z",
|
||
enrichment: {},
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
describe("toResult — reshape into the portal-skill contract", () => {
|
||
test("maps public_slug -> id and posted_at -> date", () => {
|
||
const r = toResult(job());
|
||
expect(r.id).toBe("backend-engineer-acme-ab12cd34");
|
||
// The contract shape is the date part only, like every other portal CLI.
|
||
expect(r.date).toBe("2026-07-06");
|
||
});
|
||
|
||
test("carries the required contract fields", () => {
|
||
const r = toResult(job());
|
||
expect(r).toMatchObject({
|
||
title: "Backend Engineer",
|
||
company: "Acme",
|
||
company_slug: "acme",
|
||
location: "Berlin, Germany",
|
||
url: "https://boards.greenhouse.io/acme/jobs/1",
|
||
});
|
||
});
|
||
|
||
test("missing values are null, not omitted", () => {
|
||
const r = toResult(job({ company: "", location: "", posted_at: null, work_mode: undefined }));
|
||
expect(r.company).toBeNull();
|
||
expect(r.location).toBeNull();
|
||
expect(r.date).toBeNull();
|
||
expect(r.work_mode).toBeNull();
|
||
});
|
||
|
||
test("decodes HTML entities and trims whitespace in the title", () => {
|
||
// Real title from the index (freehire slug intern-fullstack-amp-ai-innovation-noise-edaawqq2).
|
||
expect(toResult(job({ title: "Intern - Fullstack & AI Innovation " })).title).toBe(
|
||
"Intern - Fullstack & AI Innovation",
|
||
);
|
||
expect(toResult(job({ title: "R&D Engineer" })).title).toBe("R&D Engineer");
|
||
expect(toResult(job({ title: "SDE Intern - Frontend " })).title).toBe("SDE Intern - Frontend");
|
||
});
|
||
|
||
test("a whitespace-only title still falls back to (untitled)", () => {
|
||
expect(toResult(job({ title: " " })).title).toBe("(untitled)");
|
||
expect(toResult(job({ title: "" })).title).toBe("(untitled)");
|
||
});
|
||
|
||
test("detail inherits the cleaned title", () => {
|
||
expect(toDetail(job({ title: "Data & Analytics Intern" })).title).toBe("Data & Analytics Intern");
|
||
});
|
||
});
|
||
|
||
describe("toDetail — adds cleaned description + enrichment", () => {
|
||
test("strips HTML and decodes entities in the description", () => {
|
||
const d = toDetail(job());
|
||
expect(d.description).toBe("Build APIs\nShip & iterate");
|
||
});
|
||
|
||
test("surfaces enrichment fields and a formatted salary", () => {
|
||
const d = toDetail(
|
||
job({
|
||
enrichment: { seniority: "senior", category: "backend", employment_type: "full_time", salary_min: 90000, salary_max: 120000, salary_currency: "EUR" },
|
||
}),
|
||
);
|
||
expect(d.seniority).toBe("senior");
|
||
expect(d.category).toBe("backend");
|
||
expect(d.salary).toBe("EUR 90000–120000");
|
||
});
|
||
|
||
test("keeps the salary period, so a yearly figure is not read as monthly", () => {
|
||
const d = toDetail(
|
||
job({ enrichment: { salary_min: 300000, salary_max: 300000, salary_currency: "INR", salary_period: "year" } }),
|
||
);
|
||
expect(d.salary).toBe("INR 300000–300000/year");
|
||
});
|
||
|
||
test("a single-bound salary keeps its period too", () => {
|
||
const d = toDetail(job({ enrichment: { salary_min: 12000, salary_currency: "INR", salary_period: "month" } }));
|
||
expect(d.salary).toBe("INR 12000/month");
|
||
});
|
||
|
||
test("null enrichment fields when the enrichment object is empty", () => {
|
||
const d = toDetail(job({ enrichment: {} }));
|
||
expect(d.seniority).toBeNull();
|
||
expect(d.salary).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("cleanHtml", () => {
|
||
test("preserves paragraph breaks between blocks", () => {
|
||
expect(cleanHtml("<p>One</p><p>Two</p>")).toBe("One\nTwo");
|
||
});
|
||
test("decodes hex numeric entities", () => {
|
||
expect(cleanHtml("Café")).toBe("Café");
|
||
});
|
||
test("returns null for empty input", () => {
|
||
expect(cleanHtml("")).toBeNull();
|
||
expect(cleanHtml(null)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("contractDate", () => {
|
||
test("cuts the API's ISO timestamp to YYYY-MM-DD", () => {
|
||
expect(contractDate("2026-07-06T00:00:00Z")).toBe("2026-07-06");
|
||
expect(contractDate("2026-07-06T15:30:00+02:00")).toBe("2026-07-06");
|
||
expect(contractDate("2026-07-06")).toBe("2026-07-06");
|
||
});
|
||
test("is null for an absent or non-date value", () => {
|
||
expect(contractDate(null)).toBeNull();
|
||
expect(contractDate(undefined)).toBeNull();
|
||
expect(contractDate("")).toBeNull();
|
||
expect(contractDate("soon")).toBeNull();
|
||
});
|
||
test("detail carries the same shape as search", () => {
|
||
expect(toDetail(job()).date).toBe("2026-07-06");
|
||
});
|
||
});
|
||
|
||
describe("normalizeSlug", () => {
|
||
test("accepts a bare slug", () => {
|
||
expect(normalizeSlug("golang-zensar-2bxu6dxm")).toBe("golang-zensar-2bxu6dxm");
|
||
});
|
||
test("extracts the slug from a /jobs/<slug> URL", () => {
|
||
expect(normalizeSlug("https://freehire.me/jobs/golang-zensar-2bxu6dxm")).toBe("golang-zensar-2bxu6dxm");
|
||
});
|
||
test("rejects a non-slug string", () => {
|
||
expect(normalizeSlug("not a slug!")).toBeNull();
|
||
expect(normalizeSlug("")).toBeNull();
|
||
});
|
||
});
|