diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70ea782..f9fb7ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,13 +13,13 @@ # jobs: # run-tests: -# name: Run shard ${{ matrix.shardIndex }}/5 +# name: Run Playwright shards # runs-on: ubuntu-latest # strategy: # fail-fast: false # matrix: -# shardIndex: [1,2,3,4,5] +# shardIndex: [1,2,3,4,5] # shardTotal: [5] # steps: @@ -29,11 +29,11 @@ # uses: actions/setup-node@v3 # with: # node-version: '18' - # - name: Create .env file # run: | # echo "USERNAME=${{ secrets.USERNAME }}" >> .env # echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env +# echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env # echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env # echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env # echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env @@ -53,13 +53,10 @@ # - name: Install deps + browsers # run: | # npm ci -# npx playwright install --with-deps chromium firefox webkit - -# - name: List Playwright projects (debug) -# run: npx playwright list --projects | cat +# npx playwright install --with-deps -# - name: Run shard ${{ matrix.shardIndex }} -# run: npx playwright test --grep="@chromium|@firefox|@webkit|@android|@ios" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} +# - name: Run shard ${{ matrix.shardIndex }} +# run: npx playwright test --project=chromium --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} # - name: Upload blob report # if: ${{ !cancelled() }} @@ -176,7 +173,7 @@ jobs: run: | npm install npm install --save-dev @testdino/playwright@latest - npx playwright install --with-deps chromium firefox webkit + npx playwright install --with-deps chromium webkit firefox # ✅ Run Playwright shard with TestDino - name: Run Playwright shard with TestDino @@ -185,8 +182,8 @@ jobs: run: | npx playwright test \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ - --grep "@chromium|@firefox|@webkit|@android|@ios|@api" \ - + --grep="@chromium|@firefox|@webkit|@android|@ios|@api" \ + # ✅ Upload blob report (needed for merge) - name: Upload blob report if: ${{ !cancelled() }} diff --git a/.gitignore b/.gitignore index 0bdc3a1..417a361 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules/ /playwright/.cache/ .env .DS_Store +.claude/ diff --git a/README.md b/README.md index 95c8154..cff65e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Ecommerce demo store - Playwright (javascript) tests +# Ecommerce-demo-test-playwright -Automated end-to-end tests for Ecommerce demo store using [Playwright](https://playwright.dev/). +Automated end-to-end tests for Ecommerce Demo store using [Playwright](https://playwright.dev/). --- @@ -63,9 +63,7 @@ reporter: [ After your tests complete and the report is generated in `playwright-report`, upload it to Testdino: -```sh -npx --yes tdpw ./playwright-report --token="YOUR_TESTDINO_API_KEY" --upload-html -``` + Replace the token above with your own Testdino API key. @@ -82,11 +80,7 @@ npx tdpw --help Add the following step to your workflow after tests and report generation: -```yaml -- name: Send Testdino report - run: | - npx --yes tdpw ./playwright-report --token="YOUR_TESTDINO_API_KEY" --upload-html -``` + Ensure your API key is correctly placed in the command. @@ -106,4 +100,3 @@ Pull requests and issues are welcome! ## License -MIT diff --git a/__screenshots__/visual.spec.js-snapshots/github-login-changed-chromium-darwin.png b/__screenshots__/visual.spec.js-snapshots/github-login-changed-chromium-darwin.png new file mode 100644 index 0000000..81b5cb7 Binary files /dev/null and b/__screenshots__/visual.spec.js-snapshots/github-login-changed-chromium-darwin.png differ diff --git a/__screenshots__/visual.spec.js-snapshots/github-login-chromium-darwin.png b/__screenshots__/visual.spec.js-snapshots/github-login-chromium-darwin.png new file mode 100644 index 0000000..568dbbb Binary files /dev/null and b/__screenshots__/visual.spec.js-snapshots/github-login-chromium-darwin.png differ diff --git a/package-lock.json b/package-lock.json index 8e94191..f1e9df2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,21 +10,73 @@ "license": "ISC", "dependencies": { "dotenv": "^17.2.1", - "playwright": "^1.55.0" + "testdino-mcp": "file:testdino-mcp-1.0.7.tgz" }, "devDependencies": { "@playwright/test": "^1.54.1", "@types/node": "^24.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", + "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@playwright/test": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", - "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.0" + "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" @@ -34,77 +86,2685 @@ } }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.16.0" } }, - "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", - "license": "BSD-2-Clause", + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://dotenvx.com" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "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, + "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==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ambi": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ambi/-/ambi-3.2.0.tgz", + "integrity": "sha512-nj5sHLPFd7u2OLmHdFs4DHt3gK6edpNw35hTRIKyI/Vd2Th5e4io50rw1lhmCdUNO2Mm4/4FkHmv6shEANAWcw==", + "license": "MIT", + "dependencies": { + "editions": "^2.1.0", + "typechecker": "^4.3.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT", + "optional": true + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha512-u1L0ZLywRziOVjUhRxI0Qg9G+4RnFB9H/Rq40YWn0dieDgO7vAYeJz6jKAO6t/aruzlDFLAPkQTT87e+f8Imaw==", "license": "MIT", "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=0.8" } }, - "node_modules/playwright": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", - "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha512-JnJpAS0p9RmixkOvW2XwDxxzs1bd4/VAGIl6Q0EC5YOo+p+hqIhtDhn/nmFnB/xUNXbLkpE2mOjgVIBRKD4xYw==", "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT", + "optional": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "optional": true, "dependencies": { - "playwright-core": "1.55.0" - }, - "bin": { - "playwright": "cli.js" + "tweetnacl": "^0.14.3" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" }, - "optionalDependencies": { - "fsevents": "2.3.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/playwright-core": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", - "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "node_modules/boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha512-KbiZEa9/vofNcVJXGwdWWn25reQ3V3dHBWbS07FTF3/TOehLnm9GEhJV4T6ZvGPkShRpmUqYwnaCrkj0mRnP6Q==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "optional": true + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "optional": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.8" } }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT", + "optional": true + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha512-FFN5KwpvvQTTS5hWPxrU8/QE4kQUc6uwZcrnlMBN82t1MgAtq8mnoDwINBly9Tdr02seeIIhtdF+UH1feBYGog==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x" + }, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/csextends": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/csextends/-/csextends-1.2.0.tgz", + "integrity": "sha512-S/8k1bDTJIwuGgQYmsRoE+8P+ohV32WhQ0l4zqrc0XDdxOhjQQD7/wTZwCzoZX53jSX3V/qwjT+OkPTxWQcmjg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dashdash/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.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", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/docker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/docker/-/docker-1.0.0.tgz", + "integrity": "sha512-U66G/kvvsCTUh6VsZqnWgsoSG1KRu5jR473fn/64E6EU9cH65afCITx2qITmNPkr3IOehcn1wwXHmIvHYBlLgQ==", + "license": "MIT", + "dependencies": { + "async": "^1.4.0", + "commander": "^2.9.0", + "css": "^2.2.1", + "dox": "^0.8.0", + "ejs": "^2.3.3", + "extend": "^3.0.0", + "highlight.js": "^9.3.0", + "less": "^2.5.1", + "markdown-it": "^6.0.1", + "mkdirp": "^0.5.1", + "repeating": "^2.0.1", + "strip-indent": "^2.0.0", + "toc": "^0.4.0", + "watchr": "^2.4.13" + }, + "bin": { + "docker": "docker", + "docker.js": "docker" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dox": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/dox/-/dox-0.8.1.tgz", + "integrity": "sha512-CJJCQS6XYJ2FQJox4ey7pUdaAjDusPLqGtfe3Jli4N+m2jBKrT9zwEsh2thV9W5d8F359AMWqkWk50CuH3r8dw==", + "license": "MIT", + "dependencies": { + "commander": "~2.9.0", + "jsdoctypeparser": "^1.2.0", + "marked": "~0.3.5" + }, + "bin": { + "dox": "bin/dox" + } + }, + "node_modules/dox/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "license": "MIT", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eachr": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eachr/-/eachr-3.3.0.tgz", + "integrity": "sha512-yKWuGwOE283CTgbEuvqXXusLH4VBXnY2nZbDkeWev+cpAXY6zCIADSPLdvfkAROc0t8S4l07U1fateCdEDuuvg==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0", + "typechecker": "^4.9.0" + }, + "engines": { + "node": ">=0.10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/editions": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/editions/-/editions-2.3.1.tgz", + "integrity": "sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==", + "license": "MIT", + "dependencies": { + "errlop": "^2.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "license": "BSD-2-Clause" + }, + "node_modules/errlop": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/errlop/-/errlop-2.2.0.tgz", + "integrity": "sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extendr": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/extendr/-/extendr-3.5.0.tgz", + "integrity": "sha512-7zpVbnnZy91J4k916ZGwpys56DEgJc/prTXDiqCYe/Mud5pqdVsSc9mG/U6sz3lQEvHs81i8Zi7whsFwifhZyw==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0", + "typechecker": "^4.7.0" + }, + "engines": { + "node": ">=0.12" + }, + "funding": { + "type": "cooperative", + "url": "https://bevry.me/fund" + } + }, + "node_modules/extract-opts": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/extract-opts/-/extract-opts-3.4.0.tgz", + "integrity": "sha512-M7Y+1cJDkzOWqvGH5F/V2qgkD6+uitW3NV9rQGl+pLSVuXZ4IDDQgxxMeLPKcWUyfypBWczIILiroSuhXG7Ytg==", + "license": "MIT", + "dependencies": { + "eachr": "^3.2.0", + "editions": "^2.2.0", + "typechecker": "^4.9.0" + }, + "engines": { + "node": ">=0.10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": 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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha512-8HWGSLAPr+AG0hBpsqi5Ob8HrLStN/LWeqhpFl14d7FJgHK48TmgLoALPz69XSUR65YJzDfLUX/BM8+MLJLghQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/getpass/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "license": "MIT" + }, + "node_modules/har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha512-f8xf2GOR6Rgwc9FPTLNzgwB+JQ2/zMauYXSWmX5YV5acex6VomT0ocSuwR7BfXo5MpHi+jL+saaux2fwsGJDKQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha512-5Gbp6RAftMYYV3UEI4c4Vv3+a4dQ7taVyvHt+/L6kRt+f4HX1GweAk5UDWN0SvdVnRBzGQ6OG89pGaD9uSFnVw==", + "deprecated": "this library is no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha512-I/bSHSNEcFFqXLf91nchoNB9D1Kie3QKcWdchYUaoIg1+1bdWDkdfdlvdIOJbi9U8xR0y+MWc5D+won9v95WlQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha512-X8xbmTc1cbPXcQV4WkLcRMALuyoxhfpFATmyuCxJPOAvrDS4DNnsTAOmKUxMTOWU6TzrTOkxPKwIx5ZOpJVSrg==", + "deprecated": "This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + }, + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", + "deprecated": "Support has ended for 9.x series. Upgrade to @latest", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha512-V6Yw1rIcYV/4JsnggjBU0l4Kr+EXhpwqXRusENU1Xx6ro00IHPHYNynCuBTOZAPlr3AAmLvchH9I7N/VUdvOwQ==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.40" + } + }, + "node_modules/hono": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha512-iUn0NcRULlDGtqNLN1Jxmzayk8ogm7NToldASyZBpM2qggbphjXzNOiw3piN8tgz+e/DRs6X5gAzFwTI6BCRcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignorefs": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.4.1.tgz", + "integrity": "sha512-1whgvOsPWFZRNA/5OFhIk56C9Y39+/CYaRVNvsZZkLymacOSqqdSU53xk8CP3G2u5gz2PX6RLxqKPcsIpDriog==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0", + "ignorepatterns": "^1.4.0" + }, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/ignorepatterns": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.4.0.tgz", + "integrity": "sha512-YPBIFRB25iZD0WiLxmToe80+QU+mZI+bUlEh3Ze/4gbhlXHdQFk0SwAFQtPOiBAoDv3FvhtSTDUCD9DKFsHTRA==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0" + }, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT", + "optional": true + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT", + "optional": true + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT", + "optional": true + }, + "node_modules/jsdoctypeparser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-1.2.0.tgz", + "integrity": "sha512-osXm4Fr1o/Jc0YwUM7DHUliYtaunLQxh4ynZgtN02mTUN1VsNbMy75DFSkKRne8xE8jiGRV9NKVhYYYa8ZIHXQ==", + "license": "MIT", + "dependencies": { + "lodash": "^3.7.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)", + "optional": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsprim/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "license": "Apache-2.0", + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=0.12" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.2.11", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "2.81.0", + "source-map": "^0.5.3" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/linkify-it": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-1.2.4.tgz", + "integrity": "sha512-eGHwtlABkp1NOJSiKUNqBf3SYAS5jPHtvRXPAgNaQwTqmkTahjtiLH9NtxdR5IOPhNvwNMN/diswSfZKzUkhGg==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==", + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-6.1.1.tgz", + "integrity": "sha512-woFl7h/sqt9xRmiMweNuO7nu+w8Lz3SXsDlvE3TYeu1SdPqQ+VW4GZyaKP442Bq6XUN6V6IQjJTR93RDYG2mjw==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "~1.2.2", + "mdurl": "~1.0.1", + "uc.micro": "^1.0.1" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/marked": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "license": "MIT", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha512-VlF07iu3VV3+BTXj43Nmp6Irt/G7j/NgEctUS6IweH1RGhURjjCc2NWtzXFPXXWWfc7hgbXQdtiQu2LGp6MxUg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha512-YHk5ez1hmMR5LOkb9iJkLKqoBlL7WD5M8ljC75ZfzXriuBIVNuecaXuU7e+hOwyqf24Wxhh7Vxgt7Hnw9288Tg==", + "license": "MIT", + "optional": true + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT", + "optional": true + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha512-IZnsR7voF0miGSu29EXPRgPTuEsI/+aibNSBbN1pplrfartF5wDYGADz3iD9vmBVf2r00rckWZf8BtS5kk7Niw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.3.tgz", + "integrity": "sha512-ZR4x/aZZFC9YhBFeGfudUd+pY1R6NjVKbzXr/BaES046WNHPn5RWSAYEmTrlUJVU/+4pgyJEl21LFggYarCSTg==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safefs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/safefs/-/safefs-4.2.0.tgz", + "integrity": "sha512-1amPBO92jw/hWS+gH/u7z7EL7YxaJ8WecBQl49tMQ6Y6EQfndxNNKwlPqDOcwpUetdmK6nKLoVdjybVScRwq5A==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0", + "graceful-fs": "^4.2.3" + }, + "engines": { + "node": ">=0.12" + }, + "funding": { + "type": "cooperative", + "url": "https://bevry.me/fund" + } + }, + "node_modules/safeps": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/safeps/-/safeps-7.0.1.tgz", + "integrity": "sha512-aFREKZzceHZH3KZTwjhDI1oOOcyAEBcQHjImJS/Mmx+KC31EQCgwiPKfwhJLBX7R4Y5ioI2D/VEcQ6U6ya2MJw==", + "license": "MIT", + "dependencies": { + "editions": "^1.3.3", + "extract-opts": "^3.3.1", + "safefs": "^4.1.0", + "taskgroup": "^5.0.0", + "typechecker": "^4.3.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/safeps/node_modules/editions": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", + "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scandirectory": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/scandirectory/-/scandirectory-2.5.0.tgz", + "integrity": "sha512-uT0CW8Z3YyoIQs2gXIZgR5miLkN/UNl+5IptQIq1YfD2NhFldikYlC3dkOE6MvF15OZMOxjg8yOjx5J/vIIPUA==", + "license": "MIT", + "dependencies": { + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "taskgroup": "^4.0.5" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/scandirectory/node_modules/ambi": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ambi/-/ambi-2.5.0.tgz", + "integrity": "sha512-5nS0gYMPNgZz/UALDHMStcwO42youpIWBQVbI92vV5j0+2bMxv/iVqearrLu3/f0XaU6xVIbf3RRtDxOcHxSkw==", + "license": "MIT", + "dependencies": { + "editions": "^1.1.1", + "typechecker": "^4.3.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/scandirectory/node_modules/editions": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", + "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/scandirectory/node_modules/safefs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/safefs/-/safefs-3.2.2.tgz", + "integrity": "sha512-qqvuS8qslGUSgUKQbdsYIK8Qg0EAkykxlsdfy3jpBSnhtyPsee/8y4RLc5+3CD6TgazBmtT0ekoGicUTPzICdg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "*" + }, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/scandirectory/node_modules/taskgroup": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz", + "integrity": "sha512-PD97E2OfwFH7SgeVRvR6K2c+NkKXZSwMMTdcM1t/3P+f70DUWbR81Qx7TF7dJj8dV631u4dhdBmhfDQjIZvGsg==", + "license": "MIT", + "dependencies": { + "ambi": "^2.2.0", + "csextends": "^1.0.3" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slug": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/slug/-/slug-0.4.2.tgz", + "integrity": "sha512-HQRxdDjtXsKG1pw8rBXGRq9fdW2fS2xPaizvJ3MK89x9+V8U0Z8//meWzJUdFW52pFDGqkLfyX+Fij7lkRY6Kw==", + "dependencies": { + "unicode": ">= 0.3.1" + }, + "engines": { + "node": ">= 0.4.x" + } + }, + "node_modules/sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha512-7bgVOAnPj3XjrKY577S+puCKGCRlUrcrEdsMeRXlg9Ghf5df/xNi6sONUa43WrHUd3TjJBF7O04jYoiY0FVa0A==", + "deprecated": "This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.", + "optional": true, + "dependencies": { + "hoek": "2.x.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "license": "MIT" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "license": "MIT", + "optional": true + }, + "node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/taskgroup": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-5.5.0.tgz", + "integrity": "sha512-YFkdc6HU+p3xO2lZ1MWdx7R7EbrLF/bpXv5k9635bTzdgOLNbmnsDg5alSpZost+PYMk40d6ZDAJHBHNHiiLvw==", + "license": "MIT", + "dependencies": { + "ambi": "3.2.0", + "eachr": "^3.2.0", + "editions": "^2.2.0", + "extendr": "^3.5.0", + "safeps": "7.0.1", + "unbounded": "^1.2.0" + }, + "engines": { + "node": ">=0.8" + }, + "funding": { + "type": "cooperative", + "url": "https://bevry.me/fund" + } + }, + "node_modules/testdino-mcp": { + "version": "1.0.7", + "resolved": "file:testdino-mcp-1.0.7.tgz", + "integrity": "sha512-a1m+xkfKcPQC41aQikAZ29r7YFLVfZgBf4xdXUFVvJM1yGt2xyOdeqN43lrnPjAZadb0TzOfDY5aydmDsaKepw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.1", + "docker": "^1.0.0" + }, + "bin": { + "testdino-mcp": "dist/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/toc": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/toc/-/toc-0.4.0.tgz", + "integrity": "sha512-Z4MqUbtLQrbJLQQFLKK0g5tGmke0vqB8puHrXXgRfPyLJTcsn5ACy/uxVnMrg6wSWPoS2hvVpw6wSAFYAkAEVA==", + "dependencies": { + "entities": "~0.5.0", + "lodash": "~2.4.1", + "slug": "~0.4.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/toc/node_modules/entities": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-0.5.0.tgz", + "integrity": "sha512-T5XQtlzuW+PfeSsGp3uryfYQof820zYbnUnUDEkwUVIAfgYeixIN16c4jh8gs0SqJUTGLU0XD6QsvjEPbmdwzQ==", + "license": "BSD-like" + }, + "node_modules/toc/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==", + "engines": [ + "node", + "rhino" + ], + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", + "optional": true + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typechecker": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-4.11.0.tgz", + "integrity": "sha512-lz39Mc/d1UBcF/uQFL5P8L+oWdIn/stvkUgHf0tPRW4aEwGGErewNXo2Nb6We2WslWifn00rhcHbbRWRcTGhuw==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0" + }, + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, + "node_modules/unbounded": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/unbounded/-/unbounded-1.3.0.tgz", + "integrity": "sha512-RWVCkvcoItljlNTz0iTdBQU6bDj+slVLNaWN7d6DXgH02FfYrz8ytcJ4OPW8b0HqmCehwufJHOIzjHWrQUXBvg==", + "license": "MIT", + "dependencies": { + "editions": "^2.2.0" + }, + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz", + "integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg==", + "license": "MIT", + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/watchr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/watchr/-/watchr-2.6.0.tgz", + "integrity": "sha512-eHqnPA71jn+lLf/c49mjXqQzzwKLmDdLZXiB53PtgBY8X75zqUWL2PmJWjJ45Bcy8PHOMDdVUCLEud36Lk5QZQ==", + "license": "MIT", + "dependencies": { + "eachr": "^3.2.0", + "extendr": "^3.2.2", + "extract-opts": "^3.3.1", + "ignorefs": "^1.1.1", + "safefs": "^4.1.0", + "scandirectory": "^2.5.0", + "taskgroup": "^5.0.1", + "typechecker": "^4.3.0" + }, + "bin": { + "watchr": "bin/watchr" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index 4d7a316..69a6d4c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@types/node": "^24.1.0" }, "dependencies": { - "dotenv": "^17.2.1", - "playwright": "^1.55.0" + "testdino-mcp": "file:testdino-mcp-1.0.7.tgz", + "dotenv": "^17.2.1" } } diff --git a/playwright.config.js b/playwright.config.js index 8af6021..832cf2b 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,32 +1,40 @@ // @ts-check import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); const isCI = !!process.env.CI; export default defineConfig({ testDir: './tests', + snapshotDir: './__screenshots__', fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 1, + retries: isCI ? 1 : 1, // Enable retries for flaky test behavior workers: isCI ? 5 : 5, timeout: 60 * 1000, + expect: { + timeout: 10 * 1000, + }, + reporter: [ - ['html', { - outputFolder: 'playwright-report', - open: 'never' - }], - ['blob', { outputDir: 'blob-report' }], + // Mandatory reporter for JSON results ['json', { outputFile: './playwright-report/report.json' }], - ['@testdino/playwright', { token: process.env.TESTDINO_TOKEN }], + // Optional, enables native HTML upload + ['html', { outputDir: './playwright-report' }], ], use: { - baseURL: 'https://storedemo.testdino.com/', + baseURL: 'https://storedemo.testdino.com/products', headless: true, - trace: 'on', + trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', + actionTimeout: 15 * 1000, + navigationTimeout: 30 * 1000, }, projects: [ @@ -52,10 +60,9 @@ export default defineConfig({ }, { name: 'ios', - use: { ...devices['iPhone 12'] }, + use: { ...devices['iPhone 14'] }, grep: /@ios/, // only run tests tagged @ios }, - { name: 'api', use: { ...devices['API'] }, diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js index 123f880..7b86928 100644 --- a/tests/cart_checkout.spec.js +++ b/tests/cart_checkout.spec.js @@ -9,169 +9,692 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); -test.describe('Cart Module', () => { - test.describe('Product Removal', () => { - test('Verify that user is able to delete selected product from cart ',{tag: '@ios'}, async () => { - const productName = 'GoPro HERO10 Black'; - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnDeleteProductIcon(); - await allPages.cartPage.verifyCartItemDeleted(productName); - - }); - }); -}); +/* ---------- Helpers ---------- */ + +async function login( + username = process.env.USERNAME, + password = process.env.PASSWORD +) { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(username, password); +} + +/* ---------- FLAKY TESTS (fail on 1st run + 1st retry, pass on 2nd retry) ---------- */ -test.describe('Orders Module', () => { - test.describe('Order Cancellation', () => { - test('Verify new user views and cancels an order in my orders ',{tag: '@chromium'}, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - let productName = `Rode NT1-A Condenser Mic`; - - await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); +test.describe('Flaky tests (pass on 2nd retry)', () => { + test.describe.configure({ retries: 2 }); + + test( + 'Verify that user can login and logout successfully', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Login' }, + { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: login and logout on Chromium' } + ] + }, + async ({}, testInfo) => { + const start = Date.now(); + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); + } + ); - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickAllProductsNav(); - productName = await allPages.allProductsPage.getNthProductName(1); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); + test( + 'User searches products and views result (Searchbox)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Search' }, + { type: 'testdino:link', description: 'https://jira.example.com/SEARCH-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: search products on Firefox' } + ] + }, + async ({}, testInfo) => { + const start = Date.now(); + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); + } + ); - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.assertYourCartTitle(); - // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.fillShippingAddress( - // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - // ); - // await allPages.checkoutPage.clickSaveAddressButton(); - // await allPages.checkoutPage.assertAddressAddedToast(); - // }); - - // await test.step('Complete order and verify in my orders', async () => { - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.inventoryPage.clickOnContinueShopping(); - - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.clickCancelOrderButton(); - // await allPages.orderPage.confirmCancellation(); + test( + 'User navigates through product categories (Product page)', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Products' }, + { type: 'testdino:link', description: 'https://jira.example.com/PRODUCTS-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: product categories on WebKit' } + ] + }, + async ({}, testInfo) => { + const start = Date.now(); + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); - }); - }); + } + ); }); -test.describe('User Journey', () => { - test.describe('Multiple Order Placement', () => { - test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement ',{tag: '@chromium'}, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - }); - await test.step('Navigate product details and validate tabs', async () => { - await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.allProductsPage.clickNthProduct(1); - // await allPages.productDetailsPage.clickOnReviewsTab(); - // await allPages.productDetailsPage.assertReviewsTab(); - // await allPages.productDetailsPage.clickOnAdditionalInfoTab(); - // await allPages.productDetailsPage.assertAdditionalInfoTab(); - }); +/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ - await test.step('Place first order', async () => { - // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.clickIncreaseQuantityButton(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.fillShippingAddress( - // process.env.SFIRST_NAME, - // email, - // process.env.SCITY, - // process.env.SSTATE, - // process.env.SSTREET_ADD, - // process.env.SZIP_CODE, - // process.env.SCOUNTRY - // ); - // await allPages.checkoutPage.clickSaveAddressButton(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.checkoutPage.clickOnContinueShoppingButton(); - }); +test( + 'Verify that all the navbar are working properly', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar functionality on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); - await test.step('Place second order using existing address', async () => { - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.allProductsPage.clickNthProduct(1); - // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }); +test( + 'Verify that user can edit and delete a product review', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete product review on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); - }); -}); + } +); -test.describe('Authentication', () => { - test.describe('Signup & Login', () => { - test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully ',{tag: '@chromium'}, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }); - }); -}); +test( + 'Verify that User Can Complete the Journey from Login to Order Placement', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order placement journey on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); -test.describe('User Profile', () => { - test.describe('Personal Information', () => { - test('Verify that user can update personal information ',{tag: '@firefox'}, async () => { - await allPages.userPage.clickOnUserProfileIcon(); - // await allPages.userPage.updatePersonalInfo(); - // await allPages.userPage.verifyPersonalInfoUpdated(); +test( + 'Verify that user can filter products by price range', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter products by price on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); - }); -}); + } +); + +test( + 'Verify if user can add product to wishlist, move to cart and checkout', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart and checkout on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user is able to submit a product review', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that all the navbar are working properly (Navbar)', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar (Navbar) on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can edit and delete a product review (Single review)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete review (Single review) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that User Can Complete the Journey from Login to Order Placement (Single order)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order (Single order) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can filter products by price range (Price page', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter by price (Price page) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify if user can add product to wishlist, move to cart(Checkout page)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart (Checkout page) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user is able to submit a product review (Review)', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review (Review) on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can update cart quantity and verify total price', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update cart quantity and total price on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can view order history and order detail (Order page)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history and order detail (Order page) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can update cart quantity and verify total price (Pricing)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Cart quantity and total price (Pricing) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can view order history and order details properly (Order details)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history and details (Order details) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that users can update cart quantity and verify total price (Single order)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Cart quantity (Single order) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that users can view order history and order details properly (Order history)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); diff --git a/tests/delete-api.spec.js b/tests/delete-api.spec.js index e85a3da..0278cbd 100644 --- a/tests/delete-api.spec.js +++ b/tests/delete-api.spec.js @@ -7,7 +7,18 @@ const USERS_ENDPOINT = '/users'; test.describe('DELETE User API', () => { - test('Remove user 1', { tag: '@api' }, async ({ request }) => { + test('Remove user 1', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'DELETE remove user by ID' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 1; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); @@ -15,9 +26,39 @@ test.describe('DELETE User API', () => { const body = await response.json(); expect(body).toHaveProperty('id', userId); expect(body).toHaveProperty('isDeleted', true); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test.skip('Remove user twice', { tag: '@api' }, async ({ request }) => { + test('Remove user twice', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'DELETE user twice idempotency' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 2; // First deletion @@ -30,9 +71,39 @@ test.describe('DELETE User API', () => { expect(response2.status()).toBe(200); const body2 = await response2.json(); expect(body2).toHaveProperty('id', userId); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 2, + unit: 'count', + }), + }); }); - test.skip('Validate body is returned', { tag: '@api' }, async ({ request }) => { + test('Validate body is returned', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'DELETE response body validation' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 3; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); @@ -43,5 +114,24 @@ test.describe('DELETE User API', () => { expect(body).toBeInstanceOf(Object); expect(body).toHaveProperty('id'); expect(typeof body.id).toBe('number'); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/example.spec.js b/tests/example.spec.js index c2a66c6..b789a0b 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,76 +28,146 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully', { tag: '@android' }, async () => { +test('Verify that user can log in and log out successfully', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Login' }, + { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login and logout on Android' } + ] +}, async () => { + const start = Date.now(); await login(); await logout(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that user can update personal information', { tag: '@webkit' }, async () => { - // await login(); - await allPages.userPage.clickOnUserProfileIcon(); - // await allPages.userPage.updatePersonalInfo(); - // await allPages.userPage.verifyPersonalInfoUpdated(); +test('Verify that all navbar links work properly', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar functionality on WebKit' } + ] +}, async () => { + const start = Date.now(); + // await login(); + await allPages.homePage.clickBackToHomeButton(); + // await allPages.homePage.assertHomePage(); + await allPages.homePage.clickAllProductsNav(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.homePage.clickAboutUsNav(); + await allPages.homePage.assertAboutUsTitle(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', { tag: '@chromium' }, async () => { +test('Verify that user can edit and delete a product review', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete product review on Firefox' } + ] +}, async () => { + const start = Date.now(); + await test.step('Login as existing user and navigate to a product', async () => { // await login(); + }) - await test.step('Verify that user is able to add address successfully', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.fillAddressForm(); - await allPages.userPage.verifytheAddressIsAdded(); - }); + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) - await test.step('Verify that user is able to edit address successfully', async () => { - await allPages.userPage.clickOnEditAddressButton(); - await allPages.userPage.updateAddressForm(); - await allPages.userPage.verifytheUpdatedAddressIsAdded(); + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); }) - await test.step('Verify that user is able to delete address successfully', async () => { - await allPages.userPage.clickOnDeleteAddressButton(); - }); -}); + await test.step('Edit the submitted review and verify changes', async () => { + await allPages.productDetailsPage.clickOnEditReviewBtn(); + await allPages.productDetailsPage.updateReviewForm(); + await allPages.productDetailsPage.assertUpdatedReview({ + title: 'Updated Review Title', + opinion: 'This is an updated review opinion.' + }) + }); -test('Verify that user can change password successfully', { tag: '@firefox' }, async () => { - await test.step('Login with existing password', async () => { - // await login1(); + await test.step('Delete the submitted review and verify deletion', async () => { + await allPages.productDetailsPage.clickOnDeleteReviewBtn(); }); - await test.step('Change password and verify login with new password', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.enterNewPassword(); - await allPages.userPage.enterConfirmNewPassword(); - await allPages.userPage.clickOnUpdatePasswordButton(); - await allPages.userPage.getUpdatePasswordNotification(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); - await test.step('Verify login with new password and revert back to original password', async () => { - // Re-login with new password - await logout(); - await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); - - // Revert back - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.revertPasswordBackToOriginal(); - await allPages.userPage.getUpdatePasswordNotification(); - }) }); -test('Verify that User Can Complete the Journey from Login to Order Placement', { tag: '@webkit' }, async () => { +test('Verify that user can complete the journey from login to order placement', { + tag: '@ios', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order placement journey on iOS' } + ] +}, async () => { + const start = Date.now(); const productName = 'GoPro HERO10 Black'; - // await login(); + // await login(); await allPages.inventoryPage.clickOnShopNowButton(); await allPages.inventoryPage.clickOnAllProductsLink(); await allPages.inventoryPage.searchProduct(productName); await allPages.inventoryPage.verifyProductTitleVisible(productName); await allPages.inventoryPage.clickOnAddToCartIcon(); - // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.verifyCartItemVisible(productName); // await allPages.cartPage.clickOnCheckoutButton(); // await allPages.checkoutPage.verifyCheckoutTitle(); @@ -106,67 +176,30 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify user can place and cancel an order', { tag: '@webkit' }, async () => { - const productName = 'GoPro HERO10 Black'; - const productPriceAndQuantity = '₹49,999 × 1'; - const productQuantity = '1'; - const orderStatusProcessing = 'Processing'; - const orderStatusCanceled = 'Canceled'; - - await test.step('Verify that user can login successfully', async () => { - // await login(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - }) - - // await test.step('Add product to cart and checkout', async () => { - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.verifyCartItemVisible(productName); - // await allPages.cartPage.clickOnCheckoutButton(); - // }) - - // await test.step('Place order and click on continue shopping', async () => { - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.verifyProductInCheckout(productName); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.checkoutPage.verifyOrderItemName(productName); - // await allPages.inventoryPage.clickOnContinueShopping(); - // }) - - // await test.step('Verify order in My Orders', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyMyOrdersTitle(); - // await allPages.orderPage.clickOnPaginationButton(2); - // await allPages.orderPage.verifyProductInOrderList(productName); - // await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); - // await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); - // await allPages.orderPage.clickOnPaginationButton(1); - // await allPages.orderPage.clickViewDetailsButton(1); - // await allPages.orderPage.verifyOrderDetailsTitle(); - // await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); - // }) - - // await test.step('Cancel order and verify status is updated to Canceled', async () => { - // await allPages.orderPage.clickCancelOrderButton(2); - // await allPages.orderPage.confirmCancellation(); - // await allPages.orderPage.verifyCancellationConfirmationMessage(); - // await allPages.orderPage.verifyMyOrdersCount(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyMyOrdersTitle(); - // await allPages.orderPage.clickOnPaginationButton(2); - // await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); - // }) -}); - -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { tag: '@firefox' }, async () => { +test('Verify that a new user can complete the journey from registration to a single order placement', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Registration' }, + { type: 'testdino:link', description: 'https://jira.example.com/REG-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Registration to single order on Android' } + ] +}, async () => { + const start = Date.now(); // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -176,21 +209,21 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra let productPrice; let productReviewCount; - // await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - // }) + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }) - // await test.step('Verify that user can login successfully', async () => { - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - // }) + await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }) await test.step('Navigate to all product and add to wishlist section', async () => { await allPages.homePage.clickAllProductsNav(); @@ -211,7 +244,7 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }) await test.step('Add product to cart, add new address and checkout', async () => { - // await allPages.productDetailsPage.clickAddToCartButton(); + await allPages.productDetailsPage.clickAddToCartButton(); // await allPages.productDetailsPage.clickCartIcon(); // await allPages.cartPage.assertYourCartTitle(); @@ -276,9 +309,31 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra // await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); // await allPages.orderDetailsPage.clickBackToHomeButton(); }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in', { tag: '@chromium' }, async () => { +test('Verify that user can add product to cart before logging in and complete order after logging in', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Add to cart before login, order after login on WebKit' } + ] +}, async () => { + const start = Date.now(); await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -286,28 +341,70 @@ test('Verify that user add product to cart before logging in and then complete o await allPages.homePage.validateAddCartNotification(); // await allPages.loginPage.clickOnUserProfileIcon(); }) - await test.step('Login and complete order', async () => { - // await login(); - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}) + // await test.step('Login and complete order', async () => { +// await login(); +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// }) + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that user can filter products by price range', { tag: '@chromium' }, async () => { - // await login(); +test('Verify that user can filter products by price range', { + tag: '@filter', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter products by price range' } + ] +}, async () => { + const start = Date.now(); + await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); await allPages.homePage.clickOnFilterButton(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out', { tag: '@webkit' }, async () => { - // await login(); +test('Verify that user can add product to wishlist, move it to cart, and checkout', { + tag: '@wishlist', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart and checkout' } + ] +}, async () => { + const start = Date.now(); + // await login(); await test.step('Add product to wishlistand then add to cart', async () => { await allPages.homePage.clickOnShopNowButton(); @@ -319,18 +416,41 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }) await test.step('Checkout product added to cart', async () => { - // await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.clickOnCartIcon(); // await allPages.cartPage.clickOnCheckoutButton(); // await allPages.checkoutPage.verifyCheckoutTitle(); // await allPages.checkoutPage.selectCashOnDelivery(); // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) - + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify new user views and cancels an order in my orders', { tag: '@webkit' }, async () => { +test.describe('Orders Module', () => { + test.describe('Order Cancellation', () => { + test('Verify that new user can view and cancel an order in My Orders', { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p0' }, + { type: 'testdino:feature', description: 'Orders' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, + { type: 'testdino:owner', description: '@Kriti Verma' }, + { type: 'testdino:notify-slack', description: '@Kriti Verma' }, + { type: 'testdino:context', description: 'Critical order cancellation flow for new users' } + ] + }, async () => { + const start = Date.now(); const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -359,7 +479,18 @@ test('Verify new user views and cancels an order in my orders', { tag: '@webkit' productName = await allPages.allProductsPage.getNthProductName(1); await allPages.allProductsPage.clickNthProduct(1); await allPages.productDetailsPage.clickAddToCartButton(); - }) + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'order-flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); // await test.step('Add product to cart, add new address and checkout', async () => { // await allPages.productDetailsPage.clickCartIcon(); @@ -386,33 +517,28 @@ test('Verify new user views and cancels an order in my orders', { tag: '@webkit' // await allPages.orderPage.clickCancelOrderButton(); // await allPages.orderPage.confirmCancellation(); // }); + }); + }); }); -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', { tag: '@webkit' }, async () => { +test('Verify that a new user can complete the journey from registration to multiple order placements', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Registration to multiple order placement on Firefox' } + ] +}, async () => { + const start = Date.now(); const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; let productName= `Rode NT1-A Condenser Mic`; - // await test.step('Verify that user can register successfully', async () => { - // // Signup - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - // }) - - // await test.step('Verify that user can login successfully', async () => { - // // Login as new user - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - // }) - await test.step('Navigate to All Products and add view details of a random product', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.allProductsPage.assertAllProductsTitle(); @@ -451,65 +577,104 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { tag: '@ios' }, async () => { +test('Verify that a new user can sign up, log in, and navigate to the home page successfully', { + tag: '@ios', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Registration' }, + { type: 'testdino:link', description: 'https://jira.example.com/REG-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Sign up, login and navigate home on iOS' } + ] +}, async () => { + const start = Date.now(); const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; -// await test.step('Verify that user can register successfully', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) - -// await test.step('Verify that user can login successfully', async () => { -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }) + + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }) -test('Verify that user is able to fill Contact Us page successfully', { tag: '@firefox' }, async () => { - // await login(); +test('Verify that user can fill and submit the Contact Us form successfully', { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Contact' }, + { type: 'testdino:link', description: 'https://jira.example.com/CONTACT-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Contact Us form submission on Chromium' } + ] +}, async () => { + const start = Date.now(); + await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); await allPages.contactUsPage.fillContactUsForm(); await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); -}); - -test('Verify that user is able to submit a product review', { tag: '@android' }, async () => { - await test.step('Login as existing user and navigate to a product', async () => { - // await login(); - }) - - await test.step('Navigate to all product section and select a product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - }) - - await test.step('Submit a product review and verify submission', async () => { - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - - await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - await allPages.productDetailsPage.fillReviewForm(); - await allPages.productDetailsPage.assertSubmittedReview({ - name: 'John Doe', - title: 'Great Product', - opinion: 'This product exceeded my expectations. Highly recommend!' + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); - }) }); -test('Verify that user can edit and delete a product review', { tag: '@chromium' }, async () => { +test('Verify that user can submit a product review', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review on Firefox' } + ] +}, async () => { + const start = Date.now(); await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -530,58 +695,60 @@ test('Verify that user can edit and delete a product review', { tag: '@chromium' name: 'John Doe', title: 'Great Product', opinion: 'This product exceeded my expectations. Highly recommend!' - }); - }) - - await test.step('Edit the submitted review and verify changes', async () => { - await allPages.productDetailsPage.clickOnEditReviewBtn(); - await allPages.productDetailsPage.updateReviewForm(); - await allPages.productDetailsPage.assertUpdatedReview({ - title: 'Updated Review Title', - opinion: 'This is an updated review opinion.' - }) }); + }); - await test.step('Delete the submitted review and verify deletion', async () => { - await allPages.productDetailsPage.clickOnDeleteReviewBtn(); - }) -}); - -test('Verify that user can purchase multiple quantities in a single order', { tag: '@ios' }, async () => { - const productName = 'GoPro HERO10 Black'; - await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickIncreaseQuantityButton(); - await allPages.cartPage.verifyIncreasedQuantity('3'); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that all the navbar are working properly', { tag: '@ios' }, async () => { - await login(); - await allPages.homePage.clickBackToHomeButton(); - // await allPages.homePage.assertHomePage(); - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.homePage.clickOnContactUsLink(); - await allPages.contactUsPage.assertContactUsTitle(); - await allPages.homePage.clickAboutUsNav(); - await allPages.homePage.assertAboutUsTitle(); +test('Verify that user can update personal information in profile', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Profile' }, + { type: 'testdino:link', description: 'https://jira.example.com/PROFILE-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update personal information on WebKit' } + ] +}, async () => { + const start = Date.now(); + await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.updatePersonalInfo(); +// await allPages.userPage.verifyPersonalInfoUpdated(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); -test('Verify that user is able to delete selected product from cart', { tag: '@android' }, async () => { +test('Verify that user can delete a selected product from cart', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delete selected product from cart on Android' } + ] +}, async () => { + const start = Date.now(); const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -597,4 +764,14 @@ test('Verify that user is able to delete selected product from cart', { tag: '@a await allPages.cartPage.verifyEmptyCartMessage(); await allPages.cartPage.clickOnStartShoppingButton(); await allPages.allProductsPage.assertAllProductsTitle(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); \ No newline at end of file diff --git a/tests/get-users.spec.js b/tests/get-users.spec.js index 1ab0a3d..817e7e6 100644 --- a/tests/get-users.spec.js +++ b/tests/get-users.spec.js @@ -7,16 +7,57 @@ const USERS_ENDPOINT = '/users'; test.describe('GET Users API', () => { - test('Fetch all users', { tag: '@api' }, async ({ request }) => { + test('Fetch all users', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'GET fetch all users' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); const body = await response.json(); expect(body).toHaveProperty('users'); expect(Array.isArray(body.users)).toBe(true); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Fetch user by ID = 1', { tag: '@api' }, async ({ request }) => { + test('Fetch user by ID = 1', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'GET user by ID' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -24,18 +65,78 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('id', 1); expect(body).toHaveProperty('firstName'); expect(body).toHaveProperty('lastName'); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Validate total users > 0', { tag: '@api' }, async ({ request }) => { + test('Validate total users > 0', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate total users count' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); const body = await response.json(); expect(body).toHaveProperty('total'); expect(body.total).toBeGreaterThan(0); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Validate user image exists', { tag: '@api' }, async ({ request }) => { + test('Validate user image exists', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate user image field' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -43,9 +144,39 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('image'); expect(body.image).toBeTruthy(); expect(typeof body.image).toBe('string'); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Validate user 1 has firstName field', { tag: '@api' }, async ({ request }) => { + test('Validate user 1 has firstName field', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate firstName field' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -53,15 +184,75 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('firstName'); expect(typeof body.firstName).toBe('string'); expect(body.firstName.length).toBeGreaterThan(0); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Invalid user ID returns 404', { tag: '@api' }, async ({ request }) => { + test('Invalid user ID returns 404', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-006' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Invalid user ID returns 404' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/999999`); expect(response.status()).toBe(404); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('default users (no query) returns data object/array', { tag: '@api' }, async ({ request }) => { + test('default users (no query) returns data object/array', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-007' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Default users response structure' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -69,9 +260,39 @@ test.describe('GET Users API', () => { expect(body).toBeInstanceOf(Object); // Should have either 'users' array or be an array itself expect(body.users || Array.isArray(body)).toBeTruthy(); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('limit param returns limited results', { tag: '@api' }, async ({ request }) => { + test('limit param returns limited results', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-008' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Limit param pagination' } + ] + }, async ({ request }) => { + const start = Date.now(); const limit = 5; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}`); @@ -80,9 +301,39 @@ test.describe('GET Users API', () => { const users = body.users || body; const usersArray = Array.isArray(users) ? users : []; expect(usersArray.length).toBeLessThanOrEqual(limit); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('skip param shifts results', { tag: '@api' }, async ({ request }) => { + test('skip param shifts results', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-009' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Skip param pagination' } + ] + }, async ({ request }) => { + const start = Date.now(); const skip = 5; const limit = 10; @@ -105,9 +356,39 @@ test.describe('GET Users API', () => { if (firstUser1 && firstUser2) { expect(firstUser1.id).not.toBe(firstUser2.id); } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 2, + unit: 'count', + }), + }); }); - test('sorting / search query (if supported) returns filtered results', { tag: '@api' }, async ({ request }) => { + test('sorting / search query (if supported) returns filtered results', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-010' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Search query filtered results' } + ] + }, async ({ request }) => { + const start = Date.now(); // Try search query parameter (common patterns: q, search, query) const searchTerm = 'john'; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/search?q=${searchTerm}`); @@ -121,9 +402,39 @@ test.describe('GET Users API', () => { // At least verify the response structure is valid expect(Array.isArray(usersArray)).toBe(true); } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('delayed response (3s) should return 200', { tag: '@api' }, async ({ request }) => { + test('delayed response (3s) should return 200', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-011' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delayed response returns 200' } + ] + }, async ({ request }) => { + const start = Date.now(); const isRetry = test.info().retry > 0; if (!isRetry) { expect(true).toBe(false); @@ -144,9 +455,39 @@ test.describe('GET Users API', () => { const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 10000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('enforce timeout (expect to fail if too slow) — set short timeout', { tag: '@api' }, async ({ request }) => { + test('enforce timeout (expect to fail if too slow) — set short timeout', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-012' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Enforce request timeout' } + ] + }, async ({ request }) => { + const start = Date.now(); const delay = 5; const shortTimeout = 2000; @@ -160,5 +501,24 @@ test.describe('GET Users API', () => { } catch (error) { expect(error.message).toMatch(/timeout|Timeout/i); } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/post-api.spec.js b/tests/post-api.spec.js index 38059eb..a0518b3 100644 --- a/tests/post-api.spec.js +++ b/tests/post-api.spec.js @@ -8,7 +8,18 @@ const ADD_ENDPOINT = '/users/add'; test.describe('POST Create User API', () => { - test('Bad endpoint returns 404', { tag: '@api' }, async ({ request }) => { + test('Bad endpoint returns 404', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'POST bad endpoint returns 404' } + ] + }, async ({ request }) => { + const start = Date.now(); const userData = { firstName: 'Test', lastName: 'User' @@ -19,9 +30,39 @@ test.describe('POST Create User API', () => { }); expect(response.status()).toBe(404); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Invalid JSON payload handling', { tag: '@api' }, async ({ request }) => { + test('Invalid JSON payload handling', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Invalid JSON payload handling' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.post(`${API_BASE_URL}${ADD_ENDPOINT}`, { data: 'invalid json string', headers: { @@ -31,25 +72,115 @@ test.describe('POST Create User API', () => { // Should return 400 Bad Request for invalid JSON expect([400, 422]).toContain(response.status()); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Too large ID param should return 404', { tag: '@api' }, async ({ request }) => { + test('Too large ID param should return 404', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Too large ID param returns 404' } + ] + }, async ({ request }) => { + const start = Date.now(); const tooLargeId = 999999999; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/${tooLargeId}`); expect(response.status()).toBe(404); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Deleting invalid id returns 200/response but not crash', { tag: '@api' }, async ({ request }) => { + test('Deleting invalid id returns 200/response but not crash', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delete invalid id response handling' } + ] + }, async ({ request }) => { + const start = Date.now(); const invalidId = 999999; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${invalidId}`); expect([200, 404]).toContain(response.status()); const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('PUT: Invalid method usage returns appropriate response (no 500)', { tag: '@api' }, async ({ request }) => { + test('PUT: Invalid method usage returns appropriate response (no 500)', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Invalid PUT method usage response' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 1; const updateData = { firstName: 'Updated' @@ -63,9 +194,39 @@ test.describe('POST Create User API', () => { // Should return appropriate error (400, 404, 405) but not 500 expect([400, 404, 405, 200]).toContain(response.status()); expect(response.status()).not.toBe(500); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('user schema contains expected keys', { tag: '@api' }, async ({ request }) => { + test('user schema contains expected keys', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-006' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'User schema expected keys validation' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -76,9 +237,39 @@ test.describe('POST Create User API', () => { expectedKeys.forEach(key => { expect(body).toHaveProperty(key); }); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('users list contains objects with id and email', { tag: '@api' },async ({ request }) => { + test('users list contains objects with id and email', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-007' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Users list id and email validation' } + ] + }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -94,5 +285,24 @@ test.describe('POST Create User API', () => { expect(typeof firstUser.email).toBe('string'); } } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/updateUser.spec.js b/tests/updateUser.spec.js index ae4e06a..52ace44 100644 --- a/tests/updateUser.spec.js +++ b/tests/updateUser.spec.js @@ -8,7 +8,18 @@ const AUTH_ENDPOINT = '/auth/login'; test.describe('PUT / PATCH Update User API', () => { - test('Update user details', { tag: '@api' }, async ({ request }) => { + test('Update user details', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'PUT/PATCH update user API' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 1; const updateData = { firstName: 'John', @@ -16,7 +27,6 @@ test.describe('PUT / PATCH Update User API', () => { age: 30 }; - // Try PUT first, fallback to PATCH if needed const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { data: updateData }); @@ -26,9 +36,39 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toHaveProperty('id', userId); expect(body).toHaveProperty('firstName', updateData.firstName); expect(body).toHaveProperty('lastName', updateData.lastName); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Update user with empty payload', { tag: '@api' }, async ({ request }) => { + test('Update user with empty payload', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update user with empty payload' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 2; const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { data: {} @@ -38,9 +78,39 @@ test.describe('PUT / PATCH Update User API', () => { const body = await response.json(); expect(body).toBeInstanceOf(Object); expect(body).toHaveProperty('id', userId); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Update only one field', { tag: '@api' }, async ({ request }) => { + test('Update only one field', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'PATCH partial user update' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 3; const updateData = { firstName: 'UpdatedFirstName' @@ -55,9 +125,39 @@ test.describe('PUT / PATCH Update User API', () => { const body = await response.json(); expect(body).toHaveProperty('id', userId); expect(body).toHaveProperty('firstName', updateData.firstName); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Validate returned name field', { tag: '@api' }, async ({ request }) => { + test('Validate returned name field', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate returned name field from update' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 4; const updateData = { firstName: 'Jane', @@ -76,9 +176,39 @@ test.describe('PUT / PATCH Update User API', () => { expect(typeof body.lastName).toBe('string'); expect(body.firstName).toBe(updateData.firstName); expect(body.lastName).toBe(updateData.lastName); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Update and validate response contains updatedAt simulation', { tag: '@api' }, async ({ request }) => { + test('Update and validate response contains updatedAt simulation', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate updatedAt in update response' } + ] + }, async ({ request }) => { + const start = Date.now(); const userId = 5; const updateData = { firstName: 'Updated', @@ -100,9 +230,39 @@ test.describe('PUT / PATCH Update User API', () => { // At minimum, validate the response structure expect(body).toBeInstanceOf(Object); expect(body).toHaveProperty('id', userId); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Login failure (invalid creds)', { tag: '@api' }, async ({ request }) => { + test('Login failure (invalid creds)', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-006' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Auth login failure with invalid credentials' } + ] + }, async ({ request }) => { + const start = Date.now(); const loginData = { username: 'invaliduser', password: 'wrongpassword' @@ -116,9 +276,39 @@ test.describe('PUT / PATCH Update User API', () => { expect([400, 401, 403]).toContain(response.status()); const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); - test('Login missing fields returns 400', { tag: '@api' }, async ({ request }) => { + test('Login missing fields returns 400', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-007' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login missing fields returns 400' } + ] + }, async ({ request }) => { + const start = Date.now(); const loginData = { username: 'kminchelle' }; @@ -130,5 +320,24 @@ test.describe('PUT / PATCH Update User API', () => { expect(response.status()).toBe(400); const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/visual.spec.js b/tests/visual.spec.js index a410be9..d884647 100644 --- a/tests/visual.spec.js +++ b/tests/visual.spec.js @@ -11,13 +11,35 @@ test.beforeEach(async ({ page }) => { test.describe('Visual Comparison', () => { test.describe('GitHub Login Page', () => { - test('visual comparison demo test', { tag: ['@visual', '@chromium'] }, async ({ page }) => { - await page.goto('https://github.com/login'); + test('visual comparison demo test', { + tag: ['@visual', '@chromium'], + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Visual' }, + { type: 'testdino:link', description: 'https://jira.example.com/VISUAL-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Visual comparison demo for GitHub login on Chromium' } + ] + }, async ({ page }) => { + const start = Date.now(); + await page.goto('https://github.com/login'); await expect(page).toHaveScreenshot('github-login.png'); await page.getByRole('textbox', { name: 'Username or email address' }).click(); await page.getByRole('textbox', { name: 'Username or email address' }).fill('test'); await expect(page).toHaveScreenshot('github-login-changed.png'); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'visual-flow-time', + value: flowTime, + unit: 'ms', + threshold: 10000, + }), + }); }); }); });