test: TodoMVC behaviour suite, JS on and off, plus robustness and visual checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev a02281f1015bbc00ada06b567b868987f508196e parent 1a5d6e1
9 files changed, +502 −2
  • scripts/test-app.sh +13 −0
  • tests/behaviour/README.md +25 −0
  • tests/behaviour/actions.ts +74 −0
  • tests/behaviour/package.json +7 −2
  • tests/behaviour/playwright.config.ts +18 −0
  • tests/behaviour/robustness.spec.ts +87 −0
  • tests/behaviour/todomvc.spec.ts +256 −0
  • tests/behaviour/visual.spec.ts +22 −0
  • tests/behaviour/visual.spec.ts-snapshots/reference-js-on-linux.png binary
diff --git a/scripts/test-app.sh b/scripts/test-app.sh
new file mode 100755
index 0000000..53fb612
--- /dev/null
+++ b/scripts/test-app.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+# Runs the shared behaviour suite against one app, with JavaScript on and off.
+# Usage: scripts/test-app.sh <base-url> [results.json]
+set -euo pipefail
+[ $# -ge 1 ] || { echo "usage: $0 <base-url> [results.json]" >&2; exit 2; }
+here=$(cd "$(dirname "$0")/../tests/behaviour" && pwd)
+results=${2:-}
+[ -z "$results" ] || results=$(cd "$(dirname "$results")" && pwd)/$(basename "$results")
+cd "$here"
+if [ ! -d node_modules/@playwright/test ]; then
+ npm ci --cache "${TMPDIR:-/tmp}/npm-cache" --no-audit --no-fund >/dev/null
+fi
+BASE_URL="$1" RESULTS_FILE="$results" exec npx playwright test --project js-on --project js-off
diff --git a/tests/behaviour/README.md b/tests/behaviour/README.md
new file mode 100644
index 0000000..ceb799f
--- /dev/null
+++ b/tests/behaviour/README.md
@@ -0,0 +1,25 @@
+# 🤖 Behaviour suite
+
+The same tests for every app, run twice: once with JavaScript on
+(`js-on`) and once with it off (`js-off`).
+
+ scripts/test-app.sh <base-url> [results.json]
+
+- `todomvc.spec.ts`: the 29 tests of TodoMVC's own Cypress suite
+ (`tastejs/todomvc/cypress/e2e/spec.cy.js`), ported. Each test gets a
+ fresh browser context, so a fresh cookie and an empty list.
+ Persistence is checked by reloading, and routing uses real paths.
+ With JS off, "save edits on blur" and "cancel edits on escape" are
+ skipped, and editing goes through the edit link.
+- `robustness.spec.ts`: HTML in titles, UTF-8 and `Content-Length`,
+ blank and very long titles, per-visitor isolation and garbled cookies,
+ and cross-site and cross-list POSTs.
+- `visual.spec.ts`: with JS on, the page must match a screenshot of
+ `shared/reference.html` within 2% of pixels, with `footer.info`
+ masked. To regenerate the golden image: `npm run golden`.
+
+Against the reference fixture (2026-10-23): **67 passed, 3 skipped**
+(the two JS-only edit tests and the screenshot test with JS off). Each
+of these deliberate breaks was caught: a 500 on a foreign id, unescaped
+titles, `#/` filter links, untrimmed edits, and no-JS controls visible
+with JS on.
diff --git a/tests/behaviour/actions.ts b/tests/behaviour/actions.ts
new file mode 100644
index 0000000..bd47643
--- /dev/null
+++ b/tests/behaviour/actions.ts
@@ -0,0 +1,74 @@
+// Every user action, performed the way a person would with JavaScript on
+// (double-click, checkbox clicks) or off (the contract's submit controls
+// and edit link). Each one returns once the server has answered.
+import { expect, type Page, type Locator, type TestInfo } from '@playwright/test';
+
+export const jsOn = (ti: TestInfo) => ti.project.name === 'js-on';
+export const todos = (page: Page) => page.locator('.todo-list li');
+
+// Runs fn and waits for what it causes: a page load with JS off, the POST
+// response with JS on.
+async function settle(page: Page, ti: TestInfo, fn: () => Promise<void>) {
+ if (jsOn(ti)) {
+ await Promise.all([page.waitForResponse((r) => r.request().method() === 'POST'), fn()]);
+ } else {
+ await Promise.all([page.waitForEvent('load'), fn()]);
+ }
+}
+
+export async function addTodo(page: Page, ti: TestInfo, title: string) {
+ const input = page.locator('.new-todo');
+ await input.fill(title);
+ await settle(page, ti, () => input.press('Enter'));
+}
+
+export async function addDefaultTodos(page: Page, ti: TestInfo, titles: string[]) {
+ for (const t of titles) await addTodo(page, ti, t);
+ await expect(todos(page)).toHaveCount(titles.length);
+}
+
+export async function toggle(page: Page, ti: TestInfo, item: Locator) {
+ const control = jsOn(ti) ? item.locator('.toggle') : item.locator('[data-action="toggle"]');
+ await settle(page, ti, () => control.click());
+}
+
+export async function toggleAll(page: Page, ti: TestInfo) {
+ const control = jsOn(ti)
+ ? page.locator('label[for="toggle-all"]')
+ : page.locator('[data-action="toggle-all"]');
+ await settle(page, ti, () => control.click());
+}
+
+export async function destroy(page: Page, ti: TestInfo, item: Locator) {
+ await item.hover();
+ await settle(page, ti, () => item.locator('.destroy').click());
+}
+
+export async function clearCompleted(page: Page, ti: TestInfo) {
+ await settle(page, ti, () => page.locator('.clear-completed').click());
+}
+
+// Opens the item's editor: double-click with JS, the edit link without.
+// Returns the edit input.
+export async function startEditing(page: Page, ti: TestInfo, index: number): Promise<Locator> {
+ const item = todos(page).nth(index);
+ if (jsOn(ti)) {
+ await item.locator('label').dblclick();
+ } else {
+ await Promise.all([page.waitForEvent('load'), item.locator('a[data-action="edit"]').click()]);
+ }
+ const input = todos(page).nth(index).locator('.edit');
+ await expect(input).toBeVisible();
+ return input;
+}
+
+export async function submitEdit(page: Page, ti: TestInfo, input: Locator, title: string) {
+ await input.fill(title);
+ await settle(page, ti, () => input.press('Enter'));
+}
+
+export async function gotoFilter(page: Page, name: 'All' | 'Active' | 'Completed') {
+ const path = { All: '/', Active: '/active', Completed: '/completed' }[name];
+ await page.locator('.filters a', { hasText: name }).click();
+ await expect(page).toHaveURL((u) => u.pathname === path);
+}
diff --git a/tests/behaviour/package.json b/tests/behaviour/package.json
index 1281cd1..7503533 100644
--- a/tests/behaviour/package.json
+++ b/tests/behaviour/package.json
@@ -2,6 +2,11 @@
"name": "bakeoff-behaviour",
"private": true,
"type": "module",
- "scripts": { "test": "playwright test" },
- "devDependencies": { "@playwright/test": "1.62.1" }
+ "scripts": {
+ "test": "playwright test",
+ "golden": "GOLDEN=1 playwright test visual.spec.ts --project js-on --update-snapshots"
+ },
+ "devDependencies": {
+ "@playwright/test": "1.62.1"
+ }
}
diff --git a/tests/behaviour/playwright.config.ts b/tests/behaviour/playwright.config.ts
new file mode 100644
index 0000000..ce8ed44
--- /dev/null
+++ b/tests/behaviour/playwright.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from '@playwright/test';
+
+const reporter: any[] = [['list']];
+if (process.env.RESULTS_FILE) reporter.push(['json', { outputFile: process.env.RESULTS_FILE }]);
+
+export default defineConfig({
+ testDir: '.',
+ workers: 4,
+ retries: 0,
+ timeout: 30_000,
+ expect: { timeout: 5_000 },
+ reporter,
+ use: { baseURL: process.env.BASE_URL, browserName: 'chromium', viewport: { width: 1280, height: 720 } },
+ projects: [
+ { name: 'js-on', use: { javaScriptEnabled: true } },
+ { name: 'js-off', use: { javaScriptEnabled: false } },
+ ],
+});
diff --git a/tests/behaviour/robustness.spec.ts b/tests/behaviour/robustness.spec.ts
new file mode 100644
index 0000000..43c4949
--- /dev/null
+++ b/tests/behaviour/robustness.spec.ts
@@ -0,0 +1,87 @@
+// Inputs the TodoMVC suite never tries, which a real visitor will.
+import { test, expect } from '@playwright/test';
+import { todos, addTodo } from './actions';
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+});
+
+test('escapes HTML in titles', async ({ page }, ti) => {
+ const title = '<img src=x onerror=alert(1)> & "q"';
+ let dialogs = 0;
+ page.on('dialog', async (d) => { dialogs++; await d.dismiss(); });
+ await addTodo(page, ti, title);
+ await expect(todos(page).nth(0).locator('label')).toHaveText(title);
+ await page.reload();
+ await expect(todos(page).nth(0).locator('label')).toHaveText(title);
+ await expect(page.locator('.todo-list img')).toHaveCount(0);
+ expect(dialogs).toBe(0);
+});
+
+test('round-trips UTF-8 and sends correct Content-Length', async ({ page }, ti) => {
+ const title = 'naïve 🚀 日本語';
+ await addTodo(page, ti, title);
+ await page.reload();
+ expect(await todos(page).nth(0).locator('label').textContent()).toBe(title);
+ const res = await page.request.get('/');
+ const body = await res.body();
+ const declared = res.headers()['content-length'];
+ if (declared !== undefined) expect(Number(declared)).toBe(body.length);
+ expect(body.toString('utf8')).toContain(title);
+});
+
+test('rejects blank and accepts very long titles', async ({ page }, ti) => {
+ await addTodo(page, ti, ' ');
+ await expect(todos(page)).toHaveCount(0);
+ const long = 'x'.repeat(10_000);
+ await addTodo(page, ti, long);
+ await expect(todos(page)).toHaveCount(1);
+ expect((await todos(page).nth(0).locator('label').textContent())?.length).toBe(10_000);
+});
+
+test('isolates lists per visitor and survives a garbled cookie', async ({ page, browser, baseURL }, ti) => {
+ await addTodo(page, ti, 'a');
+ await expect(todos(page)).toHaveCount(1);
+ const js = ti.project.use.javaScriptEnabled;
+
+ const other = await browser.newContext({ baseURL, javaScriptEnabled: js });
+ const b = await other.newPage();
+ await b.goto('/');
+ await expect(todos(b)).toHaveCount(0);
+
+ const names = (await page.context().cookies()).map((c) => c.name);
+ expect(names.length).toBeGreaterThan(0);
+ const garbled = await browser.newContext({ baseURL, javaScriptEnabled: js });
+ await garbled.addCookies(names.map((name) => ({ name, value: '%%%', url: baseURL! })));
+ const g = await garbled.newPage();
+ const res = await g.goto('/');
+ expect(res?.status()).toBe(200);
+ await expect(todos(g)).toHaveCount(0);
+ await other.close();
+ await garbled.close();
+});
+
+test('rejects cross-site and cross-list posts', async ({ page, browser, baseURL }, ti) => {
+ const evil = await page.request.post('/todos', {
+ form: { title: 'evil', filter: 'all' },
+ headers: { Origin: 'https://evil.example' },
+ maxRedirects: 0,
+ });
+ expect(evil.status()).toBe(403);
+ await page.reload();
+ await expect(todos(page)).toHaveCount(0);
+
+ await addTodo(page, ti, 'mine');
+ const id = await todos(page).nth(0).getAttribute('data-id');
+ expect(id).toBeTruthy();
+
+ const other = await browser.newContext({ baseURL });
+ const b = await other.newPage();
+ await b.goto('/');
+ const res = await b.request.post(`/todos/${id}/toggle`, { form: { filter: 'all' }, maxRedirects: 0 });
+ expect(res.status()).toBe(404);
+ await other.close();
+
+ await page.reload();
+ await expect(todos(page).nth(0)).not.toHaveClass(/\bcompleted\b/);
+});
diff --git a/tests/behaviour/todomvc.spec.ts b/tests/behaviour/todomvc.spec.ts
new file mode 100644
index 0000000..28df196
--- /dev/null
+++ b/tests/behaviour/todomvc.spec.ts
@@ -0,0 +1,256 @@
+// Ported from tastejs/todomvc cypress/e2e/spec.cy.js. Each test runs in a
+// fresh browser context: a fresh cookie, so a fresh empty list. Persistence
+// is checked by reloading, not by reading localStorage, and routing uses
+// real paths. Only "save edits on blur" and "cancel edits on escape" are
+// skipped with JavaScript off.
+import { test, expect } from '@playwright/test';
+import {
+ jsOn, todos, addTodo, addDefaultTodos, toggle, toggleAll, clearCompleted,
+ startEditing, submitEdit, gotoFilter,
+} from './actions';
+
+const ONE = 'buy some cheese';
+const TWO = 'feed the cat';
+const THREE = 'book a doctors appointment';
+const DEFAULTS = [ONE, TWO, THREE];
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('h1')).toHaveText(/^todos$/i);
+ await expect(todos(page)).toHaveCount(0);
+});
+
+test.describe('When page is initially opened', () => {
+ test('should focus on the todo input field', async ({ page }) => {
+ await expect(page.locator('.new-todo')).toBeFocused();
+ });
+});
+
+test.describe('No Todos', () => {
+ test('starts with nothing', async ({ page }) => {
+ await expect(todos(page)).toHaveCount(0);
+ });
+
+ test('should hide #main and #footer', async ({ page }) => {
+ await expect(page.locator('.main')).toBeHidden();
+ await expect(page.locator('.footer')).toBeHidden();
+ });
+});
+
+test.describe('New Todo', () => {
+ test('should allow me to add todo items', async ({ page }, ti) => {
+ await addTodo(page, ti, ONE);
+ await expect(todos(page).nth(0).locator('label')).toHaveText(ONE);
+ await addTodo(page, ti, TWO);
+ await expect(todos(page).nth(1).locator('label')).toHaveText(TWO);
+ });
+
+ test('should clear text input field when an item is added', async ({ page }, ti) => {
+ await addTodo(page, ti, ONE);
+ await expect(page.locator('.new-todo')).toHaveValue('');
+ });
+
+ test('should append new items to the bottom of the list', async ({ page }, ti) => {
+ await addDefaultTodos(page, ti, DEFAULTS);
+ await expect(page.locator('.todo-count')).toContainText('3');
+ await expect(todos(page).locator('label')).toHaveText(DEFAULTS);
+ });
+
+ test('should trim text input', async ({ page }, ti) => {
+ await addTodo(page, ti, ` ${ONE} `);
+ await expect(todos(page)).toHaveCount(1);
+ expect(await todos(page).nth(0).locator('label').textContent()).toBe(ONE);
+ });
+
+ test('should show #main and #footer when items added', async ({ page }, ti) => {
+ await addTodo(page, ti, ONE);
+ await expect(page.locator('.main')).toBeVisible();
+ await expect(page.locator('.footer')).toBeVisible();
+ });
+});
+
+test.describe('Mark all as completed', () => {
+ test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS));
+
+ test('should allow me to mark all items as completed', async ({ page }, ti) => {
+ await toggleAll(page, ti);
+ for (let i = 0; i < 3; i++) await expect(todos(page).nth(i)).toHaveClass(/\bcompleted\b/);
+ });
+
+ test('should allow me to clear the complete state of all items', async ({ page }, ti) => {
+ await toggleAll(page, ti);
+ await toggleAll(page, ti);
+ for (let i = 0; i < 3; i++) await expect(todos(page).nth(i)).not.toHaveClass(/\bcompleted\b/);
+ });
+
+ test('complete all checkbox should update state when items are completed / cleared', async ({ page }, ti) => {
+ await toggleAll(page, ti);
+ await expect(page.locator('.toggle-all')).toBeChecked();
+ await toggle(page, ti, todos(page).nth(0));
+ await expect(page.locator('.toggle-all')).not.toBeChecked();
+ await toggle(page, ti, todos(page).nth(0));
+ await expect(page.locator('.toggle-all')).toBeChecked();
+ });
+});
+
+test.describe('Item', () => {
+ test('should allow me to mark items as complete', async ({ page }, ti) => {
+ await addDefaultTodos(page, ti, [ONE, TWO]);
+ await toggle(page, ti, todos(page).nth(0));
+ await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/);
+ await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/);
+ await toggle(page, ti, todos(page).nth(1));
+ await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/);
+ await expect(todos(page).nth(1)).toHaveClass(/\bcompleted\b/);
+ });
+
+ test('should allow me to un-mark items as complete', async ({ page }, ti) => {
+ await addDefaultTodos(page, ti, [ONE, TWO]);
+ await toggle(page, ti, todos(page).nth(0));
+ await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/);
+ await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/);
+ await toggle(page, ti, todos(page).nth(0));
+ await expect(todos(page).nth(0)).not.toHaveClass(/\bcompleted\b/);
+ await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/);
+ });
+
+ test('should allow me to edit an item', async ({ page }, ti) => {
+ await addDefaultTodos(page, ti, DEFAULTS);
+ const input = await startEditing(page, ti, 1);
+ await expect(input).toHaveValue(TWO);
+ await submitEdit(page, ti, input, 'buy some sausages');
+ await expect(todos(page).locator('label')).toHaveText([ONE, 'buy some sausages', THREE]);
+ });
+});
+
+test.describe('Editing', () => {
+ test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS));
+
+ test('should hide other controls when editing', async ({ page }, ti) => {
+ await startEditing(page, ti, 1);
+ await expect(todos(page).nth(1).locator('.toggle')).toBeHidden();
+ await expect(todos(page).nth(1).locator('label')).toBeHidden();
+ });
+
+ test('should save edits on blur', async ({ page }, ti) => {
+ test.skip(!jsOn(ti), 'blur saving needs JavaScript');
+ const input = await startEditing(page, ti, 1);
+ await input.fill('buy some sausages');
+ await Promise.all([page.waitForResponse((r) => r.request().method() === 'POST'), input.blur()]);
+ await expect(todos(page).locator('label')).toHaveText([ONE, 'buy some sausages', THREE]);
+ });
+
+ test('should trim entered text', async ({ page }, ti) => {
+ const input = await startEditing(page, ti, 1);
+ await submitEdit(page, ti, input, ' buy some sausages ');
+ await expect(todos(page).locator('label')).toHaveText([ONE, 'buy some sausages', THREE]);
+ expect(await todos(page).nth(1).locator('label').textContent()).toBe('buy some sausages');
+ });
+
+ test('should remove the item if an empty text string was entered', async ({ page }, ti) => {
+ const input = await startEditing(page, ti, 1);
+ await submitEdit(page, ti, input, '');
+ await expect(todos(page)).toHaveCount(2);
+ });
+
+ test('should cancel edits on escape', async ({ page }, ti) => {
+ test.skip(!jsOn(ti), 'escape to cancel needs JavaScript');
+ const input = await startEditing(page, ti, 1);
+ await input.fill('foo');
+ await input.press('Escape');
+ await expect(todos(page).locator('label')).toHaveText(DEFAULTS);
+ });
+});
+
+test.describe('Counter', () => {
+ test('should display the current number of todo items', async ({ page }, ti) => {
+ await addTodo(page, ti, ONE);
+ await expect(page.locator('.todo-count')).toHaveText('1 item left');
+ await addTodo(page, ti, TWO);
+ await expect(page.locator('.todo-count')).toHaveText('2 items left');
+ });
+});
+
+test.describe('Clear completed button', () => {
+ test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS));
+
+ test('should display the correct text', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(0));
+ await expect(page.locator('.clear-completed')).toHaveText('Clear completed');
+ });
+
+ test('should remove completed items when clicked', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(1));
+ await clearCompleted(page, ti);
+ await expect(todos(page).locator('label')).toHaveText([ONE, THREE]);
+ });
+
+ test('should be hidden when there are no items that are completed', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(1));
+ await expect(page.locator('.clear-completed')).toBeVisible();
+ await clearCompleted(page, ti);
+ await expect(page.locator('.clear-completed')).toBeHidden();
+ });
+});
+
+test.describe('Persistence', () => {
+ test('should persist its data', async ({ page }, ti) => {
+ await addDefaultTodos(page, ti, [ONE, TWO]);
+ await toggle(page, ti, todos(page).nth(0));
+ const check = async () => {
+ await expect(todos(page).nth(0)).toContainText(ONE);
+ await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/);
+ await expect(todos(page).nth(1)).toContainText(TWO);
+ await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/);
+ };
+ await check();
+ await page.reload();
+ await check();
+ });
+});
+
+test.describe('Routing', () => {
+ test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS));
+
+ test('should allow me to display active items', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(1));
+ await gotoFilter(page, 'Active');
+ await expect(todos(page).locator('label')).toHaveText([ONE, THREE]);
+ });
+
+ test('should respect the back button', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(1));
+ await gotoFilter(page, 'All');
+ await expect(todos(page)).toHaveCount(3);
+ await gotoFilter(page, 'Active');
+ await gotoFilter(page, 'Completed');
+ await expect(todos(page)).toHaveCount(1);
+ await page.goBack();
+ await expect(todos(page)).toHaveCount(2);
+ await page.goBack();
+ await expect(todos(page)).toHaveCount(3);
+ });
+
+ test('should allow me to display completed items', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(1));
+ await gotoFilter(page, 'Completed');
+ await expect(todos(page)).toHaveCount(1);
+ });
+
+ test('should allow me to display all items', async ({ page }, ti) => {
+ await toggle(page, ti, todos(page).nth(1));
+ await gotoFilter(page, 'Active');
+ await gotoFilter(page, 'Completed');
+ await gotoFilter(page, 'All');
+ await expect(todos(page)).toHaveCount(3);
+ });
+
+ test('should highlight the currently applied filter', async ({ page }) => {
+ const link = (name: string) => page.locator('.filters a', { hasText: name });
+ await expect(link('All')).toHaveClass(/\bselected\b/);
+ await gotoFilter(page, 'Active');
+ await expect(link('Active')).toHaveClass(/\bselected\b/);
+ await gotoFilter(page, 'Completed');
+ await expect(link('Completed')).toHaveClass(/\bselected\b/);
+ });
+});
diff --git a/tests/behaviour/visual.spec.ts b/tests/behaviour/visual.spec.ts
new file mode 100644
index 0000000..adbb346
--- /dev/null
+++ b/tests/behaviour/visual.spec.ts
@@ -0,0 +1,22 @@
+// With JavaScript on, an app must look like TodoMVC's own template.
+// The golden image is a screenshot of shared/reference.html, made with
+// `npm run golden`. Only footer.info (it names the language) is masked.
+import { test, expect } from '@playwright/test';
+import path from 'node:path';
+import { addDefaultTodos, toggle, todos } from './actions';
+
+const shot = (page) => ({ mask: [page.locator('footer.info')], maxDiffPixelRatio: 0.02 });
+
+test('matches the TodoMVC reference', async ({ page }, ti) => {
+ test.skip(ti.project.name !== 'js-on', 'compared with JavaScript on');
+ if (process.env.GOLDEN) {
+ await page.goto('file://' + path.resolve(import.meta.dirname, '../../shared/reference.html'));
+ } else {
+ await page.goto('/');
+ await addDefaultTodos(page, ti, ['Buy milk', 'Walk the dog', 'Write the report']);
+ await toggle(page, ti, todos(page).nth(0));
+ await page.locator('.new-todo').focus();
+ await page.mouse.move(0, 0);
+ }
+ await expect(page).toHaveScreenshot('reference.png', shot(page));
+});
diff --git a/tests/behaviour/visual.spec.ts-snapshots/reference-js-on-linux.png b/tests/behaviour/visual.spec.ts-snapshots/reference-js-on-linux.png
new file mode 100644
index 0000000..6fca9bb
Binary files /dev/null and b/tests/behaviour/visual.spec.ts-snapshots/reference-js-on-linux.png differ