| 1 | // Every user action, performed the way a person would with JavaScript on |
| 2 | // (double-click, checkbox clicks) or off (the contract's submit controls |
| 3 | // and edit link). Each one returns once the server has answered. |
| 4 | import { expect, type Page, type Locator, type TestInfo } from '@playwright/test'; |
| 5 | |
| 6 | export const jsOn = (ti: TestInfo) => ti.project.name === 'js-on'; |
| 7 | export const todos = (page: Page) => page.locator('.todo-list li'); |
| 8 | |
| 9 | // Runs fn and waits for what it causes: a page load with JS off, the POST |
| 10 | // response with JS on. |
| 11 | async function settle(page: Page, ti: TestInfo, fn: () => Promise<void>) { |
| 12 | if (jsOn(ti)) { |
| 13 | await Promise.all([page.waitForResponse((r) => r.request().method() === 'POST'), fn()]); |
| 14 | } else { |
| 15 | await Promise.all([page.waitForEvent('load'), fn()]); |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | export async function addTodo(page: Page, ti: TestInfo, title: string) { |
| 20 | const input = page.locator('.new-todo'); |
| 21 | await input.fill(title); |
| 22 | await settle(page, ti, () => input.press('Enter')); |
| 23 | } |
| 24 | |
| 25 | export async function addDefaultTodos(page: Page, ti: TestInfo, titles: string[]) { |
| 26 | for (const t of titles) await addTodo(page, ti, t); |
| 27 | await expect(todos(page)).toHaveCount(titles.length); |
| 28 | } |
| 29 | |
| 30 | export async function toggle(page: Page, ti: TestInfo, item: Locator) { |
| 31 | const control = jsOn(ti) ? item.locator('.toggle') : item.locator('[data-action="toggle"]'); |
| 32 | await settle(page, ti, () => control.click()); |
| 33 | } |
| 34 | |
| 35 | export async function toggleAll(page: Page, ti: TestInfo) { |
| 36 | const control = jsOn(ti) |
| 37 | ? page.locator('label[for="toggle-all"]') |
| 38 | : page.locator('[data-action="toggle-all"]'); |
| 39 | await settle(page, ti, () => control.click()); |
| 40 | } |
| 41 | |
| 42 | export async function destroy(page: Page, ti: TestInfo, item: Locator) { |
| 43 | await item.hover(); |
| 44 | await settle(page, ti, () => item.locator('.destroy').click()); |
| 45 | } |
| 46 | |
| 47 | export async function clearCompleted(page: Page, ti: TestInfo) { |
| 48 | await settle(page, ti, () => page.locator('.clear-completed').click()); |
| 49 | } |
| 50 | |
| 51 | // Opens the item's editor: double-click with JS, the edit link without. |
| 52 | // Returns the edit input. |
| 53 | export async function startEditing(page: Page, ti: TestInfo, index: number): Promise<Locator> { |
| 54 | const item = todos(page).nth(index); |
| 55 | if (jsOn(ti)) { |
| 56 | await item.locator('label').dblclick(); |
| 57 | } else { |
| 58 | await Promise.all([page.waitForEvent('load'), item.locator('a[data-action="edit"]').click()]); |
| 59 | } |
| 60 | const input = todos(page).nth(index).locator('.edit'); |
| 61 | await expect(input).toBeVisible(); |
| 62 | return input; |
| 63 | } |
| 64 | |
| 65 | export async function submitEdit(page: Page, ti: TestInfo, input: Locator, title: string) { |
| 66 | await input.fill(title); |
| 67 | await settle(page, ti, () => input.press('Enter')); |
| 68 | } |
| 69 | |
| 70 | export async function gotoFilter(page: Page, name: 'All' | 'Active' | 'Completed') { |
| 71 | const path = { All: '/', Active: '/active', Completed: '/completed' }[name]; |
| 72 | await page.locator('.filters a', { hasText: name }).click(); |
| 73 | await expect(page).toHaveURL((u) => u.pathname === path); |
| 74 | } |
| 75 | |