diff --git a/.eslintrc b/.eslintrc index 05571bd..8a97367 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,4 @@ { - "parser": "@typescript-eslint/parser", "extends": [ "./node_modules/kcd-scripts/eslint.js", "plugin:import/typescript" @@ -7,31 +6,21 @@ "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", - "project": "*/**/tsconfig.json" - }, - "plugins": ["@typescript-eslint"], - "ignorePatterns": "wdio.conf.*", - "rules": { - "babel/new-cap": "off", - "func-names": "off", - "babel/no-unused-expressions": "off", - "prefer-arrow-callback": "off", - "testing-library/no-await-sync-query": "off", - "testing-library/no-dom-import": "off", - "testing-library/prefer-screen-queries": "off", - "no-undef": "off", - "no-use-before-define": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"] + "project": "tsconfig.json" }, "overrides": [ { - "files": ["wdio.conf.js", "*/**/*.e2e.*"], + "files": ["test/**/*"], "rules": { - "max-lines-per-function": "off" + "max-lines-per-function": "off", + // See https://github.com/webdriverio/webdriverio/issues/14552 + "@typescript-eslint/no-floating-promises": "off" }, "globals": { "browser": "readonly" + }, + "parserOptions": { + "project": "test/tsconfig.json" } } ] diff --git a/.github/workflows/webdriverio-testing-library.yml b/.github/workflows/webdriverio-testing-library.yml index 1319509..21b9363 100644 --- a/.github/workflows/webdriverio-testing-library.yml +++ b/.github/workflows/webdriverio-testing-library.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [12, 14] + node: [20, 22, 24] steps: - uses: actions/setup-node@v2 with: @@ -21,9 +21,12 @@ jobs: - uses: actions/checkout@v2 - name: npm install and validate run: | - export CHROMEDRIVER_VERSION="$(chromedriver --version | awk '{print $2}')" npm install - npm run validate + npm run build + npm run lint + npm run test + npm run typecheck + # npm run validate env: CI: true diff --git a/.prettierrc b/.prettierrc index fb31ee1..77fcad5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,6 +5,5 @@ "semi": false, "singleQuote": true, "trailingComma": "all", - "bracketSpacing": false, - "jsxBracketSameLine": false + "bracketSpacing": true } diff --git a/package.json b/package.json index 4ded2e1..8f53a29 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,16 @@ "build:cjs": "tsc -p tsconfig.build.json", "build:esm": "tsc -p tsconfig.esm.json", "lint": "kcd-scripts lint", + "format": "kcd-scripts format", "test:unit": "kcd-scripts test --no-watch --config=jest.config.js", "validate": "kcd-scripts validate build,lint,test,typecheck", - "test:puppeteer": "wdio wdio.conf.js", - "test:selenium-standalone": "wdio wdio.conf.selenium-standalone.js", - "test:chromedriver": "wdio wdio.conf.chromedriver.js", - "test:geckodriver": "wdio wdio.conf.geckodriver.js", - "test": "npm-run-all test:puppeteer test:selenium-standalone test:chromedriver test:geckodriver", + "test:bidi": "wdio wdio.conf.js", + "test:classic": "wdio wdio.conf.classic.js", + "test": "npm-run-all -s test:bidi test:classic", "semantic-release": "semantic-release", - "typecheck:async": "tsc -p ./test/async/tsconfig.json", - "typecheck:sync": "tsc -p ./test/sync/tsconfig.json", + "typecheck:test": "tsc -p test/tsconfig.json --noEmit", "typecheck:build": "npm run build:cjs -- --noEmit && npm run build:esm -- --noEmit", - "typecheck": "npm-run-all typecheck:build typecheck:**", - "prepare": "selenium-standalone install --drivers.chrome.version=${CHROMEDRIVER_VERSION:-latest} --drivers.gecko.version=${GECKODRIVER_VERSION:-latest}" + "typecheck": "npm-run-all typecheck:build typecheck:test" }, "files": [ "dist" @@ -33,7 +30,7 @@ "license": "ISC", "dependencies": { "@babel/runtime": "^7.4.3", - "@testing-library/dom": "^8.17.1", + "@testing-library/dom": "^10.4.1", "simmerjs": "^0.5.6" }, "peerDependencies": { @@ -41,23 +38,16 @@ }, "devDependencies": { "@types/simmerjs": "^0.5.1", - "@typescript-eslint/eslint-plugin": "^4.14.0", - "@typescript-eslint/parser": "^4.14.0", - "@wdio/cli": "^7.19.0", - "@wdio/local-runner": "^7.19.0", - "@wdio/mocha-framework": "^7.19.0", - "@wdio/selenium-standalone-service": "^7.19.0", - "@wdio/spec-reporter": "^7.19.0", - "@wdio/sync": "^7.19.0", - "eslint": "^7.6.0", - "geckodriver": "^3.2.0", - "kcd-scripts": "^11.1.0", + "@wdio/cli": "^9.18.4", + "@wdio/local-runner": "^9.18.4", + "@wdio/mocha-framework": "^9.18.0", + "@wdio/spec-reporter": "^9.18.0", + "@wdio/types": "^9.16.2", + "geckodriver": "^5.0.0", + "kcd-scripts": "^16.0.0", "npm-run-all": "^4.1.5", "semantic-release": "^17.0.2", - "ts-node": "^9.1.1", - "typescript": "^4.4.2", - "wdio-chromedriver-service": "^7.3.2", - "wdio-geckodriver-service": "^2.0.0" + "typescript": "^5.8.3" }, "repository": { "type": "git", @@ -71,5 +61,11 @@ }, "publishConfig": { "access": "public" + }, + "overrides": { + "eslint-config-kentcdodds": { + "typescript":"5.8.3", + "eslint-plugin-jest-dom": "5.5.0" + } } } diff --git a/src/index.ts b/src/index.ts index 3932187..f0bfb02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,12 @@ -/* eslint-disable no-eval, @babel/new-cap */ - +/* eslint-disable no-eval, new-cap */ import path from 'path' import fs from 'fs' import { - Matcher, MatcherOptions, queries as baseQueries, waitForOptions as WaitForOptions, } from '@testing-library/dom' import 'simmerjs' - -import {BaseWithExecute, BrowserBase, ElementBase} from './wdio-types' import { QueryArg, Config, @@ -46,21 +42,8 @@ const SIMMERJS = fs let _config: Partial -function isContainerWithExecute(container: ElementBase | BaseWithExecute): container is BaseWithExecute { - return (container as { execute?: unknown }).execute != null; -} - -function findContainerWithExecute(container: ElementBase): BaseWithExecute { - let curContainer: ElementBase | BaseWithExecute = container.parent; - while (!isContainerWithExecute(curContainer)) { - curContainer = curContainer.parent; - } - return curContainer; -} - -async function injectDOMTestingLibrary(container: ElementBase) { - const containerWithExecute = findContainerWithExecute(container); - const shouldInject = await containerWithExecute.execute(function () { +async function injectDOMTestingLibrary(element: WebdriverIO.Element) { + const shouldInject = await element.execute(function executeShouldInject() { return { domTestingLibrary: !window.TestingLibraryDom, simmer: !window.Simmer, @@ -68,7 +51,10 @@ async function injectDOMTestingLibrary(container: ElementBase) { }) if (shouldInject.domTestingLibrary) { - await containerWithExecute.execute(function (library: string) { + await element.execute(function executeInjectTestingLibrary( + el: HTMLElement, + library: string, + ) { // add DOM Testing Library to page as a script tag to support Firefox if (navigator.userAgent.includes('Firefox')) { const script = document.createElement('script') @@ -82,10 +68,13 @@ async function injectDOMTestingLibrary(container: ElementBase) { } if (shouldInject.simmer) { - await containerWithExecute.execute(SIMMERJS) + await element.execute(SIMMERJS) } - await containerWithExecute.execute(function (config: Config) { + await element.execute(function executeConfigureTestingLibrary( + el: HTMLElement, + config: Partial, + ) { window.TestingLibraryDom.configure(config) }, _config) } @@ -96,17 +85,17 @@ function serializeObject(object: ObjectQueryArg): SerializedObject { key, serializeArg(value), ]) - .reduce((acc, [key, value]) => ({...acc, [key]: 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()} + return { serialized: 'RegExp', RegExp: arg.toString() } } if (typeof arg === 'undefined') { - return {serialized: 'Undefined', Undefined: true} + return { serialized: 'Undefined', Undefined: true } } if (arg && typeof arg === 'object') { return serializeObject(arg) @@ -115,22 +104,20 @@ function serializeArg(arg: QueryArg): SerializedArg { } type SerializedQueryResult = - | {selector: string}[] + | { selector: string }[] | string - | {selector: string} + | { selector: string } | null -function executeQuery( - query: QueryName, +async function executeQuery( container: HTMLElement, + query: QueryName, ...args: SerializedArg[] -) { - const done = args.pop() as unknown as (result: SerializedQueryResult) => void - +): Promise { function deserializeObject(object: SerializedObject) { return Object.entries(object) .map<[string, QueryArg]>(([key, value]) => [key, deserializeArg(value)]) - .reduce((acc, [key, value]) => ({...acc, [key]: value}), {}) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) } function deserializeArg(arg: SerializedArg): QueryArg { @@ -148,62 +135,59 @@ function executeQuery( const [matcher, options, waitForOptions] = args.map(deserializeArg) - void (async () => { - let result: ReturnType = null - try { - // Override RegExp to fix 'matcher instanceof RegExp' check on Firefox - window.RegExp = RegExp - - result = await window.TestingLibraryDom[query]( - container, - matcher as Matcher, - options as MatcherOptions, - waitForOptions as WaitForOptions, - ) - } catch (e: unknown) { - return done((e as Error).message) - } + let result: ReturnType<(typeof window.TestingLibraryDom)[typeof query]> = null + try { + // Override RegExp to fix 'matcher instanceof RegExp' check on Firefox + window.RegExp = RegExp - if (!result) { - return done(null) - } + result = await window.TestingLibraryDom[query]( + container, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + matcher as any, + options as MatcherOptions, + waitForOptions as WaitForOptions, + ) + } catch (e: unknown) { + return (e as Error).message + } - function makeSelectorResult(element: HTMLElement) { - // use simmer if possible to allow element refetching by position, otherwise - // situations such as a React key change causes refetching to fail. - const selector = window.Simmer(element) - if (selector) return {selector} + if (!result) { + return null + } - // use generated element id as selector if Simmer fails - const elementIdAttributeName = 'data-wdio-testing-lib-element-id' - let elementId = element.getAttribute(elementIdAttributeName) + function makeSelectorResult(element: HTMLElement) { + // use simmer if possible to allow element refetching by position, otherwise + // situations such as a React key change causes refetching to fail. + const selector = window.Simmer(element) + if (selector) return { selector } - // 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) - } + // use generated element id as selector if Simmer fails + const elementIdAttributeName = 'data-wdio-testing-lib-element-id' + let elementId = element.getAttribute(elementIdAttributeName) - return {selector:`[${elementIdAttributeName}="${elementId}"]`} + // 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) } - if (Array.isArray(result)) { - return done(result.map(makeSelectorResult)); - } + return { selector: `[${elementIdAttributeName}="${elementId}"]` } + } - return done(makeSelectorResult(result)); - })() + if (Array.isArray(result)) { + return result.map(makeSelectorResult) + } + + return makeSelectorResult(result) } -function createQuery(container: ElementBase, queryName: QueryName) { +function createQuery(element: WebdriverIO.Element, queryName: QueryName) { return async (...args: QueryArg[]) => { - await injectDOMTestingLibrary(container) + await injectDOMTestingLibrary(element) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result: SerializedQueryResult = await findContainerWithExecute(container).executeAsync( + const result: SerializedQueryResult = await element.execute( executeQuery, queryName, - container, ...args.map(serializeArg), ) @@ -216,14 +200,14 @@ function createQuery(container: ElementBase, queryName: QueryName) { } if (Array.isArray(result)) { - return Promise.all(result.map(({ selector }) => container.$(selector))) + return Promise.all(result.map(({ selector }) => element.$(selector))) } - return container.$(result.selector) + return element.$(result.selector) } } -function within(element: ElementBase) { +function within(element: WebdriverIO.Element) { return (Object.keys(baseQueries) as QueryName[]).reduce( (queries, queryName) => ({ ...queries, @@ -233,13 +217,10 @@ function within(element: ElementBase) { ) as WebdriverIOQueries } -/* -eslint-disable -@typescript-eslint/no-explicit-any, -@typescript-eslint/no-unsafe-argument -*/ -function setupBrowser(browser: Browser): WebdriverIOQueries { - const queries: {[key: string | number | symbol]: WebdriverIOQueries[QueryName]} = {} +function setupBrowser(browser: WebdriverIO.Browser): WebdriverIOQueries { + const queries: { + [key: string | number | symbol]: WebdriverIOQueries[QueryName] + } = {} Object.keys(baseQueries).forEach((key) => { const queryName = key as QueryName @@ -247,8 +228,9 @@ function setupBrowser(browser: Browser): WebdriverI const query = async ( ...args: Parameters ) => { - const body = await browser.$('body') - return within(body as ElementBase)[queryName](...(args as any[])) + const body = await browser.$('body').getElement() + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + return within(body)[queryName](...(args as any[])) } // add query to response queries @@ -258,17 +240,22 @@ function setupBrowser(browser: Browser): WebdriverI browser.addCommand(queryName, query as WebdriverIOQueries[QueryName]) browser.addCommand( queryName, - function (this, ...args) { + function addQueryCommand(this, ...args) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return within(this)[queryName](...args) }, true, ) // add chainable query to BrowserObject and Elements - browser.addCommand(`${queryName}$`, query as WebdriverIOQueriesChainable[`${QueryName}$`]) browser.addCommand( `${queryName}$`, - function (this, ...args) { + query as unknown as WebdriverIOQueriesChainable[`${QueryName}$`], + ) + browser.addCommand( + `${queryName}$`, + function addQueryCommand(this, ...args) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return within(this)[queryName](...args) }, true, @@ -277,15 +264,10 @@ function setupBrowser(browser: Browser): WebdriverI return queries as unknown as WebdriverIOQueries } -/* -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} +export { within, setupBrowser, configure } diff --git a/src/types.ts b/src/types.ts index 135e82a..7b02873 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import { SelectorMatcherOptions, MatcherOptions, } from '@testing-library/dom' -import {SelectorsBase} from './wdio-types' +import { ChainablePromiseArray, ChainablePromiseElement } from 'webdriverio' export type Queries = typeof queries export type QueryName = keyof Queries @@ -18,22 +18,27 @@ export type Config = Pick< | 'defaultHidden' | 'testIdAttribute' | 'throwSuggestions' + | 'getElementError' > export type WebdriverIOQueryReturnType = T extends Promise ? Element : T extends HTMLElement - ? Element - : T extends Promise - ? ElementArray - : T extends HTMLElement[] - ? ElementArray - : T extends null - ? null - : never + ? Element + : T extends Promise + ? ElementArray + : T extends HTMLElement[] + ? ElementArray + : T extends null + ? null + : never -export type WebdriverIOBoundFunction = ( +export type WebdriverIOBoundFunction< + Element extends WebdriverIO.Element, + ElementArray extends WebdriverIO.ElementArray, + T, +> = ( ...params: Parameters> ) => Promise< WebdriverIOQueryReturnType< @@ -43,7 +48,11 @@ export type WebdriverIOBoundFunction = ( > > -export type WebdriverIOBoundFunctionSync = ( +export type WebdriverIOBoundFunctionSync< + Element extends ChainablePromiseElement, + ElementArray extends ChainablePromiseArray, + T, +> = ( ...params: Parameters> ) => WebdriverIOQueryReturnType< Element, @@ -54,19 +63,21 @@ export type WebdriverIOBoundFunctionSync = ( export type WebdriverIOQueries = { [P in keyof Queries]: WebdriverIOBoundFunction< WebdriverIO.Element, - WebdriverIO.Element[], + WebdriverIO.ElementArray, Queries[P] > } export type WebdriverIOQueriesSync = { [P in keyof Queries]: WebdriverIOBoundFunctionSync< - WebdriverIO.Element, - WebdriverIO.Element[], + ChainablePromiseElement, + ChainablePromiseArray, Queries[P] > } +type SelectorsBase = Pick + export type WebdriverIOQueriesChainable< Container extends SelectorsBase | undefined, > = { @@ -91,8 +102,8 @@ export type SerializedObject = { serialized: 'object' [key: string]: SerializedArg } -export type SerializedRegExp = {serialized: 'RegExp'; RegExp: string} -export type SerializedUndefined = {serialized: 'Undefined'; Undefined: true} +export type SerializedRegExp = { serialized: 'RegExp'; RegExp: string } +export type SerializedUndefined = { serialized: 'Undefined'; Undefined: true } export type SerializedArg = | SerializedObject diff --git a/src/wdio-types.ts b/src/wdio-types.ts deleted file mode 100644 index 6451840..0000000 --- a/src/wdio-types.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* -Types related to WebdriverIO are intentionally loose in order to support wdio -version 6 and 7 at the same time. Disable eslint rules that prevent that. -*/ - -/* -eslint-disable @typescript-eslint/no-explicit-any, -@typescript-eslint/no-namespace, -@typescript-eslint/no-empty-interface -*/ - -declare global { - namespace WebdriverIO { - interface Element {} - } -} - -export type $ = ( - selector: any, -) => - | ChainablePromiseElementBase> - | Promise - | WebdriverIO.Element - -export type $$ = ( - selector: any, -) => - | ChainablePromiseArrayBase> - | Promise - | WebdriverIO.Element[] - -export type ChainablePromiseElementBase = Promise & {$: $} -export type ChainablePromiseArrayBase = Promise - -export type SelectorsBase = { - $: $ - $$: $$ -} - -export type BaseWithExecute = { - execute( - script: string | ((...args: any[]) => T), - ...args: any[] - ): Promise - - execute(script: string | ((...args: any[]) => T), ...args: any[]): T - - executeAsync(script: string | ((...args: any[]) => void), ...args: any[]): any -} - -export type ElementBase = SelectorsBase & { - parent: ElementBase | BaseWithExecute -} - -export type BrowserBase = SelectorsBase & { - addCommand( - queryName: string, - commandFn: ( - this: T extends true ? ElementBase : BrowserBase, - ...args: any[] - ) => void, - isElementCommand?: T, - ): any -} diff --git a/test/async/configure.e2e.ts b/test/async/configure.e2e.ts deleted file mode 100644 index cd2f96e..0000000 --- a/test/async/configure.e2e.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {setupBrowser, configure} from '../../src' - -describe('configure', () => { - afterEach(() => { - configure({}) - }) - - it('supports setting testIdAttribute', async () => { - configure({testIdAttribute: 'data-automation-id'}) - - const {getByTestId} = setupBrowser(browser) - - expect(await getByTestId('image-with-random-alt-tag')).toBeDefined() - }) - - it('supports setting asyncUtilTimeout', async () => { - configure({asyncUtilTimeout: 3000}) - - const {findByText} = setupBrowser(browser) - - expect(await findByText('Long Delayed Button Text')).toBeDefined(); - }) - - it('supports setting computedStyleSupportsPseudoElements', async () => { - configure({computedStyleSupportsPseudoElements: true}) - - const {getByRole} = setupBrowser(browser) - - expect( - await getByRole('button', {name: 'Named by pseudo element'}), - ).toBeDefined() - }) - - it('supports setting defaultHidden', async () => { - configure({defaultHidden: true}) - - const {getByRole} = setupBrowser(browser) - - expect(await getByRole('button', {name: 'Hidden button'})).toBeDefined() - }) - - it('supports setting throwSuggestions', async () => { - configure({throwSuggestions: true}) - - const {getByTestId} = setupBrowser(browser) - - await expect(() => - getByTestId('button-that-should-not-use-testid'), - ).rejects.toThrowError( - 'A better query is available', - ) - }) - - it('works after navigation', async () => { - const {getByText, findByAltText} = setupBrowser(browser) - - const goToPageTwoLink = await getByText('Go to Page 2') - await goToPageTwoLink.click() - - expect(await findByAltText('page two thing')).toBeDefined() - }) -}) diff --git a/test/async/tsconfig.json b/test/async/tsconfig.json deleted file mode 100644 index 873a709..0000000 --- a/test/async/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["node", "webdriverio/async", "@wdio/mocha-framework", "expect-webdriverio"], - "baseUrl": ".", - "skipLibCheck": false - }, - "exclude": [], - "include": ["**/*.ts"] -} diff --git a/test/async/chaining.e2e.ts b/test/chaining.e2e.ts similarity index 80% rename from test/async/chaining.e2e.ts rename to test/chaining.e2e.ts index e6b4dfd..fa8f951 100644 --- a/test/async/chaining.e2e.ts +++ b/test/chaining.e2e.ts @@ -1,12 +1,10 @@ -import {setupBrowser} from '../../src' +import { setupBrowser } from '../src' describe('chaining', () => { it('can chain browser getBy queries', async () => { setupBrowser(browser) - const button = await browser - .getByTestId$('nested') - .getByText$('Button Text') + const button = browser.getByTestId$('nested').getByText$('Button Text') await button.click() @@ -14,7 +12,7 @@ describe('chaining', () => { }) it('can chain element getBy queries', async () => { - const {getByTestId} = setupBrowser(browser) + const { getByTestId } = setupBrowser(browser) const nested = await getByTestId('nested') await nested.getByText$('Button Text').click() @@ -33,7 +31,7 @@ describe('chaining', () => { }) it('can chain element getAllBy queries', async () => { - const {getByTestId} = setupBrowser(browser) + const { getByTestId } = setupBrowser(browser) const nested = await getByTestId('nested') await nested.getAllByText$('Button Text')[0].click() diff --git a/test/configure.e2e.ts b/test/configure.e2e.ts new file mode 100644 index 0000000..7cfb875 --- /dev/null +++ b/test/configure.e2e.ts @@ -0,0 +1,72 @@ +import { setupBrowser, configure } from '../src' + +describe('configure', () => { + afterEach(() => { + configure({}) + }) + + it('supports setting testIdAttribute', async () => { + configure({ testIdAttribute: 'data-automation-id' }) + + const { getByTestId } = setupBrowser(browser) + + expect(await getByTestId('image-with-random-alt-tag')).toBeDefined() + }) + + it('supports setting getElementError', async () => { + if (browser.isBidi) { + return; // Currently not supported in BIDI: `Unsupported type: function` + } + + configure({ getElementError: () => new Error(`test`) }) + + const { getByTestId } = setupBrowser(browser) + + await expect(getByTestId('not-existing')).rejects.toThrow(`test`) + }) + + it('supports setting asyncUtilTimeout', async () => { + configure({ asyncUtilTimeout: 3000 }) + + const { findByText } = setupBrowser(browser) + + expect(await findByText('Long Delayed Button Text')).toBeDefined() + }) + + it('supports setting computedStyleSupportsPseudoElements', async () => { + configure({ computedStyleSupportsPseudoElements: true }) + + const { getByRole } = setupBrowser(browser) + + expect( + await getByRole('button', { name: 'Named by pseudo element' }), + ).toBeDefined() + }) + + it('supports setting defaultHidden', async () => { + configure({ defaultHidden: true }) + + const { getByRole } = setupBrowser(browser) + + expect(await getByRole('button', { name: 'Hidden button' })).toBeDefined() + }) + + it('supports setting throwSuggestions', async () => { + configure({ throwSuggestions: true }) + + const { getByTestId } = setupBrowser(browser) + + await expect(() => + getByTestId('button-that-should-not-use-testid'), + ).rejects.toThrow('A better query is available') + }) + + it('works after navigation', async () => { + const { getByText, findByAltText } = setupBrowser(browser) + + const goToPageTwoLink = await getByText('Go to Page 2') + await goToPageTwoLink.click() + + expect(await findByAltText('page two thing')).toBeDefined() + }) +}) diff --git a/test/async/queries.e2e.ts b/test/queries.e2e.ts similarity index 71% rename from test/async/queries.e2e.ts rename to test/queries.e2e.ts index ced2593..9484f33 100644 --- a/test/async/queries.e2e.ts +++ b/test/queries.e2e.ts @@ -1,102 +1,100 @@ -import refetchElement from 'webdriverio/build/utils/refetchElement' - -import {setupBrowser} from '../../src' +import { setupBrowser } from '../src' describe('queries', () => { it('queryBy resolves with matching element', async () => { - const {queryByText} = setupBrowser(browser) + const { queryByText } = setupBrowser(browser) const button = await queryByText('Unique Button Text') expect(await button?.getText()).toEqual('Unique Button Text') }) it('queryBy resolves with null when there are no matching elements', async () => { - const {queryByText} = setupBrowser(browser) + const { queryByText } = setupBrowser(browser) const button = await queryByText('Text that does not exist') expect(button).toBeNull() }) it('getBy resolves with matching element', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) const button = await getByText('Unique Button Text') expect(await button.getText()).toEqual('Unique Button Text') }) it('getBy rejects when there are no matching elements', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) await expect(getByText('Text that does not exist')).rejects.toThrow() }) it('getBy rejects when there are multiple matching elements', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) await expect(getByText('Button Text')).rejects.toThrow() }) it('findBy waits for matching element and resolves with it', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) const button = await findByText('Unique Delayed Button Text') expect(await button.getText()).toEqual('Unique Delayed Button Text') }) it('findBy rejects when there is no matching element after timeout', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) await expect(findByText('Text that does not exist')).rejects.toThrow() }) it('findBy rejects when there are multiple matching elements', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) await expect(findByText('Delayed Button Text')).rejects.toThrow() }) it('queryAllBy resolves with matching elements', async () => { - const {queryAllByText} = setupBrowser(browser) + const { queryAllByText } = setupBrowser(browser) const chans = await queryAllByText('Button Text') expect(chans).toHaveLength(2) }) it('queryAllBy resolves with an empty array when there are no matching elements', async () => { - const {queryAllByText} = setupBrowser(browser) + const { queryAllByText } = setupBrowser(browser) const chans = await queryAllByText('Text that does not exist') expect(chans).toHaveLength(0) }) it('getAllBy resolves matching elements', async () => { - const {getAllByText} = setupBrowser(browser) + const { getAllByText } = setupBrowser(browser) const buttons = await getAllByText('Button Text') expect(buttons).toHaveLength(2) }) it('getAllBy rejects when there are no matching elements', async () => { - const {getAllByText} = setupBrowser(browser) + const { getAllByText } = setupBrowser(browser) await expect(getAllByText('Text that does not exist')).rejects.toThrow() }) it('findAllBy waits for matching elements and resolves with them', async () => { - const {findAllByText} = setupBrowser(browser) + const { findAllByText } = setupBrowser(browser) const buttons = await findAllByText('Delayed Button Text') expect(buttons).toHaveLength(2) }) it('findAllBy rejects when there are no matching elements after timeout', async () => { - const {findAllByText} = setupBrowser(browser) + const { findAllByText } = setupBrowser(browser) await expect(findAllByText('Text that does not exist')).rejects.toThrow() }) it('can click resolved elements', async () => { - const {getByText, getAllByText} = setupBrowser(browser) + const { getByText, getAllByText } = setupBrowser(browser) const uniqueButton = await getByText('Unique Button Text') const buttons = await getAllByText('Button Text') @@ -111,21 +109,21 @@ describe('queries', () => { }) it('support Regular Expressions as matchers', async () => { - const {getAllByText} = setupBrowser(browser) + const { getAllByText } = setupBrowser(browser) const chans = await getAllByText(/jackie chan/i) expect(chans).toHaveLength(2) }) it('support options', async () => { - const {getAllByText} = setupBrowser(browser) + const { getAllByText } = setupBrowser(browser) - const chans = await getAllByText('Jackie Chan', {exact: false}) + const chans = await getAllByText('Jackie Chan', { exact: false }) expect(chans).toHaveLength(2) }) it('support Regular Expressions in options', async () => { - const {getAllByRole} = setupBrowser(browser) + const { getAllByRole } = setupBrowser(browser) const chans = await getAllByRole('button', { name: /jackie chan/i, @@ -134,15 +132,20 @@ describe('queries', () => { }) it('support waitFor options', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) + const start = Date.now() await expect( - findByText('Unique Delayed Button Text', {}, {timeout: 0}), - ).rejects.toThrow() + findByText('Not existing', {}, { timeout: 2_000 }), + ).rejects.toThrow(/Unable to find an element with the text/) + const end = Date.now() + + // Default timeout is very short. With the timeout of 10s above, the test should take 2s + expect(end - start).toBeGreaterThan(2_000) }) it('support being passed undefined arguments', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) const button = await findByText( 'Unique Delayed Button Text', @@ -153,53 +156,51 @@ describe('queries', () => { }) it('retains error messages', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) - await expect(getByText('Text that does not exist')).rejects.toThrowError( + await expect(getByText('Text that does not exist')).rejects.toThrow( /Unable to find an element with the text/, ) }) it('can refetch an element', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) const button = await getByText('Unique Button Text') - const refetchedButton = await refetchElement(button, 'click') + const refetchedButton = button // TODO: await refetchElement(button, 'click') expect(refetchedButton).not.toBeNull() - expect(await refetchedButton.getText()).toBe( - 'Unique Button Text', - ) + expect(await refetchedButton.getText()).toBe('Unique Button Text') }) it('getAllBy works when Simmer cannot create a unique selector', async () => { - const {getAllByText} = setupBrowser(browser) + const { getAllByText } = setupBrowser(browser) expect(await getAllByText(/High depth non-specific div/)).toHaveLength(2) }) it('getBy works when Simmer cannot create a unique selector', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) expect(await getByText('High depth non-specific div one')).not.toBeNull() }) // This tests fails when using Puppeteer, but passes when using other services it.skip('findByText works for elements in production React app', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) expect(await findByText('React')).not.toBeNull() }) // This tests fails when using Puppeteer, but passes when using other services it.skip('refetching works when React key changes and element recreated', async () => { - const {findByText} = setupBrowser(browser) + const { findByText } = setupBrowser(browser) const title = await findByText('React') const recreateTitleButton = await findByText('Recreate React Title') await recreateTitleButton.click() - const refetchedTitle = await refetchElement(title, '') + const refetchedTitle = title // TODO: await refetchElement(title, '') expect(refetchedTitle).not.toBeNull() expect(await refetchedTitle.getAttribute('id')).toEqual('react-title') }) diff --git a/test/async/setupBrowser.e2e.ts b/test/setupBrowser.e2e.ts similarity index 81% rename from test/async/setupBrowser.e2e.ts rename to test/setupBrowser.e2e.ts index a5f9615..67e81ce 100644 --- a/test/async/setupBrowser.e2e.ts +++ b/test/setupBrowser.e2e.ts @@ -1,7 +1,7 @@ import path from 'path' -import {queries as baseQueries} from '@testing-library/dom' +import { queries as baseQueries } from '@testing-library/dom' -import {setupBrowser} from '../../src' +import { setupBrowser } from '../src' describe('setupBrowser', () => { it('resolves with all queries', () => { @@ -14,13 +14,13 @@ describe('setupBrowser', () => { }) it('binds queries to document body', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) expect(await getByText('Page Heading')).toBeDefined() }) it('still works after page navigation', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) const goToPageTwoLink = await getByText('Go to Page 2') await goToPageTwoLink.click() @@ -29,7 +29,7 @@ describe('setupBrowser', () => { }) it('still works after refresh', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) await browser.refresh() @@ -38,7 +38,7 @@ describe('setupBrowser', () => { // QUARANTINED:- this test sometimes hangs in actions https://github.com/testing-library/webdriverio-testing-library/actions/runs/3934657982/jobs/6729701157 it.skip('still works after session reload', async () => { - const {getByText} = setupBrowser(browser) + const { getByText } = setupBrowser(browser) await browser.reloadSession() await browser.url( @@ -57,7 +57,7 @@ describe('setupBrowser', () => { it('adds queries as element commands scoped to element', async () => { setupBrowser(browser) - const nested = await browser.$('*[data-testid="nested"]') + const nested = await browser.$('*[data-testid="nested"]').getElement() const button = await nested.getByText('Button Text') await button.click() diff --git a/test/sync/setupBrowser.e2e.ts b/test/sync/setupBrowser.e2e.ts deleted file mode 100644 index e6dc9e2..0000000 --- a/test/sync/setupBrowser.e2e.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {setupBrowser} from '../../src' - -describe('setupBrowser:sync', () => { - it('adds queries as sync browser commands', () => { - setupBrowser(browser) - - const pageHeading = browser.getByText('Page Heading') - - expect(pageHeading).toBeDefined() - expect(pageHeading.getText()).toEqual('Page Heading') - }) - - it('adds queries as sync element commands scoped to element', () => { - setupBrowser(browser) - - const button = browser.$('*[data-testid="nested"]').getByText('Button Text') - - button.click() - - expect(button.getText()).toEqual('Button Clicked') - }) - - it('queries returned by setupBrowser can be used in sync tests using call', () => { - const {getByText} = setupBrowser(browser) - - browser.call(async () => { - expect(await getByText('Page Heading')).toBeDefined() - }) - }) -}) diff --git a/test/sync/tsconfig.json b/test/sync/tsconfig.json deleted file mode 100644 index c42bdce..0000000 --- a/test/sync/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["node", "webdriverio/sync", "@wdio/mocha-framework", "expect-webdriverio"], - "skipLibCheck": false - }, - "exclude": [], - "include": ["**/*.ts"] -} diff --git a/test/sync/types.ts b/test/sync/types.ts deleted file mode 100644 index 9fc1c43..0000000 --- a/test/sync/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* -eslint-disable -@typescript-eslint/no-namespace, -@typescript-eslint/no-empty-interface -*/ - -import {WebdriverIOQueriesSync} from '../../src' - -declare global { - namespace WebdriverIO { - interface Browser extends WebdriverIOQueriesSync {} - interface Element extends WebdriverIOQueriesSync {} - } -} diff --git a/test/sync/within.e2e.ts b/test/sync/within.e2e.ts deleted file mode 100644 index 386a76d..0000000 --- a/test/sync/within.e2e.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {within} from '../../src' - -describe('within:sync', () => { - it('returned queries can be used in sync tests using call', () => { - const {getByText} = within(browser.$('body')) - - browser.call(async () => { - expect(await getByText('Page Heading')).toBeDefined() - }) - }) -}) diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..c9874d6 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"], + "baseUrl": ".", + "skipLibCheck": true + }, + "exclude": [], + "include": ["**/*.ts"] +} diff --git a/test/async/types.ts b/test/types.ts similarity index 55% rename from test/async/types.ts rename to test/types.ts index 75e513b..0b699e5 100644 --- a/test/async/types.ts +++ b/test/types.ts @@ -4,8 +4,7 @@ eslint-disable @typescript-eslint/no-empty-interface */ -import {WebdriverIOQueriesChainable, WebdriverIOQueries} from '../../src' -import {SelectorsBase} from '../../src/wdio-types' +import { WebdriverIOQueriesChainable, WebdriverIOQueries } from '../src' declare global { namespace WebdriverIO { @@ -17,8 +16,3 @@ declare global { WebdriverIOQueriesChainable {} } } - -declare module 'webdriverio' { - interface ChainablePromiseElement - extends WebdriverIOQueriesChainable {} -} diff --git a/test/async/within.e2e.ts b/test/within.e2e.ts similarity index 75% rename from test/async/within.e2e.ts rename to test/within.e2e.ts index fe96515..bd02061 100644 --- a/test/async/within.e2e.ts +++ b/test/within.e2e.ts @@ -1,8 +1,10 @@ -import {within, setupBrowser} from '../../src' +import { within, setupBrowser } from '../src' + +declare const browser: WebdriverIO.Browser describe('within', () => { it('scopes queries to element', async () => { - const nested = await browser.$('*[data-testid="nested"]') + const nested = await browser.$('*[data-testid="nested"]').getElement() const button = await within(nested).getByText('Button Text') await button.click() @@ -11,7 +13,7 @@ describe('within', () => { }) it('works with elements from GetBy query', async () => { - const {getByTestId} = setupBrowser(browser) + const { getByTestId } = setupBrowser(browser) const nested = await getByTestId('nested') const button = await within(nested).getByText('Button Text') @@ -21,7 +23,7 @@ describe('within', () => { }) it('works with elements from AllBy query', async () => { - const {getAllByTestId} = setupBrowser(browser) + const { getAllByTestId } = setupBrowser(browser) const nestedDivs = await getAllByTestId(/nested/) expect(nestedDivs).toHaveLength(2) diff --git a/tsconfig.json b/tsconfig.json index 2e66c3e..42b4240 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,5 @@ "outDir": "./dist", "skipLibCheck": true, "noEmit": true - }, - "exclude": ["test"] + } } diff --git a/wdio.conf.chromedriver.js b/wdio.conf.chromedriver.js deleted file mode 100644 index 6e31af3..0000000 --- a/wdio.conf.chromedriver.js +++ /dev/null @@ -1,23 +0,0 @@ -const computeFsPaths = require('selenium-standalone/lib/compute-fs-paths'); -const createDefaultOps = require('selenium-standalone/lib/default-config'); - -const baseConfig = require('./wdio.conf') -const defaultOps = createDefaultOps() - -const fsPaths = computeFsPaths({ - ...defaultOps, - seleniumVersion: defaultOps.version, - drivers: { - chrome: { - version: process.env.CHROMEDRIVER_VERSION || 'latest', - arch: process.arch, - }, - }, -}) - -exports.config = { - ...baseConfig.config, - services: [ - ['chromedriver', {chromedriverCustomPath: fsPaths.chrome.installPath}], - ], -} diff --git a/wdio.conf.classic.mjs b/wdio.conf.classic.mjs new file mode 100644 index 0000000..5026878 --- /dev/null +++ b/wdio.conf.classic.mjs @@ -0,0 +1,9 @@ +// @ts-check +import { defineConfig } from '@wdio/config'; +import { config as baseConfig} from './wdio.conf.mjs'; + +export const config = defineConfig({ + ...baseConfig, + capabilities: baseConfig.capabilities.map(c => ({...c, 'wdio:enforceWebDriverClassic': true })), +}); + diff --git a/wdio.conf.geckodriver.js b/wdio.conf.geckodriver.js deleted file mode 100644 index bb8b6cc..0000000 --- a/wdio.conf.geckodriver.js +++ /dev/null @@ -1,32 +0,0 @@ -const computeFsPaths = require('selenium-standalone/lib/compute-fs-paths') -const createDefaultOps = require('selenium-standalone/lib/default-config') - -const baseConfig = require('./wdio.conf') -const defaultOps = createDefaultOps() - -const fsPaths = computeFsPaths({ - ...defaultOps, - seleniumVersion: defaultOps.version, - drivers: { - firefox: { - version: process.env.GECKODRIVER_VERSION || 'latest', - arch: process.arch, - }, - }, -}) - -exports.config = { - ...baseConfig.config, - services: [ - ['geckodriver', {geckodriverCustomPath: fsPaths.firefox.installPath}], - ], - capabilities: [ - { - browserName: 'firefox', - acceptInsecureCerts: true, - 'moz:firefoxOptions': { - args: process.env.CI ? ['-headless'] : [], - }, - }, - ], -} diff --git a/wdio.conf.js b/wdio.conf.mjs similarity index 97% rename from wdio.conf.js rename to wdio.conf.mjs index 3df0842..c771d8c 100644 --- a/wdio.conf.js +++ b/wdio.conf.mjs @@ -1,6 +1,9 @@ -const path = require('path') +// @ts-check +import { defineConfig } from '@wdio/config'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -exports.config = { +export const config = defineConfig({ // // ==================== // Runner Configuration @@ -201,7 +204,7 @@ exports.config = { */ beforeTest: async function () { await browser.url( - `file:///${path.join(__dirname, './test-app/index.html')}`, + `file:///${path.join(import.meta.dirname, './test-app/index.html')}`, ) }, /** @@ -272,4 +275,4 @@ exports.config = { */ //onReload: function(oldSessionId, newSessionId) { //} -} +}) diff --git a/wdio.conf.selenium-standalone.js b/wdio.conf.selenium-standalone.js deleted file mode 100644 index 53b3faa..0000000 --- a/wdio.conf.selenium-standalone.js +++ /dev/null @@ -1,27 +0,0 @@ -const baseConfig = require('./wdio.conf') - -exports.config = { - ...baseConfig.config, - capabilities: [ - ...baseConfig.config.capabilities, - { - browserName: 'firefox', - acceptInsecureCerts: true, - 'moz:firefoxOptions': { - args: process.env.CI ? ['-headless'] : [], - }, - }, - ], - services: [ - [ - 'selenium-standalone', - { - skipSeleniumInstall: true, - drivers: { - firefox: process.env.GECKODRIVER_VERSION || true, - chrome: process.env.CHROMEDRIVER_VERSION || true, - }, - }, - ], - ], -}