Install and configure playwright
This commit is contained in:
		
							parent
							
								
									36f8e63c4a
								
							
						
					
					
						commit
						3e9075b015
					
				
							
								
								
									
										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 install -g pnpm && pnpm install | ||||||
|  |     - name: Install Playwright Browsers | ||||||
|  |       run: pnpm exec playwright install --with-deps | ||||||
|  |     - name: Run Playwright tests | ||||||
|  |       run: pnpm exec 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
									
									
								
							| @ -34,3 +34,7 @@ yarn-error.log* | |||||||
| # typescript | # typescript | ||||||
| *.tsbuildinfo | *.tsbuildinfo | ||||||
| next-env.d.ts | next-env.d.ts | ||||||
|  | /test-results/ | ||||||
|  | /playwright-report/ | ||||||
|  | /blob-report/ | ||||||
|  | /playwright/.cache/ | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ | |||||||
|     "zod": "^3.23.8" |     "zod": "^3.23.8" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@playwright/test": "^1.46.0", | ||||||
|     "@types/bcrypt": "^5.0.2", |     "@types/bcrypt": "^5.0.2", | ||||||
|     "@types/node": "20.14.8", |     "@types/node": "20.14.8", | ||||||
|     "@types/react": "18.3.3", |     "@types/react": "18.3.3", | ||||||
|  | |||||||
							
								
								
									
										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: './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,
 | ||||||
|  |   // },
 | ||||||
|  | }); | ||||||
							
								
								
									
										49
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										49
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -28,10 +28,10 @@ importers: | |||||||
|         version: 2.1.1 |         version: 2.1.1 | ||||||
|       next: |       next: | ||||||
|         specifier: 15.0.0-canary.56 |         specifier: 15.0.0-canary.56 | ||||||
|         version: 15.0.0-canary.56(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) |         version: 15.0.0-canary.56(@playwright/test@1.46.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) | ||||||
|       next-auth: |       next-auth: | ||||||
|         specifier: 5.0.0-beta.19 |         specifier: 5.0.0-beta.19 | ||||||
|         version: 5.0.0-beta.19(next@15.0.0-canary.56(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) |         version: 5.0.0-beta.19(next@15.0.0-canary.56(@playwright/test@1.46.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) | ||||||
|       postcss: |       postcss: | ||||||
|         specifier: 8.4.38 |         specifier: 8.4.38 | ||||||
|         version: 8.4.38 |         version: 8.4.38 | ||||||
| @ -60,6 +60,9 @@ importers: | |||||||
|         specifier: ^3.23.8 |         specifier: ^3.23.8 | ||||||
|         version: 3.23.8 |         version: 3.23.8 | ||||||
|     devDependencies: |     devDependencies: | ||||||
|  |       '@playwright/test': | ||||||
|  |         specifier: ^1.46.0 | ||||||
|  |         version: 1.46.0 | ||||||
|       '@types/bcrypt': |       '@types/bcrypt': | ||||||
|         specifier: ^5.0.2 |         specifier: ^5.0.2 | ||||||
|         version: 5.0.2 |         version: 5.0.2 | ||||||
| @ -323,6 +326,11 @@ packages: | |||||||
|     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} |     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} | ||||||
|     engines: {node: '>=14'} |     engines: {node: '>=14'} | ||||||
| 
 | 
 | ||||||
|  |   '@playwright/test@1.46.0': | ||||||
|  |     resolution: {integrity: sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==} | ||||||
|  |     engines: {node: '>=18'} | ||||||
|  |     hasBin: true | ||||||
|  | 
 | ||||||
|   '@swc/helpers@0.5.11': |   '@swc/helpers@0.5.11': | ||||||
|     resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} |     resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} | ||||||
| 
 | 
 | ||||||
| @ -573,6 +581,11 @@ packages: | |||||||
|   fs.realpath@1.0.0: |   fs.realpath@1.0.0: | ||||||
|     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} |     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} | ||||||
| 
 | 
 | ||||||
|  |   fsevents@2.3.2: | ||||||
|  |     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} | ||||||
|  |     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} | ||||||
|  |     os: [darwin] | ||||||
|  | 
 | ||||||
|   fsevents@2.3.3: |   fsevents@2.3.3: | ||||||
|     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} |     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} | ||||||
|     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} |     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} | ||||||
| @ -869,6 +882,16 @@ packages: | |||||||
|     resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} |     resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} | ||||||
|     engines: {node: '>= 6'} |     engines: {node: '>= 6'} | ||||||
| 
 | 
 | ||||||
|  |   playwright-core@1.46.0: | ||||||
|  |     resolution: {integrity: sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==} | ||||||
|  |     engines: {node: '>=18'} | ||||||
|  |     hasBin: true | ||||||
|  | 
 | ||||||
|  |   playwright@1.46.0: | ||||||
|  |     resolution: {integrity: sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==} | ||||||
|  |     engines: {node: '>=18'} | ||||||
|  |     hasBin: true | ||||||
|  | 
 | ||||||
|   postcss-import@15.1.0: |   postcss-import@15.1.0: | ||||||
|     resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} |     resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} | ||||||
|     engines: {node: '>=14.0.0'} |     engines: {node: '>=14.0.0'} | ||||||
| @ -1399,6 +1422,10 @@ snapshots: | |||||||
|   '@pkgjs/parseargs@0.11.0': |   '@pkgjs/parseargs@0.11.0': | ||||||
|     optional: true |     optional: true | ||||||
| 
 | 
 | ||||||
|  |   '@playwright/test@1.46.0': | ||||||
|  |     dependencies: | ||||||
|  |       playwright: 1.46.0 | ||||||
|  | 
 | ||||||
|   '@swc/helpers@0.5.11': |   '@swc/helpers@0.5.11': | ||||||
|     dependencies: |     dependencies: | ||||||
|       tslib: 2.6.3 |       tslib: 2.6.3 | ||||||
| @ -1646,6 +1673,9 @@ snapshots: | |||||||
| 
 | 
 | ||||||
|   fs.realpath@1.0.0: {} |   fs.realpath@1.0.0: {} | ||||||
| 
 | 
 | ||||||
|  |   fsevents@2.3.2: | ||||||
|  |     optional: true | ||||||
|  | 
 | ||||||
|   fsevents@2.3.3: |   fsevents@2.3.3: | ||||||
|     optional: true |     optional: true | ||||||
| 
 | 
 | ||||||
| @ -1803,13 +1833,13 @@ snapshots: | |||||||
| 
 | 
 | ||||||
|   nanoid@3.3.7: {} |   nanoid@3.3.7: {} | ||||||
| 
 | 
 | ||||||
|   next-auth@5.0.0-beta.19(next@15.0.0-canary.56(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): |   next-auth@5.0.0-beta.19(next@15.0.0-canary.56(@playwright/test@1.46.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@auth/core': 0.32.0 |       '@auth/core': 0.32.0 | ||||||
|       next: 15.0.0-canary.56(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) |       next: 15.0.0-canary.56(@playwright/test@1.46.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) | ||||||
|       react: 19.0.0-rc-f38c22b244-20240704 |       react: 19.0.0-rc-f38c22b244-20240704 | ||||||
| 
 | 
 | ||||||
|   next@15.0.0-canary.56(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): |   next@15.0.0-canary.56(@playwright/test@1.46.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@next/env': 15.0.0-canary.56 |       '@next/env': 15.0.0-canary.56 | ||||||
|       '@swc/helpers': 0.5.11 |       '@swc/helpers': 0.5.11 | ||||||
| @ -1830,6 +1860,7 @@ snapshots: | |||||||
|       '@next/swc-win32-arm64-msvc': 15.0.0-canary.56 |       '@next/swc-win32-arm64-msvc': 15.0.0-canary.56 | ||||||
|       '@next/swc-win32-ia32-msvc': 15.0.0-canary.56 |       '@next/swc-win32-ia32-msvc': 15.0.0-canary.56 | ||||||
|       '@next/swc-win32-x64-msvc': 15.0.0-canary.56 |       '@next/swc-win32-x64-msvc': 15.0.0-canary.56 | ||||||
|  |       '@playwright/test': 1.46.0 | ||||||
|       sharp: 0.33.4 |       sharp: 0.33.4 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@babel/core' |       - '@babel/core' | ||||||
| @ -1901,6 +1932,14 @@ snapshots: | |||||||
| 
 | 
 | ||||||
|   pirates@4.0.6: {} |   pirates@4.0.6: {} | ||||||
| 
 | 
 | ||||||
|  |   playwright-core@1.46.0: {} | ||||||
|  | 
 | ||||||
|  |   playwright@1.46.0: | ||||||
|  |     dependencies: | ||||||
|  |       playwright-core: 1.46.0 | ||||||
|  |     optionalDependencies: | ||||||
|  |       fsevents: 2.3.2 | ||||||
|  | 
 | ||||||
|   postcss-import@15.1.0(postcss@8.4.38): |   postcss-import@15.1.0(postcss@8.4.38): | ||||||
|     dependencies: |     dependencies: | ||||||
|       postcss: 8.4.38 |       postcss: 8.4.38 | ||||||
|  | |||||||
							
								
								
									
										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); | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								tests/example.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								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(); | ||||||
|  | }); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user