mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	integration-test: Initialize Playwright
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							@@ -158,6 +158,7 @@ jobs:
 | 
			
		||||
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
 | 
			
		||||
        with:
 | 
			
		||||
          images: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
 | 
			
		||||
          tags: 
 | 
			
		||||
      - name: Extract metadata (tags, labels) for DockerHub image
 | 
			
		||||
        id: dh-meta
 | 
			
		||||
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -23,3 +23,7 @@ images/app-icons/png/32x32.png
 | 
			
		||||
images/app-icons/png/512x512.png
 | 
			
		||||
images/app-icons/png/1024x1024.png
 | 
			
		||||
images/app-icons/mac/*.png
 | 
			
		||||
/test-results/
 | 
			
		||||
/playwright-report/
 | 
			
		||||
/blob-report/
 | 
			
		||||
/playwright/.cache/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								integration-tests/example.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								integration-tests/example.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										105
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										105
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "version": "0.90.2-beta",
 | 
			
		||||
  "version": "0.90.3",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "trilium",
 | 
			
		||||
      "version": "0.90.2-beta",
 | 
			
		||||
      "version": "0.90.3",
 | 
			
		||||
      "license": "AGPL-3.0-only",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@braintree/sanitize-url": "^7.1.0",
 | 
			
		||||
@@ -93,6 +93,7 @@
 | 
			
		||||
        "@electron-forge/cli": "^6.4.2",
 | 
			
		||||
        "@electron-forge/maker-squirrel": "^6.4.2",
 | 
			
		||||
        "@electron-forge/plugin-auto-unpack-natives": "^6.4.2",
 | 
			
		||||
        "@playwright/test": "^1.46.0",
 | 
			
		||||
        "@types/archiver": "^6.0.2",
 | 
			
		||||
        "@types/better-sqlite3": "^7.6.9",
 | 
			
		||||
        "@types/cls-hooked": "^4.3.8",
 | 
			
		||||
@@ -110,6 +111,7 @@
 | 
			
		||||
        "@types/jsdom": "^21.1.6",
 | 
			
		||||
        "@types/mime-types": "^2.1.4",
 | 
			
		||||
        "@types/multer": "^1.4.11",
 | 
			
		||||
        "@types/node": "^22.1.0",
 | 
			
		||||
        "@types/safe-compare": "^1.1.2",
 | 
			
		||||
        "@types/sanitize-html": "^2.11.0",
 | 
			
		||||
        "@types/sax": "^1.2.7",
 | 
			
		||||
@@ -2062,6 +2064,21 @@
 | 
			
		||||
        "node": ">=14"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "playwright": "1.46.0"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "playwright": "cli.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@sindresorhus/is": {
 | 
			
		||||
      "version": "4.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
 | 
			
		||||
@@ -2479,11 +2496,11 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/node": {
 | 
			
		||||
      "version": "20.14.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
 | 
			
		||||
      "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
 | 
			
		||||
      "version": "22.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "undici-types": "~5.26.4"
 | 
			
		||||
        "undici-types": "~6.13.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/qs": {
 | 
			
		||||
@@ -6265,6 +6282,19 @@
 | 
			
		||||
        "node": ">=6 <7 || >=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/electron/node_modules/@types/node": {
 | 
			
		||||
      "version": "20.14.14",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz",
 | 
			
		||||
      "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "undici-types": "~5.26.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/electron/node_modules/undici-types": {
 | 
			
		||||
      "version": "5.26.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
 | 
			
		||||
      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/elkjs": {
 | 
			
		||||
      "version": "0.9.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz",
 | 
			
		||||
@@ -7354,6 +7384,19 @@
 | 
			
		||||
      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
 | 
			
		||||
      "devOptional": true
 | 
			
		||||
    },
 | 
			
		||||
    "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,
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/function-bind": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
 | 
			
		||||
@@ -11613,6 +11656,36 @@
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/playwright": {
 | 
			
		||||
      "version": "1.46.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz",
 | 
			
		||||
      "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "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==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "playwright-core": "cli.js"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=18"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/plist": {
 | 
			
		||||
      "version": "3.0.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz",
 | 
			
		||||
@@ -13929,6 +14002,20 @@
 | 
			
		||||
        "fsevents": "~2.3.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tsx/node_modules/fsevents": {
 | 
			
		||||
      "version": "2.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ],
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/tunnel-agent": {
 | 
			
		||||
      "version": "0.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
 | 
			
		||||
@@ -14033,9 +14120,9 @@
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/undici-types": {
 | 
			
		||||
      "version": "5.26.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
 | 
			
		||||
      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
 | 
			
		||||
      "version": "6.13.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
 | 
			
		||||
      "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/unescape": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -128,6 +128,7 @@
 | 
			
		||||
    "@electron-forge/cli": "^6.4.2",
 | 
			
		||||
    "@electron-forge/maker-squirrel": "^6.4.2",
 | 
			
		||||
    "@electron-forge/plugin-auto-unpack-natives": "^6.4.2",
 | 
			
		||||
    "@playwright/test": "^1.46.0",
 | 
			
		||||
    "@types/archiver": "^6.0.2",
 | 
			
		||||
    "@types/better-sqlite3": "^7.6.9",
 | 
			
		||||
    "@types/cls-hooked": "^4.3.8",
 | 
			
		||||
@@ -145,6 +146,7 @@
 | 
			
		||||
    "@types/jsdom": "^21.1.6",
 | 
			
		||||
    "@types/mime-types": "^2.1.4",
 | 
			
		||||
    "@types/multer": "^1.4.11",
 | 
			
		||||
    "@types/node": "^22.1.0",
 | 
			
		||||
    "@types/safe-compare": "^1.1.2",
 | 
			
		||||
    "@types/sanitize-html": "^2.11.0",
 | 
			
		||||
    "@types/sax": "^1.2.7",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import { defineConfig, devices } from '@playwright/test';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Read environment variables from file.
 | 
			
		||||
 * https://github.com/motdotla/dotenv
 | 
			
		||||
 */
 | 
			
		||||
// import dotenv from 'dotenv';
 | 
			
		||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * See https://playwright.dev/docs/test-configuration.
 | 
			
		||||
 */
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  testDir: './integration-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: '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,
 | 
			
		||||
  // },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										437
									
								
								tests-examples/demo-todo-app.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										437
									
								
								tests-examples/demo-todo-app.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user