├── README.md ├── index.js ├── src └── mailer.js ├── tests └── example.spec.js ├── package.json ├── tea.yaml ├── playwright.config.js ├── pnpm-lock.yaml └── tests-examples └── demo-todo-app.spec.js /README.md: -------------------------------------------------------------------------------- 1 | # 使用方法 2 | ## 1.安装playright 3 | 参考https://playwright.dev/docs/intro 4 | ## 2.配置SMTP邮箱 5 | ``` 6 | host: 'smtp.163.com', 7 | port: 465, 8 | secure: true, 9 | auth: { 10 | user: 'xxx@xxx.com', 11 | pass: 'xxxx' 12 | } 13 | ``` 14 | ## 3.运行 15 | ``` 16 | npm run dev 17 | ``` 18 | test mbv-26519-251 19 | 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // import { execa } from 'execa' 2 | let index = 0, intervalObj = null 3 | async function run() { 4 | const { execa } = await import('execa') 5 | await execa('npm', ['run', 'start'], { stdio: 'inherit' }) 6 | index = index + 1 7 | intervalObj = setInterval(async () => { 8 | if (index > 1000) { 9 | index = 0 10 | 11 | clearInterval(intervalObj) 12 | intervalObj = null 13 | } else { 14 | await execa('npm', ['run', 'start'], { stdio: 'inherit' }) 15 | index = index + 1 16 | } 17 | 18 | }, 60000); 19 | 20 | } 21 | 22 | run() -------------------------------------------------------------------------------- /src/mailer.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | 3 | // 创建一个邮件传输对象 4 | var transporter = nodemailer.createTransport({ 5 | host: 'smtp.163.com', 6 | port: 465, 7 | secure: true, 8 | auth: { 9 | user: 'xxx@xxx.com', 10 | pass: 'xxxx' 11 | } 12 | }); 13 | transporter.verify(function (err, success) { 14 | if (err) { console.error(`SMTP Error: ${err}`); } 15 | if (success) { 16 | console.log("SMTP ready..."); 17 | } else { 18 | console.error("SMTP not ready!"); 19 | } 20 | }); 21 | module.exports = transporter.sendMail.bind(transporter) -------------------------------------------------------------------------------- /tests/example.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const sendMsg = require('../src/mailer'); 3 | const { test, expect } = require('@playwright/test'); 4 | 5 | test('test', async ({ page }) => { 6 | await page.goto('https://www.blocknative.com/gas-estimator'); 7 | let res = await page.locator('.base-fee-value').innerText(); 8 | console.log('ETH当前gas费为:' + res); 9 | let no = Number(res.replace(' GWEI', '')) 10 | console.log(no); 11 | if (no < 20) { 12 | console.log('超过20'); 13 | var mailOptions = { 14 | from: 'xxx@xxx.com', // 发件人邮箱 15 | to: 'xxx0@xxx.com', // 收件人邮箱 16 | subject: '通知', // 主题 17 | text: 'ETH当前gas为:' + no // 正文 18 | }; 19 | await sendMsg(mailOptions) 20 | 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-listener", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npx playwright test --project webkit", 8 | "dev": "babel-node index.js --presets es2015,stage-2" 9 | }, 10 | "author": "", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/FiveKG/gas-listener" 14 | }, 15 | "homepage": "https://github.com/FiveKG/gas-listener", 16 | "license": "ISC", 17 | "dependencies": { 18 | "axios": "^1.6.2", 19 | "babel-cli": "^6.26.0", 20 | "babel-preset-es2015": "^6.24.1", 21 | "babel-preset-stage-2": "^6.24.1", 22 | "node-fetch": "^2.7.0", 23 | "nodemailer": "^6.9.7" 24 | }, 25 | "devDependencies": { 26 | "@playwright/test": "^1.40.0", 27 | "@types/node": "^20.10.0", 28 | "execa": "^8.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x7d76e6b2EC0C808472B1d39dB144f4481798dF6A' 6 | - '0x358fBF262b624aA50252018c544fDe279873235B' 7 | - '0xC6D60A59E8DCCE3b1F3c77b2230F9788D2F39307' 8 | - '0xb9595d3dEd031b18C4Aeed469D4eb930077989EF' 9 | - '0x66bA1A41817287391797a0281656E301fE73d3a7' 10 | - '0x7203edBE391a7c6EE94E99F550cC2C2FE1871d9D' 11 | - '0x4D8eEC0ae73eF610aCD0f5DE68b4cF12b2711643' 12 | - '0x3d27CD5EB1c2AAD34E31Bb5eC3C533D493b88620' 13 | - '0x3a9E906E353391508C57265AADc8f06598496f14' 14 | - '0xfEFA4eA534D7AF39CE2FD7E2C273fe25Ea7c7C1d' 15 | - '0xf2A53eC68ce84CD757d3463717b4314b3DCd3C38' 16 | - '0xC2d7AF809ECCccfa9c2EB2676be41971700618c4' 17 | - '0x69Ff698679Ca08f66c4F7fDD0E1DE6760E5f68eF' 18 | - '0xbE4d9611b3A2b688874126A21a5D455FeDD1Ef62' 19 | - '0x4A24BE91fAA4b8424d33a9c9Bf44A7d92fd886D1' 20 | - '0x7eB834E7FA29C086515b998A203f758c6748977a' 21 | - '0x45E4D6F5d66290B18E046751c6Ba2a544a34cE87' 22 | - '0x437b996D8BbDaecA25132Aa7fB5cDA81539FD1D0' 23 | - '0x2834926A3D8eF517A5d2525B2fF86D0a225e7D88' 24 | - '0xEC25B627ec8fB9Be0b26b4822c6109a682d6dFe7' 25 | - '0x30673ba7815c63D7d1F83D4b3b40aD9A5a6A1aa9' 26 | - '0xcC9c824768d1a367c51066eb3EF4677B4ae9152d' 27 | - '0xED1003a6ABB11C1FAcefDfA13295b50ED994B651' 28 | - '0x88f40D96EFcc88Fff413454B259385c10400fc85' 29 | - '0xe18A3299BA1efAF90D4505e375eDA70f731D9D08' 30 | - '0x6D4225Fe4F82Df6F0CC96a75fbbdfdbE9650Dbc8' 31 | - '0x0E0c191d75b022600166d719a063c7CcFc124d48' 32 | - '0xa91ccA98b435582cB93A61Eef18e954deb408Fa1' 33 | - '0x8684CF2B34AcD1f902d86413c0dc8E0186b8b8D8' 34 | - '0x6298F8B61E3d0a16AFb5c845B9FBB1243893137A' 35 | - '0xDF21209c48Cbd206Ec1CF08E37F6d35F7F90457a' 36 | quorum: 1 37 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { defineConfig, devices } = require('@playwright/test'); 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * @see https://playwright.dev/docs/test-configuration 12 | */ 13 | module.exports = defineConfig({ 14 | testDir: './tests', 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: 'on-first-retry', 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | 41 | { 42 | name: 'firefox', 43 | use: { ...devices['Desktop Firefox'] }, 44 | }, 45 | 46 | { 47 | name: 'webkit', 48 | use: { ...devices['Desktop Safari'] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | // webServer: { 74 | // command: 'npm run start', 75 | // url: 'http://127.0.0.1:3000', 76 | // reuseExistingServer: !process.env.CI, 77 | // }, 78 | }); 79 | 80 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | axios: 9 | specifier: ^1.6.2 10 | version: 1.6.2 11 | node-fetch: 12 | specifier: ^2.7.0 13 | version: 2.7.0 14 | nodemailer: 15 | specifier: ^6.9.7 16 | version: 6.9.7 17 | 18 | devDependencies: 19 | '@playwright/test': 20 | specifier: ^1.40.0 21 | version: 1.40.0 22 | '@types/node': 23 | specifier: ^20.10.0 24 | version: 20.10.0 25 | playwright: 26 | specifier: ^1.40.0 27 | version: 1.40.0 28 | 29 | packages: 30 | 31 | /@playwright/test@1.40.0: 32 | resolution: {integrity: sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==} 33 | engines: {node: '>=16'} 34 | hasBin: true 35 | dependencies: 36 | playwright: 1.40.0 37 | dev: true 38 | 39 | /@types/node@20.10.0: 40 | resolution: {integrity: sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==} 41 | dependencies: 42 | undici-types: 5.26.5 43 | dev: true 44 | 45 | /asynckit@0.4.0: 46 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 47 | dev: false 48 | 49 | /axios@1.6.2: 50 | resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} 51 | dependencies: 52 | follow-redirects: 1.15.3 53 | form-data: 4.0.0 54 | proxy-from-env: 1.1.0 55 | transitivePeerDependencies: 56 | - debug 57 | dev: false 58 | 59 | /combined-stream@1.0.8: 60 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 61 | engines: {node: '>= 0.8'} 62 | dependencies: 63 | delayed-stream: 1.0.0 64 | dev: false 65 | 66 | /delayed-stream@1.0.0: 67 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 68 | engines: {node: '>=0.4.0'} 69 | dev: false 70 | 71 | /follow-redirects@1.15.3: 72 | resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} 73 | engines: {node: '>=4.0'} 74 | peerDependencies: 75 | debug: '*' 76 | peerDependenciesMeta: 77 | debug: 78 | optional: true 79 | dev: false 80 | 81 | /form-data@4.0.0: 82 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 83 | engines: {node: '>= 6'} 84 | dependencies: 85 | asynckit: 0.4.0 86 | combined-stream: 1.0.8 87 | mime-types: 2.1.35 88 | dev: false 89 | 90 | /fsevents@2.3.2: 91 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 92 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 93 | os: [darwin] 94 | requiresBuild: true 95 | dev: true 96 | optional: true 97 | 98 | /mime-db@1.52.0: 99 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 100 | engines: {node: '>= 0.6'} 101 | dev: false 102 | 103 | /mime-types@2.1.35: 104 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 105 | engines: {node: '>= 0.6'} 106 | dependencies: 107 | mime-db: 1.52.0 108 | dev: false 109 | 110 | /node-fetch@2.7.0: 111 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} 112 | engines: {node: 4.x || >=6.0.0} 113 | peerDependencies: 114 | encoding: ^0.1.0 115 | peerDependenciesMeta: 116 | encoding: 117 | optional: true 118 | dependencies: 119 | whatwg-url: 5.0.0 120 | dev: false 121 | 122 | /nodemailer@6.9.7: 123 | resolution: {integrity: sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==} 124 | engines: {node: '>=6.0.0'} 125 | dev: false 126 | 127 | /playwright-core@1.40.0: 128 | resolution: {integrity: sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==} 129 | engines: {node: '>=16'} 130 | hasBin: true 131 | dev: true 132 | 133 | /playwright@1.40.0: 134 | resolution: {integrity: sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==} 135 | engines: {node: '>=16'} 136 | hasBin: true 137 | dependencies: 138 | playwright-core: 1.40.0 139 | optionalDependencies: 140 | fsevents: 2.3.2 141 | dev: true 142 | 143 | /proxy-from-env@1.1.0: 144 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 145 | dev: false 146 | 147 | /tr46@0.0.3: 148 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} 149 | dev: false 150 | 151 | /undici-types@5.26.5: 152 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 153 | dev: true 154 | 155 | /webidl-conversions@3.0.1: 156 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 157 | dev: false 158 | 159 | /whatwg-url@5.0.0: 160 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} 161 | dependencies: 162 | tr46: 0.0.3 163 | webidl-conversions: 3.0.1 164 | dev: false 165 | -------------------------------------------------------------------------------- /tests-examples/demo-todo-app.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('https://demo.playwright.dev/todomvc'); 6 | }); 7 | 8 | const TODO_ITEMS = [ 9 | 'buy some cheese', 10 | 'feed the cat', 11 | 'book a doctors appointment' 12 | ]; 13 | 14 | test.describe('New Todo', () => { 15 | test('should allow me to add todo items', async ({ page }) => { 16 | // create a new todo locator 17 | const newTodo = page.getByPlaceholder('What needs to be done?'); 18 | 19 | // Create 1st todo. 20 | await newTodo.fill(TODO_ITEMS[0]); 21 | await newTodo.press('Enter'); 22 | 23 | // Make sure the list only has one todo item. 24 | await expect(page.getByTestId('todo-title')).toHaveText([ 25 | TODO_ITEMS[0] 26 | ]); 27 | 28 | // Create 2nd todo. 29 | await newTodo.fill(TODO_ITEMS[1]); 30 | await newTodo.press('Enter'); 31 | 32 | // Make sure the list now has two todo items. 33 | await expect(page.getByTestId('todo-title')).toHaveText([ 34 | TODO_ITEMS[0], 35 | TODO_ITEMS[1] 36 | ]); 37 | 38 | await checkNumberOfTodosInLocalStorage(page, 2); 39 | }); 40 | 41 | test('should clear text input field when an item is added', async ({ page }) => { 42 | // create a new todo locator 43 | const newTodo = page.getByPlaceholder('What needs to be done?'); 44 | 45 | // Create one todo item. 46 | await newTodo.fill(TODO_ITEMS[0]); 47 | await newTodo.press('Enter'); 48 | 49 | // Check that input is empty. 50 | await expect(newTodo).toBeEmpty(); 51 | await checkNumberOfTodosInLocalStorage(page, 1); 52 | }); 53 | 54 | test('should append new items to the bottom of the list', async ({ page }) => { 55 | // Create 3 items. 56 | await createDefaultTodos(page); 57 | 58 | // create a todo count locator 59 | const todoCount = page.getByTestId('todo-count') 60 | 61 | // Check test using different methods. 62 | await expect(page.getByText('3 items left')).toBeVisible(); 63 | await expect(todoCount).toHaveText('3 items left'); 64 | await expect(todoCount).toContainText('3'); 65 | await expect(todoCount).toHaveText(/3/); 66 | 67 | // Check all items in one call. 68 | await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); 69 | await checkNumberOfTodosInLocalStorage(page, 3); 70 | }); 71 | }); 72 | 73 | test.describe('Mark all as completed', () => { 74 | test.beforeEach(async ({ page }) => { 75 | await createDefaultTodos(page); 76 | await checkNumberOfTodosInLocalStorage(page, 3); 77 | }); 78 | 79 | test.afterEach(async ({ page }) => { 80 | await checkNumberOfTodosInLocalStorage(page, 3); 81 | }); 82 | 83 | test('should allow me to mark all items as completed', async ({ page }) => { 84 | // Complete all todos. 85 | await page.getByLabel('Mark all as complete').check(); 86 | 87 | // Ensure all todos have 'completed' class. 88 | await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); 89 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 90 | }); 91 | 92 | test('should allow me to clear the complete state of all items', async ({ page }) => { 93 | const toggleAll = page.getByLabel('Mark all as complete'); 94 | // Check and then immediately uncheck. 95 | await toggleAll.check(); 96 | await toggleAll.uncheck(); 97 | 98 | // Should be no completed classes. 99 | await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); 100 | }); 101 | 102 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { 103 | const toggleAll = page.getByLabel('Mark all as complete'); 104 | await toggleAll.check(); 105 | await expect(toggleAll).toBeChecked(); 106 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 107 | 108 | // Uncheck first todo. 109 | const firstTodo = page.getByTestId('todo-item').nth(0); 110 | await firstTodo.getByRole('checkbox').uncheck(); 111 | 112 | // Reuse toggleAll locator and make sure its not checked. 113 | await expect(toggleAll).not.toBeChecked(); 114 | 115 | await firstTodo.getByRole('checkbox').check(); 116 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 117 | 118 | // Assert the toggle all is checked again. 119 | await expect(toggleAll).toBeChecked(); 120 | }); 121 | }); 122 | 123 | test.describe('Item', () => { 124 | 125 | test('should allow me to mark items as complete', async ({ page }) => { 126 | // create a new todo locator 127 | const newTodo = page.getByPlaceholder('What needs to be done?'); 128 | 129 | // Create two items. 130 | for (const item of TODO_ITEMS.slice(0, 2)) { 131 | await newTodo.fill(item); 132 | await newTodo.press('Enter'); 133 | } 134 | 135 | // Check first item. 136 | const firstTodo = page.getByTestId('todo-item').nth(0); 137 | await firstTodo.getByRole('checkbox').check(); 138 | await expect(firstTodo).toHaveClass('completed'); 139 | 140 | // Check second item. 141 | const secondTodo = page.getByTestId('todo-item').nth(1); 142 | await expect(secondTodo).not.toHaveClass('completed'); 143 | await secondTodo.getByRole('checkbox').check(); 144 | 145 | // Assert completed class. 146 | await expect(firstTodo).toHaveClass('completed'); 147 | await expect(secondTodo).toHaveClass('completed'); 148 | }); 149 | 150 | test('should allow me to un-mark items as complete', async ({ page }) => { 151 | // create a new todo locator 152 | const newTodo = page.getByPlaceholder('What needs to be done?'); 153 | 154 | // Create two items. 155 | for (const item of TODO_ITEMS.slice(0, 2)) { 156 | await newTodo.fill(item); 157 | await newTodo.press('Enter'); 158 | } 159 | 160 | const firstTodo = page.getByTestId('todo-item').nth(0); 161 | const secondTodo = page.getByTestId('todo-item').nth(1); 162 | const firstTodoCheckbox = firstTodo.getByRole('checkbox'); 163 | 164 | await firstTodoCheckbox.check(); 165 | await expect(firstTodo).toHaveClass('completed'); 166 | await expect(secondTodo).not.toHaveClass('completed'); 167 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 168 | 169 | await firstTodoCheckbox.uncheck(); 170 | await expect(firstTodo).not.toHaveClass('completed'); 171 | await expect(secondTodo).not.toHaveClass('completed'); 172 | await checkNumberOfCompletedTodosInLocalStorage(page, 0); 173 | }); 174 | 175 | test('should allow me to edit an item', async ({ page }) => { 176 | await createDefaultTodos(page); 177 | 178 | const todoItems = page.getByTestId('todo-item'); 179 | const secondTodo = todoItems.nth(1); 180 | await secondTodo.dblclick(); 181 | await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); 182 | await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 183 | await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); 184 | 185 | // Explicitly assert the new text value. 186 | await expect(todoItems).toHaveText([ 187 | TODO_ITEMS[0], 188 | 'buy some sausages', 189 | TODO_ITEMS[2] 190 | ]); 191 | await checkTodosInLocalStorage(page, 'buy some sausages'); 192 | }); 193 | }); 194 | 195 | test.describe('Editing', () => { 196 | test.beforeEach(async ({ page }) => { 197 | await createDefaultTodos(page); 198 | await checkNumberOfTodosInLocalStorage(page, 3); 199 | }); 200 | 201 | test('should hide other controls when editing', async ({ page }) => { 202 | const todoItem = page.getByTestId('todo-item').nth(1); 203 | await todoItem.dblclick(); 204 | await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); 205 | await expect(todoItem.locator('label', { 206 | hasText: TODO_ITEMS[1], 207 | })).not.toBeVisible(); 208 | await checkNumberOfTodosInLocalStorage(page, 3); 209 | }); 210 | 211 | test('should save edits on blur', async ({ page }) => { 212 | const todoItems = page.getByTestId('todo-item'); 213 | await todoItems.nth(1).dblclick(); 214 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 215 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); 216 | 217 | await expect(todoItems).toHaveText([ 218 | TODO_ITEMS[0], 219 | 'buy some sausages', 220 | TODO_ITEMS[2], 221 | ]); 222 | await checkTodosInLocalStorage(page, 'buy some sausages'); 223 | }); 224 | 225 | test('should trim entered text', async ({ page }) => { 226 | const todoItems = page.getByTestId('todo-item'); 227 | await todoItems.nth(1).dblclick(); 228 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); 229 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 230 | 231 | await expect(todoItems).toHaveText([ 232 | TODO_ITEMS[0], 233 | 'buy some sausages', 234 | TODO_ITEMS[2], 235 | ]); 236 | await checkTodosInLocalStorage(page, 'buy some sausages'); 237 | }); 238 | 239 | test('should remove the item if an empty text string was entered', async ({ page }) => { 240 | const todoItems = page.getByTestId('todo-item'); 241 | await todoItems.nth(1).dblclick(); 242 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); 243 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); 244 | 245 | await expect(todoItems).toHaveText([ 246 | TODO_ITEMS[0], 247 | TODO_ITEMS[2], 248 | ]); 249 | }); 250 | 251 | test('should cancel edits on escape', async ({ page }) => { 252 | const todoItems = page.getByTestId('todo-item'); 253 | await todoItems.nth(1).dblclick(); 254 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); 255 | await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); 256 | await expect(todoItems).toHaveText(TODO_ITEMS); 257 | }); 258 | }); 259 | 260 | test.describe('Counter', () => { 261 | test('should display the current number of todo items', async ({ page }) => { 262 | // create a new todo locator 263 | const newTodo = page.getByPlaceholder('What needs to be done?'); 264 | 265 | // create a todo count locator 266 | const todoCount = page.getByTestId('todo-count') 267 | 268 | await newTodo.fill(TODO_ITEMS[0]); 269 | await newTodo.press('Enter'); 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) { 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 | /** 422 | * @param {import('@playwright/test').Page} page 423 | * @param {number} expected 424 | */ 425 | async function checkNumberOfTodosInLocalStorage(page, expected) { 426 | return await page.waitForFunction(e => { 427 | return JSON.parse(localStorage['react-todos']).length === e; 428 | }, expected); 429 | } 430 | 431 | /** 432 | * @param {import('@playwright/test').Page} page 433 | * @param {number} expected 434 | */ 435 | async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { 436 | return await page.waitForFunction(e => { 437 | return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; 438 | }, expected); 439 | } 440 | 441 | /** 442 | * @param {import('@playwright/test').Page} page 443 | * @param {string} title 444 | */ 445 | async function checkTodosInLocalStorage(page, title) { 446 | return await page.waitForFunction(t => { 447 | return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); 448 | }, title); 449 | } 450 | --------------------------------------------------------------------------------