From 0e64be6b26926583e91a33e2efae6632e0cf9c28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:28:09 +0000 Subject: [PATCH 1/3] Initial plan From cb409e357d7436c63653066c2433fde48d246e76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:52:03 +0000 Subject: [PATCH 2/3] feat: add petstore SPA UI, Playwright tests, and fix numeric path params Agent-Logs-Url: https://github.com/counterfact/example-petstore/sessions/15794b61-698a-4ef6-8833-bd28f7a06353 Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com> --- .gitignore | 2 + README.md | 46 + api/routes/pet/{petId}.ts | 6 +- api/routes/pet/{petId}/uploadImage.ts | 2 +- api/routes/store/order/{orderId}.ts | 4 +- package-lock.json | 896 +++++++++++++++- package.json | 6 +- playwright.config.ts | 39 + test/petstore.ui.test.ts | 378 +++++++ ui/index.html | 1358 +++++++++++++++++++++++++ 10 files changed, 2728 insertions(+), 9 deletions(-) create mode 100644 playwright.config.ts create mode 100644 test/petstore.ui.test.ts create mode 100644 ui/index.html diff --git a/.gitignore b/.gitignore index 3c3629e..169a2af 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +playwright-report +test-results diff --git a/README.md b/README.md index 62e4be7..48a5419 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,46 @@ Once running, Counterfact provides: - An interactive REPL for inspecting and manipulating server state at runtime - A Swagger UI for exploring and testing the API +### Pet Store UI + +A single-page app is included in the `ui/` directory. After starting the mock server, open `ui/index.html` in a browser, or serve it with any static file server: + +```bash +npx serve ui +``` + +Then visit `http://localhost:3000` (or whatever port `serve` uses). The UI communicates with the Counterfact backend at `http://localhost:3100` by default. To point it at a different API server, pass the `api` query parameter: + +``` +http://localhost:3000/?api=http://localhost:3100 +``` + +The UI covers all petstore APIs: + +- **Pets tab** – browse pets by status, search by tags, add, edit, and delete pets +- **Store tab** – view live inventory, place orders, look up and delete orders +- **Users tab** – list, add, edit, look up, and delete users + +## Testing + +### Unit tests + +```bash +npm test +``` + +Runs the context unit tests with Node's built-in test runner. + +### UI tests (Playwright) + +```bash +npm run test:ui +``` + +Starts the Counterfact API server on port 3101 and a static file server on port 8080, then runs Playwright browser tests against the full UI + backend stack. + +Playwright reports are written to `playwright-report/`. Test failure artefacts (screenshots, videos) go to `test-results/`. + ## Project Structure ``` @@ -41,6 +81,12 @@ Once running, Counterfact provides: │ └── types/ # Generated TypeScript types from the OpenAPI spec ├── spec/ │ └── petstore.yaml # OpenAPI specification for the Petstore API +├── test/ +│ ├── context.test.ts # Unit tests for the Context class +│ └── petstore.ui.test.ts # Playwright end-to-end UI tests +├── ui/ +│ └── index.html # Single-page application +├── playwright.config.ts # Playwright configuration └── package.json ``` diff --git a/api/routes/pet/{petId}.ts b/api/routes/pet/{petId}.ts index 1fc4187..4a73277 100644 --- a/api/routes/pet/{petId}.ts +++ b/api/routes/pet/{petId}.ts @@ -3,7 +3,7 @@ import type { updatePetWithForm } from "../../types/paths/pet/{petId}.types.js"; import type { deletePet } from "../../types/paths/pet/{petId}.types.js"; export const GET: getPetById = ($) => { - const pet = $.context.getPetById($.path.petId); + const pet = $.context.getPetById(Number($.path.petId)); if (!pet) { return $.response[404]; } @@ -11,7 +11,7 @@ export const GET: getPetById = ($) => { }; export const POST: updatePetWithForm = ($) => { - const pet = $.context.getPetById($.path.petId); + const pet = $.context.getPetById(Number($.path.petId)); if (!pet) { return $.response[400]; } @@ -29,7 +29,7 @@ export const POST: updatePetWithForm = ($) => { }; export const DELETE: deletePet = ($) => { - const deleted = $.context.deletePet($.path.petId); + const deleted = $.context.deletePet(Number($.path.petId)); if (!deleted) { return $.response[400]; } diff --git a/api/routes/pet/{petId}/uploadImage.ts b/api/routes/pet/{petId}/uploadImage.ts index fd5de1c..13c7a05 100644 --- a/api/routes/pet/{petId}/uploadImage.ts +++ b/api/routes/pet/{petId}/uploadImage.ts @@ -1,7 +1,7 @@ import type { uploadFile } from "../../../types/paths/pet/{petId}/uploadImage.types.js"; export const POST: uploadFile = ($) => { - const pet = $.context.getPetById($.path.petId); + const pet = $.context.getPetById(Number($.path.petId)); if (!pet) { return $.response[404]; } diff --git a/api/routes/store/order/{orderId}.ts b/api/routes/store/order/{orderId}.ts index 660f051..5c4b1d1 100644 --- a/api/routes/store/order/{orderId}.ts +++ b/api/routes/store/order/{orderId}.ts @@ -2,7 +2,7 @@ import type { getOrderById } from "../../../types/paths/store/order/{orderId}.ty import type { deleteOrder } from "../../../types/paths/store/order/{orderId}.types.js"; export const GET: getOrderById = ($) => { - const order = $.context.getOrderById($.path.orderId); + const order = $.context.getOrderById(Number($.path.orderId)); if (!order) { return $.response[404]; } @@ -10,7 +10,7 @@ export const GET: getOrderById = ($) => { }; export const DELETE: deleteOrder = ($) => { - const deleted = $.context.deleteOrder($.path.orderId); + const deleted = $.context.deleteOrder(Number($.path.orderId)); if (!deleted) { return $.response[404]; } diff --git a/package-lock.json b/package-lock.json index 2d1d443..36eda9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,11 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", + "serve": "^14.2.6", "typescript-eslint": "^8.58.0" } }, @@ -741,6 +743,22 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@posthog/core": { "version": "1.25.0", "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.25.0.tgz", @@ -1229,6 +1247,13 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "license": "BSD-2-Clause" }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1302,6 +1327,74 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/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==", + "dev": true, + "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/ansi-align/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1317,6 +1410,34 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "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" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1353,6 +1474,42 @@ "node": "18 || 20 || >=22" } }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -1454,6 +1611,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1470,6 +1640,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -1500,6 +1686,66 @@ "node": ">=8" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/co-body": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", @@ -1586,6 +1832,72 @@ "node": ">=20" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -1718,6 +2030,16 @@ "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", "license": "MIT" }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1927,6 +2249,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1942,6 +2271,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2338,6 +2674,30 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2366,6 +2726,23 @@ "node": ">=10.0" } }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -2585,6 +2962,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.13.7", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", @@ -2811,6 +3201,16 @@ "node": ">=14" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2858,8 +3258,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/is-arrayish": { - "version": "0.2.1", + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" @@ -2904,6 +3311,16 @@ "node": ">=0.10.0" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2956,6 +3373,32 @@ "node": ">=0.12.0" } }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -3395,6 +3838,13 @@ "node": ">= 0.8" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3433,6 +3883,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -3590,6 +4050,19 @@ "semver": "bin/semver" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -3623,6 +4096,32 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", @@ -3855,6 +4354,13 @@ "node": ">=8" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3942,6 +4448,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -4163,6 +4716,16 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -4178,6 +4741,22 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -4257,6 +4836,40 @@ "node": ">= 4" } }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -4371,6 +4984,166 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "license": "MIT" }, + "node_modules/serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-handler/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-handler/node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "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/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/serve/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==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -4487,6 +5260,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -4555,6 +5335,60 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4820,6 +5654,17 @@ "node": ">= 0.8" } }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4881,6 +5726,22 @@ "node": ">= 8" } }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4897,6 +5758,37 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", diff --git a/package.json b/package.json index 3133b56..e0dfece 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "main": "index.js", "type": "module", "scripts": { - "test": "node --test test/**/*.test.ts", + "test": "node --test --import tsx/esm test/context.test.ts", + "test:ui": "playwright test", "start": "counterfact --spec ./spec/petstore.yaml api", + "start:test": "counterfact --spec ./spec/petstore.yaml api --port 3101", "lint": "eslint .", "format": "prettier --write ." }, @@ -17,9 +19,11 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", + "serve": "^14.2.6", "typescript-eslint": "^8.58.0" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2514d4e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,39 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./test", + testMatch: "**/*.ui.test.ts", + timeout: 30_000, + retries: 1, + reporter: [["html", { open: "never" }], ["list"]], + use: { + baseURL: "http://localhost:8080", + headless: true, + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: [ + { + command: "npm run start:test", + url: "http://localhost:3101/store/inventory", + reuseExistingServer: !process.env.CI, + timeout: 20_000, + stdout: "ignore", + stderr: "pipe", + }, + { + command: "npx serve ui --listen 8080 --no-clipboard", + url: "http://localhost:8080", + reuseExistingServer: !process.env.CI, + timeout: 10_000, + stdout: "ignore", + stderr: "pipe", + }, + ], +}); diff --git a/test/petstore.ui.test.ts b/test/petstore.ui.test.ts new file mode 100644 index 0000000..5f8446b --- /dev/null +++ b/test/petstore.ui.test.ts @@ -0,0 +1,378 @@ +import { test, expect, type Page } from "@playwright/test"; + +const API_URL = "http://localhost:3101"; +const UI_BASE = `/?api=${encodeURIComponent(API_URL)}`; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +async function navigateTo(page: Page, tab: "pets" | "store" | "users") { + await page.getByRole("button", { name: new RegExp(tab, "i") }).click(); + await page.waitForLoadState("networkidle"); +} + +// ─── Pets ───────────────────────────────────────────────────────────────────── + +test.describe("Pets tab", () => { + test.beforeEach(async ({ page }) => { + await page.goto(UI_BASE); + await page.waitForLoadState("networkidle"); + }); + + test("displays the page title", async ({ page }) => { + await expect(page).toHaveTitle(/pet store/i); + }); + + test("shows the Pets tab as active by default", async ({ page }) => { + const petsBtn = page.getByRole("button", { name: /pets/i }); + await expect(petsBtn).toHaveClass(/active/); + }); + + test("loads available pets on page load", async ({ page }) => { + // The page auto-loads available pets on init + const petGrid = page.locator("#pets-list .pet-card"); + await expect(petGrid.first()).toBeVisible({ timeout: 10_000 }); + const count = await petGrid.count(); + expect(count).toBeGreaterThan(0); + }); + + test("filters pets by status — pending", async ({ page }) => { + await page.selectOption("#status-filter", "pending"); + await page.getByRole("button", { name: /^search$/i }).first().click(); + await page.waitForLoadState("networkidle"); + + const cards = page.locator("#pets-list .pet-card"); + await expect(cards.first()).toBeVisible({ timeout: 10_000 }); + // All visible cards should show the pending badge + const count = await cards.count(); + for (let i = 0; i < count; i++) { + await expect(cards.nth(i).locator(".status-badge")).toContainText("pending", { + ignoreCase: true, + }); + } + }); + + test("filters pets by status — sold", async ({ page }) => { + await page.selectOption("#status-filter", "sold"); + await page.getByRole("button", { name: /^search$/i }).first().click(); + await page.waitForLoadState("networkidle"); + + const cards = page.locator("#pets-list .pet-card"); + await expect(cards.first()).toBeVisible({ timeout: 10_000 }); + const count = await cards.count(); + for (let i = 0; i < count; i++) { + await expect(cards.nth(i).locator(".status-badge")).toContainText("sold", { + ignoreCase: true, + }); + } + }); + + test("searches pets by tag", async ({ page }) => { + await page.fill("#tag-search", "friendly"); + await page.getByRole("button", { name: /^search$/i }).nth(1).click(); + await page.waitForLoadState("networkidle"); + + const cards = page.locator("#pets-by-tags-list .pet-card"); + await expect(cards.first()).toBeVisible({ timeout: 10_000 }); + }); + + test("shows empty message when no pets match tag", async ({ page }) => { + await page.fill("#tag-search", "zzz-no-such-tag"); + await page.getByRole("button", { name: /^search$/i }).nth(1).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#pets-by-tags-list")).toContainText( + "No pets found", + { timeout: 10_000 }, + ); + }); + + test("adds a new pet and shows it in the list", async ({ page }) => { + const uniqueName = `TestPet-${Date.now()}`; + + await page.fill("#pet-name", uniqueName); + await page.fill("#pet-category", "TestCategory"); + await page.selectOption("#pet-status-add", "available"); + await page.fill("#pet-tags", "test-tag"); + await page.fill("#pet-photo-url", "https://example.com/test.jpg"); + + await page.getByRole("button", { name: /^add pet$/i }).click(); + await page.waitForLoadState("networkidle"); + + // success alert should appear + await expect(page.locator("#pets-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#pets-alert .alert-success")).toContainText(uniqueName); + + // pet should appear in the grid + const card = page.locator("#pets-list .pet-card").filter({ hasText: uniqueName }); + await expect(card).toBeVisible({ timeout: 10_000 }); + }); + + test("edits an existing pet", async ({ page }) => { + // First load available pets + await page.waitForSelector("#pets-list .pet-card", { timeout: 10_000 }); + + // Click Edit on the first pet card + await page.locator("#pets-list .pet-card").first().getByRole("button", { name: /edit/i }).click(); + + // Wait for modal to open + await expect(page.locator("#edit-pet-modal")).toHaveClass(/open/, { + timeout: 5_000, + }); + + // Change the name + const updatedName = `Updated-${Date.now()}`; + await page.fill("#edit-pet-name", updatedName); + await page.getByRole("button", { name: /^save$/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#pets-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#pets-alert .alert-success")).toContainText( + "updated", + ); + }); + + test("deletes a pet", async ({ page }) => { + // Add a pet specifically to delete + const uniqueName = `DeleteMe-${Date.now()}`; + await page.fill("#pet-name", uniqueName); + await page.selectOption("#pet-status-add", "available"); + await page.getByRole("button", { name: /^add pet$/i }).click(); + await page.waitForLoadState("networkidle"); + + // Find the card + const card = page.locator("#pets-list .pet-card").filter({ hasText: uniqueName }); + await expect(card).toBeVisible({ timeout: 10_000 }); + + // Accept the confirm dialog and click Delete + page.once("dialog", (d) => d.accept()); + await card.getByRole("button", { name: /delete/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#pets-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#pets-alert .alert-success")).toContainText("deleted"); + + // Verify the card is gone + await expect(card).toHaveCount(0, { timeout: 10_000 }); + }); +}); + +// ─── Store ──────────────────────────────────────────────────────────────────── + +test.describe("Store tab", () => { + test.beforeEach(async ({ page }) => { + await page.goto(UI_BASE); + await navigateTo(page, "store"); + }); + + test("displays inventory counts", async ({ page }) => { + const available = page.locator("#inv-available"); + const pending = page.locator("#inv-pending"); + const sold = page.locator("#inv-sold"); + + await expect(available).not.toHaveText("—", { timeout: 10_000 }); + await expect(pending).not.toHaveText("—"); + await expect(sold).not.toHaveText("—"); + }); + + test("places a new order", async ({ page }) => { + await page.fill("#order-pet-id", "1"); + await page.fill("#order-quantity", "2"); + + await page.getByRole("button", { name: /^place order$/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#store-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#store-alert .alert-success")).toContainText( + "placed", + ); + + // Order appears in the table + const rows = page.locator("#orders-tbody tr"); + await expect(rows.first()).toBeVisible({ timeout: 10_000 }); + }); + + test("deletes an order", async ({ page }) => { + // Place an order first + await page.fill("#order-pet-id", "1"); + await page.fill("#order-quantity", "1"); + await page.getByRole("button", { name: /^place order$/i }).click(); + await page.waitForLoadState("networkidle"); + + // Delete it + page.once("dialog", (d) => d.accept()); + await page.locator("#orders-tbody tr").first().getByRole("button", { name: /delete/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#store-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#store-alert .alert-success")).toContainText("deleted"); + }); + + test("looks up an order by ID", async ({ page }) => { + // Place an order to get a known ID + await page.fill("#order-pet-id", "2"); + await page.fill("#order-quantity", "1"); + await page.getByRole("button", { name: /^place order$/i }).click(); + await page.waitForLoadState("networkidle"); + + // Extract the order ID from the success alert + const alertText = await page.locator("#store-alert .alert-success").textContent(); + const match = alertText?.match(/#(\d+)/); + const orderId = match?.[1]; + expect(orderId).toBeTruthy(); + + // Look up by ID + await page.fill("#order-lookup-id", orderId!); + await page.getByRole("button", { name: /look up/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#order-lookup-result .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#order-lookup-result .alert-success")).toContainText( + `Order #${orderId}`, + ); + }); + + test("shows error when looking up a non-existent order", async ({ page }) => { + await page.fill("#order-lookup-id", "999999"); + await page.getByRole("button", { name: /look up/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#order-lookup-result .alert-error")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("refreshes inventory after placing an order", async ({ page }) => { + const beforeText = await page.locator("#inv-available").textContent(); + const before = parseInt(beforeText ?? "0"); + + await page.fill("#order-pet-id", "1"); + await page.fill("#order-quantity", "1"); + await page.getByRole("button", { name: /^place order$/i }).click(); + await page.waitForLoadState("networkidle"); + + // Inventory should have reloaded (available may change depending on server logic) + // At minimum verify it's still a number + const afterText = await page.locator("#inv-available").textContent(); + expect(Number.isNaN(parseInt(afterText ?? ""))).toBe(false); + // Just check before was a valid number too + expect(Number.isNaN(before)).toBe(false); + }); +}); + +// ─── Users ──────────────────────────────────────────────────────────────────── + +test.describe("Users tab", () => { + test.beforeEach(async ({ page }) => { + await page.goto(UI_BASE); + await navigateTo(page, "users"); + }); + + test("shows pre-seeded users", async ({ page }) => { + const rows = page.locator("#users-tbody tr"); + await expect(rows.first()).toBeVisible({ timeout: 10_000 }); + const count = await rows.count(); + expect(count).toBeGreaterThan(0); + }); + + test("adds a new user", async ({ page }) => { + const username = `testuser-${Date.now()}`; + + await page.fill("#user-username", username); + await page.fill("#user-first", "Test"); + await page.fill("#user-last", "User"); + await page.fill("#user-email", `${username}@example.com`); + await page.fill("#user-phone", "555-0000"); + await page.fill("#user-password", "password123"); + + await page.getByRole("button", { name: /^add user$/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#users-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#users-alert .alert-success")).toContainText(username); + + // User appears in the table + const row = page.locator("#users-tbody tr").filter({ hasText: username }); + await expect(row).toBeVisible({ timeout: 10_000 }); + }); + + test("looks up a user by username", async ({ page }) => { + await page.fill("#user-lookup-username", "user1"); + await page.getByRole("button", { name: /look up/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#user-lookup-result .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#user-lookup-result .alert-success")).toContainText("user1"); + }); + + test("shows error when looking up a non-existent user", async ({ page }) => { + await page.fill("#user-lookup-username", "no-such-user"); + await page.getByRole("button", { name: /look up/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#user-lookup-result .alert-error")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("edits a user", async ({ page }) => { + await page.waitForSelector("#users-tbody tr", { timeout: 10_000 }); + + // Click Edit on the first user row + await page.locator("#users-tbody tr").first().getByRole("button", { name: /edit/i }).click(); + + // Modal should open + await expect(page.locator("#edit-user-modal")).toHaveClass(/open/, { + timeout: 5_000, + }); + + // Change first name + const newFirst = `Edited-${Date.now()}`; + await page.fill("#edit-user-first", newFirst); + await page.getByRole("button", { name: /^save$/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#users-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#users-alert .alert-success")).toContainText("updated"); + }); + + test("deletes a user", async ({ page }) => { + // Create a user to delete + const username = `deleteme-${Date.now()}`; + await page.fill("#user-username", username); + await page.fill("#user-password", "pw"); + await page.getByRole("button", { name: /^add user$/i }).click(); + await page.waitForLoadState("networkidle"); + + const row = page.locator("#users-tbody tr").filter({ hasText: username }); + await expect(row).toBeVisible({ timeout: 10_000 }); + + // Delete it + page.once("dialog", (d) => d.accept()); + await row.getByRole("button", { name: /delete/i }).click(); + await page.waitForLoadState("networkidle"); + + await expect(page.locator("#users-alert .alert-success")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.locator("#users-alert .alert-success")).toContainText("deleted"); + await expect(row).toHaveCount(0, { timeout: 10_000 }); + }); +}); diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..4e2b8d3 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,1358 @@ + + + + + + 🐾 Pet Store + + + + +
+

🐾 Pet Store

+ +
+ +
+ +
+
+ + +
+
🔍 Find Pets by Status
+
+
+ + +
+ +
+
+
+ + +
+
🏷️ Find Pets by Tags
+
+
+ + +
+ +
+
+
+ + +
+
➕ Add New Pet
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+
📦 Inventory
+ +
+
+
+
+
Available
+
+
+
+
Pending
+
+
+
+
Sold
+
+
+
+ + +
+
🛒 Place Order
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
📋 Orders
+
+
+ + + + + + + + + + + + +
IDPet IDQtyShip DateStatus
+
+ + +
+
🔎 Look Up Order by ID
+
+
+ + +
+ +
+
+
+
+ + +
+
+ + +
+
+
👥 Users
+
+
+ + + + + + + + + + + + +
IDUsernameNameEmailPhone
+
+ + +
+
➕ Add User
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
🔎 Look Up User by Username
+
+
+ + +
+ +
+
+
+
+
+ + + + + + + + + + From 8c2edc6901aff40e15cf310ab8935998e868ba2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:55:06 +0000 Subject: [PATCH 3/3] refactor: extract TEST_API_URL to shared constants, fix statusBadge consistency Agent-Logs-Url: https://github.com/counterfact/example-petstore/sessions/15794b61-698a-4ef6-8833-bd28f7a06353 Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com> --- package.json | 2 +- playwright.config.ts | 5 +++-- test/constants.ts | 9 +++++++++ test/petstore.ui.test.ts | 4 ++-- ui/index.html | 8 +++++++- 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 test/constants.ts diff --git a/package.json b/package.json index e0dfece..3c82e09 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "node --test --import tsx/esm test/context.test.ts", "test:ui": "playwright test", "start": "counterfact --spec ./spec/petstore.yaml api", - "start:test": "counterfact --spec ./spec/petstore.yaml api --port 3101", + "start:test": "counterfact --spec ./spec/petstore.yaml api", "lint": "eslint .", "format": "prettier --write ." }, diff --git a/playwright.config.ts b/playwright.config.ts index 2514d4e..2c76387 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; +import { TEST_API_PORT, TEST_API_URL } from "./test/constants.js"; export default defineConfig({ testDir: "./test", @@ -20,8 +21,8 @@ export default defineConfig({ ], webServer: [ { - command: "npm run start:test", - url: "http://localhost:3101/store/inventory", + command: `npm run start:test -- --port ${TEST_API_PORT}`, + url: `${TEST_API_URL}/store/inventory`, reuseExistingServer: !process.env.CI, timeout: 20_000, stdout: "ignore", diff --git a/test/constants.ts b/test/constants.ts new file mode 100644 index 0000000..8b52b35 --- /dev/null +++ b/test/constants.ts @@ -0,0 +1,9 @@ +/** + * Shared test configuration constants. + * + * Import these in both playwright.config.ts and UI test files + * to ensure the API URL is defined in a single place. + */ + +export const TEST_API_PORT = parseInt(process.env.TEST_API_PORT ?? "3101", 10); +export const TEST_API_URL = `http://localhost:${TEST_API_PORT}`; diff --git a/test/petstore.ui.test.ts b/test/petstore.ui.test.ts index 5f8446b..4891705 100644 --- a/test/petstore.ui.test.ts +++ b/test/petstore.ui.test.ts @@ -1,7 +1,7 @@ import { test, expect, type Page } from "@playwright/test"; +import { TEST_API_URL } from "./constants.js"; -const API_URL = "http://localhost:3101"; -const UI_BASE = `/?api=${encodeURIComponent(API_URL)}`; +const UI_BASE = `/?api=${encodeURIComponent(TEST_API_URL)}`; // ─── Helpers ────────────────────────────────────────────────────────────────── diff --git a/ui/index.html b/ui/index.html index 4e2b8d3..0c68682 100644 --- a/ui/index.html +++ b/ui/index.html @@ -172,6 +172,11 @@ color: #c2185b; } + .status-unknown { + background: #eeeeee; + color: #757575; + } + .pet-card .actions { display: flex; gap: 6px; @@ -898,7 +903,8 @@

🐾 Pet Store

} function statusBadge(status) { - return `${status || "unknown"}`; + const s = status || "unknown"; + return `${s}`; } // ─── Navigation ──────────────────────────────────────────────────────────