From e840b891127e6f28892ca6a0709c26bf4d617b6b Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Tue, 7 Oct 2025 10:13:33 +0300 Subject: [PATCH 1/6] WAT-5214 --- package-lock.json | 457 ++++++++++-------- .../static-fixtures/shadow-click.html | 242 ++++++++++ packages/e2e-test-app/test/selenium/config.js | 5 +- .../test/selenium/test/shadow-click.spec.js | 55 +++ packages/plugin-selenium-driver/package.json | 2 +- 5 files changed, 566 insertions(+), 195 deletions(-) create mode 100644 packages/e2e-test-app/static-fixtures/shadow-click.html create mode 100644 packages/e2e-test-app/test/selenium/test/shadow-click.spec.js diff --git a/package-lock.json b/package-lock.json index 87ec5b062..08d23df1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5994,42 +5994,51 @@ "license": "ISC" }, "node_modules/@wdio/config": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.2.5.tgz", - "integrity": "sha512-gqblHShjriGH3va2nSnQ2wktwargHK2oEDepNPG1LMPB5uWd997f+zyaR4s4vtqqUkdxwciIA1H4rDC3fIlesw==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.20.0.tgz", + "integrity": "sha512-ggwd3EMsVj/LTcbYw2h+hma+/7fQ1cTXMuy9B5WTkLjDlOtbLjsqs9QLt4BLIo1cdsxvAw/UVpRVUuYy7rTmtQ==", "license": "MIT", "dependencies": { - "@wdio/logger": "9.1.3", - "@wdio/types": "9.2.2", - "@wdio/utils": "9.2.5", - "decamelize": "^6.0.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.20.0", + "@wdio/utils": "9.20.0", "deepmerge-ts": "^7.0.3", "glob": "^10.2.2", - "import-meta-resolve": "^4.0.0" + "import-meta-resolve": "^4.0.0", + "jiti": "^2.5.1" }, "engines": { "node": ">=18.20.0" } }, - "node_modules/@wdio/config/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@wdio/config/node_modules/@types/node": { + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "undici-types": "~6.21.0" } }, - "node_modules/@wdio/config/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "node_modules/@wdio/config/node_modules/@wdio/types": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.20.0.tgz", + "integrity": "sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==", "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "dependencies": { + "@types/node": "^20.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/@wdio/config/node_modules/glob": { @@ -6104,15 +6113,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@wdio/config/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/@wdio/logger": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.1.3.tgz", - "integrity": "sha512-cumRMK/gE1uedBUw3WmWXOQ7HtB6DR8EyKQioUz2P0IJtRRpglMBdZV7Svr3b++WWawOuzZHMfbTkJQmaVt8Gw==", + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", "license": "MIT", "dependencies": { "chalk": "^5.1.2", "loglevel": "^1.6.0", "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", "strip-ansi": "^7.1.0" }, "engines": { @@ -6120,9 +6136,9 @@ } }, "node_modules/@wdio/logger/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -6132,9 +6148,9 @@ } }, "node_modules/@wdio/logger/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -6144,9 +6160,9 @@ } }, "node_modules/@wdio/logger/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==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -6159,15 +6175,15 @@ } }, "node_modules/@wdio/protocols": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.2.2.tgz", - "integrity": "sha512-0GMUSHCbYm+J+rnRU6XPtaUgVCRICsiH6W5zCXpePm3wLlbmg/mvZ+4OnNErssbpIOulZuAmC2jNmut2AEfWSw==", + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.16.2.tgz", + "integrity": "sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==", "license": "MIT" }, "node_modules/@wdio/repl": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", - "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", "license": "MIT", "dependencies": { "@types/node": "^20.1.0" @@ -6177,14 +6193,20 @@ } }, "node_modules/@wdio/repl/node_modules/@types/node": { - "version": "20.17.51", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.51.tgz", - "integrity": "sha512-hccptBl7C8lHiKxTBsY6vYYmqpmw1E/aGR/8fmueE+B390L3pdMOpNSRvFO4ZnXzW5+p2HBXV0yNABd2vdk22Q==", + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, + "node_modules/@wdio/repl/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/@wdio/types": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.2.2.tgz", @@ -6207,22 +6229,23 @@ } }, "node_modules/@wdio/utils": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.2.5.tgz", - "integrity": "sha512-QgBxscPVuqC/fP62ggssBfWLnonVs2/r1xaj5MgH+tLNez/OVS98pWx0/FYsFHVeTVO5vPnJIhoTo2CXz8tQzA==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.20.0.tgz", + "integrity": "sha512-T1ze005kncUTocYImSBQc/FAVcOwP/vOU4MDJFgzz/RTcps600qcKX98sVdWM5/ukXCVkjOufWteDHIbX5/tEA==", "license": "MIT", "dependencies": { "@puppeteer/browsers": "^2.2.0", - "@wdio/logger": "9.1.3", - "@wdio/types": "9.2.2", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.20.0", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", - "edgedriver": "^5.6.1", - "geckodriver": "^4.3.3", + "edgedriver": "^6.1.2", + "geckodriver": "^5.0.0", "get-port": "^7.0.0", "import-meta-resolve": "^4.0.0", "locate-app": "^2.2.24", - "safaridriver": "^0.1.2", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", "split2": "^4.2.0", "wait-port": "^1.1.0" }, @@ -6230,10 +6253,31 @@ "node": ">=18.20.0" } }, + "node_modules/@wdio/utils/node_modules/@types/node": { + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/utils/node_modules/@wdio/types": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.20.0.tgz", + "integrity": "sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==", + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, "node_modules/@wdio/utils/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -6263,6 +6307,12 @@ "node": ">= 10.x" } }, + "node_modules/@wdio/utils/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6538,14 +6588,14 @@ "license": "BSD-3-Clause" }, "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==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.7.tgz", + "integrity": "sha512-8daf29EMM3gUpH/vSBSCYo2bY/wbamgRPxPpE2b+cDnbOLBHAcZikWad79R4Guemth/qtipzEHrZMq1lFXxWIA==", "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", - "node": ">=16.5.0" + "node": ">=18.0.0" } }, "node_modules/@zkochan/js-yaml": { @@ -10705,61 +10755,27 @@ } }, "node_modules/edgedriver": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.6.1.tgz", - "integrity": "sha512-3Ve9cd5ziLByUdigw6zovVeWJjVs8QHVmqOB0sJ0WNeVPcwf4p18GnxMmVvlFmYRloUwf5suNuorea4QzwBIOA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.1.2.tgz", + "integrity": "sha512-UvFqd/IR81iPyWMcxXbUNi+xKWR7JjfoHjfuwjqsj9UHQKn80RpQmS0jf+U25IPi+gKVPcpOSKm0XkqgGMq4zQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@wdio/logger": "^8.38.0", - "@zip.js/zip.js": "^2.7.48", + "@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.4.1", + "fast-xml-parser": "^5.0.8", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", "node-fetch": "^3.3.2", - "which": "^4.0.0" + "which": "^5.0.0" }, "bin": { "edgedriver": "bin/edgedriver.js" - } - }, - "node_modules/edgedriver/node_modules/@wdio/logger": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", - "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", - "license": "MIT", - "dependencies": { - "chalk": "^5.1.2", - "loglevel": "^1.6.0", - "loglevel-plugin-prefix": "^0.8.4", - "strip-ansi": "^7.1.0" }, "engines": { - "node": "^16.13 || >=18" - } - }, - "node_modules/edgedriver/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/edgedriver/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/edgedriver/node_modules/data-uri-to-buffer": { @@ -10772,9 +10788,9 @@ } }, "node_modules/edgedriver/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -10810,25 +10826,10 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/edgedriver/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==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/edgedriver/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -10837,7 +10838,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/ee-first": { @@ -12203,9 +12204,9 @@ "license": "BSD-3-Clause" }, "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==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.0.tgz", + "integrity": "sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==", "funding": [ { "type": "github", @@ -12214,7 +12215,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^1.1.1" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -12878,26 +12879,26 @@ } }, "node_modules/geckodriver": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.5.1.tgz", - "integrity": "sha512-lGCRqPMuzbRNDWJOQcUqhNqPvNsIFu6yzXF8J/6K3WCYFd2r5ckbeF7h1cxsnjA7YLSEiWzERCt6/gjZ3tW0ug==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-5.0.0.tgz", + "integrity": "sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==", "hasInstallScript": true, - "license": "MPL-2.0", + "license": "MIT", "dependencies": { - "@wdio/logger": "^9.0.0", - "@zip.js/zip.js": "^2.7.48", + "@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": "^4.0.0" + "which": "^5.0.0" }, "bin": { "geckodriver": "bin/geckodriver.js" }, "engines": { - "node": "^16.13 || >=18 || >=20" + "node": ">=18.0.0" } }, "node_modules/geckodriver/node_modules/data-uri-to-buffer": { @@ -12910,9 +12911,9 @@ } }, "node_modules/geckodriver/node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -12949,9 +12950,9 @@ } }, "node_modules/geckodriver/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -12960,7 +12961,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/gensync": { @@ -13718,9 +13719,9 @@ "license": "MIT" }, "node_modules/htmlfy": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.3.2.tgz", - "integrity": "sha512-FsxzfpeDYRqn1emox9VpxMPfGjADoUmmup8D604q497R0VNxiXs4ZZTN2QzkaMA5C9aHGUoe1iQRVSm+HK9xuA==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", "license": "MIT" }, "node_modules/htmlparser2": { @@ -13970,9 +13971,9 @@ } }, "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", "license": "MIT", "funding": { "type": "github", @@ -15115,6 +15116,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -21224,6 +21234,15 @@ "dev": true, "license": "ISC" }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -21319,10 +21338,13 @@ } }, "node_modules/safaridriver": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", - "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.0.tgz", + "integrity": "sha512-J92IFbskyo7OYB3Dt4aTdyhag1GlInrfbPCmMteb7aBK7PwlnGz1HI0+oyNN97j7pV9DqUAVoVgkNRMrfY47mQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/safe-array-concat": { "version": "1.1.3", @@ -21399,6 +21421,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -21591,27 +21632,27 @@ } }, "node_modules/serialize-error": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", "license": "MIT", "dependencies": { - "type-fest": "^2.12.2" + "type-fest": "^4.31.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/serialize-error/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=12.20" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -22717,9 +22758,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", "funding": [ { "type": "github", @@ -24042,19 +24083,21 @@ } }, "node_modules/webdriver": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.5.tgz", - "integrity": "sha512-7rmK6oD3oYq+6E0qa9FQ1/67Ajf2APTOghmqlcDnSBTnTGF51UPOWnzgy0x3yGozmCTVRe13O75JXRRWpvxOvA==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.20.0.tgz", + "integrity": "sha512-Kk+AGV1xWLNHVpzUynQJDULMzbcO3IjXo3s0BzfC30OpGxhpaNmoazMQodhtv0Lp242Mb1VYXD89dCb4oAHc4w==", "license": "MIT", "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "9.2.5", - "@wdio/logger": "9.1.3", - "@wdio/protocols": "9.2.2", - "@wdio/types": "9.2.2", - "@wdio/utils": "9.2.5", + "@wdio/config": "9.20.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/types": "9.20.0", + "@wdio/utils": "9.20.0", "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", "ws": "^8.8.0" }, "engines": { @@ -24062,53 +24105,69 @@ } }, "node_modules/webdriver/node_modules/@types/node": { - "version": "20.17.51", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.51.tgz", - "integrity": "sha512-hccptBl7C8lHiKxTBsY6vYYmqpmw1E/aGR/8fmueE+B390L3pdMOpNSRvFO4ZnXzW5+p2HBXV0yNABd2vdk22Q==", + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriver/node_modules/@wdio/types": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.20.0.tgz", + "integrity": "sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==", + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" } }, + "node_modules/webdriver/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/webdriverio": { - "version": "9.2.6", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.6.tgz", - "integrity": "sha512-GjQykwwYBXwqWAs0CUCU0Hg9nR5DonOwFkWQUcUpCfBVSTLdeH0fBEDikKVf7ohuDOUSOGo0wM9LN8ZTMroMXA==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.20.0.tgz", + "integrity": "sha512-cqaXfahTzCFaQLlk++feZaze6tAsW8OSdaVRgmOGJRII1z2A4uh4YGHtusTpqOiZAST7OBPqycOwfh01G/Ktbg==", "license": "MIT", "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.2.5", - "@wdio/logger": "9.1.3", - "@wdio/protocols": "9.2.2", - "@wdio/repl": "9.0.8", - "@wdio/types": "9.2.2", - "@wdio/utils": "9.2.5", + "@wdio/config": "9.20.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.20.0", + "@wdio/utils": "9.20.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.3.0", - "import-meta-resolve": "^4.0.0", + "htmlfy": "^0.8.1", "is-plain-obj": "^4.1.0", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", "lodash.zip": "^4.2.0", - "minimatch": "^9.0.3", "query-selector-shadow-dom": "^1.0.1", "resq": "^1.11.0", "rgb2hex": "0.2.5", - "serialize-error": "^11.0.3", + "serialize-error": "^12.0.0", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.2.5" + "webdriver": "9.20.0" }, "engines": { "node": ">=18.20.0" }, "peerDependencies": { - "puppeteer-core": "^22.3.0" + "puppeteer-core": ">=22.x || <=24.x" }, "peerDependenciesMeta": { "puppeteer-core": { @@ -24125,6 +24184,18 @@ "undici-types": "~6.19.2" } }, + "node_modules/webdriverio/node_modules/@wdio/types": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.20.0.tgz", + "integrity": "sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==", + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, "node_modules/webdriverio/node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -25545,7 +25616,7 @@ "deepmerge": "4.3.1", "puppeteer-core": "22.3.0", "selenium-server": "3.141.59", - "webdriverio": "9.2.6" + "webdriverio": "9.20.0" } }, "packages/test-utils": { diff --git a/packages/e2e-test-app/static-fixtures/shadow-click.html b/packages/e2e-test-app/static-fixtures/shadow-click.html new file mode 100644 index 000000000..83df6eede --- /dev/null +++ b/packages/e2e-test-app/static-fixtures/shadow-click.html @@ -0,0 +1,242 @@ + + + + + Shadow DOM Click Test + + + +

Shadow DOM Click Test

+ + +
+

Regular DOM

+ +
Regular button clicked!
+
+ + +
+

Shadow DOM Level 1

+
+ +
+
Shadow button clicked!
+
+ + +
+

Nested Shadow DOM

+
+ +
+
Nested shadow button clicked!
+
+ + + + diff --git a/packages/e2e-test-app/test/selenium/config.js b/packages/e2e-test-app/test/selenium/config.js index 1cd90e7b5..c0e6ec8c1 100644 --- a/packages/e2e-test-app/test/selenium/config.js +++ b/packages/e2e-test-app/test/selenium/config.js @@ -40,7 +40,7 @@ module.exports = async (config) => { screenshots: 'disable', retryCount: 0, testTimeout: local ? 0 : config.testTimeout, - tests: 'test/selenium/test/**/set-custom-config.spec.js', + tests: 'test/selenium/test/**/shadow-click.spec.js', plugins: [ [ 'selenium-driver', @@ -48,17 +48,20 @@ module.exports = async (config) => { clientTimeout: local ? 0 : config.testTimeout, path: '/wd/hub', chromeDriverPath: chromedriver.executablePath, + localVersion: 'v4', capabilities: local ? { 'goog:chromeOptions': { binary: chrome.executablePath, }, + 'wdio:enforceWebDriverClassic': false, } : { 'goog:chromeOptions': { binary: chrome.executablePath, args: ['--headless=new', '--no-sandbox'], }, + 'wdio:enforceWebDriverClassic': false, }, }, ], diff --git a/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js b/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js new file mode 100644 index 000000000..1a3ff382b --- /dev/null +++ b/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js @@ -0,0 +1,55 @@ +import {run} from 'testring'; +import {getTargetUrl} from './utils'; + +/** + * Test for shadow DOM element interaction using the new shadow$ concept. + * + * The shadow$ property provides a clean API for accessing elements within shadow DOM: + * - app.root.shadowHost - path to shadow host element + * - shadow$.elementName - access to elements inside the shadow DOM + * - For nested shadow DOM, shadow$ can be chained: shadow$.outer.shadow$.inner.element + * + * This concept is not yet implemented in the test framework but serves as a specification + * for the desired API design. + */ +run(async (api) => { + let app = api.application; + await app.url(getTargetUrl(api, 'shadow-click.html')); + + + // Test 1: Click regular DOM button and verify result appears + // Regular DOM elements are accessed through standard property chain + await app.click(app.root.regularSection.regularButton); + + const regularResultText = await app.getText(app.root.regularSection.regularResult); + await app.assert.equal(regularResultText, 'Regular button clicked successfully!'); + + // Test 2: Click shadow DOM button using new shadow$ concept + // shadow$ property provides access to elements inside shadow DOM + // app.root.shadowHost - path to root shadow element + // shadow$.shadowButton - path inside shadow DOM + await app.click(app.root.shadowHost.shadow$.shadowButton); + + const shadowResultText = await app.getText(app.root.shadowSection.shadowResult); + await app.assert.equal(shadowResultText, 'Shadow button clicked successfully!'); + + // Test 3: Click nested shadow DOM button using new shadow$ concept + // For nested shadow DOM, shadow$ can be chained to access deeper shadow elements + // app.root.nestedShadowHost - path to nested shadow host element + // shadow$.outerShadowContent - access to outer shadow content + // shadow$.innerShadowHost - access to inner shadow host within outer shadow + // shadow$.nestedShadowButton - access to button within nested shadow DOM + await app.click(app.root.nestedShadowHost.shadow$.outerShadowContent.shadow$.innerShadowHost.shadow$.nestedShadowButton); + + const nestedShadowResultText = await app.getText(app.root.nestedShadowSection.nestedShadowResult); + await app.assert.equal(nestedShadowResultText, 'Nested shadow button clicked successfully!'); + + // Verify all results are visible + const regularResultVisible = await app.isVisible(app.root.regularSection.regularResult); + const shadowResultVisible = await app.isVisible(app.root.shadowSection.shadowResult); + const nestedShadowResultVisible = await app.isVisible(app.root.nestedShadowSection.nestedShadowResult); + + await app.assert.equal(regularResultVisible, true, 'Regular result should be visible'); + await app.assert.equal(shadowResultVisible, true, 'Shadow result should be visible'); + await app.assert.equal(nestedShadowResultVisible, true, 'Nested shadow result should be visible'); +}); \ No newline at end of file diff --git a/packages/plugin-selenium-driver/package.json b/packages/plugin-selenium-driver/package.json index f990c36b0..b3cf55ff9 100644 --- a/packages/plugin-selenium-driver/package.json +++ b/packages/plugin-selenium-driver/package.json @@ -26,6 +26,6 @@ "deepmerge": "4.3.1", "puppeteer-core": "22.3.0", "selenium-server": "3.141.59", - "webdriverio": "9.2.6" + "webdriverio": "9.20.0" } } From 6e8497d830d0ee5763961f95b0575dac6c9ec56e Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 8 Oct 2025 14:58:44 +0300 Subject: [PATCH 2/6] WAT-5214 --- core/types/src/browser-proxy/index.ts | 78 ++-- packages/e2e-test-app/test/selenium/config.js | 4 +- .../test/selenium/test/shadow-click.spec.js | 2 +- .../element-path/src/create-element-path.ts | 2 + packages/element-path/src/element-path.ts | 2 +- packages/element-path/src/index.ts | 1 + packages/element-path/src/proxify.ts | 6 + .../element-path/src/shadow-element-path.ts | 129 ++++++ .../css-basic-selectors.spec.ts | 292 ++++++++++++ .../src/plugin/index.ts | 418 +++++++++--------- .../web-application/src/web-application.ts | 248 ++++++----- packages/web-application/src/web-client.ts | 151 +++---- 12 files changed, 917 insertions(+), 416 deletions(-) create mode 100644 packages/element-path/src/shadow-element-path.ts create mode 100644 packages/element-path/test/shadow-css-selector/css-basic-selectors.spec.ts diff --git a/core/types/src/browser-proxy/index.ts b/core/types/src/browser-proxy/index.ts index f860a30fd..8ebc316da 100644 --- a/core/types/src/browser-proxy/index.ts +++ b/core/types/src/browser-proxy/index.ts @@ -1,6 +1,28 @@ import {IBrowserProxyCommand} from './structs'; import {WindowFeaturesConfig} from '../web-application'; +export type XpathSelector = { + type: 'xpath'; + xpath: string; +}; + +export type ShadowCssSelector = { + type: 'shadow-css'; + css: string; + parentSelectors: string[]; + isShadowElement: true; +}; + +export type Selector = + | XpathSelector + | ShadowCssSelector; + +export const isXpathSelector = (selector: Selector): selector is XpathSelector => + selector.type === 'xpath'; + +export const isShadowCssSelector = (selector: Selector): selector is ShadowCssSelector => + selector.type === 'shadow-css'; + export interface IBrowserProxyController { init(): Promise; @@ -24,7 +46,7 @@ export interface IBrowserProxyPlugin { refresh(applicant: string): Promise; - click(applicant: string, selector: string, options?: any): Promise; + click(applicant: string, selector: Selector, options?: any): Promise; url(applicant: string, val: string): Promise; @@ -37,21 +59,21 @@ export interface IBrowserProxyPlugin { waitForExist( applicant: string, - xpath: string, + selector: Selector, timeout: number, ): Promise; waitForVisible( applicant: string, - xpath: string, + selector: Selector, timeout: number, ): Promise; - isVisible(applicant: string, xpath: string): Promise; + isVisible(applicant: string, selector: Selector): Promise; moveToObject( applicant: string, - xpath: string, + selector: Selector, x: number, y: number, ): Promise; @@ -66,44 +88,44 @@ export interface IBrowserProxyPlugin { getTitle(applicant: string): Promise; - clearValue(applicant: string, xpath: string): Promise; + clearValue(applicant: string, selector: Selector): Promise; keys(applicant: string, value: any): Promise; elementIdText(applicant: string, elementId: string): Promise; - elements(applicant: string, xpath: string): Promise; + elements(applicant: string, selector: Selector): Promise; - getValue(applicant: string, xpath: string): Promise; + getValue(applicant: string, selector: Selector): Promise; - setValue(applicant: string, xpath: string, value: any): Promise; + setValue(applicant: string, selector: Selector, value: any): Promise; - selectByIndex(applicant: string, xpath: string, value: any): Promise; + selectByIndex(applicant: string, selector: Selector, value: any): Promise; - selectByValue(applicant: string, xpath: string, value: any): Promise; + selectByValue(applicant: string, selector: Selector, value: any): Promise; selectByVisibleText( applicant: string, - xpath: string, + selector: Selector, str: string, ): Promise; - getAttribute(applicant: string, xpath: string, attr: any): Promise; + getAttribute(applicant: string, selector: Selector, attr: any): Promise; windowHandleMaximize(applicant: string): Promise; - isEnabled(applicant: string, xpath: string): Promise; + isEnabled(applicant: string, selector: Selector): Promise; scroll( applicant: string, - xpath: string, + selector: Selector, x: number, y: number, ): Promise; scrollIntoView( applicant: string, - xpath: string, + selector: Selector, scrollIntoViewOptions?: boolean, ): Promise; @@ -117,8 +139,8 @@ export interface IBrowserProxyPlugin { dragAndDrop( applicant: string, - xpathSource: string, - xpathDestination: string, + sourceSelector: Selector, + destinationSelector: Selector, ): Promise; setCookie(applicant: string, cookieName: any): Promise; @@ -127,9 +149,9 @@ export interface IBrowserProxyPlugin { deleteCookie(applicant: string, cookieName: string): Promise; - getHTML(applicant: string, xpath: string, b: any): Promise; + getHTML(applicant: string, selector: Selector, b: any): Promise; - getSize(applicant: string, xpath: string): Promise; + getSize(applicant: string, selector: Selector): Promise; getCurrentTabId(applicant: string): Promise; @@ -143,11 +165,11 @@ export interface IBrowserProxyPlugin { windowHandles(applicant: string): Promise; - getTagName(applicant: string, xpath: string): Promise; + getTagName(applicant: string, selector: Selector): Promise; - isSelected(applicant: string, xpath: string): Promise; + isSelected(applicant: string, selector: Selector): Promise; - getText(applicant: string, xpath: string): Promise; + getText(applicant: string, selector: Selector): Promise; elementIdSelected(applicant: string, id: string): Promise; @@ -157,24 +179,24 @@ export interface IBrowserProxyPlugin { getCssProperty( applicant: string, - xpath: string, + selector: Selector, cssProperty: string, ): Promise; getSource(applicant: string): Promise; - isExisting(applicant: string, xpath: string): Promise; + isExisting(applicant: string, selector: Selector): Promise; waitForValue( applicant: string, - xpath: string, + selector: Selector, timeout: number, reverse: boolean, ): Promise; waitForSelected( applicant: string, - xpath: string, + selector: Selector, timeout: number, reverse: boolean, ): Promise; @@ -189,7 +211,7 @@ export interface IBrowserProxyPlugin { selectByAttribute( applicant: string, - xpath: string, + selector: Selector, attribute: string, value: string, ): Promise; diff --git a/packages/e2e-test-app/test/selenium/config.js b/packages/e2e-test-app/test/selenium/config.js index c0e6ec8c1..306f89496 100644 --- a/packages/e2e-test-app/test/selenium/config.js +++ b/packages/e2e-test-app/test/selenium/config.js @@ -40,7 +40,7 @@ module.exports = async (config) => { screenshots: 'disable', retryCount: 0, testTimeout: local ? 0 : config.testTimeout, - tests: 'test/selenium/test/**/shadow-click.spec.js', + tests: 'test/selenium/test/**/*.spec.js', plugins: [ [ 'selenium-driver', @@ -54,14 +54,12 @@ module.exports = async (config) => { 'goog:chromeOptions': { binary: chrome.executablePath, }, - 'wdio:enforceWebDriverClassic': false, } : { 'goog:chromeOptions': { binary: chrome.executablePath, args: ['--headless=new', '--no-sandbox'], }, - 'wdio:enforceWebDriverClassic': false, }, }, ], diff --git a/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js b/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js index 1a3ff382b..916c9f9d3 100644 --- a/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js +++ b/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js @@ -39,7 +39,7 @@ run(async (api) => { // shadow$.outerShadowContent - access to outer shadow content // shadow$.innerShadowHost - access to inner shadow host within outer shadow // shadow$.nestedShadowButton - access to button within nested shadow DOM - await app.click(app.root.nestedShadowHost.shadow$.outerShadowContent.shadow$.innerShadowHost.shadow$.nestedShadowButton); + await app.click(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowButton); const nestedShadowResultText = await app.getText(app.root.nestedShadowSection.nestedShadowResult); await app.assert.equal(nestedShadowResultText, 'Nested shadow button clicked successfully!'); diff --git a/packages/element-path/src/create-element-path.ts b/packages/element-path/src/create-element-path.ts index 417db0c15..a1c9a190e 100644 --- a/packages/element-path/src/create-element-path.ts +++ b/packages/element-path/src/create-element-path.ts @@ -1,5 +1,6 @@ import {proxify, XpathLocatorProxified} from './proxify'; import {ElementPath, FlowsObject} from './element-path'; +import {ShadowElementPathProxy} from './shadow-element-path'; export type createElementPathOptions = { flows?: FlowsObject; @@ -15,6 +16,7 @@ export type ElementPathProxy = ElementPath & { __findChildren: (options: any) => ElementPathProxy; __getReversedChain: ElementPath['getReversedChain']; __getChildType: ElementPath['getElementType']; + shadow$: ShadowElementPathProxy; } & { [key: string]: ElementPathProxy; }; diff --git a/packages/element-path/src/element-path.ts b/packages/element-path/src/element-path.ts index 3d3846d08..24d8f0c33 100644 --- a/packages/element-path/src/element-path.ts +++ b/packages/element-path/src/element-path.ts @@ -233,7 +233,7 @@ export class ElementPath { /* Search get xpath mask */ - protected getAttributeName(): string { + public getAttributeName(): string { return this.attributeName; } diff --git a/packages/element-path/src/index.ts b/packages/element-path/src/index.ts index 0686ca3c2..812bb229a 100644 --- a/packages/element-path/src/index.ts +++ b/packages/element-path/src/index.ts @@ -1,2 +1,3 @@ export {ElementPath} from './element-path'; export {createElementPath, ElementPathProxy} from './create-element-path'; +export {createShadowElementPathProxy, ShadowElementPathProxy} from './shadow-element-path'; diff --git a/packages/element-path/src/proxify.ts b/packages/element-path/src/proxify.ts index 9d942f5aa..91b472053 100644 --- a/packages/element-path/src/proxify.ts +++ b/packages/element-path/src/proxify.ts @@ -1,6 +1,7 @@ /* eslint-disable prefer-spread,prefer-rest-params */ import {hasOwn, isGenKeyType} from './utils'; import {ElementPath, SearchObject} from './element-path'; +import {createShadowElementPathProxy} from './shadow-element-path'; type KeyType = string | number | symbol; @@ -29,6 +30,7 @@ const PROXY_PROPS = [ '__flows', '__searchOptions', '__proxy', + 'shadow$', ]; export function proxify(instance: ElementPath, strictMode = true) { @@ -129,6 +131,10 @@ export function proxify(instance: ElementPath, strictMode = true) { return receiver; } + if (key === 'shadow$') { + return createShadowElementPathProxy([target.toString()], target.getAttributeName()); + } + if (key === '__getInstance') { return function __getInstance(this: ElementPath) { if (this === receiver) { diff --git a/packages/element-path/src/shadow-element-path.ts b/packages/element-path/src/shadow-element-path.ts new file mode 100644 index 000000000..4a43c6b86 --- /dev/null +++ b/packages/element-path/src/shadow-element-path.ts @@ -0,0 +1,129 @@ + +export interface ShadowElementPathProxy { + toShadowCSSSelector(): string; + toString(): string; + toFormattedString(): string; + isShadowElement: true; + and(selector: string): ShadowElementPathProxy; + then(selector: string): ShadowElementPathProxy; + getParentSelectors(): string[]; + shadow$: ShadowElementPathProxy; + [key: string]: any; + [key: symbol]: any; +} + +/** + * Regex patterns for CSS selector pattern matching + * - '*' - all elements with the specified attribute + * - '*foo' - attribute ends with foo + * - 'foo*' - attribute starts with foo + * - '*foo*' - attribute contains foo + * - 'foo' - exact foo value + */ +export const CSS_SELECTOR_PATTERNS = { + // Wildcard pattern: '*' + WILDCARD: /^\*$/, + + // Suffix pattern: '*foo' (starts with * but doesn't end with *) + SUFFIX: /^\*[^*]+$/, + + // Prefix pattern: 'foo*' (ends with * but doesn't start with *) + PREFIX: /^[^*]+\*$/, + + // Contains pattern: '*foo*' (starts and ends with *, length > 2) + CONTAINS: /^\*[^*]+\*$/ +} as const; + +function propertyToCSSSelector(prop: string, attributeName: string): string { + // Handle escaped asterisks: foo\\*bar -> foo*bar + if (prop.includes('\\*')) { + return `[${attributeName}="${prop.replace(/\\\*/g, '*')}"]`; + } + + // Handle wildcard + if (CSS_SELECTOR_PATTERNS.WILDCARD.test(prop)) { + return `[${attributeName}]`; + } + + // Handle suffix pattern: *foo + if (CSS_SELECTOR_PATTERNS.SUFFIX.test(prop)) { + const value = prop.substring(1); + return `[${attributeName}$="${value}"]`; + } + + // Handle prefix pattern: foo* + if (CSS_SELECTOR_PATTERNS.PREFIX.test(prop)) { + const value = prop.substring(0, prop.length - 1); + return `[${attributeName}^="${value}"]`; + } + + // Handle contains pattern: *foo* + if (CSS_SELECTOR_PATTERNS.CONTAINS.test(prop)) { + const value = prop.substring(1, prop.length - 1); + return `[${attributeName}*="${value}"]`; + } + + // Handle exact match + return `[${attributeName}="${prop}"]`; +} + +export function createShadowElementPathProxy(parentSelectors: string[], attributeName: string): ShadowElementPathProxy { + const cssParts: string[] = []; + + const proxy = new Proxy(Object.create(null), { + get(_t, prop: string | symbol) { + if (prop === 'toShadowCSSSelector') { + return () => cssParts.join(' '); + } + if (prop === 'toString' || prop === 'toFormattedString') { + return () => parentSelectors[0]; + } + if (prop === 'isShadowElement') { + return true; + } + // getParentSelectors + if (prop === 'getParentSelectors') { + return () => parentSelectors; + } + // handle shadow$ + if (prop === 'shadow$') { + const newParentSelectors = parentSelectors.concat(cssParts.join(' ')); + return createShadowElementPathProxy(newParentSelectors, attributeName); + } + // handle and + if (prop === 'and') { + return (selector: string) => { + if (cssParts.length > 0) { + cssParts[cssParts.length - 1] += selector; + } + return proxy; + }; + } + if (prop === 'then') { + return (selector: string) => { + cssParts.push(selector); + return proxy; + }; + } + + // Throw error for empty string properties + if (typeof prop === 'string' && prop === '') { + throw new Error('Empty string property is not supported'); + } + + // Throw error for integer properties + if (typeof prop === 'string' && !isNaN(Number(prop)) && Number.isInteger(Number(prop))) { + throw new Error(`Index access is not supported. Received integer property: ${prop}`); + } + + // Add CSS part for property access + if (typeof prop === 'string') { + cssParts.push(propertyToCSSSelector(prop, attributeName)); + } + + return proxy; + } + }); + + return proxy as ShadowElementPathProxy; +} diff --git a/packages/element-path/test/shadow-css-selector/css-basic-selectors.spec.ts b/packages/element-path/test/shadow-css-selector/css-basic-selectors.spec.ts new file mode 100644 index 000000000..b363ab4f3 --- /dev/null +++ b/packages/element-path/test/shadow-css-selector/css-basic-selectors.spec.ts @@ -0,0 +1,292 @@ +import {createElementPath} from '../../src/create-element-path'; +import { + ShadowElementPathProxy, + createShadowElementPathProxy, + CSS_SELECTOR_PATTERNS, +} from '../../src/shadow-element-path'; +import {expect} from 'chai'; + +const getNewShadowElement = () => { + const root = createElementPath(); + const shadowRootElement = root['shadowHost']; + return shadowRootElement?.shadow$ as ShadowElementPathProxy; +}; + +describe('CSS Basic Selectors', () => { + describe('Basic functionality', () => { + it('should generate exact key selector', () => { + const shadowElement = getNewShadowElement()['shadowButton']; + expect(shadowElement.isShadowElement).to.equal(true); + expect(shadowElement.toString()).to.equal( + `(//*[@data-test-automation-id='root']//*[@data-test-automation-id='shadowHost'])[1]`, + ); + expect(shadowElement.toShadowCSSSelector()).to.equal( + '[data-test-automation-id="shadowButton"]', + ); + }); + + it('should return correct toString and isShadowElement', () => { + const proxy = createShadowElementPathProxy(['/test/path'], 'data-test'); + expect(proxy.toString()).to.equal('/test/path'); + expect(proxy.isShadowElement).to.equal(true); + }); + + it('should handle simple property chaining', () => { + const proxy = createShadowElementPathProxy(['/test'], 'data-test-automation-id'); + const result = proxy['app']['test']; + expect(result.toShadowCSSSelector()).to.equal('[data-test-automation-id="app"] [data-test-automation-id="test"]'); + }); + + it('should handle shadow$ deep traversal', () => { + const proxy = createShadowElementPathProxy(['/root'], 'data-test-automation-id'); + const result = proxy['outer'].shadow$['inner'].shadow$['deep']; + expect(result.toShadowCSSSelector()).to.equal('[data-test-automation-id="deep"]'); + expect(result.getParentSelectors()).to.deep.equal(['/root', '[data-test-automation-id="outer"]', '[data-test-automation-id="inner"]']); + }); + + it('should return parent selectors', () => { + const proxy = createShadowElementPathProxy(['/root', '/parent'], 'data-test'); + expect(proxy.getParentSelectors()).to.deep.equal(['/root', '/parent']); + }); + }); + + describe('CSS Selector Patterns', () => { + let proxy: ShadowElementPathProxy; + + beforeEach(() => { + proxy = createShadowElementPathProxy(['/test'], 'data-test'); + }); + + it('should handle wildcard pattern (*)', () => { + const result = proxy['*']; + expect(result.toShadowCSSSelector()).to.equal('[data-test]'); + }); + + it('should handle suffix pattern (*foo)', () => { + const result = proxy['*button']; + expect(result.toShadowCSSSelector()).to.equal('[data-test$="button"]'); + }); + + it('should handle prefix pattern (foo*)', () => { + const result = proxy['button*']; + expect(result.toShadowCSSSelector()).to.equal('[data-test^="button"]'); + }); + + it('should handle contains pattern (*foo*)', () => { + const result = proxy['*button*']; + expect(result.toShadowCSSSelector()).to.equal('[data-test*="button"]'); + }); + + it('should handle exact match', () => { + const result = proxy['button']; + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"]'); + }); + + it('should handle escaped asterisks', () => { + const result = proxy['foo\\*bar']; + expect(result.toShadowCSSSelector()).to.equal('[data-test="foo*bar"]'); + }); + + it('should handle multiple escaped asterisks', () => { + const result = proxy['foo\\*bar\\*baz']; + expect(result.toShadowCSSSelector()).to.equal('[data-test="foo*bar*baz"]'); + }); + }); + + describe('.and() method', () => { + let proxy: ShadowElementPathProxy; + + beforeEach(() => { + proxy = createShadowElementPathProxy(['/test'], 'data-test'); + }); + + it('should append pseudo-selector', () => { + const result = proxy['button'].and(':hover'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"]:hover'); + }); + + it('should append attribute selector', () => { + const result = proxy['button'].and('[disabled]'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"][disabled]'); + }); + + it('should append class selector', () => { + const result = proxy['button'].and('.active'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"].active'); + }); + + it('should append ID selector', () => { + const result = proxy['button'].and('#main'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"]#main'); + }); + + it('should append complex pseudo-selector', () => { + const result = proxy['button'].and(':nth-child(2 of .item)'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"]:nth-child(2 of .item)'); + }); + + it('should handle multiple .and() calls', () => { + const result = proxy['button'].and(':hover').and('.active'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="button"]:hover.active'); + }); + + it('should handle .and() on empty selector', () => { + const result = proxy.and(':hover'); + expect(result.toShadowCSSSelector()).to.equal(''); + }); + }); + + describe('.then() method', () => { + let proxy: ShadowElementPathProxy; + + beforeEach(() => { + proxy = createShadowElementPathProxy(['/test'], 'data-test'); + }); + + it('should add descendant selector', () => { + const result = proxy['container'].then('button'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] button'); + }); + + it('should add child selector', () => { + const result = proxy['container'].then('> button'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] > button'); + }); + + it('should add adjacent sibling selector', () => { + const result = proxy['container'].then('+ .badge'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] + .badge'); + }); + + it('should add general sibling selector', () => { + const result = proxy['container'].then('~ .sibling'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] ~ .sibling'); + }); + + it('should handle multiple .then() calls', () => { + const result = proxy['container'].then('ul').then('> li'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] ul > li'); + }); + + it('should handle .then() on empty selector', () => { + const result = proxy.then('button'); + expect(result.toShadowCSSSelector()).to.equal('button'); + }); + }); + + describe('Method chaining', () => { + let proxy: ShadowElementPathProxy; + + beforeEach(() => { + proxy = createShadowElementPathProxy(['/test'], 'data-test'); + }); + + it('should chain .then() and .and()', () => { + const result = proxy['container'].then('button').and(':hover'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] button:hover'); + }); + + it('should chain multiple .then() and .and()', () => { + const result = proxy['container'].then('ul').then('> li').and(':nth-child(2)'); + expect(result.toShadowCSSSelector()).to.equal('[data-test="container"] ul > li:nth-child(2)'); + }); + + it('should chain pattern matching with methods', () => { + const result = proxy['*card*'].then('> .title').and('.active'); + expect(result.toShadowCSSSelector()).to.equal('[data-test*="card"] > .title.active'); + }); + }); + + describe('Error handling', () => { + let proxy: ShadowElementPathProxy; + + beforeEach(() => { + proxy = createShadowElementPathProxy(['/test'], 'data-test'); + }); + + it('should throw error for integer property access', () => { + expect(() => proxy[0]).to.throw('Index access is not supported. Received integer property: 0'); + }); + + it('should throw error for negative integer property access', () => { + expect(() => proxy[-1]).to.throw('Index access is not supported. Received integer property: -1'); + }); + + it('should throw error for string integer property access', () => { + expect(() => proxy['5']).to.throw('Index access is not supported. Received integer property: 5'); + }); + + it('should not throw error for non-integer numeric strings', () => { + expect(() => proxy['5.5']).to.not.throw(); + }); + + it('should not throw error for NaN strings', () => { + expect(() => proxy['abc']).to.not.throw(); + }); + }); + + describe('Edge cases', () => { + let proxy: ShadowElementPathProxy; + + beforeEach(() => { + proxy = createShadowElementPathProxy(['/test'], 'data-test'); + }); + + it('should throw error for empty string property', () => { + expect(() => proxy['']).to.throw('Empty string property is not supported'); + }); + + it('should handle single character patterns', () => { + const result = proxy['a']; + expect(result.toShadowCSSSelector()).to.equal('[data-test="a"]'); + }); + + it('should handle patterns with only asterisks', () => { + const result = proxy['**']; + expect(result.toShadowCSSSelector()).to.equal('[data-test="**"]'); + }); + + it('should handle patterns with multiple asterisks', () => { + const result = proxy['*a*b*']; + expect(result.toShadowCSSSelector()).to.equal('[data-test="*a*b*"]'); + }); + + it('should handle .and() on empty cssParts array', () => { + const result = proxy.and(':hover'); + expect(result.toShadowCSSSelector()).to.equal(''); + }); + + it('should handle symbol properties', () => { + const symbol = Symbol('test'); + const result = proxy[symbol]; + expect(result).to.equal(proxy); + }); + }); + + describe('CSS_SELECTOR_PATTERNS', () => { + it('should match wildcard pattern', () => { + expect(CSS_SELECTOR_PATTERNS.WILDCARD.test('*')).to.equal(true); + expect(CSS_SELECTOR_PATTERNS.WILDCARD.test('**')).to.equal(false); + expect(CSS_SELECTOR_PATTERNS.WILDCARD.test('a*')).to.equal(false); + }); + + it('should match suffix pattern', () => { + expect(CSS_SELECTOR_PATTERNS.SUFFIX.test('*foo')).to.equal(true); + expect(CSS_SELECTOR_PATTERNS.SUFFIX.test('*foo*')).to.equal(false); + expect(CSS_SELECTOR_PATTERNS.SUFFIX.test('foo*')).to.equal(false); + }); + + it('should match prefix pattern', () => { + expect(CSS_SELECTOR_PATTERNS.PREFIX.test('foo*')).to.equal(true); + expect(CSS_SELECTOR_PATTERNS.PREFIX.test('*foo*')).to.equal(false); + expect(CSS_SELECTOR_PATTERNS.PREFIX.test('*foo')).to.equal(false); + }); + + it('should match contains pattern', () => { + expect(CSS_SELECTOR_PATTERNS.CONTAINS.test('*foo*')).to.equal(true); + expect(CSS_SELECTOR_PATTERNS.CONTAINS.test('*foo')).to.equal(false); + expect(CSS_SELECTOR_PATTERNS.CONTAINS.test('foo*')).to.equal(false); + expect(CSS_SELECTOR_PATTERNS.CONTAINS.test('**')).to.equal(false); + }); + }); +}); diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index 31a2ce2f0..0c2d13ac1 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -3,7 +3,11 @@ import { IBrowserProxyPlugin, SavePdfOptions, WindowFeaturesConfig, - IWindowFeatures + IWindowFeatures, + Selector, + ShadowCssSelector, + isXpathSelector, + isShadowCssSelector } from '@testring/types'; import {ChildProcess} from 'child_process'; @@ -313,7 +317,11 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { this.localSelenium = spawnWithPipes('java', seleniumArgs); this.waitForReadyState = new Promise((resolve, reject) => { - setupProcessListeners(this.localSelenium!, resolve, reject, version, this.logger); + if (this.localSelenium) { + setupProcessListeners(this.localSelenium, resolve, reject, version, this.logger); + } else { + reject(new Error('Failed to spawn Selenium process')); + } }); // Wait for the server to be ready @@ -499,6 +507,88 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client as BrowserObjectCustom; } + private async getElement(applicant: string, selector: Selector) { + await this.createClient(applicant); + const client = this.getBrowserClient(applicant); + + if (isXpathSelector(selector)) { + return client.$(selector.xpath); + } else if (isShadowCssSelector(selector)) { + return this.getElementFromShadowCss(client, selector); + } + + throw new Error('Unknown selector type'); + } + + private async getElementFromShadowCss(client: BrowserObjectCustom, selector: ShadowCssSelector) { + const {css, parentSelectors} = selector; + + // Error first: validate selector structure + this.validateShadowCssSelector(selector); + + try { + return await this.traverseShadowDom(client, css, parentSelectors); + } catch (error) { + // Provide more context in error messages + if (error instanceof Error) { + throw new Error(`Shadow DOM traversal failed: ${error.message}. Selector: ${JSON.stringify(selector)}`); + } + throw error; + } + } + + private validateShadowCssSelector(selector: ShadowCssSelector): void { + const {css, parentSelectors} = selector; + + if (!css || typeof css !== 'string') { + throw new Error('Shadow CSS selector must have a valid CSS string'); + } + + if (!Array.isArray(parentSelectors) || parentSelectors.length === 0) { + throw new Error('Shadow CSS selector must have at least one parent selector'); + } + + // Validate all parent selectors are non-empty strings + for (const [index, parentSelector] of parentSelectors.entries()) { + if (!parentSelector || typeof parentSelector !== 'string') { + throw new Error(`Parent selector at index ${index} must be a non-empty string`); + } + } + } + + private async traverseShadowDom(client: BrowserObjectCustom, css: string, parentSelectors: string[]) { + const [firstParentSelector, ...restParentSelectors] = parentSelectors; + + // TypeScript assertion: we know firstParentSelector exists due to validation + if (!firstParentSelector) { + throw new Error('First parent selector is required'); + } + + // Get the first parent element + let currentElement = await client.$(firstParentSelector); + + if (!currentElement) { + throw new Error(`Failed to find parent element with selector: ${firstParentSelector}`); + } + + // Traverse through shadow DOM hierarchy + for (const parentSelector of restParentSelectors) { + const shadowElement = await currentElement.shadow$(parentSelector); + if (!shadowElement) { + throw new Error(`Failed to find shadow element with selector: ${parentSelector}`); + } + currentElement = shadowElement; + } + + // Get the final target element within the shadow DOM + const targetElement = await currentElement.shadow$(css); + if (!targetElement) { + throw new Error(`Failed to find target element with CSS selector: ${css}`); + } + + return targetElement; + } + public async end(applicant: string) { await this.waitForReadyState; @@ -638,24 +728,17 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async click( applicant: string, - selector: string, + selector: Selector, options?: ClickOptions, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const element = await client.$(selector); + const element = await this.getElement(applicant, selector); return options && Object.keys(options).length > 0 ? element.click(options) : element.click(); } - public async getSize(applicant: string, selector: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const element = await client.$(selector); - + public async getSize(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); return element.getSize(); } @@ -695,47 +778,35 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async waitForExist( applicant: string, - xpath: string, + selector: Selector, timeout: number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.waitForExist({timeout}); + const element = await this.getElement(applicant, selector); + return element.waitForExist({timeout}); } public async waitForVisible( applicant: string, - xpath: string, + selector: Selector, timeout: number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.waitForDisplayed({timeout}); + const element = await this.getElement(applicant, selector); + return element.waitForDisplayed({timeout}); } - public async isVisible(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isDisplayed(); + public async isVisible(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isDisplayed(); } public async moveToObject( applicant: string, - xpath: string, + selector: Selector, xOffset = 0, yOffset = 0, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.moveTo({xOffset, yOffset}); + const element = await this.getElement(applicant, selector); + return element.moveTo({xOffset, yOffset}); } public async execute(applicant: string, fn: any, args: Array) { @@ -759,12 +830,9 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.getTitle(); } - public async clearValue(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.clearValue(); + public async clearValue(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.clearValue(); } public async keys(applicant: string, value: any) { @@ -781,19 +849,26 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.getElementText(elementId); } - public async elements(applicant: string, xpath: string) { + public async elements(applicant: string, selector: Selector) { await this.createClient(applicant); const client = this.getBrowserClient(applicant); - const elements = (await client.findElements('xpath', xpath)) as unknown; - return (elements as Array>).map((o) => { - const keys = Object.keys(o); - const firstKey = keys[0]; - if (firstKey === undefined) { - return {ELEMENT: ''}; - } - return {ELEMENT: o[firstKey]}; - }); + if (isXpathSelector(selector)) { + const elements = (await client.findElements('xpath', selector.xpath)) as unknown; + return (elements as Array>).map((o) => { + const keys = Object.keys(o); + const firstKey = keys[0]; + if (firstKey === undefined) { + return {ELEMENT: ''}; + } + return {ELEMENT: o[firstKey]}; + }); + } else if (isShadowCssSelector(selector)) { + // TODO: Implement shadow DOM CSS selector logic + throw new Error('ShadowCssSelector not implemented yet'); + } + + throw new Error('Unknown selector type'); } public async frame(applicant: string, frameID: any) { @@ -809,60 +884,42 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.switchToParentFrame(); } - public async getValue(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.getValue(); + public async getValue(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.getValue(); } - public async setValue(applicant: string, xpath: string, value: any) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.setValue(value); + public async setValue(applicant: string, selector: Selector, value: any) { + const element = await this.getElement(applicant, selector); + return element.setValue(value); } - public async selectByIndex(applicant: string, xpath: string, value: any) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.selectByIndex(value); + public async selectByIndex(applicant: string, selector: Selector, value: any) { + const element = await this.getElement(applicant, selector); + return element.selectByIndex(value); } - public async selectByValue(applicant: string, xpath: string, value: any) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.selectByAttribute('value', value); + public async selectByValue(applicant: string, selector: Selector, value: any) { + const element = await this.getElement(applicant, selector); + return element.selectByAttribute('value', value); } public async selectByVisibleText( applicant: string, - xpath: string, + selector: Selector, str: string, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.selectByVisibleText(str); + const element = await this.getElement(applicant, selector); + return element.selectByVisibleText(str); } public async getAttribute( applicant: string, - xpath: string, + selector: Selector, attr: string, ): Promise { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.getAttribute(attr); + const element = await this.getElement(applicant, selector); + return element.getAttribute(attr); } public async windowHandleMaximize(applicant: string) { @@ -872,37 +929,28 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.maximizeWindow(); } - public async isEnabled(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isEnabled(); + public async isEnabled(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isEnabled(); } public async scroll( applicant: string, - xpath: string, + selector: Selector, xOffset: number, yOffset: number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const element = await client.$(xpath); + const element = await this.getElement(applicant, selector); await element.scrollIntoView(); return element.moveTo({xOffset, yOffset}); } public async scrollIntoView( applicant: string, - xpath: string, + selector: Selector, scrollIntoViewOptions?: boolean | null, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const element = await client.$(xpath); + const element = await this.getElement(applicant, selector); await element.scrollIntoView( scrollIntoViewOptions !== null ? scrollIntoViewOptions : undefined, ); @@ -950,14 +998,11 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async dragAndDrop( applicant: string, - xpathSource: string, - xpathDestination: string, + sourceSelector: Selector, + destinationSelector: Selector, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const sourceElement = await client.$(xpathSource); - const destinationElement = await client.$(xpathDestination); + const sourceElement = await this.getElement(applicant, sourceSelector); + const destinationElement = await this.getElement(applicant, destinationSelector); return sourceElement.dragAndDrop(destinationElement); } @@ -995,12 +1040,9 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.deleteAllCookies(); } - public async getHTML(applicant: string, xpath: string, b: any) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.getHTML(b); + public async getHTML(applicant: string, selector: Selector, b: any) { + const element = await this.getElement(applicant, selector); + return element.getHTML(b); } public async getCurrentTabId(applicant: string) { @@ -1054,28 +1096,19 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.closeWindow(); } - public async getTagName(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.getTagName(); + public async getTagName(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.getTagName(); } - public async isSelected(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isSelected(); + public async isSelected(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isSelected(); } - public async getText(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.getText(); + public async getText(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.getText(); } public async elementIdSelected(applicant: string, id: string) { @@ -1104,13 +1137,10 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async getCssProperty( applicant: string, - xpath: string, + selector: Selector, cssProperty: string, ): Promise { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const element = await client.$(xpath); + const element = await this.getElement(applicant, selector); const property = await element.getCSSProperty(cssProperty); return property.value; } @@ -1122,17 +1152,14 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.getPageSource(); } - public async isExisting(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isExisting(); + public async isExisting(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isExisting(); } public async waitForValue( applicant: string, - xpath: string, + selector: Selector, timeout: number, reverse: boolean, ) { @@ -1141,7 +1168,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.waitUntil( async () => { - const elemValue = await (await client.$(xpath)).getValue(); + const element = await this.getElement(applicant, selector); + const elemValue = await element.getValue(); return reverse ? !elemValue : !!elemValue; }, {timeout}, @@ -1150,7 +1178,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async waitForSelected( applicant: string, - xpath: string, + selector: Selector, timeout: number, reverse: boolean, ) { @@ -1159,7 +1187,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.waitUntil( async () => { - const isSelected = await (await client.$(xpath)).isSelected(); + const element = await this.getElement(applicant, selector); + const isSelected = await element.isSelected(); return reverse ? !isSelected : isSelected; }, {timeout}, @@ -1193,15 +1222,12 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async selectByAttribute( applicant: string, - xpath: string, + selector: Selector, attribute: string, value: string, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.selectByAttribute(attribute, value); + const element = await this.getElement(applicant, selector); + return element.selectByAttribute(attribute, value); } public async gridTestSession(applicant: string) { @@ -1299,10 +1325,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return client.getActiveElement(); } - public async getLocation(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - const element = client.$(xpath); + public async getLocation(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); return element.getLocation(); } @@ -1329,82 +1353,58 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { public async addValue( applicant: string, - xpath: string, + selector: Selector, value: string | number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.addValue(value); + const element = await this.getElement(applicant, selector); + return element.addValue(value); } - public async doubleClick(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.doubleClick(); + public async doubleClick(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.doubleClick(); } - public async isClickable(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isClickable(); + public async isClickable(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isClickable(); } public async waitForClickable( applicant: string, - xpath: string, + selector: Selector, timeout: number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.waitForClickable({timeout}); + const element = await this.getElement(applicant, selector); + return element.waitForClickable({timeout}); } - public async isFocused(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isFocused(); + public async isFocused(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isFocused(); } - public async isStable(applicant: string, xpath: string) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.isStable(); + public async isStable(applicant: string, selector: Selector) { + const element = await this.getElement(applicant, selector); + return element.isStable(); } public async waitForEnabled( applicant: string, - xpath: string, + selector: Selector, timeout: number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.waitForEnabled({timeout}); + const element = await this.getElement(applicant, selector); + return element.waitForEnabled({timeout}); } public async waitForStable( applicant: string, - xpath: string, + selector: Selector, timeout: number, ) { - await this.createClient(applicant); - const client = this.getBrowserClient(applicant); - - const selector = await client.$(xpath); - return selector.waitForStable({timeout}); + const element = await this.getElement(applicant, selector); + return element.waitForStable({timeout}); } } diff --git a/packages/web-application/src/web-application.ts b/packages/web-application/src/web-application.ts index ffce7c024..c57b27342 100644 --- a/packages/web-application/src/web-application.ts +++ b/packages/web-application/src/web-application.ts @@ -16,13 +16,16 @@ import { ExtensionPostMessageTypes, FSFileLogType, SavePdfOptions, + XpathSelector, + ShadowCssSelector, + Selector, } from '@testring/types'; import {asyncBreakpoints} from '@testring/async-breakpoints'; import {loggerClient, LoggerClient} from '@testring/logger'; import {generateUniqId} from '@testring/utils'; import {PluggableModule} from '@testring/pluggable-module'; -import {createElementPath, ElementPathProxy} from '@testring/element-path'; +import {createElementPath, ElementPathProxy, ShadowElementPathProxy} from '@testring/element-path'; import {createAssertion} from '@testring/async-assert'; import {WebClient} from './web-client'; @@ -41,7 +44,7 @@ type ClickOptions = { y?: number | 'top' | 'center' | 'bottom'; }; -type ElementPath = string | ElementPathProxy; +type ElementPath = string | ElementPathProxy | ShadowElementPathProxy; export class WebApplication extends PluggableModule { protected LOGGER_PREFIX = '[web-application]'; @@ -174,17 +177,30 @@ export class WebApplication extends PluggableModule { return this.root as ElementPathProxy; } + // type guard for ShadowElementPathProxy + protected isShadowElementPathProxy(elementPath: ElementPath): elementPath is ShadowElementPathProxy { + return (elementPath as any).isShadowElement === true; + } + protected normalizeSelector( selector: ElementPath, allowMultipleNodesInResult = false, - ): string { + ): Selector { if (!selector) { - return this.getRootSelector().toString(); + const rootXpath = this.getRootSelector().toString(); + return { type: 'xpath', xpath: rootXpath } as XpathSelector; } - return (selector as ElementPathProxy).toString( - allowMultipleNodesInResult, - ); + if (typeof selector === 'string') { + return { type: 'xpath', xpath: selector } as XpathSelector; + } + + if (this.isShadowElementPathProxy(selector)) { + return { type: 'shadow-css', css: selector.toShadowCSSSelector(), parentSelectors: selector.getParentSelectors(), isShadowElement: true } as ShadowCssSelector; + } + + const xpath = selector.toString(allowMultipleNodesInResult); + return { type: 'xpath', xpath } as XpathSelector; } protected async asyncErrorHandler(_error: Error) { @@ -204,6 +220,12 @@ export class WebApplication extends PluggableModule { if (this.config.devtool) { try { + if (normalizedXPath && normalizedXPath.type !== 'xpath') { + throw new Error( + `devtoolHighlight only supports xpath selectors. Received type: ${normalizedXPath.type}, value: ${JSON.stringify(normalizedXPath)}` + ); + } + const xpathString = normalizedXPath ? normalizedXPath.xpath : null; await this.client.execute((addHighlightXpath: string) => { window.postMessage( { @@ -221,7 +243,7 @@ export class WebApplication extends PluggableModule { '*', ); } - }, normalizedXPath); + }, xpathString); } catch (e) { this.logger.error('Failed to highlight element:', e); } @@ -262,8 +284,9 @@ export class WebApplication extends PluggableModule { }) public async waitForRoot(timeout: number = this.WAIT_TIMEOUT) { const xpath = this.getRootSelector().toString(); + const selector: XpathSelector = { type: 'xpath', xpath }; - return this.client.waitForExist(xpath, timeout); + return this.client.waitForExist(selector, timeout); } // TODO (flops) remove it and make extension via initCustomApp @@ -278,8 +301,8 @@ export class WebApplication extends PluggableModule { let exists = false; try { - xpath = this.normalizeSelector(xpath); - await this.client.waitForExist(xpath, timeout); + const normalizedXPath = this.normalizeSelector(xpath); + await this.client.waitForExist(normalizedXPath, timeout); exists = true; } catch (ignore) { /* ignore */ @@ -317,9 +340,9 @@ export class WebApplication extends PluggableModule { ); } - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); - return this.client.waitForVisible(xpath, waitTime); + return this.client.waitForVisible(normalizedXPath, waitTime); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { @@ -329,7 +352,7 @@ export class WebApplication extends PluggableModule { const path = this.formatXpath(xpath); const expires = Date.now() + timeout; - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); try { await this.waitForRoot(timeout); @@ -340,7 +363,7 @@ export class WebApplication extends PluggableModule { } while (expires - Date.now() >= 0) { - const visible = await this.client.isVisible(xpath); + const visible = await this.client.isVisible(normalizedXPath); if (!visible) { return false; @@ -471,7 +494,7 @@ export class WebApplication extends PluggableModule { public async click(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { const normalizedSelector = this.normalizeSelector(xpath); - await this.waitForExist(normalizedSelector, timeout); + await this.waitForExist(xpath, timeout); await this.makeScreenshot(); return this.client.click(normalizedSelector, {x: 1, y: 1}); @@ -483,7 +506,7 @@ export class WebApplication extends PluggableModule { public async clickButton(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { const normalizedSelector = this.normalizeSelector(xpath); - await this.waitForExist(normalizedSelector, timeout); + await this.waitForExist(xpath, timeout); await this.makeScreenshot(); return this.client.click(normalizedSelector, {button: 'left'}); @@ -499,7 +522,7 @@ export class WebApplication extends PluggableModule { ) { const normalizedSelector = this.normalizeSelector(xpath); - await this.waitForExist(normalizedSelector, timeout); + await this.waitForExist(xpath, timeout); await this.makeScreenshot(); let hPos = 0; @@ -579,9 +602,17 @@ export class WebApplication extends PluggableModule { return `Simulating JS field change for ${this.formatXpath(xpath)} with value ${value}`; }) public async simulateJSFieldChange(xpath: ElementPath, value: string) { + const normalizedXPath = this.normalizeSelector(xpath); + if (normalizedXPath.type !== 'xpath') { + throw new Error( + `simulateJSFieldChange only supports xpath selectors. Received type: ${normalizedXPath.type}, value: ${JSON.stringify(normalizedXPath)}` + ); + } + const xpathString = normalizedXPath.xpath; + const result = await this.client.executeAsync( simulateJSFieldChangeScript, - xpath, + xpathString, value, ); @@ -600,12 +631,12 @@ export class WebApplication extends PluggableModule { ) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); if (emulateViaJs) { return this.simulateJSFieldClear(xpath); } - await this.client.setValue(xpath, '_'); + await this.client.setValue(normalizedXPath, '_'); await this.waitForExist(xpath, timeout); return this.client.keys(['Backspace']); } @@ -632,7 +663,7 @@ export class WebApplication extends PluggableModule { await this.clearElement(xpath, emulateViaJS, timeout); } else { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); if (emulateViaJS) { this.simulateJSFieldChange(xpath, value as string); @@ -643,7 +674,7 @@ export class WebApplication extends PluggableModule { )} using JS emulation`, ); } else { - await this.client.setValue(xpath, value); + await this.client.setValue(normalizedXPath, value); this.logger.debug( `Value ${value} was entered into ${this.formatXpath( xpath, @@ -723,9 +754,15 @@ export class WebApplication extends PluggableModule { ) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + if (normalizedXPath.type !== 'xpath') { + throw new Error( + `getOptionsProperty only supports xpath selectors. Received type: ${normalizedXPath.type}, value: ${JSON.stringify(normalizedXPath)}` + ); + } + const xpathString = normalizedXPath.xpath; - return this.client.executeAsync(getOptionsPropertyScript, xpath, prop); + return this.client.executeAsync(getOptionsPropertyScript, xpathString, prop); } @stepLog(function (this: WebApplication, xpath: ElementPath, _trim = true, timeout: number = this.WAIT_TIMEOUT) { @@ -765,9 +802,8 @@ export class WebApplication extends PluggableModule { }) public async selectNotCurrent(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { const options: any[] = await this.getSelectValues(xpath, timeout); - const value: any = await this.client.getValue( - this.normalizeSelector(xpath), - ); + const normalizedXPath = this.normalizeSelector(xpath); + const value: any = await this.client.getValue(normalizedXPath); const index = options.indexOf(value); if (index > -1) { options.splice(index, 1); @@ -786,12 +822,12 @@ export class WebApplication extends PluggableModule { const logXpath = this.formatXpath(xpath); const errorMessage = `Could not select by index "${value}": ${logXpath}`; - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); try { - return await this.client.selectByIndex(xpath, value); + return await this.client.selectByIndex(normalizedXPath, value); } catch (error) { (error as Error).message = errorMessage; throw error; @@ -810,12 +846,12 @@ export class WebApplication extends PluggableModule { xpath, )}`; - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); try { - return await this.client.selectByValue(xpath, value); + return await this.client.selectByValue(normalizedXPath, value); } catch (error) { (error as Error).message = errorMessage; throw error; @@ -833,12 +869,12 @@ export class WebApplication extends PluggableModule { const logXpath = this.formatXpath(xpath); const errorMessage = `Could not select by visible text "${value}": ${logXpath}`; - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); try { - return await this.client.selectByVisibleText(xpath, String(value)); + return await this.client.selectByVisibleText(normalizedXPath, String(value)); } catch (error) { (error as Error).message = errorMessage; throw error; @@ -849,19 +885,25 @@ export class WebApplication extends PluggableModule { return `Getting selected text for ${this.formatXpath(xpath)} for ${timeout}`; }) public async getSelectedText(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); - const value = await this.client.getValue(xpath); + if (normalizedXPath.type !== 'xpath') { + throw new Error( + `getSelectedText only supports xpath selectors. Received type: ${normalizedXPath.type}, value: ${JSON.stringify(normalizedXPath)}` + ); + } + + const value = await this.client.getValue(normalizedXPath); if (typeof value === 'string' || typeof value === 'number') { // TODO (flops) rework this for supporting custom selectors - xpath += `//option[@value='${value}']`; + const optionXPath = { type: 'xpath' as const, xpath: normalizedXPath.xpath + `//option[@value='${value}']` }; try { - const options = await this.client.getText(xpath); - if (options instanceof Array) { + const options = await this.client.getText(optionXPath); + if (Array.isArray(options)) { return options[0] || ''; } return options || ''; @@ -905,11 +947,11 @@ export class WebApplication extends PluggableModule { return `Checking if ${this.formatXpath(xpath)} is checked for ${timeout}`; }) public async isChecked(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); - const isSelected = await this.client.isSelected(xpath); + const isSelected = await this.client.isSelected(normalizedXPath); return !!isSelected; } @@ -922,14 +964,14 @@ export class WebApplication extends PluggableModule { checked = true, timeout: number = this.WAIT_TIMEOUT, ) { - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); - const isChecked = await this.client.isSelected(xpath); + const isChecked = await this.client.isSelected(normalizedXPath); if (!!isChecked !== !!checked) { - return this.client.click(xpath); + return this.client.click(normalizedXPath); } } @@ -939,9 +981,9 @@ export class WebApplication extends PluggableModule { public async isVisible(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { await this.waitForRoot(timeout); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); - return this.client.isVisible(xpath); + return this.client.isVisible(normalizedXPath); } @stepLog(function (this: WebApplication, xpath: ElementPath, ...suitableClasses: string[]) { @@ -967,11 +1009,11 @@ export class WebApplication extends PluggableModule { attr: string, timeout: number = this.WAIT_TIMEOUT, ) { - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); - return this.client.getAttribute(xpath, attr); + return this.client.getAttribute(normalizedXPath, attr); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { @@ -980,15 +1022,15 @@ export class WebApplication extends PluggableModule { public async isReadOnly(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { const inputTags = ['input', 'select', 'textarea']; - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); const readonly: string = await this.client.getAttribute( - xpath, + normalizedXPath, 'readonly', ); - const str: string = await this.client.getTagName(xpath); + const str: string = await this.client.getTagName(normalizedXPath); if ( readonly === 'true' || @@ -1000,7 +1042,7 @@ export class WebApplication extends PluggableModule { } const disabled: string = await this.client.getAttribute( - xpath, + normalizedXPath, 'disabled', ); @@ -1013,9 +1055,9 @@ export class WebApplication extends PluggableModule { public async isEnabled(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); - return this.client.isEnabled(xpath); + return this.client.isEnabled(normalizedXPath); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { @@ -1065,9 +1107,9 @@ export class WebApplication extends PluggableModule { ) { await this.waitForExist(xpath, timeout, true); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); - return this.client.scroll(xpath, x, y); + return this.client.scroll(normalizedXPath, x, y); } // eslint-disable-next-line sonarjs/cognitive-complexity @@ -1075,9 +1117,16 @@ export class WebApplication extends PluggableModule { const normalizedXpath = this.normalizeSelector(xpath); if (topOffset || leftOffset) { + if (normalizedXpath.type !== 'xpath') { + throw new Error( + `scrollIntoViewCall only supports xpath selectors. Received type: ${normalizedXpath.type}, value: ${JSON.stringify(normalizedXpath)}` + ); + } + const xpathString = normalizedXpath.xpath; + const result = await this.client.executeAsync( scrollIntoViewCallScript, - normalizedXpath, + xpathString, topOffset, leftOffset, ); @@ -1112,9 +1161,16 @@ export class WebApplication extends PluggableModule { ) { const normalizedXpath = this.normalizeSelector(xpath); + if (normalizedXpath.type !== 'xpath') { + throw new Error( + `scrollIntoViewIfNeededCall only supports xpath selectors. Received type: ${normalizedXpath.type}, value: ${JSON.stringify(normalizedXpath)}` + ); + } + const xpathString = normalizedXpath.xpath; + const result: string = await this.client.executeAsync( scrollIntoViewIfNeededCallScript, - normalizedXpath, + xpathString, topOffset, leftOffset, ); @@ -1148,10 +1204,10 @@ export class WebApplication extends PluggableModule { await this.waitForExist(xpathSource, timeout); await this.waitForExist(xpathDestination, timeout); - xpathSource = this.normalizeSelector(xpathSource); - xpathDestination = this.normalizeSelector(xpathDestination); + const normalizedSource = this.normalizeSelector(xpathSource); + const normalizedDestination = this.normalizeSelector(xpathDestination); - return this.client.dragAndDrop(xpathSource, xpathDestination); + return this.client.dragAndDrop(normalizedSource, normalizedDestination); } @stepLog(function (this: WebApplication, xpath: ElementPath) { @@ -1180,10 +1236,7 @@ export class WebApplication extends PluggableModule { return `Checking if elements do not exist for ${this.formatXpath(xpath)} for ${timeout}`; }) public async notExists(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - const elementsCount = await this.getElementsCount( - this.normalizeSelector(xpath), - timeout, - ); + const elementsCount = await this.getElementsCount(xpath, timeout); return elementsCount === 0; } @@ -1192,10 +1245,7 @@ export class WebApplication extends PluggableModule { return `Checking if elements exist for ${this.formatXpath(xpath)} for ${timeout}`; }) public async isElementsExist(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - const elementsCount = await this.getElementsCount( - this.normalizeSelector(xpath), - timeout, - ); + const elementsCount = await this.getElementsCount(xpath, timeout); return elementsCount > 0; } @@ -1348,9 +1398,9 @@ export class WebApplication extends PluggableModule { public async getHTML(xpath: ElementPath, timeout = this.WAIT_TIMEOUT) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); - return this.client.getHTML(xpath, {prettify: false}); + return this.client.getHTML(normalizedXPath, {prettify: false}); } @stepLog(function (this: WebApplication) { @@ -1693,8 +1743,8 @@ export class WebApplication extends PluggableModule { ): Promise { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); - return await this.client.getCssProperty(xpath, cssProperty); + const normalizedXPath = this.normalizeSelector(xpath); + return await this.client.getCssProperty(normalizedXPath, cssProperty); } @stepLog(function (this: WebApplication) { @@ -1708,8 +1758,8 @@ export class WebApplication extends PluggableModule { return `Checking if ${this.formatXpath(xpath)} exists`; }) public async isExisting(xpath: ElementPath) { - xpath = this.normalizeSelector(xpath); - return await this.client.isExisting(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + return await this.client.isExisting(normalizedXPath); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT, reverse = false) { @@ -1720,8 +1770,8 @@ export class WebApplication extends PluggableModule { timeout: number = this.WAIT_TIMEOUT, reverse = false, ) { - xpath = this.normalizeSelector(xpath); - return await this.client.waitForValue(xpath, timeout, reverse); + const normalizedXPath = this.normalizeSelector(xpath); + return await this.client.waitForValue(normalizedXPath, timeout, reverse); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT, reverse = false) { @@ -1732,8 +1782,8 @@ export class WebApplication extends PluggableModule { timeout: number = this.WAIT_TIMEOUT, reverse = false, ) { - xpath = this.normalizeSelector(xpath); - return await this.client.waitForSelected(xpath, timeout, reverse); + const normalizedXPath = this.normalizeSelector(xpath); + return await this.client.waitForSelected(normalizedXPath, timeout, reverse); } @stepLog(function (this: WebApplication, condition: () => boolean | Promise, timeout: number = this.WAIT_TIMEOUT, timeoutMsg = 'Wait by condition failed!', interval = 500) { @@ -1765,12 +1815,12 @@ export class WebApplication extends PluggableModule { const logXpath = this.formatXpath(xpath); const errorMessage = `Could not select by attribute "${attribute}" with value "${value}": ${logXpath}`; - xpath = this.normalizeSelector(xpath); + const normalizedXPath = this.normalizeSelector(xpath); await this.waitForExist(xpath, timeout); try { - return await this.client.selectByAttribute(xpath, attribute, value); + return await this.client.selectByAttribute(normalizedXPath, attribute, value); } catch (error) { (error as Error).message = errorMessage; throw error; @@ -1783,8 +1833,8 @@ export class WebApplication extends PluggableModule { public async getLocation(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); - return await this.client.getLocation(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + return await this.client.getLocation(normalizedXPath); } @stepLog(function (this: WebApplication) { @@ -1824,8 +1874,8 @@ export class WebApplication extends PluggableModule { timeout: number = this.WAIT_TIMEOUT, ) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); - return this.client.addValue(xpath, value); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.addValue(normalizedXPath, value); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { @@ -1833,56 +1883,56 @@ export class WebApplication extends PluggableModule { }) public async doubleClick(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { await this.waitForExist(xpath, timeout); - xpath = this.normalizeSelector(xpath); - return this.client.doubleClick(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.doubleClick(normalizedXPath); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { return `Waiting for ${this.formatXpath(xpath)} to be clickable for ${timeout}`; }) public async waitForClickable(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - xpath = this.normalizeSelector(xpath); - return this.client.waitForClickable(xpath, timeout); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.waitForClickable(normalizedXPath, timeout); } @stepLog(function (this: WebApplication, xpath: ElementPath, _timeout: number = this.WAIT_TIMEOUT) { return `Checking if ${this.formatXpath(xpath)} is clickable`; }) public async isClickable(xpath: ElementPath, _timeout: number = this.WAIT_TIMEOUT) { - xpath = this.normalizeSelector(xpath); - return this.client.isClickable(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.isClickable(normalizedXPath); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { return `Waiting for ${this.formatXpath(xpath)} to be enabled for ${timeout}`; }) public async waitForEnabled(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - xpath = this.normalizeSelector(xpath); - return this.client.waitForEnabled(xpath, timeout); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.waitForEnabled(normalizedXPath, timeout); } @stepLog(function (this: WebApplication, xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { return `Waiting for ${this.formatXpath(xpath)} to be stable for ${timeout}`; }) public async waitForStable(xpath: ElementPath, timeout: number = this.WAIT_TIMEOUT) { - xpath = this.normalizeSelector(xpath); - return this.client.waitForStable(xpath, timeout); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.waitForStable(normalizedXPath, timeout); } @stepLog(function (this: WebApplication, xpath: ElementPath) { return `Checking if ${this.formatXpath(xpath)} is focused`; }) public async isFocused(xpath: ElementPath) { - xpath = this.normalizeSelector(xpath); - return this.client.isFocused(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.isFocused(normalizedXPath); } @stepLog(function (this: WebApplication, xpath: ElementPath) { return `Checking if ${this.formatXpath(xpath)} is stable`; }) public async isStable(xpath: ElementPath) { - xpath = this.normalizeSelector(xpath); - return this.client.isStable(xpath); + const normalizedXPath = this.normalizeSelector(xpath); + return this.client.isStable(normalizedXPath); } } diff --git a/packages/web-application/src/web-client.ts b/packages/web-application/src/web-client.ts index 28e0a06a4..d1a6a73cd 100644 --- a/packages/web-application/src/web-client.ts +++ b/packages/web-application/src/web-client.ts @@ -7,6 +7,7 @@ import { SavePdfOptions, WebApplicationMessageType, WindowFeaturesConfig, + Selector, } from '@testring/types'; import {generateUniqId} from '@testring/utils'; @@ -75,12 +76,12 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.getHubConfig, []); } - public click(xpath: string, options?: any) { - return this.makeRequest(BrowserProxyActions.click, [xpath, options]); + public click(selector: Selector, options?: any) { + return this.makeRequest(BrowserProxyActions.click, [selector, options]); } - public getSize(xpath: string) { - return this.makeRequest(BrowserProxyActions.getSize, [xpath]); + public getSize(selector: Selector) { + return this.makeRequest(BrowserProxyActions.getSize, [selector]); } public url(val: any) { @@ -99,27 +100,27 @@ export class WebClient implements IWebApplicationClient { ]); } - public waitForExist(xpath: string, timeout: number) { + public waitForExist(selector: Selector, timeout: number) { return this.makeRequest(BrowserProxyActions.waitForExist, [ - xpath, + selector, timeout, ]); } - public waitForVisible(xpath: string, timeout: number) { + public waitForVisible(selector: Selector, timeout: number) { return this.makeRequest(BrowserProxyActions.waitForVisible, [ - xpath, + selector, timeout, ]); } - public isVisible(xpath: string) { - return this.makeRequest(BrowserProxyActions.isVisible, [xpath]); + public isVisible(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isVisible, [selector]); } - public moveToObject(xpath: string, x: number, y: number) { + public moveToObject(selector: Selector, x: number, y: number) { return this.makeRequest(BrowserProxyActions.moveToObject, [ - xpath, + selector, x, y, ]); @@ -137,8 +138,8 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.getTitle, []); } - public clearValue(xpath: string) { - return this.makeRequest(BrowserProxyActions.clearValue, [xpath]); + public clearValue(selector: Selector) { + return this.makeRequest(BrowserProxyActions.clearValue, [selector]); } public keys(value: any) { @@ -149,49 +150,49 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.elementIdText, [elementId]); } - public elements(xpath: string) { - return this.makeRequest(BrowserProxyActions.elements, [xpath]); + public elements(selector: Selector) { + return this.makeRequest(BrowserProxyActions.elements, [selector]); } - public getValue(xpath: string) { - return this.makeRequest(BrowserProxyActions.getValue, [xpath]); + public getValue(selector: Selector) { + return this.makeRequest(BrowserProxyActions.getValue, [selector]); } - public setValue(xpath: string, value: any) { - return this.makeRequest(BrowserProxyActions.setValue, [xpath, value]); + public setValue(selector: Selector, value: any) { + return this.makeRequest(BrowserProxyActions.setValue, [selector, value]); } - public keysOnElement(xpath: string, value: any) { + public keysOnElement(selector: Selector, value: any) { return this.makeRequest(BrowserProxyActions.keysOnElement, [ - xpath, + selector, value, ]); } - public selectByIndex(xpath: string, value: any) { + public selectByIndex(selector: Selector, value: any) { return this.makeRequest(BrowserProxyActions.selectByIndex, [ - xpath, + selector, value, ]); } - public selectByValue(xpath: string, value: any) { + public selectByValue(selector: Selector, value: any) { return this.makeRequest(BrowserProxyActions.selectByValue, [ - xpath, + selector, value, ]); } - public selectByVisibleText(xpath: string, str: any) { + public selectByVisibleText(selector: Selector, str: any) { return this.makeRequest(BrowserProxyActions.selectByVisibleText, [ - xpath, + selector, str, ]); } - public getAttribute(xpath: string, attr: string) { + public getAttribute(selector: Selector, attr: string) { return this.makeRequest(BrowserProxyActions.getAttribute, [ - xpath, + selector, attr, ]); } @@ -200,17 +201,17 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.windowHandleMaximize, []); } - public isEnabled(xpath: string) { - return this.makeRequest(BrowserProxyActions.isEnabled, [xpath]); + public isEnabled(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isEnabled, [selector]); } - public scroll(xpath: string, x: number, y: number) { - return this.makeRequest(BrowserProxyActions.scroll, [xpath, x, y]); + public scroll(selector: Selector, x: number, y: number) { + return this.makeRequest(BrowserProxyActions.scroll, [selector, x, y]); } - public scrollIntoView(xpath: string, scrollIntoViewOptions?: boolean) { + public scrollIntoView(selector: Selector, scrollIntoViewOptions?: boolean) { return this.makeRequest(BrowserProxyActions.scrollIntoView, [ - xpath, + selector, scrollIntoViewOptions, ]); } @@ -231,10 +232,10 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.alertText, []); } - public dragAndDrop(xpathSource: string, xpathDestination: string) { + public dragAndDrop(sourceSelector: Selector, destinationSelector: Selector) { return this.makeRequest(BrowserProxyActions.dragAndDrop, [ - xpathSource, - xpathDestination, + sourceSelector, + destinationSelector, ]); } @@ -258,8 +259,8 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.deleteCookie, [cookieName]); } - public getHTML(xpath: string, b: any) { - return this.makeRequest(BrowserProxyActions.getHTML, [xpath, b]); + public getHTML(selector: Selector, b: any) { + return this.makeRequest(BrowserProxyActions.getHTML, [selector, b]); } public getCurrentTabId() { @@ -286,16 +287,16 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.windowHandles, []); } - public getTagName(xpath: string) { - return this.makeRequest(BrowserProxyActions.getTagName, [xpath]); + public getTagName(selector: Selector) { + return this.makeRequest(BrowserProxyActions.getTagName, [selector]); } - public isSelected(xpath: string) { - return this.makeRequest(BrowserProxyActions.isSelected, [xpath]); + public isSelected(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isSelected, [selector]); } - public getText(xpath: string) { - return this.makeRequest(BrowserProxyActions.getText, [xpath]); + public getText(selector: Selector) { + return this.makeRequest(BrowserProxyActions.getText, [selector]); } public elementIdSelected(id: string) { @@ -314,9 +315,9 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.kill); } - public getCssProperty(xpath: string, cssProperty: string) { + public getCssProperty(selector: Selector, cssProperty: string) { return this.makeRequest(BrowserProxyActions.getCssProperty, [ - xpath, + selector, cssProperty, ]); } @@ -325,21 +326,21 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.getSource, []); } - public isExisting(xpath: string) { - return this.makeRequest(BrowserProxyActions.isExisting, [xpath]); + public isExisting(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isExisting, [selector]); } - public waitForValue(xpath: string, timeout: number, reverse: boolean) { + public waitForValue(selector: Selector, timeout: number, reverse: boolean) { return this.makeRequest(BrowserProxyActions.waitForValue, [ - xpath, + selector, timeout, reverse, ]); } - public waitForSelected(xpath: string, timeout: number, reverse: boolean) { + public waitForSelected(selector: Selector, timeout: number, reverse: boolean) { return this.makeRequest(BrowserProxyActions.waitForSelected, [ - xpath, + selector, timeout, reverse, ]); @@ -354,9 +355,9 @@ export class WebClient implements IWebApplicationClient { ]); } - public selectByAttribute(xpath: string, attribute: string, value: any) { + public selectByAttribute(selector: Selector, attribute: string, value: any) { return this.makeRequest(BrowserProxyActions.selectByAttribute, [ - xpath, + selector, attribute, value, ]); @@ -421,8 +422,8 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.getActiveElement, []); } - public getLocation(xpath: string) { - return this.makeRequest(BrowserProxyActions.getLocation, [xpath]); + public getLocation(selector: Selector) { + return this.makeRequest(BrowserProxyActions.getLocation, [selector]); } public setTimeZone(timeZone: string) { @@ -437,43 +438,43 @@ export class WebClient implements IWebApplicationClient { return this.makeRequest(BrowserProxyActions.savePDF, [options]); } - public addValue(xpath: string, value: string | number) { - return this.makeRequest(BrowserProxyActions.addValue, [xpath, value]); + public addValue(selector: Selector, value: string | number) { + return this.makeRequest(BrowserProxyActions.addValue, [selector, value]); } - public doubleClick(xpath: string) { - return this.makeRequest(BrowserProxyActions.doubleClick, [xpath]); + public doubleClick(selector: Selector) { + return this.makeRequest(BrowserProxyActions.doubleClick, [selector]); } - public isClickable(xpath: string) { - return this.makeRequest(BrowserProxyActions.isClickable, [xpath]); + public isClickable(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isClickable, [selector]); } - public waitForClickable(xpath: string, timeout: number) { + public waitForClickable(selector: Selector, timeout: number) { return this.makeRequest(BrowserProxyActions.waitForClickable, [ - xpath, + selector, timeout, ]); } - public isFocused(xpath: string) { - return this.makeRequest(BrowserProxyActions.isFocused, [xpath]); + public isFocused(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isFocused, [selector]); } - public isStable(xpath: string) { - return this.makeRequest(BrowserProxyActions.isStable, [xpath]); + public isStable(selector: Selector) { + return this.makeRequest(BrowserProxyActions.isStable, [selector]); } - public waitForEnabled(xpath: string, timeout: number) { + public waitForEnabled(selector: Selector, timeout: number) { return this.makeRequest(BrowserProxyActions.waitForEnabled, [ - xpath, + selector, timeout, ]); } - public waitForStable(xpath: string, timeout: number) { + public waitForStable(selector: Selector, timeout: number) { return this.makeRequest(BrowserProxyActions.waitForStable, [ - xpath, + selector, timeout, ]); } From 68b3b68b5686dfa2d456bd97c6c36e3317c3c4ec Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 8 Oct 2025 15:19:11 +0300 Subject: [PATCH 3/6] WAT-5214 --- packages/web-application/src/web-application.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/web-application/src/web-application.ts b/packages/web-application/src/web-application.ts index c57b27342..80e60bdd8 100644 --- a/packages/web-application/src/web-application.ts +++ b/packages/web-application/src/web-application.ts @@ -170,6 +170,11 @@ export class WebApplication extends PluggableModule { } protected formatXpath(xpath: ElementPath): string { + if (this.isShadowElementPathProxy(xpath)) { + const parentSelectors = xpath.getParentSelectors(); + const targetSelector = xpath.toShadowCSSSelector(); + return [...parentSelectors, targetSelector].join(' >>> '); + } return utils.getFormattedString(xpath); } From fb8b4a9513e586512ef1764217e4511c5d0c5212 Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 8 Oct 2025 16:31:51 +0300 Subject: [PATCH 4/6] WAT-5214 --- .../plugin-selenium-driver/src/plugin/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index 0c2d13ac1..9f3aa4055 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -191,6 +191,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { if (this.config.host === undefined) { this.runLocalSelenium(); + this.setupProcessCleanup(); } this.initIntervals(); @@ -244,6 +245,19 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { } } + private setupProcessCleanup() { + process.on('SIGINT', () => this.forceKillSelenium()); + process.on('SIGTERM', () => this.forceKillSelenium()); + process.on('SIGKILL', () => this.forceKillSelenium()); + } + + private forceKillSelenium() { + if (this.localSelenium && !this.localSelenium.killed) { + this.logger.debug('Force killing Selenium process due to signal'); + this.localSelenium.kill('SIGKILL'); + } + } + private stopAllSessions() { const clientsRequests: Promise[] = []; From 96e8b364c6910448fd88b3cb19a16cf6cf1b8ee7 Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 8 Oct 2025 17:38:20 +0300 Subject: [PATCH 5/6] WAT-5214 --- .../src/browser-proxy/browser-proxy.ts | 1 + .../static-fixtures/shadow-click.html | 14 +++++ .../test/selenium/test/shadow-click.spec.js | 54 +++++++++++++++++++ .../src/plugin/index.ts | 18 +++++-- 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/browser-proxy/src/browser-proxy/browser-proxy.ts b/packages/browser-proxy/src/browser-proxy/browser-proxy.ts index 9842acd7a..1e3bbc096 100644 --- a/packages/browser-proxy/src/browser-proxy/browser-proxy.ts +++ b/packages/browser-proxy/src/browser-proxy/browser-proxy.ts @@ -50,6 +50,7 @@ export class BrowserProxy { try { this.plugin = pluginFactory(pluginConfig); } catch (error) { + this.logger.error(`Can't initialize plugin ${pluginPath}`, error); this.transportInstance.broadcastUniversally( BrowserProxyMessageTypes.exception, error instanceof Error ? error : new Error(String(error)), diff --git a/packages/e2e-test-app/static-fixtures/shadow-click.html b/packages/e2e-test-app/static-fixtures/shadow-click.html index 83df6eede..c6b9f1b77 100644 --- a/packages/e2e-test-app/static-fixtures/shadow-click.html +++ b/packages/e2e-test-app/static-fixtures/shadow-click.html @@ -139,6 +139,13 @@

Nested Shadow DOM

Shadow click registered!
+
+ + +
+
+

Initial shadow text content

+
`; @@ -215,6 +222,13 @@

Nested Shadow DOM

Nested shadow click registered!
+
+ + +
+
+

Initial nested shadow text

+
`; diff --git a/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js b/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js index 916c9f9d3..02d17bec4 100644 --- a/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js +++ b/packages/e2e-test-app/test/selenium/test/shadow-click.spec.js @@ -33,6 +33,26 @@ run(async (api) => { const shadowResultText = await app.getText(app.root.shadowSection.shadowResult); await app.assert.equal(shadowResultText, 'Shadow button clicked successfully!'); + // Test 2a: Test isVisible on shadow DOM elements + const shadowTextVisible = await app.isVisible(app.root.shadowHost.shadow$.shadowText); + await app.assert.equal(shadowTextVisible, true, 'Shadow text should be visible'); + + const shadowButtonVisible = await app.isVisible(app.root.shadowHost.shadow$.shadowButton); + await app.assert.equal(shadowButtonVisible, true, 'Shadow button should be visible'); + + // Test 2b: Test getText on shadow DOM elements + const shadowTextContent = await app.getText(app.root.shadowHost.shadow$.shadowText); + await app.assert.equal(shadowTextContent, 'This is inside shadow DOM', 'Shadow text content should match'); + + // Test 2c: Test setValue and getValue on shadow DOM input + await app.setValue(app.root.shadowHost.shadow$.shadowInput, 'Test shadow input value'); + const shadowInputValue = await app.getValue(app.root.shadowHost.shadow$.shadowInput); + await app.assert.equal(shadowInputValue, 'Test shadow input value', 'Shadow input value should match set value'); + + // Test 2d: Test getText on shadow DOM display text + const shadowDisplayText = await app.getText(app.root.shadowHost.shadow$.shadowDisplayText); + await app.assert.equal(shadowDisplayText, 'Initial shadow text content', 'Shadow display text should match'); + // Test 3: Click nested shadow DOM button using new shadow$ concept // For nested shadow DOM, shadow$ can be chained to access deeper shadow elements // app.root.nestedShadowHost - path to nested shadow host element @@ -44,6 +64,26 @@ run(async (api) => { const nestedShadowResultText = await app.getText(app.root.nestedShadowSection.nestedShadowResult); await app.assert.equal(nestedShadowResultText, 'Nested shadow button clicked successfully!'); + // Test 3a: Test isVisible on nested shadow DOM elements + const nestedShadowTextVisible = await app.isVisible(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.innerShadowText); + await app.assert.equal(nestedShadowTextVisible, true, 'Nested shadow text should be visible'); + + const nestedShadowButtonVisible = await app.isVisible(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowButton); + await app.assert.equal(nestedShadowButtonVisible, true, 'Nested shadow button should be visible'); + + // Test 3b: Test getText on nested shadow DOM elements + const nestedShadowTextContent = await app.getText(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.innerShadowText); + await app.assert.equal(nestedShadowTextContent, 'This is nested inner shadow DOM', 'Nested shadow text content should match'); + + // Test 3c: Test setValue and getValue on nested shadow DOM input + await app.setValue(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowInput, 'Test nested shadow input value'); + const nestedShadowInputValue = await app.getValue(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowInput); + await app.assert.equal(nestedShadowInputValue, 'Test nested shadow input value', 'Nested shadow input value should match set value'); + + // Test 3d: Test getText on nested shadow DOM display text + const nestedShadowDisplayText = await app.getText(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowDisplayText); + await app.assert.equal(nestedShadowDisplayText, 'Initial nested shadow text', 'Nested shadow display text should match'); + // Verify all results are visible const regularResultVisible = await app.isVisible(app.root.regularSection.regularResult); const shadowResultVisible = await app.isVisible(app.root.shadowSection.shadowResult); @@ -52,4 +92,18 @@ run(async (api) => { await app.assert.equal(regularResultVisible, true, 'Regular result should be visible'); await app.assert.equal(shadowResultVisible, true, 'Shadow result should be visible'); await app.assert.equal(nestedShadowResultVisible, true, 'Nested shadow result should be visible'); + + // Additional verification for shadow DOM input fields + const shadowInputVisible = await app.isVisible(app.root.shadowHost.shadow$.shadowInput); + const nestedShadowInputVisible = await app.isVisible(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowInput); + + await app.assert.equal(shadowInputVisible, true, 'Shadow input should be visible'); + await app.assert.equal(nestedShadowInputVisible, true, 'Nested shadow input should be visible'); + + // Verify shadow DOM text elements are visible + const shadowDisplayTextVisible = await app.isVisible(app.root.shadowHost.shadow$.shadowDisplayText); + const nestedShadowDisplayTextVisible = await app.isVisible(app.root.nestedShadowHost.shadow$.outerShadowContent.innerShadowHost.shadow$.nestedShadowDisplayText); + + await app.assert.equal(shadowDisplayTextVisible, true, 'Shadow display text should be visible'); + await app.assert.equal(nestedShadowDisplayTextVisible, true, 'Nested shadow display text should be visible'); }); \ No newline at end of file diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index 9f3aa4055..b511b4021 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -570,7 +570,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { } } - private async traverseShadowDom(client: BrowserObjectCustom, css: string, parentSelectors: string[]) { + private async traverseToLastParentSelector(client: BrowserObjectCustom, parentSelectors: string[]) { const [firstParentSelector, ...restParentSelectors] = parentSelectors; // TypeScript assertion: we know firstParentSelector exists due to validation @@ -594,8 +594,15 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { currentElement = shadowElement; } + return currentElement; + } + + private async traverseShadowDom(client: BrowserObjectCustom, css: string, parentSelectors: string[]) { + // Traverse to the last parent selector + const lastParentElement = await this.traverseToLastParentSelector(client, parentSelectors); + // Get the final target element within the shadow DOM - const targetElement = await currentElement.shadow$(css); + const targetElement = await lastParentElement.shadow$(css); if (!targetElement) { throw new Error(`Failed to find target element with CSS selector: ${css}`); } @@ -878,8 +885,11 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { return {ELEMENT: o[firstKey]}; }); } else if (isShadowCssSelector(selector)) { - // TODO: Implement shadow DOM CSS selector logic - throw new Error('ShadowCssSelector not implemented yet'); + const lastParentElement = await this.traverseToLastParentSelector(client, selector.parentSelectors); + const elements = lastParentElement.shadow$$(selector.css); + return elements.map((element) => { + return {ELEMENT: element.elementId}; + }); } throw new Error('Unknown selector type'); From 2d3337de350317f920c3829b05309febe56b3ee7 Mon Sep 17 00:00:00 2001 From: "artem.sukhinin" Date: Wed, 8 Oct 2025 17:46:24 +0300 Subject: [PATCH 6/6] WAT-5214 --- packages/plugin-selenium-driver/src/plugin/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-selenium-driver/src/plugin/index.ts b/packages/plugin-selenium-driver/src/plugin/index.ts index b511b4021..803238d5d 100644 --- a/packages/plugin-selenium-driver/src/plugin/index.ts +++ b/packages/plugin-selenium-driver/src/plugin/index.ts @@ -248,7 +248,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin { private setupProcessCleanup() { process.on('SIGINT', () => this.forceKillSelenium()); process.on('SIGTERM', () => this.forceKillSelenium()); - process.on('SIGKILL', () => this.forceKillSelenium()); + // Note: SIGKILL cannot be caught or handled - it immediately terminates the process } private forceKillSelenium() {