diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b245580..41b1066f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -872,10 +872,10 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: vrt-screenshots - # QNBS-v3: upload both baselines and diff outputs so PR reviewers can compare visually. + # QNBS-v3: upload both baselines and diff outputs so PR reviewers can compare visually — playwright.config.ts sets no outputDir, so Playwright's default (repo-root test-results/, not tests/e2e/test-results/) is where actual/diff failure images actually land. path: | tests/e2e/visual-regression.spec.ts-snapshots/ - tests/e2e/test-results/ + test-results/ retention-days: 7 # QNBS-v3: same playwright.config.ts reporter as the required e2e job, produced independently on this runner — previously never uploaded. diff --git a/tests/e2e/visual-regression.spec.ts b/tests/e2e/visual-regression.spec.ts index e37e63ec..2ff3619c 100644 --- a/tests/e2e/visual-regression.spec.ts +++ b/tests/e2e/visual-regression.spec.ts @@ -4,10 +4,15 @@ * Local shortcut: pnpm run test:vrt */ import { expect, test } from '@playwright/test'; +import { clickNavItem, ensureBlankProject, selectEnglish, waitForSpaReady } from './helpers'; // QNBS-v3: Skip in the main E2E job (PLAYWRIGHT_SKIP_VRT=true) — handled by the dedicated VRT job // that serves the production dist build. Running against the dev server here would compare against // production-build baselines and always mismatch on unrelated HMR/port differences. + +// QNBS-v3: fully-qualified, VRT-local entry, deliberately bypassing the shared baseURL — every other e2e spec's page.goto('/') targets Vite's dev server (which serves at root regardless), while VRT's static server genuinely serves the app under this GH-Pages-style subpath, so scoping this here avoids any risk to the rest of the suite's navigation. +const APP_ENTRY = 'http://127.0.0.1:3000/WorldScript-Studio/'; + test.describe('Visual regression', () => { test.use({ viewport: { width: 1280, height: 720 } }); @@ -23,6 +28,13 @@ test.describe('Visual regression', () => { ); }); + // QNBS-v3: proves the served page is actually WorldScript Studio before any screenshot — a static-file-server fallback (e.g. an http-server directory listing) has a visible too, so a body-visibility check alone can't tell the two apart; waitForSpaReady's own landmarks (#sidebar/nav-mobile/welcome-portal) already can't exist on a listing page, and the title/text checks below are an independent second signal. + async function assertRealAppLoaded(page: import('@playwright/test').Page) { + await waitForSpaReady(page); + await expect(page).not.toHaveTitle(/Index of/i); + await expect(page.locator('body')).not.toContainText('Index of /'); + } + async function settle(page: import('@playwright/test').Page) { await page.waitForLoadState('load'); await page.evaluate(async () => { @@ -43,31 +55,50 @@ test.describe('Visual regression', () => { timeout: 30_000, }; + // QNBS-v3: APP_ENTRY, not '/' — an absolute-path goto discards the shared baseURL's entire path per the URL spec, landing on the static server's bare root instead of the app (this was the root cause of every VRT baseline actually being an http-server directory listing). test('home / dashboard loads', async ({ page }) => { - await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.goto(APP_ENTRY, { waitUntil: 'domcontentloaded' }); + await assertRealAppLoaded(page); await settle(page); - await expect(page.locator('body')).toBeVisible(); await expect(page).toHaveScreenshot('home.png', opts); }); + // QNBS-v3: '#view=writer' was never a real route (no code in the app parses it) — reuses the same clickNavItem/ensureBlankProject navigation every other e2e spec already relies on, instead of a URL fragment the router never read. 'manuscript' and 'writer' are distinct views (types.ts's View union); the canonical nav match for the actual Writer/editor view used across the rest of this suite (writer.spec.ts et al.) is /AI Writing Studio|Writer/i, not /Manuscript/i. test('writer view loads', async ({ page }) => { - await page.goto('/#view=writer', { waitUntil: 'domcontentloaded' }); + await page.goto(APP_ENTRY, { waitUntil: 'domcontentloaded' }); + await assertRealAppLoaded(page); + await selectEnglish(page); + await ensureBlankProject(page); + await clickNavItem(page, /AI Writing Studio|Writer/i); + // QNBS-v3: proves the Writer view specifically loaded, not just "some" app view — a screenshot diff alone can silently under-detect a wrong destination when both the wrong and right pages happen to be sparse enough to fall under the pixel-diff threshold. + await expect(page.getByTestId('writer-studio-editor').first()).toBeVisible(); await settle(page); - await expect(page.locator('body')).toBeVisible(); await expect(page).toHaveScreenshot('writer.png', opts); }); test('characters view loads', async ({ page }) => { - await page.goto('/#view=characters', { waitUntil: 'domcontentloaded' }); + await page.goto(APP_ENTRY, { waitUntil: 'domcontentloaded' }); + await assertRealAppLoaded(page); + await selectEnglish(page); + await ensureBlankProject(page); + await clickNavItem(page, /Characters/i); + // QNBS-v3: proves the Characters view specifically loaded — see the writer test's identical rationale above. + await expect(page.getByRole('button', { name: /Add Manually/i })).toBeVisible(); await settle(page); - await expect(page.locator('body')).toBeVisible(); await expect(page).toHaveScreenshot('characters.png', opts); }); test('settings view loads', async ({ page }) => { - await page.goto('/#view=settings', { waitUntil: 'domcontentloaded' }); + await page.goto(APP_ENTRY, { waitUntil: 'domcontentloaded' }); + await assertRealAppLoaded(page); + await selectEnglish(page); + await ensureBlankProject(page); + await clickNavItem(page, /Settings/i); + // QNBS-v3: proves the Settings view specifically loaded — see the writer test's identical rationale above. + await expect( + page.getByRole('heading', { name: /Settings|Einstellungen/i }).first(), + ).toBeVisible(); await settle(page); - await expect(page.locator('body')).toBeVisible(); await expect(page).toHaveScreenshot('settings.png', opts); }); }); diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/characters-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/characters-chromium.png index 3fd10da7..7b155e71 100644 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/characters-chromium.png and b/tests/e2e/visual-regression.spec.ts-snapshots/characters-chromium.png differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/home-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/home-chromium.png index 3fd10da7..04172855 100644 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/home-chromium.png and b/tests/e2e/visual-regression.spec.ts-snapshots/home-chromium.png differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/settings-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/settings-chromium.png index 3fd10da7..1b569ade 100644 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/settings-chromium.png and b/tests/e2e/visual-regression.spec.ts-snapshots/settings-chromium.png differ diff --git a/tests/e2e/visual-regression.spec.ts-snapshots/writer-chromium.png b/tests/e2e/visual-regression.spec.ts-snapshots/writer-chromium.png index 3fd10da7..bc7aa9c5 100644 Binary files a/tests/e2e/visual-regression.spec.ts-snapshots/writer-chromium.png and b/tests/e2e/visual-regression.spec.ts-snapshots/writer-chromium.png differ