diff --git a/docs/examples/authentication.js b/docs/examples/authentication.js deleted file mode 100644 index 35ab34c5a6..0000000000 --- a/docs/examples/authentication.js +++ /dev/null @@ -1,53 +0,0 @@ -const { chromium, webkit } = require('playwright'); -const assert = require('assert'); - -/** - * In this script, we will login on GitHub.com through Chromium, - * and reuse login state inside WebKit. This recipe can be - * used to speed up tests by logging in once and reusing login state. - * - * Steps summary - * 1. Login on GitHub.com in Chromium - * 2. Export storage state from Chromium browser context - * 3. Set storage state in WebKit browser context and verify login - */ - -const account = { login: '', password: '' }; - -(async () => { - // Create a Chromium browser context - const crBrowser = await chromium.launch(); - const crContext = await crBrowser.newContext(); - const crPage = await crContext.newPage(); - - // Navigate and auto-wait on the page to load after navigation - await crPage.goto('https://github.com/login'); - - // Fill login form elements - await crPage.fill('input[name="login"]', account.login); - await crPage.fill('input[name="password"]', account.password); - - // Submit form and auto-wait for the navigation to complete - await crPage.click('input[type="submit"]'); - await verifyIsLoggedIn(crPage); - - // Get storage state from Chromium browser context - const storageState = await crContext.storageState(); - await crBrowser.close(); - - // Create WebKit browser context with saved storage state - const wkBrowser = await webkit.launch(); - const wkContext = await wkBrowser.newContext({ storageState }); - - // Navigate to GitHub.com and verify that we are logged in - const wkPage = await wkContext.newPage(); - await wkPage.goto('http://github.com'); - await wkPage.screenshot({ path: 'webkit.png' }); - await verifyIsLoggedIn(wkPage); - await wkBrowser.close(); -})(); - -const verifyIsLoggedIn = async (page) => { - await page.click('summary[aria-label="View profile and more"]') - assert(await page.waitForSelector(`text="Your profile"`)); -} diff --git a/examples/github-api/.gitignore b/examples/github-api/.gitignore new file mode 100644 index 0000000000..dbd64df830 --- /dev/null +++ b/examples/github-api/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +test-results/ +playwright-report/ diff --git a/examples/github-api/package.json b/examples/github-api/package.json new file mode 100644 index 0000000000..5f513f8cbe --- /dev/null +++ b/examples/github-api/package.json @@ -0,0 +1,13 @@ +{ + "name": "github-api", + "version": "0.0.1", + "scripts": { + "test": "playwright test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.17.1" + } +} diff --git a/examples/github-api/playwright.config.ts b/examples/github-api/playwright.config.ts new file mode 100644 index 0000000000..1584eb33ae --- /dev/null +++ b/examples/github-api/playwright.config.ts @@ -0,0 +1,36 @@ +/* eslint-disable notice/notice */ + +import { PlaywrightTestConfig } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + + testDir: './tests', + + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, +}; +export default config; diff --git a/docs/examples/test-api.spec.js b/examples/github-api/tests/test-api.spec.ts similarity index 95% rename from docs/examples/test-api.spec.js rename to examples/github-api/tests/test-api.spec.ts index f70c7bf4a6..947c38c188 100644 --- a/docs/examples/test-api.spec.js +++ b/examples/github-api/tests/test-api.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable notice/notice */ + /** * In this script, we will login and run a few tests that use GitHub API. * @@ -7,8 +9,7 @@ * 3. Delete the repo. */ - -const { test, expect } = require('@playwright/test'); +import { test, expect } from '@playwright/test'; const user = process.env.GITHUB_USER; const repo = 'Test-Repo-1'; diff --git a/examples/svgomg/.gitignore b/examples/svgomg/.gitignore new file mode 100644 index 0000000000..90b9c1bc17 --- /dev/null +++ b/examples/svgomg/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +test-results/ +playwright-report/ +package-lock.json \ No newline at end of file diff --git a/examples/svgomg/package.json b/examples/svgomg/package.json new file mode 100644 index 0000000000..93302628c0 --- /dev/null +++ b/examples/svgomg/package.json @@ -0,0 +1,16 @@ +{ + "name": "svgomg-tests", + "version": "0.0.1", + "scripts": { + "test": "playwright test", + "ctest": "playwright test --project=chromium", + "ftest": "playwright test --project=firefox", + "wtest": "playwright test --project=webkit" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.17.1" + } +} diff --git a/examples/svgomg/playwright.config.ts b/examples/svgomg/playwright.config.ts new file mode 100644 index 0000000000..4b8bde2567 --- /dev/null +++ b/examples/svgomg/playwright.config.ts @@ -0,0 +1,114 @@ +/* eslint-disable notice/notice */ + +import { PlaywrightTestConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + + testDir: './tests', + + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + + expect: { + + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + acceptDownloads: true, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + + /* Project-specific settings. */ + use: { + ...devices['Desktop Chrome'], + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; +export default config; diff --git a/examples/svgomg/tests/example.spec.ts b/examples/svgomg/tests/example.spec.ts new file mode 100644 index 0000000000..79b7983db4 --- /dev/null +++ b/examples/svgomg/tests/example.spec.ts @@ -0,0 +1,105 @@ +/* eslint-disable notice/notice */ + +import { test, expect } from '@playwright/test'; +import fs from 'fs'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/svgomg'); +}); + +test('verify menu items', async ({ page }) => { + await expect(page.locator('.menu li')).toHaveText([ + 'Open SVG', + 'Paste markup', + 'Demo', + 'Contribute' + ]); +}); + +test.describe('demo tests', () => { + test.beforeEach(async ({ page }) => { + await page.locator('.menu-item >> text=Demo').click(); + }); + + test('verify default global settings', async ({ page }) => { + const menuItems = page.locator('.settings-scroller .global .setting-item-toggle'); + await expect(menuItems).toHaveText([ + 'Show original', + 'Compare gzipped', + 'Prettify markup', + 'Multipass', + ]); + + const toggle = page.locator('.setting-item-toggle'); + await expect(toggle.locator('text=Show original')).not.toBeChecked(); + await expect(toggle.locator('text=Compare gzipped')).toBeChecked(); + await expect(toggle.locator('text=Prettify markup')).not.toBeChecked(); + await expect(toggle.locator('text=Multipass')).not.toBeChecked(); + }); + + test('verify default features', async ({ page }) => { + const enabledOptions = [ + 'Clean up attribute whitespace', + 'Clean up IDs', + 'Collapse useless groups', + 'Convert non-eccentric to ', + 'Inline styles', + ]; + + const disabledOptions = [ + 'Prefer viewBox to width/height', + 'Remove raster images', + 'Remove script elements', + 'Remove style elements', + ]; + + for (const option of enabledOptions) { + const locator = page.locator(`.setting-item-toggle >> text=${option}`); + await expect(locator).toBeChecked(); + } + + for (const option of disabledOptions) { + const locator = page.locator(`.setting-item-toggle >> text=${option}`); + await expect(locator).not.toBeChecked(); + } + }); + + test('reset settings', async ({ page }) => { + const showOriginalSetting = page.locator('.setting-item-toggle >> text=Show original'); + await showOriginalSetting.click(); + await expect(showOriginalSetting).toBeChecked(); + await page.locator('button >> text=Reset all').click(); + await expect(showOriginalSetting).not.toBeChecked(); + }); + + test('download result', async ({ page }) => { + const downloadButton = page.locator('a[title=Download]'); + await expect(downloadButton).toHaveAttribute('href', /blob/); + const [download] = await Promise.all([ + page.waitForEvent('download'), + downloadButton.click() + ]); + expect(download.suggestedFilename()).toBe('car-lite.svg'); + const result = fs.readFileSync(await download.path(), 'utf-8'); + expect(result).toContain(' { + // Start waiting for the file chooser, then click the button. + const [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser'), + page.click('text=Open SVG'), + ]); + + // Set file to the chooser. + await fileChooser.setFiles({ + name: 'file.svg', + mimeType: 'image/svg+xml', + buffer: Buffer.from(``) + }); + + // Verify provided svg was rendered. + const markup = await page.frameLocator('.svg-frame').locator('svg').evaluate(svg => svg.outerHTML); + expect(markup).toMatch(//); +}); diff --git a/examples/todomvc/.gitignore b/examples/todomvc/.gitignore new file mode 100644 index 0000000000..023876884f --- /dev/null +++ b/examples/todomvc/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +test-results/ +playwright-report/ +package-lock.json diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json new file mode 100644 index 0000000000..d8289f50d7 --- /dev/null +++ b/examples/todomvc/package.json @@ -0,0 +1,16 @@ +{ + "name": "todomvc-test", + "version": "0.0.1", + "scripts": { + "test": "playwright test", + "ctest": "playwright test --project=chromium", + "ftest": "playwright test --project=firefox", + "wtest": "playwright test --project=webkit" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.17.1" + } +} diff --git a/examples/todomvc/playwright.config.ts b/examples/todomvc/playwright.config.ts new file mode 100644 index 0000000000..ce488c07e9 --- /dev/null +++ b/examples/todomvc/playwright.config.ts @@ -0,0 +1,112 @@ +/* eslint-disable notice/notice */ + +import { PlaywrightTestConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + + testDir: './tests', + + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + + expect: { + + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + + /* Project-specific settings. */ + use: { + ...devices['Desktop Chrome'], + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; +export default config; diff --git a/examples/todomvc/tests/integration.spec.ts b/examples/todomvc/tests/integration.spec.ts new file mode 100644 index 0000000000..5c5ba88fcd --- /dev/null +++ b/examples/todomvc/tests/integration.spec.ts @@ -0,0 +1,401 @@ +/* eslint-disable notice/notice */ + +import { test, expect, Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +]; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // Create 1st todo. + await page.locator('.new-todo').fill(TODO_ITEMS[0]); + await page.locator('.new-todo').press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.locator('.view label')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await page.locator('.new-todo').fill(TODO_ITEMS[1]); + await page.locator('.new-todo').press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.locator('.view label')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // Create one todo item. + await page.locator('.new-todo').fill(TODO_ITEMS[0]); + await page.locator('.new-todo').press('Enter'); + + // Check that input is empty. + await expect(page.locator('.new-todo')).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // Check test using different methods. + await expect(page.locator('.todo-count')).toHaveText('3 items left'); + await expect(page.locator('.todo-count')).toContainText('3'); + await expect(page.locator('.todo-count')).toHaveText(/3/); + + // Check all items in one call. + await expect(page.locator('.view label')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should show #main and #footer when items added', async ({ page }) => { + await page.locator('.new-todo').fill(TODO_ITEMS[0]); + await page.locator('.new-todo').press('Enter'); + + await expect(page.locator('.main')).toBeVisible(); + await expect(page.locator('.footer')).toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.locator('.toggle-all').check(); + + // Ensure all todos have 'completed' class. + await expect(page.locator('.todo-list li')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + // Check and then immediately uncheck. + await page.locator('.toggle-all').check(); + await page.locator('.toggle-all').uncheck(); + + // Should be no completed classes. + await expect(page.locator('.todo-list li')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.locator('.toggle-all'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.locator('.todo-list li').nth(0); + await firstTodo.locator('.toggle').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.locator('.toggle').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await page.locator('.new-todo').fill(item); + await page.locator('.new-todo').press('Enter'); + } + + // Check first item. + const firstTodo = page.locator('.todo-list li').nth(0); + await firstTodo.locator('.toggle').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.locator('.todo-list li').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.locator('.toggle').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await page.locator('.new-todo').fill(item); + await page.locator('.new-todo').press('Enter'); + } + + const firstTodo = page.locator('.todo-list li').nth(0); + const secondTodo = page.locator('.todo-list li').nth(1); + await firstTodo.locator('.toggle').check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodo.locator('.toggle').uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.locator('.todo-list li'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.locator('.edit')).toHaveValue(TODO_ITEMS[1]); + await secondTodo.locator('.edit').fill('buy some sausages'); + await secondTodo.locator('.edit').press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.locator('.todo-list li').nth(1); + await todoItem.dblclick(); + await expect(todoItem.locator('.toggle')).not.toBeVisible(); + await expect(todoItem.locator('label')).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.locator('.todo-list li'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).locator('.edit').fill('buy some sausages'); + await todoItems.nth(1).locator('.edit').dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.locator('.todo-list li'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).locator('.edit').fill(' buy some sausages '); + await todoItems.nth(1).locator('.edit').press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.locator('.todo-list li'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).locator('.edit').fill(''); + await todoItems.nth(1).locator('.edit').press('Enter'); + + await page.pause(); + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.locator('.todo-list li'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).locator('.edit').press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + await page.locator('.new-todo').fill(TODO_ITEMS[0]); + await page.locator('.new-todo').press('Enter'); + await expect(page.locator('.todo-count')).toContainText('1'); + + await page.locator('.new-todo').fill(TODO_ITEMS[1]); + await page.locator('.new-todo').press('Enter'); + await expect(page.locator('.todo-count')).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.locator('.clear-completed')).toHaveText('Clear completed'); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.locator('.todo-list li'); + await todoItems.nth(1).locator('.toggle').check(); + await page.locator('.clear-completed').click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.locator('.clear-completed').click(); + await expect(page.locator('.clear-completed')).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + for (const item of TODO_ITEMS.slice(0, 2)) { + await page.locator('.new-todo').fill(item); + await page.locator('.new-todo').press('Enter'); + } + + const todoItems = page.locator('.todo-list li'); + await todoItems.nth(0).locator('.toggle').check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + await page.locator('.todo-list li .toggle').nth(1).check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.locator('.filters >> text=Active').click(); + await expect(page.locator('.todo-list li')).toHaveCount(2); + await expect(page.locator('.todo-list li')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + await page.locator('.todo-list li .toggle').nth(1).check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.locator('.filters >> text=All').click(); + await expect(page.locator('.todo-list li')).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.locator('.filters >> text=Active').click(); + }); + + await test.step('Showing completed items', async () => { + await page.locator('.filters >> text=Completed').click(); + }); + + await expect(page.locator('.todo-list li')).toHaveCount(1); + await page.goBack(); + await expect(page.locator('.todo-list li')).toHaveCount(2); + await page.goBack(); + await expect(page.locator('.todo-list li')).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.locator('.todo-list li .toggle').nth(1).check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.locator('.filters >> text=Completed').click(); + await expect(page.locator('.todo-list li')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.locator('.todo-list li .toggle').nth(1).check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.locator('.filters >> text=Active').click(); + await page.locator('.filters >> text=Completed').click(); + await page.locator('.filters >> text=All').click(); + await expect(page.locator('.todo-list li')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.locator('.filters >> text=All')).toHaveClass('selected'); + await page.locator('.filters >> text=Active').click(); + // Page change - active items. + await expect(page.locator('.filters >> text=Active')).toHaveClass('selected'); + await page.locator('.filters >> text=Completed').click(); + // Page change - completed items. + await expect(page.locator('.filters >> text=Completed')).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + for (const item of TODO_ITEMS) { + await page.locator('.new-todo').fill(item); + await page.locator('.new-todo').press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); + }, title); +}