// 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 = ' & "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); // Ask for the bytes as the app sends them: an edge that compresses // reports the compressed size, which the decoded body can't match. const res = await page.request.get('/', { headers: { 'Accept-Encoding': 'identity' } }); const body = await res.body(); const declared = res.headers()['content-length']; if (declared !== undefined && !res.headers()['content-encoding']) 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/); });