| 1 | // Ported from tastejs/todomvc cypress/e2e/spec.cy.js. Each test runs in a |
| 2 | // fresh browser context: a fresh cookie, so a fresh empty list. Persistence |
| 3 | // is checked by reloading, not by reading localStorage, and routing uses |
| 4 | // real paths. Only "save edits on blur" and "cancel edits on escape" are |
| 5 | // skipped with JavaScript off. |
| 6 | import { test, expect } from '@playwright/test'; |
| 7 | import { |
| 8 | jsOn, todos, addTodo, addDefaultTodos, toggle, toggleAll, clearCompleted, |
| 9 | startEditing, submitEdit, gotoFilter, |
| 10 | } from './actions'; |
| 11 | |
| 12 | const ONE = 'buy some cheese'; |
| 13 | const TWO = 'feed the cat'; |
| 14 | const THREE = 'book a doctors appointment'; |
| 15 | const DEFAULTS = [ONE, TWO, THREE]; |
| 16 | |
| 17 | test.beforeEach(async ({ page }) => { |
| 18 | await page.goto('/'); |
| 19 | await expect(page.locator('h1')).toHaveText(/^todos$/i); |
| 20 | await expect(todos(page)).toHaveCount(0); |
| 21 | }); |
| 22 | |
| 23 | test.describe('When page is initially opened', () => { |
| 24 | test('should focus on the todo input field', async ({ page }) => { |
| 25 | await expect(page.locator('.new-todo')).toBeFocused(); |
| 26 | }); |
| 27 | }); |
| 28 | |
| 29 | test.describe('No Todos', () => { |
| 30 | test('starts with nothing', async ({ page }) => { |
| 31 | await expect(todos(page)).toHaveCount(0); |
| 32 | }); |
| 33 | |
| 34 | test('should hide #main and #footer', async ({ page }) => { |
| 35 | await expect(page.locator('.main')).toBeHidden(); |
| 36 | await expect(page.locator('.footer')).toBeHidden(); |
| 37 | }); |
| 38 | }); |
| 39 | |
| 40 | test.describe('New Todo', () => { |
| 41 | test('should allow me to add todo items', async ({ page }, ti) => { |
| 42 | await addTodo(page, ti, ONE); |
| 43 | await expect(todos(page).nth(0).locator('label')).toHaveText(ONE); |
| 44 | await addTodo(page, ti, TWO); |
| 45 | await expect(todos(page).nth(1).locator('label')).toHaveText(TWO); |
| 46 | }); |
| 47 | |
| 48 | test('should clear text input field when an item is added', async ({ page }, ti) => { |
| 49 | await addTodo(page, ti, ONE); |
| 50 | await expect(page.locator('.new-todo')).toHaveValue(''); |
| 51 | }); |
| 52 | |
| 53 | test('should append new items to the bottom of the list', async ({ page }, ti) => { |
| 54 | await addDefaultTodos(page, ti, DEFAULTS); |
| 55 | await expect(page.locator('.todo-count')).toContainText('3'); |
| 56 | await expect(todos(page).locator('label')).toHaveText(DEFAULTS); |
| 57 | }); |
| 58 | |
| 59 | test('should trim text input', async ({ page }, ti) => { |
| 60 | await addTodo(page, ti, ` ${ONE} `); |
| 61 | await expect(todos(page)).toHaveCount(1); |
| 62 | expect(await todos(page).nth(0).locator('label').textContent()).toBe(ONE); |
| 63 | }); |
| 64 | |
| 65 | test('should show #main and #footer when items added', async ({ page }, ti) => { |
| 66 | await addTodo(page, ti, ONE); |
| 67 | await expect(page.locator('.main')).toBeVisible(); |
| 68 | await expect(page.locator('.footer')).toBeVisible(); |
| 69 | }); |
| 70 | }); |
| 71 | |
| 72 | test.describe('Mark all as completed', () => { |
| 73 | test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS)); |
| 74 | |
| 75 | test('should allow me to mark all items as completed', async ({ page }, ti) => { |
| 76 | await toggleAll(page, ti); |
| 77 | for (let i = 0; i < 3; i++) await expect(todos(page).nth(i)).toHaveClass(/\bcompleted\b/); |
| 78 | }); |
| 79 | |
| 80 | test('should allow me to clear the complete state of all items', async ({ page }, ti) => { |
| 81 | await toggleAll(page, ti); |
| 82 | await toggleAll(page, ti); |
| 83 | for (let i = 0; i < 3; i++) await expect(todos(page).nth(i)).not.toHaveClass(/\bcompleted\b/); |
| 84 | }); |
| 85 | |
| 86 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }, ti) => { |
| 87 | await toggleAll(page, ti); |
| 88 | await expect(page.locator('.toggle-all')).toBeChecked(); |
| 89 | await toggle(page, ti, todos(page).nth(0)); |
| 90 | await expect(page.locator('.toggle-all')).not.toBeChecked(); |
| 91 | await toggle(page, ti, todos(page).nth(0)); |
| 92 | await expect(page.locator('.toggle-all')).toBeChecked(); |
| 93 | }); |
| 94 | }); |
| 95 | |
| 96 | test.describe('Item', () => { |
| 97 | test('should allow me to mark items as complete', async ({ page }, ti) => { |
| 98 | await addDefaultTodos(page, ti, [ONE, TWO]); |
| 99 | await toggle(page, ti, todos(page).nth(0)); |
| 100 | await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/); |
| 101 | await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/); |
| 102 | await toggle(page, ti, todos(page).nth(1)); |
| 103 | await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/); |
| 104 | await expect(todos(page).nth(1)).toHaveClass(/\bcompleted\b/); |
| 105 | }); |
| 106 | |
| 107 | test('should allow me to un-mark items as complete', async ({ page }, ti) => { |
| 108 | await addDefaultTodos(page, ti, [ONE, TWO]); |
| 109 | await toggle(page, ti, todos(page).nth(0)); |
| 110 | await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/); |
| 111 | await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/); |
| 112 | await toggle(page, ti, todos(page).nth(0)); |
| 113 | await expect(todos(page).nth(0)).not.toHaveClass(/\bcompleted\b/); |
| 114 | await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/); |
| 115 | }); |
| 116 | |
| 117 | test('should allow me to edit an item', async ({ page }, ti) => { |
| 118 | await addDefaultTodos(page, ti, DEFAULTS); |
| 119 | const input = await startEditing(page, ti, 1); |
| 120 | await expect(input).toHaveValue(TWO); |
| 121 | await submitEdit(page, ti, input, 'buy some sausages'); |
| 122 | await expect(todos(page).locator('label')).toHaveText([ONE, 'buy some sausages', THREE]); |
| 123 | }); |
| 124 | }); |
| 125 | |
| 126 | test.describe('Editing', () => { |
| 127 | test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS)); |
| 128 | |
| 129 | test('should hide other controls when editing', async ({ page }, ti) => { |
| 130 | await startEditing(page, ti, 1); |
| 131 | await expect(todos(page).nth(1).locator('.toggle')).toBeHidden(); |
| 132 | await expect(todos(page).nth(1).locator('label')).toBeHidden(); |
| 133 | }); |
| 134 | |
| 135 | test('should save edits on blur', async ({ page }, ti) => { |
| 136 | test.skip(!jsOn(ti), 'blur saving needs JavaScript'); |
| 137 | const input = await startEditing(page, ti, 1); |
| 138 | await input.fill('buy some sausages'); |
| 139 | await Promise.all([page.waitForResponse((r) => r.request().method() === 'POST'), input.blur()]); |
| 140 | await expect(todos(page).locator('label')).toHaveText([ONE, 'buy some sausages', THREE]); |
| 141 | }); |
| 142 | |
| 143 | test('should trim entered text', async ({ page }, ti) => { |
| 144 | const input = await startEditing(page, ti, 1); |
| 145 | await submitEdit(page, ti, input, ' buy some sausages '); |
| 146 | await expect(todos(page).locator('label')).toHaveText([ONE, 'buy some sausages', THREE]); |
| 147 | expect(await todos(page).nth(1).locator('label').textContent()).toBe('buy some sausages'); |
| 148 | }); |
| 149 | |
| 150 | test('should remove the item if an empty text string was entered', async ({ page }, ti) => { |
| 151 | const input = await startEditing(page, ti, 1); |
| 152 | await submitEdit(page, ti, input, ''); |
| 153 | await expect(todos(page)).toHaveCount(2); |
| 154 | }); |
| 155 | |
| 156 | test('should cancel edits on escape', async ({ page }, ti) => { |
| 157 | test.skip(!jsOn(ti), 'escape to cancel needs JavaScript'); |
| 158 | const input = await startEditing(page, ti, 1); |
| 159 | await input.fill('foo'); |
| 160 | await input.press('Escape'); |
| 161 | await expect(todos(page).locator('label')).toHaveText(DEFAULTS); |
| 162 | }); |
| 163 | }); |
| 164 | |
| 165 | test.describe('Counter', () => { |
| 166 | test('should display the current number of todo items', async ({ page }, ti) => { |
| 167 | await addTodo(page, ti, ONE); |
| 168 | await expect(page.locator('.todo-count')).toHaveText('1 item left'); |
| 169 | await addTodo(page, ti, TWO); |
| 170 | await expect(page.locator('.todo-count')).toHaveText('2 items left'); |
| 171 | }); |
| 172 | }); |
| 173 | |
| 174 | test.describe('Clear completed button', () => { |
| 175 | test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS)); |
| 176 | |
| 177 | test('should display the correct text', async ({ page }, ti) => { |
| 178 | await toggle(page, ti, todos(page).nth(0)); |
| 179 | await expect(page.locator('.clear-completed')).toHaveText('Clear completed'); |
| 180 | }); |
| 181 | |
| 182 | test('should remove completed items when clicked', async ({ page }, ti) => { |
| 183 | await toggle(page, ti, todos(page).nth(1)); |
| 184 | await clearCompleted(page, ti); |
| 185 | await expect(todos(page).locator('label')).toHaveText([ONE, THREE]); |
| 186 | }); |
| 187 | |
| 188 | test('should be hidden when there are no items that are completed', async ({ page }, ti) => { |
| 189 | await toggle(page, ti, todos(page).nth(1)); |
| 190 | await expect(page.locator('.clear-completed')).toBeVisible(); |
| 191 | await clearCompleted(page, ti); |
| 192 | await expect(page.locator('.clear-completed')).toBeHidden(); |
| 193 | }); |
| 194 | }); |
| 195 | |
| 196 | test.describe('Persistence', () => { |
| 197 | test('should persist its data', async ({ page }, ti) => { |
| 198 | await addDefaultTodos(page, ti, [ONE, TWO]); |
| 199 | await toggle(page, ti, todos(page).nth(0)); |
| 200 | const check = async () => { |
| 201 | await expect(todos(page).nth(0)).toContainText(ONE); |
| 202 | await expect(todos(page).nth(0)).toHaveClass(/\bcompleted\b/); |
| 203 | await expect(todos(page).nth(1)).toContainText(TWO); |
| 204 | await expect(todos(page).nth(1)).not.toHaveClass(/\bcompleted\b/); |
| 205 | }; |
| 206 | await check(); |
| 207 | await page.reload(); |
| 208 | await check(); |
| 209 | }); |
| 210 | }); |
| 211 | |
| 212 | test.describe('Routing', () => { |
| 213 | test.beforeEach(async ({ page }, ti) => addDefaultTodos(page, ti, DEFAULTS)); |
| 214 | |
| 215 | test('should allow me to display active items', async ({ page }, ti) => { |
| 216 | await toggle(page, ti, todos(page).nth(1)); |
| 217 | await gotoFilter(page, 'Active'); |
| 218 | await expect(todos(page).locator('label')).toHaveText([ONE, THREE]); |
| 219 | }); |
| 220 | |
| 221 | test('should respect the back button', async ({ page }, ti) => { |
| 222 | await toggle(page, ti, todos(page).nth(1)); |
| 223 | await gotoFilter(page, 'All'); |
| 224 | await expect(todos(page)).toHaveCount(3); |
| 225 | await gotoFilter(page, 'Active'); |
| 226 | await gotoFilter(page, 'Completed'); |
| 227 | await expect(todos(page)).toHaveCount(1); |
| 228 | await page.goBack(); |
| 229 | await expect(todos(page)).toHaveCount(2); |
| 230 | await page.goBack(); |
| 231 | await expect(todos(page)).toHaveCount(3); |
| 232 | }); |
| 233 | |
| 234 | test('should allow me to display completed items', async ({ page }, ti) => { |
| 235 | await toggle(page, ti, todos(page).nth(1)); |
| 236 | await gotoFilter(page, 'Completed'); |
| 237 | await expect(todos(page)).toHaveCount(1); |
| 238 | }); |
| 239 | |
| 240 | test('should allow me to display all items', async ({ page }, ti) => { |
| 241 | await toggle(page, ti, todos(page).nth(1)); |
| 242 | await gotoFilter(page, 'Active'); |
| 243 | await gotoFilter(page, 'Completed'); |
| 244 | await gotoFilter(page, 'All'); |
| 245 | await expect(todos(page)).toHaveCount(3); |
| 246 | }); |
| 247 | |
| 248 | test('should highlight the currently applied filter', async ({ page }) => { |
| 249 | const link = (name: string) => page.locator('.filters a', { hasText: name }); |
| 250 | await expect(link('All')).toHaveClass(/\bselected\b/); |
| 251 | await gotoFilter(page, 'Active'); |
| 252 | await expect(link('Active')).toHaveClass(/\bselected\b/); |
| 253 | await gotoFilter(page, 'Completed'); |
| 254 | await expect(link('Completed')).toHaveClass(/\bselected\b/); |
| 255 | }); |
| 256 | }); |
| 257 | |