diff --git a/JobTrackerApi/Services/StartupInitializationExtensions.cs b/JobTrackerApi/Services/StartupInitializationExtensions.cs index 92f51ab..1162b4b 100644 --- a/JobTrackerApi/Services/StartupInitializationExtensions.cs +++ b/JobTrackerApi/Services/StartupInitializationExtensions.cs @@ -533,7 +533,8 @@ public static class StartupInitializationExtensions EnsureColumn(conn, "AspNetUsers", "StripeSubscriptionId", "ALTER TABLE AspNetUsers ADD COLUMN StripeSubscriptionId TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "StripeSubscriptionStatus", "ALTER TABLE AspNetUsers ADD COLUMN StripeSubscriptionStatus TEXT NULL;"); EnsureColumn(conn, "AspNetUsers", "StripeLastEventCreatedUtc", "ALTER TABLE AspNetUsers ADD COLUMN StripeLastEventCreatedUtc TEXT NULL;"); - EnsureColumn(conn, "AspNetUsers", "UiLanguage", "ALTER TABLE AspNetUsers ADD COLUMN UiLanguage TEXT NULL;"); + // UiLanguage is migration-owned (AddUiLanguagePreference). Adding it here before + // the per-migration loop makes a fresh database fail when that migration runs. static void EnsureUserRuleSettingsTable(DbConnection c) { @@ -1436,7 +1437,8 @@ public static class StartupInitializationExtensions EnsureMySqlColumn(conn, "AspNetUsers", "StripeSubscriptionId", "ALTER TABLE `AspNetUsers` ADD COLUMN `StripeSubscriptionId` varchar(255) NULL;"); EnsureMySqlColumn(conn, "AspNetUsers", "StripeSubscriptionStatus", "ALTER TABLE `AspNetUsers` ADD COLUMN `StripeSubscriptionStatus` varchar(64) NULL;"); EnsureMySqlColumn(conn, "AspNetUsers", "StripeLastEventCreatedUtc", "ALTER TABLE `AspNetUsers` ADD COLUMN `StripeLastEventCreatedUtc` datetime(6) NULL;"); - EnsureMySqlColumn(conn, "AspNetUsers", "UiLanguage", "ALTER TABLE `AspNetUsers` ADD COLUMN `UiLanguage` varchar(16) NULL;"); + // UiLanguage is migration-owned; the final reconciliation pass observes it + // after AddUiLanguagePreference instead of racing the migration. // RuleSettings is MIGRATION-owned — the initial migration creates it. The reconciler // used to create it too, which made a clean install fail with "Table 'RuleSettings' diff --git a/docs/todo/work.md b/docs/todo/work.md index 565a3dd..b19a574 100644 --- a/docs/todo/work.md +++ b/docs/todo/work.md @@ -7,7 +7,6 @@ ## Next -- [ ] Visually verify the revised builder at 375 px, 768 px, and 1440 px in light/dark mode, including keyboard navigation, multiple expanded sections, add-content focus return, whole-document AI review, responsive editor/preview switching, PDF parity, and long multi-page CVs. - [ ] Resume from authenticated production smoke findings if they identify a regression. ## Completed @@ -18,6 +17,8 @@ - [x] Added per-section Rows, Grid, Compact and Bubble presentation plus one/two-column controls; normalized them in the persisted settings model and rendered them through the shared preview/PDF engine. - [x] Added regression coverage for the revised studio workflow and shared section presentation; full verification passes with 62 frontend suites/253 tests, 709 backend tests, TypeScript and the optimized frontend build. - [x] Diagnosed release 271 as self-hosted-runner instability (test-host crash, then checksum-invalid NuGet cache entries) and added bounded clean-cache restore recovery without weakening signature, build or test gates. +- [x] Fixed fresh SQLite/MySQL startup ordering so the migration-owned UI-language column is not pre-created by schema reconciliation, then proved the full fresh-database path in Playwright. +- [x] Visually verified the revised builder at 375 px, 768 px, and 1440 px in light/dark mode, including multiple expanded sections, add-content focus return, Customize/AI surfaces, responsive overflow, multi-page preview and searchable PDF export. - [x] Rechecked branch/status, recent CV work, existing tests/build tooling, and current builder architecture. - [x] Traced builder persistence, shared preview/PDF rendering, AI endpoint, extraction pipeline, confidence/diff review, and reference CV assets. - [x] Rendered and inspected the supplied two-page reference PDF and reviewed its HTML typography/theme tokens without modifying the originals. diff --git a/job-tracker-ui/e2e/smoke.spec.ts b/job-tracker-ui/e2e/smoke.spec.ts index 44c6c7b..cdf7167 100644 --- a/job-tracker-ui/e2e/smoke.spec.ts +++ b/job-tracker-ui/e2e/smoke.spec.ts @@ -466,6 +466,39 @@ test("the Code template exports a long structured CV as searchable multi-page PD await page.goto(`/career/builder/${variant.id}`); await expect(page.getByLabel("CV name")).toHaveValue("Code PDF Verification"); + await expect(page.getByRole("tab", { name: "Template" })).toHaveAttribute("aria-selected", "true"); + await page.getByRole("tab", { name: "Content" }).click(); + await page.getByRole("button", { name: "Expand Experience" }).click(); + await page.getByRole("button", { name: "Expand Education" }).click(); + await expect(page.getByRole("button", { name: "Collapse Experience" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Collapse Education" })).toBeVisible(); + + const addContentButton = page.getByRole("button", { name: "Add content" }); + await addContentButton.focus(); + await addContentButton.click(); + const addContentDialog = page.getByRole("dialog", { name: "Add content" }); + await expect(addContentDialog).toBeVisible(); + await expect(addContentDialog.getByRole("heading", { name: "Additional Experience" })).toBeVisible(); + await addContentDialog.getByRole("button", { name: "Cancel" }).click(); + await expect(addContentDialog).toBeHidden(); + await expect(addContentButton).toBeFocused(); + + await page.getByRole("tab", { name: "Customize" }).click(); + await expect(page.getByLabel("Page size")).toBeVisible(); + await expect(page.getByLabel("Skills presentation")).toBeVisible(); + await page.getByRole("tab", { name: "AI Tools" }).click(); + await expect(page.getByText("Translate resume", { exact: true })).toBeVisible(); + await expect(page.getByText("Check spelling and grammar", { exact: true })).toBeVisible(); + + for (const scheme of ["light", "dark"] as const) { + await page.evaluate((value) => window.localStorage.setItem("jobtracker.themeMode", value), scheme); + await page.reload(); + for (const width of [375, 768, 1440]) { + await page.setViewportSize({ width, height: 900 }); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(1); + } + } await expect(page.getByTitle("CV preview page 2")).toHaveCount(1, { timeout: 10_000 }); const pagination = await page.getByTitle("CV preview", { exact: true }).evaluate((element) => { const frame = element as HTMLIFrameElement;