diff --git a/assets/demo-todo-app.spec.js b/assets/demo-todo-app.spec.js new file mode 100644 index 0000000..e2051fc --- /dev/null +++ b/assets/demo-todo-app.spec.js @@ -0,0 +1,411 @@ +// @ts-check +const { test, expect } = require('@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 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) { + for (const item of TODO_ITEMS) { + await page.locator('.new-todo').fill(item); + await page.locator('.new-todo').press('Enter'); + } +} + +/** + * @param {import('@playwright/test').Page} page + * @param {number} expected + */ + async function checkNumberOfTodosInLocalStorage(page, expected) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +/** + * @param {import('@playwright/test').Page} page + * @param {number} expected + */ + async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; + }, expected); +} + +/** + * @param {import('@playwright/test').Page} page + * @param {string} title + */ +async function checkTodosInLocalStorage(page, title) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); + }, title); +} diff --git a/assets/demo-todo-app.spec.ts b/assets/demo-todo-app.spec.ts new file mode 100644 index 0000000..5bba0ad --- /dev/null +++ b/assets/demo-todo-app.spec.ts @@ -0,0 +1,398 @@ +import { test, expect, type 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 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((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} diff --git a/assets/example.spec.js b/assets/example.spec.js index e2051fc..39fa164 100644 --- a/assets/example.spec.js +++ b/assets/example.spec.js @@ -1,411 +1,21 @@ // @ts-check const { test, expect } = require('@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); +test('homepage has Playwright in title and get started link linking to the intro page', async ({ page }) => { + await page.goto('https://playwright.dev/'); - // 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/); + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); - // Check all items in one call. - await expect(page.locator('.view label')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); + // create a locator + const getStarted = page.locator('text=Get Started'); - 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'); + // Expect an attribute "to be strictly equal" to the value. + await expect(getStarted).toHaveAttribute('href', '/docs/intro'); - await expect(page.locator('.main')).toBeVisible(); - await expect(page.locator('.footer')).toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); + // Click the get started link. + await getStarted.click(); + + // Expects the URL to contain intro. + await expect(page).toHaveURL(/.*intro/); }); - -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 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) { - for (const item of TODO_ITEMS) { - await page.locator('.new-todo').fill(item); - await page.locator('.new-todo').press('Enter'); - } -} - -/** - * @param {import('@playwright/test').Page} page - * @param {number} expected - */ - async function checkNumberOfTodosInLocalStorage(page, expected) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -/** - * @param {import('@playwright/test').Page} page - * @param {number} expected - */ - async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; - }, expected); -} - -/** - * @param {import('@playwright/test').Page} page - * @param {string} title - */ -async function checkTodosInLocalStorage(page, title) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); - }, title); -} diff --git a/assets/example.spec.ts b/assets/example.spec.ts index 5bba0ad..ab79c19 100644 --- a/assets/example.spec.ts +++ b/assets/example.spec.ts @@ -1,398 +1,20 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test, expect } 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); +test('homepage has Playwright in title and get started link linking to the intro page', async ({ page }) => { + await page.goto('https://playwright.dev/'); - 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'); + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); - // 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 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]]); - }); + // create a locator + const getStarted = page.locator('text=Get Started'); - 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); + // Expect an attribute "to be strictly equal" to the value. + await expect(getStarted).toHaveAttribute('href', '/docs/intro'); - await test.step('Showing all items', async () => { - await page.locator('.filters >> text=All').click(); - await expect(page.locator('.todo-list li')).toHaveCount(3); - }); + // Click the get started link. + await getStarted.click(); - 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'); - }); + // Expects the URL to contain intro. + await expect(page).toHaveURL(/.*intro/); }); - -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((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/src/generator.ts b/src/generator.ts index 75a33d4..25e5bdc 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -152,8 +152,10 @@ export class Generator { files.set('.github/workflows/playwright.yml', githubActionsScript); } - if (installExamples) + if (installExamples) { files.set(path.join(answers.testDir, `example.spec.${fileExtension}`), this._readAsset(`example.spec.${fileExtension}`)); + files.set(path.join('tests-examples', `demo-todo-app.spec.${fileExtension}`), this._readAsset(`demo-todo-app.spec.${fileExtension}`)); + } if (!fs.existsSync(path.join(this.rootDir, 'package.json'))) { commands.push({ @@ -236,7 +238,8 @@ export class Generator { console.log(colors.green('✔ Success!') + ' ' + colors.bold(`Created a Playwright Test project at ${this.rootDir}`)); const pathToNavigate = path.relative(process.cwd(), this.rootDir); const prefix = pathToNavigate !== '' ? ` cd ${pathToNavigate}\n` : ''; - const exampleSpecPath = `example.spec.${languageToFileExtension(answers.language)}`; + const exampleSpecPath = path.join(answers.testDir, `example.spec.${languageToFileExtension(answers.language)}`); + const demoTodoAppSpecPath = path.join('tests-examples', `demo-todo-app.spec.${languageToFileExtension(answers.language)}`); const playwrightConfigPath = `playwright.config.${languageToFileExtension(answers.language)}`; console.log(` Inside that directory, you can run several commands: @@ -247,18 +250,22 @@ Inside that directory, you can run several commands: ${colors.cyan(commandToRunTests(this.packageManager, '--project=chromium'))} Runs the tests only on Desktop Chrome. - ${colors.cyan(commandToRunTests(this.packageManager, exampleSpecPath))} - Runs the tests in the specific file. + ${colors.cyan(commandToRunTests(this.packageManager, 'example'))} + Runs the tests in a specific file. ${colors.cyan(`${commandToRunTests(this.packageManager, '--debug')}`)} Runs the tests in debug mode. + ${colors.cyan(`${commandToRunCodegen(this.packageManager)}`)} + Auto generate tests with Codegen. + We suggest that you begin by typing: ${colors.cyan(prefix + ' ' + commandToRunTests(this.packageManager))} And check out the following files: - .${path.sep}${pathToNavigate ? path.join(pathToNavigate, exampleSpecPath) : exampleSpecPath} - Example end-to-end test + - .${path.sep}${pathToNavigate ? path.join(pathToNavigate, demoTodoAppSpecPath) : demoTodoAppSpecPath} - Demo Todo App end-to-end tests - .${path.sep}${pathToNavigate ? path.join(pathToNavigate, playwrightConfigPath) : playwrightConfigPath} - Playwright Test configuration Visit https://playwright.dev/docs/intro for more information. ✨ @@ -298,3 +305,9 @@ export function commandToRunTests(packageManager: 'npm' | 'yarn', args?: string) return `yarn playwright test${args ? (' ' + args) : ''}`; return `npx playwright test${args ? (' ' + args) : ''}`; } + +export function commandToRunCodegen(packageManager: 'npm' | 'yarn') { + if (packageManager === 'yarn') + return `yarn playwright codegen`; + return `npx playwright codegen`; +}