From 2b6a2f64aec7b04ddf803d4a9b98455c7e606ed2 Mon Sep 17 00:00:00 2001 From: talkenson Date: Thu, 29 May 2025 14:45:28 +0700 Subject: [PATCH 1/4] feat: add support of @testing-library/dom queries --- package-lock.json | 189 +++++++++++++++++++++- package.json | 1 + src/browser/browser.ts | 5 + src/browser/existing-browser.ts | 1 + src/browser/new-browser.ts | 1 + src/browser/queries/index.ts | 271 ++++++++++++++++++++++++++++++++ src/browser/queries/types.ts | 70 +++++++++ src/browser/types.ts | 4 +- src/index.ts | 3 + 9 files changed, 539 insertions(+), 6 deletions(-) create mode 100644 src/browser/queries/index.ts create mode 100644 src/browser/queries/types.ts diff --git a/package-lock.json b/package-lock.json index 5af7c5b5d..f9bd8b385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@jspm/core": "2.0.1", "@puppeteer/browsers": "2.7.1", "@rrweb/record": "2.0.0-alpha.18", + "@testing-library/dom": "10.4.0", "@testplane/devtools": "8.32.3", "@testplane/wdio-protocols": "9.4.6", "@testplane/wdio-utils": "9.5.3", @@ -708,7 +709,6 @@ }, "node_modules/@babel/runtime": { "version": "7.24.1", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2332,6 +2332,82 @@ "node": ">=14.16" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/@testplane/devtools": { "version": "8.32.3", "resolved": "https://registry.npmjs.org/@testplane/devtools/-/devtools-8.32.3.tgz", @@ -3009,6 +3085,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT" + }, "node_modules/@types/babel__code-frame": { "version": "7.0.6", "dev": true, @@ -6458,6 +6540,15 @@ "deps-sort": "bin/cmd.js" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/des.js": { "version": "1.0.1", "dev": true, @@ -6582,6 +6673,12 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "1.4.1", "dev": true, @@ -9854,6 +9951,15 @@ "dev": true, "license": "ISC" }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "dev": true, @@ -11954,7 +12060,6 @@ }, "node_modules/regenerator-runtime": { "version": "0.14.1", - "dev": true, "license": "MIT" }, "node_modules/regexpp": { @@ -14837,7 +14942,6 @@ }, "@babel/runtime": { "version": "7.24.1", - "dev": true, "requires": { "regenerator-runtime": "^0.14.0" } @@ -15867,6 +15971,62 @@ "defer-to-connect": "^2.0.1" } }, + "@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "requires": { + "dequal": "^2.0.3" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "@testplane/devtools": { "version": "8.32.3", "resolved": "https://registry.npmjs.org/@testplane/devtools/-/devtools-8.32.3.tgz", @@ -16391,6 +16551,11 @@ "version": "1.0.3", "dev": true }, + "@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" + }, "@types/babel__code-frame": { "version": "7.0.6", "dev": true @@ -18732,6 +18897,11 @@ "through2": "^2.0.0" } }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + }, "des.js": { "version": "1.0.1", "dev": true, @@ -18817,6 +18987,11 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + }, "dom-serializer": { "version": "1.4.1", "dev": true, @@ -20914,6 +21089,11 @@ } } }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" + }, "make-error": { "version": "1.3.6", "dev": true @@ -22281,8 +22461,7 @@ } }, "regenerator-runtime": { - "version": "0.14.1", - "dev": true + "version": "0.14.1" }, "regexpp": { "version": "3.2.0", diff --git a/package.json b/package.json index b567c573f..834430890 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@jspm/core": "2.0.1", "@puppeteer/browsers": "2.7.1", "@rrweb/record": "2.0.0-alpha.18", + "@testing-library/dom": "10.4.0", "@testplane/devtools": "8.32.3", "@testplane/wdio-protocols": "9.4.6", "@testplane/wdio-utils": "9.5.3", diff --git a/src/browser/browser.ts b/src/browser/browser.ts index a7f304561..5fa0b7eaa 100644 --- a/src/browser/browser.ts +++ b/src/browser/browser.ts @@ -13,6 +13,7 @@ import { AsyncEmitter } from "../events"; import { BrowserConfig } from "../config/browser-config"; import type { Callstack } from "./history/callstack"; import type { WdProcess, WebdriverPool } from "../browser-pool/webdriver-pool"; +import { setupBrowser } from "./queries"; const CUSTOM_SESSION_OPTS = [ "outputDir", @@ -102,6 +103,10 @@ export class Browser { this._addExtendOptionsMethod(this._session!); } + protected _addQueries(): void { + setupBrowser(this._session!); + } + protected _addSteps(): void { addRunStepCommand(this); } diff --git a/src/browser/existing-browser.ts b/src/browser/existing-browser.ts index b4ea002aa..805454173 100644 --- a/src/browser/existing-browser.ts +++ b/src/browser/existing-browser.ts @@ -84,6 +84,7 @@ export class ExistingBrowser extends Browser { this._extendStacktrace(); this._addSteps(); + this._addQueries(); this._addHistory(); await history.runGroup( diff --git a/src/browser/new-browser.ts b/src/browser/new-browser.ts index c0f857b62..e5fcd93e0 100644 --- a/src/browser/new-browser.ts +++ b/src/browser/new-browser.ts @@ -73,6 +73,7 @@ export class NewBrowser extends Browser { this._extendStacktrace(); this._addSteps(); + this._addQueries(); this._addHistory(); await runGroup( diff --git a/src/browser/queries/index.ts b/src/browser/queries/index.ts new file mode 100644 index 000000000..16996f5b6 --- /dev/null +++ b/src/browser/queries/index.ts @@ -0,0 +1,271 @@ +/* +Portions of this functionality are copyright of their respective authors +and released under the MIT license: +https://github.com/testing-library/webdriverio-testing-library +*/ + +import path from "path"; +import fs from "fs"; +import { + Matcher, + MatcherOptions, + queries as baseQueries, + waitForOptions as WaitForOptions, +} from "@testing-library/dom"; + +import { + QueryArg, + Config, + QueryName, + TestplaneQueries, + TestplaneQueriesChainable, + ObjectQueryArg, + SerializedObject, + SerializedArg, + SelectorsBase, +} from "./types"; +import { ElementBase } from "@testplane/webdriverio"; + +declare global { + interface Window { + TestingLibraryDom: typeof baseQueries & { + configure: typeof configure; + }; + } +} + +/* +eslint-disable +@typescript-eslint/explicit-function-return-type +*/ + +const DOM_TESTING_LIBRARY_UMD_PATH = path.join( + require.resolve("@testing-library/dom"), + "../../", + "dist/@testing-library/dom.umd.js", +); +const DOM_TESTING_LIBRARY_UMD = fs.readFileSync(DOM_TESTING_LIBRARY_UMD_PATH).toString().replace("define.amd", "false"); + +let _config: Partial; + +function isContainerWithExecute(container: ElementBase | WebdriverIO.Browser): container is WebdriverIO.Browser { + return (container as { execute?: unknown }).execute !== null; +} + +function findContainerWithExecute(container: ElementBase): WebdriverIO.Browser { + let curContainer: ElementBase | WebdriverIO.Browser = container.parent; + while (!isContainerWithExecute(curContainer)) { + curContainer = curContainer.parent; + } + return curContainer; +} + +async function injectDOMTestingLibrary(container: ElementBase) { + const containerWithExecute = findContainerWithExecute(container); + const shouldInjectDTL = await containerWithExecute.execute(function () { + return !window.TestingLibraryDom; + }); + + if (shouldInjectDTL) { + await containerWithExecute.execute(function (library) { + // add DOM Testing Library to page as a script tag to support Firefox + if (navigator.userAgent.indexOf("Firefox") !== -1) { + const script = window.document.createElement("script"); + script.textContent = library; + window.document.head.append(script); + window.eval(library); + } else { + eval(library); + } + }, DOM_TESTING_LIBRARY_UMD); + } + + await containerWithExecute.execute(function (config: Partial) { + window.TestingLibraryDom.configure(config); + }, _config); +} + +function serializeObject(object: ObjectQueryArg): SerializedObject { + return Object.entries(object) + .map<[string, SerializedArg]>(([key, value]: [string, QueryArg]) => [key, serializeArg(value)]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), { + serialized: "object", + }); +} + +function serializeArg(arg: QueryArg): SerializedArg { + if (arg instanceof RegExp) { + return { serialized: "RegExp", RegExp: arg.toString() }; + } + if (typeof arg === "undefined") { + return { serialized: "Undefined", Undefined: true }; + } + if (arg && typeof arg === "object") { + return serializeObject(arg); + } + return arg; +} + +type SerializedQueryResult = { selector: string }[] | string | { selector: string } | null; + +async function executeQuery(query: QueryName, container: HTMLElement, ...args: SerializedArg[]) { + // const done = args.pop() as unknown as (result: SerializedQueryResult) => void; + return new Promise((done: (result: SerializedQueryResult) => void) => { + function deserializeObject(object: SerializedObject) { + return Object.entries(object) + .map<[string, QueryArg]>(([key, value]) => [key, deserializeArg(value)]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + } + + function deserializeArg(arg: SerializedArg): QueryArg { + if (typeof arg === "object" && arg.serialized === "RegExp") { + return eval(arg.RegExp); + } + if (typeof arg === "object" && arg.serialized === "Undefined") { + return undefined; + } + if (typeof arg === "object") { + return deserializeObject(arg); + } + return arg; + } + + const [matcher, options, waitForOptions] = args.map(deserializeArg); + + void (async () => { + let result: ReturnType<(typeof window.TestingLibraryDom)[typeof query]> = null; + try { + // Override RegExp to fix 'matcher instanceof RegExp' check on Firefox + window.RegExp = RegExp; + + result = await window.TestingLibraryDom[query]( + container, + // @ts-expect-error Matcher can be any type from @testing-library/dom + matcher as Matcher, + options as MatcherOptions, + waitForOptions as WaitForOptions, + ); + } catch (e: unknown) { + return done((e as Error).message); + } + + if (!result) { + return done(null); + } + + function makeSelectorResult(element: HTMLElement): { selector: string } { + const elementIdAttributeName = "data-wdio-testing-lib-element-id"; + let elementId = element.getAttribute(elementIdAttributeName); + + // if id doesn't already exist create one and add it to element + if (!elementId) { + elementId = (Math.abs(Math.random()) * 1000000000000).toFixed(0); + element.setAttribute(elementIdAttributeName, elementId); + } + + return { selector: `[${elementIdAttributeName}="${elementId}"]` }; + } + + if (Array.isArray(result)) { + return done(result.map(makeSelectorResult)); + } + + return done(makeSelectorResult(result)); + })(); + }); +} + +function createQuery(container: ElementBase & SelectorsBase, queryName: QueryName) { + return async (...args: QueryArg[]) => { + await injectDOMTestingLibrary(container); + + const result: SerializedQueryResult = await findContainerWithExecute(container).execute( + executeQuery, + queryName, + container as unknown as HTMLElement, + ...args.map(serializeArg), + ); + + if (typeof result === "string") { + throw new Error(result); + } + + if (!result) { + return null; + } + + if (Array.isArray(result)) { + return Promise.all(result.map(({ selector }) => container.$(selector))); + } + + return container.$(result.selector); + }; +} + +function within(element: ElementBase & SelectorsBase) { + return (Object.keys(baseQueries) as QueryName[]).reduce( + (queries, queryName) => ({ + ...queries, + [queryName]: createQuery(element, queryName), + }), + {}, + ) as TestplaneQueries; +} + +/* +eslint-disable +@typescript-eslint/no-explicit-any, +@typescript-eslint/no-unsafe-argument +*/ + +// Patches (via addCommand) the browser object with the DomTestingLibrary queries +function setupBrowser(browser: Browser): TestplaneQueries { + const queries: { [key: string | number | symbol]: TestplaneQueries[QueryName] } = {}; + + Object.keys(baseQueries).forEach(key => { + const queryName = key as QueryName; + + const query = async (...args: Parameters) => { + const body = await browser.$("body"); + return within(body as ElementBase & SelectorsBase)[queryName](...(args as any[])); + }; + + // add query to response queries + queries[queryName] = query as TestplaneQueries[QueryName]; + + // add query to BrowserObject and Elements + browser.addCommand(queryName, query as TestplaneQueries[QueryName]); + browser.addCommand( + queryName, + function (this: ElementBase & SelectorsBase, ...args: any) { + return within(this)[queryName](...args); + }, + true, + ); + + // add chainable query to BrowserObject and Elements + browser.addCommand(`${queryName}$`, query as TestplaneQueriesChainable[`${QueryName}$`]); + browser.addCommand( + `${queryName}$`, + function (this: ElementBase & SelectorsBase, ...args) { + return within(this)[queryName](...args); + }, + true, + ); + }); + + return queries as unknown as TestplaneQueries; +} + +/* +eslint-enable +@typescript-eslint/no-explicit-any, +@typescript-eslint/no-unsafe-argument +*/ + +function configure(config: Partial) { + _config = config; +} + +export * from "./types"; +export { within, setupBrowser, configure }; diff --git a/src/browser/queries/types.ts b/src/browser/queries/types.ts new file mode 100644 index 000000000..4d2d00595 --- /dev/null +++ b/src/browser/queries/types.ts @@ -0,0 +1,70 @@ +import { + Config as BaseConfig, + BoundFunction as BoundFunctionBase, + queries, + waitForOptions, + SelectorMatcherOptions, + MatcherOptions, +} from "@testing-library/dom"; + +export type SelectorsBase = Pick; + +export type Queries = typeof queries; +export type QueryName = keyof Queries; + +export type Config = Pick< + BaseConfig, + | "asyncUtilTimeout" + | "computedStyleSupportsPseudoElements" + | "defaultHidden" + | "testIdAttribute" + | "throwSuggestions" +>; + +export type QueryReturnType = T extends Promise + ? Element + : T extends HTMLElement + ? Element + : T extends Promise + ? ElementArray + : T extends HTMLElement[] + ? ElementArray + : T extends null + ? null + : never; + +export type BoundFunction = ( + ...params: Parameters> +) => Promise>>>; + +export type BoundFunctionSync = ( + ...params: Parameters> +) => QueryReturnType>>; + +export type TestplaneQueries = { + [P in keyof Queries]: BoundFunction; +}; + +export type TestplaneQueriesSync = { + [P in keyof Queries]: BoundFunctionSync; +}; + +export type TestplaneQueriesChainable = { + [P in keyof Queries as `${string & P}$`]: Container extends SelectorsBase + ? BoundFunctionSync, ReturnType, Queries[P]> + : undefined; +}; + +export type ObjectQueryArg = MatcherOptions | queries.ByRoleOptions | SelectorMatcherOptions | waitForOptions; + +export type QueryArg = ObjectQueryArg | RegExp | number | string | undefined; + +export type SerializedObject = { + serialized: "object"; + // eslint-disable-next-line no-use-before-define + [key: string]: SerializedArg; +}; +export type SerializedRegExp = { serialized: "RegExp"; RegExp: string }; +export type SerializedUndefined = { serialized: "Undefined"; Undefined: true }; + +export type SerializedArg = SerializedObject | SerializedRegExp | SerializedUndefined | number | string; diff --git a/src/browser/types.ts b/src/browser/types.ts index 9f3bd7cf3..5ec4e4b0a 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -23,6 +23,7 @@ export type W3CBrowserName = Exclude<(typeof BrowserName)[keyof typeof BrowserNa export interface BrowserMeta { pid: number; browserVersion: string; + [name: string]: unknown; } @@ -57,8 +58,9 @@ declare global { ...args: Parameters ) => ReturnType; - interface Browser { + interface Browser extends TestplaneQueries, TestplaneQueriesSync, TestplaneQueriesChainable { getMeta(this: WebdriverIO.Browser): Promise; + getMeta(this: WebdriverIO.Browser, key: string): Promise; setMeta(this: WebdriverIO.Browser, key: string, value: unknown): Promise; diff --git a/src/index.ts b/src/index.ts index 488d0d673..edccb9ee5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,12 +7,14 @@ import "./browser/types"; import "expect-webdriverio"; import { GlobalHelper } from "./types"; + export { run as runCli } from "./cli"; export { Testplane as default } from "./testplane"; export { Key } from "@testplane/webdriverio"; export * from "./mock"; export * as unstable from "./unstable"; +export * as queries from "./browser/queries"; export type { WdioBrowser, @@ -40,6 +42,7 @@ export type { export type { StatsResult } from "./stats"; import type { TestDefinition, SuiteDefinition, TestHookDefinition } from "./test-reader/test-object/types"; + export type { TestDefinition, SuiteDefinition, TestHookDefinition }; declare global { From 04338c7e1ab0ab6c5eb817135f50a5bfe6d646e9 Mon Sep 17 00:00:00 2001 From: talkenson Date: Thu, 29 May 2025 14:45:28 +0700 Subject: [PATCH 2/4] feat: add support of @testing-library/dom queries --- src/browser/browser.ts | 3 ++- src/browser/queries/index.ts | 30 +++++++++++++++++++----------- src/config/defaults.js | 1 + src/config/types.ts | 9 +++++++++ src/index.ts | 1 - 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/browser/browser.ts b/src/browser/browser.ts index 5fa0b7eaa..8b83f0b09 100644 --- a/src/browser/browser.ts +++ b/src/browser/browser.ts @@ -13,7 +13,7 @@ import { AsyncEmitter } from "../events"; import { BrowserConfig } from "../config/browser-config"; import type { Callstack } from "./history/callstack"; import type { WdProcess, WebdriverPool } from "../browser-pool/webdriver-pool"; -import { setupBrowser } from "./queries"; +import { configure, setupBrowser } from "./queries"; const CUSTOM_SESSION_OPTS = [ "outputDir", @@ -104,6 +104,7 @@ export class Browser { } protected _addQueries(): void { + configure({ testIdAttribute: this._config.testIdAttribute, asyncUtilTimeout: this._config.waitTimeout }); setupBrowser(this._session!); } diff --git a/src/browser/queries/index.ts b/src/browser/queries/index.ts index 16996f5b6..0ad5541a2 100644 --- a/src/browser/queries/index.ts +++ b/src/browser/queries/index.ts @@ -61,26 +61,35 @@ function findContainerWithExecute(container: ElementBase): WebdriverIO.Browser { } async function injectDOMTestingLibrary(container: ElementBase) { - const containerWithExecute = findContainerWithExecute(container); - const shouldInjectDTL = await containerWithExecute.execute(function () { + const browser = findContainerWithExecute(container); + const shouldInjectDTL = await browser.execute(function () { return !window.TestingLibraryDom; }); if (shouldInjectDTL) { - await containerWithExecute.execute(function (library) { - // add DOM Testing Library to page as a script tag to support Firefox + await browser.execute(function (library) { if (navigator.userAgent.indexOf("Firefox") !== -1) { - const script = window.document.createElement("script"); - script.textContent = library; - window.document.head.append(script); - window.eval(library); + try { + // Inject via inline-script + const script = window.document.createElement("script"); + script.textContent = library; + window.document.head.append(script); + if (!window.TestingLibraryDom) { + // Inject via eval + window.eval(library); + } + } catch (error) { + throw new Error( + `The DOM Testing Library cannot be injected on certain domains, particularly "${window.location.host}", due to restrictions imposed by the Content-Security-Policy (CSP) header.`, + ); + } } else { eval(library); } }, DOM_TESTING_LIBRARY_UMD); } - await containerWithExecute.execute(function (config: Partial) { + await browser.execute(function (config: Partial) { window.TestingLibraryDom.configure(config); }, _config); } @@ -109,7 +118,6 @@ function serializeArg(arg: QueryArg): SerializedArg { type SerializedQueryResult = { selector: string }[] | string | { selector: string } | null; async function executeQuery(query: QueryName, container: HTMLElement, ...args: SerializedArg[]) { - // const done = args.pop() as unknown as (result: SerializedQueryResult) => void; return new Promise((done: (result: SerializedQueryResult) => void) => { function deserializeObject(object: SerializedObject) { return Object.entries(object) @@ -154,7 +162,7 @@ async function executeQuery(query: QueryName, container: HTMLElement, ...args: S } function makeSelectorResult(element: HTMLElement): { selector: string } { - const elementIdAttributeName = "data-wdio-testing-lib-element-id"; + const elementIdAttributeName = "data-testplane-element-id"; let elementId = element.getAttribute(elementIdAttributeName); // if id doesn't already exist create one and add it to element diff --git a/src/config/defaults.js b/src/config/defaults.js index be013bd9d..70b315c83 100644 --- a/src/config/defaults.js +++ b/src/config/defaults.js @@ -118,6 +118,7 @@ module.exports = { }, passive: false, timeTravel: TimeTravelMode.Off, + testIdAttribute: "data-testid", }; module.exports.configPaths = [ diff --git a/src/config/types.ts b/src/config/types.ts index 6000557f7..a1248525b 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -277,7 +277,9 @@ export interface CommonConfig { sessionsPerBrowser: number; testsPerSession: number; retry: number; + shouldRetry(testInfo: { ctx: Test; retriesLeft: number }): boolean | null; + httpTimeout: number; urlHttpTimeout: number | null; pageLoadTimeout: number | null; @@ -293,9 +295,13 @@ export interface CommonConfig { }; takeScreenshotOnFailsTimeout: number | null; takeScreenshotOnFailsMode: "fullpage" | "viewport"; + prepareBrowser(browser: WebdriverIO.Browser): void | null; + screenshotPath: string | null; + screenshotsDir(test: Test): string; + calibrate: boolean; compositeImage: boolean; strictTestsOrder: boolean; @@ -350,6 +356,8 @@ export interface CommonConfig { }; timeTravel: TimeTravelConfig; + + testIdAttribute?: string; } export interface SetsConfig { @@ -386,6 +394,7 @@ export interface ConfigParsed extends CommonConfig { export interface RuntimeConfig { extend: (data: unknown) => this; + [key: string]: unknown; } diff --git a/src/index.ts b/src/index.ts index edccb9ee5..10e866d89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ export { Key } from "@testplane/webdriverio"; export * from "./mock"; export * as unstable from "./unstable"; -export * as queries from "./browser/queries"; export type { WdioBrowser, From 16b5880fa19f903186869df68816317d6278d95b Mon Sep 17 00:00:00 2001 From: talkenson Date: Fri, 27 Jun 2025 17:38:56 +0700 Subject: [PATCH 3/4] feat: add custom firefox extension to bypass csp --- assets/csp-bypass@testplane.io.xpi | Bin 0 -> 9103 bytes package.json | 2 +- src/browser/browser.ts | 8 ++++++++ src/browser/existing-browser.ts | 1 + src/browser/new-browser.ts | 1 + src/browser/queries/firefoxCSPAddOn.ts | 7 +++++++ 6 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 assets/csp-bypass@testplane.io.xpi create mode 100644 src/browser/queries/firefoxCSPAddOn.ts diff --git a/assets/csp-bypass@testplane.io.xpi b/assets/csp-bypass@testplane.io.xpi new file mode 100644 index 0000000000000000000000000000000000000000..96875bacc62c4c0eba36f601d72a73caaecaa79a GIT binary patch literal 9103 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
  • `+kP(g0UDYi%?=$hHCAcA2@Dg`*7@uUj3jVfdGsCOXOU z&ez^K(6gAgWaN3s#Hx1OVDEYUt?uG9K}+xA2uTf#^e@3A4^m^xbnc27muKJUn;UUF z(t!@V1@-!*;2Jr?9+4!tF7S=Uf@gxyCI7!23jZiZI3RMua$U-{*fs-_MO#3E>H_sfPKXmh-R zeqV4v@g>vtHlIND*JYsq+6UJk{?_yjg&6a|;a>@$2S_}ZoL@Bh=&S)y)|waa;Z<>p zi}wKG;KQP8ejzPg&UCepF+qs>s}r}|z&%0!AR_t5!}Z-+=X6a6=dN=bjI9Zm>h@OG z-NSFX4KzB+!C#kLt_;}lX`?@A@io_G*p~EoO0pW-UJ~lYTmSmCz&gieoZ`YqfG1sg zY9g0Vy@aSo%VZzB511=4U5iKf;&3O7ZqgYg3a>{F4@J@TO9Vk=1ubetIn{fa;(0}I zZQ?qJj{du&n?E3;Wve z#QHKKKP~}LvanDIaSkuC2}X-&m-P-EK~*!M>w@R2+sW64Aw&odS7UGIEbH`Z8V;EY zo=(2j_a$!9>~gD%xk9!<3$RfT(`yYAuI>k#&5^y~8y{PDZ}!TIU0?D_TVVc6#HSl$bccZGVzkeRCep{wYn%A|0)<^WnU*)LziG%O3h zr}d6Ge9>e4X~DPTa1dUw$`Tt|+TCKaKx`fytNUHcaH%V~;sRB8b}~yU+N$DQ)@&U9 zI>!OK9+tGn@4WuWAMVc{6{%V_GyP>70^^0h0?I?f;6nYiF!B3xz#j_}f0zHvI{sLn z_^0acJoVpISzj9BzY)^^RQ;U={)6lNOO{^4S>DIqnX?f|!f3xzhFn*{0 z{-9?6k}oeS6#r+U_Ma#DeT?)6N%)tj!vE983uX9E_1}jGe{|V@i5%u{>i>aZZ}CqH zzjyF|3>*HE2<+c2{Acv=PXoW_$ls3ue6jZ*FWCRp_5 { + if (getNormalizedBrowserName(this._session!.capabilities.browserName) !== "firefox") return Promise.resolve(""); + const firefoxCSPAddOn = await getFirefoxCSPAddOn(); + return this._session!.installAddOn(firefoxCSPAddOn, false); + } + protected _addQueries(): void { configure({ testIdAttribute: this._config.testIdAttribute, asyncUtilTimeout: this._config.waitTimeout }); setupBrowser(this._session!); diff --git a/src/browser/existing-browser.ts b/src/browser/existing-browser.ts index 805454173..b38195965 100644 --- a/src/browser/existing-browser.ts +++ b/src/browser/existing-browser.ts @@ -84,6 +84,7 @@ export class ExistingBrowser extends Browser { this._extendStacktrace(); this._addSteps(); + await this._installFirefoxCSPAddOn(); this._addQueries(); this._addHistory(); diff --git a/src/browser/new-browser.ts b/src/browser/new-browser.ts index e5fcd93e0..d51f8ee8f 100644 --- a/src/browser/new-browser.ts +++ b/src/browser/new-browser.ts @@ -73,6 +73,7 @@ export class NewBrowser extends Browser { this._extendStacktrace(); this._addSteps(); + await this._installFirefoxCSPAddOn(); this._addQueries(); this._addHistory(); diff --git a/src/browser/queries/firefoxCSPAddOn.ts b/src/browser/queries/firefoxCSPAddOn.ts new file mode 100644 index 000000000..b5b2f6707 --- /dev/null +++ b/src/browser/queries/firefoxCSPAddOn.ts @@ -0,0 +1,7 @@ +import fs from "fs/promises"; +import path from "path"; + +export const getFirefoxCSPAddOn = async (): Promise => { + const extension = await fs.readFile(path.join(__dirname, "../../../assets/csp-bypass@testplane.io.xpi")); + return extension.toString("base64"); +}; From f3f21ba79cffe272a757b0d4dd97f56baa9d1b20 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Tue, 1 Jul 2025 19:02:55 +0300 Subject: [PATCH 4/4] fix: fix testing-library errors --- src/browser/queries/index.ts | 17 ++++++++++++++++- src/browser/queries/types.ts | 11 +++++++---- src/browser/types.ts | 3 ++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/browser/queries/index.ts b/src/browser/queries/index.ts index 0ad5541a2..8650b9800 100644 --- a/src/browser/queries/index.ts +++ b/src/browser/queries/index.ts @@ -60,6 +60,14 @@ function findContainerWithExecute(container: ElementBase): WebdriverIO.Browser { return curContainer; } +const findBrowser = (container: ElementBase): WebdriverIO.Browser => { + let browser: ElementBase | WebdriverIO.Browser = container; + while ((browser as ElementBase).parent && browser.capabilities === undefined) { + browser = (browser as ElementBase).parent; + } + return browser as WebdriverIO.Browser; +}; + async function injectDOMTestingLibrary(container: ElementBase) { const browser = findContainerWithExecute(container); const shouldInjectDTL = await browser.execute(function () { @@ -126,6 +134,12 @@ async function executeQuery(query: QueryName, container: HTMLElement, ...args: S } function deserializeArg(arg: SerializedArg): QueryArg { + if (typeof arg === "object" && arg === null) { + return undefined; + } + if (typeof arg === "object" && (arg as { nodeType?: string }).nodeType !== undefined) { + return arg as QueryArg; + } if (typeof arg === "object" && arg.serialized === "RegExp") { return eval(arg.RegExp); } @@ -187,7 +201,8 @@ function createQuery(container: ElementBase & SelectorsBase, queryName: QueryNam return async (...args: QueryArg[]) => { await injectDOMTestingLibrary(container); - const result: SerializedQueryResult = await findContainerWithExecute(container).execute( + const browser = findBrowser(container); + const result: SerializedQueryResult = await browser.execute( executeQuery, queryName, container as unknown as HTMLElement, diff --git a/src/browser/queries/types.ts b/src/browser/queries/types.ts index 4d2d00595..e1726b99a 100644 --- a/src/browser/queries/types.ts +++ b/src/browser/queries/types.ts @@ -45,10 +45,6 @@ export type TestplaneQueries = { [P in keyof Queries]: BoundFunction; }; -export type TestplaneQueriesSync = { - [P in keyof Queries]: BoundFunctionSync; -}; - export type TestplaneQueriesChainable = { [P in keyof Queries as `${string & P}$`]: Container extends SelectorsBase ? BoundFunctionSync, ReturnType, Queries[P]> @@ -68,3 +64,10 @@ export type SerializedRegExp = { serialized: "RegExp"; RegExp: string }; export type SerializedUndefined = { serialized: "Undefined"; Undefined: true }; export type SerializedArg = SerializedObject | SerializedRegExp | SerializedUndefined | number | string; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebdriverIO { + interface Element extends TestplaneQueries {} + } +} diff --git a/src/browser/types.ts b/src/browser/types.ts index 5ec4e4b0a..6035f02c6 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -8,6 +8,7 @@ import { OpenAndWaitCommand } from "./commands/openAndWait"; import type { Callstack } from "./history/callstack"; import { Test, Hook } from "../test-reader/test-object"; import type { CaptureSnapshotOptions, CaptureSnapshotResult } from "./commands/captureDomSnapshot"; +import { TestplaneQueries, TestplaneQueriesChainable } from "./queries"; export const BrowserName = { CHROME: PuppeteerBrowser.CHROME, @@ -58,7 +59,7 @@ declare global { ...args: Parameters ) => ReturnType; - interface Browser extends TestplaneQueries, TestplaneQueriesSync, TestplaneQueriesChainable { + interface Browser extends TestplaneQueries, TestplaneQueriesChainable { getMeta(this: WebdriverIO.Browser): Promise; getMeta(this: WebdriverIO.Browser, key: string): Promise;