Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,8 @@ jobs:
cp -r dist/. serve_root/WorldScript-Studio/
pnpm exec http-server serve_root -p 3000 -s &
pnpm exec wait-on http://127.0.0.1:3000 --timeout 30000
pnpm run test:vrt
# TEMP-DIAGNOSTIC: --update-snapshots to regenerate baselines from real CI evidence; removed before this PR is ready for review.
pnpm run test:vrt -- --update-snapshots
Comment thread
qnbs marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- name: Upload VRT screenshots
if: always()
Expand Down
39 changes: 31 additions & 8 deletions tests/e2e/visual-regression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });

Expand All @@ -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 <body> 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 () => {
Expand All @@ -43,31 +55,42 @@ 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);
await settle(page);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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);
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);
await settle(page);
await expect(page.locator('body')).toBeVisible();
await expect(page).toHaveScreenshot('settings.png', opts);
});
});
Loading