From 7bc581a6e31280b32cb57b5de6b2e24258a563d7 Mon Sep 17 00:00:00 2001 From: sadaf895 <116058905+sadaf895@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:55:10 +0530 Subject: [PATCH 01/11] Record some sample test. --- .github/workflows/playwright.yml | 27 ++ .gitignore | 4 + e2e/example.spec.ts | 18 ++ e2e/test-1.spec.ts | 5 + e2e/test-2.spec.ts | 55 ++++ package-lock.json | 73 ++++- package.json | 2 + playwright.config.ts | 79 +++++ tests-examples/demo-todo-app.spec.ts | 437 +++++++++++++++++++++++++++ 9 files changed, 696 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 e2e/example.spec.ts create mode 100644 e2e/test-1.spec.ts create mode 100644 e2e/test-2.spec.ts create mode 100644 playwright.config.ts create mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000..3eb13143c3 --- /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: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index efa61f6c69..e5008b984b 100644 --- a/.gitignore +++ b/.gitignore @@ -197,3 +197,7 @@ fabric.properties # video and screenshot of the tests from Cypress ./e2e/screenshots ./e2e/videos +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 0000000000..54a906a4e8 --- /dev/null +++ b/e2e/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/e2e/test-1.spec.ts b/e2e/test-1.spec.ts new file mode 100644 index 0000000000..f6ce62c23d --- /dev/null +++ b/e2e/test-1.spec.ts @@ -0,0 +1,5 @@ +import { test, expect } from "@playwright/test"; + +test("test", async ({ page }) => { + await page.goto("about:blank"); +}); diff --git a/e2e/test-2.spec.ts b/e2e/test-2.spec.ts new file mode 100644 index 0000000000..cc5959899e --- /dev/null +++ b/e2e/test-2.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from "@playwright/test"; + +test("test", async ({ page }) => { + await page.goto("https://demo.playwright.dev/todomvc/#/"); + await page.getByPlaceholder("What needs to be done?").click(); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("D"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page + .getByPlaceholder("What needs to be done?") + .fill("Do the breakfast"); + await page.getByPlaceholder("What needs to be done?").press("Enter"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("P"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("Plant the water"); + await page.getByPlaceholder("What needs to be done?").press("Enter"); + await page.getByPlaceholder("What needs to be done?").fill(""); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("D"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("Do some "); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("Do some U"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page + .getByPlaceholder("What needs to be done?") + .fill("Do some University task"); + await page.getByPlaceholder("What needs to be done?").press("Enter"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("P"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill(""); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("F"); + await page.getByPlaceholder("What needs to be done?").press("CapsLock"); + await page.getByPlaceholder("What needs to be done?").fill("Feed the pets"); + await page.getByPlaceholder("What needs to be done?").press("Enter"); + await page.goto("https://demo.playwright.dev/todomvc/#/"); + + await page + .locator("li") + .filter({ hasText: "Do the breakfast" }) + .getByLabel("Toggle Todo") + .check(); + await page + .locator("li") + .filter({ hasText: "Plant the water" }) + .getByLabel("Toggle Todo") + .check(); + await page.getByRole("link", { name: "Active" }).click(); + await expect(page.getByText("Do the breakfast")).toHaveCount(0); + await page.getByRole("link", { name: "Completed" }).click(); + await expect(page.getByText("Plant the water")).toHaveCount(1); +}); diff --git a/package-lock.json b/package-lock.json index ba9aeec130..0c363201e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "@compodoc/compodoc": "^1.1.26", "@cypress/schematic": "~2.5.2", "@percy/cli": "^1.30.1", + "@playwright/test": "^1.48.2", "@schematics/angular": "^18.2.9", "@storybook/addon-actions": "^8.3.6", "@storybook/addon-essentials": "^8.3.6", @@ -82,6 +83,7 @@ "@types/leaflet": "^1.9.13", "@types/lodash-es": "^4.17.12", "@types/md5": "^2.3.5", + "@types/node": "^22.8.5", "@types/pouchdb": "^6.4.2", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.11.0", @@ -5767,6 +5769,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", @@ -7428,12 +7446,12 @@ } }, "node_modules/@types/node": { - "version": "22.7.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.8.tgz", - "integrity": "sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==", + "version": "22.8.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", + "integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/node-forge": { @@ -18977,6 +18995,53 @@ "pathe": "^1.1.2" } }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", diff --git a/package.json b/package.json index 1eb6e9a9f7..0615cbe6d2 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@compodoc/compodoc": "^1.1.26", "@cypress/schematic": "~2.5.2", "@percy/cli": "^1.30.1", + "@playwright/test": "^1.48.2", "@schematics/angular": "^18.2.9", "@storybook/addon-actions": "^8.3.6", "@storybook/addon-essentials": "^8.3.6", @@ -96,6 +97,7 @@ "@types/leaflet": "^1.9.13", "@types/lodash-es": "^4.17.12", "@types/md5": "^2.3.5", + "@types/node": "^22.8.5", "@types/pouchdb": "^6.4.2", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.11.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..efd9b88be0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* 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://127.0.0.1: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: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000000..8641cb5f5d --- /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); +} From 72db8ba2bbe4e35e309121ca0d96b1cb882a1db3 Mon Sep 17 00:00:00 2001 From: sadaf895 <116058905+sadaf895@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:27:51 +0530 Subject: [PATCH 02/11] test(e2e): add common scenarios for end-to-end testing - Added coverage for navigation and content checks on Attendance page - Verified elements load correctly and interactions behave as expected - Ensured handling of edge cases like NaN values in counts --- e2e/example.spec.ts | 18 ++--- e2e/test-1.spec.ts | 159 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 16 deletions(-) diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts index 54a906a4e8..c76dd59933 100644 --- a/e2e/example.spec.ts +++ b/e2e/example.spec.ts @@ -1,18 +1,8 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); +test("has title", async ({ page }) => { + await page.goto("http://localhost:4200/"); // 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(); + await expect(page).toHaveTitle(/Aam Digital - Demo/); }); diff --git a/e2e/test-1.spec.ts b/e2e/test-1.spec.ts index f6ce62c23d..bbbe210dcc 100644 --- a/e2e/test-1.spec.ts +++ b/e2e/test-1.spec.ts @@ -1,5 +1,160 @@ import { test, expect } from "@playwright/test"; -test("test", async ({ page }) => { - await page.goto("about:blank"); +test.describe("Dashboard Tests", () => { + test("should display Quick Actions widget on dashboard", async ({ page }) => { + // Go to the dashboard page + await page.goto("http://localhost:4200"); + + // Wait for "Quick Actions" element to load + await page.waitForSelector("text=Quick actions"); + + // Check if "Quick Actions" element is visible + const quickActionsElement = page.locator("text=Quick actions"); + await expect(quickActionsElement).toBeVisible(); + + // Verify that Record Attendence button exists in Quick actions widget + const recordAttendeceButton = page.getByRole("cell", { + name: "Record Attendance", + }); + await expect(recordAttendeceButton).toBeVisible(); + + // Verify children count is displayed + const childrenCount = page.locator("app-entity-count-dashboard-widget"); + await expect(childrenCount).toContainText(/120/); + }); + + test("should navigate to the attendence page", async ({ page }) => { + await page.goto("http://localhost:4200/"); + + await page.click("text=Attendance"); + + // Check if the URL contains "attendance" + await expect(page).toHaveURL(/.*attendance/); + }); + + test("should display tasks due with correct due dates", async ({ page }) => { + // Go to the dashboard page + await page.goto("http://localhost:4200"); + + await page.waitForSelector("text=Tasks due"); + + // Check for the "Tasks due" widget + const tasksDueElement = page.locator("text=Tasks due"); + await expect(tasksDueElement).toBeVisible(); + + // Verify that the task names and due dates are displayed + const taskNames = await page + .locator("app-widget-content") + .filter({ hasText: "get signed agreement Nov 5," }) + .allTextContents(); + const dueDates = await page + .locator("app-widget-content") + .filter({ hasText: "get signed agreement Nov 5," }) + .allTextContents(); + + expect(taskNames.length).toBeGreaterThan(0); // Ensure there is at least one task + expect(dueDates.length).toBe(taskNames.length); // Ensure every task has a due date + }); + + test("should display absence this week", async ({ page }) => { + await page.goto("http://localhost:4200"); + + await page.waitForSelector("text=Absences this week"); + + const absencesThisWeekElement = page.locator("text=Absences this week"); + await expect(absencesThisWeekElement).toBeVisible(); + + const absenceCountText = await page + .getByText("4 Absences this week") + .innerText(); + const absenceCount = parseInt(absenceCountText.split(" ")[0], 10); + expect(absenceCount).toBeGreaterThanOrEqual(0); + }); + + test("Attendance page elements should be visible and clickable with navigation checks", async ({ + page, + }) => { + // Navigate to the Attendance page + await page.goto("http://localhost:4200/attendance"); + + // Verify that "Record Attendance" section is visible + await page.waitForSelector("text=Managing Attendance"); + const recordAttendanceSection = page.getByText("Record Attendance", { + exact: true, + }); + await expect(recordAttendanceSection).toBeVisible(); + + // Verify that "Recurring Activities" section is visible + const recurringActivitiesSection = page.getByText("Recurring Activities", { + exact: true, + }); + await expect(recurringActivitiesSection).toBeVisible(); + + // Verify that "Monthly Attendance" section is visible + const monthlyAttendanceSection = page.getByText("Monthly Attendance"); + await expect(monthlyAttendanceSection).toBeVisible(); + + // Click the "Record" button in the "Record Attendance" section and check navigation + const recordButton = page.getByRole("button", { name: "Record" }); + await expect(recordButton).toBeVisible(); + await recordButton.click(); + + // Check if navigation to "Record Attendance" page was successful + await expect(page).toHaveURL("http://localhost:4200/attendance/add-day"); + await expect( + page.getByRole("heading", { name: "Record Attendance" }), + ).toHaveText("Record Attendance"); // Check page title or header + await page.goBack(); // Return to Attendance page to continue testing + + // Click the "Manage Activities" button in the "Recurring Activities" section and check navigation + const manageActivitiesButton = page.getByRole("button", { + name: "Manage Activities", + }); + await expect(manageActivitiesButton).toBeVisible(); + await manageActivitiesButton.click(); + + // Check if navigation to "Manage Activities" page was successful + await expect(page).toHaveURL( + "http://localhost:4200/attendance/recurring-activity", + ); + await expect( + page.getByRole("heading", { name: "Recurring Activities" }), + ).toHaveText("Recurring Activities"); // Check page title or header + await page.goBack(); // Return to Attendance page to continue testing + }); + + test('Navigation to "Recurring Activities" page and verify elements', async ({ + page, + }) => { + // Navigate to the Attendance page + await page.goto("http://localhost:4200/attendance/recurring-activity"); + + await page.waitForSelector("text=Recurring Activities"); + + await expect( + page.getByRole("heading", { name: "Recurring Activities" }), + ).toHaveText("Recurring Activities"); // Confirm page header + + // Check for the "Add New" button on the page + const addNewButton = page.getByRole("button", { + name: "add elementAdd New", + }); + await expect(addNewButton).toBeVisible(); + + // Verify table columns are visible + await expect(page.locator("text=Title")).toBeVisible(); + await expect(page.locator("text=Type")).toBeVisible(); + await expect(page.locator("text=Assigned user(s)")).toBeVisible(); + + // Verify some specific table rows content (replace with exact selectors or text as needed) + await expect(page.locator("text=Coaching Class 2F")).toBeVisible(); + await expect(page.locator("text=School Class 4J")).toBeVisible(); + + // Verify pagination controls are visible + await expect(page.locator("text=Items per page")).toBeVisible(); + + // Verify "Include archived records" toggle + const archivedRecordsToggle = page.locator("text=Include archived records"); + await expect(archivedRecordsToggle).toBeVisible(); + }); }); From 3d931d11af5d9f0392f8257318a7af662c575eff Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Fri, 15 Nov 2024 12:25:13 +0100 Subject: [PATCH 03/11] refactor: restructured e2e tests and removed cypress --- angular.json | 21 - e2e/fixtures/example.json | 5 - .../HidingFilterOnSmallScreen.cy.ts | 21 - e2e/integration/LinkingChildToSchool.cy.ts | 49 - .../RecordingAttendanceOfChild.cy.ts | 35 - e2e/plugins/index.ts | 3 - e2e/support/commands.ts | 63 -- e2e/support/index.ts | 17 - e2e/tests-examples/demo-todo-app.spec.ts | 489 ++++++++++ .../check-browser-title.spec.ts} | 0 e2e/{ => tests}/test-1.spec.ts | 0 e2e/{ => tests}/test-2.spec.ts | 0 e2e/tsconfig.json | 14 - package-lock.json | 873 ++---------------- package.json | 8 +- playwright.config.ts | 30 +- tests-examples/demo-todo-app.spec.ts | 437 --------- tsconfig.json | 4 +- 18 files changed, 563 insertions(+), 1506 deletions(-) delete mode 100644 e2e/fixtures/example.json delete mode 100644 e2e/integration/HidingFilterOnSmallScreen.cy.ts delete mode 100644 e2e/integration/LinkingChildToSchool.cy.ts delete mode 100644 e2e/integration/RecordingAttendanceOfChild.cy.ts delete mode 100644 e2e/plugins/index.ts delete mode 100644 e2e/support/commands.ts delete mode 100644 e2e/support/index.ts create mode 100644 e2e/tests-examples/demo-todo-app.spec.ts rename e2e/{example.spec.ts => tests/check-browser-title.spec.ts} (100%) rename e2e/{ => tests}/test-1.spec.ts (100%) rename e2e/{ => tests}/test-2.spec.ts (100%) delete mode 100644 e2e/tsconfig.json delete mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/angular.json b/angular.json index b55ae9bd8a..0c00e5be88 100644 --- a/angular.json +++ b/angular.json @@ -157,27 +157,6 @@ "src/**/*.html" ] } - }, - "cypress-open": { - "builder": "@cypress/schematic:cypress", - "options": { - "watch": true, - "headless": false, - "devServerTarget": "ndb-core:serve" - } - }, - "e2e": { - "builder": "@cypress/schematic:cypress", - "options": { - "devServerTarget": "ndb-core:serve", - "watch": false, - "headless": true - }, - "configurations": { - "production": { - "devServerTarget": "ndb-core:serve:production" - } - } } } } diff --git a/e2e/fixtures/example.json b/e2e/fixtures/example.json deleted file mode 100644 index 02e4254378..0000000000 --- a/e2e/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/e2e/integration/HidingFilterOnSmallScreen.cy.ts b/e2e/integration/HidingFilterOnSmallScreen.cy.ts deleted file mode 100644 index 339312019c..0000000000 --- a/e2e/integration/HidingFilterOnSmallScreen.cy.ts +++ /dev/null @@ -1,21 +0,0 @@ -describe("When on a big screen, the filter component above the entity list should be visible", () => { - before("GIVEN that the screen is big", () => { - cy.visit("child"); - cy.viewport("macbook-15"); - }); - - it("THEN I should see filter options on a list page", () => { - cy.get("app-filter").should("be.visible"); - }); -}); - -describe("When on a small screen, the filter component above the entity list should not be visible", () => { - before("GIVEN that the screen is small", () => { - cy.visit("child"); - cy.viewport("iphone-6"); - }); - - it("THEN I should not see filter options on a list page", () => { - cy.get("app-filter").should("not.exist"); - }); -}); diff --git a/e2e/integration/LinkingChildToSchool.cy.ts b/e2e/integration/LinkingChildToSchool.cy.ts deleted file mode 100644 index fe91cb1176..0000000000 --- a/e2e/integration/LinkingChildToSchool.cy.ts +++ /dev/null @@ -1,49 +0,0 @@ -describe("Scenario: Linking a child to a school - E2E test", function () { - before("GIVEN I am on the details page of a child", function () { - cy.visit(""); - cy.create("Children", "E2E Child"); - cy.create("Schools", "E2E School"); - cy.get("[ng-reflect-angulartics-label=Children]").click(); - }); - - it("WHEN I add an entry in the 'Previous Schools' section with a specific school", function () { - // type to the input "Filter" the name of child - cy.get('[placeholder="e.g. name, age"]').type("E2E Child"); - - // Click on the Child in Table list - cy.get("tbody > :nth-child(1)").click(); - - // get the Education button and click on it - cy.contains("div", "Education").should("be.visible").click(); - - // get the Add School button and click on it - cy.contains("div", "School History") - .get('[icon="plus-circle"]') - .first() - .should("be.visible") - .click(); - - // choose the school to add - cy.contains("mat-form-field", "School") - .find("[matInput]:visible") - .type("E2E School{enter}"); - - // save school in child profile - cy.contains("button", "Save").click({ force: true }); - // wait for the popup-close animation - cy.wait(100); - }); - - it("THEN I can see that child in the 'Children Overview' of the details page of this school", function () { - // Click on the school that was added to the child profile - cy.contains("span", "E2E School").click(); - // Open the students overview - cy.contains("div", "Students").should("be.visible").click(); - - // Check if student is in the school students list - cy.contains( - "app-child-school-overview > app-entity-subrecord", - "E2E Child", - ); - }); -}); diff --git a/e2e/integration/RecordingAttendanceOfChild.cy.ts b/e2e/integration/RecordingAttendanceOfChild.cy.ts deleted file mode 100644 index b9737967ef..0000000000 --- a/e2e/integration/RecordingAttendanceOfChild.cy.ts +++ /dev/null @@ -1,35 +0,0 @@ -describe("Scenario: Recording attendance of a child - E2E test", function () { - before("GIVEN A specific child is attending a specific class", function () { - cy.visit(""); - cy.get(`[ng-reflect-angulartics-label="Attendance"]`).click(); - }); - - it("WHEN I record attendance for this class", () => { - cy.contains("button", "Record").click(); - }); - - it("AND I set the attendance of the specific child to 'present'", function () { - cy.contains("mat-card", "School Class") - .eq(0) - .click({ scrollBehavior: "center" }); - cy.get(".font-size-rel").first().invoke("text").as("childName"); - cy.contains("div", "Present").click(); - cy.get('[icon="arrow-left"]').click({ force: true }); - cy.contains("button", "Save").click(); - }); - - it("THEN in the details page of this child under 'attendance' for the specific class I should see a green background for the current day", function () { - cy.get('[placeholder="Search"]') - .focus() - .type(this.childName) - .wait(1500) - .type("{downArrow}") - .type("{enter}"); - cy.get("#mat-tab-label-0-2").click(); - cy.get(".mat-calendar-body-active").should( - "have.css", - "background-color", - "rgb(200, 230, 201)", - ); - }); -}); diff --git a/e2e/plugins/index.ts b/e2e/plugins/index.ts deleted file mode 100644 index edf74d3186..0000000000 --- a/e2e/plugins/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress -// For more info, visit https://on.cypress.io/plugins-api -module.exports = (on, config) => {}; diff --git a/e2e/support/commands.ts b/e2e/support/commands.ts deleted file mode 100644 index ab0a468194..0000000000 --- a/e2e/support/commands.ts +++ /dev/null @@ -1,63 +0,0 @@ -// *********************************************** -// This example namespace declaration will help -// with Intellisense and code completion in your -// IDE or Text Editor. -// *********************************************** -declare namespace Cypress { - interface Chainable { - /** - * Create a child or school with the given name - * @param menuItem name e.g. 'Children' or 'Schools' where a entity should be created - * @param name of the entity that should be created - */ - create(menuItem: string, name: string): typeof create; - } -} - -function create(menuItem: string, name: string): void { - cy.get(`[ng-reflect-angulartics-label="${menuItem}"]`).click(); - cy.contains("button", "Add New").click(); - cy.contains("mat-label", "Name").type(name); - cy.contains("button", "Save").click(); -} - -// -// NOTE: You can use it like so: -Cypress.Commands.add("create", create); -// Overwriting default visit function to wait for index creation -Cypress.Commands.overwrite("visit", (originalFun, url, options) => { - originalFun(url, options); - // wait for demo data generation - cy.contains("button", "Continue in background", { timeout: 20000 }).should( - "exist", - ); - cy.contains("button", "Continue in background", { timeout: 20000 }).should( - "not.exist", - ); -}); - -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/e2e/support/index.ts b/e2e/support/index.ts deleted file mode 100644 index 80b80d1bf1..0000000000 --- a/e2e/support/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// When a command from ./commands is ready to use, import with `import './commands'` syntax -import "./commands"; diff --git a/e2e/tests-examples/demo-todo-app.spec.ts b/e2e/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000000..379e072ff3 --- /dev/null +++ b/e2e/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,489 @@ +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); +} diff --git a/e2e/example.spec.ts b/e2e/tests/check-browser-title.spec.ts similarity index 100% rename from e2e/example.spec.ts rename to e2e/tests/check-browser-title.spec.ts diff --git a/e2e/test-1.spec.ts b/e2e/tests/test-1.spec.ts similarity index 100% rename from e2e/test-1.spec.ts rename to e2e/tests/test-1.spec.ts diff --git a/e2e/test-2.spec.ts b/e2e/tests/test-2.spec.ts similarity index 100% rename from e2e/test-2.spec.ts rename to e2e/tests/test-2.spec.ts diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json deleted file mode 100644 index 627f03a522..0000000000 --- a/e2e/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": [ - "**/*.ts", - "../cypress.config.ts" - ], - "exclude": [], - "compilerOptions": { - "sourceMap": false, - "types": [ - "cypress" - ] - } -} diff --git a/package-lock.json b/package-lock.json index 0c363201e7..9b628e86d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,9 +69,8 @@ "@angular/compiler-cli": "^18.2.8", "@babel/core": "^7.25.8", "@compodoc/compodoc": "^1.1.26", - "@cypress/schematic": "~2.5.2", "@percy/cli": "^1.30.1", - "@playwright/test": "^1.48.2", + "@playwright/test": "1.48.2", "@schematics/angular": "^18.2.9", "@storybook/addon-actions": "^8.3.6", "@storybook/addon-essentials": "^8.3.6", @@ -89,7 +88,6 @@ "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "^8.11.0", "babel-loader": "^9.2.1", - "cypress": "~13.14.0", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", @@ -3620,102 +3618,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@cypress/request": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", - "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.0", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "6.13.0", - "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@cypress/request/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@cypress/schematic": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.2.tgz", - "integrity": "sha512-H+V3ZP3KQVOs6b49N66jioXa+rkLzszVi+Bl3jiroVTURUNMOpSa4BOrt10Pn8F57TO0Bamhch2WOk/e9cq98w==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsonc-parser": "^3.0.0", - "rxjs": "~6.6.0" - }, - "peerDependencies": { - "@angular/cli": ">=14", - "@angular/core": ">=14" - } - }, - "node_modules/@cypress/schematic/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@cypress/schematic/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", @@ -7757,20 +7659,6 @@ "@types/send": "*" } }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", - "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sizzle": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -8639,27 +8527,6 @@ "node": ">=8" } }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -8747,23 +8614,6 @@ "node": ">=4" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -8771,16 +8621,6 @@ "dev": true, "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -9104,20 +8944,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/blob-util": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", - "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -9404,16 +9230,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cachedir": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", - "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -9557,16 +9373,6 @@ "node": ">= 16" } }, - "node_modules/check-more-types": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", - "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -9669,22 +9475,6 @@ "node": ">=6.0" } }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cjs-module-lexer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", @@ -9751,22 +9541,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, "node_modules/cli-truncate": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", @@ -10013,16 +9787,6 @@ "dev": true, "license": "ISC" }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -10441,362 +10205,81 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cypress": { - "version": "13.14.2", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.2.tgz", - "integrity": "sha512-lsiQrN17vHMB2fnvxIrKLAjOr9bPwsNbPZNrWf99s4u+DVmCY6U+w7O3GGG9FvP4EUVYaDu+guWeNLiUzBrqvA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cypress/request": "^3.0.1", - "@cypress/xvfb": "^1.2.4", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.7.1", - "cachedir": "^2.3.0", - "chalk": "^4.1.0", - "check-more-types": "^2.24.0", - "cli-cursor": "^3.1.0", - "cli-table3": "~0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "enquirer": "^2.3.6", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", - "fs-extra": "^9.1.0", - "getos": "^3.2.1", - "is-ci": "^3.0.1", - "is-installed-globally": "~0.4.0", - "lazy-ass": "^1.6.0", - "listr2": "^3.8.3", - "lodash": "^4.17.21", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "semver": "^7.5.3", - "supports-color": "^8.1.1", - "tmp": "~0.2.3", - "untildify": "^4.0.0", - "yauzl": "^2.10.0" - }, - "bin": { - "cypress": "bin/cypress" - }, - "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" - } - }, - "node_modules/cypress/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cypress/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cypress/node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cypress/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/cypress/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cypress/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cypress/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cypress/node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/cypress/node_modules/listr2/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/cypress/node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cypress/node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">= 6" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cypress/node_modules/slice-ansi": { + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "bin": { + "cssesc": "bin/cssesc" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/cypress/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } + "license": "MIT" + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" }, "node_modules/cytoscape": { "version": "3.30.2", @@ -11389,8 +10872,8 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/debug": { "version": "4.3.7", @@ -12073,20 +11556,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/ent": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", @@ -12714,13 +12183,6 @@ "through": "^2.3.8" } }, - "node_modules/eventemitter2": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", - "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", - "dev": true, - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -12768,19 +12230,6 @@ "dev": true, "license": "ISC" }, - "node_modules/executable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -13129,22 +12578,6 @@ "tough-cookie": "^4.0.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -13576,21 +13009,6 @@ "node": ">= 6" } }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13770,16 +13188,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/getos": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", - "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^3.2.0" - } - }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -13856,32 +13264,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -14471,21 +13853,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/http-signature": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", - "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", @@ -14795,19 +14162,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -14908,23 +14262,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -15465,22 +14802,6 @@ ], "license": "MIT" }, - "node_modules/jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -15939,16 +15260,6 @@ "license": "MIT", "optional": true }, - "node_modules/lazy-ass": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", - "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "> 0.8" - } - }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", @@ -16414,13 +15725,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -18420,13 +17724,6 @@ "node": ">=0.10.0" } }, - "node_modules/ospath": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", - "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", - "dev": true, - "license": "MIT" - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -18859,16 +18156,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/piscina": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", @@ -19456,19 +18743,6 @@ "node": ">=6.0.0" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -19589,13 +18863,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true, - "license": "MIT" - }, "node_modules/proxy-middleware": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", @@ -20134,16 +19401,6 @@ "node": ">= 6" } }, - "node_modules/request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "throttleit": "^1.0.0" - } - }, "node_modules/request/node_modules/form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -22028,16 +21285,6 @@ "tslib": "^2" } }, - "node_modules/throttleit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", - "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -22727,16 +21974,6 @@ } } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/package.json b/package.json index 0615cbe6d2..ad35bf43a0 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "test": "ng test", "test-ci": "ng test --code-coverage --karma-config=build/karma-ci.conf.js ", "lint": "ng lint ndb-core", - "e2e": "ng e2e", - "e2e-open": "ng run ndb-core:cypress-open", + "e2e": "playwright test", + "e2e:debug": "playwright test --project chromium --headed", "percy-storybook": "npm run build-storybook && percy storybook ./storybook-static", "compodoc": "npx compodoc -c doc/compodoc_sources/.compodocrc.json", "docs:json": "compodoc -p ./tsconfig.json -e json -d .", @@ -83,9 +83,8 @@ "@angular/compiler-cli": "^18.2.8", "@babel/core": "^7.25.8", "@compodoc/compodoc": "^1.1.26", - "@cypress/schematic": "~2.5.2", "@percy/cli": "^1.30.1", - "@playwright/test": "^1.48.2", + "@playwright/test": "1.48.2", "@schematics/angular": "^18.2.9", "@storybook/addon-actions": "^8.3.6", "@storybook/addon-essentials": "^8.3.6", @@ -103,7 +102,6 @@ "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "^8.11.0", "babel-loader": "^9.2.1", - "cypress": "~13.14.0", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", diff --git a/playwright.config.ts b/playwright.config.ts index efd9b88be0..c7a50f500c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -12,7 +12,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './e2e', + testDir: "./e2e/tests", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -22,31 +22,31 @@ export default defineConfig({ /* 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', + 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://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ @@ -71,9 +71,9 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: "npm run start", + url: "http://localhost:4200", + reuseExistingServer: !process.env.CI, + }, }); diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 8641cb5f5d..0000000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -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); -} diff --git a/tsconfig.json b/tsconfig.json index 10744c2068..21a9b25dc6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,9 +49,7 @@ } }, "exclude": [ - "e2e", - "./cypress.config.ts", - "**/*.cy.tsx" + "e2e" ], "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, From 2e7ce02c60f0f2a539c601240f4e4a1d6581c620 Mon Sep 17 00:00:00 2001 From: sadaf895 <116058905+sadaf895@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:16:52 +0530 Subject: [PATCH 04/11] Updated some tests --- e2e/tests/test-1.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e/tests/test-1.spec.ts b/e2e/tests/test-1.spec.ts index bbbe210dcc..9710ca6f12 100644 --- a/e2e/tests/test-1.spec.ts +++ b/e2e/tests/test-1.spec.ts @@ -20,7 +20,8 @@ test.describe("Dashboard Tests", () => { // Verify children count is displayed const childrenCount = page.locator("app-entity-count-dashboard-widget"); - await expect(childrenCount).toContainText(/120/); + + await expect(childrenCount).toContainText(/107/); }); test("should navigate to the attendence page", async ({ page }) => { @@ -40,16 +41,17 @@ test.describe("Dashboard Tests", () => { // Check for the "Tasks due" widget const tasksDueElement = page.locator("text=Tasks due"); + await expect(tasksDueElement).toBeVisible(); // Verify that the task names and due dates are displayed const taskNames = await page .locator("app-widget-content") - .filter({ hasText: "get signed agreement Nov 5," }) + .filter({ hasText: "get signed agreement Nov 19," }) .allTextContents(); const dueDates = await page .locator("app-widget-content") - .filter({ hasText: "get signed agreement Nov 5," }) + .filter({ hasText: "get signed agreement Nov 19," }) .allTextContents(); expect(taskNames.length).toBeGreaterThan(0); // Ensure there is at least one task From 45f44cd4d4b1eb5cd1ff71ea475274f7ca612b2d Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Wed, 27 Nov 2024 09:52:35 +0100 Subject: [PATCH 05/11] test: remove test template --- e2e/tests/test-2.spec.ts | 55 ------------------- .../entity-actions-menu.component.spec.ts | 2 +- 2 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 e2e/tests/test-2.spec.ts diff --git a/e2e/tests/test-2.spec.ts b/e2e/tests/test-2.spec.ts deleted file mode 100644 index cc5959899e..0000000000 --- a/e2e/tests/test-2.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("test", async ({ page }) => { - await page.goto("https://demo.playwright.dev/todomvc/#/"); - await page.getByPlaceholder("What needs to be done?").click(); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("D"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page - .getByPlaceholder("What needs to be done?") - .fill("Do the breakfast"); - await page.getByPlaceholder("What needs to be done?").press("Enter"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("P"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("Plant the water"); - await page.getByPlaceholder("What needs to be done?").press("Enter"); - await page.getByPlaceholder("What needs to be done?").fill(""); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("D"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("Do some "); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("Do some U"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page - .getByPlaceholder("What needs to be done?") - .fill("Do some University task"); - await page.getByPlaceholder("What needs to be done?").press("Enter"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("P"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill(""); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("F"); - await page.getByPlaceholder("What needs to be done?").press("CapsLock"); - await page.getByPlaceholder("What needs to be done?").fill("Feed the pets"); - await page.getByPlaceholder("What needs to be done?").press("Enter"); - await page.goto("https://demo.playwright.dev/todomvc/#/"); - - await page - .locator("li") - .filter({ hasText: "Do the breakfast" }) - .getByLabel("Toggle Todo") - .check(); - await page - .locator("li") - .filter({ hasText: "Plant the water" }) - .getByLabel("Toggle Todo") - .check(); - await page.getByRole("link", { name: "Active" }).click(); - await expect(page.getByText("Do the breakfast")).toHaveCount(0); - await page.getByRole("link", { name: "Completed" }).click(); - await expect(page.getByText("Plant the water")).toHaveCount(1); -}); diff --git a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts index e45398ff17..4e53c3343d 100644 --- a/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts +++ b/src/app/core/entity-details/entity-actions-menu/entity-actions-menu.component.spec.ts @@ -42,7 +42,7 @@ describe("EntityActionsMenuComponent", () => { actionsService.registerActions([testAction]); component.entity = new Entity(); - let actionEvent; + let actionEvent: any; component.actionTriggered.subscribe((x) => (actionEvent = x)); component.ngOnChanges({ entity: { currentValue: {} } as any }); From 8db3abb2fe5b3d6ab87c7d857876f9761f55176c Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Wed, 27 Nov 2024 09:52:58 +0100 Subject: [PATCH 06/11] test: rename dashboard tests --- e2e/tests/{test-1.spec.ts => dashboard-tests.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename e2e/tests/{test-1.spec.ts => dashboard-tests.spec.ts} (100%) diff --git a/e2e/tests/test-1.spec.ts b/e2e/tests/dashboard-tests.spec.ts similarity index 100% rename from e2e/tests/test-1.spec.ts rename to e2e/tests/dashboard-tests.spec.ts From 547a9154f384b9df55e20b91daf6e45a8c3c5afd Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Wed, 27 Nov 2024 09:53:31 +0100 Subject: [PATCH 07/11] fix: typo --- e2e/tests/dashboard-tests.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/tests/dashboard-tests.spec.ts b/e2e/tests/dashboard-tests.spec.ts index 9710ca6f12..f5e89d46e6 100644 --- a/e2e/tests/dashboard-tests.spec.ts +++ b/e2e/tests/dashboard-tests.spec.ts @@ -12,11 +12,11 @@ test.describe("Dashboard Tests", () => { const quickActionsElement = page.locator("text=Quick actions"); await expect(quickActionsElement).toBeVisible(); - // Verify that Record Attendence button exists in Quick actions widget - const recordAttendeceButton = page.getByRole("cell", { + // Verify that Record Attendance button exists in Quick actions widget + const recordAttendanceButton = page.getByRole("cell", { name: "Record Attendance", }); - await expect(recordAttendeceButton).toBeVisible(); + await expect(recordAttendanceButton).toBeVisible(); // Verify children count is displayed const childrenCount = page.locator("app-entity-count-dashboard-widget"); From 91d371d744133e5ea4292dbcbb684481d0d72e88 Mon Sep 17 00:00:00 2001 From: Abu Sadaf Date: Thu, 19 Dec 2024 21:42:41 +0530 Subject: [PATCH 08/11] test: for attendance scenarios --- e2e/tests/attendance-tests.spec.ts | 106 +++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 e2e/tests/attendance-tests.spec.ts diff --git a/e2e/tests/attendance-tests.spec.ts b/e2e/tests/attendance-tests.spec.ts new file mode 100644 index 0000000000..4520ea4fbe --- /dev/null +++ b/e2e/tests/attendance-tests.spec.ts @@ -0,0 +1,106 @@ +import { test, expect } from '@playwright/test'; + +test.describe.configure({ timeout: 120000 }); + +test('test', async ({ page }) => { + await page.goto('http://localhost:4200/'); + await page.getByRole('cell', { name: 'Record Attendance' }).click(); + + // Verify the date field displays the current date + const currentDate = new Date(); + const formattedDate = `${(currentDate.getMonth() + 1).toString().padStart(2, '0')}/` + + `${currentDate.getDate().toString().padStart(2, '0')}/` + + `${currentDate.getFullYear()}`; + const dateField = page.getByLabel('Date') + await expect(dateField).toHaveValue(formattedDate); + + // Verify backdated editing is allowed + const backdatedDate = '12/15/2024'; + await dateField.fill(backdatedDate); + await expect(dateField).toHaveValue(backdatedDate); + + // Verify list of classes is displayed + const classList = page.locator('mat-card-header'); + const count = await classList.count(); + + expect(count).toBeGreaterThan(0); + + const firstClass = page.locator('mat-card-header').first(); + await firstClass.evaluate((el: HTMLElement) => el?.click()); // Force the DOM click + + let totalParticipants = 0; + + const pageIndicator = page.locator('text=/\\d+\\s\\/\\s\\d+/'); // "1 / 5" pattern + // Extract total participants from "x / y" format + if (await pageIndicator.isVisible()) { + const text = await pageIndicator.textContent(); + const match = text?.match(/(\d+)\s\/\s(\d+)/); // Extract current/total numbers + if (match) { + totalParticipants = parseInt(match[2]); // Total participants + } + } + + // Iterate through participants and mark attendance till the "Review Details" button is visible + const ReviewDetailsBtn = page.getByRole('button', { name: 'Review Details' }); + + // Loop to mark attendance dynamically until the "Review Details" button is visible + while (!(await ReviewDetailsBtn.isVisible())) { + // Randomly select an attendance option using the respective locators + const randomIndex = Math.floor(Math.random() * 4); // Randomly select index 0 to 3 + const attendanceButton = page.getByRole('paragraph').nth(randomIndex); + + // Ensure the selected button is visible and enabled + await expect(attendanceButton).toBeVisible(); + await expect(attendanceButton).toBeEnabled(); + + // Click the selected attendance button + await attendanceButton.click(); + + // Wait for the next participant to load + await page.waitForTimeout(1000); + } + + // Verify the "Review Details" button is visible and click it + await expect(ReviewDetailsBtn).toBeVisible(); + await ReviewDetailsBtn.click(); + + // Verify the status are editable and can be updated + const EditStatusBtn = page.locator('app-attendance-status-select > .mat-mdc-form-field > .mat-mdc-text-field-wrapper > .mat-mdc-form-field-flex').first(); + await expect(EditStatusBtn).toBeVisible(); + await EditStatusBtn.click(); + + const UpdateStatus = page.getByRole('option', { name: 'Present' }); + await expect(UpdateStatus).toBeVisible(); + await UpdateStatus.click(); + + // Verify remarks are editable + const RemarksField = page.locator('td:nth-child(4) > .mat-mdc-form-field input').first(); + await expect(RemarksField).toBeVisible(); + await RemarksField.click(); + await RemarksField.fill('Remarks updated'); + + // Verify the details are saved + await page.getByRole('button', { name: 'Save' }).click(); + + const BackOverviewBtn = page.getByRole('button', { name: 'Back to Overview' }); + await expect(BackOverviewBtn).toBeVisible(); // "Back to Overview" button is visible + await BackOverviewBtn.click(); // "Back to Overview" button is clickable + + await page.getByRole('button', { name: 'Show more' }).click(); + + // Verify the class just recorded attendance should be highlighted in green and all others in orange + const classCard = page.locator('mat-card'); + const classCardCount = await classCard.count(); + for (let i = 0; i < classCardCount; i++) { + const card = page.locator('mat-card').nth(i); + + // Extract the border color + const borderColor = await card.evaluate((el) => getComputedStyle(el).borderLeftColor); + + if (i === 0) { + expect(borderColor).toBe('rgb(76, 175, 80)'); // Green + } else { + expect(borderColor).toBe('rgb(255, 152, 0)'); // Orange + } + } +}); diff --git a/package-lock.json b/package-lock.json index 9b628e86d9..d9dd5b71eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,7 @@ "@babel/core": "^7.25.8", "@compodoc/compodoc": "^1.1.26", "@percy/cli": "^1.30.1", - "@playwright/test": "1.48.2", + "@playwright/test": "^1.48.2", "@schematics/angular": "^18.2.9", "@storybook/addon-actions": "^8.3.6", "@storybook/addon-essentials": "^8.3.6", diff --git a/package.json b/package.json index ad35bf43a0..319a88885e 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@babel/core": "^7.25.8", "@compodoc/compodoc": "^1.1.26", "@percy/cli": "^1.30.1", - "@playwright/test": "1.48.2", + "@playwright/test": "^1.48.2", "@schematics/angular": "^18.2.9", "@storybook/addon-actions": "^8.3.6", "@storybook/addon-essentials": "^8.3.6", From b5043db1b4c82b3d2f0548224f184f36690daa99 Mon Sep 17 00:00:00 2001 From: Abu Sadaf Date: Wed, 25 Dec 2024 13:57:42 +0530 Subject: [PATCH 09/11] Implemented new test case --- e2e/tests/attendance-tests.spec.ts | 39 +++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/e2e/tests/attendance-tests.spec.ts b/e2e/tests/attendance-tests.spec.ts index 4520ea4fbe..0387c91680 100644 --- a/e2e/tests/attendance-tests.spec.ts +++ b/e2e/tests/attendance-tests.spec.ts @@ -69,9 +69,15 @@ test('test', async ({ page }) => { await expect(EditStatusBtn).toBeVisible(); await EditStatusBtn.click(); - const UpdateStatus = page.getByRole('option', { name: 'Present' }); - await expect(UpdateStatus).toBeVisible(); - await UpdateStatus.click(); + const statusOptions = page.getByText('Present Absent Late Excused'); + const statusCount = await statusOptions.count(); + + // Pick a random status option + const randomIndex = Math.floor(Math.random() * statusCount); + const selectedStatus = statusOptions.nth(randomIndex); + + await expect(selectedStatus).toBeVisible(); + await selectedStatus.click(); // Verify remarks are editable const RemarksField = page.locator('td:nth-child(4) > .mat-mdc-form-field input').first(); @@ -103,4 +109,31 @@ test('test', async ({ page }) => { expect(borderColor).toBe('rgb(255, 152, 0)'); // Orange } } + + // Attendance Overview + await firstClass.evaluate((el: HTMLElement) => el?.click()); // Force the DOM click + + const firstStudentName = await page.locator('app-entity-block').first().textContent(); // Get the first student name + if (!firstStudentName) throw new Error('First student name could not be retrieved.'); // Handle error + + await page.locator('mat-list-item').filter({ hasText: 'Children' }).click(); + await page.locator('div').filter({ hasText: 'Filter' }).nth(4).click(); + await page.getByPlaceholder('e.g. name, age').fill(firstStudentName); + await page.getByRole('cell', { name: firstStudentName }).click(); + await page.locator('span').filter({ hasText: 'Attendance' }).nth(2).click(); + + // Verify attendance report + await page.locator('mat-list-item').filter({ hasText: 'Reports' }).click(); + await page.locator('div').filter({ hasText: 'Select Report' }).nth(4).click(); + await page.getByRole('option', { name: 'Attendance Report' }).click(); + await page.getByLabel('Open calendar').click(); + await page.getByLabel('December 2,').click(); + await page.getByLabel('December 23,').click(); + await page.getByRole('button', { name: 'Calculate' }).click(); + + // Attendace Percentage + await page.locator('mat-list-item').filter({ hasText: 'Children' }).click(); + await page.getByRole('tab', { name: 'School Info' }).click(); + + }); From 6edaddb31c21edc73ed88347687af95679076166 Mon Sep 17 00:00:00 2001 From: Abu Sadaf Date: Fri, 27 Dec 2024 19:46:40 +0530 Subject: [PATCH 10/11] test for attendance report and attendance percantage --- e2e/tests/attendance-tests.spec.ts | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/e2e/tests/attendance-tests.spec.ts b/e2e/tests/attendance-tests.spec.ts index 0387c91680..e7538685bc 100644 --- a/e2e/tests/attendance-tests.spec.ts +++ b/e2e/tests/attendance-tests.spec.ts @@ -131,9 +131,50 @@ test('test', async ({ page }) => { await page.getByLabel('December 23,').click(); await page.getByRole('button', { name: 'Calculate' }).click(); + const childNameColumn = page.getByRole('columnheader', { name: 'Name' }); + await expect(childNameColumn).toBeVisible(); //Children list is visible + + const classColumn = page.getByRole('columnheader', { name: 'Class' }); + await expect(classColumn).toBeVisible(); //Class they are in is visible + + const schoolName = page.getByRole('columnheader', { name: 'School' }); + await expect(schoolName).toBeVisible(); //School name is visible + + const totalDays = page.getByRole('columnheader', { name: 'Total' }); + await expect(totalDays).toBeVisible(); //Total no. of days is visible + + const presentDays = page.getByRole('columnheader', { name: 'Present' }); + await expect(presentDays).toBeVisible(); //No. of days present is visible + + const PresentRate = page.getByRole('columnheader', { name: 'Rate' }); + await expect(PresentRate).toBeVisible(); //Present rate is visible + + const LateDays = page.getByRole('columnheader', { name: 'Late' }); + await expect(LateDays).toBeVisible(); //No. of days late is visible + // Attendace Percentage await page.locator('mat-list-item').filter({ hasText: 'Children' }).click(); await page.getByRole('tab', { name: 'School Info' }).click(); - + const CoachingAttendance = page.getByRole('columnheader', { name: 'Attendance (Coaching)' }).locator('app-entity-field-label'); + const rowCount = await CoachingAttendance.count(); + + for (let i = 0; i < rowCount; i++) { + const cell = CoachingAttendance.nth(i); + + // Get the percentage value as text + const percentageText = await cell.textContent(); + const percentage = parseInt(percentageText?.replace('%', '').trim() || '0', 10); + + if(percentage >=80){ + const color = await cell.evaluate((el) => window.getComputedStyle(el).backgroundColor); + expect(color).toBe('rgb(144, 238, 144)'); // Green highlight + } else if (percentage >= 60 && percentage < 80) { + const color = await cell.evaluate((el) => window.getComputedStyle(el).backgroundColor); + expect(color).toBe('rgb(255, 165, 0)'); // Orange highlight + } else if (percentage < 60) { + const color = await cell.evaluate((el) => window.getComputedStyle(el).backgroundColor); + expect(color).toBe('rgb(255, 99, 71)'); // Red highlight + } + } }); From 2eb7a5fcbe23f832f1babfe9b6bb5b4e26f91519 Mon Sep 17 00:00:00 2001 From: Abu Sadaf Date: Tue, 31 Dec 2024 21:21:31 +0530 Subject: [PATCH 11/11] test: restructure dashboard test --- e2e/tests/dashboard-tests.spec.ts | 90 ++++++++----------------------- 1 file changed, 22 insertions(+), 68 deletions(-) diff --git a/e2e/tests/dashboard-tests.spec.ts b/e2e/tests/dashboard-tests.spec.ts index f5e89d46e6..7720ff33e6 100644 --- a/e2e/tests/dashboard-tests.spec.ts +++ b/e2e/tests/dashboard-tests.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; test.describe("Dashboard Tests", () => { - test("should display Quick Actions widget on dashboard", async ({ page }) => { + test('test', async ({ page }) => { // Go to the dashboard page await page.goto("http://localhost:4200"); @@ -22,62 +22,25 @@ test.describe("Dashboard Tests", () => { const childrenCount = page.locator("app-entity-count-dashboard-widget"); await expect(childrenCount).toContainText(/107/); - }); - - test("should navigate to the attendence page", async ({ page }) => { - await page.goto("http://localhost:4200/"); - - await page.click("text=Attendance"); - - // Check if the URL contains "attendance" - await expect(page).toHaveURL(/.*attendance/); - }); - - test("should display tasks due with correct due dates", async ({ page }) => { - // Go to the dashboard page - await page.goto("http://localhost:4200"); + // Verify that the "Tasks due" widget is visible and display task await page.waitForSelector("text=Tasks due"); - - // Check for the "Tasks due" widget + + // Check that the "Tasks due" widget is visible const tasksDueElement = page.locator("text=Tasks due"); - await expect(tasksDueElement).toBeVisible(); - - // Verify that the task names and due dates are displayed - const taskNames = await page - .locator("app-widget-content") - .filter({ hasText: "get signed agreement Nov 19," }) - .allTextContents(); - const dueDates = await page - .locator("app-widget-content") - .filter({ hasText: "get signed agreement Nov 19," }) - .allTextContents(); - - expect(taskNames.length).toBeGreaterThan(0); // Ensure there is at least one task - expect(dueDates.length).toBe(taskNames.length); // Ensure every task has a due date - }); - - test("should display absence this week", async ({ page }) => { - await page.goto("http://localhost:4200"); - - await page.waitForSelector("text=Absences this week"); - - const absencesThisWeekElement = page.locator("text=Absences this week"); - await expect(absencesThisWeekElement).toBeVisible(); - - const absenceCountText = await page - .getByText("4 Absences this week") - .innerText(); - const absenceCount = parseInt(absenceCountText.split(" ")[0], 10); - expect(absenceCount).toBeGreaterThanOrEqual(0); - }); - - test("Attendance page elements should be visible and clickable with navigation checks", async ({ - page, - }) => { - // Navigate to the Attendance page - await page.goto("http://localhost:4200/attendance"); + + // Locate task names and due dates dynamically + const taskElements = page.locator("app-widget-content"); + + // Ensure at least one task is listed + const taskCount = await taskElements.count(); + expect(taskCount).toBeGreaterThan(0); + + // Should navigate to the attendance page + await page.locator('mat-list-item').filter({ hasText: 'Attendance' }).click(); + // Check if the URL contains "attendance" + await expect(page).toHaveURL(/.*attendance/); // Verify that "Record Attendance" section is visible await page.waitForSelector("text=Managing Attendance"); @@ -122,16 +85,10 @@ test.describe("Dashboard Tests", () => { await expect( page.getByRole("heading", { name: "Recurring Activities" }), ).toHaveText("Recurring Activities"); // Check page title or header - await page.goBack(); // Return to Attendance page to continue testing - }); - - test('Navigation to "Recurring Activities" page and verify elements', async ({ - page, - }) => { - // Navigate to the Attendance page - await page.goto("http://localhost:4200/attendance/recurring-activity"); - - await page.waitForSelector("text=Recurring Activities"); + await page.goBack(); + + // Navigate to "Recurring Activities" page and verify elements + await page.getByRole('button', { name: 'Manage Activities' }).click(); await expect( page.getByRole("heading", { name: "Recurring Activities" }), @@ -148,15 +105,12 @@ test.describe("Dashboard Tests", () => { await expect(page.locator("text=Type")).toBeVisible(); await expect(page.locator("text=Assigned user(s)")).toBeVisible(); - // Verify some specific table rows content (replace with exact selectors or text as needed) - await expect(page.locator("text=Coaching Class 2F")).toBeVisible(); - await expect(page.locator("text=School Class 4J")).toBeVisible(); - // Verify pagination controls are visible await expect(page.locator("text=Items per page")).toBeVisible(); // Verify "Include archived records" toggle const archivedRecordsToggle = page.locator("text=Include archived records"); await expect(archivedRecordsToggle).toBeVisible(); - }); }); +}); +