diff --git a/JobTrackerApi.Tests/JobDiscoveryControllerTests.cs b/JobTrackerApi.Tests/JobDiscoveryControllerTests.cs index b8d70c8..2c3232e 100644 --- a/JobTrackerApi.Tests/JobDiscoveryControllerTests.cs +++ b/JobTrackerApi.Tests/JobDiscoveryControllerTests.cs @@ -31,6 +31,22 @@ public sealed class JobDiscoveryControllerTests Assert.Equal("NO", job.CountryCode); } + [Fact] + public async Task Search_keeps_latest_active_event_and_removes_inactive_duplicates() + { + var feed = """{"next_url":"","items":[{"date_modified":"2026-07-30T09:00:00Z","_feed_entry":{"uuid":"1","status":"ACTIVE","title":"Old title"}},{"date_modified":"2026-07-30T10:00:00Z","_feed_entry":{"uuid":"1","status":"ACTIVE","title":"Current title"}},{"date_modified":"2026-07-30T10:00:00Z","_feed_entry":{"uuid":"2","status":"ACTIVE","title":"Withdrawn"}},{"date_modified":"2026-07-30T11:00:00Z","_feed_entry":{"uuid":"2","status":"INACTIVE","title":"Withdrawn"}}]}"""; + var client = new HttpClient(new Handler(_ => feed)); + var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["NavJobs:Token"] = "configured-test-token" }).Build(); + var controller = new JobDiscoveryController(new ClientFactory(client), configuration, new MemoryCache(new MemoryCacheOptions())); + + var result = await controller.Search(null, null, CancellationToken.None); + + var ok = Assert.IsType(result.Result); + var job = Assert.Single(Assert.IsAssignableFrom>(ok.Value)); + Assert.Equal("1", job.Id); + Assert.Equal("Current title", job.Title); + } + private sealed class ClientFactory(HttpClient client) : IHttpClientFactory { public HttpClient CreateClient(string name) => client; diff --git a/docs/audits/evidence/jobs-001-discovery-1440-dark.png b/docs/audits/evidence/jobs-001-discovery-1440-dark.png new file mode 100644 index 0000000..a8a53a5 Binary files /dev/null and b/docs/audits/evidence/jobs-001-discovery-1440-dark.png differ diff --git a/docs/audits/evidence/jobs-001-discovery-375-light.png b/docs/audits/evidence/jobs-001-discovery-375-light.png new file mode 100644 index 0000000..14318a4 Binary files /dev/null and b/docs/audits/evidence/jobs-001-discovery-375-light.png differ diff --git a/job-tracker-ui/e2e/job-discovery.spec.ts b/job-tracker-ui/e2e/job-discovery.spec.ts new file mode 100644 index 0000000..9ed4621 --- /dev/null +++ b/job-tracker-ui/e2e/job-discovery.spec.ts @@ -0,0 +1,90 @@ +import { expect, test, type Page } from "@playwright/test"; +import path from "node:path"; + +async function login(page: Page) { + await page.goto("/login"); + await page.getByLabel("Email").fill("e2e@example.test"); + await page.getByLabel("Current password").fill("E2ePassword123!"); + await page.getByRole("button", { name: "Sign in", exact: true }).click(); + await expect(page).toHaveURL(/\/dashboard$/); +} + +test.beforeEach(async ({ page }) => { + await page.addInitScript(() => window.localStorage.setItem("uiLanguage", "en")); +}); + +test("discovery remains usable across widths and opens the reviewed import", async ({ page }) => { + await page.route("**/api/job-discovery/search**", async (route) => { + await route.fulfill({ json: [ + { + id: "long-no", + title: "Senior systemutvikler for robuste og samfunnskritiske plattformer", + company: "Eksempelbedriften for norske bokstaver Æ Ø Å", + location: "TRONDHEIM", + modifiedAt: "2026-08-09T10:00:00Z", + deadline: "2026-08-20T23:59:59Z", + url: "https://arbeidsplassen.nav.no/stillinger/stilling/long-no", + source: "nav", + sourceName: "NAV Arbeidsplassen", + acquisitionType: "searched", + retrievedAt: "2026-08-10T10:00:00Z", + countryCode: "NO", + }, + { + id: "analyst", + title: "Analyst", + url: "https://arbeidsplassen.nav.no/stillinger/stilling/analyst", + source: "nav", + sourceName: "NAV Arbeidsplassen", + acquisitionType: "searched", + retrievedAt: "2026-08-10T10:00:00Z", + countryCode: "NO", + }, + ] }); + }); + await page.route("**/api/jobimport/preview", async (route) => { + await route.fulfill({ json: { success: true, title: "Imported NAV role", company: "Synthetic AS", description: "Synthetic description", tags: [], sourceUrl: "https://arbeidsplassen.nav.no/stillinger/stilling/long-no", source: "nav", countryCode: "NO" } }); + }); + + await login(page); + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto("/discover"); + + await page.getByLabel("Role or company").fill("utvikler"); + await page.keyboard.press("Tab"); + await expect(page.getByLabel("Municipality")).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(page.getByRole("button", { name: "Search NAV" })).toBeFocused(); + await page.keyboard.press("Enter"); + + await expect(page.getByText("2 vacancies found")).toBeVisible(); + await expect(page.getByText("NAV Arbeidsplassen").first()).toBeVisible(); + await expect(page.getByText("Work arrangement not provided by this source").first()).toBeVisible(); + await expect(page.getByText(/Application deadline:/)).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); + await page.screenshot({ path: path.resolve("..", "docs", "audits", "evidence", "jobs-001-discovery-375-light.png"), fullPage: true }); + + await page.getByLabel("Sort results").click(); + await page.getByRole("option", { name: "Title A–Z" }).click(); + await expect(page.getByRole("combobox", { name: "Sort results" })).toHaveText("Title A–Z"); + await expect(page.getByRole("heading", { name: "Analyst", exact: true })).toBeVisible(); + + for (const width of [768, 1440]) { + await page.setViewportSize({ width, height: 900 }); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); + } + + await page.evaluate(() => { + const userKey = window.localStorage.getItem("authUserKey") || "anon"; + window.localStorage.setItem(`themeMode:${userKey}`, "dark"); + }); + await page.reload(); + await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark"); + await page.getByRole("button", { name: "Search NAV" }).click(); + await expect(page.getByText("2 vacancies found")).toBeVisible(); + await page.screenshot({ path: path.resolve("..", "docs", "audits", "evidence", "jobs-001-discovery-1440-dark.png"), fullPage: true }); + + await page.getByRole("button", { name: "Save to tracker" }).first().click(); + await expect(page.getByRole("dialog")).toBeVisible(); + await expect(page.getByLabel("Job title")).toHaveValue("Imported NAV role"); +}); diff --git a/job-tracker-ui/src/views/JobDiscoveryPage.tsx b/job-tracker-ui/src/views/JobDiscoveryPage.tsx index 2b95715..0edbd5b 100644 --- a/job-tracker-ui/src/views/JobDiscoveryPage.tsx +++ b/job-tracker-ui/src/views/JobDiscoveryPage.tsx @@ -65,10 +65,10 @@ export default function JobDiscoveryPage() { setLocation(event.target.value)} /> - Official Norwegian vacancies from NAV. FINN, Indeed and LinkedIn jobs can still be captured by URL. - {error ? void runSearch(lastSearch.query, lastSearch.location)} disabled={loading}>Retry}>{error} : null} + Official Norwegian vacancies from NAV. FINN, Indeed and LinkedIn jobs can still be captured by URL. + {error ? void runSearch(lastSearch.query, lastSearch.location)} disabled={loading}>Retry}>{error} : null} {!loading && !hasSearched ? Search recent active vacancies by role, company, or municipality. : null} - {!loading && hasSearched && !error && jobs.length === 0 ? No active vacancies matched this search. Try a broader role, company, or municipality. : null} + {!loading && hasSearched && !error && jobs.length === 0 ? No active vacancies matched this search. Try a broader role, company, or municipality. : null} {loading ? Searching NAV vacancies… : null} {jobs.length > 0 ? (