From a185073f9f676cb3cdd370a4e1e27ec360c4459e Mon Sep 17 00:00:00 2001 From: Toluwalase Abigail Date: Thu, 15 Aug 2024 09:38:50 +0100 Subject: [PATCH 1/3] test automation --- .gitignore | 1 + test/.env | 1 + test/.github/workflows/playwright.yml | 27 ++ test/.gitignore | 9 + test/package-lock.json | 93 +++++ test/package.json | 10 + test/playwright.config.js | 73 ++++ test/tests-examples/demo-todo-app.spec.js | 449 ++++++++++++++++++++++ test/tests-examples/test-hng.spec.js | 64 +++ test/tests/e2e/test_footer.spec.js | 85 ++++ test/tests/e2e/test_homePage.spec.js | 13 + 11 files changed, 825 insertions(+) create mode 100644 test/.env create mode 100644 test/.github/workflows/playwright.yml create mode 100644 test/.gitignore create mode 100644 test/package-lock.json create mode 100644 test/package.json create mode 100644 test/playwright.config.js create mode 100644 test/tests-examples/demo-todo-app.spec.js create mode 100644 test/tests-examples/test-hng.spec.js create mode 100644 test/tests/e2e/test_footer.spec.js create mode 100644 test/tests/e2e/test_homePage.spec.js diff --git a/.gitignore b/.gitignore index ef23b7b..594207d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ _ignore/ app.env tmp/ .idea/ +./test/node_modules dump.rdb app.env diff --git a/test/.env b/test/.env new file mode 100644 index 0000000..ca53a6f --- /dev/null +++ b/test/.env @@ -0,0 +1 @@ +BASE_URL="https://kimiko-golang.teams.hng.tech/" \ No newline at end of file diff --git a/test/.github/workflows/playwright.yml b/test/.github/workflows/playwright.yml new file mode 100644 index 0000000..467190b --- /dev/null +++ b/test/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..f8048ca --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/test/package-lock.json b/test/package-lock.json new file mode 100644 index 0000000..0758659 --- /dev/null +++ b/test/package-lock.json @@ -0,0 +1,93 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "playwright": "^1.46.0" + }, + "devDependencies": { + "@playwright/test": "^1.46.0", + "@types/node": "^22.2.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz", + "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..878f3ce --- /dev/null +++ b/test/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "playwright": "^1.46.0" + }, + "devDependencies": { + "@playwright/test": "^1.46.0", + "@types/node": "^22.2.0" + }, + "scripts": {} +} diff --git a/test/playwright.config.js b/test/playwright.config.js new file mode 100644 index 0000000..77277d7 --- /dev/null +++ b/test/playwright.config.js @@ -0,0 +1,73 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: './tests', + /* 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: 'https://kimiko-golang.teams.hng.tech/', + + /* 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: '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/test/tests-examples/demo-todo-app.spec.js b/test/tests-examples/demo-todo-app.spec.js new file mode 100644 index 0000000..e2eb87c --- /dev/null +++ b/test/tests-examples/demo-todo-app.spec.js @@ -0,0 +1,449 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +]; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create 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) { + // 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'); + } +} + +/** + * @param {import('@playwright/test').Page} page + * @param {number} expected + */ + async function checkNumberOfTodosInLocalStorage(page, expected) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +/** + * @param {import('@playwright/test').Page} page + * @param {number} expected + */ + async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; + }, expected); +} + +/** + * @param {import('@playwright/test').Page} page + * @param {string} title + */ +async function checkTodosInLocalStorage(page, title) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); + }, title); +} diff --git a/test/tests-examples/test-hng.spec.js b/test/tests-examples/test-hng.spec.js new file mode 100644 index 0000000..256df47 --- /dev/null +++ b/test/tests-examples/test-hng.spec.js @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('https://kimiko-golang.teams.hng.tech/'); + await page + .getByRole('navigation') + .getByRole('link', { name: 'Home' }) + .click(); + await page + .locator('div') + .filter({ hasText: /^hero\.headlinehero\.descriptionhero\.cta$/ }) + .first() + .click(); + await page.locator('.flex-1 > .bg-background').click(); + await page.locator('.swiper-slide > .flex').first().click(); + await page + .locator('#swiper-wrapper-b2b95c96fbbb4fbf > div:nth-child(2) > .flex') + .click(); + await page.locator('div:nth-child(3) > .flex').first().click(); + await page + .locator('div') + .filter({ hasText: 'Find The Perfect FitChoose' }) + .nth(3) + .click(); + await page.getByRole('heading', { name: 'Find The Perfect Fit' }).click(); + await page.getByText('Choose the boilerplate plan').click(); + await page + .locator('div') + .filter({ + hasText: 'Boiler plateLogo subject details and addressSign Up For', + }) + .nth(3) + .click(); + await page.getByTestId('pricing').click(); + await page.getByTestId('pricing-tag').click(); + await page.getByText('Simple and').click(); + await page.getByText('Affordable').click(); + await page.getByText('Pricing Plan').click(); + await page.getByTestId('pricing-description').click(); + await page.getByTestId('monthly-toggle').click(); + await page.getByTestId('annual-toggle').click(); + await page + .getByRole('button', { name: 'What is the purpose of this' }) + .click(); + await page + .getByRole('button', { name: 'What is the purpose of this' }) + .click(); + await page + .getByRole('button', { name: 'How do I reset my password?' }) + .click(); + await page + .getByRole('button', { name: 'How do I reset my password?' }) + .click(); + await page + .getByRole('button', { name: 'Can I use this application on' }) + .click(); + await page + .getByRole('button', { name: 'Can I use this application on' }) + .click(); + await page + .getByRole('heading', { name: 'Frequently Asked Questions' }) + .click(); + await page.getByTestId('contact-button').click(); +}); diff --git a/test/tests/e2e/test_footer.spec.js b/test/tests/e2e/test_footer.spec.js new file mode 100644 index 0000000..12154b0 --- /dev/null +++ b/test/tests/e2e/test_footer.spec.js @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle(/HNG Boilerplate/); +}); + +test('About Us link', async ({ page }) => { + await page.goto('/'); + const aboutUsLink = page.locator('text=About Us'); + const footer = page.locator('footer'); + await aboutUsLink.click(); + await expect(footer.locator('text=About Us')).toBeVisible(); +}); + +test('FAQ link', async ({ page }) => { + await page.goto('/'); + const footer = page.locator('footer'); + await footer.scrollIntoViewIfNeeded(); + const faqLink = footer.locator('text=FAQ'); + if (await faqLink.isVisible()) { + await faqLink.click(); + await page.waitForNavigation(); + await expect(page).toHaveURL(/faqs/); + await expect(page.locator('p', { hasText: 'FAQS' })).toBeVisible(); + await expect(footer.locator('text=FAQ')).toBeVisible(); + } else { + throw new Error('FAQ link not found in the footer'); + } +}); + +test('Terms and Conditions link', async ({ page }) => { + await page.goto('/'); // Use relative path + const footer = page.locator('footer'); + await footer.scrollIntoViewIfNeeded(); + const termsLink = footer.locator('text=Terms and Condition'); + if (await termsLink.isVisible()) { + await termsLink.click(); + await page.waitForNavigation(); + await expect(page).toHaveURL(/terms-and-conditions/); + await expect( + page.locator('p', { hasText: 'Terms and Conditions'}) + ).toBeVisible(); + await expect(footer.locator('text=Terms and Conditions')).toBeVisible(); + } else { + throw new Error('Terms and Conditions link not found in the footer'); + } +}); + +test('Career link', async ({ page }) => { + await page.goto('/'); + const footer = page.locator('footer'); + await footer.scrollIntoViewIfNeeded(); + const careerLink = footer.locator('text=Career'); + if (await careerLink.isVisible()) { + await careerLink.click(); + await page.waitForNavigation(); + await expect(page).toHaveURL(/career/); + await expect( + page.locator('p').filter({ hasText: 'Career' }).first() + ).toBeVisible(); + await expect(footer.locator('text=Career')).toBeVisible(); + } else { + throw new Error('Career link not found in the footer'); + } +}); + +test('Waiting List link', async ({ page }) => { + await page.goto('/'); + const footer = page.locator('footer'); + await footer.scrollIntoViewIfNeeded(); + const waitingListLink = footer.locator('text=Waiting List'); + if (await waitingListLink.isVisible()) { + await waitingListLink.click(); + await page.waitForNavigation(); + await expect(page).toHaveURL(/waitlist/); + await expect( + page.locator('div').filter({ hasText: 'waitlist' }).first() + ).toBeVisible(); + await expect(footer.locator('text=Waiting List')).toBeVisible(); + } else { + throw new Error('Waitlist link not found in the footer'); + } +}); + diff --git a/test/tests/e2e/test_homePage.spec.js b/test/tests/e2e/test_homePage.spec.js new file mode 100644 index 0000000..a252a7d --- /dev/null +++ b/test/tests/e2e/test_homePage.spec.js @@ -0,0 +1,13 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle(/HNG Boilerplate/); +}); + +test('get started link', async ({ page }) => { + await page.goto('/'); + await page.getByRole('link', { name: 'Get Started' }).first().click(); + await expect(page.getByRole('heading', { name: 'Sign Up' })).toBeVisible(); +}); From df1f77a7ccf0180ed8c313644166b4274a96652a Mon Sep 17 00:00:00 2001 From: Toluwalase Abigail Date: Thu, 15 Aug 2024 10:15:31 +0100 Subject: [PATCH 2/3] footer --- test/package.json | 6 +++++- test/playwright.config.js | 44 --------------------------------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/test/package.json b/test/package.json index 878f3ce..fc7696a 100644 --- a/test/package.json +++ b/test/package.json @@ -6,5 +6,9 @@ "@playwright/test": "^1.46.0", "@types/node": "^22.2.0" }, - "scripts": {} + "scripts": { + "playwright install": "npm init playwright@latest", + "playwright run": "npx playwright test", + "playwright report": "npx playwright show-report" + } } diff --git a/test/playwright.config.js b/test/playwright.config.js index 77277d7..18ead2e 100644 --- a/test/playwright.config.js +++ b/test/playwright.config.js @@ -1,37 +1,20 @@ // @ts-check const { defineConfig, devices } = require('@playwright/test'); -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config({ path: path.resolve(__dirname, '.env') }); - /** * @see https://playwright.dev/docs/test-configuration */ module.exports = defineConfig({ testDir: './tests', - /* 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: 'https://kimiko-golang.teams.hng.tech/', - - /* 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', @@ -41,33 +24,6 @@ module.exports = defineConfig({ 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, - // }, }); From 9a73f4de480d703135066035fdf71ec589670325 Mon Sep 17 00:00:00 2001 From: Toluwalase Abigail Date: Thu, 15 Aug 2024 10:53:06 +0100 Subject: [PATCH 3/3] footer --- test/.env | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/.env diff --git a/test/.env b/test/.env deleted file mode 100644 index ca53a6f..0000000 --- a/test/.env +++ /dev/null @@ -1 +0,0 @@ -BASE_URL="https://kimiko-golang.teams.hng.tech/" \ No newline at end of file