From 5b5dcd0b80aa0713545ecf1fdab079fb9dec28ee Mon Sep 17 00:00:00 2001 From: cesnimda Date: Fri, 9 Oct 2026 18:42:45 +0200 Subject: [PATCH] test(settings): reproduce Rules draft loss across tabs --- .../p5-15-01/rules-draft-repair/README.md | 28 ++++++++++ docs/roadmap/todo.md | 2 + job-tracker-ui/e2e/rules-draft-repair.spec.ts | 55 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 docs/redesign/phase-5/p5-15/p5-15-01/rules-draft-repair/README.md create mode 100644 job-tracker-ui/e2e/rules-draft-repair.spec.ts diff --git a/docs/redesign/phase-5/p5-15/p5-15-01/rules-draft-repair/README.md b/docs/redesign/phase-5/p5-15/p5-15-01/rules-draft-repair/README.md new file mode 100644 index 0000000..0b9788e --- /dev/null +++ b/docs/redesign/phase-5/p5-15/p5-15-01/rules-draft-repair/README.md @@ -0,0 +1,28 @@ +# P5-15-01R-02 — Rules draft preservation + +Source: `c8e32238ef628f34c6ae8f64c6cb40ca5d8c68ac`, development `codex/ui-redesign-phase4`. Production `main` remains unchanged/NO-GO. This checkpoint owns SETTINGS-01 only; Analytics, P5-13, P5-15-02, Docker and B07 are excluded. + +## Demonstrated reproduction before implementation + +The new ordinary browser regression uses a disposable authenticated SQLite application. Seed the existing six-field Rules configuration (Applied follow-up14); edit it to5; switch Follow-ups→Notifications→Follow-ups. The entered5 becomes14, the persisted value remains14, and zero Rules PUTs occur. The run fails on the actual visible value (`expected5, received14`), not a mocked server or missing selector. Raw trace/report: ignored `.artifacts/p5-15-01R-02/browser/rules-red.*`. + +`SettingsView.TabPanel` returns null for inactive sections, unmounting `RulesSettingsCard`. The card's local `s` state is both its initially fetched data and edited draft. Returning creates a new card, performs a new GET and initializes from persisted14. This is destruction of editing-session state, not a backend write or deletion. + +## Bounded repair contract + +- Move the single Rules draft, persisted baseline and load/save/error state into the existing Settings parent. Inactive forms may still unmount; no keep-all-tabs-mounted workaround, browser storage, cross-session draft or automatic save. +- Preserve the existing per-account six-field atomic GET/PUT contract: Applied, Offer and Feedback are separate scenarios within one configuration, not three separately saved records. PUT returns204; the API clamps integer day values to1–365 and has no revision/ETag contract. +- Internal tab/phone-section changes preserve pending fields/errors. Save submits one validated snapshot; failure retains it; later edits during a pending save must stay dirty. Explicit discard returns to the latest successful baseline without a write. +- Clearly communicate the in-memory Settings-session boundary. Existing authentication/session disposal remains authoritative; no global draft store or second identity boundary. +- Keep S03 and W11 traceability, E07/E08/E09 reachability/accessibility/account protection. Full-group acceptance requires other retained sub-workflows; this repair alone cannot grant PASS. + +## Checkpoint plan / current status + +- [x] Trace and reproduce the exact native draft loss. +- [x] Add the ordinary failing browser regression before application changes. +- [ ] Repair and validate controlled draft/save/discard/error ownership. +- [ ] Complete native/browser, localisation/responsive and account evidence. +- [ ] Run the fresh full baseline and reconcile ordinary failures/counts. +- [ ] Update acceptance/defect evidence, commit/push each completed unit and verify clean remote equality. + +This is the **reproducer checkpoint**, not repair acceptance. No application change or fresh full-suite pass is claimed yet. The original audit and R-01 evidence remain unchanged. Production NO-GO and all deferred qualification obligations remain open. diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md index df84310..28cba57 100644 --- a/docs/roadmap/todo.md +++ b/docs/roadmap/todo.md @@ -4,6 +4,8 @@ Status reconciled: 2026-10-09. ## Current repository work +- **P5-15-01R-02 — bounded Rules draft repair in progress:** [record](../redesign/phase-5/p5-15/p5-15-01/rules-draft-repair/README.md). Native RED reproduces unsaved14→5→tab round trip→14, persisted14/zero writes. Preserve the Settings-session draft, explicit save/discard/error semantics and account boundary; full regression and acceptance evidence pending. Analytics repair/P5-15-02/P5-13/B07/production remain outside this checkpoint. + - **P5-15-00 — acceptance planning only:** [final acceptance programme](../redesign/phase-5/p5-15/README.md) reconciles every Phase 5 slice, all 57 original PRES groups and retained routes, incomplete axe/manual/device qualifications, CV/native/revision boundaries and A/B/C/D debt/release gates. No new application/test change or fresh suite result; inherited 1,023 backend / 459 frontend / 117 browser remains. P5-13 deferred, full Phase 5 incomplete and production NO-GO. Push this documentation checkpoint and stop for review; each subsequently completed logical P5-15 checkpoint must be separately committed/pushed. No production `main` push, B07 or automatic 01 execution. - **P5-14 — scoped responsive CV checkpoint, CONDITIONAL GO:** [implementation/acceptance record](../redesign/phase-5/p5-14/README.md) covers implemented P5-09–12, measured shell clearance, bounded preview chrome/canvas, pane-owned Ctrl+Home/End, nested drag/drop protection, save/conflict recovery and 320–1440px/landscape evidence. Final baseline: **1,023 backend / 459 frontend / 117 actual browser passes**, sidecar 49, real HTTP 2, security/configuration 9/6/1/3; revision 5/5, original fidelity 16/16 with 36 aligned pages and settings exports 8/8 with 24 aligned pages. Production remains NO-GO and no `main` deployment occurred. P5-13 stays deferred/unqualified; incomplete/human/device/zoom/font/production gates remain open. Stop before P5-15/B07; acceptance planning is the next recommendation only. diff --git a/job-tracker-ui/e2e/rules-draft-repair.spec.ts b/job-tracker-ui/e2e/rules-draft-repair.spec.ts new file mode 100644 index 0000000..871125d --- /dev/null +++ b/job-tracker-ui/e2e/rules-draft-repair.spec.ts @@ -0,0 +1,55 @@ +import { expect, test as base, type APIRequestContext } from '@playwright/test'; + +const apiUrl = 'http://localhost:5302/api'; +const password = 'E2ePassword123!'; + +async function login(api: APIRequestContext, email: string) { + expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); + const me = await (await api.get(`${apiUrl}/auth/me`)).json(); + const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; + return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string }; +} + +const test = base.extend<{ headers: Record }>({ + headers: async ({ playwright, page, context }, provide) => { + const admin = await playwright.request.newContext(); + try { + const adminHeaders = await login(admin, 'e2e@example.test'); + const email = `rules-repair-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); + await context.addInitScript(() => localStorage.setItem('uiLanguage', 'en')); + await context.route('**/*', route => { + const url = new URL(route.request().url()); + return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) + ? route.continue() : route.abort(); + }); + await provide(await login(page.request, email)); + } finally { await admin.dispose(); } + }, +}); + +test('[P5-15-01R-02 PRES-S03 W11] unsaved Rules draft survives an internal tab round trip without saving', async ({ page, headers }, info) => { + expect((await page.request.put(`${apiUrl}/rules`, { headers, data: { + id: 1, appliedFollowUpDays: 14, appliedGhostDays: 30, + offerFollowUpDays: 7, offerGhostDays: 14, feedbackFollowUpDays: 7, feedbackGhostDays: 14, + } })).status()).toBe(204); + let writes = 0; + page.on('request', request => { if (new URL(request.url()).pathname === '/api/rules' && request.method() === 'PUT') writes++; }); + await page.goto('/settings'); + await page.getByRole('tab', { name: 'Follow-ups', exact: true }).click(); + const field = page.getByRole('spinbutton', { name: 'Applied: follow-up days', exact: true }); + await expect(field).toHaveValue('14'); + await field.fill('5'); + await expect(field).toHaveValue('5'); + await page.getByRole('tab', { name: 'Notifications', exact: true }).click(); + await expect(page.getByLabel('Email reminders for follow-ups', { exact: true })).toBeVisible(); + await page.getByRole('tab', { name: 'Follow-ups', exact: true }).click(); + await expect(field).toBeVisible(); + const persisted = await (await page.request.get(`${apiUrl}/rules`)).json(); + await info.attach('rules-draft-tab-round-trip', { body: JSON.stringify({ + initial: 14, entered: 5, afterReturn: await field.inputValue(), persisted: persisted.appliedFollowUpDays, writes, + }), contentType: 'application/json' }); + expect(writes).toBe(0); + expect(persisted.appliedFollowUpDays).toBe(14); + await expect(field).toHaveValue('5'); +});