From 7e3856288f92277146cffc274a0092f9892912c2 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 29 May 2025 18:59:14 +0300 Subject: [PATCH 1/4] feat: add clickOnElement tool --- .gitignore | 1 + README.md | 44 + package-lock.json | 1865 +++++++++++++++++++++++++-- package.json | 2 + src/tools/click-on-element.ts | 171 +++ src/tools/index.ts | 3 +- test/playground/index.html | 285 ++++ test/test-server.ts | 92 ++ test/tools/click-on-element.test.ts | 414 ++++++ 9 files changed, 2769 insertions(+), 108 deletions(-) create mode 100644 src/tools/click-on-element.ts create mode 100644 test/playground/index.html create mode 100644 test/test-server.ts create mode 100644 test/tools/click-on-element.test.ts diff --git a/.gitignore b/.gitignore index dd87e2d..63ab401 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules build +testplane-mcp-*.tgz diff --git a/README.md b/README.md index fee592f..43db5c9 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,47 @@ Navigate to URL in the browser. Close the current browser session. + +
+Element Interaction + +### `clickOnElement` +Click an element on the page using semantic queries (`testing-library`-style) or CSS selectors. + +- Semantic Queries: + - **Parameters:** + - `queryType` (string, optional): Semantic query type. One of: + - `"role"` - Find by ARIA role (e.g., "button", "link", "heading") + - `"text"` - Find by visible text content + - `"labelText"` - Find form inputs by their label text + - `"placeholderText"` - Find inputs by placeholder text + - `"altText"` - Find images by alt text + - `"testId"` - Find by data-testid attribute + - `"title"` - Find by title attribute + - `"displayValue"` - Find inputs by their current value + - `queryValue` (string, required when using queryType): The value to search for + - `queryOptions` (object, optional): Additional options: + - `name` (string): Accessible name for role queries + - `exact` (boolean): Whether to match exact text (default: true) + - `hidden` (boolean): Include hidden elements (default: false) + - `level` (number): Heading level for role="heading" (1-6) + +- CSS Selectors: + - **Parameters:** + - `selector` (string, optional): CSS selector or XPath when semantic queries cannot locate the element + +**Examples:** +```javascript +// Semantic queries (preferred) +{ queryType: "role", queryValue: "button", queryOptions: { name: "Submit" } } +{ queryType: "text", queryValue: "Click here" } +{ queryType: "labelText", queryValue: "Email Address" } + +// CSS selector fallback +{ selector: ".submit-btn" } +{ selector: "#unique-element" } +``` + +**Note:** Provide either semantic query parameters OR selector, not both. + +
diff --git a/package-lock.json b/package-lock.json index a84020c..dafe852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.2", + "@testing-library/webdriverio": "^3.2.1", "commander": "^13.1.0", "testplane": "latest", "zod": "^3.22.4" @@ -22,6 +23,7 @@ "@types/node": "^20.8.0", "eslint": "^9.26.0", "globals": "^16.1.0", + "http-server": "^14.1.1", "prettier": "^3.5.3", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", @@ -136,6 +138,14 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz", + "integrity": "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -1277,6 +1287,74 @@ "node": ">=14.16" } }, + "node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/@testing-library/webdriverio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/webdriverio/-/webdriverio-3.2.1.tgz", + "integrity": "sha512-mgMyCiwW+4zCidmlab9lwcO+UBz+PzlWnz9idDQ4ZS1SIHVSfJwvRLMWi+s3vNGFmc8duQxTiUHf1alW/Z48Og==", + "dependencies": { + "@babel/runtime": "^7.4.3", + "@testing-library/dom": "^8.17.1", + "simmerjs": "^0.5.6" + }, + "peerDependencies": { + "webdriverio": "*" + } + }, "node_modules/@testplane/devtools": { "version": "8.32.3", "resolved": "https://registry.npmjs.org/@testplane/devtools/-/devtools-8.32.3.tgz", @@ -1617,6 +1695,11 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" + }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -2088,6 +2171,77 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@wdio/config": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.14.0.tgz", + "integrity": "sha512-mW6VAXfUgd2j+8YJfFWvg8Ba/7g1Brr6/+MFBpp5rTQsw/2bN3PBJsQbWpNl99OCgoS8vgc5Ykps5ZUEeffSVQ==", + "peer": true, + "dependencies": { + "@wdio/logger": "9.4.4", + "@wdio/types": "9.14.0", + "@wdio/utils": "9.14.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config/node_modules/@wdio/logger": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.4.4.tgz", + "integrity": "sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==", + "peer": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@wdio/config/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/config/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@wdio/logger": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", @@ -2142,140 +2296,361 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@xstate/fsm": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", - "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==", - "license": "MIT" + "node_modules/@wdio/protocols": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.14.0.tgz", + "integrity": "sha512-inJR+G8iiFrk8/JPMfxpy6wA7rvMIZFV0T8vDN1Io7sGGj+EXX7ujpDxoCns53qxV4RytnSlgHRcCaASPFcecQ==", + "peer": true }, - "node_modules/@zip.js/zip.js": { - "version": "2.7.62", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.62.tgz", - "integrity": "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA==", - "license": "BSD-3-Clause", + "node_modules/@wdio/repl": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.4.4.tgz", + "integrity": "sha512-kchPRhoG/pCn4KhHGiL/ocNhdpR8OkD2e6sANlSUZ4TGBVi86YSIEjc2yXUwLacHknC/EnQk/SFnqd4MsNjGGg==", + "peer": true, + "dependencies": { + "@types/node": "^20.1.0" + }, "engines": { - "bun": ">=0.7.0", - "deno": ">=1.0.0", - "node": ">=16.5.0" + "node": ">=18.20.0" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", + "node_modules/@wdio/types": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.14.0.tgz", + "integrity": "sha512-Zqc4sxaQLIXdI1EHItIuVIOn7LvPmDvl9JEANwiJ35ck82Xlj+X55Gd9NtELSwChzKgODD0OBzlLgXyxTr69KA==", + "peer": true, "dependencies": { - "event-target-shim": "^5.0.0" + "@types/node": "^20.1.0" }, "engines": { - "node": ">=6.5" + "node": ">=18.20.0" } }, - "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", + "node_modules/@wdio/utils": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.14.0.tgz", + "integrity": "sha512-oJapwraSflOe0CmeF3TBocdt983hq9mCutLCfie4QmE+TKRlCsZz4iidG1NRAZPGdKB32nfHtyQlW0Dfxwn6RA==", + "peer": true, "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.14.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.1", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18.20.0" } }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "node_modules/@wdio/utils/node_modules/@wdio/logger": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.4.4.tgz", + "integrity": "sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==", + "peer": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "node": ">=18.20.0" } }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", + "node_modules/@wdio/utils/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "peer": true, "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "node": ">=12" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "license": "MIT", - "engines": { - "node": ">=6" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "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", + "node_modules/@wdio/utils/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "peer": true, "engines": { - "node": ">=8" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", + "node_modules/@wdio/utils/node_modules/edgedriver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.1.1.tgz", + "integrity": "sha512-/dM/PoBf22Xg3yypMWkmRQrBKEnSyNaZ7wHGCT9+qqT14izwtFT+QvdR89rjNkMfXwW+bSFoqOfbcvM+2Cyc7w==", + "hasInstallScript": true, + "peer": true, "dependencies": { - "color-convert": "^2.0.1" + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^4.5.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "which": "^5.0.0" }, - "engines": { - "node": ">=8" + "bin": { + "edgedriver": "bin/edgedriver.js" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", + "node_modules/@wdio/utils/node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "peer": true, "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "strnum": "^1.1.1" }, - "engines": { - "node": ">= 8" + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@wdio/utils/node_modules/geckodriver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-5.0.0.tgz", + "integrity": "sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==", + "hasInstallScript": true, + "peer": true, + "dependencies": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^5.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@wdio/utils/node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wdio/utils/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@wdio/utils/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@wdio/utils/node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "peer": true + }, + "node_modules/@wdio/utils/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "peer": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==", + "license": "MIT" + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.62", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.62.tgz", + "integrity": "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, "node_modules/archiver": { @@ -2329,6 +2704,21 @@ "node": ">= 0.4" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2356,6 +2746,20 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -2478,6 +2882,24 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -2574,6 +2996,12 @@ "node": ">=18" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "peer": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2692,6 +3120,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "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", @@ -2805,6 +3250,57 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "peer": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "peer": true, + "engines": { + "node": ">=18.17" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3096,6 +3592,15 @@ "node": ">= 0.10" } }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -3164,6 +3669,22 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-shorthand-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", @@ -3175,6 +3696,18 @@ "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==" }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3249,6 +3782,42 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3283,6 +3852,38 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "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/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -3339,6 +3940,66 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "peer": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3469,6 +4130,31 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3649,6 +4335,18 @@ } } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -3688,6 +4386,30 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -3995,6 +4717,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4362,6 +5090,40 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4463,6 +5225,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/geckodriver": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.5.0.tgz", @@ -4813,6 +5583,17 @@ "dev": true, "license": "MIT" }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4822,6 +5603,17 @@ "node": ">=8" } }, + "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==", + "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", @@ -4834,6 +5626,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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", @@ -4855,12 +5661,43 @@ "he": "bin/he" } }, - "node_modules/htmlfy": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.7.tgz", - "integrity": "sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==", - "license": "MIT" - }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlfy": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.7.tgz", + "integrity": "sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -4883,6 +5720,20 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -4896,6 +5747,33 @@ "node": ">= 14" } }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -5030,6 +5908,19 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -5052,12 +5943,57 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5070,6 +6006,47 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -5115,6 +6092,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5124,6 +6112,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -5142,6 +6145,48 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -5154,6 +6199,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5166,6 +6242,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -5555,6 +6657,21 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "node_modules/lodash.flatmap": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", + "integrity": "sha512-/OcpcAGWlrZyoHGeHh3cAoa6nGdX6QYtmzNP84Jqol6UEQQ2gIaU3H+0eICcjcKGl0/XF8LWOujNn9lffsnaOg==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5562,6 +6679,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.take": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.take/-/lodash.take-4.1.1.tgz", + "integrity": "sha512-3T118EQjnhr9c0aBKCCMhQn0OBwRMz/O2WaRU6VH0TSKoMCmFtUpr0iUp+eWKODEiRXtYOK7R7SiBneKHdk7og==" + }, + "node_modules/lodash.takeright": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.takeright/-/lodash.takeright-4.1.1.tgz", + "integrity": "sha512-/I41i2h8VkHtv3PYD8z1P4dkLIco5Z3z35hT/FJl18AxwSdifcATaaiBOxuQOT3T/F1qfRTct3VWMFSj1xCtAw==" + }, "node_modules/lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -5659,6 +6786,14 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -5727,6 +6862,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": 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", @@ -6113,6 +7260,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6134,6 +7293,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6155,6 +7356,15 @@ "wrappy": "1" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6292,6 +7502,55 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "peer": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "peer": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "peer": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6416,6 +7675,27 @@ "integrity": "sha512-MlRLyPI1p3/dJbsjVH+4xOPucycrz8T3EvO0BzCXaNtrUhZkZROtzib9J6mnC81AJO8eBIwiDZwTFel2cMmSuQ==", "license": "MIT" }, + "node_modules/portfinder": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "dev": true, + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -7061,6 +8341,25 @@ "node": ">=0.10.0" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7070,6 +8369,12 @@ "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", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -7293,12 +8598,34 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -7384,6 +8711,36 @@ "node": ">= 18" } }, + "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==", + "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/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -7530,6 +8887,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simmerjs": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/simmerjs/-/simmerjs-0.5.6.tgz", + "integrity": "sha512-Z00zGHUp2IVSDUuni6gzBxVVQwAEZ7jVHnqL97+2RaHVWTYKfgCNyCvgm68Uc1M6X84hjatxvtOc24Y9ECLPWQ==", + "dependencies": { + "lodash.difference": "^4.5.0", + "lodash.flatmap": "^4.5.0", + "lodash.isfunction": "^3.0.8", + "lodash.take": "^4.1.1", + "lodash.takeright": "^4.1.1" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -7922,6 +9291,18 @@ "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", "dev": true }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/streamx": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", @@ -8558,6 +9939,18 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -8834,12 +10227,215 @@ "node": ">= 8" } }, + "node_modules/webdriver": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.14.0.tgz", + "integrity": "sha512-0mVjxafQ5GNdK4l/FVmmmXGUfLHCSBE4Ml2LG23rxgmw53CThAos6h01UgIEINonxIzgKEmwfqJioo3/frbpbQ==", + "peer": true, + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.14.0", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.14.0", + "@wdio/types": "9.14.0", + "@wdio/utils": "9.14.0", + "deepmerge-ts": "^7.0.3", + "undici": "^6.20.1", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriver/node_modules/@wdio/logger": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.4.4.tgz", + "integrity": "sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==", + "peer": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriver/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "peer": true, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/webdriverio": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.14.0.tgz", + "integrity": "sha512-GP0p6J+yjcCXF9uXW7HjB6IEh33OKmZcLTSg/W2rnVYSWgsUEYPujKSXe5I8q5a99QID7OOKNKVMfs5ANoZ2BA==", + "peer": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.14.0", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.14.0", + "@wdio/repl": "9.4.4", + "@wdio/types": "9.14.0", + "@wdio/utils": "9.14.0", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.6.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.14.0" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@wdio/logger": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.4.4.tgz", + "integrity": "sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==", + "peer": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -8865,6 +10461,61 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index e43d755..68f6f2e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.2", + "@testing-library/webdriverio": "^3.2.1", "commander": "^13.1.0", "testplane": "latest", "zod": "^3.22.4" @@ -37,6 +38,7 @@ "@types/node": "^20.8.0", "eslint": "^9.26.0", "globals": "^16.1.0", + "http-server": "^14.1.1", "prettier": "^3.5.3", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", diff --git a/src/tools/click-on-element.ts b/src/tools/click-on-element.ts new file mode 100644 index 0000000..a2c3552 --- /dev/null +++ b/src/tools/click-on-element.ts @@ -0,0 +1,171 @@ +import { z } from "zod"; +import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { setupBrowser } from "@testing-library/webdriverio"; +import { ToolDefinition } from "../types.js"; +import { contextProvider } from "../context-provider.js"; +import { createBrowserStateResponse, createErrorResponse } from "../responses/index.js"; + +export const elementClickSchema = { + queryType: z + .enum(["role", "text", "labelText", "placeholderText", "displayValue", "altText", "title", "testId"]) + .optional() + .describe( + "Semantic query type (PREFERRED). Use this whenever possible for better accessibility and robustness.", + ), + + queryValue: z + .string() + .optional() + .describe("The value to search for with the specified queryType (e.g., 'button' for role, 'Submit' for text)."), + + queryOptions: z + .object({ + name: z + .string() + .optional() + .describe("Accessible name for role queries (e.g., getByRole('button', {name: 'Submit'}))"), + exact: z.boolean().optional().describe("Whether to match exact text (default: true)"), + hidden: z.boolean().optional().describe("Include elements hidden from accessibility tree (default: false)"), + level: z.number().optional().describe("Heading level for role='heading' (1-6)"), + }) + .optional() + .describe("Additional options for semantic queries"), + + selector: z + .string() + .optional() + .describe("CSS selector or XPath. Use only when semantic queries cannot locate the element."), +}; + +const clickOnElementCb: ToolCallback = async args => { + try { + const { queryType, queryValue, queryOptions, selector } = args; + + const hasSemanticQuery = queryType && queryValue; + const hasSelector = selector; + + if (!hasSemanticQuery && !hasSelector) { + throw new Error("Provide either semantic query (queryType + queryValue) or selector"); + } + + if (hasSemanticQuery && hasSelector) { + throw new Error( + "Provide EITHER semantic query (queryType + queryValue) OR selector, not both. Prefer semantic queries for better accessibility.", + ); + } + + const context = contextProvider.getContext(); + const browser = await context.browser.get(); + + let element; + let testplaneCode = ""; + let queryDescription = ""; + + if (queryType && queryValue) { + const { + getByRole, + getByText, + getByLabelText, + getByPlaceholderText, + getByDisplayValue, + getByAltText, + getByTitle, + getByTestId, + } = setupBrowser(browser as any); // eslint-disable-line @typescript-eslint/no-explicit-any + + switch (queryType) { + case "role": + element = await getByRole(queryValue, queryOptions); + queryDescription = `role "${queryValue}"${queryOptions?.name ? ` with name "${queryOptions.name}"` : ""}`; + testplaneCode = `const element = await browser.getByRole("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "text": + element = await getByText(queryValue, queryOptions); + queryDescription = `text "${queryValue}"`; + testplaneCode = `const element = await browser.getByText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "labelText": + element = await getByLabelText(queryValue, queryOptions); + queryDescription = `label text "${queryValue}"`; + testplaneCode = `const element = await browser.getByLabelText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "placeholderText": + element = await getByPlaceholderText(queryValue, queryOptions); + queryDescription = `placeholder text "${queryValue}"`; + testplaneCode = `const element = await browser.getByPlaceholderText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "displayValue": + element = await getByDisplayValue(queryValue, queryOptions); + queryDescription = `display value "${queryValue}"`; + testplaneCode = `const element = await browser.getByDisplayValue("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "altText": + element = await getByAltText(queryValue, queryOptions); + queryDescription = `alt text "${queryValue}"`; + testplaneCode = `const element = await browser.getByAltText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "title": + element = await getByTitle(queryValue, queryOptions); + queryDescription = `title "${queryValue}"`; + testplaneCode = `const element = await browser.getByTitle("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + case "testId": + element = await getByTestId(queryValue, queryOptions); + queryDescription = `test ID "${queryValue}"`; + testplaneCode = `const element = await browser.getByTestId("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; + break; + default: + throw new Error(`Unsupported queryType: ${queryType}`); + } + testplaneCode += `\nIMPORTANT for AI agents: if user asks to write tests, prefer to use semantic queries over CSS selectors, if user tests also use testing-library queries.`; + } else if (selector) { + element = await browser.$(selector); + queryDescription = `CSS selector "${selector}"`; + testplaneCode = `const element = await browser.$("${selector}");\nawait element.click();`; + } else { + throw new Error("No valid element selection method provided"); + } + + if (!element) { + throw new Error("Element not found"); + } + + await element.click(); + + console.error(`Successfully clicked element with ${queryDescription}`); + + return await createBrowserStateResponse(browser, { + action: `Successfully clicked element found by ${queryDescription}`, + testplaneCode, + additionalInfo: `Element selection strategy: ${queryType ? `Semantic query (${queryType})` : "CSS selector (fallback)"}`, + }); + } catch (error) { + console.error("Error clicking element:", error); + let errorMessage = "Error clicking element"; + + if (error instanceof Error && error.message.includes("Unable to find")) { + errorMessage = + "Element not found. Try using a different query strategy or check if the element exists on the page."; + } + + return createErrorResponse(errorMessage, error instanceof Error ? error : undefined); + } +}; + +export const clickOnElement: ToolDefinition = { + name: "clickOnElement", + description: `Click an element on the page. + +PREFERRED APPROACH (for AI agents): Use semantic queries (queryType + queryValue) which are more robust and accessibility-focused: +- queryType="role" + queryValue="button" + queryOptions.name="Submit" → finds submit button +- queryType="text" + queryValue="Click here" → finds element containing that text +- queryType="labelText" + queryValue="Email" → finds input with Email label + +FALLBACK APPROACH: Use selector only when semantic queries cannot locate the element: +- selector="button.submit-btn" → CSS selector +- selector="//button[text()='Submit']" → XPath + +AI agents should prioritize semantic queries for better accessibility and test maintainability.`, + schema: elementClickSchema, + cb: clickOnElementCb, +}; diff --git a/src/tools/index.ts b/src/tools/index.ts index 390a6c2..c8ddc1f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,5 +1,6 @@ import { ToolDefinition } from "../types.js"; import { navigate } from "./navigate.js"; import { closeBrowser } from "./close-browser.js"; +import { clickOnElement } from "./click-on-element.js"; -export const tools = [navigate, closeBrowser] as const satisfies ToolDefinition[]; // eslint-disable-line @typescript-eslint/no-explicit-any +export const tools = [navigate, closeBrowser, clickOnElement] as const satisfies ToolDefinition[]; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/test/playground/index.html b/test/playground/index.html new file mode 100644 index 0000000..99eed88 --- /dev/null +++ b/test/playground/index.html @@ -0,0 +1,285 @@ + + + + + + Element Click Test Playground + + + +

Element Click Test Playground

+

This page contains various elements for testing the element-click tool with different query strategies.

+ + +
+

Role-based Elements (getByRole)

+ + + + + + + +

Navigation Menu

+ + +

Headings

+

+ Click this heading + ✓ Clicked! +

+
+ + +
+

Text-based Elements (getByText)

+ +

+ Click here to test text selection + ✓ Clicked! +

+ +
+ Custom div with clickable text + ✓ Clicked! +
+ + + Download file + ✓ Clicked! + +
+ + +
+

Form Elements (getByLabelText)

+ +
+
+ + + ✓ Clicked! +
+ +
+ + + ✓ Clicked! +
+ +
+ + + ✓ Clicked! +
+
+
+ + +
+

Placeholder Elements (getByPlaceholderText)

+ + + ✓ Clicked! + + + ✓ Clicked! + + + ✓ Clicked! +
+ + +
+

Alt Text Elements (getByAltText)

+ + Company Logo + ✓ Clicked! + + Success icon + ✓ Clicked! +
+ + +
+

Test ID Elements (getByTestId)

+ + + +
+ Widget Container + ✓ Clicked! +
+
+ + +
+

CSS Selector Elements (Fallback)

+ + + +
+ Unique ID Element + ✓ Clicked! +
+
+ + +
+

Click Results

+

Click on any element above to see it marked as clicked!

+ +
+ + + + diff --git a/test/test-server.ts b/test/test-server.ts new file mode 100644 index 0000000..f34e79a --- /dev/null +++ b/test/test-server.ts @@ -0,0 +1,92 @@ +import { spawn, ChildProcess } from "child_process"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export interface TestServer { + start(): Promise; + stop(): Promise; +} + +export class PlaygroundServer implements TestServer { + private serverProcess: ChildProcess | null = null; + private readonly port: number; + private readonly playgroundPath: string; + + constructor(port: number = 8090) { + this.port = port; + this.playgroundPath = path.join(__dirname, "playground"); + } + + async start(): Promise { + return new Promise((resolve, reject) => { + this.serverProcess = spawn( + "npx", + ["http-server", this.playgroundPath, "-p", this.port.toString(), "--silent"], + { + stdio: ["ignore", "pipe", "pipe"], + cwd: path.join(__dirname, ".."), + }, + ); + + this.serverProcess.on("error", error => { + reject(new Error(`Failed to start server: ${error.message}`)); + }); + + this.serverProcess.on("exit", code => { + if (code !== 0 && code !== null) { + reject(new Error(`Server exited with code ${code}`)); + } + }); + + setTimeout(() => { + if (this.serverProcess && !this.serverProcess.killed) { + resolve(`http://localhost:${this.port}`); + } else { + reject(new Error(`Server startup failed`)); + } + }, 1000); + }); + } + + async stop(): Promise { + return new Promise(resolve => { + if (this.serverProcess && !this.serverProcess.killed) { + this.serverProcess.kill(); + this.serverProcess.on("exit", () => { + this.serverProcess = null; + resolve(); + }); + + setTimeout(() => { + if (this.serverProcess && !this.serverProcess.killed) { + this.serverProcess.kill("SIGKILL"); + this.serverProcess = null; + resolve(); + } + }, 2000); + } else { + resolve(); + } + }); + } +} + +let globalTestServer: PlaygroundServer | null = null; + +export const getTestServerUrl = async (): Promise => { + if (!globalTestServer) { + globalTestServer = new PlaygroundServer(); + return globalTestServer.start(); + } + return `http://localhost:8090`; +}; + +export const stopTestServer = async (): Promise => { + if (globalTestServer) { + await globalTestServer.stop(); + globalTestServer = null; + } +}; diff --git a/test/tools/click-on-element.test.ts b/test/tools/click-on-element.test.ts new file mode 100644 index 0000000..7f64a2c --- /dev/null +++ b/test/tools/click-on-element.test.ts @@ -0,0 +1,414 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest"; +import { startClient } from "../utils"; +import { INTEGRATION_TEST_TIMEOUT } from "../constants"; +import { getTestServerUrl, stopTestServer } from "../test-server"; + +describe( + "tools/clickOnElement", + () => { + let client: Client; + let playgroundUrl: string; + + beforeAll(async () => { + playgroundUrl = await getTestServerUrl(); + }, 20000); + + afterAll(async () => { + await stopTestServer(); + }); + + beforeEach(async () => { + client = await startClient(); + await client.callTool({ name: "navigate", arguments: { url: playgroundUrl } }); + }); + + afterEach(async () => { + if (client) { + await client.close(); + } + }); + + describe("clickOnElement tool availability", () => { + it("should list clickOnElement tool in available tools", async () => { + const tools = await client.listTools(); + + const elementClickTool = tools.tools.find(tool => tool.name === "clickOnElement"); + + expect(elementClickTool).toBeDefined(); + expect(elementClickTool?.description).toContain("Click an element on the page"); + }); + }); + + describe("semantic queries", () => { + describe("getByRole queries", () => { + it("should click button by role", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + queryValue: "button", + queryOptions: { name: "Submit Form" }, + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by role "button" with name "Submit Form"', + ); + expect(content[0].text).toContain('browser.getByRole("button"'); + }); + + it("should click button by role without name option", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + queryValue: "button", + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Found multiple elements with the role "button"'); + }); + + it("should click link by role", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + queryValue: "link", + queryOptions: { name: "Home" }, + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by role "link" with name "Home"', + ); + }); + + it("should click heading by role with level", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + queryValue: "heading", + queryOptions: { level: 3, name: "Click this heading" }, + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully clicked element found by role "heading"'); + }); + }); + + describe("getByText queries", () => { + it("should click element by text content", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "text", + queryValue: "Click here to test text selection", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by text "Click here to test text selection"', + ); + expect(content[0].text).toContain('browser.getByText("Click here to test text selection"'); + }); + + it("should click element by partial text with exact: false", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "text", + queryValue: "Download", + queryOptions: { exact: false }, + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully clicked element found by text "Download"'); + }); + }); + + describe("getByLabelText queries", () => { + it("should click input by label text", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "labelText", + queryValue: "Email Address", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by label text "Email Address"', + ); + expect(content[0].text).toContain('browser.getByLabelText("Email Address"'); + }); + + it("should click textarea by label text", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "labelText", + queryValue: "Message", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully clicked element found by label text "Message"'); + }); + }); + + describe("getByPlaceholderText queries", () => { + it("should click input by placeholder text", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "placeholderText", + queryValue: "Enter your name", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by placeholder text "Enter your name"', + ); + expect(content[0].text).toContain('browser.getByPlaceholderText("Enter your name"'); + }); + + it("should click textarea by placeholder text", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "placeholderText", + queryValue: "Type your feedback here...", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by placeholder text "Type your feedback here..."', + ); + }); + }); + + describe("getByAltText queries", () => { + it("should click image by alt text", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "altText", + queryValue: "Company Logo", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully clicked element found by alt text "Company Logo"'); + expect(content[0].text).toContain('browser.getByAltText("Company Logo"'); + }); + + it("should click another image by alt text", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "altText", + queryValue: "Success icon", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully clicked element found by alt text "Success icon"'); + }); + }); + + describe("getByTestId queries", () => { + it("should click element by test id", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "testId", + queryValue: "action-button", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully clicked element found by test ID "action-button"'); + expect(content[0].text).toContain('browser.getByTestId("action-button"'); + }); + + it("should click container by test id", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "testId", + queryValue: "widget-container", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by test ID "widget-container"', + ); + }); + }); + }); + + describe("CSS selector", () => { + it("should click element by CSS class selector", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + selector: ".custom-class-btn", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by CSS selector ".custom-class-btn"', + ); + expect(content[0].text).toContain('browser.$(".custom-class-btn")'); + }); + + it("should click element by ID selector", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + selector: "#unique-element", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by CSS selector "#unique-element"', + ); + expect(content[0].text).toContain('browser.$("#unique-element")'); + }); + + it("should click element by complex CSS selector", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + selector: "button.success-btn", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain( + 'Successfully clicked element found by CSS selector "button.success-btn"', + ); + }); + }); + + describe("error handling", () => { + it("should reject when both semantic query and selector are provided", async () => { + try { + await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + queryValue: "button", + selector: "#some-button", + }, + }); + expect.fail("Expected the call to fail"); + } catch (error) { + expect(error).toBeDefined(); + } + }); + + it("should reject when neither semantic query nor selector is provided", async () => { + try { + await client.callTool({ + name: "clickOnElement", + arguments: {}, + }); + expect.fail("Expected the call to fail"); + } catch (error) { + expect(error).toBeDefined(); + } + }); + + it("should reject when queryType is provided without queryValue", async () => { + try { + await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + }, + }); + expect.fail("Expected the call to fail"); + } catch (error) { + expect(error).toBeDefined(); + } + }); + + it("should handle element not found gracefully", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "role", + queryValue: "button", + queryOptions: { name: "Non-existent Button" }, + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain("Element not found"); + }); + + it("should handle invalid CSS selector gracefully", async () => { + const result = await client.callTool({ + name: "clickOnElement", + arguments: { + selector: ".non-existent-class", + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain("Error clicking element"); + }); + + it("should reject unsupported queryType", async () => { + try { + await client.callTool({ + name: "clickOnElement", + arguments: { + queryType: "invalidType" as "role", + queryValue: "button", + }, + }); + expect.fail("Expected the call to fail"); + } catch (error) { + expect(error).toBeDefined(); + } + }); + }); + }, + INTEGRATION_TEST_TIMEOUT, +); From b645fa1ce916b4037ec5c96a5b6ee34aaf66669f Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 30 May 2025 02:21:49 +0300 Subject: [PATCH 2/4] feat: implement typeIntoElement command and refactor semantic queries --- package-lock.json | 263 +++++---------- package.json | 2 +- src/tools/click-on-element.ts | 125 +------ src/tools/element-selector.ts | 155 +++++++++ src/tools/index.ts | 3 +- src/tools/type-into-element.ts | 64 ++++ src/tools/utils/element-selector.ts | 153 +++++++++ test/simple-http-server.ts | 20 ++ test/test-server.ts | 74 +---- test/tools/click-on-element.test.ts | 356 ++------------------ test/tools/type-into-element.test.ts | 114 +++++++ test/tools/utils/element-selector.test.ts | 379 ++++++++++++++++++++++ 12 files changed, 1014 insertions(+), 694 deletions(-) create mode 100644 src/tools/element-selector.ts create mode 100644 src/tools/type-into-element.ts create mode 100644 src/tools/utils/element-selector.ts create mode 100644 test/simple-http-server.ts create mode 100644 test/tools/type-into-element.test.ts create mode 100644 test/tools/utils/element-selector.test.ts diff --git a/package-lock.json b/package-lock.json index dafe852..b7501ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@types/node": "^20.8.0", "eslint": "^9.26.0", "globals": "^16.1.0", - "http-server": "^14.1.1", "prettier": "^3.5.3", + "serve-handler": "^6.1.6", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", "vitest": "^3.1.4" @@ -2882,24 +2882,6 @@ "node": "^4.5.0 || >= 5.9" } }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -3592,15 +3574,6 @@ "node": ">= 0.10" } }, - "node_modules/corser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", - "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -4143,18 +4116,6 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "peer": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4717,12 +4678,6 @@ "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -5090,26 +5045,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5661,18 +5596,6 @@ "he": "bin/he" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/htmlfy": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.7.tgz", @@ -5720,20 +5643,6 @@ "node": ">= 0.8" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5747,33 +5656,6 @@ "node": ">= 14" } }, - "node_modules/http-server": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", - "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", - "dev": true, - "dependencies": { - "basic-auth": "^2.0.1", - "chalk": "^4.1.2", - "corser": "^2.0.1", - "he": "^1.2.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy": "^1.18.1", - "mime": "^1.6.0", - "minimist": "^1.2.6", - "opener": "^1.5.1", - "portfinder": "^1.0.28", - "secure-compare": "3.0.1", - "union": "~0.5.0", - "url-join": "^4.0.1" - }, - "bin": { - "http-server": "bin/http-server" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -6862,18 +6744,6 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": 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", @@ -7356,15 +7226,6 @@ "wrappy": "1" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "bin": { - "opener": "bin/opener-bin.js" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7578,6 +7439,12 @@ "node": ">=0.10.0" } }, + "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 + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7675,19 +7542,6 @@ "integrity": "sha512-MlRLyPI1p3/dJbsjVH+4xOPucycrz8T3EvO0BzCXaNtrUhZkZROtzib9J6mnC81AJO8eBIwiDZwTFel2cMmSuQ==", "license": "MIT" }, - "node_modules/portfinder": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", - "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", - "dev": true, - "dependencies": { - "async": "^3.2.6", - "debug": "^4.3.6" - }, - "engines": { - "node": ">= 10.12" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8369,12 +8223,6 @@ "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", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -8620,12 +8468,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/secure-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", - "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", - "dev": true - }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -8696,6 +8538,75 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "dev": true, + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "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, + "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, + "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, + "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, + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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 + }, + "node_modules/serve-handler/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, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -9939,18 +9850,6 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, - "node_modules/union": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", - "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", - "dev": true, - "dependencies": { - "qs": "^6.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -10416,15 +10315,15 @@ "license": "BSD-2-Clause" }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-mimetype": { diff --git a/package.json b/package.json index 68f6f2e..9b97fd3 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "@types/node": "^20.8.0", "eslint": "^9.26.0", "globals": "^16.1.0", - "http-server": "^14.1.1", "prettier": "^3.5.3", + "serve-handler": "^6.1.6", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", "vitest": "^3.1.4" diff --git a/src/tools/click-on-element.ts b/src/tools/click-on-element.ts index a2c3552..dfbd901 100644 --- a/src/tools/click-on-element.ts +++ b/src/tools/click-on-element.ts @@ -1,134 +1,17 @@ -import { z } from "zod"; import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { setupBrowser } from "@testing-library/webdriverio"; import { ToolDefinition } from "../types.js"; import { contextProvider } from "../context-provider.js"; import { createBrowserStateResponse, createErrorResponse } from "../responses/index.js"; +import { elementSelectorSchema, findElement } from "./utils/element-selector.js"; -export const elementClickSchema = { - queryType: z - .enum(["role", "text", "labelText", "placeholderText", "displayValue", "altText", "title", "testId"]) - .optional() - .describe( - "Semantic query type (PREFERRED). Use this whenever possible for better accessibility and robustness.", - ), - - queryValue: z - .string() - .optional() - .describe("The value to search for with the specified queryType (e.g., 'button' for role, 'Submit' for text)."), - - queryOptions: z - .object({ - name: z - .string() - .optional() - .describe("Accessible name for role queries (e.g., getByRole('button', {name: 'Submit'}))"), - exact: z.boolean().optional().describe("Whether to match exact text (default: true)"), - hidden: z.boolean().optional().describe("Include elements hidden from accessibility tree (default: false)"), - level: z.number().optional().describe("Heading level for role='heading' (1-6)"), - }) - .optional() - .describe("Additional options for semantic queries"), - - selector: z - .string() - .optional() - .describe("CSS selector or XPath. Use only when semantic queries cannot locate the element."), -}; +export const elementClickSchema = elementSelectorSchema; const clickOnElementCb: ToolCallback = async args => { try { - const { queryType, queryValue, queryOptions, selector } = args; - - const hasSemanticQuery = queryType && queryValue; - const hasSelector = selector; - - if (!hasSemanticQuery && !hasSelector) { - throw new Error("Provide either semantic query (queryType + queryValue) or selector"); - } - - if (hasSemanticQuery && hasSelector) { - throw new Error( - "Provide EITHER semantic query (queryType + queryValue) OR selector, not both. Prefer semantic queries for better accessibility.", - ); - } - const context = contextProvider.getContext(); const browser = await context.browser.get(); - let element; - let testplaneCode = ""; - let queryDescription = ""; - - if (queryType && queryValue) { - const { - getByRole, - getByText, - getByLabelText, - getByPlaceholderText, - getByDisplayValue, - getByAltText, - getByTitle, - getByTestId, - } = setupBrowser(browser as any); // eslint-disable-line @typescript-eslint/no-explicit-any - - switch (queryType) { - case "role": - element = await getByRole(queryValue, queryOptions); - queryDescription = `role "${queryValue}"${queryOptions?.name ? ` with name "${queryOptions.name}"` : ""}`; - testplaneCode = `const element = await browser.getByRole("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "text": - element = await getByText(queryValue, queryOptions); - queryDescription = `text "${queryValue}"`; - testplaneCode = `const element = await browser.getByText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "labelText": - element = await getByLabelText(queryValue, queryOptions); - queryDescription = `label text "${queryValue}"`; - testplaneCode = `const element = await browser.getByLabelText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "placeholderText": - element = await getByPlaceholderText(queryValue, queryOptions); - queryDescription = `placeholder text "${queryValue}"`; - testplaneCode = `const element = await browser.getByPlaceholderText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "displayValue": - element = await getByDisplayValue(queryValue, queryOptions); - queryDescription = `display value "${queryValue}"`; - testplaneCode = `const element = await browser.getByDisplayValue("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "altText": - element = await getByAltText(queryValue, queryOptions); - queryDescription = `alt text "${queryValue}"`; - testplaneCode = `const element = await browser.getByAltText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "title": - element = await getByTitle(queryValue, queryOptions); - queryDescription = `title "${queryValue}"`; - testplaneCode = `const element = await browser.getByTitle("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - case "testId": - element = await getByTestId(queryValue, queryOptions); - queryDescription = `test ID "${queryValue}"`; - testplaneCode = `const element = await browser.getByTestId("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\nawait element.click();`; - break; - default: - throw new Error(`Unsupported queryType: ${queryType}`); - } - testplaneCode += `\nIMPORTANT for AI agents: if user asks to write tests, prefer to use semantic queries over CSS selectors, if user tests also use testing-library queries.`; - } else if (selector) { - element = await browser.$(selector); - queryDescription = `CSS selector "${selector}"`; - testplaneCode = `const element = await browser.$("${selector}");\nawait element.click();`; - } else { - throw new Error("No valid element selection method provided"); - } - - if (!element) { - throw new Error("Element not found"); - } + const { element, queryDescription, testplaneCode } = await findElement(browser, args, `await element.click();`); await element.click(); @@ -137,7 +20,7 @@ const clickOnElementCb: ToolCallback = async args => return await createBrowserStateResponse(browser, { action: `Successfully clicked element found by ${queryDescription}`, testplaneCode, - additionalInfo: `Element selection strategy: ${queryType ? `Semantic query (${queryType})` : "CSS selector (fallback)"}`, + additionalInfo: `Element selection strategy: ${args.queryType ? `Semantic query (${args.queryType})` : "CSS selector (fallback)"}`, }); } catch (error) { console.error("Error clicking element:", error); diff --git a/src/tools/element-selector.ts b/src/tools/element-selector.ts new file mode 100644 index 0000000..93ac835 --- /dev/null +++ b/src/tools/element-selector.ts @@ -0,0 +1,155 @@ +import { z } from "zod"; +import { setupBrowser } from "@testing-library/webdriverio"; + +export const elementSelectorSchema = { + queryType: z + .enum(["role", "text", "labelText", "placeholderText", "displayValue", "altText", "title", "testId"]) + .optional() + .describe( + "Semantic query type (PREFERRED). Use this whenever possible for better accessibility and robustness.", + ), + + queryValue: z + .string() + .optional() + .describe("The value to search for with the specified queryType (e.g., 'button' for role, 'Submit' for text)."), + + queryOptions: z + .object({ + name: z + .string() + .optional() + .describe("Accessible name for role queries (e.g., getByRole('button', {name: 'Submit'}))"), + exact: z.boolean().optional().describe("Whether to match exact text (default: true)"), + hidden: z.boolean().optional().describe("Include elements hidden from accessibility tree (default: false)"), + level: z.number().optional().describe("Heading level for role='heading' (1-6)"), + }) + .optional() + .describe("Additional options for semantic queries"), + + selector: z + .string() + .optional() + .describe("CSS selector or XPath. Use only when semantic queries cannot locate the element."), +}; + +export interface ElementSelectorArgs { + queryType?: "role" | "text" | "labelText" | "placeholderText" | "displayValue" | "altText" | "title" | "testId"; + queryValue?: string; + queryOptions?: { + name?: string; + exact?: boolean; + hidden?: boolean; + level?: number; + }; + selector?: string; +} + +export interface ElementResult { + element: any; // eslint-disable-line @typescript-eslint/no-explicit-any + queryDescription: string; + testplaneCode: string; +} + +export async function findElement( + browser: any, // eslint-disable-line @typescript-eslint/no-explicit-any + args: ElementSelectorArgs, + actionType: "click" | "setValue", +): Promise { + const { queryType, queryValue, queryOptions, selector } = args; + + const hasSemanticQuery = queryType && queryValue; + const hasSelector = selector; + + if (!hasSemanticQuery && !hasSelector) { + throw new Error("Provide either semantic query (queryType + queryValue) or selector"); + } + + if (hasSemanticQuery && hasSelector) { + throw new Error( + "Provide EITHER semantic query (queryType + queryValue) OR selector, not both. Prefer semantic queries for better accessibility.", + ); + } + + let element; + let testplaneCode = ""; + let queryDescription = ""; + + if (queryType && queryValue) { + const { + getByRole, + getByText, + getByLabelText, + getByPlaceholderText, + getByDisplayValue, + getByAltText, + getByTitle, + getByTestId, + } = setupBrowser(browser as any); // eslint-disable-line @typescript-eslint/no-explicit-any + + const actionCode = actionType === "click" ? "await element.click();" : "await element.setValue(text);"; + + switch (queryType) { + case "role": + element = await getByRole(queryValue, queryOptions); + queryDescription = `role "${queryValue}"${queryOptions?.name ? ` with name "${queryOptions.name}"` : ""}`; + testplaneCode = `const element = await browser.getByRole("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "text": + element = await getByText(queryValue, queryOptions); + queryDescription = `text "${queryValue}"`; + testplaneCode = `const element = await browser.getByText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "labelText": + element = await getByLabelText(queryValue, queryOptions); + queryDescription = `label text "${queryValue}"`; + testplaneCode = `const element = await browser.getByLabelText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "placeholderText": + element = await getByPlaceholderText(queryValue, queryOptions); + queryDescription = `placeholder text "${queryValue}"`; + testplaneCode = `const element = await browser.getByPlaceholderText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "displayValue": + element = await getByDisplayValue(queryValue, queryOptions); + queryDescription = `display value "${queryValue}"`; + testplaneCode = `const element = await browser.getByDisplayValue("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "altText": + element = await getByAltText(queryValue, queryOptions); + queryDescription = `alt text "${queryValue}"`; + testplaneCode = `const element = await browser.getByAltText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "title": + element = await getByTitle(queryValue, queryOptions); + queryDescription = `title "${queryValue}"`; + testplaneCode = `const element = await browser.getByTitle("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "testId": + element = await getByTestId(queryValue, queryOptions); + queryDescription = `test ID "${queryValue}"`; + testplaneCode = `const element = await browser.getByTestId("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + default: + throw new Error(`Unsupported queryType: ${queryType}`); + } + testplaneCode += `\nIMPORTANT for AI agents: if user asks to write tests, prefer to use semantic queries over CSS selectors, if user tests also use testing-library queries.`; + } else if (selector) { + element = await browser.$(selector); + queryDescription = `CSS selector "${selector}"`; + const actionCode = actionType === "click" ? "await element.click();" : "await element.setValue(text);"; + testplaneCode = `const element = await browser.$("${selector}");\n${actionCode}`; + } else { + throw new Error("No valid element selection method provided"); + } + + if (!element) { + throw new Error("Element not found"); + } + + return { + element, + queryDescription, + testplaneCode, + }; +} diff --git a/src/tools/index.ts b/src/tools/index.ts index c8ddc1f..ec3fb3b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,5 +2,6 @@ import { ToolDefinition } from "../types.js"; import { navigate } from "./navigate.js"; import { closeBrowser } from "./close-browser.js"; import { clickOnElement } from "./click-on-element.js"; +import { typeIntoElement } from "./type-into-element.js"; -export const tools = [navigate, closeBrowser, clickOnElement] as const satisfies ToolDefinition[]; // eslint-disable-line @typescript-eslint/no-explicit-any +export const tools = [navigate, closeBrowser, clickOnElement, typeIntoElement] as const satisfies ToolDefinition[]; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/src/tools/type-into-element.ts b/src/tools/type-into-element.ts new file mode 100644 index 0000000..9b78925 --- /dev/null +++ b/src/tools/type-into-element.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; +import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ToolDefinition } from "../types.js"; +import { contextProvider } from "../context-provider.js"; +import { createBrowserStateResponse, createErrorResponse } from "../responses/index.js"; +import { elementSelectorSchema, findElement } from "./utils/element-selector.js"; + +export const typeIntoElementSchema = { + ...elementSelectorSchema, + text: z.string().describe("The text to type into the element"), +}; + +const typeIntoElementCb: ToolCallback = async args => { + try { + const { text, ...selectorArgs } = args; + + const context = contextProvider.getContext(); + const browser = await context.browser.get(); + + const { element, queryDescription, testplaneCode } = await findElement( + browser, + selectorArgs, + `await element.setValue("${text}");`, + ); + + await element.setValue(text); + + console.error(`Successfully typed "${text}" into element with ${queryDescription}`); + + return await createBrowserStateResponse(browser, { + action: `Successfully typed "${text}" into element found by ${queryDescription}`, + testplaneCode, + additionalInfo: `Element selection strategy: ${selectorArgs.queryType ? `Semantic query (${selectorArgs.queryType})` : "CSS selector (fallback)"}`, + }); + } catch (error) { + console.error("Error typing into element:", error); + let errorMessage = "Error typing into element"; + + if (error instanceof Error && error.message.includes("Unable to find")) { + errorMessage = + "Element not found. Try using a different query strategy or check if the element exists on the page."; + } + + return createErrorResponse(errorMessage, error instanceof Error ? error : undefined); + } +}; + +export const typeIntoElement: ToolDefinition = { + name: "typeIntoElement", + description: `Type text into an element on the page. The API is very similar to clickOnElement for consistency. + +PREFERRED APPROACH (for AI agents): Use semantic queries (queryType + queryValue) which are more robust and accessibility-focused: +- queryType="role" + queryValue="textbox" + queryOptions.name="Email" → finds email input +- queryType="labelText" + queryValue="Password" → finds input with Password label +- queryType="placeholderText" + queryValue="Enter your name" → finds input with specific placeholder + +FALLBACK APPROACH: Use selector only when semantic queries cannot locate the element: +- selector="input[name='email']" → CSS selector +- selector="//input[@placeholder='Search...']" → XPath + +AI agents should prioritize semantic queries for better accessibility and test maintainability.`, + schema: typeIntoElementSchema, + cb: typeIntoElementCb, +}; diff --git a/src/tools/utils/element-selector.ts b/src/tools/utils/element-selector.ts new file mode 100644 index 0000000..ccdc159 --- /dev/null +++ b/src/tools/utils/element-selector.ts @@ -0,0 +1,153 @@ +import { z } from "zod"; +import { WdioBrowser } from "testplane"; +import { setupBrowser } from "@testing-library/webdriverio"; + +export const elementSelectorSchema = { + queryType: z + .enum(["role", "text", "labelText", "placeholderText", "displayValue", "altText", "title", "testId"]) + .optional() + .describe( + "Semantic query type (PREFERRED). Use this whenever possible for better accessibility and robustness.", + ), + + queryValue: z + .string() + .optional() + .describe("The value to search for with the specified queryType (e.g., 'button' for role, 'Submit' for text)."), + + queryOptions: z + .object({ + name: z + .string() + .optional() + .describe("Accessible name for role queries (e.g., getByRole('button', {name: 'Submit'}))"), + exact: z.boolean().optional().describe("Whether to match exact text (default: true)"), + hidden: z.boolean().optional().describe("Include elements hidden from accessibility tree (default: false)"), + level: z.number().optional().describe("Heading level for role='heading' (1-6)"), + }) + .optional() + .describe("Additional options for semantic queries"), + + selector: z + .string() + .optional() + .describe("CSS selector or XPath. Use only when semantic queries cannot locate the element."), +}; + +export interface ElementSelectorArgs { + queryType?: "role" | "text" | "labelText" | "placeholderText" | "displayValue" | "altText" | "title" | "testId"; + queryValue?: string; + queryOptions?: { + name?: string; + exact?: boolean; + hidden?: boolean; + level?: number; + }; + selector?: string; +} + +export interface ElementResult { + element: WebdriverIO.Element; + queryDescription: string; + testplaneCode: string; +} + +export async function findElement( + browser: WdioBrowser, + args: ElementSelectorArgs, + actionCode: string, +): Promise { + const { queryType, queryValue, queryOptions, selector } = args; + + const hasSemanticQuery = queryType && queryValue; + const hasSelector = selector; + + if (!hasSemanticQuery && !hasSelector) { + throw new Error("Provide either semantic query (queryType + queryValue) or selector"); + } + + if (hasSemanticQuery && hasSelector) { + throw new Error( + "Provide EITHER semantic query (queryType + queryValue) OR selector, not both. Prefer semantic queries for better accessibility.", + ); + } + + let element; + let testplaneCode = ""; + let queryDescription = ""; + + if (queryType && queryValue) { + const { + getByRole, + getByText, + getByLabelText, + getByPlaceholderText, + getByDisplayValue, + getByAltText, + getByTitle, + getByTestId, + } = setupBrowser(browser as any); // eslint-disable-line @typescript-eslint/no-explicit-any + + switch (queryType) { + case "role": + element = await getByRole(queryValue, queryOptions); + queryDescription = `role "${queryValue}"${queryOptions?.name ? ` with name "${queryOptions.name}"` : ""}`; + testplaneCode = `const element = await browser.getByRole("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "text": + element = await getByText(queryValue, queryOptions); + queryDescription = `text "${queryValue}"`; + testplaneCode = `const element = await browser.getByText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "labelText": + element = await getByLabelText(queryValue, queryOptions); + queryDescription = `label text "${queryValue}"`; + testplaneCode = `const element = await browser.getByLabelText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "placeholderText": + element = await getByPlaceholderText(queryValue, queryOptions); + queryDescription = `placeholder text "${queryValue}"`; + testplaneCode = `const element = await browser.getByPlaceholderText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "displayValue": + element = await getByDisplayValue(queryValue, queryOptions); + queryDescription = `display value "${queryValue}"`; + testplaneCode = `const element = await browser.getByDisplayValue("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "altText": + element = await getByAltText(queryValue, queryOptions); + queryDescription = `alt text "${queryValue}"`; + testplaneCode = `const element = await browser.getByAltText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "title": + element = await getByTitle(queryValue, queryOptions); + queryDescription = `title "${queryValue}"`; + testplaneCode = `const element = await browser.getByTitle("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + case "testId": + element = await getByTestId(queryValue, queryOptions); + queryDescription = `test ID "${queryValue}"`; + testplaneCode = `const element = await browser.getByTestId("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; + break; + default: + throw new Error(`Unsupported queryType: ${queryType}`); + } + testplaneCode += `\n// IMPORTANT for AI agents: if user asks to write tests, prefer to use semantic queries over CSS selectors, if user tests also use testing-library queries.`; + } else if (selector) { + element = await browser.$(selector); + queryDescription = `CSS selector "${selector}"`; + testplaneCode = `const element = await browser.$("${selector}");\n${actionCode}`; + } else { + throw new Error("No valid element selection method provided"); + } + + if (!element) { + throw new Error("Element not found"); + } + + return { + element, + queryDescription, + testplaneCode, + }; +} diff --git a/test/simple-http-server.ts b/test/simple-http-server.ts new file mode 100644 index 0000000..1101d6c --- /dev/null +++ b/test/simple-http-server.ts @@ -0,0 +1,20 @@ +import http from "http"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import handler from "serve-handler"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export function launchServer(port = 8090, rootDir = join(__dirname, "playground")): http.Server { + const server = http.createServer((request, response) => { + return handler(request, response, { + public: rootDir, + }); + }); + + server.listen(port, () => { + console.log(`Running at http://localhost:${port}`); + }); + + return server; +} diff --git a/test/test-server.ts b/test/test-server.ts index f34e79a..f5b945b 100644 --- a/test/test-server.ts +++ b/test/test-server.ts @@ -1,6 +1,7 @@ -import { spawn, ChildProcess } from "child_process"; +import { launchServer } from "./simple-http-server.js"; import path from "path"; import { fileURLToPath } from "url"; +import http from "http"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -11,82 +12,35 @@ export interface TestServer { } export class PlaygroundServer implements TestServer { - private serverProcess: ChildProcess | null = null; + private server: http.Server | null = null; private readonly port: number; private readonly playgroundPath: string; - constructor(port: number = 8090) { - this.port = port; + constructor(port?: number) { + this.port = port ?? Math.floor(Math.random() * (65535 - 3000) + 3000); this.playgroundPath = path.join(__dirname, "playground"); } async start(): Promise { - return new Promise((resolve, reject) => { - this.serverProcess = spawn( - "npx", - ["http-server", this.playgroundPath, "-p", this.port.toString(), "--silent"], - { - stdio: ["ignore", "pipe", "pipe"], - cwd: path.join(__dirname, ".."), - }, - ); + this.server = await launchServer(this.port, this.playgroundPath); + const url = `http://localhost:${this.port}`; - this.serverProcess.on("error", error => { - reject(new Error(`Failed to start server: ${error.message}`)); - }); - - this.serverProcess.on("exit", code => { - if (code !== 0 && code !== null) { - reject(new Error(`Server exited with code ${code}`)); - } - }); - - setTimeout(() => { - if (this.serverProcess && !this.serverProcess.killed) { - resolve(`http://localhost:${this.port}`); - } else { - reject(new Error(`Server startup failed`)); - } - }, 1000); - }); + return url; } async stop(): Promise { + console.log("Stopping HTTP server..."); return new Promise(resolve => { - if (this.serverProcess && !this.serverProcess.killed) { - this.serverProcess.kill(); - this.serverProcess.on("exit", () => { - this.serverProcess = null; + if (this.server) { + this.server.close(() => { + console.log("HTTP server stopped"); + this.server = null; resolve(); }); - - setTimeout(() => { - if (this.serverProcess && !this.serverProcess.killed) { - this.serverProcess.kill("SIGKILL"); - this.serverProcess = null; - resolve(); - } - }, 2000); } else { + console.log("No server to stop"); resolve(); } }); } } - -let globalTestServer: PlaygroundServer | null = null; - -export const getTestServerUrl = async (): Promise => { - if (!globalTestServer) { - globalTestServer = new PlaygroundServer(); - return globalTestServer.start(); - } - return `http://localhost:8090`; -}; - -export const stopTestServer = async (): Promise => { - if (globalTestServer) { - await globalTestServer.stop(); - globalTestServer = null; - } -}; diff --git a/test/tools/click-on-element.test.ts b/test/tools/click-on-element.test.ts index 7f64a2c..17fabf1 100644 --- a/test/tools/click-on-element.test.ts +++ b/test/tools/click-on-element.test.ts @@ -1,21 +1,25 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; import { startClient } from "../utils"; import { INTEGRATION_TEST_TIMEOUT } from "../constants"; -import { getTestServerUrl, stopTestServer } from "../test-server"; +import { PlaygroundServer } from "../test-server"; describe( "tools/clickOnElement", () => { let client: Client; let playgroundUrl: string; + let testServer: PlaygroundServer; beforeAll(async () => { - playgroundUrl = await getTestServerUrl(); + testServer = new PlaygroundServer(); + playgroundUrl = await testServer.start(); }, 20000); afterAll(async () => { - await stopTestServer(); + if (testServer) { + await testServer.stop(); + } }); beforeEach(async () => { @@ -29,269 +33,34 @@ describe( } }); - describe("clickOnElement tool availability", () => { + describe("tool availability", () => { it("should list clickOnElement tool in available tools", async () => { const tools = await client.listTools(); const elementClickTool = tools.tools.find(tool => tool.name === "clickOnElement"); expect(elementClickTool).toBeDefined(); - expect(elementClickTool?.description).toContain("Click an element on the page"); - }); - }); - - describe("semantic queries", () => { - describe("getByRole queries", () => { - it("should click button by role", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "role", - queryValue: "button", - queryOptions: { name: "Submit Form" }, - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by role "button" with name "Submit Form"', - ); - expect(content[0].text).toContain('browser.getByRole("button"'); - }); - - it("should click button by role without name option", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "role", - queryValue: "button", - }, - }); - - expect(result.isError).toBe(true); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Found multiple elements with the role "button"'); - }); - - it("should click link by role", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "role", - queryValue: "link", - queryOptions: { name: "Home" }, - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by role "link" with name "Home"', - ); - }); - - it("should click heading by role with level", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "role", - queryValue: "heading", - queryOptions: { level: 3, name: "Click this heading" }, - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Successfully clicked element found by role "heading"'); - }); - }); - - describe("getByText queries", () => { - it("should click element by text content", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "text", - queryValue: "Click here to test text selection", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by text "Click here to test text selection"', - ); - expect(content[0].text).toContain('browser.getByText("Click here to test text selection"'); - }); - - it("should click element by partial text with exact: false", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "text", - queryValue: "Download", - queryOptions: { exact: false }, - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Successfully clicked element found by text "Download"'); - }); - }); - - describe("getByLabelText queries", () => { - it("should click input by label text", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "labelText", - queryValue: "Email Address", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by label text "Email Address"', - ); - expect(content[0].text).toContain('browser.getByLabelText("Email Address"'); - }); - - it("should click textarea by label text", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "labelText", - queryValue: "Message", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Successfully clicked element found by label text "Message"'); - }); - }); - - describe("getByPlaceholderText queries", () => { - it("should click input by placeholder text", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "placeholderText", - queryValue: "Enter your name", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by placeholder text "Enter your name"', - ); - expect(content[0].text).toContain('browser.getByPlaceholderText("Enter your name"'); - }); - - it("should click textarea by placeholder text", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "placeholderText", - queryValue: "Type your feedback here...", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by placeholder text "Type your feedback here..."', - ); - }); - }); - - describe("getByAltText queries", () => { - it("should click image by alt text", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "altText", - queryValue: "Company Logo", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Successfully clicked element found by alt text "Company Logo"'); - expect(content[0].text).toContain('browser.getByAltText("Company Logo"'); - }); - - it("should click another image by alt text", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "altText", - queryValue: "Success icon", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Successfully clicked element found by alt text "Success icon"'); - }); - }); - - describe("getByTestId queries", () => { - it("should click element by test id", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "testId", - queryValue: "action-button", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain('Successfully clicked element found by test ID "action-button"'); - expect(content[0].text).toContain('browser.getByTestId("action-button"'); - }); - - it("should click container by test id", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "testId", - queryValue: "widget-container", - }, - }); - - expect(result.isError).toBe(false); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by test ID "widget-container"', - ); - }); }); }); - describe("CSS selector", () => { - it("should click element by CSS class selector", async () => { + describe("clicking functionality", () => { + it("should click an element using semantic query", async () => { const result = await client.callTool({ name: "clickOnElement", arguments: { - selector: ".custom-class-btn", + queryType: "role", + queryValue: "button", + queryOptions: { name: "Submit Form" }, }, }); expect(result.isError).toBe(false); const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by CSS selector ".custom-class-btn"', - ); - expect(content[0].text).toContain('browser.$(".custom-class-btn")'); + expect(content[0].text).toContain("Successfully clicked element"); + expect(content[0].text).toContain("clicked-indicator show"); }); - it("should click element by ID selector", async () => { + it("should click an element using CSS selector", async () => { const result = await client.callTool({ name: "clickOnElement", arguments: { @@ -301,72 +70,29 @@ describe( expect(result.isError).toBe(false); const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by CSS selector "#unique-element"', - ); - expect(content[0].text).toContain('browser.$("#unique-element")'); + expect(content[0].text).toContain("Successfully clicked element"); + expect(content[0].text).toContain("clicked-indicator show"); }); - it("should click element by complex CSS selector", async () => { + it("should return correct testplane code for clicked element", async () => { const result = await client.callTool({ name: "clickOnElement", arguments: { - selector: "button.success-btn", + queryType: "role", + queryValue: "button", + queryOptions: { name: "Submit Form" }, }, }); expect(result.isError).toBe(false); const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain( - 'Successfully clicked element found by CSS selector "button.success-btn"', - ); + expect(content[0].text).toContain('browser.getByRole("button"'); + expect(content[0].text).toContain("await element.click();"); }); }); - describe("error handling", () => { - it("should reject when both semantic query and selector are provided", async () => { - try { - await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "role", - queryValue: "button", - selector: "#some-button", - }, - }); - expect.fail("Expected the call to fail"); - } catch (error) { - expect(error).toBeDefined(); - } - }); - - it("should reject when neither semantic query nor selector is provided", async () => { - try { - await client.callTool({ - name: "clickOnElement", - arguments: {}, - }); - expect.fail("Expected the call to fail"); - } catch (error) { - expect(error).toBeDefined(); - } - }); - - it("should reject when queryType is provided without queryValue", async () => { - try { - await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "role", - }, - }); - expect.fail("Expected the call to fail"); - } catch (error) { - expect(error).toBeDefined(); - } - }); - - it("should handle element not found gracefully", async () => { + describe("error handling specific to clicking", () => { + it("should provide helpful error messages for clicking failures", async () => { const result = await client.callTool({ name: "clickOnElement", arguments: { @@ -380,34 +106,6 @@ describe( const content = result.content as Array<{ type: string; text: string }>; expect(content[0].text).toContain("Element not found"); }); - - it("should handle invalid CSS selector gracefully", async () => { - const result = await client.callTool({ - name: "clickOnElement", - arguments: { - selector: ".non-existent-class", - }, - }); - - expect(result.isError).toBe(true); - const content = result.content as Array<{ type: string; text: string }>; - expect(content[0].text).toContain("Error clicking element"); - }); - - it("should reject unsupported queryType", async () => { - try { - await client.callTool({ - name: "clickOnElement", - arguments: { - queryType: "invalidType" as "role", - queryValue: "button", - }, - }); - expect.fail("Expected the call to fail"); - } catch (error) { - expect(error).toBeDefined(); - } - }); }); }, INTEGRATION_TEST_TIMEOUT, diff --git a/test/tools/type-into-element.test.ts b/test/tools/type-into-element.test.ts new file mode 100644 index 0000000..f934055 --- /dev/null +++ b/test/tools/type-into-element.test.ts @@ -0,0 +1,114 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; +import { startClient } from "../utils"; +import { INTEGRATION_TEST_TIMEOUT } from "../constants"; +import { PlaygroundServer } from "../test-server"; + +describe( + "tools/typeIntoElement", + () => { + let client: Client; + let playgroundUrl: string; + let testServer: PlaygroundServer; + + beforeAll(async () => { + testServer = new PlaygroundServer(); + playgroundUrl = await testServer.start(); + }, 20000); + + afterAll(async () => { + if (testServer) { + await testServer.stop(); + } + }); + + beforeEach(async () => { + client = await startClient(); + await client.callTool({ name: "navigate", arguments: { url: playgroundUrl } }); + }); + + afterEach(async () => { + if (client) { + await client.close(); + } + }); + + describe("tool availability", () => { + it("should list typeIntoElement tool in available tools", async () => { + const tools = await client.listTools(); + + const typeIntoElementTool = tools.tools.find(tool => tool.name === "typeIntoElement"); + + expect(typeIntoElementTool).toBeDefined(); + }); + }); + + describe("typing functionality", () => { + it("should successfully type text into an element using semantic query", async () => { + const result = await client.callTool({ + name: "typeIntoElement", + arguments: { + queryType: "labelText", + queryValue: "Email Address", + text: "test@example.com", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + + expect(content[0].text).toContain('Successfully typed "test@example.com" into element'); + // TODO: once page snapshots will have info about form values, we can check that the form value is updated + }); + + it("should successfully type text into element using CSS selector", async () => { + const result = await client.callTool({ + name: "typeIntoElement", + arguments: { + selector: "#username", + text: "john_doe", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('Successfully typed "john_doe" into element'); + // TODO: once page snapshots will have info about form values, we can check that the form value is updated + }); + + it("should return correct testplane code for typed element", async () => { + const result = await client.callTool({ + name: "typeIntoElement", + arguments: { + queryType: "placeholderText", + queryValue: "Enter your name", + text: "John Smith", + }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain('browser.getByPlaceholderText("Enter your name"'); + expect(content[0].text).toContain('await element.setValue("John Smith");'); + }); + }); + + describe("error handling specific to typing", () => { + it("should provide helpful error messages for element not found", async () => { + const result = await client.callTool({ + name: "typeIntoElement", + arguments: { + queryType: "labelText", + queryValue: "Non-existent Field", + text: "test", + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain("Element not found"); + }); + }); + }, + INTEGRATION_TEST_TIMEOUT, +); diff --git a/test/tools/utils/element-selector.test.ts b/test/tools/utils/element-selector.test.ts new file mode 100644 index 0000000..865483f --- /dev/null +++ b/test/tools/utils/element-selector.test.ts @@ -0,0 +1,379 @@ +import { WdioBrowser } from "testplane"; +import { launchBrowser } from "testplane/unstable"; +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; + +import { findElement } from "../../../src/tools/utils/element-selector"; +import { PlaygroundServer } from "../../test-server"; + +describe("tools/utils/element-selector", () => { + let browser: WdioBrowser; + let playgroundUrl: string; + let testServer: PlaygroundServer; + + beforeAll(async () => { + testServer = new PlaygroundServer(); + playgroundUrl = await testServer.start(); + browser = await launchBrowser({ + headless: "new", + desiredCapabilities: { + "goog:chromeOptions": { + args: process.env.DISABLE_BROWSER_SANDBOX ? ["--no-sandbox", "--disable-dev-shm-usage"] : [], + }, + }, + }); + }, 20000); + + afterAll(async () => { + if (browser) { + await browser.deleteSession(); + } + if (testServer) { + await testServer.stop(); + } + }); + + beforeEach(async () => { + await browser.url(playgroundUrl); + }); + + describe("semantic queries", () => { + describe("getByRole queries", () => { + it("should find button by role with name", async () => { + const result = await findElement( + browser, + { + queryType: "role", + queryValue: "button", + queryOptions: { name: "Submit Form" }, + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('role "button" with name "Submit Form"'); + expect(result.testplaneCode).toContain('browser.getByRole("button"'); + expect(result.testplaneCode).toContain('{"name":"Submit Form"}'); + expect(result.testplaneCode).toContain("await element.click();"); + }); + + it("should find link by role with name", async () => { + const result = await findElement( + browser, + { + queryType: "role", + queryValue: "link", + queryOptions: { name: "Home" }, + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('role "link" with name "Home"'); + expect(result.testplaneCode).toContain('browser.getByRole("link"'); + }); + + it("should find heading by role with level", async () => { + const result = await findElement( + browser, + { + queryType: "role", + queryValue: "heading", + queryOptions: { level: 3, name: "Click this heading" }, + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('role "heading" with name "Click this heading"'); + expect(result.testplaneCode).toContain('browser.getByRole("heading"'); + expect(result.testplaneCode).toContain('"level":3'); + }); + + it("should handle role without options", async () => { + const result = await findElement( + browser, + { + queryType: "role", + queryValue: "button", + queryOptions: { name: "Submit Form" }, + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('role "button" with name "Submit Form"'); + expect(result.testplaneCode).toContain('browser.getByRole("button"'); + expect(result.testplaneCode).toContain('{"name":"Submit Form"}'); + }); + }); + + describe("getByText queries", () => { + it("should find element by exact text content", async () => { + const result = await findElement( + browser, + { + queryType: "text", + queryValue: "Click here to test text selection", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('text "Click here to test text selection"'); + expect(result.testplaneCode).toContain('browser.getByText("Click here to test text selection")'); + }); + + it("should find element by partial text with exact: false", async () => { + const result = await findElement( + browser, + { + queryType: "text", + queryValue: "Download", + queryOptions: { exact: false }, + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('text "Download"'); + expect(result.testplaneCode).toContain('browser.getByText("Download"'); + expect(result.testplaneCode).toContain('"exact":false'); + }); + }); + + describe("getByLabelText queries", () => { + it("should find input by label text", async () => { + const result = await findElement( + browser, + { + queryType: "labelText", + queryValue: "Email Address", + }, + "await element.setValue('test');", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('label text "Email Address"'); + expect(result.testplaneCode).toContain('browser.getByLabelText("Email Address")'); + }); + + it("should find textarea by label text", async () => { + const result = await findElement( + browser, + { + queryType: "labelText", + queryValue: "Message", + }, + "await element.setValue('test');", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('label text "Message"'); + expect(result.testplaneCode).toContain('browser.getByLabelText("Message")'); + }); + }); + + describe("getByPlaceholderText queries", () => { + it("should find input by placeholder text", async () => { + const result = await findElement( + browser, + { + queryType: "placeholderText", + queryValue: "Enter your name", + }, + "await element.setValue('test');", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('placeholder text "Enter your name"'); + expect(result.testplaneCode).toContain('browser.getByPlaceholderText("Enter your name")'); + }); + + it("should find textarea by placeholder text", async () => { + const result = await findElement( + browser, + { + queryType: "placeholderText", + queryValue: "Type your feedback here...", + }, + "await element.setValue('test');", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('placeholder text "Type your feedback here..."'); + expect(result.testplaneCode).toContain('browser.getByPlaceholderText("Type your feedback here...")'); + }); + }); + + describe("getByAltText queries", () => { + it("should find image by alt text", async () => { + const result = await findElement( + browser, + { + queryType: "altText", + queryValue: "Company Logo", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('alt text "Company Logo"'); + expect(result.testplaneCode).toContain('browser.getByAltText("Company Logo")'); + }); + + it("should find another image by alt text", async () => { + const result = await findElement( + browser, + { + queryType: "altText", + queryValue: "Success icon", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('alt text "Success icon"'); + expect(result.testplaneCode).toContain('browser.getByAltText("Success icon")'); + }); + }); + + describe("getByTestId queries", () => { + it("should find element by test id", async () => { + const result = await findElement( + browser, + { + queryType: "testId", + queryValue: "action-button", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('test ID "action-button"'); + expect(result.testplaneCode).toContain('browser.getByTestId("action-button")'); + }); + + it("should find container by test id", async () => { + const result = await findElement( + browser, + { + queryType: "testId", + queryValue: "widget-container", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('test ID "widget-container"'); + expect(result.testplaneCode).toContain('browser.getByTestId("widget-container")'); + }); + }); + }); + + describe("CSS selector fallback", () => { + it("should find element by CSS class selector", async () => { + const result = await findElement( + browser, + { + selector: ".custom-class-btn", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('CSS selector ".custom-class-btn"'); + expect(result.testplaneCode).toContain('browser.$(".custom-class-btn")'); + }); + + it("should find element by ID selector", async () => { + const result = await findElement( + browser, + { + selector: "#unique-element", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('CSS selector "#unique-element"'); + expect(result.testplaneCode).toContain('browser.$("#unique-element")'); + }); + + it("should find element by complex CSS selector", async () => { + const result = await findElement( + browser, + { + selector: "button.success-btn", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect(result.queryDescription).toBe('CSS selector "button.success-btn"'); + expect(result.testplaneCode).toContain('browser.$("button.success-btn")'); + }); + }); + + describe("error handling", () => { + it("should reject when both semantic query and selector are provided", async () => { + await expect( + findElement( + browser, + { + queryType: "role", + queryValue: "button", + selector: "#some-button", + }, + "await element.click();", + ), + ).rejects.toThrow("Provide EITHER semantic query"); + }); + + it("should reject when neither semantic query nor selector is provided", async () => { + await expect(findElement(browser, {}, "await element.click();")).rejects.toThrow( + "Provide either semantic query", + ); + }); + + it("should handle element not found gracefully", async () => { + await expect( + findElement( + browser, + { + queryType: "role", + queryValue: "button", + queryOptions: { name: "Non-existent Button" }, + }, + "await element.click();", + ), + ).rejects.toThrow("Unable to find an accessible element"); + }); + + it("should handle invalid CSS selector gracefully", async () => { + const result = await findElement( + browser, + { + selector: ".non-existent-class", + }, + "await element.click();", + ); + + expect(result.element).toBeDefined(); + expect((result.element as any).error).toBeDefined(); // eslint-disable-line @typescript-eslint/no-explicit-any + expect((result.element as any).error.error).toBe("no such element"); // eslint-disable-line @typescript-eslint/no-explicit-any + }); + + it("should reject unsupported queryType", async () => { + await expect( + findElement( + browser, + { + queryType: "invalidType" as "role", + queryValue: "button", + }, + "await element.click();", + ), + ).rejects.toThrow("Unsupported queryType"); + }); + }); +}); From 881cd78d46ceae334d06833bd31088c24ef97017 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 30 May 2025 02:24:05 +0300 Subject: [PATCH 3/4] docs: add typeIntoElement command to docs in README --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 43db5c9..d39b9f4 100644 --- a/README.md +++ b/README.md @@ -184,4 +184,44 @@ Click an element on the page using semantic queries (`testing-library`-style) or **Note:** Provide either semantic query parameters OR selector, not both. +### `typeIntoElement` +Type text into an input element on the page using semantic queries (`testing-library`-style) or CSS selectors. + +- Semantic Queries: + - **Parameters:** + - `queryType` (string, optional): Semantic query type. One of: + - `"role"` - Find by ARIA role (e.g., "textbox", "searchbox") + - `"text"` - Find by visible text content + - `"labelText"` - Find form inputs by their label text + - `"placeholderText"` - Find inputs by placeholder text + - `"altText"` - Find images by alt text + - `"testId"` - Find by data-testid attribute + - `"title"` - Find by title attribute + - `"displayValue"` - Find inputs by their current value + - `queryValue` (string, required when using queryType): The value to search for + - `text` (string, required): The text to type into the element + - `queryOptions` (object, optional): Additional options: + - `name` (string): Accessible name for role queries + - `exact` (boolean): Whether to match exact text (default: true) + - `hidden` (boolean): Include hidden elements (default: false) + +- CSS Selectors: + - **Parameters:** + - `selector` (string, optional): CSS selector or XPath when semantic queries cannot locate the element + - `text` (string, required): The text to type into the element + +**Examples:** +```javascript +// Semantic queries (preferred) +{ queryType: "labelText", queryValue: "Email Address", text: "test@example.com" } +{ queryType: "placeholderText", queryValue: "Enter your name", text: "John Smith" } +{ queryType: "role", queryValue: "textbox", queryOptions: { name: "Username" }, text: "john_doe" } + +// CSS selector fallback +{ selector: "#username", text: "john_doe" } +{ selector: "input[name='email']", text: "user@domain.com" } +``` + +**Note:** Provide either semantic query parameters OR selector, not both. + From 9dd6a829bce9b9711cbf350465ef90c3683e5f08 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 30 May 2025 02:26:49 +0300 Subject: [PATCH 4/4] fix: remove unnecessary file --- src/tools/element-selector.ts | 155 ---------------------------------- 1 file changed, 155 deletions(-) delete mode 100644 src/tools/element-selector.ts diff --git a/src/tools/element-selector.ts b/src/tools/element-selector.ts deleted file mode 100644 index 93ac835..0000000 --- a/src/tools/element-selector.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { z } from "zod"; -import { setupBrowser } from "@testing-library/webdriverio"; - -export const elementSelectorSchema = { - queryType: z - .enum(["role", "text", "labelText", "placeholderText", "displayValue", "altText", "title", "testId"]) - .optional() - .describe( - "Semantic query type (PREFERRED). Use this whenever possible for better accessibility and robustness.", - ), - - queryValue: z - .string() - .optional() - .describe("The value to search for with the specified queryType (e.g., 'button' for role, 'Submit' for text)."), - - queryOptions: z - .object({ - name: z - .string() - .optional() - .describe("Accessible name for role queries (e.g., getByRole('button', {name: 'Submit'}))"), - exact: z.boolean().optional().describe("Whether to match exact text (default: true)"), - hidden: z.boolean().optional().describe("Include elements hidden from accessibility tree (default: false)"), - level: z.number().optional().describe("Heading level for role='heading' (1-6)"), - }) - .optional() - .describe("Additional options for semantic queries"), - - selector: z - .string() - .optional() - .describe("CSS selector or XPath. Use only when semantic queries cannot locate the element."), -}; - -export interface ElementSelectorArgs { - queryType?: "role" | "text" | "labelText" | "placeholderText" | "displayValue" | "altText" | "title" | "testId"; - queryValue?: string; - queryOptions?: { - name?: string; - exact?: boolean; - hidden?: boolean; - level?: number; - }; - selector?: string; -} - -export interface ElementResult { - element: any; // eslint-disable-line @typescript-eslint/no-explicit-any - queryDescription: string; - testplaneCode: string; -} - -export async function findElement( - browser: any, // eslint-disable-line @typescript-eslint/no-explicit-any - args: ElementSelectorArgs, - actionType: "click" | "setValue", -): Promise { - const { queryType, queryValue, queryOptions, selector } = args; - - const hasSemanticQuery = queryType && queryValue; - const hasSelector = selector; - - if (!hasSemanticQuery && !hasSelector) { - throw new Error("Provide either semantic query (queryType + queryValue) or selector"); - } - - if (hasSemanticQuery && hasSelector) { - throw new Error( - "Provide EITHER semantic query (queryType + queryValue) OR selector, not both. Prefer semantic queries for better accessibility.", - ); - } - - let element; - let testplaneCode = ""; - let queryDescription = ""; - - if (queryType && queryValue) { - const { - getByRole, - getByText, - getByLabelText, - getByPlaceholderText, - getByDisplayValue, - getByAltText, - getByTitle, - getByTestId, - } = setupBrowser(browser as any); // eslint-disable-line @typescript-eslint/no-explicit-any - - const actionCode = actionType === "click" ? "await element.click();" : "await element.setValue(text);"; - - switch (queryType) { - case "role": - element = await getByRole(queryValue, queryOptions); - queryDescription = `role "${queryValue}"${queryOptions?.name ? ` with name "${queryOptions.name}"` : ""}`; - testplaneCode = `const element = await browser.getByRole("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "text": - element = await getByText(queryValue, queryOptions); - queryDescription = `text "${queryValue}"`; - testplaneCode = `const element = await browser.getByText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "labelText": - element = await getByLabelText(queryValue, queryOptions); - queryDescription = `label text "${queryValue}"`; - testplaneCode = `const element = await browser.getByLabelText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "placeholderText": - element = await getByPlaceholderText(queryValue, queryOptions); - queryDescription = `placeholder text "${queryValue}"`; - testplaneCode = `const element = await browser.getByPlaceholderText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "displayValue": - element = await getByDisplayValue(queryValue, queryOptions); - queryDescription = `display value "${queryValue}"`; - testplaneCode = `const element = await browser.getByDisplayValue("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "altText": - element = await getByAltText(queryValue, queryOptions); - queryDescription = `alt text "${queryValue}"`; - testplaneCode = `const element = await browser.getByAltText("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "title": - element = await getByTitle(queryValue, queryOptions); - queryDescription = `title "${queryValue}"`; - testplaneCode = `const element = await browser.getByTitle("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - case "testId": - element = await getByTestId(queryValue, queryOptions); - queryDescription = `test ID "${queryValue}"`; - testplaneCode = `const element = await browser.getByTestId("${queryValue}"${queryOptions ? `, ${JSON.stringify(queryOptions)}` : ""});\n${actionCode}`; - break; - default: - throw new Error(`Unsupported queryType: ${queryType}`); - } - testplaneCode += `\nIMPORTANT for AI agents: if user asks to write tests, prefer to use semantic queries over CSS selectors, if user tests also use testing-library queries.`; - } else if (selector) { - element = await browser.$(selector); - queryDescription = `CSS selector "${selector}"`; - const actionCode = actionType === "click" ? "await element.click();" : "await element.setValue(text);"; - testplaneCode = `const element = await browser.$("${selector}");\n${actionCode}`; - } else { - throw new Error("No valid element selection method provided"); - } - - if (!element) { - throw new Error("Element not found"); - } - - return { - element, - queryDescription, - testplaneCode, - }; -}