// 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/); }); });