diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc973ea..b417a53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,45 +1,43 @@ -# .github/workflows/playwright-daily.yml -# Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) -# plus anytime you trigger it manually from the Actions tab. - name: Run Playwright tests on: + push: + pull_request: schedule: - - cron: '30 3 * * 1-5' + - cron: '30 3 * * *' # 9:00 AM IST everyday # 11:00 AM IST workflow_dispatch: jobs: run-tests: - name: Run Playwright shards + name: Run shard ${{ matrix.shardIndex }}/5 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - shardIndex: [1,2,3] - shardTotal: [2] + shardIndex: [1, 2, 3, 4, 5] + shardTotal: [5] steps: - - uses: actions/checkout@v4 + - name: Checkout repo + uses: actions/checkout@v4 - - name: Setup Node.js 18.x + - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: '18' + node-version: "20" + - name: Create .env file run: | - echo "USERNAME=${{ secrets.USERNAME }}" >> .env - echo "USERNAME1=${{ secrets.USERNAME1 }}" >> .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 - echo "CITY=${{ secrets.CITY }}" >> .env - echo "STATE=${{ secrets.STATE }}" >> .env - echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env - echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - + echo "USERNAME=${{ secrets.USERNAME }}" >> .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 + echo "CITY=${{ secrets.CITY }}" >> .env + echo "STATE=${{ secrets.STATE }}" >> .env + echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env + echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -47,15 +45,15 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - - name: Install deps + browsers + - name: Install dependencies run: | npm ci npx playwright install --with-deps - - - name: Run shard ${{ matrix.shardIndex }} - run: npx playwright test --project=chromium --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - + - name: Run Playwright Tests + run: | + npx playwright test \ + --grep="@chromium|@firefox|@webkit" \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 @@ -67,30 +65,21 @@ jobs: merge-reports: name: Merge Reports needs: run-tests - if: always() # run even if some shards fail + if: always() runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Node.js 18.x + - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: '18' + node-version: "20" - - name: Cache npm dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install deps + browsers + - name: Install dependencies run: | npm ci npx playwright install --with-deps - - name: Download all blob reports uses: actions/download-artifact@v4 with: @@ -99,18 +88,19 @@ jobs: merge-multiple: true - name: Merge HTML & JSON reports - run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports - - - name: Upload combined report + run: | + npx playwright merge-reports \ + --config=playwright.config.js \ + ./all-blob-reports + - name: Upload combined Playwright report uses: actions/upload-artifact@v4 with: name: Playwright Test Report path: ./playwright-report retention-days: 14 - - name: Send TestDino report + - name: Upload report to TestDino + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --verbose + npx tdpw upload ./playwright-report --token="$TESTDINO_TOKEN" \ No newline at end of file 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 f9a4348..cff65e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# playwright-sample-tests-javascript +# Ecommerce-demo-test-playwright -Automated end-to-end tests for [e-commerce demo site](https://demo.alphabin.co/) 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..a434ed0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,21 +10,89 @@ "license": "ISC", "dependencies": { "dotenv": "^17.2.1", - "playwright": "^1.55.0" + "tdpw": "^1.0.34", + "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/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "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 +102,3086 @@ } }, "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": ">= 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": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "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/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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": ">= 0.8" + } + }, + "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/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "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/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "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/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "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/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=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-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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/tdpw": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/tdpw/-/tdpw-1.0.34.tgz", + "integrity": "sha512-XGNOjoakRhlma1vvRtkcIRU/BrEQcBKg9T5OlB6rkrgLys9Eo0Yt38V55K4U1zXjvEv8oRBiHR4Sbh7CWQFLyg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-progress": "^3.12.0", + "commander": "^14.0.3", + "dotenv-expand": "^12.0.3", + "figures": "^6.1.0", + "ora": "^8.2.0", + "simple-git": "~3.33.0", + "tslib": "^2.8.1", + "zod": "^4.3.6" + }, + "bin": { + "tdpw": "bin/tdpw.js" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/tdpw/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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..dedc491 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "dotenv": "^17.2.1", - "playwright": "^1.55.0" + "tdpw": "^1.0.34" } } diff --git a/pages/AllPages.js b/pages/AllPages.js index f9f7c91..c4c22f0 100644 --- a/pages/AllPages.js +++ b/pages/AllPages.js @@ -9,6 +9,7 @@ import CheckoutPage from "./CheckoutPage"; import OrderPage from "./OrderPage"; // Import OrderPage import UserPage from "./UserPage"; // Import UserPage import OrderDetailsPage from "./OrderDetailsPage"; +import ContactUsPage from "./ContactUsPage"; class AllPages { constructor(page) { @@ -24,6 +25,8 @@ class AllPages { this.orderPage = new OrderPage(page); // Instantiate OrderPage this.userPage = new UserPage(page); // Instantiate UserPage this.orderDetailsPage = new OrderDetailsPage(page); + this.contactUsPage = new ContactUsPage(page); + } } diff --git a/pages/CartPage.js b/pages/CartPage.js index 9de37c5..5b667b8 100644 --- a/pages/CartPage.js +++ b/pages/CartPage.js @@ -1,3 +1,4 @@ +import { start } from 'repl'; import BasePage from './BasePage.js'; import { expect } from '@playwright/test'; @@ -29,6 +30,9 @@ class CartPage extends BasePage{ checkoutButton: '[data-testid="checkout-button"]', viewCartButton: '[data-testid="view-cart-button"]', shoppingCartIcon: `[data-testid="header-cart-icon"]`, + deleteItemButton: '[aria-label="Remove item"]', + cartEmpty: `[data-testid="empty-cart"]`, + startShoppingButton: `[data-testid="continue-shopping-btn"]` } async assertYourCartTitle() { @@ -124,6 +128,27 @@ class CartPage extends BasePage{ await this.page.waitForTimeout(2000); await this.page.locator(this.locators.checkoutButton).click({ force: true }); } + + async verifyIncreasedQuantity(expectedQuantity) { + await expect(this.page.locator(this.locators.cartItemQuantity)).toHaveText(expectedQuantity); + } + + async clickOnDeleteProductIcon() { + await this.page.locator(this.locators.deleteItemButton).click(); + } + + async verifyCartItemDeleted() { + await expect(this.page.locator(this.locators.cartItemName)).toHaveCount(0); + } + + async verifyEmptyCartMessage() { + await expect(this.page.locator(this.locators.cartEmpty)).toBeVisible(); + } + + async clickOnStartShoppingButton() { + await this.page.locator(this.locators.startShoppingButton).click(); + } + } export default CartPage; \ No newline at end of file diff --git a/pages/ContactUsPage.js b/pages/ContactUsPage.js new file mode 100644 index 0000000..5053db5 --- /dev/null +++ b/pages/ContactUsPage.js @@ -0,0 +1,40 @@ +import BasePage from './BasePage.js'; +import { expect } from '@playwright/test'; + +class ContactUsPage extends BasePage{ + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.page = page; + } + + locators = { + contactUsBtn: `[data-testid="header-menu-contact-us"]`, + contactUsTitle: `[data-testid="contact-us-heading"]`, + firstNameInput: `[data-testid="contact-us-first-name-input"]`, + lastNameInput: `[data-testid="contact-us-last-name-input"]`, + subjectInput: `[data-testid="contact-us-subject-input"]`, + messageInput: `[data-testid="contact-us-message-input"]`, + sendMessageBtn: `//button[@data-testid="contact-us-submit-button"]`, + successMessage: `[data-testid="contact-us-success-message"]` + } + + async assertContactUsTitle() { + await expect(this.page.locator(this.locators.contactUsTitle)).toHaveText('Contact Us'); + } + + async fillContactUsForm() { + await this.page.fill(this.locators.firstNameInput, 'John'); + await this.page.fill(this.locators.lastNameInput, 'Doe'); + await this.page.fill(this.locators.subjectInput, 'Test Subject'); + await this.page.fill(this.locators.messageInput, 'This is a test message.'); + await this.page.click(this.locators.sendMessageBtn); + } + async verifySuccessContactUsFormSubmission() { + await expect(this.page.locator(this.locators.successMessage)).toBeVisible(); + } +} +export default ContactUsPage; \ No newline at end of file diff --git a/pages/HomePage.js b/pages/HomePage.js index 72c55b3..593898a 100644 --- a/pages/HomePage.js +++ b/pages/HomePage.js @@ -23,7 +23,8 @@ class HomePage extends BasePage{ AddCartNotification: `div[role="status"][aria-live="polite"]:has-text("Added to the cart")`, priceRangeSlider2 : `[data-testid="all-products-price-range-input-1"]`, priceRangeSlider1 : `[data-testid="all-products-price-range-input-0"]`, - filterButton : `[data-testid="all-products-filter-toggle"]` + filterButton : `[data-testid="all-products-filter-toggle"]`, + aboutUsTitle: `[data-testid="about-us-title"]`, } } @@ -88,6 +89,25 @@ class HomePage extends BasePage{ return this.page.locator(this.locators.navbar.showNowButton); } + async clickOnContactUsLink() { + await this.getContactUsNav().click(); + } + + async clickBackToHomeButton() { + await this.getHomeNav().click(); + } + + async assertHomePage() { + await expect(this.page.locator(this.locators.navbar.homeNav)).toBeVisible({ timeout: 10000 }); + } + + async clickAboutUsNav() { + await this.getAboutUsNav().click(); + } + + async assertAboutUsTitle() { + await expect(this.page.locator(this.locators.navbar.aboutUsTitle)).toBeVisible({ timeout: 10000 }); + } } export default HomePage; \ No newline at end of file diff --git a/pages/LoginPage.js b/pages/LoginPage.js index 22e010d..b27d956 100644 --- a/pages/LoginPage.js +++ b/pages/LoginPage.js @@ -2,7 +2,7 @@ import BasePage from './BasePage.js'; import { expect } from '@playwright/test'; class LoginPage extends BasePage{ - + /** * @param {import('@playwright/test').Page} page */ @@ -23,6 +23,7 @@ class LoginPage extends BasePage{ successSignInMessage: `Logged in successfully`, } + async navigateToLoginPage() { await this.navigateTo('/'); } @@ -49,6 +50,11 @@ class LoginPage extends BasePage{ async clickOnUserProfileIcon() { await this.page.locator(this.locators.userIcon).click(); + // Wait for dropdown menu to appear + // await this.page.waitForSelector(this.locators.logoutButton, { + // state: 'visible', + // timeout: 5000 + // }); } async assertLoginPage() { @@ -66,7 +72,9 @@ class LoginPage extends BasePage{ } async clickOnLogoutButton() { - await this.page.locator(this.locators.logoutButton).click(); + const logoutBtn = this.page.locator(this.locators.logoutButton); + await logoutBtn.waitFor({ state: 'visible', timeout: 10000 }); + await logoutBtn.click(); } async validateSignInPage() { await expect(this.getLoginPageTitle()).toBeVisible(); diff --git a/pages/ProductDetailsPage.js b/pages/ProductDetailsPage.js index 1256cbf..748b3ef 100644 --- a/pages/ProductDetailsPage.js +++ b/pages/ProductDetailsPage.js @@ -17,7 +17,16 @@ class ProductDetailsPage extends BasePage{ addToCartButton: 'ADD TO CART', headerCartIcon: 'header-cart-icon', productAdditionalInfoTab : `[data-testid="additional-info-tab"]`, - productReviewsTab : `[data-testid="reviews-tab"]` + productReviewsTab : `[data-testid="reviews-tab"]`, + writeReviewBtn: `//button[@data-testid="write-review-button"]`, + yourNameInput: `[data-testid="review-form-name-input"]`, + emailInput: `[data-testid="review-form-email-input"]`, + ratingStars: `[data-testid="review-form-rating-4"]`, + reviewTitleInput: `[data-testid="review-form-title-input"]`, + giveYourOpinionInput: `[data-testid="review-form-review-input"]`, + submitBtn: `[data-testid="review-form-submit-button"]`, + editReviewBtn: `[data-testid="edit-review-button"]`, + deleteReviewBtn: `[data-testid="delete-review-button"]` } @@ -85,6 +94,47 @@ class ProductDetailsPage extends BasePage{ async clickCartIcon() { await this.getCartIcon().click(); } + + async clickOnWriteAReviewBtn() { + await this.page.locator(this.locators.writeReviewBtn).click(); + } + + async fillReviewForm() { + await this.page.fill(this.locators.yourNameInput, 'John Doe'); + await this.page.fill(this.locators.emailInput, 'testing@gmail.com'); + await this.page.click(this.locators.ratingStars); + await this.page.fill(this.locators.reviewTitleInput, 'Great Product'); + await this.page.fill(this.locators.giveYourOpinionInput, 'This product exceeded my expectations. Highly recommend!'); + await this.page.click(this.locators.submitBtn); + } + async assertSubmittedReview({ name, title, opinion }) { + await this.page.waitForTimeout(3000); // Wait for 1 second to ensure the form is ready + await expect(this.page.locator(`text=${name}`)).toBeVisible(); + await expect(this.page.locator(`text=${title}`)).toBeVisible(); + await expect(this.page.locator(`text=${opinion}`)).toBeVisible(); + } + + async clickOnEditReviewBtn() { + await this.page.locator(this.locators.editReviewBtn).click(); + } + + async updateReviewForm() { + await this.page.waitForTimeout(3000); // Wait for 1 second to ensure the form is ready + await this.page.fill(this.locators.reviewTitleInput, 'Updated Review Title'); + await this.page.fill(this.locators.giveYourOpinionInput, 'This is an updated review opinion.'); + await this.page.click(this.locators.submitBtn); + } + + async assertUpdatedReview({ title, opinion }) { + await this.page.waitForTimeout(3000); // Wait for 1 second to ensure the form is ready + await expect(this.page.locator(`text=${title}`)).toBeVisible(); + await expect(this.page.locator(`text=${opinion}`)).toBeVisible(); + } + + async clickOnDeleteReviewBtn() { + await this.page.locator(this.locators.deleteReviewBtn).click(); + await this.page.keyboard.press('Enter'); + } } export default ProductDetailsPage; \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index f6c3911..82cf3e4 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,39 +1,71 @@ // @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 : 0, - workers: isCI ? 1 : 1, + retries: isCI ? 1 : 1, // Enable retries for flaky test behavior + workers: isCI ? 5 : 5, - timeout: 60 * 1000, // ⏱️ each test fails after 1 min - // In CI we only show a list reporter. The workflow sets --reporter=blob. - // Locally you also get HTML and JSON. + timeout: 60 * 1000, + expect: { + timeout: 10 * 1000, + }, + reporter: [ - ['html', { - outputFolder: 'playwright-report', - open: 'never' - }], - ['blob', { outputDir: 'blob-report' }], // Use blob reporter + ['blob'], // ✅ REQUIRED for sharding + ['html', { outputDir: './playwright-report' }], ['json', { outputFile: './playwright-report/report.json' }], ], use: { - baseURL: 'https://demo.alphabin.co/', + baseURL: 'https://storedemo.testdino.com/products', headless: true, - trace: 'on-first-retry', + trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', + actionTimeout: 15 * 1000, + navigationTimeout: 30 * 1000, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + grep: /@chromium/, // only run tests tagged @chromium + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + grep: /@firefox/, // only run tests tagged @firefox + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + grep: /@webkit/, // only run tests tagged @webkit + }, + { + name: 'android', + use: { ...devices['Pixel 5'] }, + grep: /@android/, // only run tests tagged @android + }, + { + name: 'ios', + use: { ...devices['iPhone 14'] }, + grep: /@ios/, // only run tests tagged @ios + }, + { + name: 'api', + use: { ...devices['API'] }, + grep: /@api/, // only run tests tagged @api }, ], -}); \ No newline at end of file +}); diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js new file mode 100644 index 0000000..7b86928 --- /dev/null +++ b/tests/cart_checkout.spec.js @@ -0,0 +1,700 @@ +// @ts-check +import { expect, test } from '@playwright/test'; +import AllPages from '../pages/AllPages.js'; + +let allPages; + +test.beforeEach(async ({ page }) => { + allPages = new AllPages(page); + await page.goto('/'); +}); + +/* ---------- 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('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, + }), + }); + } + ); + + 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, + }), + }); + } + ); + + 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, + }), + }); + } + ); +}); + + +/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ + +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, + }), + }); + } +); + +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( + '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( + '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 new file mode 100644 index 0000000..0278cbd --- /dev/null +++ b/tests/delete-api.spec.js @@ -0,0 +1,137 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; + +test.describe('DELETE User API', () => { + + 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}`); + + expect(response.status()).toBe(200); + 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('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 + const response1 = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + expect(response1.status()).toBe(200); + const body1 = await response1.json(); + expect(body1).toHaveProperty('id', userId); + + const response2 = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + 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('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}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Validate response body structure + 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 a921143..f0efea0 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -1,464 +1,777 @@ -// @ts-check -import { expect, test } from '@playwright/test'; -import AllPages from '../pages/AllPages.js'; -import dotenv from 'dotenv'; -dotenv.config({ override: true }); - -let allPages; - -test.beforeEach(async ({ page }) => { - allPages = new AllPages(page); - await page.goto('/'); -}); - -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); -} - -async function login1(username = process.env.USERNAME1, password = process.env.PASSWORD) { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(username, password); -} - -async function logout() { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.clickOnLogoutButton(); -} - -test('Verify that user can login and logout successfully', async () => { - await login(); - await logout(); -}); - -test('Verify that user can update personal information', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.updatePersonalInfo(); - await allPages.userPage.verifyPersonalInfoUpdated(); -}); - -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', 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('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('Verify that user is able to delete address successfully', async () => { - await allPages.userPage.clickOnDeleteAddressButton(); - }); -}); - -test('Verify that user can change password successfully', async () => { - await test.step('Login with existing password', async () => { - await login1(); - }); - - 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(); - }); - 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 the New User is able to add Addresses in the Address section', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.checkAddNewAddressMenu(); - await allPages.userPage.fillAddressForm(); -}); - -test('Verify that User Can Complete the Journey from Login to Order Placement', 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.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(); -}); - -test('Verify user can place and cancel an order', 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', async () => { - // fresh test data - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName; - 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 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(); - await allPages.allProductsPage.assertAllProductsTitle(); - - productName = await allPages.allProductsPage.getNthProductName(1); - productPrice = await allPages.allProductsPage.getNthProductPrice(1); - productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); - - await allPages.allProductsPage.clickNthProductWishlistIcon(1); - await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); - await allPages.allProductsPage.clickNthProduct(1); - - await allPages.productDetailsPage.assertProductNameTitle(productName); - await allPages.productDetailsPage.assertProductPrice(productName, productPrice); - await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); - await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); - }) - - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickAddToCartButton(); - - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); - await allPages.cartPage.clickIncreaseQuantityButton(); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); - - const cleanPrice = productPrice.replace(/[₹,]/g, ''); - const priceValue = parseFloat(cleanPrice) * 2; - await expect(allPages.cartPage.getTotalValue()).toContainText( - priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') - ); - await allPages.cartPage.clickOnCheckoutButton(); - - // Fill shipping address and save - 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(); - - // COD, verify summary, place order - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.assertOrderSummaryTitle(); - await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); - await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); - await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); - - const subtotalValue = parseFloat(cleanPrice) * 2; - const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); - await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); - await allPages.checkoutPage.clickOnPlaceOrder(); - - // Order details and return to home - await allPages.orderDetailsPage.assertOrderDetailsTitle(); - await allPages.orderDetailsPage.assertOrderPlacedName(); - await allPages.orderDetailsPage.assertOrderPlacedMessage(); - await allPages.orderDetailsPage.assertOrderPlacedDate(); - await allPages.orderDetailsPage.assertOrderInformationTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedMessage(); - await allPages.orderDetailsPage.assertShippingDetailsTitle(); - await allPages.orderDetailsPage.assertShippingEmailValue(email); - await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); - await allPages.orderDetailsPage.assertDeliveryAddressLabel(); - await allPages.orderDetailsPage.assertDeliveryAddressValue(); - await allPages.orderDetailsPage.assertContinueShoppingButton(); - - await allPages.orderDetailsPage.assertOrderSummaryTitle(); - await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); - await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); - await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); - await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); - await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); - await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); - await allPages.orderDetailsPage.clickBackToHomeButton(); - }); -}); - -test('Verify that user add product to cart before logging in and then complete order after logging in', async () => { - await test.step('Navigate and add product to cart before logging in', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.homePage.clickProductImage(); - await allPages.homePage.clickAddToCartButton(); - 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(); -}) -}); - -test('Verify that user can filter products by price range', async () => { - await login(); - await allPages.homePage.clickOnShopNowButton(); - await allPages.homePage.clickOnFilterButton(); - await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); - await allPages.homePage.clickOnFilterButton(); -}); - -test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { - await login(); +// // @ts-check +// import { expect, test } from '@playwright/test'; +// import AllPages from '../pages/AllPages.js'; +// import dotenv from 'dotenv'; +// dotenv.config({ override: true }); + +// let allPages; + +// test.beforeEach(async ({ page }) => { +// allPages = new AllPages(page); +// await page.goto('/'); +// }); + +// 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); +// } + +// async function login1(username = process.env.USERNAME1, password = process.env.PASSWORD) { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.login(username, password); +// } + +// async function logout() { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.clickOnLogoutButton(); +// } + +// 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 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 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('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!' +// }); +// }) + +// 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(); +// }); + +// 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', { +// 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 allPages.inventoryPage.clickOnShopNowButton(); +// await allPages.inventoryPage.clickOnAllProductsLink(); +// await allPages.inventoryPage.searchProduct(productName); +// await allPages.inventoryPage.verifyProductTitleVisible(productName); +// await allPages.inventoryPage.clickOnAddToCartIcon(); + + +// // await allPages.cartPage.verifyCartItemVisible(productName); +// // 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 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'; +// const lastName = 'User'; + +// let productName; +// 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 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(); +// await allPages.allProductsPage.assertAllProductsTitle(); + +// productName = await allPages.allProductsPage.getNthProductName(1); +// productPrice = await allPages.allProductsPage.getNthProductPrice(1); +// productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); + +// await allPages.allProductsPage.clickNthProductWishlistIcon(1); +// await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); +// await allPages.allProductsPage.clickNthProduct(1); + +// await allPages.productDetailsPage.assertProductNameTitle(productName); +// await allPages.productDetailsPage.assertProductPrice(productName, productPrice); +// await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); +// await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); +// }) + +// await test.step('Add product to cart, add new address and checkout', async () => { +// await allPages.productDetailsPage.clickAddToCartButton(); + +// // await allPages.productDetailsPage.clickCartIcon(); +// // await allPages.cartPage.assertYourCartTitle(); +// // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); +// // await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); +// // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); +// // await allPages.cartPage.clickIncreaseQuantityButton(); +// // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); + +// // const cleanPrice = productPrice.replace(/[₹,]/g, ''); +// // const priceValue = parseFloat(cleanPrice) * 2; +// // await expect(allPages.cartPage.getTotalValue()).toContainText( +// // priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') +// // ); +// // await allPages.cartPage.clickOnCheckoutButton(); + +// // // Fill shipping address and save +// // 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(); + +// // // COD, verify summary, place order +// // await allPages.checkoutPage.selectCashOnDelivery(); +// // await allPages.checkoutPage.verifyCheckoutTitle(); +// // await allPages.checkoutPage.assertOrderSummaryTitle(); +// // await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); +// // await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); +// // await allPages.checkoutPage.verifyProductInCheckout(productName); +// // await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); +// // await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); + +// // const subtotalValue = parseFloat(cleanPrice) * 2; +// // const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +// // await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); +// // await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); +// // await allPages.checkoutPage.clickOnPlaceOrder(); + +// // // Order details and return to home +// // await allPages.orderDetailsPage.assertOrderDetailsTitle(); +// // await allPages.orderDetailsPage.assertOrderPlacedName(); +// // await allPages.orderDetailsPage.assertOrderPlacedMessage(); +// // await allPages.orderDetailsPage.assertOrderPlacedDate(); +// // await allPages.orderDetailsPage.assertOrderInformationTitle(); +// // await allPages.orderDetailsPage.assertOrderConfirmedTitle(); +// // await allPages.orderDetailsPage.assertOrderConfirmedMessage(); +// // await allPages.orderDetailsPage.assertShippingDetailsTitle(); +// // await allPages.orderDetailsPage.assertShippingEmailValue(email); +// // await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); +// // await allPages.orderDetailsPage.assertDeliveryAddressLabel(); +// // await allPages.orderDetailsPage.assertDeliveryAddressValue(); +// // await allPages.orderDetailsPage.assertContinueShoppingButton(); + +// // await allPages.orderDetailsPage.assertOrderSummaryTitle(); +// // await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); +// // await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); +// // await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); +// // await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); +// // await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); +// // 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 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(); +// await allPages.homePage.clickAddToCartButton(); +// 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(); +// // }) +// 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: '@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 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(); - await allPages.inventoryPage.addToWishlist(); - await allPages.inventoryPage.assertWishlistIcon(); - await allPages.inventoryPage.clickOnWishlistIconHeader(); - await allPages.inventoryPage.assertWishlistPage(); - await allPages.inventoryPage.clickOnWishlistAddToCard(); - }) +// await test.step('Add product to wishlistand then add to cart', async () => { +// await allPages.homePage.clickOnShopNowButton(); +// await allPages.inventoryPage.addToWishlist(); +// await allPages.inventoryPage.assertWishlistIcon(); +// await allPages.inventoryPage.clickOnWishlistIconHeader(); +// await allPages.inventoryPage.assertWishlistPage(); +// await allPages.inventoryPage.clickOnWishlistAddToCard(); +// }) - await test.step('Checkout product added to cart', async () => { - 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(); - }) - -}); - -test('Verify new user views and cancels an order in my orders', 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(); - }) - - 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 Products and add view details of a random product', async () => { - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - productName = await allPages.allProductsPage.getNthProductName(1); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - }) - - 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('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', 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 () => { - // 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(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - await allPages.productDetailsPage.clickOnAdditionalInfoTab(); - await allPages.productDetailsPage.assertAdditionalInfoTab(); - }) - - await test.step('Add product to cart, change quantity, add new address and checkout', 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.verifyCashOnDeliverySelected(); - 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.verifyOrderConfirmedTitle(); - await allPages.checkoutPage.clickOnContinueShoppingButton(); - }) - - await test.step('Add another product to cart, select existing address and checkout', 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.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) -}); +// await test.step('Checkout product added to cart', async () => { +// 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.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'; + +// 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(); +// // }) + +// // 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 Products and add view details of a random product', async () => { +// await allPages.homePage.clickAllProductsNav(); +// await allPages.allProductsPage.assertAllProductsTitle(); +// 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(); +// // 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('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('Navigate to All Products and add view details of a random product', 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(); +// }) + +// await test.step('Add product to cart, change quantity, add new address and checkout', 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.verifyCashOnDeliverySelected(); +// // 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.verifyOrderConfirmedTitle(); +// // await allPages.checkoutPage.clickOnContinueShoppingButton(); +// }) + +// await test.step('Add another product to cart, select existing address and checkout', 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.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 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 }); +// // }) +// 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 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(); +// 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 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(); +// }) + +// 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 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 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(); +// 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); +// 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 new file mode 100644 index 0000000..89e0144 --- /dev/null +++ b/tests/get-users.spec.js @@ -0,0 +1,524 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; + +test.describe('GET Users API', () => { + + 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', + 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); + const body = await response.json(); + 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', + 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', + 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); + const body = await response.json(); + 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', + 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); + const body = await response.json(); + 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', + 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', + 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); + const body = await response.json(); + 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', + 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}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + 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', + 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; + + // Get first page + const response1 = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}&skip=0`); + const body1 = await response1.json(); + const users1 = body1.users || body1; + const firstUser1 = Array.isArray(users1) ? users1[0] : null; + + // Get second page with skip + const response2 = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}&skip=${skip}`); + const body2 = await response2.json(); + const users2 = body2.users || body2; + const firstUser2 = Array.isArray(users2) ? users2[0] : null; + + expect(response1.status()).toBe(200); + expect(response2.status()).toBe(200); + + // If both have users, they should be different (unless skip doesn't work) + 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', + 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}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + + if (usersArray.length > 0) { + // 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.skip('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); + } + + const delay = 3; + const startTime = Date.now(); + + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?delay=${delay}`, { + timeout: 10000 // 10 second timeout + }); + + const endTime = Date.now(); + const duration = (endTime - startTime) / 1000; + + expect(response.status()).toBe(200); + expect(duration).toBeGreaterThanOrEqual(delay - 0.5); + + 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', + 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; + + try { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?delay=${delay}`, { + timeout: shortTimeout + }); + + + expect(response.status()).toBe(200); + } 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 new file mode 100644 index 0000000..a0518b3 --- /dev/null +++ b/tests/post-api.spec.js @@ -0,0 +1,308 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; +const ADD_ENDPOINT = '/users/add'; + +test.describe('POST Create User API', () => { + + 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' + }; + + const response = await request.post(`${API_BASE_URL}${USERS_ENDPOINT}/invalid-endpoint`, { + data: userData + }); + + 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', + 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: { + 'Content-Type': 'application/json' + } + }); + + // 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', + 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', + 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', + 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' + }; + + // Try PUT on an endpoint that might not support it properly + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}/invalid`, { + data: updateData + }); + + // 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', + 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); + const body = await response.json(); + + // Validate expected keys in user schema + const expectedKeys = ['id', 'firstName', 'lastName']; + 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', + 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); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + + if (usersArray.length > 0) { + // Check first user has id and email + const firstUser = usersArray[0]; + expect(firstUser).toHaveProperty('id'); + if (firstUser.email !== undefined) { + 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 new file mode 100644 index 0000000..52ace44 --- /dev/null +++ b/tests/updateUser.spec.js @@ -0,0 +1,343 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; +const AUTH_ENDPOINT = '/auth/login'; + +test.describe('PUT / PATCH Update User API', () => { + + 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', + lastName: 'Doe', + age: 30 + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + 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', + 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: {} + }); + + expect(response.status()).toBe(200); + 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', + 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' + }; + + // Use PATCH for partial update + const response = await request.patch(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + 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', + 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', + lastName: 'Smith' + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('firstName'); + expect(body).toHaveProperty('lastName'); + expect(typeof body.firstName).toBe('string'); + 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', + 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', + lastName: 'User' + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Check for updatedAt or similar timestamp field + const hasTimestamp = body.hasOwnProperty('updatedAt') || + body.hasOwnProperty('updated') || + body.hasOwnProperty('modifiedAt'); + + // 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', + 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' + }; + + const response = await request.post(`${API_BASE_URL}${AUTH_ENDPOINT}`, { + data: loginData + }); + + // Should return error status (400 or 401) + 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', + 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' + }; + + const response = await request.post(`${API_BASE_URL}${AUTH_ENDPOINT}`, { + data: loginData + }); + + 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 new file mode 100644 index 0000000..76f2783 --- /dev/null +++ b/tests/visual.spec.js @@ -0,0 +1,45 @@ +import { expect, test } from '@playwright/test'; +import AllPages from '../pages/AllPages.js'; + +let allPages; + +test.beforeEach(async ({ page }) => { + allPages = new AllPages(page); + await page.goto('https://github.com/login'); +}); + +test.describe('Visual Comparison', () => { + + test.describe('GitHub Login Page', () => { + test.skip('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, + }), + }); + }); + }); +});