carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1// Inputs the TodoMVC suite never tries, which a real visitor will.
2import { test, expect } from '@playwright/test';
3import { todos, addTodo } from './actions';
4
5test.beforeEach(async ({ page }) => {
6 await page.goto('/');
7});
8
9test('escapes HTML in titles', async ({ page }, ti) => {
10 const title = '<img src=x onerror=alert(1)> & "q"';
11 let dialogs = 0;
12 page.on('dialog', async (d) => { dialogs++; await d.dismiss(); });
13 await addTodo(page, ti, title);
14 await expect(todos(page).nth(0).locator('label')).toHaveText(title);
15 await page.reload();
16 await expect(todos(page).nth(0).locator('label')).toHaveText(title);
17 await expect(page.locator('.todo-list img')).toHaveCount(0);
18 expect(dialogs).toBe(0);
19});
20
21test('round-trips UTF-8 and sends correct Content-Length', async ({ page }, ti) => {
22 const title = 'naïve 🚀 日本語';
23 await addTodo(page, ti, title);
24 await page.reload();
25 expect(await todos(page).nth(0).locator('label').textContent()).toBe(title);
26 // Ask for the bytes as the app sends them: an edge that compresses
27 // reports the compressed size, which the decoded body can't match.
28 const res = await page.request.get('/', { headers: { 'Accept-Encoding': 'identity' } });
29 const body = await res.body();
30 const declared = res.headers()['content-length'];
31 if (declared !== undefined && !res.headers()['content-encoding']) expect(Number(declared)).toBe(body.length);
32 expect(body.toString('utf8')).toContain(title);
33});
34
35test('rejects blank and accepts very long titles', async ({ page }, ti) => {
36 await addTodo(page, ti, ' ');
37 await expect(todos(page)).toHaveCount(0);
38 const long = 'x'.repeat(10_000);
39 await addTodo(page, ti, long);
40 await expect(todos(page)).toHaveCount(1);
41 expect((await todos(page).nth(0).locator('label').textContent())?.length).toBe(10_000);
42});
43
44test('isolates lists per visitor and survives a garbled cookie', async ({ page, browser, baseURL }, ti) => {
45 await addTodo(page, ti, 'a');
46 await expect(todos(page)).toHaveCount(1);
47 const js = ti.project.use.javaScriptEnabled;
48
49 const other = await browser.newContext({ baseURL, javaScriptEnabled: js });
50 const b = await other.newPage();
51 await b.goto('/');
52 await expect(todos(b)).toHaveCount(0);
53
54 const names = (await page.context().cookies()).map((c) => c.name);
55 expect(names.length).toBeGreaterThan(0);
56 const garbled = await browser.newContext({ baseURL, javaScriptEnabled: js });
57 await garbled.addCookies(names.map((name) => ({ name, value: '%%%', url: baseURL! })));
58 const g = await garbled.newPage();
59 const res = await g.goto('/');
60 expect(res?.status()).toBe(200);
61 await expect(todos(g)).toHaveCount(0);
62 await other.close();
63 await garbled.close();
64});
65
66test('rejects cross-site and cross-list posts', async ({ page, browser, baseURL }, ti) => {
67 const evil = await page.request.post('/todos', {
68 form: { title: 'evil', filter: 'all' },
69 headers: { Origin: 'https://evil.example' },
70 maxRedirects: 0,
71 });
72 expect(evil.status()).toBe(403);
73 await page.reload();
74 await expect(todos(page)).toHaveCount(0);
75
76 await addTodo(page, ti, 'mine');
77 const id = await todos(page).nth(0).getAttribute('data-id');
78 expect(id).toBeTruthy();
79
80 const other = await browser.newContext({ baseURL });
81 const b = await other.newPage();
82 await b.goto('/');
83 const res = await b.request.post(`/todos/${id}/toggle`, { form: { filter: 'all' }, maxRedirects: 0 });
84 expect(res.status()).toBe(404);
85 await other.close();
86
87 await page.reload();
88 await expect(todos(page).nth(0)).not.toHaveClass(/\bcompleted\b/);
89});
90