From 4a2e268344ede8fc861eca4686c499014de36beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Thu, 6 Feb 2020 13:34:46 +0100 Subject: [PATCH 01/17] PR TEST 1 --- test3.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 test3.js diff --git a/test3.js b/test3.js new file mode 100644 index 0000000..8cd2718 --- /dev/null +++ b/test3.js @@ -0,0 +1 @@ +return 2; From 0009fef3131ecfc70e4f95cd89e7aeaf847fa06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Tue, 11 Feb 2020 11:25:49 +0100 Subject: [PATCH 02/17] Create test8 --- test8 | 1 + 1 file changed, 1 insertion(+) create mode 100644 test8 diff --git a/test8 b/test8 new file mode 100644 index 0000000..f03f694 --- /dev/null +++ b/test8 @@ -0,0 +1 @@ +dd From 22a7d24fb2777f134db4d1f2d8b48cbe81a8d71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Thu, 27 Feb 2020 13:01:34 +0100 Subject: [PATCH 03/17] Create newPR3 --- newPR3 | 1 + 1 file changed, 1 insertion(+) create mode 100644 newPR3 diff --git a/newPR3 b/newPR3 new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/newPR3 @@ -0,0 +1 @@ + From c8a82557ab6d66ab1da826c8457e962a9f6fec8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Tue, 9 Feb 2021 13:32:16 +0100 Subject: [PATCH 04/17] tst test --- test_again | 1 + 1 file changed, 1 insertion(+) create mode 100644 test_again diff --git a/test_again b/test_again new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/test_again @@ -0,0 +1 @@ +test From 7cf5b8cd8c71e6c406e151731837924cf1a139d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Tue, 9 Feb 2021 13:46:45 +0100 Subject: [PATCH 05/17] testAnother test --- testAnother | 1 + 1 file changed, 1 insertion(+) create mode 100644 testAnother diff --git a/testAnother b/testAnother new file mode 100644 index 0000000..b478595 --- /dev/null +++ b/testAnother @@ -0,0 +1 @@ +s From bdb426378dd7c9cd01001f63895df17af977d42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Thu, 25 Feb 2021 16:00:49 +0100 Subject: [PATCH 06/17] Create fileForTest --- fileForTest | 1 + 1 file changed, 1 insertion(+) create mode 100644 fileForTest diff --git a/fileForTest b/fileForTest new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/fileForTest @@ -0,0 +1 @@ +test From d8e40a1738a7f56196e4662f4920ee8c2601d309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Thu, 25 Feb 2021 17:12:27 +0100 Subject: [PATCH 07/17] Create work_please --- work_please | 1 + 1 file changed, 1 insertion(+) create mode 100644 work_please diff --git a/work_please b/work_please new file mode 100644 index 0000000..1df28a2 --- /dev/null +++ b/work_please @@ -0,0 +1 @@ +ss From 18009085a2569ad698671d3365ebdf6a5d1f3c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Thu, 25 Feb 2021 17:32:28 +0100 Subject: [PATCH 08/17] Create pr_test_4 --- pr_test_4 | 1 + 1 file changed, 1 insertion(+) create mode 100644 pr_test_4 diff --git a/pr_test_4 b/pr_test_4 new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/pr_test_4 @@ -0,0 +1 @@ +test From 42a08d65fce4e2af97d1722f415fd46040c2b02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Blachli=C5=84ski?= Date: Fri, 26 Feb 2021 17:30:46 +0100 Subject: [PATCH 09/17] Create xx --- xx | 1 + 1 file changed, 1 insertion(+) create mode 100644 xx diff --git a/xx b/xx new file mode 100644 index 0000000..ccc9bd6 --- /dev/null +++ b/xx @@ -0,0 +1 @@ +xx From 849c5b49137862a5939a0ede5fd7f1db29704f5b Mon Sep 17 00:00:00 2001 From: bre1470 <40056287+bre1470@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:33:52 +0200 Subject: [PATCH 10/17] Added toast. --- toast.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 toast.js diff --git a/toast.js b/toast.js new file mode 100644 index 0000000..d2596e6 --- /dev/null +++ b/toast.js @@ -0,0 +1 @@ +console.log('taostscript'); From e33524d3d91e952ac9f6756c69b7d67a9a3f4c9a Mon Sep 17 00:00:00 2001 From: bre1470 <40056287+bre1470@users.noreply.github.com> Date: Wed, 10 Aug 2022 18:29:22 +0200 Subject: [PATCH 11/17] Fixed typo. --- toast.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toast.js b/toast.js index d2596e6..2b0de93 100644 --- a/toast.js +++ b/toast.js @@ -1 +1 @@ -console.log('taostscript'); +console.log('toastscript'); From 8fd341518075dcd33641b2592ef65306f03ed3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Fus?= Date: Fri, 12 Aug 2022 15:06:51 +0200 Subject: [PATCH 12/17] Fixed #146, test scenario 1. --- toast.js | 1 + 1 file changed, 1 insertion(+) diff --git a/toast.js b/toast.js index 2b0de93..9e56410 100644 --- a/toast.js +++ b/toast.js @@ -1 +1,2 @@ console.log('toastscript'); +console.clear(); \ No newline at end of file From 2845490d427cc9b9fa69d69c4365037682c891d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Fus?= Date: Thu, 18 Aug 2022 12:01:09 +0200 Subject: [PATCH 13/17] Fixed typo. --- work_please | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/work_please b/work_please index 1df28a2..23fa7d3 100644 --- a/work_please +++ b/work_please @@ -1 +1 @@ -ss +k \ No newline at end of file From d787ca6cecf2b16c8dbb463b9eb7e08b7a22d4fe Mon Sep 17 00:00:00 2001 From: pawellysy Date: Wed, 14 Sep 2022 11:20:44 +0200 Subject: [PATCH 14/17] Revert "Another test" --- test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test diff --git a/test b/test deleted file mode 100644 index 1208c8a..0000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -ada From b479ad1ac5684d404fa38069ec687e9d427a2482 Mon Sep 17 00:00:00 2001 From: pawellysy Date: Mon, 3 Jul 2023 11:59:41 +0200 Subject: [PATCH 15/17] Revert "Izothep patch 2" --- test | 1 - test4 | 1 - 2 files changed, 2 deletions(-) delete mode 100644 test delete mode 100644 test4 diff --git a/test b/test deleted file mode 100644 index 1208c8a..0000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -ada diff --git a/test4 b/test4 deleted file mode 100644 index 4bcfe98..0000000 --- a/test4 +++ /dev/null @@ -1 +0,0 @@ -d From 05f80ab9ab207ff8bb0921e5f2135f97856b53f5 Mon Sep 17 00:00:00 2001 From: pawellysy Date: Mon, 3 Jul 2023 12:12:43 +0200 Subject: [PATCH 16/17] Revert "Revert "Izothep patch 2"" --- test | 1 + test4 | 1 + 2 files changed, 2 insertions(+) create mode 100644 test create mode 100644 test4 diff --git a/test b/test new file mode 100644 index 0000000..1208c8a --- /dev/null +++ b/test @@ -0,0 +1 @@ +ada diff --git a/test4 b/test4 new file mode 100644 index 0000000..4bcfe98 --- /dev/null +++ b/test4 @@ -0,0 +1 @@ +d From db62e9df5163df612d1b3ec1698f53c61bfbd151 Mon Sep 17 00:00:00 2001 From: Pawel Lysy Date: Mon, 26 Aug 2024 17:00:04 +0200 Subject: [PATCH 17/17] added playwright + browserstack test in gh actions --- .github/workflows/playwright-browserstack.yml | 96 ++++ .github/workflows/playwright.yml | 27 ++ .gitignore | 5 + browserstack.yml | 13 + package-lock.json | 91 ++++ package.json | 16 + playwright.config.ts | 50 ++ test | 1 - test/playwright/example.spec.ts | 18 + test/playwright/stock-tools.spec.ts | 10 + tests-examples/demo-todo-app.spec.ts | 437 ++++++++++++++++++ 11 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright-browserstack.yml create mode 100644 .github/workflows/playwright.yml create mode 100644 .gitignore create mode 100644 browserstack.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts delete mode 100644 test create mode 100644 test/playwright/example.spec.ts create mode 100644 test/playwright/stock-tools.spec.ts create mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/.github/workflows/playwright-browserstack.yml b/.github/workflows/playwright-browserstack.yml new file mode 100644 index 0000000..6fdf136 --- /dev/null +++ b/.github/workflows/playwright-browserstack.yml @@ -0,0 +1,96 @@ +name: Playwright Browserstack Tests + +on: + pull_request: + branches: [ main, master ] + workflow_dispatch: + +jobs: + test_playwright_browserstack: + timeout-minutes: 60 + runs-on: ubuntu-latest + environment: browserstack + name: Playwright + + steps: + - name: 'BrowserStack Env Setup' # Invokes the setup-env action + uses: browserstack/github-actions/setup-env@master + with: + username: ${{ secrets.BROWSERSTACK_USERNAME }} + access-key: ${{ secrets.BROWSERSTACK_KEY }} + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: | + npm i + npm i https://github.com/browserstack/node-js-playwright-browserstack.git + + - name: Checkout utils + uses: actions/checkout@v4 + with: + repository: highcharts/highcharts-utils + path: utils + fetch-depth: 1 + + - name: Install Highcharts Utils + run: | + cd utils + npm i + + - name: Update Chrome + if: ${{ runner.os == 'Linux' }} + run: | + sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo apt update + sudo apt --only-upgrade install google-chrome-stable + + - name: Chrome info + run: | + set CHROME_BIN $(which chrome || which google-chrome || which chromium) + echo $CHROME_BIN + $CHROME_BIN --version + - uses: nanasess/setup-chromedriver@v2 + - uses: browser-actions/setup-firefox@latest + with: + firefox-version: latest-esr + - uses: browser-actions/setup-geckodriver@latest + + - name: Setup Display + run: | + chromedriver --url-base=/wd/hub & + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + + + - name: 'BrowserStack Env Setup' # Invokes the setup-env action + uses: browserstack/github-actions/setup-env@master + with: + username: ${{ secrets.BROWSERSTACK_USERNAME }} + access-key: ${{ secrets.BROWSERSTACK_KEY }} + + - name: 'BrowserStackLocal Setup' + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: start + local-identifier: 'abc123' + + - name: Run Playwright tests + env: + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_KEY }} + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + run: | + export DISPLAY=:99 + npx browserstack-node-sdk playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: 'BrowserStackLocal Stop' + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: stop diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..467190b --- /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@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - 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@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68c5d18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/browserstack.yml b/browserstack.yml new file mode 100644 index 0000000..c578262 --- /dev/null +++ b/browserstack.yml @@ -0,0 +1,13 @@ +projectName: Highcharts +buildName: highcharts build +buildIdentifier: '#${BUILD_NUMBER}' +platforms: + - deviceName: Samsung Galaxy S22 Ultra + browserName: chrome + osVersion: 12.0 +parallelsPerPlatform: 1 +debug: true +browserstackLocal: true +networkLogs: false +consoleLogs: errors +testObservability: true diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f2e7eb3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "projects-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "projects-test", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.46.1", + "@types/node": "^22.5.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", + "dev": true, + "dependencies": { + "playwright": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb113ac --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "projects-test", + "version": "1.0.0", + "description": "Used for testing possibilities of GH projects", + "main": "test2.js", + "directories": { + "test": "test" + }, + "scripts": {}, + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.46.1", + "@types/node": "^22.5.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..63b99d9 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,50 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./test/playwright", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3030/samples/", + + viewport: { width: 1280, height: 720 }, + /* 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"] }, + }, + + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "node utils/server --localOnly", + url: "http://127.0.0.1:3030", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/test b/test deleted file mode 100644 index 1208c8a..0000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -ada diff --git a/test/playwright/example.spec.ts b/test/playwright/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/test/playwright/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/test/playwright/stock-tools.spec.ts b/test/playwright/stock-tools.spec.ts new file mode 100644 index 0000000..e624a9c --- /dev/null +++ b/test/playwright/stock-tools.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from '@playwright/test'; + +test('stock tools gui is working', async ({ page }) => { + + const urlPrefix = "view?path="; + await page.goto(urlPrefix + 'highcharts/cypress/stock-tools-gui'); + await page.locator('.highcharts-indicators').first().click(); + await page.locator('div.highcharts-popup').isVisible(); +}); + diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..8641cb5 --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +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' +] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).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 a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +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.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').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 a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').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 a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.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.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: '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.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: '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.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: '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.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).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.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: '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.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + 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 }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.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); +}