feat: complete release readiness work #28

Open
cesnimda wants to merge 110 commits from release-readiness into main
5 changed files with 109 additions and 3 deletions
Showing only changes of commit 82f45269d0 - Show all commits
@@ -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<string, string?> { ["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<OkObjectResult>(result.Result);
var job = Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<JobDiscoveryController.DiscoveredJob>>(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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+90
View File
@@ -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 AZ" }).click();
await expect(page.getByRole("combobox", { name: "Sort results" })).toHaveText("Title AZ");
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");
});
@@ -65,10 +65,10 @@ export default function JobDiscoveryPage() {
<TextField label="Municipality" value={location} onChange={(event) => setLocation(event.target.value)} />
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <SearchIcon />} disabled={loading}>Search NAV</Button>
</Box>
<Alert severity="info">Official Norwegian vacancies from NAV. FINN, Indeed and LinkedIn jobs can still be captured by URL.</Alert>
{error ? <Alert severity="error" action={<Button color="inherit" onClick={() => void runSearch(lastSearch.query, lastSearch.location)} disabled={loading}>Retry</Button>}>{error}</Alert> : null}
<Alert severity="info" variant="outlined" sx={{ color: "text.primary", "& .MuiAlert-icon": { color: "info.main" } }}>Official Norwegian vacancies from NAV. FINN, Indeed and LinkedIn jobs can still be captured by URL.</Alert>
{error ? <Alert severity="error" variant="outlined" sx={{ color: "text.primary" }} action={<Button color="inherit" onClick={() => void runSearch(lastSearch.query, lastSearch.location)} disabled={loading}>Retry</Button>}>{error}</Alert> : null}
{!loading && !hasSearched ? <Typography color="text.secondary">Search recent active vacancies by role, company, or municipality.</Typography> : null}
{!loading && hasSearched && !error && jobs.length === 0 ? <Alert severity="info">No active vacancies matched this search. Try a broader role, company, or municipality.</Alert> : null}
{!loading && hasSearched && !error && jobs.length === 0 ? <Alert severity="info" variant="outlined" sx={{ color: "text.primary" }}>No active vacancies matched this search. Try a broader role, company, or municipality.</Alert> : null}
{loading ? <Typography role="status" color="text.secondary">Searching NAV vacancies</Typography> : null}
{jobs.length > 0 ? (
<Box sx={{ display: "flex", gap: 2, alignItems: { xs: "stretch", sm: "center" }, justifyContent: "space-between", flexDirection: { xs: "column", sm: "row" } }}>