├── .gitignore ├── utils ├── testbase.ts ├── testData.json └── api.ts ├── tests ├── pageTitle.spec.ts ├── updateCurrency.spec.ts ├── loginUI.spec.ts ├── hoverOver_menu.spec.ts ├── addToCart_featured.spec.ts ├── newsletter_subscription.spec.ts ├── addToCart_API.spec.ts └── wishlist_API.spec.ts ├── README.md ├── package.json ├── .github └── workflows │ └── playwright.yml ├── pageObjects ├── loginPage.ts ├── homePage.ts └── accountPage.ts ├── playwright.config.ts └── tests-examples └── demo-todo-app.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /playwright/.cache/ 5 | .env 6 | -------------------------------------------------------------------------------- /utils/testbase.ts: -------------------------------------------------------------------------------- 1 | import { test as base } from "@playwright/test"; 2 | import testData from "./testData.json"; 3 | 4 | type MyFixtures = { 5 | data: typeof testData; 6 | }; 7 | 8 | export const fixture = base.extend({ 9 | data: testData, 10 | }); 11 | -------------------------------------------------------------------------------- /utils/testData.json: -------------------------------------------------------------------------------- 1 | { 2 | "featured": "iPhone", 3 | "wishlistItem": { "product_id": 43 }, 4 | "wishlist_endpoint": "/index.php?route=account/wishlist/add", 5 | "addToCartItem": { "product_id": 43, "quantity": 1 }, 6 | "addCart_endpoint": "/index.php?route=checkout/cart/add", 7 | "addCartProductName": "MacBook", 8 | "dollar_symbol": "$" 9 | } 10 | -------------------------------------------------------------------------------- /tests/pageTitle.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("opencart has title", async ({ page }) => { 4 | await page.goto(""); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Your Store/); 8 | 9 | // Expects the URL to contain opencart 10 | await expect(page).toHaveURL(/opencart/); 11 | }); 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Playwright using Typescript 2 | 3 | I have used Open Cart by Naveen automation Labs to automate tests using Playwright. 4 | 5 | [Please click here for Reference](https://naveenautomationlabs.com/opencart/) 6 | 7 | ## Get Started 8 | 9 | - Install VS Code and Node.js 10 | - Clone this repository 11 | - Run "npm i install" to install dependencies 12 | 13 | ## Specifications: 14 | 15 | - Playwright [Version 1.35.0] 16 | - Typescript 17 | - Fixtures for test inputs 18 | - API and UI validations 19 | - User Credentials passed through Github Secrets 20 | 21 | ## CI/CD 22 | 23 | - Integrated Github Actions 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright_ts", 3 | "version": "1.0.0", 4 | "description": "A demo framework built using Playwright and Typescript.", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/SwathiVisagn123/Playwright_TS.git" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "bugs": { 15 | "url": "https://github.com/SwathiVisagn123/Playwright_TS/issues" 16 | }, 17 | "homepage": "https://github.com/SwathiVisagn123/Playwright_TS#readme", 18 | "devDependencies": { 19 | "@playwright/test": "^1.35.0" 20 | }, 21 | "dependencies": { 22 | "dotenv": "^16.3.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/api.ts: -------------------------------------------------------------------------------- 1 | import { APIRequestContext, expect } from "@playwright/test"; 2 | 3 | export class api { 4 | readonly apiContext: Promise; 5 | 6 | constructor(apiContext: Promise) { 7 | this.apiContext = apiContext; 8 | } 9 | async wishlist(productDetails: object, endpointUrl: string) { 10 | //api request to add wish list 11 | const response = await ( 12 | await this.apiContext 13 | ).post(endpointUrl, { 14 | data: productDetails, 15 | }); 16 | 17 | //assert api response 18 | expect(response.ok()).toBeTruthy(); 19 | } 20 | 21 | async addToCart(product: object, endpoint: string) { 22 | const response = await ( 23 | await this.apiContext 24 | ).post(endpoint, { 25 | data: product, 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/updateCurrency.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { fixture } from "../utils/testbase"; 3 | import { homePage } from "../pageObjects/homePage"; 4 | 5 | test.beforeEach(async ({ page }) => { 6 | //go to opencart 7 | await page.goto(""); 8 | 9 | // Expect a title "to contain" a substring. 10 | await expect(page).toHaveTitle(/Your Store/); 11 | 12 | // Expects the URL to contain opencart 13 | await expect(page).toHaveURL(/opencart/); 14 | }); 15 | 16 | fixture("update currency in home page", async ({ page, data }) => { 17 | const home = new homePage(page); 18 | 19 | await home.currencyDropdown.click(); 20 | 21 | //choose currency 22 | await home.dollar.click(); 23 | 24 | //assert change of currency 25 | await expect(home.dollarSymbol).toHaveText(data.dollar_symbol); 26 | }); 27 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [main, master] 5 | pull_request: 6 | branches: [main, master] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Install Playwright Browsers 19 | run: npx playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: EMAIL=${{secrets.EMAIL}} PASSWORD=${{secrets.PASSWORD}} npx playwright test 22 | - uses: actions/upload-artifact@v3 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /tests/loginUI.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { fixture } from "../utils/testbase"; 3 | import { loginPage } from "../pageObjects/loginPage"; 4 | import { accountPage } from "../pageObjects/accountPage"; 5 | 6 | test.beforeEach(async ({ page }) => { 7 | //go to opencart 8 | await page.goto(""); 9 | 10 | // Expect a title "to contain" a substring. 11 | await expect(page).toHaveTitle(/Your Store/); 12 | 13 | // Expects the URL to contain opencart 14 | await expect(page).toHaveURL(/opencart/); 15 | }); 16 | 17 | fixture("login through UI", async ({ page, data }) => { 18 | const login = new loginPage(page); 19 | const account = new accountPage(page); 20 | 21 | //go to Login Page 22 | await login.goToLogin(); 23 | 24 | //enter credentials 25 | await login.login(process.env.EMAIL, process.env.PASSWORD); 26 | await expect(account.accountPage).toBeVisible(); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/hoverOver_menu.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { homePage } from "../pageObjects/homePage"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | //go to opencart 6 | await page.goto(""); 7 | 8 | // Expect a title "to contain" a substring. 9 | await expect(page).toHaveTitle(/Your Store/); 10 | 11 | // Expects the URL to contain opencart 12 | await expect(page).toHaveURL(/opencart/); 13 | 14 | // 15 | }); 16 | 17 | test("hover over menu and inner menu selection", async ({ page }) => { 18 | const home = new homePage(page); 19 | //wait for page to load 20 | await page.waitForLoadState("networkidle"); 21 | 22 | //hover over menu 23 | 24 | await home.componentsMenu.hover(); 25 | 26 | //select menu for Monitors and wait for content to load 27 | 28 | await Promise.all([ 29 | page.waitForSelector("#content"), 30 | home.innerMenuMonitors.click(), 31 | ]); 32 | 33 | //confirm menu navigation 34 | await home.confirmMenuPage(); 35 | }); 36 | -------------------------------------------------------------------------------- /pageObjects/loginPage.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator, Page } from "@playwright/test"; 2 | 3 | export class loginPage { 4 | readonly page: Page; 5 | readonly accountMenu: Locator; 6 | readonly loginMenu: Locator; 7 | readonly returningCust: Locator; 8 | readonly email: Locator; 9 | readonly password: Locator; 10 | readonly submit: Locator; 11 | 12 | constructor(page: Page) { 13 | this.page = page; 14 | this.accountMenu = page.locator(".list-inline .dropdown"); 15 | this.loginMenu = page.getByRole("link", { name: "Login" }); 16 | this.returningCust = page.locator('text="Returning Customer"'); 17 | this.email = page.getByPlaceholder("E-Mail Address"); 18 | this.password = page.getByPlaceholder("Password"); 19 | this.submit = page.locator('input[type="submit"]'); 20 | } 21 | 22 | async goToLogin() { 23 | await this.page.waitForLoadState("networkidle"); 24 | await this.accountMenu.click(); 25 | await this.loginMenu.click(); 26 | await expect(this.page).toHaveURL(/login/); 27 | await expect(this.returningCust).toBeVisible(); 28 | } 29 | 30 | async login(email, password) { 31 | await this.email.type(email); 32 | await this.password.type(password); 33 | await this.submit.click(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/addToCart_featured.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { fixture } from "../utils/testbase"; 3 | import { homePage } from "../pageObjects/homePage"; 4 | 5 | test.beforeEach(async ({ page }) => { 6 | //go to opencart 7 | await page.goto(""); 8 | 9 | // Expect a title "to contain" a substring. 10 | await expect(page).toHaveTitle(/Your Store/); 11 | 12 | // Expects the URL to contain opencart 13 | await expect(page).toHaveURL(/opencart/); 14 | }); 15 | 16 | fixture("add to cart from featured", async ({ page, data }) => { 17 | const home = new homePage(page); 18 | //wait for page to load 19 | await page.waitForLoadState("networkidle"); 20 | 21 | //fetch all products under featured section 22 | const featured: string[] = await page 23 | .locator(".caption > h4") 24 | .allInnerTexts(); 25 | 26 | //get the count of featured products 27 | const count: number = await page.locator(".caption > h4").count(); 28 | 29 | //iterate and click on the particluar product 30 | for (let i = 0; i <= count; i++) { 31 | if ( 32 | (await page.locator(".caption > h4").nth(i).textContent()) === 33 | data.featured 34 | ) { 35 | await page 36 | .locator(".button-group") 37 | .nth(i) 38 | .getByRole("button") 39 | .nth(0) 40 | .click(); 41 | break; 42 | } 43 | } 44 | //validate the success message 45 | await expect(home.cart_success).toBeVisible(); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/newsletter_subscription.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@playwright/test"; 2 | import { fixture } from "../utils/testbase"; 3 | import { loginPage } from "../pageObjects/loginPage"; 4 | import { homePage } from "../pageObjects/homePage"; 5 | import { accountPage } from "../pageObjects/accountPage"; 6 | 7 | fixture.beforeEach(async ({ page, data }) => { 8 | const login = new loginPage(page); 9 | const home = new homePage(page); 10 | //go to opencart 11 | await page.goto(""); 12 | 13 | // Expect a title "to contain" a substring. 14 | await expect(page).toHaveTitle(/Your Store/); 15 | 16 | // Expects the URL to contain opencart 17 | await expect(page).toHaveURL(/opencart/); 18 | 19 | //access account menu 20 | await login.goToLogin(); 21 | 22 | //login 23 | await login.login(process.env.EMAIL, process.env.PASSWORD); 24 | }); 25 | 26 | fixture("Cancel or Add newsletter subscription", async ({ page, data }) => { 27 | const account = new accountPage(page); 28 | //wait for page to load 29 | await page.waitForLoadState("networkidle"); 30 | 31 | //assert account page url 32 | await expect(page).toHaveURL(/account/); 33 | 34 | //click newsletter menu 35 | expect(await account.newsletterHeading.isHidden()).toBeFalsy(); 36 | 37 | await account.subscription.click(); 38 | 39 | //assert newsletter page 40 | await page.waitForURL(/newsletter/); 41 | await expect(page).toHaveURL(/newsletter/); 42 | 43 | //change the subscription option 44 | 45 | await account.changeSubscription(); 46 | }); 47 | -------------------------------------------------------------------------------- /pageObjects/homePage.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator, Page } from "@playwright/test"; 2 | 3 | export class homePage { 4 | readonly page: Page; 5 | readonly cart_success: Locator; 6 | readonly componentsMenu: Locator; 7 | readonly innerMenuMonitors: Locator; 8 | readonly content: Locator; 9 | readonly confirmHeading: Locator; 10 | readonly logo: Locator; 11 | readonly wishlistIcon: Locator; 12 | readonly wishlistProducts: Locator; 13 | readonly cartIcon: Locator; 14 | readonly currencyDropdown: Locator; 15 | readonly dollar: Locator; 16 | readonly dollarSymbol: Locator; 17 | 18 | constructor(page: Page) { 19 | this.page = page; 20 | this.cart_success = page.getByText( 21 | "Success: You have added iPhone to your shopping cart! ×" 22 | ); 23 | this.componentsMenu = page.getByRole("link", { name: "Components" }); 24 | this.innerMenuMonitors = page.getByRole("link", { name: "Monitors (2)" }); 25 | this.logo = page.getByAltText("naveenopencart"); 26 | this.wishlistIcon = page.getByRole("link", { name: / Wish List/ }); 27 | this.wishlistProducts = page.locator("tbody tr"); 28 | this.cartIcon = page.getByRole("link", { name: " Shopping Cart" }); 29 | this.currencyDropdown = page.getByRole("button", { name: "$ Currency " }); 30 | this.dollar = page.getByRole("button", { name: "$ US Dollar" }); 31 | this.dollarSymbol = page.locator("strong"); 32 | } 33 | 34 | async confirmMenuPage() { 35 | await expect(this.page.locator("h2")).toContainText("Monitors"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/addToCart_API.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, request } from "@playwright/test"; 2 | import { fixture } from "../utils/testbase"; 3 | import { loginPage } from "../pageObjects/loginPage"; 4 | import { homePage } from "../pageObjects/homePage"; 5 | import { api } from "../utils/api"; 6 | 7 | fixture.beforeEach(async ({ page, data }) => { 8 | const login = new loginPage(page); 9 | const home = new homePage(page); 10 | //go to opencart 11 | await page.goto(""); 12 | 13 | // Expect a title "to contain" a substring. 14 | await expect(page).toHaveTitle(/Your Store/); 15 | 16 | // Expects the URL to contain opencart 17 | await expect(page).toHaveURL(/opencart/); 18 | 19 | //access account menu 20 | await login.goToLogin(); 21 | 22 | //login 23 | await login.login(process.env.EMAIL, process.env.PASSWORD); 24 | 25 | //go to home 26 | await home.logo.click(); 27 | }); 28 | 29 | fixture("add to cart through api", async ({ page, data }) => { 30 | const home = new homePage(page); 31 | 32 | //create a new context for api requests 33 | const apiContext = request.newContext(); 34 | const apiUtil = new api(apiContext); 35 | 36 | //wait for page to load 37 | await page.waitForLoadState("networkidle"); 38 | 39 | //add product thro' API 40 | await apiUtil.addToCart(data.addToCartItem, data.addCart_endpoint); 41 | 42 | //go to cart 43 | await home.cartIcon.click(); 44 | 45 | //assert presence of product added to cart 46 | await expect( 47 | page.getByRole("cell", { name: data.addCartProductName, exact: true }) 48 | ).toBeVisible(); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/wishlist_API.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, request } from "@playwright/test"; 2 | import { fixture } from "../utils/testbase"; 3 | import { loginPage } from "../pageObjects/loginPage"; 4 | import { homePage } from "../pageObjects/homePage"; 5 | import { api } from "../utils/api"; 6 | 7 | fixture.beforeEach(async ({ page, data }) => { 8 | const login = new loginPage(page); 9 | const home = new homePage(page); 10 | //go to opencart 11 | await page.goto(""); 12 | 13 | // Expect a title "to contain" a substring. 14 | await expect(page).toHaveTitle(/Your Store/); 15 | 16 | // Expects the URL to contain opencart 17 | await expect(page).toHaveURL(/opencart/); 18 | 19 | //access account menu 20 | await login.goToLogin(); 21 | 22 | //login 23 | await login.login(process.env.EMAIL, process.env.PASSWORD); 24 | 25 | //navigate to home page 26 | await home.logo.click(); 27 | }); 28 | 29 | fixture("add wish list item through api", async ({ page, data }) => { 30 | const home = new homePage(page); 31 | 32 | //create a new context for api requests 33 | const apiContext = request.newContext(); 34 | const apiUtil = new api(apiContext); 35 | 36 | //wait for page to load 37 | await page.waitForLoadState("networkidle"); 38 | 39 | //add wishlist item thro' API 40 | //BUG FOUND : WISHLIST API ADDS PRODUCT IN BASKET SOMETIMES OR DOESN'T WORK. INTERMITTENT 41 | await apiUtil.wishlist(data.wishlistItem, data.wishlist_endpoint); 42 | 43 | //assert UI response 44 | await home.wishlistIcon.click(); 45 | 46 | expect(await home.wishlistProducts.count()).not.toBeNull(); 47 | }); 48 | -------------------------------------------------------------------------------- /pageObjects/accountPage.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator, Page } from "@playwright/test"; 2 | 3 | export class accountPage { 4 | readonly page: Page; 5 | readonly newsletterHeading: Locator; 6 | readonly subscription: Locator; 7 | readonly yesRadio: Locator; 8 | readonly noRadio: Locator; 9 | readonly continueBtn: Locator; 10 | readonly successAlert: Locator; 11 | readonly accountBreadcrumb: Locator; 12 | readonly accountPage: Locator; 13 | 14 | constructor(page: Page) { 15 | this.page = page; 16 | this.newsletterHeading = page.getByRole("heading", { name: "Newsletter" }); 17 | this.subscription = page.getByRole("link", { 18 | name: "Subscribe / unsubscribe to newsletter", 19 | }); 20 | this.yesRadio = page.getByLabel("Yes"); 21 | this.noRadio = page.getByLabel("No"); 22 | this.continueBtn = page.locator('input[type="submit"]'); 23 | this.successAlert = page.locator(".alert.alert-success.alert-dismissible"); 24 | this.accountBreadcrumb = page.locator(".breadcrumb li", { 25 | hasText: "Account", 26 | }); 27 | this.accountPage = page 28 | .locator("#content") 29 | .getByRole("heading", { name: "My Account" }); 30 | } 31 | 32 | async changeSubscription() { 33 | if (await this.yesRadio.isChecked()) { 34 | await this.noRadio.check(); 35 | await this.continueBtn.click(); 36 | await expect(this.successAlert).toBeVisible(); 37 | 38 | //assert if no is checked 39 | await this.accountBreadcrumb.click(); 40 | await this.page.waitForLoadState("domcontentloaded"); 41 | 42 | this.subscription.click(); 43 | await this.page.waitForLoadState("domcontentloaded"); 44 | expect(await this.noRadio.isChecked()).toBeTruthy(); 45 | } else { 46 | await this.yesRadio.check(); 47 | await this.continueBtn.click(); 48 | await expect(this.successAlert).toBeVisible(); 49 | 50 | //assert if Yes is checked 51 | await this.accountBreadcrumb.click(); 52 | await this.page.waitForLoadState("domcontentloaded"); 53 | 54 | await this.subscription.click(); 55 | await this.page.waitForLoadState("domcontentloaded"); 56 | expect(await this.yesRadio.isChecked()).toBeTruthy(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | require("dotenv").config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: "./tests", 14 | timeout: 60 * 1000, 15 | expect: { 16 | timeout: 5000, 17 | }, 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: "html", 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | // baseURL: 'http://127.0.0.1:3000', 32 | 33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 34 | trace: "on-first-retry", 35 | }, 36 | 37 | /* Configure projects for major browsers */ 38 | projects: [ 39 | { 40 | name: "chromium", 41 | use: { 42 | baseURL: "https://naveenautomationlabs.com/opencart", 43 | ...devices["Desktop Chrome"], 44 | headless: true, 45 | }, 46 | }, 47 | 48 | // { 49 | // name: "firefox", 50 | // use: { ...devices["Desktop Firefox"], headless: false }, 51 | // }, 52 | 53 | // { 54 | // name: "webkit", 55 | // use: { ...devices["Desktop Safari"], headless: false }, 56 | // }, 57 | 58 | /* Test against mobile viewports. */ 59 | // { 60 | // name: 'Mobile Chrome', 61 | // use: { ...devices['Pixel 5'] }, 62 | // }, 63 | // { 64 | // name: 'Mobile Safari', 65 | // use: { ...devices['iPhone 12'] }, 66 | // }, 67 | 68 | /* Test against branded browsers. */ 69 | // { 70 | // name: 'Microsoft Edge', 71 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 72 | // }, 73 | // { 74 | // name: 'Google Chrome', 75 | // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, 76 | // }, 77 | ], 78 | 79 | /* Run your local dev server before starting the tests */ 80 | // webServer: { 81 | // command: 'npm run start', 82 | // url: 'http://127.0.0.1:3000', 83 | // reuseExistingServer: !process.env.CI, 84 | // }, 85 | }); 86 | -------------------------------------------------------------------------------- /tests-examples/demo-todo-app.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, type Page } from '@playwright/test'; 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto('https://demo.playwright.dev/todomvc'); 5 | }); 6 | 7 | const TODO_ITEMS = [ 8 | 'buy some cheese', 9 | 'feed the cat', 10 | 'book a doctors appointment' 11 | ]; 12 | 13 | test.describe('New Todo', () => { 14 | test('should allow me to add todo items', async ({ page }) => { 15 | // create a new todo locator 16 | const newTodo = page.getByPlaceholder('What needs to be done?'); 17 | 18 | // Create 1st todo. 19 | await newTodo.fill(TODO_ITEMS[0]); 20 | await newTodo.press('Enter'); 21 | 22 | // Make sure the list only has one todo item. 23 | await expect(page.getByTestId('todo-title')).toHaveText([ 24 | TODO_ITEMS[0] 25 | ]); 26 | 27 | // Create 2nd todo. 28 | await newTodo.fill(TODO_ITEMS[1]); 29 | await newTodo.press('Enter'); 30 | 31 | // Make sure the list now has two todo items. 32 | await expect(page.getByTestId('todo-title')).toHaveText([ 33 | TODO_ITEMS[0], 34 | TODO_ITEMS[1] 35 | ]); 36 | 37 | await checkNumberOfTodosInLocalStorage(page, 2); 38 | }); 39 | 40 | test('should clear text input field when an item is added', async ({ page }) => { 41 | // create a new todo locator 42 | const newTodo = page.getByPlaceholder('What needs to be done?'); 43 | 44 | // Create one todo item. 45 | await newTodo.fill(TODO_ITEMS[0]); 46 | await newTodo.press('Enter'); 47 | 48 | // Check that input is empty. 49 | await expect(newTodo).toBeEmpty(); 50 | await checkNumberOfTodosInLocalStorage(page, 1); 51 | }); 52 | 53 | test('should append new items to the bottom of the list', async ({ page }) => { 54 | // Create 3 items. 55 | await createDefaultTodos(page); 56 | 57 | // create a todo count locator 58 | const todoCount = page.getByTestId('todo-count') 59 | 60 | // Check test using different methods. 61 | await expect(page.getByText('3 items left')).toBeVisible(); 62 | await expect(todoCount).toHaveText('3 items left'); 63 | await expect(todoCount).toContainText('3'); 64 | await expect(todoCount).toHaveText(/3/); 65 | 66 | // Check all items in one call. 67 | await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); 68 | await checkNumberOfTodosInLocalStorage(page, 3); 69 | }); 70 | }); 71 | 72 | test.describe('Mark all as completed', () => { 73 | test.beforeEach(async ({ page }) => { 74 | await createDefaultTodos(page); 75 | await checkNumberOfTodosInLocalStorage(page, 3); 76 | }); 77 | 78 | test.afterEach(async ({ page }) => { 79 | await checkNumberOfTodosInLocalStorage(page, 3); 80 | }); 81 | 82 | test('should allow me to mark all items as completed', async ({ page }) => { 83 | // Complete all todos. 84 | await page.getByLabel('Mark all as complete').check(); 85 | 86 | // Ensure all todos have 'completed' class. 87 | await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); 88 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 89 | }); 90 | 91 | test('should allow me to clear the complete state of all items', async ({ page }) => { 92 | const toggleAll = page.getByLabel('Mark all as complete'); 93 | // Check and then immediately uncheck. 94 | await toggleAll.check(); 95 | await toggleAll.uncheck(); 96 | 97 | // Should be no completed classes. 98 | await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); 99 | }); 100 | 101 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { 102 | const toggleAll = page.getByLabel('Mark all as complete'); 103 | await toggleAll.check(); 104 | await expect(toggleAll).toBeChecked(); 105 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 106 | 107 | // Uncheck first todo. 108 | const firstTodo = page.getByTestId('todo-item').nth(0); 109 | await firstTodo.getByRole('checkbox').uncheck(); 110 | 111 | // Reuse toggleAll locator and make sure its not checked. 112 | await expect(toggleAll).not.toBeChecked(); 113 | 114 | await firstTodo.getByRole('checkbox').check(); 115 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 116 | 117 | // Assert the toggle all is checked again. 118 | await expect(toggleAll).toBeChecked(); 119 | }); 120 | }); 121 | 122 | test.describe('Item', () => { 123 | 124 | test('should allow me to mark items as complete', async ({ page }) => { 125 | // create a new todo locator 126 | const newTodo = page.getByPlaceholder('What needs to be done?'); 127 | 128 | // Create two items. 129 | for (const item of TODO_ITEMS.slice(0, 2)) { 130 | await newTodo.fill(item); 131 | await newTodo.press('Enter'); 132 | } 133 | 134 | // Check first item. 135 | const firstTodo = page.getByTestId('todo-item').nth(0); 136 | await firstTodo.getByRole('checkbox').check(); 137 | await expect(firstTodo).toHaveClass('completed'); 138 | 139 | // Check second item. 140 | const secondTodo = page.getByTestId('todo-item').nth(1); 141 | await expect(secondTodo).not.toHaveClass('completed'); 142 | await secondTodo.getByRole('checkbox').check(); 143 | 144 | // Assert completed class. 145 | await expect(firstTodo).toHaveClass('completed'); 146 | await expect(secondTodo).toHaveClass('completed'); 147 | }); 148 | 149 | test('should allow me to un-mark items as complete', async ({ page }) => { 150 | // create a new todo locator 151 | const newTodo = page.getByPlaceholder('What needs to be done?'); 152 | 153 | // Create two items. 154 | for (const item of TODO_ITEMS.slice(0, 2)) { 155 | await newTodo.fill(item); 156 | await newTodo.press('Enter'); 157 | } 158 | 159 | const firstTodo = page.getByTestId('todo-item').nth(0); 160 | const secondTodo = page.getByTestId('todo-item').nth(1); 161 | const firstTodoCheckbox = firstTodo.getByRole('checkbox'); 162 | 163 | await firstTodoCheckbox.check(); 164 | await expect(firstTodo).toHaveClass('completed'); 165 | await expect(secondTodo).not.toHaveClass('completed'); 166 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 167 | 168 | await firstTodoCheckbox.uncheck(); 169 | await expect(firstTodo).not.toHaveClass('completed'); 170 | await expect(secondTodo).not.toHaveClass('completed'); 171 | await checkNumberOfCompletedTodosInLocalStorage(page, 0); 172 | }); 173 | 174 | test('should allow me to edit an item', async ({ page }) => { 175 | await createDefaultTodos(page); 176 | 177 | const todoItems = page.getByTestId('todo-item'); 178 | const secondTodo = todoItems.nth(1); 179 | await secondTodo.dblclick(); 180 | await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); 181 | await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 182 | await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); 183 | 184 | // Explicitly assert the new text value. 185 | await expect(todoItems).toHaveText([ 186 | TODO_ITEMS[0], 187 | 'buy some sausages', 188 | TODO_ITEMS[2] 189 | ]); 190 | await checkTodosInLocalStorage(page, 'buy some sausages'); 191 | }); 192 | }); 193 | 194 | test.describe('Editing', () => { 195 | test.beforeEach(async ({ page }) => { 196 | await createDefaultTodos(page); 197 | await checkNumberOfTodosInLocalStorage(page, 3); 198 | }); 199 | 200 | test('should hide other controls when editing', async ({ page }) => { 201 | const todoItem = page.getByTestId('todo-item').nth(1); 202 | await todoItem.dblclick(); 203 | await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); 204 | await expect(todoItem.locator('label', { 205 | hasText: TODO_ITEMS[1], 206 | })).not.toBeVisible(); 207 | await checkNumberOfTodosInLocalStorage(page, 3); 208 | }); 209 | 210 | test('should save edits on blur', async ({ page }) => { 211 | const todoItems = page.getByTestId('todo-item'); 212 | await todoItems.nth(1).dblclick(); 213 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 214 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); 215 | 216 | await expect(todoItems).toHaveText([ 217 | TODO_ITEMS[0], 218 | 'buy some sausages', 219 | TODO_ITEMS[2], 220 | ]); 221 | await checkTodosInLocalStorage(page, 'buy some sausages'); 222 | }); 223 | 224 | test('should trim entered text', async ({ page }) => { 225 | const todoItems = page.getByTestId('todo-item'); 226 | await todoItems.nth(1).dblclick(); 227 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); 228 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 229 | 230 | await expect(todoItems).toHaveText([ 231 | TODO_ITEMS[0], 232 | 'buy some sausages', 233 | TODO_ITEMS[2], 234 | ]); 235 | await checkTodosInLocalStorage(page, 'buy some sausages'); 236 | }); 237 | 238 | test('should remove the item if an empty text string was entered', async ({ page }) => { 239 | const todoItems = page.getByTestId('todo-item'); 240 | await todoItems.nth(1).dblclick(); 241 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); 242 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 243 | 244 | await expect(todoItems).toHaveText([ 245 | TODO_ITEMS[0], 246 | TODO_ITEMS[2], 247 | ]); 248 | }); 249 | 250 | test('should cancel edits on escape', async ({ page }) => { 251 | const todoItems = page.getByTestId('todo-item'); 252 | await todoItems.nth(1).dblclick(); 253 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 254 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); 255 | await expect(todoItems).toHaveText(TODO_ITEMS); 256 | }); 257 | }); 258 | 259 | test.describe('Counter', () => { 260 | test('should display the current number of todo items', async ({ page }) => { 261 | // create a new todo locator 262 | const newTodo = page.getByPlaceholder('What needs to be done?'); 263 | 264 | // create a todo count locator 265 | const todoCount = page.getByTestId('todo-count') 266 | 267 | await newTodo.fill(TODO_ITEMS[0]); 268 | await newTodo.press('Enter'); 269 | 270 | await expect(todoCount).toContainText('1'); 271 | 272 | await newTodo.fill(TODO_ITEMS[1]); 273 | await newTodo.press('Enter'); 274 | await expect(todoCount).toContainText('2'); 275 | 276 | await checkNumberOfTodosInLocalStorage(page, 2); 277 | }); 278 | }); 279 | 280 | test.describe('Clear completed button', () => { 281 | test.beforeEach(async ({ page }) => { 282 | await createDefaultTodos(page); 283 | }); 284 | 285 | test('should display the correct text', async ({ page }) => { 286 | await page.locator('.todo-list li .toggle').first().check(); 287 | await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); 288 | }); 289 | 290 | test('should remove completed items when clicked', async ({ page }) => { 291 | const todoItems = page.getByTestId('todo-item'); 292 | await todoItems.nth(1).getByRole('checkbox').check(); 293 | await page.getByRole('button', { name: 'Clear completed' }).click(); 294 | await expect(todoItems).toHaveCount(2); 295 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 296 | }); 297 | 298 | test('should be hidden when there are no items that are completed', async ({ page }) => { 299 | await page.locator('.todo-list li .toggle').first().check(); 300 | await page.getByRole('button', { name: 'Clear completed' }).click(); 301 | await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); 302 | }); 303 | }); 304 | 305 | test.describe('Persistence', () => { 306 | test('should persist its data', async ({ page }) => { 307 | // create a new todo locator 308 | const newTodo = page.getByPlaceholder('What needs to be done?'); 309 | 310 | for (const item of TODO_ITEMS.slice(0, 2)) { 311 | await newTodo.fill(item); 312 | await newTodo.press('Enter'); 313 | } 314 | 315 | const todoItems = page.getByTestId('todo-item'); 316 | const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); 317 | await firstTodoCheck.check(); 318 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 319 | await expect(firstTodoCheck).toBeChecked(); 320 | await expect(todoItems).toHaveClass(['completed', '']); 321 | 322 | // Ensure there is 1 completed item. 323 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 324 | 325 | // Now reload. 326 | await page.reload(); 327 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 328 | await expect(firstTodoCheck).toBeChecked(); 329 | await expect(todoItems).toHaveClass(['completed', '']); 330 | }); 331 | }); 332 | 333 | test.describe('Routing', () => { 334 | test.beforeEach(async ({ page }) => { 335 | await createDefaultTodos(page); 336 | // make sure the app had a chance to save updated todos in storage 337 | // before navigating to a new view, otherwise the items can get lost :( 338 | // in some frameworks like Durandal 339 | await checkTodosInLocalStorage(page, TODO_ITEMS[0]); 340 | }); 341 | 342 | test('should allow me to display active items', async ({ page }) => { 343 | const todoItem = page.getByTestId('todo-item'); 344 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 345 | 346 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 347 | await page.getByRole('link', { name: 'Active' }).click(); 348 | await expect(todoItem).toHaveCount(2); 349 | await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 350 | }); 351 | 352 | test('should respect the back button', async ({ page }) => { 353 | const todoItem = page.getByTestId('todo-item'); 354 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 355 | 356 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 357 | 358 | await test.step('Showing all items', async () => { 359 | await page.getByRole('link', { name: 'All' }).click(); 360 | await expect(todoItem).toHaveCount(3); 361 | }); 362 | 363 | await test.step('Showing active items', async () => { 364 | await page.getByRole('link', { name: 'Active' }).click(); 365 | }); 366 | 367 | await test.step('Showing completed items', async () => { 368 | await page.getByRole('link', { name: 'Completed' }).click(); 369 | }); 370 | 371 | await expect(todoItem).toHaveCount(1); 372 | await page.goBack(); 373 | await expect(todoItem).toHaveCount(2); 374 | await page.goBack(); 375 | await expect(todoItem).toHaveCount(3); 376 | }); 377 | 378 | test('should allow me to display completed items', async ({ page }) => { 379 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 380 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 381 | await page.getByRole('link', { name: 'Completed' }).click(); 382 | await expect(page.getByTestId('todo-item')).toHaveCount(1); 383 | }); 384 | 385 | test('should allow me to display all items', async ({ page }) => { 386 | await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); 387 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 388 | await page.getByRole('link', { name: 'Active' }).click(); 389 | await page.getByRole('link', { name: 'Completed' }).click(); 390 | await page.getByRole('link', { name: 'All' }).click(); 391 | await expect(page.getByTestId('todo-item')).toHaveCount(3); 392 | }); 393 | 394 | test('should highlight the currently applied filter', async ({ page }) => { 395 | await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); 396 | 397 | //create locators for active and completed links 398 | const activeLink = page.getByRole('link', { name: 'Active' }); 399 | const completedLink = page.getByRole('link', { name: 'Completed' }); 400 | await activeLink.click(); 401 | 402 | // Page change - active items. 403 | await expect(activeLink).toHaveClass('selected'); 404 | await completedLink.click(); 405 | 406 | // Page change - completed items. 407 | await expect(completedLink).toHaveClass('selected'); 408 | }); 409 | }); 410 | 411 | async function createDefaultTodos(page: Page) { 412 | // create a new todo locator 413 | const newTodo = page.getByPlaceholder('What needs to be done?'); 414 | 415 | for (const item of TODO_ITEMS) { 416 | await newTodo.fill(item); 417 | await newTodo.press('Enter'); 418 | } 419 | } 420 | 421 | async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { 422 | return await page.waitForFunction(e => { 423 | return JSON.parse(localStorage['react-todos']).length === e; 424 | }, expected); 425 | } 426 | 427 | async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { 428 | return await page.waitForFunction(e => { 429 | return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; 430 | }, expected); 431 | } 432 | 433 | async function checkTodosInLocalStorage(page: Page, title: string) { 434 | return await page.waitForFunction(t => { 435 | return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); 436 | }, title); 437 | } 438 | --------------------------------------------------------------------------------