From 0bf6a54be060c9ed7ea3641014c79bcd5f1ea2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 23 Oct 2026 13:00:00 +0200 Subject: [PATCH] test(e2e): import lab results from a CSV on an account of its own The import journey ran on the shared account in both projects, so one project's duplicate verdict could rest on the other's import. It now runs on the labs journey's account, emptied first, checks that the dry run writes nothing and the import writes exactly the accepted row, and finds the result on the labs page before the second run. --- e2e/record-csv-import.spec.ts | 101 ++++++++++++++---- .../e2e-critical-no-api-stub-guard.test.ts | 2 +- 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/e2e/record-csv-import.spec.ts b/e2e/record-csv-import.spec.ts index 5ad83d964..d1168700a 100644 --- a/e2e/record-csv-import.spec.ts +++ b/e2e/record-csv-import.spec.ts @@ -1,29 +1,69 @@ +import type { Page } from "@playwright/test"; + import { expect, test } from "./setup/test"; -import { STORAGE_STATE_PATH } from "./setup/global-setup"; +import { + criticalAccount, + criticalStorageState, + resetCriticalAccount, +} from "./setup/critical-fixture"; /** - * Lab results from a spreadsheet (#1215), at phone width, against the real - * server: the mapping is suggested from the headers, the check reports a - * refused row by line and writes nothing, the import writes the rest, and a - * second import of the same file only finds duplicates. + * Lab results from a spreadsheet (#1215), against the real server: the + * mapping is suggested from the headers, the check reports a refused row by + * line and writes nothing, the import writes the rest, the imported row is on + * the labs page, and a second import of the same file only finds duplicates. + * + * The account is the labs journey's own, one per project + * (`critical-fixture.ts`), emptied of results first. Before v1.46 this ran on + * the shared account in both projects, so the "only duplicates" verdict of + * one project could be the other project's first import. */ + +const ANALYTE = "E2E Marker CSV"; + function csv(): string { const day = new Date(Date.now() - 30 * 86_400_000).toISOString().slice(0, 10); return [ "Analyte;Value;Unit;Date", - `E2E Marker CSV;5,4;mmol/L;${day}`, - `E2E Marker CSV;1.234;mmol/L;${day}`, + // A decimal comma, as a German lab export writes it. + `${ANALYTE};5,4;mmol/L;${day}`, + // Either 1.234 or 1234: refused as ambiguous rather than guessed. + `${ANALYTE};1.234;mmol/L;${day}`, ].join("\n"); } +/** The account's lab results, as the list route answers them. */ +async function storedResults( + page: Page, +): Promise> { + const res = await page.request.get("/api/labs"); + expect(res.status()).toBe(200); + const { data } = (await res.json()) as { + data: { + results: Array<{ analyte: string; value: number | null; unit: string }>; + }; + }; + return data.results.map(({ analyte, value, unit }) => ({ + analyte, + value, + unit, + })); +} + test.describe("record CSV import", () => { - test.use({ - storageState: STORAGE_STATE_PATH, - viewport: { width: 390, height: 844 }, + test.use({ storageState: criticalStorageState("labs") }); + + test.beforeEach(async ({}, testInfo) => { + // Fixture hygiene: what a previous run or a retry left on the account. + await resetCriticalAccount(criticalAccount("labs", testInfo), [ + "DELETE FROM lab_results WHERE user_id = $1", + ]); }); - test("checks, imports and then only finds duplicates", async ({ page }) => { + test("checks, imports, lists the result and then only finds duplicates @critical", async ({ + page, + }) => { await page.goto("/settings/export"); await page.locator('[data-slot="record-import-open-labs"]').click(); const sheet = page.locator('[data-slot="record-import-sheet"]'); @@ -48,19 +88,42 @@ test.describe("record CSV import", () => { await expect( page.locator('[data-slot="record-import-error-row"]'), ).toHaveCount(1); + // The check is a dry run. + expect(await storedResults(page)).toEqual([]); await page.locator('[data-slot="record-import-run"]').click(); await expect( page.locator('[data-slot="record-import-done"]'), ).toBeVisible(); + expect(await storedResults(page)).toEqual([ + { analyte: ANALYTE, value: 5.4, unit: "mmol/L" }, + ]); - // Second run of the same file. - await page.keyboard.press("Escape"); - await page.locator('[data-slot="record-import-open-labs"]').click(); - await page.locator('[data-slot="record-import-file"]').setInputFiles(file); - await page.locator('[data-slot="record-import-check"]').click(); - await expect( - page.locator('[data-slot="record-import-run"]'), - ).toBeDisabled(); + await test.step("the result is on the labs page", async () => { + await page.goto("/labs"); + const analyte = page + .locator('[data-slot="lab-list-analyte"]') + .filter({ hasText: ANALYTE }) + .filter({ visible: true }); + await expect(analyte).toHaveCount(1); + // The cell the name heads carries the latest reading under it. + await expect(analyte.locator("xpath=../..")).toContainText("5.4"); + }); + + await test.step("a second run of the same file", async () => { + await page.goto("/settings/export"); + await page.locator('[data-slot="record-import-open-labs"]').click(); + await page + .locator('[data-slot="record-import-file"]') + .setInputFiles(file); + await page.locator('[data-slot="record-import-check"]').click(); + await expect( + page.locator('[data-slot="record-import-checked"]'), + ).toBeVisible(); + await expect( + page.locator('[data-slot="record-import-run"]'), + ).toBeDisabled(); + expect(await storedResults(page)).toHaveLength(1); + }); }); }); diff --git a/src/__tests__/e2e-critical-no-api-stub-guard.test.ts b/src/__tests__/e2e-critical-no-api-stub-guard.test.ts index c571d4bbd..5a51fb4a1 100644 --- a/src/__tests__/e2e-critical-no-api-stub-guard.test.ts +++ b/src/__tests__/e2e-critical-no-api-stub-guard.test.ts @@ -49,7 +49,7 @@ const E2E = join(process.cwd(), "e2e"); * the set is the release gate, so a journey that loses its tag is a change * somebody should see here. */ -const CRITICAL_TEST_FLOOR = 12; +const CRITICAL_TEST_FLOOR = 13; /** A test whose title literal carries the tag, however the call is spelled. */ const CRITICAL_TEST =