From 2be9e21238402d6b73b7a42f46d29ebb859788ee Mon Sep 17 00:00:00 2001 From: Daniel Gamage Date: Mon, 16 May 2022 19:40:05 -0400 Subject: [PATCH 1/5] playwright init --- .github/workflows/playwright.yml | 27 +++ .gitignore | 3 + package-lock.json | 50 +++- package.json | 1 + playwright.config.ts | 105 ++++++++ tests/example.spec.ts | 398 +++++++++++++++++++++++++++++++ 6 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 playwright.config.ts create mode 100644 tests/example.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..fd5663c --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '14.x' + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v2 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index e4504a3..acef850 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ node_modules npm-debug.log* yarn-debug.log* yarn-error.log* +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 4288948..021c1e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@oakstudios/mechanical-ragger", - "version": "0.3.0", + "version": "0.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@oakstudios/mechanical-ragger", - "version": "0.3.0", + "version": "0.4.1", "license": "MIT", "devDependencies": { "@babel/cli": "^7.12.13", @@ -15,6 +15,7 @@ "@babel/preset-env": "^7.16.4", "@babel/preset-react": "^7.12.13", "@babel/preset-typescript": "^7.16.7", + "@playwright/test": "^1.22.0", "@rollup/plugin-babel": "^5.2.3", "@rollup/plugin-node-resolve": "^13.0.6", "@testing-library/jest-dom": "^5.15.1", @@ -33,6 +34,9 @@ "tslib": "^2.3.1", "typescript": "^4.5.3" }, + "engines": { + "node": ">=14.17.0" + }, "peerDependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" @@ -2527,6 +2531,21 @@ "dev": true, "optional": true }, + "node_modules/@playwright/test": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.22.0.tgz", + "integrity": "sha512-ExcAjiECo3uTG5Sl5H4a7rKp/5TEHTI87dv9NHYEoUFuOHPhSVxB7QsuM70ktO+wTTZ9KzhwzcegxAGRmUFKEA==", + "dev": true, + "dependencies": { + "playwright-core": "1.22.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz", @@ -7154,6 +7173,18 @@ "node": ">=8" } }, + "node_modules/playwright-core": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.22.0.tgz", + "integrity": "sha512-XnDPiV4NCzTtXWxQdyJ6Wg8xhST3ciUjt5mITaxoqOoYggmRtofKm0PXLehfbetXh2ppPYj5U8UhtUpdIE4wag==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -10109,6 +10140,15 @@ "dev": true, "optional": true }, + "@playwright/test": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.22.0.tgz", + "integrity": "sha512-ExcAjiECo3uTG5Sl5H4a7rKp/5TEHTI87dv9NHYEoUFuOHPhSVxB7QsuM70ktO+wTTZ9KzhwzcegxAGRmUFKEA==", + "dev": true, + "requires": { + "playwright-core": "1.22.0" + } + }, "@rollup/plugin-babel": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz", @@ -13637,6 +13677,12 @@ "find-up": "^4.0.0" } }, + "playwright-core": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.22.0.tgz", + "integrity": "sha512-XnDPiV4NCzTtXWxQdyJ6Wg8xhST3ciUjt5mITaxoqOoYggmRtofKm0PXLehfbetXh2ppPYj5U8UhtUpdIE4wag==", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", diff --git a/package.json b/package.json index 61a359e..693e554 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@babel/preset-env": "^7.16.4", "@babel/preset-react": "^7.12.13", "@babel/preset-typescript": "^7.16.7", + "@playwright/test": "^1.22.0", "@rollup/plugin-babel": "^5.2.3", "@rollup/plugin-node-resolve": "^13.0.6", "@testing-library/jest-dom": "^5.15.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..37c417d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,105 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * 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', + 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/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 0000000..19239e5 --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,398 @@ +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 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); +} From b38dfa0c0348a32a3681fc6bbe7ba786f113f22f Mon Sep 17 00:00:00 2001 From: Daniel Gamage Date: Tue, 17 May 2022 00:28:28 -0400 Subject: [PATCH 2/5] removing type:module for playwright --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 693e554..7550e69 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "@oakstudios/mechanical-ragger", "version": "0.4.1", "description": "A layout tool that automatically rags long text by line", - "type": "module", "main": "./index.js", "types": "./index.d.ts", "exports": { From a7838dddf89b311dc7dca1236149fd033ef12a4f Mon Sep 17 00:00:00 2001 From: Daniel Gamage Date: Tue, 17 May 2022 01:43:35 -0400 Subject: [PATCH 3/5] working web component tests --- tests/example.spec.ts | 414 +++--------------------------------------- 1 file changed, 21 insertions(+), 393 deletions(-) diff --git a/tests/example.spec.ts b/tests/example.spec.ts index 19239e5..75bc796 100644 --- a/tests/example.spec.ts +++ b/tests/example.spec.ts @@ -1,398 +1,26 @@ -import { test, expect, Page } from '@playwright/test'; +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 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 page.goto( + "http://localhost:6006/iframe.html?id=web-component--slotted-element-styles" + ); +}); + +test.describe("Web Component", () => { + test("should allow slotted elements to be targetted by page CSS", async ({ + page, + }) => { + const element = await page.locator("mechanical-ragger a"); + await expect(element).toHaveCSS("border-bottom-color", "rgb(255, 0, 0)"); + }); + test("should propagate content updates to the shadow dom", async ({ + page, + }) => { + const ragger = await page.waitForSelector("mechanical-ragger p"); + await ragger.evaluate((el) => { + el.innerHTML = "two"; }); - - 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'); + const element = await page.locator("mechanical-ragger >>> p"); + await expect(element).toHaveText("two"); }); }); - -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); -} From 48a801b1894939cc0653df120f66e1f47225070c Mon Sep 17 00:00:00 2001 From: Daniel Gamage Date: Tue, 17 May 2022 01:47:39 -0400 Subject: [PATCH 4/5] removing dom-centric jest tests --- src/__tests__/components.spec.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/__tests__/components.spec.js b/src/__tests__/components.spec.js index 0f30e54..74845e4 100644 --- a/src/__tests__/components.spec.js +++ b/src/__tests__/components.spec.js @@ -35,29 +35,6 @@ test("the web component is created without errors.", async () => { expect(el).toHaveProperty("ragger"); }); -test("the web component reflects updated content.", async () => { - const container = document.createElement("div"); - container.innerHTML = "one"; - const el = container.querySelector("mechanical-ragger"); - el.innerHTML = "two"; - // el.shadowRoot seems like it's not thoroughly implemented in JSDOM. - expect(el.shadowRoot).toHaveTextContent("two"); -}); - -test("the web component's slot element inherit styles.", async () => { - const container = document.createElement("div"); - const style = (document.createElement( - "style" - ).innerHTML = `a { border: 1px solid blue; }`); - container.innerHTML = - "one"; - document.body.append(container, style); - const el = container.querySelector("mechanical-ragger a"); - // jsdom doessn't return anything from getComputedStyle beside visibility. - const borderStyle = window.getComputedStyle(el).border; - expect(borderStyle).toEqual("1px solid blue"); -}); - test("the web component calls unobserve when it's removed from the DOM.", async () => { const el = document.createElement("mechanical-ragger"); document.body.appendChild(el); From be000296320a63bc5c7e34be472aa48e3f34af64 Mon Sep 17 00:00:00 2001 From: Daniel Gamage Date: Tue, 17 May 2022 01:54:14 -0400 Subject: [PATCH 5/5] added storybook to webserver config in playwright needs some adjustments --- playwright.config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 37c417d..415fe90 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -96,10 +96,10 @@ const config: PlaywrightTestConfig = { // outputDir: 'test-results/', /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // port: 3000, - // }, + webServer: { + command: 'npm run storybook', + port: 6006, + }, }; export default config;