From 309c0d7488585d33c344c1f0566e455bc1284130 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 13 Dec 2024 14:38:23 +0400 Subject: [PATCH] tests: add playwright tests --- .env | 1 + .github/workflows/playwright.yml | 25 + .gitignore | 5 + package-lock.json | 2533 +++++++++++++++++ package.json | 9 + playwright.config.ts | 54 + tests/e2e/playwright/pages/dashboard.page.ts | 7 + tests/e2e/playwright/pages/welcome.page.ts | 40 + tests/e2e/playwright/test-data/login.data.ts | 19 + .../e2e/playwright/tests/SH_appStore.spec.ts | 13 + .../playwright/tests/SH_wallet_ext.spec.ts | 460 +++ .../tests/SH_wallet_homepage.spec.ts | 25 + .../playwright/tests/SH_wallet_web.spec.ts | 1388 +++++++++ .../tests/SH_wallet_web_btc.spec.ts | 216 ++ .../tests/SH_wallet_web_eth.spec.ts | 281 ++ tests/e2e/playwright/tests/auth.setup.ts | 17 + tests/e2e/playwright/tests/commands.ts | 90 + tests/e2e/playwright/tests/fixtures.ts | 40 + tests/e2e/playwright/tests/utils.ts | 50 + 19 files changed, 5273 insertions(+) create mode 100644 .github/workflows/playwright.yml create mode 100644 playwright.config.ts create mode 100644 tests/e2e/playwright/pages/dashboard.page.ts create mode 100644 tests/e2e/playwright/pages/welcome.page.ts create mode 100644 tests/e2e/playwright/test-data/login.data.ts create mode 100644 tests/e2e/playwright/tests/SH_appStore.spec.ts create mode 100644 tests/e2e/playwright/tests/SH_wallet_ext.spec.ts create mode 100644 tests/e2e/playwright/tests/SH_wallet_homepage.spec.ts create mode 100644 tests/e2e/playwright/tests/SH_wallet_web.spec.ts create mode 100644 tests/e2e/playwright/tests/SH_wallet_web_btc.spec.ts create mode 100644 tests/e2e/playwright/tests/SH_wallet_web_eth.spec.ts create mode 100644 tests/e2e/playwright/tests/auth.setup.ts create mode 100644 tests/e2e/playwright/tests/commands.ts create mode 100644 tests/e2e/playwright/tests/fixtures.ts create mode 100644 tests/e2e/playwright/tests/utils.ts diff --git a/.env b/.env index a795f8618..7188a1f87 100644 --- a/.env +++ b/.env @@ -3,3 +3,4 @@ ETHERSCAN_API_KEY= WALLET_CONNECT_PROJECT_ID= TOKEN_SALES_URL_MAINNET= TOKEN_SALES_URL_TESTNET= +APPLITOOLS_KEY= diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..c03e7a5ad --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,25 @@ +name: Playwright tests +on: [push] +jobs: + main: + runs-on: ubuntu-latest + env: + APP_NAME: wallet + APPLITOOLS_KEY: ${{ secrets.APPLITOOLS_KEY }} + TOKEN_SALES_URL_MAINNET: ${{ secrets.TOKEN_SALES_URL_MAINNET }} + TOKEN_SALES_URL_TESTNET: ${{ secrets.TOKEN_SALES_URL_TESTNET }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 100 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + - name: Install app dependencies + run: npm ci + - name: Run parallel tests + run: npm run test:playwright:parallel diff --git a/.gitignore b/.gitignore index db939999f..ba2bf1ceb 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,8 @@ *.pub *.zip /artifacts + +# playwright +/playwright +/playwright-report +/test-results diff --git a/package-lock.json b/package-lock.json index 99ca20fe0..726b64d34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@ionic/vue": "^8.2.6", "@ionic/vue-router": "^8.2.6", "@ngraveio/bc-ur": "^1.1.13", + "@playwright/test": "^1.48.0", "@trapezedev/configure": "^7.0.10", "@vee-validate/i18n": "^4.13.2", "@vee-validate/rules": "^4.13.2", @@ -62,6 +63,7 @@ "ecpair": "^2.1.0", "lodash-es": "^4.17.21", "node-polyfill-webpack-plugin": "^3.0.0", + "playwright": "^1.42.1", "qr-code-styling": "github:aeternity/qr-code-styling", "qr-scanner": "^1.4.2", "satoshi-bitcoin": "^1.0.5", @@ -85,6 +87,7 @@ "webextension-polyfill": "^0.12.0" }, "devDependencies": { + "@applitools/eyes-playwright": "^1.33.2", "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.2", "@commitlint/cli": "^19.3.0", @@ -346,6 +349,1108 @@ "node": ">=16.15.1" } }, + "node_modules/@applitools/core": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@applitools/core/-/core-4.26.0.tgz", + "integrity": "sha512-lKvl4cV5fHw64Avx3tywPtVE2aJbjGw/MQzCEgV8OCrLIIEIk6Nt7O0z2flkQkwJaSyIyiTflZDf/zVPuDlHZA==", + "dev": true, + "dependencies": { + "@applitools/core-base": "1.20.0", + "@applitools/dom-capture": "11.5.2", + "@applitools/dom-snapshot": "4.11.11", + "@applitools/driver": "1.20.0", + "@applitools/ec-client": "1.9.16", + "@applitools/logger": "2.0.19", + "@applitools/nml-client": "1.8.19", + "@applitools/req": "1.7.4", + "@applitools/screenshoter": "3.10.0", + "@applitools/snippets": "2.6.3", + "@applitools/socket": "1.1.19", + "@applitools/spec-driver-webdriver": "1.1.20", + "@applitools/ufg-client": "1.15.0", + "@applitools/utils": "1.7.5", + "@types/ws": "8.5.5", + "abort-controller": "3.0.0", + "chalk": "4.1.2", + "node-fetch": "2.6.7", + "semver": "7.6.2", + "webdriver": "7.31.1", + "ws": "8.17.1", + "yargs": "17.7.2" + }, + "bin": { + "eyes-check-network": "dist/troubleshoot/check-network.js", + "eyes-core": "dist/cli/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/core-base": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@applitools/core-base/-/core-base-1.20.0.tgz", + "integrity": "sha512-NwGKWCqkaycg/ZqoO9wHFnb3FsdC7ukDvKkplBIjX4IgorSZ0lmez1AT8mHvVr6srwJappC+UHmNlT9py4CCJA==", + "dev": true, + "dependencies": { + "@applitools/image": "1.1.14", + "@applitools/logger": "2.0.19", + "@applitools/req": "1.7.4", + "@applitools/utils": "1.7.5", + "abort-controller": "3.0.0", + "throat": "6.0.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/core/node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@applitools/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@applitools/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@applitools/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@applitools/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@applitools/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/core/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@applitools/core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@applitools/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/core/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/@applitools/core/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/@applitools/core/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@applitools/css-tree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@applitools/css-tree/-/css-tree-1.1.4.tgz", + "integrity": "sha512-rH3aq/dkTweEUgS/MKuthD79CZDqpQVJlqmxqVxLZVAzbeFxYdTG/gnfG0zj6YJ025jzcPH2ktdW16Rl3QLutg==", + "dev": true, + "dependencies": { + "mdn-data": "2.1.0", + "source-map-js": "1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@applitools/css-tree/node_modules/mdn-data": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.1.0.tgz", + "integrity": "sha512-dbAWH6A+2NGuVJlQFrTKHJc07Vqn5frnhyTOGz+7BsK7V2hHdoBcwoiyV3QVhLHYpM/zqe2OSUn5ZWbVXLBB8A==", + "dev": true + }, + "node_modules/@applitools/css-tree/node_modules/source-map-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@applitools/dom-capture": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/@applitools/dom-capture/-/dom-capture-11.5.2.tgz", + "integrity": "sha512-RlbeqyCS27T4ptl4GlXhDQ3jC2cJ11trxDC2JKqWnjgcTh0Y62wuwWbsVnO17FKqiDRMqpdc4ET6cBx4gvgBPg==", + "dev": true, + "dependencies": { + "@applitools/dom-shared": "1.0.16", + "@applitools/functional-commons": "1.6.0" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/dom-shared": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@applitools/dom-shared/-/dom-shared-1.0.16.tgz", + "integrity": "sha512-P0JA5mq1f8rIi/xbh2+gCsEvv1CGenf0sGrC2UxXjmaFRpgoVS9BfpNg5aZyFJ9OPoi4qRMi9LCGsFiqZNNcTQ==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/dom-snapshot": { + "version": "4.11.11", + "resolved": "https://registry.npmjs.org/@applitools/dom-snapshot/-/dom-snapshot-4.11.11.tgz", + "integrity": "sha512-CW56By303USO36LQloYt+dKWe2wj8rLB702lCiF5yCZt1MQyvcB89DGouC2sKmZSpuzWlq2Vh/CjRPY9qRhH/Q==", + "dev": true, + "dependencies": { + "@applitools/css-tree": "1.1.4", + "@applitools/dom-shared": "1.0.16", + "@applitools/functional-commons": "1.6.0", + "pako": "1.0.11" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/driver": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@applitools/driver/-/driver-1.20.0.tgz", + "integrity": "sha512-GvlkMoqDuAwzzr/hYgRdqlzRGoQL6ZxP2ahaj+CL8HbVyWImZt81KpcD66pexjraIg8djzIaS4cAnLIL703FKA==", + "dev": true, + "dependencies": { + "@applitools/logger": "2.0.19", + "@applitools/snippets": "2.6.2", + "@applitools/utils": "1.7.5", + "semver": "7.6.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/driver/node_modules/@applitools/snippets": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.6.2.tgz", + "integrity": "sha512-r5CHjta0pWiQv+rMrdlgwTYq+2XMLrE4CQqsow9ZOOXBdnNgFTcDZlpoDbcWkep+s6EOd/BRMOJNjZPREgjfxA==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/driver/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@applitools/ec-client": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@applitools/ec-client/-/ec-client-1.9.16.tgz", + "integrity": "sha512-tBVH98ePPCOQi1sT8qwBHN1CGd+//Ar22wqGcsfx7mQZ1kOhS5q5gIyGnQy9Iqr9eTqqzRdTKGtbtWIADG7jlw==", + "dev": true, + "dependencies": { + "@applitools/core-base": "1.20.0", + "@applitools/driver": "1.20.0", + "@applitools/logger": "2.0.19", + "@applitools/req": "1.7.4", + "@applitools/socket": "1.1.19", + "@applitools/spec-driver-webdriver": "1.1.20", + "@applitools/tunnel-client": "1.5.10", + "@applitools/utils": "1.7.5", + "abort-controller": "3.0.0", + "webdriver": "7.31.1", + "yargs": "^17.7.2" + }, + "bin": { + "ec-client": "dist/cli/cli.js", + "eg-client": "dist/cli/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/eg-frpc": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@applitools/eg-frpc/-/eg-frpc-1.0.5.tgz", + "integrity": "sha512-9qUNiCK3R3VKxIAaLr5HO5QnUx6TioLFkJ2JcpU1ZqefApt1X2bdfS7eA4TGDXDWv/a0OIl2Lddzuo5/h3vbTw==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/eg-socks5-proxy-server": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@applitools/eg-socks5-proxy-server/-/eg-socks5-proxy-server-0.5.6.tgz", + "integrity": "sha512-SjjDBFeiKspX3nHKOoSQ+l4JUiJK3xJiWAEaR8b+GuMvnGFLnrvAECHhuXXG00+LwBJM8WKmfxEe17nvZe+nhg==", + "dev": true, + "dependencies": { + "binary": "^0.3.0", + "is-localhost-ip": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@applitools/execution-grid-tunnel": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@applitools/execution-grid-tunnel/-/execution-grid-tunnel-3.0.8.tgz", + "integrity": "sha512-4S6NcpxELH4NXketD3g6VUhWDUCuwAm4F1sCZdZLpPWOSMu5QwQDYUoe6/4t5KuktTQ4K7N90NmTzQrxiFtDKA==", + "dev": true, + "dependencies": { + "@applitools/eg-frpc": "1.0.5", + "@applitools/eg-socks5-proxy-server": "^0.5.5", + "@applitools/logger": "^1.0.12", + "dotenv": "^16.0.0", + "encoding": "^0.1.13", + "fastify": "^4.28.0", + "fastify-plugin": "^3.0.1", + "find-process": "^1.4.7", + "ini": "^3.0.0", + "node-cleanup": "^2.1.2", + "node-fetch": "^2.6.7", + "p-retry": "^4.6.2", + "teen_process": "^1.16.0", + "uuid": "^9.0.1" + }, + "bin": { + "run-execution-grid-tunnel": "scripts/run-execution-grid-tunnel.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/@applitools/logger": { + "version": "1.1.53", + "resolved": "https://registry.npmjs.org/@applitools/logger/-/logger-1.1.53.tgz", + "integrity": "sha512-4mlzYxc0MgM3WIxEwKqIjn9W7G7kMtQc2bFRxozViKOXypTfr72j8iODs88wcetP0GsXtplhZQ5/6aZN5WY9ug==", + "dev": true, + "dependencies": { + "@applitools/utils": "1.3.36", + "chalk": "4.1.2", + "debug": "4.3.3" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/@applitools/utils": { + "version": "1.3.36", + "resolved": "https://registry.npmjs.org/@applitools/utils/-/utils-1.3.36.tgz", + "integrity": "sha512-eROEssh7wIW+V87PvLiHI2hUPxqoBxXFMRx3+z5qOZqXUPSR1Uz7EMFwxZcDDR7T6C3O3UDckB2aVB5fJAg5JA==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@applitools/execution-grid-tunnel/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/eyes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@applitools/eyes/-/eyes-1.29.2.tgz", + "integrity": "sha512-SPZ2A2VV4T80k00qjv2pRVq8VXno+jALSd0/WcOCN2xgn/VZ/23rpApBRSM+i59Qo06vfrB7er28xPN808oHiQ==", + "dev": true, + "dependencies": { + "@applitools/core": "4.26.0", + "@applitools/logger": "2.0.19", + "@applitools/utils": "1.7.5", + "chalk": "4.1.2" + }, + "bin": { + "eyes": "dist/cli/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/eyes-playwright": { + "version": "1.33.2", + "resolved": "https://registry.npmjs.org/@applitools/eyes-playwright/-/eyes-playwright-1.33.2.tgz", + "integrity": "sha512-ThWwuhBFC0fET8qRG1tTDmW9wwQQn64acStTWJ2xEVGJv0kBdCnEHrxMlCqcb7DDr073lNgHc8qiEJrHhzs90w==", + "dev": true, + "dependencies": { + "@applitools/eyes": "1.29.2", + "@applitools/spec-driver-playwright": "1.5.3", + "@applitools/utils": "1.7.5", + "@inquirer/prompts": "7.0.1", + "chalk": "4.1.2", + "yargs": "17.7.2" + }, + "bin": { + "eyes-setup": "dist/fixture/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "@playwright/test": ">=1.0.0", + "playwright": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + }, + "playwright": { + "optional": true + } + } + }, + "node_modules/@applitools/eyes-playwright/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@applitools/eyes-playwright/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@applitools/eyes-playwright/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@applitools/eyes-playwright/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@applitools/eyes-playwright/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/eyes-playwright/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/eyes/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@applitools/eyes/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@applitools/eyes/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@applitools/eyes/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@applitools/eyes/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/eyes/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/functional-commons": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@applitools/functional-commons/-/functional-commons-1.6.0.tgz", + "integrity": "sha512-fwiF0CbeYHDEOTD/NKaFgaI8LvRcGYG2GaJJiRwcedKko16sQ8F3TK5wXfj2Ytjf+8gjwHwsEEX550z3yvDWxA==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@applitools/image": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@applitools/image/-/image-1.1.14.tgz", + "integrity": "sha512-dgCPvJTNFpf5FJYc/BRNrQqovqNIhQiQOKoeV3Ld/RtjbeiIJRqcpAML/HbbCDvBXHushtY4Ie8Q38QxgNWi7A==", + "dev": true, + "dependencies": { + "@applitools/utils": "1.7.5", + "bmpimagejs": "1.0.4", + "jpeg-js": "0.4.4", + "omggif": "1.0.10", + "png-async": "0.9.4" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/logger": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/@applitools/logger/-/logger-2.0.19.tgz", + "integrity": "sha512-MLuI7rgr79pRHH28bPtEkVQZww3v5+UinnSKfvaQvxeOg8AgyhIm+BZTf0LwIRw/HAGM9b1J+DQfNQ4+UYmUow==", + "dev": true, + "dependencies": { + "@applitools/utils": "1.7.5", + "chalk": "4.1.2", + "debug": "4.3.4" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@applitools/logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@applitools/logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@applitools/logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@applitools/logger/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@applitools/logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@applitools/nml-client": { + "version": "1.8.19", + "resolved": "https://registry.npmjs.org/@applitools/nml-client/-/nml-client-1.8.19.tgz", + "integrity": "sha512-e+rce5K0z+Mbk/Z88Tm7WM5ZoFeC1V4PJDroqBEdLoRxJCN/tBie1esg7GmttKJemEuIHljXt1H/VSKoCvDBRw==", + "dev": true, + "dependencies": { + "@applitools/logger": "2.0.19", + "@applitools/req": "1.7.4", + "@applitools/utils": "1.7.5" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/req": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@applitools/req/-/req-1.7.4.tgz", + "integrity": "sha512-KIoxWByRGuSqJI8ey0vHFtp6Pcu9hbiyCXMGo0G7myRafiNAbpgl9aZiBJz1lqEBJXnIj+NLFiR38zVdSBmlvw==", + "dev": true, + "dependencies": { + "@applitools/utils": "1.7.5", + "abort-controller": "3.0.0", + "http-proxy-agent": "5.0.0", + "https-proxy-agent": "5.0.1", + "node-fetch": "3.3.1" + }, + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/@applitools/req/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@applitools/req/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@applitools/req/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@applitools/req/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@applitools/req/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@applitools/req/node_modules/node-fetch": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz", + "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@applitools/screenshoter": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@applitools/screenshoter/-/screenshoter-3.10.0.tgz", + "integrity": "sha512-Woz9sPt2TR47WNIBV3iuSan9p2tET0LHMF00tDpkMBYJqZprAKOgy7T4hGneG23bNmSgIGU86arw+iSchibWqA==", + "dev": true, + "dependencies": { + "@applitools/image": "1.1.14", + "@applitools/logger": "2.0.19", + "@applitools/snippets": "2.6.2", + "@applitools/utils": "1.7.5" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/screenshoter/node_modules/@applitools/snippets": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.6.2.tgz", + "integrity": "sha512-r5CHjta0pWiQv+rMrdlgwTYq+2XMLrE4CQqsow9ZOOXBdnNgFTcDZlpoDbcWkep+s6EOd/BRMOJNjZPREgjfxA==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/snippets": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@applitools/snippets/-/snippets-2.6.3.tgz", + "integrity": "sha512-dijzo489BWMRfIRP0U4pITk/p2CJgTLBOeBp1D5bgMtDCdKhRaTRFpjf4xvrZoGE/vRs8bGEbtluk7gkSl/ozA==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/socket": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@applitools/socket/-/socket-1.1.19.tgz", + "integrity": "sha512-mBpNTAr8uHEmYv6a291Eqp9ErVWfLG8C+zFUjs4gzIYimtXn11JLxCIqy9FYfRQAz9nWMXdT+vxQtfQx84BcDw==", + "dev": true, + "dependencies": { + "@applitools/logger": "2.0.19", + "@applitools/utils": "1.7.5" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/spec-driver-playwright": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@applitools/spec-driver-playwright/-/spec-driver-playwright-1.5.3.tgz", + "integrity": "sha512-3BVaNngv75g9aI/aPD0BpJaWY9tLUvN56TvTStjRK0RGGTG+jkZtWrE4ZlTuQ8AS8QmzTyfQBjupArtxOIrezA==", + "dev": true, + "dependencies": { + "@applitools/driver": "1.20.0", + "@applitools/utils": "1.7.5" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "playwright": ">=1.0.0" + } + }, + "node_modules/@applitools/spec-driver-webdriver": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@applitools/spec-driver-webdriver/-/spec-driver-webdriver-1.1.20.tgz", + "integrity": "sha512-NNjwCooXKD6YpFaopjeWrL1veFWmoOgDCsEVrx1ozyc1jrJNn0OfzkCi0K0+FfIRbS2otG+S9NiSUQy2SsnDog==", + "dev": true, + "dependencies": { + "@applitools/driver": "1.20.0", + "@applitools/utils": "1.7.5", + "http-proxy-agent": "5.0.0", + "https-proxy-agent": "5.0.1" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "webdriver": ">=6.0.0" + } + }, + "node_modules/@applitools/spec-driver-webdriver/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@applitools/spec-driver-webdriver/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@applitools/spec-driver-webdriver/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@applitools/spec-driver-webdriver/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@applitools/tunnel-client": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@applitools/tunnel-client/-/tunnel-client-1.5.10.tgz", + "integrity": "sha512-M3CcJbB1ZuXLCi2grhxtehvPBRH3zTYZFIDu/H+qzKDazIbrbzL7e7nESd0jA2dkuwirndhytADlhKqfuqs6ng==", + "dev": true, + "dependencies": { + "@applitools/execution-grid-tunnel": "3.0.8", + "@applitools/logger": "2.0.19", + "@applitools/req": "1.7.4", + "@applitools/socket": "1.1.19", + "@applitools/utils": "1.7.5", + "abort-controller": "3.0.0", + "yargs": "17.7.2" + }, + "bin": { + "tunnel-client": "dist/cli/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/ufg-client": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@applitools/ufg-client/-/ufg-client-1.15.0.tgz", + "integrity": "sha512-UurhpteMfEbpRbMMDnlLDPLbBX2NAkVzYB5+oTaDMJC0s48Fx/zP2VnbecMlHR/2iDAmcDcVNP2VhgJIQ4EfMw==", + "dev": true, + "dependencies": { + "@applitools/css-tree": "1.1.4", + "@applitools/image": "1.1.14", + "@applitools/logger": "2.0.19", + "@applitools/req": "1.7.4", + "@applitools/utils": "1.7.5", + "@xmldom/xmldom": "0.8.10", + "abort-controller": "3.0.0", + "throat": "6.0.2" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@applitools/ufg-client/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@applitools/utils": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@applitools/utils/-/utils-1.7.5.tgz", + "integrity": "sha512-T02rEmbTPdmQcwKwaw1Issc6KIrw0SjsbjPLgjY2bF/maD+mTBlfq2p95ht+B897reaiMByD7DjCdmOpBtbRSw==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/@awesome-cordova-plugins/core": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@awesome-cordova-plugins/core/-/core-6.7.0.tgz", @@ -3510,6 +4615,41 @@ "@ethersproject/strings": "^5.7.0" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "dev": true, + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "dev": true + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dev": true, + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/@fontsource/ibm-plex-mono": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.0.13.tgz", @@ -3575,6 +4715,320 @@ "node": ">=6.9.0" } }, + "node_modules/@inquirer/checkbox": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.3.tgz", + "integrity": "sha512-CEt9B4e8zFOGtc/LYeQx5m8nfqQeG/4oNNv0PUvXGG0mys+wR/WbJ3B4KfSQ4Fcr3AQfpiuFOi3fVvmPfvNbxw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.0.tgz", + "integrity": "sha512-osaBbIMEqVFjTX5exoqPXs6PilWQdjaLhGtMDXMXg/yxkHXNq43GlxGyTA35lK2HpzUgDN+Cjh/2AmqCN0QJpw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.1.tgz", + "integrity": "sha512-rmZVXy9iZvO3ZStEe/ayuuwIJ23LSF13aPMlLMTQARX6lGUBDHGV8UB5i9MRrfy0+mZwt5/9bdy8llszSD3NQA==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@inquirer/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.0.tgz", + "integrity": "sha512-Z3LeGsD3WlItDqLxTPciZDbGtm0wrz7iJGS/uUxSiQxef33ZrBq7LhsXg30P7xrWz1kZX4iGzxxj5SKZmJ8W+w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.3.tgz", + "integrity": "sha512-MDszqW4HYBpVMmAoy/FA9laLrgo899UAga0itEjsYrBthKieDZNc0e16gdn7N3cQ0DSf/6zsTBZMuDYDQU4ktg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.0.tgz", + "integrity": "sha512-16B8A9hY741yGXzd8UJ9R8su/fuuyO2e+idd7oVLYjP23wKJ6ILRIIHcnXe8/6AoYgwRS2zp4PNsW/u/iZ24yg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.3.tgz", + "integrity": "sha512-HA/W4YV+5deKCehIutfGBzNxWH1nhvUC67O4fC9ufSijn72yrYnRmzvC61dwFvlXIG1fQaYWi+cqNE9PaB9n6Q==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.3.tgz", + "integrity": "sha512-3qWjk6hS0iabG9xx0U1plwQLDBc/HA/hWzLFFatADpR6XfE62LqPr9GpFXBkLU0KQUaIXZ996bNG+2yUvocH8w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.0.1.tgz", + "integrity": "sha512-cu2CpGC2hz7WTt2VBvdkzahDvYice6vYA/8Dm7Fy3tRNzKuQTF2EY3CV4H2GamveWE6tA2XzyXtbWX8+t4WMQg==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^4.0.1", + "@inquirer/confirm": "^5.0.1", + "@inquirer/editor": "^4.0.1", + "@inquirer/expand": "^4.0.1", + "@inquirer/input": "^4.0.1", + "@inquirer/number": "^3.0.1", + "@inquirer/password": "^4.0.1", + "@inquirer/rawlist": "^4.0.1", + "@inquirer/search": "^3.0.1", + "@inquirer/select": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.3.tgz", + "integrity": "sha512-5MhinSzfmOiZlRoPezfbJdfVCZikZs38ja3IOoWe7H1dxL0l3Z2jAUgbBldeyhhOkELdGvPlBfQaNbeLslib1w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.3.tgz", + "integrity": "sha512-mQTCbdNolTGvGGVCJSI6afDwiSGTV+fMLPEIMDJgIV6L/s3+RYRpxt6t0DYnqMQmemnZ/Zq0vTIRwoHT1RgcTg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.3.tgz", + "integrity": "sha512-OZfKDtDE8+J54JYAFTUGZwvKNfC7W/gFCjDkcsO7HnTH/wljsZo9y/FJquOxMy++DY0+9l9o/MOZ8s5s1j5wmw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", + "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, "node_modules/@intlify/bundle-utils": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.4.0.tgz", @@ -5824,6 +7278,20 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -6053,6 +7521,18 @@ "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", "dev": true }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -6379,6 +7859,18 @@ "npm": ">=7.10.0" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -6636,6 +8128,18 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -6716,6 +8220,16 @@ "@types/node": "*" } }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -6731,6 +8245,12 @@ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", "dev": true }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -6795,6 +8315,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.5", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", @@ -6820,6 +8349,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -6899,6 +8434,15 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -9230,6 +10774,152 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@wdio/config": { + "version": "7.31.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-7.31.1.tgz", + "integrity": "sha512-WAfswbCatwiaDVqy6kfF/5T8/WS/US/SRhBGUFrfBuGMIe+RRoHgy7jURFWSvUIE7CNHj8yvs46fLUcxhXjzcQ==", + "dev": true, + "dependencies": { + "@types/glob": "^8.1.0", + "@wdio/logger": "7.26.0", + "@wdio/types": "7.30.2", + "@wdio/utils": "7.30.2", + "deepmerge": "^4.0.0", + "glob": "^8.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/logger": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.26.0.tgz", + "integrity": "sha512-kQj9s5JudAG9qB+zAAcYGPHVfATl2oqKgqj47yjehOQ1zzG33xmtL1ArFbQKWhDG32y1A8sN6b0pIqBEIwgg8Q==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@wdio/logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@wdio/logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wdio/logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wdio/protocols": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-7.27.0.tgz", + "integrity": "sha512-hT/U22R5i3HhwPjkaKAG0yd59eaOaZB0eibRj2+esCImkb5Y6rg8FirrlYRxIGFVBl0+xZV0jKHzR5+o097nvg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/types": { + "version": "7.30.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz", + "integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==", + "dev": true, + "dependencies": { + "@types/node": "^18.0.0", + "got": "^11.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "^4.6.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wdio/utils": { + "version": "7.30.2", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.30.2.tgz", + "integrity": "sha512-np7I+smszFUennbQKdzbMN/zUL3s3EZq9pCCUcTRjjs9TE4tnn0wfmGdoz2o7REYu6kn9NfFFJyVIM2VtBbKEA==", + "dev": true, + "dependencies": { + "@wdio/logger": "7.26.0", + "@wdio/types": "7.30.2", + "p-iteration": "^1.1.8" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -9424,6 +11114,12 @@ "node": ">=6.5" } }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -10242,6 +11938,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "dev": true, + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -10690,6 +12396,19 @@ "node": "*" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -10794,6 +12513,12 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "node_modules/bmpimagejs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bmpimagejs/-/bmpimagejs-1.0.4.tgz", + "integrity": "sha512-21oKU7kbRt2OgOOj7rdiNr/yznDNUQ585plxR00rsmECcZr+6O1oCwB8OIoSHk/bDhbG8mFXIdeQuCPHgZ6QBw==", + "dev": true + }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", @@ -11187,6 +12912,15 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bufferutil": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", @@ -11213,6 +12947,48 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -11387,6 +13163,27 @@ "integrity": "sha512-GWlXN4wiz0vdWWXBU71Dvc1q3aBo0HytqwAZnXF1wOwjqNnDWA1vZ1gDMFLlqohak31VQzmhiYfiCX5QSSfagA==", "license": "MIT" }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chainsaw/node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -11929,6 +13726,27 @@ "node": ">=0.10.0" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14134,6 +15952,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -14866,6 +16693,27 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "devOptional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "devOptional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -16337,6 +18185,18 @@ "node >=0.6.0" ] }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "dev": true + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -16372,11 +18232,52 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "dev": true, + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, "node_modules/fast-redact": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", @@ -16390,6 +18291,12 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -16399,6 +18306,152 @@ "node": ">= 4.9.1" } }, + "node_modules/fastify": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.0.tgz", + "integrity": "sha512-MaaUHUGcCgC8fXQDsDtioaCcag1fmPJ9j64vAKunqZF4aSub040ZGi/ag8NGE2714yREPOKZuHCfpPzuUD3UQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.1.tgz", + "integrity": "sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==", + "dev": true + }, + "node_modules/fastify/node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/fastify/node_modules/pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/fastify/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dev": true, + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/fastify/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "dev": true + }, + "node_modules/fastify/node_modules/pino/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "dev": true + }, + "node_modules/fastify/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true + }, + "node_modules/fastify/node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/fastify/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastify/node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/fastify/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/fastify/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -16436,6 +18489,38 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -16598,6 +18683,113 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/find-process": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz", + "integrity": "sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "commander": "^5.1.0", + "debug": "^4.1.1" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/find-process/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/find-process/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/find-process/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/find-process/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/find-process/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-process/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", @@ -17004,6 +19196,18 @@ "node": ">= 12.20" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", @@ -17633,6 +19837,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -18108,6 +20337,12 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -18227,6 +20462,31 @@ "node": ">=0.10" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -19062,6 +21322,15 @@ "@babel/runtime": "^7.14.0" } }, + "node_modules/is-localhost-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-localhost-ip/-/is-localhost-ip-2.0.0.tgz", + "integrity": "sha512-vlgs2cSgMOfnKU8c1ewgKPyum9rVrjjLLW2HBdL5i0iAJjOs8NY55ZBd/hqUTaYR0EO9CKZd3hVSC2HlIbygTQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -21883,6 +24152,12 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", @@ -22207,6 +24482,15 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -22370,6 +24654,18 @@ "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true }, + "node_modules/ky": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.30.0.tgz", + "integrity": "sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -22523,6 +24819,32 @@ "libsodium": "0.7.6" } }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "dev": true, + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -23036,6 +25358,25 @@ "node": ">=8" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -23062,6 +25403,15 @@ "tslib": "^2.0.3" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -23773,6 +26123,12 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-cleanup": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", + "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", + "dev": true + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -24269,6 +26625,12 @@ "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==" }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "dev": true + }, "node_modules/on-exit-leak-free": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", @@ -24485,6 +26847,15 @@ "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", "dev": true }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -24494,6 +26865,15 @@ "node": ">=4" } }, + "node_modules/p-iteration": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/p-iteration/-/p-iteration-1.1.8.tgz", + "integrity": "sha512-IMFBSDIYcPNnW7uWYGrBqmvTiq7W0uB0fJn6shQZs7dlF3OvrHOre+JT9ikSZ7gZS3vWqclVgoQSvToJrns7uQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -24965,6 +27345,47 @@ "pathe": "^1.1.2" } }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -24986,6 +27407,12 @@ "node": ">=10.0.0" } }, + "node_modules/png-async": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/png-async/-/png-async-0.9.4.tgz", + "integrity": "sha512-B//AXX9TkneKfgtOpT1mdUnnhk2BImGD+a98vImsMU8uo1dBeHyW/kM2erWZ/CsYteTPU/xKG+t6T62heHkC3A==", + "dev": true + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -27053,6 +29480,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -27092,6 +29525,18 @@ "node": ">=10" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -27126,6 +29571,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -27331,6 +29785,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "dev": true, + "dependencies": { + "ret": "~0.4.0" + } + }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -27453,6 +29916,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -27652,6 +30121,12 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -29454,6 +31929,23 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/teen_process": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-1.16.0.tgz", + "integrity": "sha512-RnW7HHZD1XuhSTzD3djYOdIl1adE3oNEprE3HOFFxWs5m4FZsqYRhKJ4mDU2udtNGMLUS7jV7l8vVRLWAvmPDw==", + "dev": true, + "engines": [ + "node" + ], + "dependencies": { + "@babel/runtime": "^7.0.0", + "bluebird": "^3.5.1", + "lodash": "^4.17.4", + "shell-quote": "^1.4.3", + "source-map-support": "^0.5.3", + "which": "^2.0.2" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -29892,6 +32384,15 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -31552,6 +34053,26 @@ "npm": ">=6.12.0" } }, + "node_modules/webdriver": { + "version": "7.31.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-7.31.1.tgz", + "integrity": "sha512-nCdJLxRnYvOMFqTEX7sqQtF/hV/Jgov0Y6ICeOm1DMTlZSRRDaUsBMlEAPkEwif9uBJYdM0znv8qzfX358AGqQ==", + "dev": true, + "dependencies": { + "@types/node": "^18.0.0", + "@wdio/config": "7.31.1", + "@wdio/logger": "7.26.0", + "@wdio/protocols": "7.27.0", + "@wdio/types": "7.30.2", + "@wdio/utils": "7.30.2", + "got": "^11.0.2", + "ky": "0.30.0", + "lodash.merge": "^4.6.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/webextension-polyfill": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz", @@ -32637,6 +35158,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yorkie": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz", diff --git a/package.json b/package.json index df036e7e7..afef91d1f 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,18 @@ "build:web": "cross-env PLATFORM=web ionic build", "build-zip": "node scripts/build-zip.js", "serve": "cross-env PLATFORM=web ionic serve --port=8080", + "serve:production": "npm run build:web && npx serve dist/web/root -l 7771", "watch": "npm run build:extension:chrome -- --watch --mode production", "watch:ff": "npm run build:extension:ff -- --watch --mode production", "test:unit": "vue-cli-service test:unit", "test:unit:snapshot": "npm run test:unit -- --updateSnapshot", "test:e2e": "cross-env PLATFORM=web RUNNING_IN_TESTS=true vue-cli-service test:e2e", "test": "npm run test:unit && npm run test:e2e -- --headless", + "test:playwright:parallel": "npx playwright test web --grep-invert @sequence --project=chromium", + "test:playwright:sequence": "npx playwright test web --grep @sequence --workers=1 --project=chromium", + "test:playwright": "npm run test:parallel && npm run test:sequence", + "test:playwright:homepage": "npx playwright test homepage --project=chromium", + "test:playwright:extension": "npx playwright test ext --project=chromiumExt --workers=1", "build:Testnet": "cross-env NETWORK=Testnet npm run build:extension", "gen:capacitor-assets": "npx @capacitor/assets generate --iconBackgroundColor '#141414' --splashBackgroundColor '#141414' --splashBackgroundColorDark '#141414' --android --ios ", "cap-config": "trapeze run config.yaml", @@ -65,6 +71,7 @@ "@ionic/vue": "^8.2.6", "@ionic/vue-router": "^8.2.6", "@ngraveio/bc-ur": "^1.1.13", + "@playwright/test": "^1.48.0", "@trapezedev/configure": "^7.0.10", "@vee-validate/i18n": "^4.13.2", "@vee-validate/rules": "^4.13.2", @@ -85,6 +92,7 @@ "ecpair": "^2.1.0", "lodash-es": "^4.17.21", "node-polyfill-webpack-plugin": "^3.0.0", + "playwright": "^1.42.1", "qr-code-styling": "github:aeternity/qr-code-styling", "qr-scanner": "^1.4.2", "satoshi-bitcoin": "^1.0.5", @@ -108,6 +116,7 @@ "webextension-polyfill": "^0.12.0" }, "devDependencies": { + "@applitools/eyes-playwright": "^1.33.2", "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.2", "@commitlint/cli": "^19.3.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..602d38afb --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,54 @@ +import { defineConfig, devices } from '@playwright/test'; + +// eslint-disable-next-line import/no-extraneous-dependencies +require('dotenv').config(); + +/** See https://playwright.dev/docs/test-configuration. */ +export default defineConfig({ + testDir: './tests/e2e/playwright/tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 2 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { open: 'never' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + timeout: 70000, + webServer: { + command: 'npm run serve:production', + url: 'http://localhost:7771', + stdout: 'ignore', + stderr: 'pipe', + }, + use: { + testIdAttribute: 'data-cy', + baseURL: 'http://localhost:7771', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + + permissions: ['camera', 'clipboard-read'], + }, + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/user.json', + }, + dependencies: ['setup'], + }, + + { + name: 'chromiumExt', + testIgnore: ['**/*.setup.ts'], + use: { + ...devices['Desktop Chrome'], + deviceScaleFactor: undefined, + viewport: null, + }, + + }, + ], +}); diff --git a/tests/e2e/playwright/pages/dashboard.page.ts b/tests/e2e/playwright/pages/dashboard.page.ts new file mode 100644 index 000000000..9654c9d05 --- /dev/null +++ b/tests/e2e/playwright/pages/dashboard.page.ts @@ -0,0 +1,7 @@ +import { Page } from '@playwright/test'; + +export class Dashboard { + constructor(private page: Page) { } + + firefoxExtensionLink = this.page.getByRole('link', { name: 'Firefox' }); +} diff --git a/tests/e2e/playwright/pages/welcome.page.ts b/tests/e2e/playwright/pages/welcome.page.ts new file mode 100644 index 000000000..c36a1c9b2 --- /dev/null +++ b/tests/e2e/playwright/pages/welcome.page.ts @@ -0,0 +1,40 @@ +import { Page } from "@playwright/test"; +import { WalletPassword } from '../test-data/login.data'; + +export class WelcomePage { + constructor(private page: Page) { } + + // Download links + firefoxExtensionLink = this.page.getByRole('link', { name: 'Firefox' }); + chromeExtensionLink = this.page.getByRole('link', { name: 'Chrome' }); + appStoreLink = this.page.getByRole('link', { name: 'AppS tore' }); + googlePlayLink = this.page.getByRole('link', { name: 'Google Play' }); + + // web version terms acceptance + // acceptanceTerms = this.page.locator('label span'); + acceptanceTerms = this.page.locator('//label[@data-cy="checkbox"]//span'); + enterSeedPhraseBtn = this.page.getByRole('button', { name: 'Restore existing wallet Enter seed phrase' }); + seedPhraseInputField = this.page.getByPlaceholder('Enter or paste your seed phrase here'); + restoreWalletBtn = this.page.getByRole('button', { name: 'Restore wallet' }); + passwordField= this.page.locator('//input[@name="password"]'); + confirmPasswordField = this.page.locator('//input[@name="confirmPassword"]'); + continueToWallet = this.page.getByTestId('btn-set-password'); + skipPasswordBtn = this.page.getByTestId('btn-skip-password'); + + async recoveredWalletLogin(seedPhrase: string): Promise { + await this.acceptanceTerms.first().click(); + await this.enterSeedPhraseBtn.click(); + await this.seedPhraseInputField.fill(seedPhrase); + await this.restoreWalletBtn.click(); + await this.skipPasswordBtn.click(); + } + + async SetPassword() { + await this.passwordField.clear; + await this.passwordField.fill(WalletPassword.walletPassword); + await this.confirmPasswordField.clear; + await this.confirmPasswordField.fill(WalletPassword.walletPassword); + await this.continueToWallet.click(); + } + +} diff --git a/tests/e2e/playwright/test-data/login.data.ts b/tests/e2e/playwright/test-data/login.data.ts new file mode 100644 index 000000000..05c4b70b0 --- /dev/null +++ b/tests/e2e/playwright/test-data/login.data.ts @@ -0,0 +1,19 @@ +export const RestoreWalletData = { + // First test account data, mostly used for tests + testWalletSeed: 'cream anger stove cause myth spread citizen elephant twenty soda frown brain', + aeAccAddress: 'ak_2srCJ3y29JChwrPSjLbTC1ymqHNmJzmHgzYrjfPUhnwF3EE2TL', + aeAccAddressChainName: 'addinganewchainnameforfree.chain', + // Second account for the above wallet + secAeAccAddress: 'ak_2Ek1uRRRXiWwyXWvFjjQU7x9ZYaLx5b4ReTrTdyMxiwuDNNUp9', + secAeAccChainName: 'OldFootholdLandmarkBonyRelieveUnsorted.chain', + // Account with no txn, names, nothing. For checking new wallet info text + virginWalletSeed: 'foot praise enforce era want antenna dish truth exit actual tongue caught', + // ETH account data + ethAccAddress: '0x22Fa8128467F549eD9eAd9Ae9b3BEdFA62987c48', + secEthAccAddress: '0x56eFaF7299AA9C7b3F7935f88908596A8b11a016', +}; + +export const WalletPassword = { + // Set Wallet Password + walletPassword: '1234567890', +}; diff --git a/tests/e2e/playwright/tests/SH_appStore.spec.ts b/tests/e2e/playwright/tests/SH_appStore.spec.ts new file mode 100644 index 000000000..5343f7c39 --- /dev/null +++ b/tests/e2e/playwright/tests/SH_appStore.spec.ts @@ -0,0 +1,13 @@ +import { test, expect } from '@playwright/test'; + +test.describe('App Store checks', () => { + test('SH Wallet app store check', async ({ page }) => { + await page.goto('https://apps.apple.com/bg/app/superhero-wallet/id1502786641'); + await expect(page.getByRole('heading', { name: 'Superhero Wallet' })).toBeVisible(); + }); + + test('SH Wallet Play store check', async ({ page }) => { + await page.goto('https://play.google.com/store/apps/details?id=com.superhero.cordova&hl=en&gl=US'); + await expect(page.getByText('Superhero Wallet', { exact: true })).toBeVisible(); + }); +}); diff --git a/tests/e2e/playwright/tests/SH_wallet_ext.spec.ts b/tests/e2e/playwright/tests/SH_wallet_ext.spec.ts new file mode 100644 index 000000000..c00cd9317 --- /dev/null +++ b/tests/e2e/playwright/tests/SH_wallet_ext.spec.ts @@ -0,0 +1,460 @@ +/* eslint-disable no-console */ +import { test, expect } from './fixtures'; +import { + switchToTestnet, + interceptApiRequests, + recoverWalletExtension, + addBitcoinAccount, + addEthereumAccount, +} from './commands'; +import { RestoreWalletData } from '../test-data/login.data'; +import { WelcomePage } from '../pages/welcome.page'; + +test.describe('Chromium extension tests', () => { + const { aeAccAddress, secAeAccAddress } = RestoreWalletData; + const { ethAccAddress } = RestoreWalletData; + + test.beforeEach(async ({ page, context, extensionId }) => { + await interceptApiRequests(context); + await page.goto(`chrome-extension://${extensionId}/index.html`); + }); + + test('SH Wallet_new_wallet and dashboard', { tag: '@extension' }, async ({ page }) => { + // Create a new Wallet and check initial dashboard screen + await page.getByTestId('checkbox').click(); + await page.getByRole('button', { name: 'Create new wallet Get started with Superhero Wallet' }).click(); + const welcomePage = new WelcomePage(page); + await page.getByTestId('btn-add-aeternity').click(); + await welcomePage.SetPassword(); + await expect(page.getByRole('button', { name: 'Receive from existing wallet' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Send assets to others' })).toBeVisible(); + await expect(page.getByText('Claim your own .chain name')).toBeVisible(); + await expect(page.getByText('Back up now')).toBeVisible(); + // Check if a second account wasn't created + await expect(page.locator('//a[@data-cy="account-card-base" and @idx="1"]')).not.toBeVisible(); + }); + + test('SH Wallet_rst_receive_screen', async ({ page }) => { + // Check the AE Receive screen + // Open Receive screen from Dashboard + await recoverWalletExtension(page); + await page.getByRole('button', { name: 'Receive from existing wallet' }).click(); + await page.getByTestId('btn-close').click(); + // Open Receive screen from Account details page + await page.getByTestId('account-card-base').first().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive funds to public address'); + await expect(page.getByText('Request specific amount')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + + // Copy the account address + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + + // The ae account address from the URL to aescan + // and check if it match the current account address + const aeAddressStr = await page.locator('//div[@class="account-row"]//a').getAttribute('href') as string; + const aeAddress = aeAddressStr.slice(aeAddressStr.length - 53); + console.assert(aeAddress === aeAccAddress); + + // Open aeScan + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="account-row"]').click(); + const page1 = await page1Promise; + // Check if link to aescan.io does open the correct account page + if (await page1.getByAltText('æScan logo').first().isVisible() === true) { + console.assert(await page1.locator('//div[@class="copy-chip__text"]').nth(0).innerText() === aeAccAddress); + } else { + // eslint-disable-next-line no-unused-expressions + test.skip; + console.log('aeScan was not available'); + } + + // Receive amount is 0 + await page.locator('//input[@name="amount"]').fill('0'); + await expect(page.locator('//div[@name="amount"]//label[@data-cy="input-field-message"]')) + .toHaveText('Amount must be more than 0.'); + }); + + test('SH Wallet_rst_send_screen', async ({ page }) => { + // Check the AE Send screen with error input messages + // Open Send screen from dashboard + await recoverWalletExtension(page); + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.getByTestId('btn-close').click(); + // Open Send screen from Account details page + await page.getByTestId('account-card-base').first().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="send"]').click(); + + // Check Send main screen + await expect(page.getByRole('heading', { name: 'Send funds' })).toBeVisible(); + await expect(page.getByText('Transaction fee')).toBeVisible(); + + // Open Recipient help pop up, make screen check, close pop up + await page.getByTestId('address').getByRole('button').first().click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + + // Missing receiver address + await page.locator('//textarea[@data-cy="textarea"]').click(); + await page.locator('//input[@name="amount"]').click(); + await expect.soft(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Missing amount value + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Incorrect address string + await page.locator('//textarea[@data-cy="textarea"]').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid address or .chain name'); + + // Send amount over max available + await page.locator('//input[@name="amount"]').fill('12345678'); + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('Amount exceeds maximum available:'); + + // Send amount is 0 + await page.locator('//input[@name="amount"]').fill('0'); + await page.locator('//textarea[@data-cy="textarea"]').click(); + + // Sender and receiver the same address + await page.locator('//textarea[@data-cy="textarea"]').fill(aeAccAddress); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toHaveText("Sender's and recipient's addresses are the same. You are about to send AE to your own account."); + + // Open Payload help pop up, make screen check, close pop up + await page.locator('//div[@class="payload-add-wrapper"]/button[@class="btn-help button-plain btn-help"]').click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // Open Payload screen + await page.getByRole('button', { name: 'Payload' }).click(); + await page.getByLabel('Payload message').fill('Hello. This is a test!'); + // Close Payload pop up + await page.getByRole('button', { name: 'Done' }).click(); + }); + + test('SH Wallet_rst_send_funds_qr_scan', async ({ page }) => { + // Check the appearance of QR code in send funds modal + await recoverWalletExtension(page); + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.getByTestId('scan-button').click(); + await page.waitForTimeout(600); + await expect.soft(page.locator('//div[@class="title"]')).toContainText('Scan the recipient’s account address QR code in order to send æternity to them.'); + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_more_network change', async ({ page }) => { + // Check the Network settings, changing the network and adding custom network + // Open network menu + await recoverWalletExtension(page); + await page.getByRole('button', { name: 'Mainnet' }).click(); + await expect(page.getByRole('paragraph')).toContainText('Connect to network'); + + // Select Testnet + await page.getByRole('button', { name: 'Testnet Connect to testnet' }).click(); + await expect(page.locator('ion-toolbar')).toContainText('Testnet'); + + // Open Network menu and select Mainnet + await page.getByRole('button', { name: 'Testnet' }).first().click(); + await page.getByRole('button', { name: 'Mainnet Connect to mainnet' }).click(); + await expect(page.locator('ion-toolbar')).toContainText('Mainnet'); + + // Open Network menu and select more + await page.getByRole('button', { name: 'Mainnet' }).first().click(); + await page.locator('//a[@header="More"]').click(); + await expect(page.getByText('Connecting to the node...')).not.toBeVisible(); + await expect(page.getByText('Connected')).not.toBeVisible(); + + // Add custom network screen + await page.locator('//a[@data-cy="to-add"]').click(); + await page.getByLabel('Network name').fill('Regression test'); + await page.locator('//button[@data-cy="btn-add-network"]').click(); + + // Check if new network was added + await expect(page.locator('#app-wrapper')).toContainText('Regression test'); + + // Remove custom network + await page.locator('span').filter({ hasText: 'Regression test' }).getByRole('button').click(); + await expect(page.locator('#app-wrapper')).toContainText('Are you sure you want to delete this custom network?'); + await page.getByRole('button', { name: 'Confirm' }).click(); + // Removed Test network should not be listed anymore + await expect(page.locator('//p[@data-cy="network-name" and text()="Regression test"]')).not.toBeVisible(); + }); + + test('SH Wallet_rst_ae_airgap_account_import', async ({ page }) => { + // Check AirGap functionality screens + await recoverWalletExtension(page); + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-aeternity').click(); + await page.locator('//button[@header="Import AirGap accounts"]').click(); + // Check scan functionality availability + await page.getByTestId('scan-button').click(); + await expect(page.locator('//div[text()="Scan QR code"]')).toBeVisible(); + await page.getByTestId('btn-close').last().click(); + await page.getByTestId('input-wrapper').fill('2LpfcPxwkWDgnKsCXYUkxBtFPjZg8YD5DWqcLx8UjMnFWjpSVMbiVquMkCGQp66Xc7cVi2uV6Vr7hSAiD564Dxgt3tNimE2EyrWyasZQwH7uYprwkEuKhgqFAnLgY4Sg1sadtFskTyyZp4yJFZPqmCmY'); + await page.getByRole('button', { name: 'Import Account' }).last().click(); + await expect(page.locator('//div[@data-cy="account-name-number"]').last()) + .toContainText('AirGap account 1', { timeout: 8000 }); + // Check Send modal for AirGap account + await page.locator('//button[@data-cy="send"]').click(); + await expect(page.locator('//h2[text()="Send funds from AirGap account"]')).toBeVisible(); + await page.locator('//textarea[@data-cy="textarea"]').fill(aeAccAddress); + await page.locator('//input[@name="amount"]').fill('1'); + await page.locator('//button[@data-cy="next-step-button"]').click(); + await expect(page.locator('//div[@class="custom-header-title"]')) + .toContainText('Sign transaction'); + await page.locator('//div[@class="custom-header-title" and contains(text(),"Sign transaction")]//button[@class="btn-help button-plain btn-help"]').click(); + await page.getByRole('button', { name: 'OK' }).click(); + await page.locator('//button[@data-cy="next-step-button"]').click(); + await expect(page.locator('//div[@class="custom-header-title"]').nth(1)) + .toContainText('Broadcast transaction'); + }); + + test('SH Wallet_rst_ae_share_account_address', async ({ page }) => { + // Check Share screen + await recoverWalletExtension(page); + await switchToTestnet(page); + // Open Account details screen + await page.getByTestId('account-card-base').first().click(); + await page.locator('//button[@data-cy="share-address"]').click(); + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_ae_address_book', async ({ page }) => { + // Check Address book screens + await recoverWalletExtension(page); + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Address book + await page.locator('a').filter({ hasText: 'Address book' }).click(); + await expect(page.locator('//div[@class="truncate text"]')).toContainText('Address book'); + // Add ae address + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('ae test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill(secAeAccAddress); + await page.waitForTimeout(800); + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.waitForTimeout(800); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="0"]')).toContainText('ae test name'); + // Add BTC address + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('btc test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill('bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + await page.waitForTimeout(800); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="1"]')).toContainText('btc test name'); + // Add ETH address + await page.waitForTimeout(800); + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('eth test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill('0x56eFaF7299AA9C7b3F7935f88908596A8b11a016'); + await page.waitForTimeout(800); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="2"]')).toContainText('eth test name'); + // Filter saved accounts + await page.getByTestId('aeternity-filter').click(); + await expect(page.getByTestId('address-book-item')).toContainText('ae test name'); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="eth test name"]')).not.toBeVisible(); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="btc test name"]')).not.toBeVisible(); + await page.getByTestId('bitcoin-filter').click(); + await expect(page.getByTestId('address-book-item')).toContainText('btc test name'); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="eth test name"]')).not.toBeVisible(); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="ae test name"]')).not.toBeVisible(); + await page.getByTestId('ethereum-filter').click(); + await expect(page.getByTestId('address-book-item')).toContainText('eth test name'); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="btc test name"]')).not.toBeVisible(); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="ae test name"]')).not.toBeVisible(); + await page.getByTestId('all-filter').click(); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="0"]')).toContainText('ae test name'); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="1"]')).toContainText('btc test name'); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="2"]')).toContainText('eth test name'); + // Remove added address + await page.locator('//a[@data-cy="address-book-item" and @idx="0"]').click(); + await page.getByRole('button', { name: 'Delete address record' }).click(); + await page.waitForTimeout(800); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="ae test name"]')).not.toBeVisible(); + }); + + test('SH Wallet_rst_ae_address_book_send', async ({ page }) => { + // Check Address book screen in send modal + await recoverWalletExtension(page); + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Address book + await page.locator('a').filter({ hasText: 'Address book' }).click(); + // Add ae address + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('ae test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill(secAeAccAddress); + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.getByTestId('btn-close').click(); + // Open send screen + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.getByTestId('address-book-button').click(); + await page.locator('//button[@data-cy="address-book-item" and @idx="0"]').click(); + await page.waitForTimeout(1500); + await expect(page.locator('//div[@class="address-truncated"]').last()).toContainText(secAeAccAddress.substring(0, 6)); + }); + + /* Etherium Tests */ + + test('SH Wallet_rst_receive eth_mainnet', async ({ page }) => { + // Add ETH account to be visible + await recoverWalletExtension(page); + await addEthereumAccount(page, expect); + // Open Receive screen from Dashboard + await page.getByTestId('receive').click(); + await page.getByTestId('btn-close').click(); + // Open Receive screen from Account details page + await page.waitForTimeout(800); + await page.locator('//div[@data-cy="account-name-number" and contains(text(),"Ethereum account")]').click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + // Check for elements + await expect(page.getByRole('heading')).toContainText('Receive Ethereum to public address'); + await expect(page.getByText('Request specific amount')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + // Link to ETH explorer + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="address-truncated address"]').click(); + const page1 = await page1Promise; + await page.waitForTimeout(800); + + if (await page1.locator('//header[@id="masterHeader"]').isVisible() === true) { + console.assert(await page1.locator('//span[@id="mainaddress"]').nth(0).innerText() === ethAccAddress); + } else { + // eslint-disable-next-line no-unused-expressions + test.skip; + console.log('Etherscan was not available'); + } + }); + + test('SH Wallet_rst_eth_send_screen_mainnet', async ({ page }) => { + // Add ETH account to be visible + await recoverWalletExtension(page); + await addEthereumAccount(page, expect); + // Open Send screen from dashboard + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByTestId('btn-close').click(); + // Open Send screen from Account details page + await page.locator('//div[@data-cy="account-name-number" and contains(text(),"Ethereum account")]').click(); + await page.locator('//div[@class="horizontal-scroll buttons"]/button[@data-cy="send"]').click(); + + // Check Send main screen + await expect(page.getByRole('heading', { name: 'Send Ethereum' })).toBeVisible(); + await page.waitForTimeout(600); + await expect(page.getByText('Estimated transaction fee ')).toBeVisible(); + await expect(page.getByText('Maximum transaction fee ')).toBeVisible(); + + // Open Recipient help pop up, make screen check, close pop up + await page.getByTestId('address').getByRole('button').first().click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + // Missing receiver address + await page.locator('//textarea[@data-cy="textarea"]').click(); + await page.locator('//input[@name="amount"]').click(); + await expect.soft(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Missing amount value + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Incorrect address string + await page.locator('//textarea[@data-cy="textarea"]').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid ethereum address'); + + // Sender and receiver the same address + await page.locator('//textarea[@data-cy="textarea"]').fill('0x22Fa8128467F549eD9eAd9Ae9b3BEdFA62987c48'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toHaveText("Sender's and recipient's addresses are the same. You are about to send ETH to your own account."); + }); + + /* Bitcoin Tests */ + + test('SH Wallet_rst_receive btc_mainnet', async ({ page }) => { + // Add BTC account to be visible + await recoverWalletExtension(page); + await addBitcoinAccount(page, expect); + // Open Receive screen from Dashboard + await page.getByTestId('receive').click(); + await page.getByTestId('btn-close').click(); + // Open Receive screen from Account details page + await page.locator('//a[@data-cy="account-card-base"]//span[text()="BTC"]').click(); + await page.getByTestId('receive').last().click(); + await expect(page.getByRole('heading')).toContainText('Receive Bitcoin to public address'); + await expect(page.getByText('Request specific amount')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + + // Link to BTC explorer + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="address-truncated address"]').click(); + const page1 = await page1Promise; + await page.waitForTimeout(800); + if (await page1.locator('//div[@id="explorer"]//a[@class="navbar-brand"]').isVisible() === true) { + console.assert(await page1.locator('//div[@class="addr-page"]').nth(0).innerText() === 'bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + } else { + // eslint-disable-next-line no-unused-expressions + test.skip; + console.log('Blockstream was not available'); + } + }); + + test('SH Wallet_rst_btc_send_screen', async ({ page }) => { + // Add BTC account to be visible + await recoverWalletExtension(page); + await addBitcoinAccount(page, expect); + // Open Send screen from dashboard + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByTestId('btn-close').click(); + // Open Send screen from Account details page + await page.locator('//a[@data-cy="account-card-base"]//span[text()="BTC"]').click(); + await page.locator('//div[@class="horizontal-scroll buttons"]/button[@data-cy="send"]').click(); + + // Check Send main screen + await expect(page.getByRole('heading', { name: 'Send Bitcoin' })).toBeVisible(); + await expect(page.getByText('Transaction fee')).toBeVisible(); + + // Open Recipient help pop up, make screen check, close pop up + await page.locator('//div[@class="transfer-send-recipient"]//button[@class="btn-help button-plain btn-help"]').click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + // Missing receiver address + await page.getByTestId('textarea').click(); + await page.getByTestId('input').click(); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + // Missing amount value + await page.getByTestId('textarea').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Incorrect address string + await page.getByTestId('textarea').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid bitcoin address'); + + // Sender and receiver the same address + await page.getByTestId('textarea').fill('bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toHaveText("Sender's and recipient's addresses are the same. You are about to send BTC to your own account."); + + // Close Send screen + await page.getByTestId('btn-close').nth(1).click(); + }); +}); diff --git a/tests/e2e/playwright/tests/SH_wallet_homepage.spec.ts b/tests/e2e/playwright/tests/SH_wallet_homepage.spec.ts new file mode 100644 index 000000000..69006b75b --- /dev/null +++ b/tests/e2e/playwright/tests/SH_wallet_homepage.spec.ts @@ -0,0 +1,25 @@ +import { test } from '@playwright/test'; + +test('SH Wallet production web platforms check', async ({ context, page }) => { + await page.goto('https://wallet.superhero.com/'); + + await page.getByRole('link', { name: 'Firefox' }).click(); + await Promise.all([ + context.waitForEvent('page'), + page.getByRole('heading', { name: 'Superhero by Superhero Wallet' }).isVisible()]); + + await page.getByRole('link', { name: 'Chrome' }).click(); + await Promise.all([ + context.waitForEvent('page'), + page.getByRole('heading', { name: 'Superhero' }).isVisible()]); + + await page.getByRole('link', { name: 'App Store' }).click(); + await Promise.all([ + context.waitForEvent('page'), + page.getByRole('heading', { name: 'Superhero Wallet 4+' }).isVisible()]); + + await page.getByRole('link', { name: 'Google Play' }).click(); + await Promise.all([ + context.waitForEvent('page'), + page.getByText('Superhero Wallet').isVisible()]); +}); diff --git a/tests/e2e/playwright/tests/SH_wallet_web.spec.ts b/tests/e2e/playwright/tests/SH_wallet_web.spec.ts new file mode 100644 index 000000000..697db3bde --- /dev/null +++ b/tests/e2e/playwright/tests/SH_wallet_web.spec.ts @@ -0,0 +1,1388 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable no-console */ +/* eslint-disable no-await-in-loop */ +import { test, expect } from '@playwright/test'; +import { + Configuration, EyesRunner, Eyes, Target, MatchLevel, +} from '@applitools/eyes-playwright'; +import BigNumber from 'bignumber.js'; + +import { RestoreWalletData } from '../test-data/login.data'; +import { WelcomePage } from '../pages/welcome.page'; +import { + openWalletSettings, + resetWallet, + switchToTestnet, + interceptApiRequests, + revokePendingProposal, + addBitcoinAccount, + addEthereumAccount, + addAeternityAccount, +} from './commands'; +import { genRandomString, initializeRunnerAndConfig } from './utils'; + +let Config: Configuration; +let Runner: EyesRunner; + +test.beforeAll(() => { + [Config, Runner] = initializeRunnerAndConfig('AE'); +}); + +test.describe('SH Wallet checks', () => { + let eyes: Eyes; + + const { aeAccAddress, secAeAccAddress, secAeAccChainName } = RestoreWalletData; + const txnValue = 0.002; + + test.beforeEach(async ({ page, context }) => { + await interceptApiRequests(context); + eyes = new Eyes(Runner, Config); + + // Start Applitools Visual AI Test + // Args: Playwright Page, App Name, Test Name, Viewport Size for local driver + await eyes.open(page, 'SH Wallet', test.info().title, { width: 360, height: 600 }); + await page.goto('./'); + }); + + test.afterEach(async () => { + // End Applitools Visual AI Test + await eyes.closeAsync(); + }); + + test('SH Wallet_new_wallet and dashboard', async ({ page }) => { + // Create a new Wallet and check initial dashboard screen + await resetWallet(page); + const welcomePage = new WelcomePage(page); + await welcomePage.acceptanceTerms.first().click(); + await eyes.check('Terms', Target.window().fully()); + // Act + await page.getByRole('button', { name: 'Create new wallet Get started with Superhero Wallet' }).click(); + // Select account to be added + await page.getByTestId('btn-add-aeternity').click(); + await page.getByTestId('btn-help').click(); + await eyes.check('Set a password', Target.window().fully()); + await page.getByTestId('btn-close').click(); + await welcomePage.SetPassword(); + // Assert + await expect(page.getByRole('button', { name: 'Receive from existing wallet' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Send assets to others' })).toBeVisible(); + // await expect(page.getByText('Claim your own .chain name')).toBeVisible(); + await expect(page.getByText('Back up now')).toBeVisible(); + // Check if a second account wasn't created + await expect(page.locator('//a[@data-cy="account-card-base" and @idx="1"]')).not.toBeVisible(); + await eyes.check('Dashboard', Target.window().fully()); + }); + + test('SH Wallet_rst_wallet', async ({ page }) => { + // Recover Wallet from seed phrase and check if account address is correct + await resetWallet(page); + const welcomePage = new WelcomePage(page); + await welcomePage.recoveredWalletLogin(RestoreWalletData.testWalletSeed); + await expect(page.getByText('Connected')).not.toBeVisible(); + await eyes.check('RecoverWallet', Target.window().fully()); + await page.getByRole('button', { name: 'Receive from existing wallet' }).click(); + await expect(page.locator('a').filter({ hasText: aeAccAddress })).toBeVisible(); + }); + + test('SH Wallet_rst_receive_screen', async ({ page }) => { + // Check the AE Receive screen + // Open Receive screen from Dashboard + await page.getByRole('button', { name: 'Receive from existing wallet' }).click(); + await page.getByTestId('btn-close').click(); + + // Open Receive screen from Account details page + await page.getByTestId('account-card-base').first().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive funds to public address'); + await expect(page.getByText('Request specific amount')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + + // Copy the account address + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + await eyes.check('Receive_ae', Target.window().matchLevel(MatchLevel.Layout)); + + // The the ae account address from the URL to aescan + // and check if it match the current account address + const aeAddressStr = await page.locator('//div[@class="account-row"]//a').getAttribute('href') as string; + const aeAddress = aeAddressStr.slice(aeAddressStr.length - 53); + console.assert(aeAddress === aeAccAddress); + + // Open aeScan + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="account-row"]').click(); + const page1 = await page1Promise; + // Check if link to aescan.io does open the correct account page + if (await page1.getByAltText('æScan logo').first().isVisible() === true) { + console.assert(await page1.locator('//div[@class="copy-chip__text"]').nth(0).innerText() === aeAccAddress); + } else { + test.skip; + console.log('aeScan was not available'); + } + + // Receive amount is 0 + await page.locator('//input[@name="amount"]').fill('0'); + await expect(page.getByTestId('input-field-message')).toHaveText('Amount must be more than 0.'); + }); + + test('SH Wallet_rst_send_screen', async ({ page }) => { + // Check the AE Send screen with error input messages + // Open Send screen from dashboard + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.getByTestId('btn-close').click(); + // Open Send screen from Account details page + await page.getByTestId('account-card-base').first().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="send"]').click(); + + // Check Send main screen + await eyes.check('Send_ae', Target.window().matchLevel(MatchLevel.Layout)); Target.window().fully(); + await expect(page.getByRole('heading', { name: 'Send funds' })).toBeVisible(); + await expect(page.getByText('Transaction fee')).toBeVisible(); + + // Open Recipient help pop up, make screen check, close pop up + await page.getByTestId('address').getByRole('button').first().click(); + await eyes.check('Recipient_help', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + + // Missing receiver address + await page.locator('//textarea[@data-cy="textarea"]').click(); + await page.locator('//input[@name="amount"]').click(); + await expect.soft(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Missing amount value + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Incorrect address string + await page.locator('//textarea[@data-cy="textarea"]').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid address or .chain name'); + + // Send amount over max available + await page.locator('//input[@name="amount"]').fill('12345678'); + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('Amount exceeds maximum available:'); + + // Send amount is 0 + await page.locator('//input[@name="amount"]').fill('0'); + await page.locator('//textarea[@data-cy="textarea"]').click(); + // expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')). + // toHaveText('Amount must be more than 0.'); + + // Sender and receiver the same address + await page.locator('//textarea[@data-cy="textarea"]').fill(aeAccAddress); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toHaveText("Sender's and recipient's addresses are the same. You are about to send AE to your own account."); + + // Open Payload help pop up, make screen check, close pop up + await page.locator('//div[@class="payload-add-wrapper"]/button[@class="btn-help button-plain btn-help"]').click(); + await eyes.check('Payload_help', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + + // Open Payload screen + await page.getByRole('button', { name: 'Payload' }).click(); + await page.getByLabel('Payload message').fill('Hello. This is a test!'); + await eyes.check('Add_Payload', Target.window().fully()); + // Close Payload pop up + await page.getByRole('button', { name: 'Done' }).click(); + await page.waitForTimeout(500); + // Check Send screen with payload message + await eyes.check('Send_ae_Payload', Target.window().matchLevel(MatchLevel.Strict)); + // Close Send screen + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_account_details', async ({ page }) => { + // Check the Account Asset tab + await resetWallet(page); + const welcomePage = new WelcomePage(page); + await welcomePage.recoveredWalletLogin(RestoreWalletData.virginWalletSeed); + // Select account to be added + await page.getByTestId('btn-add-aeternity').click(); + // Open Account details screen + await page.getByTestId('account-card-base').first().click(); + await page.waitForTimeout(500); + // If the loader takes longer then 10 sec to show the no transactions message, + // check if loader is visible + if (await page.locator('//p[@class="message"]').isVisible({ timeout: 10000 }) === true) { + await expect(page.locator('//p[@class="message"]')).toContainText('There are no recent transactions for this account.'); + } else { + console.log('Loader is visible!'); + } + await eyes.check('acc_details', Target.window().fully()); + + // Open Assets tab + await page.getByRole('link', { name: 'Assets' }).click(); + await eyes.check('acc_details_assets', Target.window().fully()); + + // Open Names tab + await page.getByRole('link', { name: 'Names' }).click(); + await expect(page.locator('//div[@class="register-name"]')).toBeVisible(); + await eyes.check('acc_details_names_mynames', Target.window().matchLevel(MatchLevel.Layout)); + await expect(page.getByRole('paragraph')).toContainText('There are no .chain names owned by this account. Would you like to register one?'); + + // Open Auctions + await page.getByText('Auctions').click(); + await eyes.check('acc_details_names_auctions', Target.window().matchLevel(MatchLevel.Layout)); + + // Open Register name + await page.getByText('Register name').click(); + await page.waitForTimeout(100); + await expect(page.getByRole('paragraph')).toContainText('Ownership of .chain names shorter than 13 characters will be acquired at an auction by the account that places the highest bid.'); + await eyes.check('acc_details_names_registerName', Target.window().fully()); + await page.locator('//div[@class="claim"]//input[@data-cy="input"]').click(); + await page.getByText('Register name').first().click(); + await expect(page.getByText('This field is required')).toBeVisible(); + await page.locator('//div[@class="claim"]//input[@data-cy="input"]').fill('A'); + await expect(page.getByText('AE balance is not enough to pay for transaction fee')).toBeVisible(); + + // Open auto extend name help pop up + await page.locator('label').filter({ hasText: 'Auto extend name' }).getByRole('button').click(); + await eyes.check('acc_details_names_autoExtendName_popUp', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + + // Close Account details screen + await page.getByRole('button').first().click(); + }); + + test('SH Wallet_rst_network_btn', async ({ page }) => { + // Check the possibility to change to Network from Mainnet to Testnet on Dashboard + // Change network to Testnet + await page.getByRole('button', { name: ' Mainnet' }).hover({ timeout: 5000 }); + await eyes.check('Network button indicator hover effect_Mainnet', Target.window().matchLevel(MatchLevel.Layout)); + await switchToTestnet(page); + await page.getByRole('button', { name: ' Testnet', exact: true }).hover(); + await eyes.check('Network button indicator hover effect_Testnet', Target.window().matchLevel(MatchLevel.Layout)); + }); + + test('SH Wallet_rst_balance_check_mainnet', async ({ page }) => { + // Check the Account balance of the first Wallet acc against the account amount info in aeScan + // Open Account details screen + + await page.getByTestId('account-card-base').first().click(); + // Open aeScan of this account and take the AE balance + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + // Opens new tab with aescan + const page2Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="account-row"]').click(); + const page2 = await page2Promise; + const aeScanBalance: string = await page2.locator('//tr[@class="account-details-panel__row"]//div[@class="price-label"]').first() + .textContent() as string; + // Convert the balance string from aeScan + // to the same format as it is in the Wallet account details page + let aeScan: string; + const aeScanInt = aeScanBalance.substring(0, aeScanBalance.indexOf('.')); + + // Check if the amount in aeScan is a whole number if so add ".00" to it + if (aeScanInt.length === 0) { + aeScan = aeScanBalance.substring(0, aeScanBalance.indexOf(' ')); + aeScan += '.00'; + } else { + const aeScanFloat = aeScanBalance.slice(aeScanBalance.indexOf('.'), 5); + const aeScanToken = parseFloat(aeScanInt.concat(aeScanFloat)); + aeScan = aeScanToken.toFixed(2); + } + await page.getByTestId('btn-close').nth(1).click(); + await page.locator('//div[@class="transaction-token-rows"]').nth(0).isVisible(); + // Take the account balance which are two strings and concatenate them + const aeTokenInt: string = await page.locator('//div[@class="account-details"]//div[@data-cy="balance-info"]//span[@class="asset-integer"]') + .textContent() as string; + const aeTokenFraction: string = await page.locator('//div[@class="account-details"]//div[@data-cy="balance-info"]//span[@class="asset-fractional"]') + .textContent() as string; + const aeToken = aeTokenInt.concat(aeTokenFraction); + // Compare the aeScan balance with the wallet account balance + expect(aeScan).toBe(aeToken); + }); + + test('SH Wallet_rst_balance_check_testnet', async ({ page }) => { + await switchToTestnet(page); + // Open Account details screen + await page.getByTestId('account-card-base').first().click(); + // Open aeScan of this account and take the AE balance + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + // Opens new tab with aescan + const page2Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="account-row"]').click(); + const page2 = await page2Promise; + await page.waitForTimeout(5000); + const aeScanBalance: string = await page2.locator('//tr[@class="account-details-panel__row"]//div[@class="price-label"]').first() + .textContent() as string; + // Convert the balance string from aeScan + // to the same format as it is in the Wallet account details page + let aeScan: string; + const aeScanInt = aeScanBalance.substring(0, aeScanBalance.indexOf('.')); + + // Check if the amount in aeScan is a whole number if so add ".00" to it + if (aeScanInt.length === 0) { + aeScan = aeScanBalance.substring(0, aeScanBalance.indexOf(' ')); + aeScan += '.00'; + } else { + // Align the aeScan amount to the shown Wallet amount + const aeScanString = aeScanBalance.slice(0, -3); + const aeScanFloat = parseFloat(aeScanString); + aeScan = aeScanFloat.toFixed(2); + } + await page.getByTestId('btn-close').nth(1).click(); + await page.locator('//div[@class="transaction-token-rows"]').nth(0).isVisible(); + // Take the account balance which are two strings and concatenate them + const aeTokenInt: string = await page.locator('//div[@class="account-details"]//div[@data-cy="balance-info"]//span[@class="asset-integer"]') + .textContent() as string; + const aeTokenFraction: string = await page.locator('//div[@class="account-details"]//div[@data-cy="balance-info"]//span[@class="asset-fractional"]') + .textContent() as string; + const aeToken = aeTokenInt.concat(aeTokenFraction); + // Compare the aeScan balance with the wallet account balance + expect(aeScan).toBe(aeToken); + }); + + test('SH Wallet_rst_more_seed phrase verification', async ({ page }) => { + // Check the Seed Phrase verification function in the Wallet settings. + // It is using the Seed Phrase of the Test Wallet, + // that's why the seed phrase is hard coded in this test. + + await openWalletSettings(page); + await page.locator('a').filter({ hasText: 'Seed phrase' }).click(); + await eyes.check('Seed phrase info msg', Target.window().fully()); + await page.getByText('Show seed phrase').click(); + await eyes.check('Seed phrase', Target.window().fully()); + // Verify your seed phrase screen + await page.getByText('Verify your seed phrase', { exact: true }).click(); + + // Enter seed phrase + await page.getByRole('button', { name: 'cream' }).click(); + await page.getByRole('button', { name: 'anger' }).click(); + await page.getByRole('button', { name: 'stove' }).click(); + await page.getByRole('button', { name: 'cause' }).click(); + await page.getByRole('button', { name: 'myth' }).click(); + await page.getByRole('button', { name: 'spread' }).click(); + await page.getByRole('button', { name: 'citizen' }).click(); + await page.getByRole('button', { name: 'elephant' }).click(); + await page.getByRole('button', { name: 'twenty' }).click(); + await page.getByRole('button', { name: 'soda' }).click(); + await page.getByRole('button', { name: 'frown' }).click(); + await page.getByRole('button', { name: 'brain' }).click(); + await page.getByRole('button', { name: 'Verify seed phrase' }).click(); + await expect(page.getByText('Seed phrase has been verified!')).toBeVisible(); + await eyes.check('Seed phrase verified', Target.window().matchLevel(MatchLevel.Layout)); + await page.getByTestId('btn-close').click(); + }); + + test('SH Wallet_rst_more_seed phrase verification_failure', async ({ page }) => { + // Check the Seed Phrase verification function in the Wallet settings. + // The test is using the a Test Seed in wrong order, aiming the Seed verification to fail. + + await openWalletSettings(page); + await page.locator('a').filter({ hasText: 'Seed phrase' }).click(); + await eyes.check('Seed phrase info msg', Target.window().fully()); + await page.getByText('Show seed phrase').click(); + await eyes.check('Seed phrase', Target.window().fully()); + // Verify your seed phrase screen + await page.getByText('Verify your seed phrase', { exact: true }).click(); + // Enter seed phrase in wrong order + await page.getByRole('button', { name: 'cream' }).click(); + await page.getByRole('button', { name: 'anger' }).click(); + await page.getByRole('button', { name: 'stove' }).click(); + await page.getByRole('button', { name: 'cause' }).click(); + await page.getByRole('button', { name: 'myth' }).click(); + await page.getByRole('button', { name: 'spread' }).click(); + await page.getByRole('button', { name: 'citizen' }).click(); + await page.getByRole('button', { name: 'elephant' }).click(); + await page.getByRole('button', { name: 'twenty' }).click(); + await page.getByRole('button', { name: 'frown' }).click(); + await page.getByRole('button', { name: 'soda' }).click(); + await page.getByRole('button', { name: 'brain' }).click(); + await page.getByRole('button', { name: 'Verify seed phrase' }).click(); + await expect(page.getByText('Seed phrase words are not in the correct order. Please enter your seed phrase again!')).toBeVisible(); + await eyes.check('Seed phrase again', Target.window().fully()); + await page.getByTestId('btn-close').click(); + }); + + test('SH Wallet_rst_more_network change', async ({ page }) => { + // Check the Network settings, changing the network and adding custom network + // Open network menu + + await page.getByRole('button', { name: 'Mainnet' }).click(); + await expect(page.getByRole('paragraph')).toContainText('Connect to network'); + await eyes.check('Connect to network menu', Target.window().fully()); + + // Select Testnet + await page.getByRole('button', { name: 'Testnet Connect to testnet' }).click(); + await expect(page.locator('ion-toolbar')).toContainText('Testnet'); + + // Open Network menu and select Mainnet + await page.getByRole('button', { name: 'Testnet' }).first().click(); + await page.getByRole('button', { name: 'Mainnet Connect to mainnet' }).click(); + await expect(page.locator('ion-toolbar')).toContainText('Mainnet'); + + // Open Network menu and select more + await page.getByRole('button', { name: 'Mainnet' }).first().click(); + await page.locator('//a[@header="More"]').click(); + await expect(page.getByText('Connecting to the node...')).not.toBeVisible(); + await expect(page.getByText('Connected')).not.toBeVisible(); + await eyes.check('Network selection screen', Target.window().fully()); + + // Add custom network screen + await page.locator('//a[@data-cy="to-add"]').click(); + await page.getByLabel('Network name').fill('Regression test'); + await page.locator('//button[@data-cy="btn-add-network"]').click(); + + // Check if new network was added + await expect(page.locator('#app-wrapper')).toContainText('Regression test'); + + // Remove custom network + await page.locator('span').filter({ hasText: 'Regression test' }).getByRole('button').click(); + await expect(page.locator('#app-wrapper')).toContainText('Are you sure you want to delete this custom network?'); + await eyes.check('Remove custom network', Target.window().fully()); + await page.getByRole('button', { name: 'Confirm' }).click(); + // Removed Test network should not be listed anymore + await expect(page.locator('//p[@data-cy="network-name" and text()="Regression test"]')).not.toBeVisible(); + }); + + test('SH Wallet_rst_more_permissions', async ({ page }) => { + // Check Permission screen and adding custom permission + await openWalletSettings(page); + // Open Permissions + await page.locator('a').filter({ hasText: 'Permissions' }).click(); + await eyes.check('Permissions', Target.window().fully()); + await page.getByText('Add permission').click(); + await page.getByLabel('Permissions for URL').fill('chat.superhero.com'); + await page.getByLabel('Custom name').fill('Custom perm test'); + await page.locator('//div[@class="switch-button"]//label//span').last().click(); + await page.locator('//div[@class="switch-button"]//label//span').last().click(); + await page.locator('//div[@class="switch-button"]//label//span').last().click(); + await page.locator('//div[@class="switch-button"]//label//span').last().click(); + await page.locator('//input[@name="transactionSignLimit"]').fill('1000'); + await eyes.check('Custom permission', Target.region(page.locator('//div[@class="container"]')).fully().matchLevel(MatchLevel.Layout)); + await page.locator('//button[text()="Confirm"]').click(); + // Check if new custom permission was added + await page.waitForTimeout(500); + await expect(page.locator('//button//div/div[text()="Custom perm test"]')).toBeVisible(); + // Remove custom permission + await page.locator('button').filter({ hasText: 'Custom perm test' }).click(); + await page.getByRole('button', { name: 'Remove permission' }).click(); + await expect(page.locator('#app-wrapper')).not.toContainText('Custom perm test'); + // Close menu + await page.getByTestId('btn-close').last().click(); + }); + + test('SH Wallet_rst_more_notifications', async ({ page }) => { + // Wallet notifications screen check + await openWalletSettings(page); + // Open Notifications menu + await page.locator('a').filter({ hasText: 'Notifications' }).click(); + await eyes.check('Notifications', Target.window().matchLevel(MatchLevel.Layout)); + await page.getByTestId('btn-close').click(); + }); + + test('SH Wallet_rst_more_language', async ({ page }) => { + // Available languages check + await openWalletSettings(page); + // Open Language menu + await page.locator('a').filter({ hasText: 'Language' }).click(); + await expect(page.getByText('Connected')).not.toBeVisible(); + await eyes.check('Language', Target.window().fully()); + // Close menu + await page.getByTestId('btn-close').click(); + }); + + test('SH Wallet_rst_more_currency', async ({ page }) => { + // Superhero Wallet fiat currency conversion selection screen check + await openWalletSettings(page); + // Open currency menu + await page.locator('a').filter({ hasText: 'Currency' }).click(); + await expect(page.getByText('Connected')).not.toBeVisible(); + await eyes.check('Currency', Target.window().fully()); + // Select Euro from the list + await page.locator('label').filter({ hasText: 'eur (€) Euro' }).locator('span').first() + .click(); + await page.getByTestId('btn-close').click(); + // Check if Euro is visible on account card + await expect(page.locator('#app-wrapper')).toContainText('€'); + }); + + test('SH Wallet_rst_more_saveErrorLog', async ({ page }) => { + // Error log switch screen check + await openWalletSettings(page); + // Open Save error log menu + await page.getByText('Save error log').click(); + await expect(page.getByRole('paragraph')).toContainText('This will help us to identify what causes the errors. Thank you for being a fellow Superhero!'); + await page.locator('ion-content').filter({ hasText: 'This will help us to identify' }).locator('span').click(); + await page.getByTestId('back-arrow').click(); + await expect(page.locator('#app-wrapper')).toContainText('On'); + await eyes.check('Save error log', Target.window().fully()); + // Close menu + await page.getByTestId('btn-close').click(); + }); + + test('SH Wallet_rst_more_walletReset', async ({ page }) => { + await openWalletSettings(page); + // Open Reset Wallet menu + await page.locator('a').filter({ hasText: 'Reset wallet' }).click(); + await eyes.check('Reset wallet', Target.window().fully()); + await page.getByRole('button', { name: 'Reset wallet' }).click(); + await eyes.check('Reset wallet confirmation', Target.window().fully()); + // Cancel wallet reset + await page.getByRole('button', { name: 'Cancel' }).click(); + // Select Reset wallet + await page.getByRole('button', { name: 'Reset wallet' }).click(); + // Confirm wallet reset + await page.getByRole('button', { name: 'Reset', exact: true }).click(); + // Welcome screen open + await expect(page.locator('ion-content')).toContainText('The multi-blockchain wallet to manage crypto assets and navigate', { timeout: 10000 }); + await eyes.check('Welcome after reset', Target.window().fully()); + }); + + test('SH Wallet_rst_more_claimTips', async ({ page }) => { + // Open Wallet settings + + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Claim tips + await page.locator('a').filter({ hasText: 'Claim tips' }).click(); + // Open Claim tips help + await page.locator('div').filter({ hasText: /^You need to verify ownership of the URL before you can claim any tips\.$/ }).getByRole('button').click(); + await expect(page.getByRole('heading')).toContainText('Verify your URL'); + await eyes.check('Verify your URL', Target.window().matchLevel(MatchLevel.Layout)); + await page.getByRole('button', { name: 'OK' }).click(); + // No tips claim + await page.getByLabel('Claim tips from this URL:').fill('superhero.com'); + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.waitForTimeout(500); + await expect(page.locator('//span[@class="claimed"]')).toContainText('Claim request sent!'); + await page.getByRole('button', { name: 'OK' }).click(); + }); + + test('SH Wallet_rst_more_reportBug', async ({ context, page }) => { + // Open new tab with bug report possibility + // Open Wallet settings + + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Report Bug site + await page.getByRole('link', { name: 'Report a bug' }).click(); + // Check new tab opening and content + await Promise.all([ + context.waitForEvent('page'), + page.getByRole('heading', { name: 'Report to us – we are listening' }).isVisible()]); + // Eyes check of new tab + }); + + test('SH Wallet_rst_more_superheroDex', async ({ context, page }) => { + // Open Wallet settings + + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Superhero DEX + await page.getByRole('link', { name: 'Superhero DEX' }).click(); + await Promise.all([ + context.waitForEvent('page'), + page.getByRole('button', { name: 'Connect Wallet' }).isVisible()]); + }); + + test('SH Wallet_rst_more_about', async ({ page }) => { + // Check Terms of Use and Privacy Policy screens + // Open Wallet settings + + await page.locator('//a[@data-cy="page-more"]').click(); + // Open About screen + await page.locator('a').filter({ hasText: 'About' }).click(); + await expect(page.locator('#app-wrapper')).toContainText('Superhero Wallet'); + // Open Terms and Conditions screen + await page.locator('a').filter({ hasText: /^Terms$/ }).click(); + // Take a whole screenshot of the terms + await expect(page.getByText('Connected')).not.toBeVisible(); + await eyes.check('Terms screen', Target.region(page.locator('//ion-content[@data-cy="terms-of-service"]')).fully()); + await page.getByTestId('back-arrow').click(); + // Open Privacy screen + await page.locator('a').filter({ hasText: 'Privacy' }).click(); + // Take a whole screenshot of the privacy text + await eyes.check('Privacy screen', Target.region(page.locator('//ion-content[@data-cy="privacy-policy"]')).fully()); + await page.getByTestId('btn-close').click(); + }); + + test('SH Wallet_rst_more_gift_cards', { tag: '@sequence' }, async ({ page }) => { + // Gift card creation screen check + + const giftCardAmount = 1; + await switchToTestnet(page); + // Open Wallet settings + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Gift card screen + await page.locator('//a[@data-cy="invite"]').click(); + await page.getByTestId('input').fill(giftCardAmount.toString()); + await page.getByRole('button', { name: 'Generate gift card' }).click(); + await expect(page.getByTestId('loader')).not.toBeVisible({ timeout: 15000 }); + // Check created gift card + await expect(page.locator('#app-wrapper')).toContainText('Gift cards'); + await eyes.check('New Gift card', Target.window().matchLevel(MatchLevel.Layout)); + // Invite link + await expect(page.locator('//span[@class="invite-link-url"]')).toContainText('https://wallet.superhero.com/invite'); + await expect(page.locator('//span[@class="token-amount"]//span[@class="amount"]')).toContainText(giftCardAmount.toString()); + + // Top up created gift card + await page.getByRole('button', { name: 'Top up' }).click(); + await eyes.check('Top up gift card', Target.region(page.locator('//div[@class="ion-page can-go-back"]//ion-content[@class="md content-ltr ion-padding ion-content-bg"]')).fully().matchLevel(MatchLevel.Layout)); + await page.getByTestId('input').nth(1).fill(giftCardAmount.toString()); + await page.getByRole('button', { name: 'Top up' }).click(); + // Claim back gift card amount + await page.getByRole('button', { name: 'Claim Back' }).click(); + // Check that the gift card has no amount + await page.waitForTimeout(1000); + await expect(page.locator('//div[@class="invite-info"]//span[@class="token-amount"]//span[@class="amount"]')).toContainText('0 AE'); + // Delete empty gift card + await page.getByRole('button', { name: 'Delete' }).click(); + await expect(page.locator('//div[@class="generated-links"]//p[@class="section-title"]')).toBeHidden(); + }); + + test('SH Wallet_rst_more_faucet', async ({ page }) => { + // Use of aeternity faucet + + await switchToTestnet(page); + // Open Account details page + await page.getByTestId('account-card-base').first().click(); + const page1Promise = page.waitForEvent('popup'); + await page.getByRole('link', { name: 'Faucet' }).click(); + // Will open Faucet in new browser tab + const page1 = await page1Promise; + await expect(page1.locator('h1')).toContainText('Æternity Blockchain\'s Faucet Aepp'); + await page.locator('//div[@class="account-info-wrapper"]//button').click(); + await page1.getByRole('button', { name: 'Top UP' }).click(); + await expect(page1.locator('//div[@id="result"]')).not.toContainText('Adding'); + // Take the time to wait for next faucet possible use. + const faucetMsg = page1.locator('//div[@id="result"]'); + const faucetMsgText = await faucetMsg.textContent() as string; + const faucetRemainingTime = faucetMsgText.slice(faucetMsgText.indexOf('another') + 7, faucetMsgText.lastIndexOf('Please') - 1); + // Check if faucet was used recently + if ((await page1.locator('//div[@id="result"]').innerText()).includes('Something went wrong') === false) { + await expect(page1.locator('//div[@id="result"]')).toContainText('Added 5 AE!'); + console.log('5 ae has been added to account.'); + await page1.getByRole('button', { name: 'Top UP' }).click(); + await expect(page1.locator('//div[@id="result"]')).toContainText('Something went wrong. '); + } else { + console.log(`Faucet was already used. Blocked for ${faucetRemainingTime}`); + } + }); + + test('SH Wallet_rst_send_funds', { tag: '@sequence' }, async ({ page }) => { + // Send funds to second wallet account via account address, add payload text. + + await switchToTestnet(page); + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.locator('//textarea[@data-cy="textarea"]').fill(secAeAccAddress); + await page.locator('//input[@name="amount"]').fill(txnValue.toString()); + // Read txn fee value + await page.waitForTimeout(3000); + const txnFeeValueStr = await page.locator('//span[@data-cy="review-fee"]//span[@class="amount"]').textContent() as string; + const txnFeeValue = parseFloat(txnFeeValueStr); + await page.locator('//button[@data-cy="next-step-button"]').click(); + await page.waitForTimeout(500); + await eyes.check('Review transaction', Target.window().fully()); + + // Go back check values for editing + await page.getByRole('button', { name: 'Edit' }).click(); + await expect(page.locator('//textarea[@data-cy="textarea"]')).toHaveValue(secAeAccAddress); + await expect(page.locator('//input[@name="amount"]')).toHaveValue(txnValue.toString()); + await page.waitForTimeout(5000); + await page.locator('//button[@data-cy="next-step-button"]').click(); + + // Open Review transaction screen. Check address and ae amounts + await expect(page.locator('#app-wrapper')).toContainText(aeAccAddress); + await expect(page.locator('#app-wrapper')).toContainText(secAeAccAddress); + await expect(page.getByTestId('review-amount')).toContainText(`${txnValue.toString()} AE`); + await page.waitForTimeout(500); + await expect(page.locator('//div[@class="transfer-review-base transfer-review"]//span[@data-cy="review-fee"]//span[@class="amount"]')).toContainText(txnFeeValueStr); + // Get the total amount of transaction amount + transaction fee + let totalAmount = 0; + totalAmount = txnValue + txnFeeValue; + // Check total amount + await page.waitForTimeout(800); + await expect(page.locator('//span[@data-cy="review-total"]//span[@class="amount"]')) + .toContainText(totalAmount.toString()); + // Send transaction + await page.getByRole('button', { name: 'Send', exact: true }).click(); + // If the loader takes longer then 10 sec to show, check if loader is visible + if (await page.locator('//div[@class="panel-body"]').isVisible({ timeout: 10000 }) === true) { + // Check the Latest Transaction widget if send txn is shown + await expect(page.locator('//div[@class="panel-body"]')).toContainText('Sent', { timeout: 8000 }); + await expect(page.getByRole('link', { name: `æternity æternity − ${totalAmount.toString()} AE` }).first()).toBeVisible({ timeout: 8000 }); + } else { + console.log('SH Wallet_rst_send_funds- loader is visible!'); + } + }); + + test('SH Wallet_rst_send_funds_chain_name_payload', { tag: '@sequence' }, async ({ page }) => { + // Send funds to second wallet account via chain name, add payload text. + + await switchToTestnet(page); + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.locator('//label[@data-cy="input-wrapper"]//textarea[@data-cy="textarea"]').fill(secAeAccChainName); + await page.locator('//input[@name="amount"]').fill(txnValue.toString()); + // Add payload + await page.getByRole('button', { name: 'Payload' }).click(); + await page.locator('//label[@data-cy="input-wrapper"]//textarea').nth(1).fill('test text'); + await page.getByRole('button', { name: 'Done' }).click(); + // Read txn fee value + await page.waitForTimeout(3000); + const txnFeeValueStr = await page.locator('//span[@data-cy="review-fee"]//span[@class="amount"]').textContent() as string; + const txnFeeValue = new BigNumber(txnFeeValueStr.slice(0, -3)); + const totalAmount = txnFeeValue.plus(txnValue).toString(); + // Check payload value + await expect(page.locator('//div[@class="payload-text"]')).toContainText('test text'); + await page.locator('//button[@data-cy="next-step-button"]').click(); + await eyes.check('Review transaction', Target.window().matchLevel(MatchLevel.Layout)); + // Open Review transaction screen. Check address and ae amounts + await expect(page.locator('//div[@data-cy="review-recipient"]/div[@class="value"]/div[@class="avatar-with-chain-name only-name"]//div[@class="chain-name"]')) + .toContainText(secAeAccChainName); + await expect(page.locator('//div[@class="transfer-review-base transfer-review"]//span[@data-cy="review-fee"]//span[@class="amount"]')).toContainText(txnFeeValueStr); + await expect(page.locator('//span[@data-cy="review-total"]//span[@class="amount"]')).toContainText(totalAmount); + // Go back and check values for editing + await page.getByRole('button', { name: 'Edit' }).click(); + await expect(page.locator('//textarea[@data-cy="textarea"]')).toHaveValue(secAeAccChainName); + await expect(page.locator('//input[@name="amount"]')).toHaveValue(txnValue.toString()); + await page.locator('//input[@name="amount"]').fill(txnValue.toString()); + await page.locator('//button[@data-cy="next-step-button"]').click(); + // Send transaction + await page.getByRole('button', { name: 'Send', exact: true }).click(); + }); + + test('SH Wallet_rst_send_funds_qr_scan', async ({ page }) => { + // Check the appearance of QR code in send funds modal + + await switchToTestnet(page); + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.getByTestId('scan-button').click(); + await page.waitForTimeout(600); + await expect.soft(page.locator('//div[@class="title"]')).toContainText('Scan the recipient’s account address QR code in order to send æternity to them.'); + await eyes.check('Scan QR code page', Target.window().matchLevel(MatchLevel.Layout)); + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_faucet_missing_mainnet', async ({ page }) => { + // Check on Mainnet if Faucet is not available + + await page.getByTestId('account-card-base').first().click(); + await expect(page.getByRole('link', { name: 'Faucet' })).not.toBeVisible(); + await page.getByTestId('btn-close').click(); + await page.locator('//a[@data-cy="page-more"]').click(); + await expect(page.getByRole('link', { name: 'Faucet' })).not.toBeVisible(); + }); + + test('SH Wallet_rst_add ae acc_testnet', async ({ page }) => { + // Add a new aeternity account + + await switchToTestnet(page); + await page.locator('//*[local-name()="svg" and @data-cy="bullet-switcher-add"]').click(); + await page.getByTestId('account-card-add').click(); + await eyes.check('Add account', Target.window().fully()); + await page.getByTestId('btn-add-aeternity').click(); + await eyes.check('Blockchain selector', Target.window().fully()); + await page.getByTestId('create-plain-account').last().click(); + // await eyes.check('Add aeternity account', Target.window().fully()); + + // TODO Add check to see if new account was added + }); + + test('SH Wallet_rst_ae_details_testnet', async ({ page }) => { + // Check coin details screen + + await switchToTestnet(page); + await page.getByTestId('account-card-base').first().click(); + await page.getByTestId('account-details-assets').click(); + await page.locator('//a[starts-with(@href, "/coins/aeternity")]').click(); + await expect(page.getByText('Connected')).not.toBeVisible(); + await expect(page.getByTestId('loader')).not.toBeVisible(); + await eyes.check('AE details txn', Target.window().matchLevel(MatchLevel.Layout)); + // Check In filter + await page.getByRole('button', { name: 'In' }).click(); + await page.locator('//div[@class="label"]//span[text()="Received "]').nth(1).scrollIntoViewIfNeeded(); + await expect(page.getByTestId('list')).toContainText('Received'); + await expect(page.getByTestId('list')).not.toContainText('Sent'); + // Check Out filter + await page.getByRole('button', { name: 'Out' }).click(); + await page.locator('//div[@class="label"]//span[text()="Sent "]').nth(1).scrollIntoViewIfNeeded(); + await expect(page.getByTestId('list')).toContainText('Sent'); + await expect(page.getByTestId('list')).not.toContainText('Received'); + // Check All filter + await page.getByRole('button', { name: 'All' }).click(); + await expect(page.getByTestId('list')).toContainText('Sent'); + await expect(page.getByTestId('list')).toContainText('Received'); + // Check Coin details tab under Con details + await page.locator('//a[starts-with(@href, "/coins/aeternity/details")]').click(); + await eyes.check('AE details details', Target.window().matchLevel(MatchLevel.Layout)); + await page.getByTestId('btn-close').click(); + }); + + // MULTI SIG + + test('SH Wallet_rst_multisig_creation screen_testnet', async ({ page }) => { + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-aeternity').click(); + await page.getByRole('button', { name: 'Create multisig vault Manage' }).click(); + // Select signer 1 + await page.locator('label').filter({ hasText: 'Signer\'s account address' }).click(); + await page.locator('//div[@class="account-details list-name"]').first().click(); + // Add signer help text + await page.locator('//div[@class="signers-add-wrapper"]//button[@class="btn-help button-plain btn-help"]').click(); + await expect(page.locator('#app-wrapper')).toContainText('Authorized signers'); + await eyes.check('Authorized signer text', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + // Consensus required help + await page.getByTestId('multisig-num-of-signers-selector-help').click(); + await expect(page.locator('//h2').nth(1)).toContainText('Consensus required'); + await eyes.check('Consensus required text', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + await expect(page.getByRole('button', { name: 'Create multisig vault', exact: true })).toBeDisabled(); + // Add signer + await page.locator('//div[@data-signer-idx="signer-address-1"]//textarea').fill(secAeAccAddress); + // Signer 3 should be visible + await page.locator('//div[@class="signers-add-wrapper"]//button').first().click(); + await page.locator('//div[@data-signer-idx="signer-address-2"]//textarea').click(); + await page.locator('//div[@data-signer-idx="signer-address-1"]//textarea').click(); + await expect(page.getByTestId('input-message')).toContainText('This field is required'); + // Consensus required for tx confirmation should have 3 signers + await expect(page.locator('//div[@class="signers-count"]//span[@class="text-emphasis"]')).toHaveText('3'); + // Remove added signer + await page.locator('//div[@data-signer-idx="signer-address-2"]//label//button').click(); + await expect(page.locator('//div[@class="signers-count"]//span[@class="text-emphasis"]')).toHaveText('2'); + }); + + // Creating to many multisigs will result in all test failing due to heavy load + test.skip('SH Wallet_rst_multisig_creation_testnet', { tag: '@sequence' }, async ({ page }) => { + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-aeternity').click(); + await page.getByRole('button', { name: 'Create multisig vault Manage' }).click(); + // Select signer 1 + await page.locator('label').filter({ hasText: 'Signer\'s account address' }).click(); + await page.locator('//div[@class="account-details list-name"]').click(); + // Enter signer 2 address + await page.locator('//textarea[@data-cy="textarea"]').fill(secAeAccAddress); + // Select only 1 of 2 Signers + await page.locator('//div[@data-cy="multisig-num-of-signers-selector"]//select[@class="number-select-input"]').selectOption('1'); + // Select create button + await page.locator('//div[@class="fixed-screen-footer multisig-vault-create"]/button').click(); + await page.waitForTimeout(1000); + await expect(page.getByText('Creating account (pays for the transaction)')).toBeVisible(); + await eyes.check('Create new multisig vault', Target.window().fully()); + await page.getByRole('button', { name: 'Advanced transaction details' }).click(); + await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible(); + await expect(page.getByText('Authorized signers')).toBeVisible(); + // TODO Add variable for account address + await expect(page.getByRole('link', { name: 'ak_2sr ··· 2TL' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'ak_2Ek ··· Up9' })).toBeVisible(); + await page.getByRole('button', { name: 'Create vault' }).click(); + await page.waitForTimeout(200); + await expect(page.getByText('It\'s ready! Your new multisig vault is added to Superhero wallet.')).toBeVisible({ timeout: 15000 }); + await eyes.check('Creating vault progress', Target.window().matchLevel(MatchLevel.Layout)); + // Open confirmation screen + await page.getByRole('button', { name: 'Go to multisig vault' }).click(); + // Multisig vaults details screen is opening after successful creation + await expect(page.locator('//div[text()="Multisig vault address "]')).toBeVisible(); + await eyes.check('Created vault details', Target.region(page.locator('//div[@class="multisig-details"]/parent::ion-content')).fully()); + }); + + test('SH Wallet_rst_multisig acc_testnet', async ({ page }) => { + await switchToTestnet(page); + await page.getByRole('button', { name: 'Show multisig vaults' }).click(); + // Check Show multisig screen + await expect(page.locator('//span[text()="ak_2Mw"]')).toBeVisible({ timeout: 12000 }); + await expect.soft(page.getByText('Total in multisig vaults')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Receive to multisig vault' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Propose Tx to other signers' })).toBeVisible(); + await eyes.check('Multisig vault acc screen', Target.window().fully()); + // Select current multisig account + await page.locator('//a[@data-cy="account-card-base"]//div[text()="Multisig vault"]').nth(0).click(); + // Check receive screen + await page.waitForTimeout(1000); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.locator('//div[@data-cy="top-up-container"]//h2')).toContainText('Receive funds to multisig vault'); + await expect(page.getByText('Request specific amount (optional)')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + await eyes.check('Multisig receive', Target.window().fully()); + await page.getByTestId('btn-close').nth(1).click(); + // Check propose txn screen + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="send"]').click(); + await eyes.check('Propose Txn screen', Target.window().fully()); + await page.getByTestId('btn-close').nth(1).click(); + // Open details tab + await page.getByTestId('multisig-account-details-info').click(); + await expect(page.getByText('Authorized signers ')).toBeVisible(); + await eyes.check('Multisig details tab', Target.region(page.locator('//div[@class="multisig-details"]/parent::ion-content')).fully()); + await page.locator('//div[text()="Consensus "]//button').click(); + // Open Multisig consensus help screen + await expect(page.getByRole('heading', { name: 'Multisig consensus' })).toBeVisible(); + await eyes.check('Multisig consensus help text', Target.window().fully()); + await page.getByRole('button', { name: 'ok' }).click(); + }); + + test('SH Wallet_rst_multisig_receive screen_testnet', async ({ page }) => { + await switchToTestnet(page); + // Go to Multisig vaults + await page.getByRole('button', { name: 'Show multisig vaults' }).click(); + // Select current multisig account + await page.locator('//a[@data-cy="account-card-base"]//div[text()="Multisig vault"]').nth(0).click(); + // Check receive screen + await expect(page.locator('//span[text()="ak_2Mw"]')).toBeVisible({ timeout: 1000 }); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive funds to multisig vault'); + // Enter some number into amount field so the link under QR code will be + // updated and the account address displayed without spaces + await page.locator('//input[@data-cy="input"]').fill('1'); + await expect(page.getByText('Request specific amount (optional)')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + await eyes.check('Multisig receive', Target.window().matchLevel(MatchLevel.Layout)); + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + // Take the account address from the QR code link + const multisigAddressStr = await page.locator('//div[@class="address"]//span[text()]').textContent() as string; + const multisigAddress = multisigAddressStr.slice(0, 53); + // Link to aescan.io + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="address-truncated address"]').click(); + const page1 = await page1Promise; + // Check on aeScan + if (await page1.getByAltText('æScan logo').first().isVisible() === true) { + console.assert(await page1.locator('//div[@class="copy-chip__text"]').nth(0).innerText() === multisigAddress); + } else { + test.skip; + console.log('aeScan was not available'); + } + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_multisig_propose tx_screen_testnet', { tag: '@sequence' }, async ({ page }) => { + // Propose z multisig transaction with 1/1 of 2 consensus and send + + await switchToTestnet(page); + // Go to Multisig vaults + await page.getByRole('button', { name: 'Show multisig vaults' }).click(); + // Check if a transaction proposal is awaiting + await revokePendingProposal(page, expect); + // Check propose txn screen + await expect(page.locator('//span[text()="ak_2Mw"]')).toBeVisible({ timeout: 10000 }); + await page.locator('//a[@data-cy="account-card-base"]//div[text()="Multisig vault"]').nth(0).click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//span[text()="Propose Tx"]').click(); + await expect(page.getByRole('heading')).toContainText('Multisig transaction proposal', { timeout: 8000 }); + await eyes.check('Propose Txn', Target.window().matchLevel(MatchLevel.Layout)); + // Open Recipient help pop up, make screen check, close pop up + await page.getByTestId('address').getByRole('button').first().click(); + await eyes.check('Recipient_help', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + // Missing receiver address + await page.locator('//textarea[@data-cy="textarea"]').click(); + await page.locator('//input[@name="amount"]').click(); + await expect.soft(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + // Missing amount value + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + // Incorrect address string + await page.locator('//textarea[@data-cy="textarea"]').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid address or .chain name'); + // Send amount over max available + await page.locator('//input[@name="amount"]').fill('12345678'); + await page.locator('//textarea[@data-cy="textarea"]').click(); + await page.waitForTimeout(600); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('Amount exceeds vault balance'); + // Send amount is 0 + await page.locator('//input[@name="amount"]').fill('0'); + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('Amount must be more than 0.'); + // Open Payload help pop up, make screen check, close pop up + await page.locator('//div[@class="payload-add-wrapper"]/button[@class="btn-help button-plain btn-help"]').click(); + await eyes.check('Payload_help', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + // Open Payload screen + await page.getByRole('button', { name: 'Payload' }).click(); + await page.getByLabel('Payload message').fill('Hello. This is a test!'); + await eyes.check('Add_Payload', Target.window().fully()); + // Close Payload pop up + await page.getByRole('button', { name: 'Done' }).click(); + // Fill out multisig txn proposal fields + await page.locator('//div[@data-cy="address"]//textarea[@data-cy="textarea"]').fill(secAeAccAddress); + await page.locator('//input[@name="amount"]').fill(txnValue.toString()); + // Check propose txn screen with payload message + await eyes.check('Propose txn_Payload', Target.window().matchLevel(MatchLevel.Layout)); + // Click propose and approve button + await page.getByTestId('next-step-button').click(); + await page.waitForTimeout(500); + // Multisig transaction proposal screen + await eyes.check('Multisig txn proposal', Target.region(page.locator('//div[@class="container"]')).fully()); + await page.getByTestId('next-step-button').click(); + // Confirm of reject screen + await expect(page.locator('//span[text()="Multisig Tx proposal details"]')).toBeVisible({ timeout: 20000 }); + // Multisig Tx proposal details + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await expect(page.getByRole('button', { name: 'Disapprove' })).toBeVisible({ timeout: 8000 }); + await expect(page.getByRole('button', { name: 'Revoke transaction proposal' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Send' })).toBeVisible(); + await page.waitForTimeout(1000); + await eyes.check('Multisig Tx proposal', Target.region(page.locator('//div[@class="multisig-proposal-details"]/parent::ion-content')).fully()); + await page.getByRole('button', { name: 'Send' }).click(); + // Multisig Tx proposal details after sending + await expect(page.getByTestId('loader')).not.toBeVisible({ timeout: 25000 }); + await expect(page.locator('//div[@class="consensus"]//div[@class="info-box success"]')).toContainText('Transaction has been successfully sent.', { timeout: 20000 }); + await page.waitForTimeout(500); + await expect(page.locator('//div[@class="payload-text"]')).toContainText('Hello. This is a test!'); + await eyes.check('Multisig Tx proposal details', Target.region(page.locator('//div[@class="multisig-proposal-details"]/parent::ion-content')).fully()); + }); + + test('SH Wallet_rst_multisig_propose_and_disapprove_screen_testnet', { tag: '@sequence' }, async ({ page }) => { + // Propose z multisig transaction with 1/1 of 2 consensus and disapprove instead of sending + + await switchToTestnet(page); + // Go to Multisig vaults + await page.getByRole('button', { name: 'Show multisig vaults' }).click(); + // Check if a transaction proposal is awaiting + await revokePendingProposal(page, expect); + // Select current multisig account + await expect(page.locator('//span[text()="ak_2Mw"]')).toBeVisible({ timeout: 10000 }); + await page.locator('//a[@data-cy="account-card-base"]//div[text()="Multisig vault"]').nth(0).click(); + // Check propose txn screen + await page.locator('//div[@class="horizontal-scroll buttons"]//span[text()="Propose Tx"]').click(); + await expect(page.getByRole('heading')).toContainText('Multisig transaction proposal'); + // Fill out multisig txn proposal fields + await page.locator('//textarea[@data-cy="textarea"]').fill(secAeAccAddress); + await page.locator('//input[@name="amount"]').fill(txnValue.toString()); + // Click propose and approve button + await page.getByTestId('next-step-button').click(); + // Multisig transaction proposal screen + await page.getByTestId('next-step-button').click(); + // Confirm of reject screen + await expect(page.locator('//div[text()="Propose Tx"]')).toBeVisible({ timeout: 10000 }); + await page.getByTestId('accept').click(); + await expect(page.getByTestId('loader')).toBeVisible(); + await expect(page.getByTestId('loader')).not.toBeVisible({ timeout: 15000 }); + // Multisig Tx proposal details + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + // Multisig Tx proposal details + await expect(page.getByRole('button', { name: 'Disapprove' })).toBeVisible({ timeout: 8000 }); + await expect(page.getByRole('button', { name: 'Revoke transaction proposal' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Send' })).toBeVisible(); + // Disapprove the txn proposal + await page.getByRole('button', { name: 'Disapprove' }).click(); + // Disapprove pop up + await expect(page.locator('//h2')).toContainText('Disapprove transaction proposal'); + await eyes.check('Disapprove transaction proposal', Target.window().matchLevel(MatchLevel.Strict)); + await page.getByTestId('to-confirm').click(); + // await page.locator('//button[@data-cy="accept"]').click(); + }); + + test('SH Wallet_rst_multisig_propose_and_revoke_testnet', { tag: '@sequence' }, async ({ page }) => { + // Propose z multisig transaction with 1/1 of 2 consensus and revoke transaction proposal + + await switchToTestnet(page); + // Go to Multisig vaults + await page.getByRole('button', { name: 'Show multisig vaults' }).click(); + // Check if a transaction proposal is awaiting + await revokePendingProposal(page, expect); + // Select current multisig account + await expect(page.locator('//span[text()="ak_2Mw"]')).toBeVisible({ timeout: 12000 }); + await page.locator('//a[@data-cy="account-card-base"]//div[text()="Multisig vault"]').nth(0).click(); + // Check propose txn screen + await page.locator('//div[@class="horizontal-scroll buttons"]//span[text()="Propose Tx"]').click(); + await expect(page.getByRole('heading')).toContainText('Multisig transaction proposal'); + // Fill out multisig txn proposal fields + await page.locator('//textarea[@data-cy="textarea"]').fill(secAeAccAddress); + await page.locator('//input[@name="amount"]').fill('0.00001'); + // Click propose and approve button + await page.getByTestId('next-step-button').click(); + // Multisig transaction proposal screen + await page.getByTestId('next-step-button').click(); + // Confirm of reject screen + await page.waitForTimeout(1500); + await expect(page.locator('//div[text()="Propose Tx"]')).toBeVisible(); + await page.getByTestId('accept').click(); + await expect(page.getByTestId('loader')).toBeVisible(); + await expect(page.getByTestId('loader')).not.toBeVisible({ timeout: 15000 }); + // Multisig Tx proposal details + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await expect(page.getByRole('button', { name: 'Disapprove' })).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole('button', { name: 'Revoke transaction proposal' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Send' })).toBeVisible(); + // Revoke the txn proposal + await page.getByRole('button', { name: 'Revoke transaction proposal' }).click(); + // Revoke pop up + await expect(page.locator('//h2')).toContainText('Revoke transaction proposal'); + await eyes.check('Revoke transaction proposal', Target.window().fully()); + await page.getByTestId('to-confirm').click(); + await page.locator('//button[@data-cy="accept"]').click({ timeout: 10000 }); + }); + // Registering a name is a long operation and it will make other operation stuck + // until we will implement the nonce handling on our side, or sdk will improve it + test('SH Wallet_rst_names_register', { tag: '@sequence' }, async ({ page }) => { + await switchToTestnet(page); + await page.getByTestId('account-card-base').first().click(); + await page.getByRole('link', { name: 'Assets' }).click(); + await expect(page.locator('//div[@class="account-details-tokens"]//span[text()="æternity"]')).toBeVisible(); + await page.waitForTimeout(500); + await page.getByRole('link', { name: 'Names' }).click(); + await page.waitForTimeout(800); + // Skip the test if a name is alreadt pending + if (await page.locator('//span[@class="pending"]').isVisible()) { + console.log('Name registering is pending. Skipping the test.'); + test.skip(); + } else { + const newChainName = genRandomString('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 63); + // This waitForTimeout is required since the ionic routing + // is failing switching to a new route that fast. + await page.waitForTimeout(800); + await page.locator('//div[@class="account-details-navigation"]//a[text()="Register name"]').click(); + await page.getByLabel('.chain').fill(newChainName); + await page.waitForTimeout(600); + await expect(page.locator('//div[@class="claim"]//button[contains(@class,"btn-register")]')).toBeEnabled(); + await page.waitForTimeout(5000); + await page.locator('//div[@class="claim"]//div[@class="label-text"]').focus(); + await page.locator('//div[@class="claim"]//button').nth(1).click(); + await page.locator('//button[@data-cy="accept"]').click(); + await page.locator('//button[@data-cy="accept"]').click({ timeout: 20000 }); + // Chosen chain name should appear in the Names list with status pending + await expect(page.getByText(newChainName)).toBeVisible({ timeout: 25000 }); + await expect(page.locator('//div[@class="pending"]')).toBeVisible(); + // Currently the pending status take very long + // await expect.soft(page.locator('//span[text()="Name update successful!"]')) + // .toBeVisible({ timeout: 10000 }); + } + }); + + test('SH Wallet_rst_move_up_btn', { tag: ['@functional'] }, async ({ page }) => { + // Open a transaction list and scroll down the list so the move up button + + await switchToTestnet(page); + await page.getByTestId('account-card-base').first().click(); + await page.locator('//div[@data-cy="list"]//a').isVisible(); + await page.waitForTimeout(2000); + await page.locator('//div[@data-cy="list"]//a').last().focus(); + await expect(page.locator('//div[@class="back-to-top-btn-container"]//button')).toBeVisible(); + await page.locator('//div[@class="back-to-top-btn-container"]//button').click(); + }); + + test('SH Wallet_rst_acc_counter', async ({ page }) => { + // Checking account counter which does appear >=6 accounts + + await switchToTestnet(page); + let x = 2; + // Create 4 aeternity accounts in a loop + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 4; i++) { + await addAeternityAccount(page); + await expect(page.getByTestId('account-name-number').last()).toContainText(`æternity account ${x += 1}`); + } + // Check if account counter is now visible and take screenshot for eyes + await expect(page.locator('//div[@class="account-number"]')).toBeVisible(); + await expect(page.locator('//div[@class="account-number"]')).toContainText('6 / 6'); + await eyes.check('Account counter', Target.window().fully()); + await page.locator('//div[@class="account-number"]').click(); + await eyes.check('Account counter list', Target.window().fully()); + await page.getByTestId('btn-close').click(); + + // Adding ETH account + await addEthereumAccount(page, expect); + await expect(page.locator('//div[@class="account-number"]')).toContainText('7 / 7'); + await page.locator('//div[@class="account-number"]').click(); + await eyes.check('Account counter list with ETH', Target.window().fully()); + await page.getByTestId('btn-close').click(); + + // Add BTC account + await addBitcoinAccount(page, expect); + await expect(page.locator('//div[@class="account-number"]')).toContainText('8 / 8'); + await page.locator('//div[@class="account-number"]').click(); + await eyes.check('Account counter list with BTC', Target.window().fully()); + }); + + test('SH Wallet_rst_ae_send_no_balance_for_txn_fee', { tag: ['@functional'] }, async ({ page }) => { + // Account has less amount then the transaction fee + + await switchToTestnet(page); + // Create 3 AE accounts in a loop + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 3; i++) { + await addAeternityAccount(page); + } + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.locator('//textarea[@data-cy="textarea"]').fill(secAeAccAddress); + await page.locator('//input[@name="amount"]').fill('12'); + // As the mainnet account does not have any coins, the error message will be different + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('AE balance is not enough to pay for transaction fee', { timeout: 10000 }); + }); + + test('SH Wallet_rst_ae_airgap_account_import', async ({ page }) => { + // Check AirGap functionality screens + + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-aeternity').click(); + await page.locator('//button[@header="Import AirGap accounts"]').click(); + await eyes.check('Import account from AirGap', Target.window().fully()); + // Check scan functionality availability + await page.getByTestId('scan-button').click(); + await expect(page.locator('//div[text()="Scan QR code"]')).toBeVisible(); + await page.getByTestId('btn-close').last().click(); + await page.getByTestId('input-wrapper').fill('2LpfcPxwkWDgnKsCXYUkxBtFPjZg8YD5DWqcLx8UjMnFWjpSVMbiVquMkCGQp66Xc7cVi2uV6Vr7hSAiD564Dxgt3tNimE2EyrWyasZQwH7uYprwkEuKhgqFAnLgY4Sg1sadtFskTyyZp4yJFZPqmCmY'); + await page.waitForTimeout(1000); + await eyes.check('Imported AirGap account', Target.window().fully()); + await page.getByRole('button', { name: 'Import Account' }).last().click(); + await expect(page.locator('//div[@data-cy="account-name-number"]').last()) + .toContainText('AirGap account 1', { timeout: 8000 }); + await eyes.check('AirGap account 1', Target.window().fully()); + // Check Send modal for AirGap account + await page.locator('//button[@data-cy="send"]').click(); + await expect(page.locator('//h2[text()="Send funds from AirGap account"]')).toBeVisible(); + await page.locator('//textarea[@data-cy="textarea"]').fill(aeAccAddress); + await page.locator('//input[@name="amount"]').fill('1'); + await page.locator('//button[@data-cy="next-step-button"]').click(); + await expect(page.locator('//div[@class="custom-header-title"]')) + .toContainText('Sign transaction'); + await page.waitForTimeout(2000); + await page.locator('//div[@class="custom-header-title" and contains(text(),"Sign transaction")]//button[@class="btn-help button-plain btn-help"]').click(); + await eyes.check('Sign AirGap help', Target.region(page.locator('//div[@class="container"]').nth(1)).fully()); + await page.getByRole('button', { name: 'OK' }).click(); + await page.waitForTimeout(800); + await eyes.check('Sign transaction details', Target.region(page.locator('//div[@class="container"]')).fully()); + await page.locator('//button[@data-cy="next-step-button"]').click(); + await page.waitForTimeout(500); + await expect(page.locator('//div[@class="custom-header-title"]')) + .toContainText('Broadcast transaction'); + await eyes.check('Broadcast transaction', Target.window().fully()); + }); + + test('SH Wallet_rst_ae_share_account_address', async ({ page }) => { + // Check Share screen + + await switchToTestnet(page); + // Open Account details screen + await page.getByTestId('account-card-base').first().click(); + await page.locator('//button[@data-cy="share-address"]').click(); + await eyes.check('Share your public address', Target.window().fully()); + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_ae_address_book', async ({ page }) => { + // Check Address book screens + + await page.locator('//a[@data-cy="page-more"]').click(); + await eyes.check('Address book', Target.window().fully()); + // Open Address book + await page.locator('a').filter({ hasText: 'Address book' }).click(); + await expect(page.locator('//div[@class="truncate text"]')).toContainText('Address book'); + // Add ae address + await page.getByTestId('add-address').click(); + await eyes.check('add address screen', Target.window().fully()); + await page.locator('//div[@data-cy="name"]//textarea').fill('ae test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill(secAeAccAddress); + await page.waitForTimeout(800); + await eyes.check('ae test address', Target.window().fully()); + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.waitForTimeout(800); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="0"]')).toContainText('ae test name'); + // Add BTC address + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('btc test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill('bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + await page.waitForTimeout(800); + await eyes.check('btc test address', Target.window().fully()); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="1"]')).toContainText('btc test name'); + // Add ETH address + await page.waitForTimeout(800); + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('eth test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill('0x56eFaF7299AA9C7b3F7935f88908596A8b11a016'); + await page.waitForTimeout(800); + await eyes.check('eth test address', Target.window().fully()); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="2"]')).toContainText('eth test name'); + // Filter saved accounts + await page.getByTestId('aeternity-filter').click(); + await expect(page.getByTestId('address-book-item')).toContainText('ae test name'); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="eth test name"]')).not.toBeVisible(); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="btc test name"]')).not.toBeVisible(); + await page.getByTestId('bitcoin-filter').click(); + await expect(page.getByTestId('address-book-item')).toContainText('btc test name'); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="eth test name"]')).not.toBeVisible(); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="ae test name"]')).not.toBeVisible(); + await page.getByTestId('ethereum-filter').click(); + await expect(page.getByTestId('address-book-item')).toContainText('eth test name'); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="btc test name"]')).not.toBeVisible(); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="ae test name"]')).not.toBeVisible(); + await page.getByTestId('all-filter').click(); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="0"]')).toContainText('ae test name'); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="1"]')).toContainText('btc test name'); + await expect(page.locator('//a[@data-cy="address-book-item" and @idx="2"]')).toContainText('eth test name'); + // Remove added address + await page.locator('//a[@data-cy="address-book-item" and @idx="0"]').click(); + await page.getByRole('button', { name: 'Delete address record' }).click(); + await page.waitForTimeout(800); + await expect(page.locator('//div[@class="address-book-item"]//span[text()="ae test name"]')).not.toBeVisible(); + }); + + test('SH Wallet_rst_ae_address_book_send', async ({ page }) => { + // Check Address book screen in send modal + + await page.locator('//a[@data-cy="page-more"]').click(); + // Open Address book + await page.locator('a').filter({ hasText: 'Address book' }).click(); + // Add ae address + await page.getByTestId('add-address').click(); + await page.locator('//div[@data-cy="name"]//textarea').fill('ae test name'); + await page.locator('//div[@data-cy="address"]//textarea').fill(secAeAccAddress); + await page.waitForTimeout(800); + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.getByTestId('btn-close').click(); + // Open send screen + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.getByTestId('address-book-button').click(); + await eyes.check('Select recipient address', Target.window().fully()); + await page.locator('//button[@data-cy="address-book-item" and @idx="0"]').click(); + await page.waitForTimeout(1500); + await expect(page.locator('//div[@class="address-truncated"]').last()).toContainText(secAeAccAddress.substring(0, 6)); + }); + + test('SH Wallet_rst_import_key', async ({ page }) => { + // Checking account counter which does appear >=6 accounts + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-aeternity').click(); + await page.locator('//button[@header="Import with private key"]').click(); + await expect(page.locator('//div[@class="heading text-center"]')).toContainText('Import æternity account'); + // Account private key helper text + await page.locator('//div[@data-cy="field-private-key"]//button').click(); + await eyes.check('Prvate Key helper', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + await page.getByPlaceholder('Enter or paste your private key here').fill('2602a31e7a3ce342a6f287ee10164cadb88fd16eaa883bd9b93a75f653be03322594d09b5907f8a6f1a9fd2c7e5b067720fe78d862e83b9d6a7ef0f0fb3173f2'); + await page.getByTestId('btn-import').click(); + await expect(page.locator('//div[@data-cy="account-name-number"]').last()).toHaveText('PK æternity account 1'); + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive funds to public address'); + }); + + test.afterEach(async () => { + // End Applitools Visual AI Test + await eyes.closeAsync(); + }); +}); + +test.afterAll(async () => { +// Wait for Ultrast Grid Renders to finish and gather results + await Runner.getAllTestResults(); +}); diff --git a/tests/e2e/playwright/tests/SH_wallet_web_btc.spec.ts b/tests/e2e/playwright/tests/SH_wallet_web_btc.spec.ts new file mode 100644 index 000000000..52ce1ecb3 --- /dev/null +++ b/tests/e2e/playwright/tests/SH_wallet_web_btc.spec.ts @@ -0,0 +1,216 @@ +/* eslint-disable no-console */ +import { test, expect } from '@playwright/test'; +import { + Configuration, EyesRunner, Eyes, Target, MatchLevel, +} from '@applitools/eyes-playwright'; +import { interceptApiRequests, switchToTestnet, addBitcoinAccount } from './commands'; +import { initializeRunnerAndConfig } from './utils'; + +let Config: Configuration; +let Runner: EyesRunner; + +test.beforeAll(() => { + [Config, Runner] = initializeRunnerAndConfig('BTC'); +}); + +test.describe('SH Wallet checks BTC', () => { + let eyes: Eyes; + + test.beforeEach(async ({ page, context }) => { + await interceptApiRequests(context); + eyes = new Eyes(Runner, Config); + + // Start Applitools Visual AI Test + // Args: Playwright Page, App Name, Test Name, Viewport Size for local driver + await eyes.open(page, 'SH Wallet', test.info().title, { width: 360, height: 600 }); + await page.goto('./'); + }); + + test('SH Wallet_rst_add btc acc_testnet', async ({ page }) => { + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await eyes.check('Add BTC account', Target.window().fully()); + await page.getByTestId('btn-add-bitcoin').click(); + await page.getByTestId('create-plain-account').last().click(); + await expect(page.getByTestId('account-name-number').last()).toContainText('Bitcoin account'); + await eyes.check('Add BTC account', Target.window().matchLevel(MatchLevel.Layout)); + }); + + test('SH Wallet_rst_receive btc_mainnet', async ({ page }) => { + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Open Receive screen from Dashboard + await page.getByTestId('receive').click(); + await page.getByTestId('btn-close').click(); + // Open Receive screen from Account details page + await page.locator('//a[@data-cy="account-card-base"]//span[text()="BTC"]').click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//span[text()="Receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive Bitcoin to public address'); + await expect(page.getByText('Request specific amount')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + + await eyes.check('Receive_BTC', Target.window().fully()); + // Link to BTC explorer + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="address-truncated address"]').click(); + const page1 = await page1Promise; + await page.waitForTimeout(800); + if (await page1.locator('//div[@id="explorer"]//a[@class="navbar-brand"]').isVisible() === true) { + console.assert(await page1.locator('//div[@class="addr-page"]').nth(0).innerText() === 'bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + } else { + // eslint-disable-next-line no-unused-expressions + test.skip; + console.log('Blockstream was not available'); + } + // await expect(page1.locator('//div[@class="block-hash"]')) + // .toContainText('bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + }); + + test('SH Wallet_rst_btc_send_screen', async ({ page }) => { + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Open Send screen from dashboard + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByTestId('btn-close').click(); + // Open Send screen from Account details page + await page.locator('//a[@data-cy="account-card-base"]//span[text()="BTC"]').click(); + await page.locator('//div[@class="horizontal-scroll buttons"]/button[@data-cy="send"]').click(); + + // Check Send main screen + await eyes.check('Send Bitcoin', Target.window().matchLevel(MatchLevel.Strict)); + await expect(page.getByRole('heading', { name: 'Send Bitcoin' })).toBeVisible(); + await expect(page.getByText('Transaction fee')).toBeVisible(); + + // Open Recipient help pop up, make screen check, close pop up + await page.locator('//div[@class="transfer-send-recipient"]//button[@class="btn-help button-plain btn-help"]').click(); + await eyes.check('Recipient_help', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + // Missing receiver address + await page.getByTestId('textarea').click(); + await page.getByTestId('input').click(); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + // Missing amount value + await page.getByTestId('textarea').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Incorrect address string + await page.getByTestId('textarea').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid bitcoin address'); + + // Sender and receiver the same address + await page.getByTestId('textarea').fill('bc1qkwagn38f4zv80wdlhw539vn7geyvsyaj79jejw'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toHaveText("Sender's and recipient's addresses are the same. You are about to send BTC to your own account."); + + // Close Send screen + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_btc_send_screen_mainnet', async ({ page }) => { + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Open Send screen from dashboard + await page.getByRole('button', { name: 'Send' }).click(); + // Check Send main screen with transaction fee information + await expect(page.getByText('Transaction speed')).toBeVisible(); + await eyes.check('Send Bitcoin mainnet txn fee', Target.window().fully()); + }); + + test('SH Wallet_rst_btc_acc details_testnet', async ({ page }) => { + await switchToTestnet(page); + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Check account information screen + await page.locator('//a[@data-cy="account-card-base"]//span[text()="BTC"]').click(); + + await expect(page.getByTestId('list')).toContainText('Received', { timeout: 10000 }); + await expect(page.getByTestId('list')).toContainText('Sent'); + await eyes.check('BTC acc screen', Target.window().matchLevel(MatchLevel.Layout)); + + await expect(page.getByTestId('list')).toContainText('BTC'); + await expect(page.locator('#app-wrapper')).toContainText('BTC'); + await expect(page.locator('#app-wrapper')).toContainText('Bitcoin account'); + await page.getByRole('link', { name: 'Received' }).click(); + // Check BTC explorer link + const page2Promise = page.waitForEvent('popup'); + await page.getByRole('link', { name: 'View transaction in' }).click(); + const page2 = await page2Promise; + await expect(page2.locator('#transaction-box')).toContainText('tb1qca3vvfkzwgs2msss56d7x7prgmlua9qf2j0rnf', { timeout: 10000 }); + }); + + test('SH Wallet_rst_btc_more_disabled_options', async ({ page }) => { + // Check for BTC if some More options are not available + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Open More + await page.locator('//a[@data-cy="page-more"]').click(); + await eyes.check('BTC More options', Target.window().fully()); + }); + + test('SH Wallet_rst_btc_send_no_balance_for_txn_fee', { tag: ['@functional'] }, async ({ page }) => { + await switchToTestnet(page); + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Add BTC account to be visible + await addBitcoinAccount(page, expect); + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.locator('//textarea[@data-cy="textarea"]').fill('tb1qgygjzxl69hr7e0la295824n8nwz6cxpa07fs9p'); + await page.locator('//input[@name="amount"]').fill('12'); + // As the mainnet account does not have any coins, the error message will be different + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('BTC balance is not enough to pay for transaction fee', { timeout: 8000 }); + }); + + test('SH Wallet_rst_btc_share_account_address', async ({ page }) => { + // Check Share screen + + await switchToTestnet(page); + // Add BTC account to wallet + await addBitcoinAccount(page, expect); + // Open Account details screen + await page.locator('//a[@data-cy="account-card-base"]//span[text()="BTC"]').click(); + await page.locator('//button[@data-cy="share-address"]').click(); + await eyes.check('Share your public address_BTC', Target.window().fully()); + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_btc_import_key', async ({ page }) => { + // Checking account counter which does appear >=6 accounts + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-bitcoin').click(); + await page.locator('//button[@header="Import with private key"]').click(); + await expect(page.locator('//div[@class="heading text-center"]')).toContainText('Import Bitcoin account'); + // Account private key helper text + await page.locator('//div[@data-cy="field-private-key"]//button').click(); + await eyes.check('Prvate Key helper', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + await page.getByPlaceholder('Enter or paste your private key here').fill('95a74a5d7e7c43cbbcc2d66fd850b8fa59d99e4a70460519d1f1dcb2b19bb8d2'); + await page.getByTestId('btn-import').click(); + await expect(page.locator('//div[@data-cy="account-name-number"]').last()).toHaveText('PK Bitcoin account 1'); + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive Bitcoin to public address'); + }); + + test.afterEach(async () => { + // End Applitools Visual AI Test + await eyes.closeAsync(); + }); +}); + +test.afterAll(async () => { + // Wait for Ultrast Grid Renders to finish and gather results + await Runner.getAllTestResults(); +}); diff --git a/tests/e2e/playwright/tests/SH_wallet_web_eth.spec.ts b/tests/e2e/playwright/tests/SH_wallet_web_eth.spec.ts new file mode 100644 index 000000000..970acaeca --- /dev/null +++ b/tests/e2e/playwright/tests/SH_wallet_web_eth.spec.ts @@ -0,0 +1,281 @@ +/* eslint-disable no-console */ +import { test, expect } from '@playwright/test'; +import { + Configuration, + EyesRunner, + Eyes, + Target, + MatchLevel, +} from '@applitools/eyes-playwright'; +import { RestoreWalletData } from '../test-data/login.data'; +import { interceptApiRequests, switchToTestnet, addEthereumAccount } from './commands'; +import { initializeRunnerAndConfig } from './utils'; + +let Config: Configuration; +let Runner: EyesRunner; + +const ethFaucetUrl = 'https://faucet.triangleplatform.com/ethereum/sepolia'; + +test.beforeAll(() => { + [Config, Runner] = initializeRunnerAndConfig('ETH'); +}); + +test.describe('SH Wallet checks', () => { + let eyes: Eyes; + + const { ethAccAddress } = RestoreWalletData; + + test.beforeEach(async ({ page, context }) => { + await interceptApiRequests(context); + eyes = new Eyes(Runner, Config); + + // Start Applitools Visual AI Test + // Args: Playwright Page, App Name, Test Name, Viewport Size for local driver + await eyes.open(page, 'SH Wallet', test.info().title, { width: 360, height: 600 }); + await page.goto('./'); + }); + + test.afterEach(async () => { + // End Applitools Visual AI Test + await eyes.closeAsync(); + }); + + test('SH Wallet_rst_add eth acc_testnet', async ({ page }) => { + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await eyes.check('Etherum', Target.window().fully()); + await page.getByTestId('btn-add-ethereum').click(); + await page.getByTestId('create-plain-account').last().click(); + await expect(page.getByTestId('account-name-number').last()).toContainText('Ethereum account'); + await eyes.check('Add ETH account', Target.window().matchLevel(MatchLevel.Strict)); + }); + + test('SH Wallet_rst_receive eth_mainnet', async ({ page }) => { + // Add ETH account to be visible + await addEthereumAccount(page, expect); + // Open Receive screen from Dashboard + await page.getByTestId('receive').click(); + await page.getByTestId('btn-close').click(); + // Open Receive screen from Account details page + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]/button[@data-cy="receive"]').click(); + // Check for elements + await expect(page.getByRole('heading')).toContainText('Receive Ethereum to public address'); + await expect(page.getByText('Request specific amount')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Copy' })).toBeVisible(); + + await page.locator('//button[@data-cy="copy"]').click(); + await expect(page.getByRole('button', { name: 'Copied!' })).toBeVisible(); + await eyes.check('Receive_ETH', Target.window().matchLevel(MatchLevel.Strict)); + // Link to ETH explorer + const page1Promise = page.waitForEvent('popup'); + await page.locator('//div[@class="address-truncated address"]').click(); + const page1 = await page1Promise; + await page.waitForTimeout(800); + + if (await page1.locator('//header[@id="masterHeader"]').isVisible() === true) { + console.assert(await page1.locator('//span[@id="mainaddress"]').nth(0).innerText() === ethAccAddress); + } else { + // eslint-disable-next-line no-unused-expressions + test.skip; + console.log('Etherscan was not available'); + } + }); + + test('SH Wallet_rst_eth_send_screen_mainnet', async ({ page }) => { + // Add ETH account to be visible + await addEthereumAccount(page, expect); + + // Open Send screen from dashboard + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByTestId('btn-close').click(); + // Open Send screen from Account details page + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]/button[@data-cy="send"]').click(); + + // Check Send main screen + await expect(page.getByRole('heading', { name: 'Send Ethereum' })).toBeVisible(); + await page.waitForTimeout(600); + await eyes.check('Send Ethereum', Target.window().matchLevel(MatchLevel.Strict)); + await expect(page.getByText('Estimated transaction fee ')).toBeVisible(); + await expect(page.getByText('Maximum transaction fee ')).toBeVisible(); + + // Open Recipient help pop up, make screen check, close pop up + await page.getByTestId('address').getByRole('button').first().click(); + await eyes.check('Recipient_help', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + + // Check info/error msg + // Missing receiver address + await page.locator('//textarea[@data-cy="textarea"]').click(); + await page.locator('//input[@name="amount"]').click(); + await expect.soft(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Missing amount value + await page.locator('//textarea[@data-cy="textarea"]').click(); + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('This field is required'); + + // Incorrect address string + await page.locator('//textarea[@data-cy="textarea"]').fill('12345678'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toContainText('Invalid ethereum address'); + + // Sender and receiver the same address + await page.locator('//textarea[@data-cy="textarea"]').fill('0x22Fa8128467F549eD9eAd9Ae9b3BEdFA62987c48'); + await expect(page.locator('//div[@data-cy="address"]//label[@data-cy="input-field-message"]')) + .toHaveText("Sender's and recipient's addresses are the same. You are about to send ETH to your own account."); + }); + + test('SH Wallet_rst_eth_acc details_testnet', async ({ page }) => { + await switchToTestnet(page); + // Add ETH account to be visible + await addEthereumAccount(page, expect); + + // Check account information screen + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + // If the loader takes longer then 10 sec to show the no transactions message, + // check if loader is visible + if (await page.getByTestId('list').isVisible({ timeout: 10000 }) === true) { + await expect(page.getByTestId('list')).toContainText('Received'); + } else { + console.log('Loader is visible!'); + } + await eyes.check('ETH acc screen', Target.window().matchLevel(MatchLevel.Strict)); + // await expect(page.getByTestId('list')).toContainText('Received', {timeout:15000}); + + // Check Assets screen + await page.locator('//a[@data-cy="account-details-assets"]').click(); + await page.locator('//div[@protocol="ethereum"]').click(); + await expect(page.locator('//div[@class="title"]//span//span')).toContainText('Coin details'); + await eyes.check('ETH coin details', Target.window().matchLevel(MatchLevel.Strict)); + await page.locator('//a[@href="/coins/ethereum/details"]').click(); + await eyes.check('ETH details', Target.region(page.locator('//div[@class="token-details"]/parent::ion-content')).fully()); + }); + + /* TODO + > ERC-20 Token + > Faucet +*/ + + test('SH Wallet_rst_eth_get eth from faucet', async ({ page }) => { + await switchToTestnet(page); + // Add ETH account to wallet + await addEthereumAccount(page, expect); + + // Open Facet URL to claim free ETH + await page.goto(ethFaucetUrl); + await page.getByPlaceholder('0x0000000000000000000000000000000000000000').fill(ethAccAddress); + await page.locator('//button').click(); + + if (await page.getByText('Success').isVisible()) { + console.log('Ethereum Faucet claim was a success!'); + } else { + console.log('Ethereum Faucet claim failed!'); + } + }); + + test('SH Wallet_rst_eth_more_disabled_options', async ({ page }) => { + // Check for ETH if some More options are not available + // Add ETH account to wallet + await addEthereumAccount(page, expect); + // Open More + await page.locator('//a[@data-cy="page-more"]').click(); + await eyes.check('ETH More options', Target.window().fully()); + }); + + test('SH Wallet_rst_eth_send_no_balance_for_txn_fee', { tag: ['@functional'] }, async ({ page }) => { + // Account has less amount then the transaction fee + await switchToTestnet(page); + // Create 3 ETH accounts in a loop + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 3; i++) { + // eslint-disable-next-line no-await-in-loop + await addEthereumAccount(page, expect); + } + // Open send and make transaction + await page.getByRole('button', { name: 'Send assets to others' }).click(); + await page.locator('//textarea[@data-cy="textarea"]').fill(ethAccAddress); + await page.locator('//input[@name="amount"]').fill('12'); + // As the mainnet account does not have any coins, the error message will be different + await expect(page.locator('//div[@data-cy="amount"]//label[@data-cy="input-field-message"]')) + .toContainText('ETH balance is not enough to pay for transaction fee', { timeout: 8000 }); + }); + + test('SH Wallet_rst_eth_share_account_address', async ({ page }) => { + // Check Share screen + await switchToTestnet(page); + // Add ETH account to wallet + await addEthereumAccount(page, expect); + // Open Account details screen + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + await page.locator('//button[@data-cy="share-address"]').click(); + await eyes.check('Share your public address_ETH', Target.window().fully()); + await page.getByTestId('btn-close').nth(1).click(); + }); + + test('SH Wallet_rst_eth_walletconnect', async ({ page, context }) => { + // Open Uniswap for WalletConnect URI + const page2 = await context.newPage(); + await page2.goto('https://app.uniswap.org/swap?chain=sepolia'); + // Close pop up add + await page2.locator('//button[@data-testid="navbar-connect-wallet"]').click(); + await page2.locator('//button[@data-testid="wallet-option-walletConnect"]').click(); + + // Copy WC URI + const element = page2.locator('wcm-qrcode'); + const uri = await element.getAttribute('uri'); + + // Use the Wallet Connect URI to connect with ETH account + await switchToTestnet(page); + // Add ETH account to wallet + await addEthereumAccount(page, expect); + + // Open WalletConnect screen + await page.locator('//button[@data-cy="btn-wallet-connect"]').click(); + await expect(page.locator('//div//h2')).toContainText('Connect to Ethereum dapp'); + await page.waitForTimeout(500); + await eyes.check('Connect to Ethereum dapp', Target.window().fully()); + await page.getByTestId('scan-button').click(); + await expect(page.locator('//div[@class="title"]')).toContainText('WalletConnect URI'); + await page.getByTestId('btn-close').nth(1).click(); + await page.locator('//textarea[@data-cy="textarea"]').fill(uri as string); + await page.waitForTimeout(800); + await page.getByRole('button', { name: 'Connect' }).click(); + await page.waitForTimeout(1500); + await expect(page.locator('//div[@class="value"]').first()).toContainText('Connected', { timeout: 15000 }); + await page.waitForTimeout(1000); + await eyes.check('WC connected', Target.window().fully()); + await page.getByRole('button', { name: 'Disconnect' }).click(); + }); + + test('SH Wallet_rst_eth_import_key', async ({ page }) => { + await switchToTestnet(page); + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-ethereum').click(); + await page.locator('//button[@header="Import with private key"]').click(); + // Account private key helper text + await page.locator('//div[@data-cy="field-private-key"]//button').click(); + await eyes.check('Prvate Key helper', Target.window().fully()); + await page.getByRole('button', { name: 'OK' }).click(); + await page.getByPlaceholder('Enter or paste your private key here').fill('d72b0e021eb1265d2471932286708037ceda28ecc500921735a39a302b866fa7'); + await page.getByTestId('btn-import').click(); + await expect(page.locator('//div[@data-cy="account-name-number"]').last()).toHaveText('PK Ethereum account 1'); + await page.locator('//div[@data-cy="account-name-number"]').last().click(); + await page.locator('//div[@class="horizontal-scroll buttons"]//button[@data-cy="receive"]').click(); + await expect(page.getByRole('heading')).toContainText('Receive Ethereum to public address'); + }); + + test.afterEach(async () => { + // End Applitools Visual AI Test + await eyes.closeAsync(); + }); +}); + +test.afterAll(async () => { + // Wait for Ultrast Grid Renders to finish and gather results + await Runner.getAllTestResults(); +}); diff --git a/tests/e2e/playwright/tests/auth.setup.ts b/tests/e2e/playwright/tests/auth.setup.ts new file mode 100644 index 000000000..fb3a3aaa3 --- /dev/null +++ b/tests/e2e/playwright/tests/auth.setup.ts @@ -0,0 +1,17 @@ +import { test as setup } from '@playwright/test'; +import { RestoreWalletData } from '../test-data/login.data'; +import { WelcomePage } from '../pages/welcome.page'; +import { interceptApiRequests } from './commands'; + +const authFile = 'playwright/.auth/user.json'; + +setup('authenticate', async ({ page, context }) => { + await interceptApiRequests(context); + const welcomePage = new WelcomePage(page); + + await page.goto('./'); + await welcomePage.recoveredWalletLogin(RestoreWalletData.testWalletSeed); + await page.waitForURL('./account'); + + await page.context().storageState({ path: authFile }); +}); diff --git a/tests/e2e/playwright/tests/commands.ts b/tests/e2e/playwright/tests/commands.ts new file mode 100644 index 000000000..bde83cf6a --- /dev/null +++ b/tests/e2e/playwright/tests/commands.ts @@ -0,0 +1,90 @@ +import { Page, BrowserContext, Expect } from '@playwright/test'; +import { WalletPassword, RestoreWalletData } from '../test-data/login.data'; +import { WelcomePage } from '../pages/welcome.page'; + +export async function openWalletSettings(page: Page) { + await page.locator('//a[@data-cy="page-more"]').click(); + await page.locator('//a[@data-cy="settings"]').click(); +} + +export async function resetWallet(page: Page) { + await openWalletSettings(page); + // Open Reset Wallet menu + await page.locator('a').filter({ hasText: 'Reset wallet' }).click(); + await page.getByRole('button', { name: 'Reset wallet' }).click(); + // Confirm wallet reset + await page.getByRole('button', { name: 'Reset', exact: true }).click(); +} + +export async function switchToTestnet(page: Page) { + await page.getByRole('button', { name: 'Mainnet' }).click(); + await page.getByRole('button', { name: 'Testnet Connect to testnet' }).click(); +} + +export async function interceptApiRequests(context: BrowserContext) { + await context.route('https://api.coingecko.com/api/v3/simple/price?*', async (route) => { + const json = { aeternity: { usd: 0.06096 } }; + await route.fulfill({ json }); + }); + await context.route('https://api.coingecko.com/api/v3/coins/markets?*', async (route) => { + const json = [{ id: 'aeternity', symbol: 'ae', name: 'Aeternity' }]; + await route.fulfill({ json }); + }); +} + +export async function revokePendingProposal(page: Page, expect: Expect) { + if (await page.locator('//div[@data-swiper-slide-index="0"]//div[@class="account-card-consensus"]').isVisible() === true) { + await page.waitForTimeout(1000); + // Check if Propose tx buttons are disabled + await expect(page.locator('//button[@data-cy="send"]').nth(1)).toHaveAttribute('aria-disabled', 'true'); + await page.locator('//div[@class="dashboard-base dashboard-multisig"]//a').first().click(); + // await page.getByTestId('account-card-base').nth(0).click(); + await expect(page.locator('//button[@data-cy="send"]').nth(1)).toHaveAttribute('aria-disabled', 'true'); + await page.getByTestId('btn-close').click(); + + await page.locator('//div[@class="dashboard-base-cards"]//div[@class="panel pending-multisig-transaction-card"]//a').click(); + await page.locator('//div[@class="multisig-proposal-details"]//button[text()="Revoke transaction proposal"]').click(); + + // Revoke confirmation pop up + await expect(page.locator('//h2[text()="Revoke transaction proposal"]')).toBeVisible(); + await page.locator('//button[@data-cy="to-confirm"]').click(); + // await page.locator('//button[@data-cy="accept"]').click(); + await expect(page.getByTestId('account-card-base').nth(1)).not.toBeVisible(); + // await page.getByRole('button', { name: 'Show multisig vaults' }).click({ timeout: 15000 }); + } +} + +export async function unlockWallet(page: Page) { + await page.locator('//input[@data-cy="input"]').fill(WalletPassword.walletPassword); + await page.getByRole('button', { name: 'Unlock' }).click(); +} + +export async function recoverWalletExtension(page: Page) { + const welcomePage = new WelcomePage(page); + await page.getByTestId('checkbox').click(); + await welcomePage.recoveredWalletLogin(RestoreWalletData.testWalletSeed); + await page.getByRole('button', { name: 'Skip password' }); +} + +export async function addBitcoinAccount(page: Page, expect: Expect) { + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-bitcoin').click(); + await page.getByTestId('create-plain-account').last().click(); + await expect(page.getByTestId('account-name-number').last()).toContainText('Bitcoin account'); +} + +export async function addEthereumAccount(page: Page, expect: Expect) { + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-ethereum').click(); + await page.getByTestId('create-plain-account').last().click(); + await expect(page.getByTestId('account-name-number').last()).toContainText('Ethereum account'); +} + +export async function addAeternityAccount(page: Page) { + await page.getByTestId('bullet-switcher-add').click(); + await page.getByTestId('account-card-add').click(); + await page.getByTestId('btn-add-aeternity').click(); + await page.getByTestId('create-plain-account').last().click(); +} diff --git a/tests/e2e/playwright/tests/fixtures.ts b/tests/e2e/playwright/tests/fixtures.ts new file mode 100644 index 000000000..e57d6742e --- /dev/null +++ b/tests/e2e/playwright/tests/fixtures.ts @@ -0,0 +1,40 @@ +import { test as base, chromium, type BrowserContext } from '@playwright/test'; +import path from 'path'; + +process.env.PW_CHROMIUM_ATTACH_TO_OTHER = '1'; + +export const test = base.extend<{ + context: BrowserContext; + extensionId: string; +}>({ + context: async ({ }, use) => { + const pathToExtension = path.join(__dirname, 'my-extension'); + const context = await chromium.launchPersistentContext('', { + headless: false, + args: [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + '--start-maximized', + ], + }); + await use(context); + await context.close(); + }, + extensionId: async ({ context }, use) => { + /* + // for manifest v2: + let [background] = context.backgroundPages() + if (!background) + background = await context.waitForEvent('backgroundpage') + */ + + // for manifest v3: + let [background] = context.serviceWorkers(); + if (!background) + background = await context.waitForEvent('serviceworker'); + + const extensionId = background.url().split('/')[2]; + await use(extensionId); + }, +}); +export const expect = test.expect; \ No newline at end of file diff --git a/tests/e2e/playwright/tests/utils.ts b/tests/e2e/playwright/tests/utils.ts new file mode 100644 index 000000000..881cd3db3 --- /dev/null +++ b/tests/e2e/playwright/tests/utils.ts @@ -0,0 +1,50 @@ +import { + BatchInfo, + BrowserType, + Configuration, + VisualGridRunner, +} from '@applitools/eyes-playwright'; +import { chromium } from '@playwright/test'; +import path from 'path'; + +export function genRandomString(characters: string, length: number) { + let str = ''; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < length; i++) { + str += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return str; +} + +export function initializeRunnerAndConfig(coinName: string): [Configuration, VisualGridRunner] { + // eslint-disable-next-line global-require + const branchName = require('child_process').execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); + // Configure Applitools SDK to run on the Ultrafast Grid + const Runner = new VisualGridRunner({ testConcurrency: 5 }); + const Batch = new BatchInfo({ + name: `SH Wallet_${coinName}-${branchName}`, + id: genRandomString('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12), + }); + + // Create a configuration for Applitools Eyes. + const Config = new Configuration(); + // Set the batch for the config. + Config.setBatch(Batch); + Config.addBrowser(360, 600, BrowserType.CHROME); + Config.setApiKey(process.env.APPLITOOLS_KEY!); + return [Config, Runner]; +} + +export const createBrowserContext = async () => { + // assuming your extension is built to the 'public' directory + const pathToExtension = path.join(__dirname, './public'); + const userDataDir = '/tmp/test-user-data-dir'; + await chromium.launchPersistentContext( + userDataDir, + { + headless: false, + args: [`--disable-extensions-except=${pathToExtension}`], + ignoreDefaultArgs: ['--disable-component-extensions-with-background-pages'], + }, + ); +};