diff --git a/eslint.config.mjs b/eslint.config.mjs
index 5e661b65b..c82440f3c 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -28,5 +28,65 @@ export default wdioEslint.config([
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
+ },
+ {
+ files: ['src/**/*.ts'],
+ plugins: {
+ local: {
+ rules: {
+ 'enforce-options-list': {
+ create(context) {
+ const fileName = context.filename || context.getFilename()
+ const isConstantsFile = fileName.endsWith('src/constants.ts')
+ const definedOptions = new Set()
+ let listNode = null
+ const listElements = new Set()
+
+ return {
+ VariableDeclarator(node) {
+ if (node.id.type === 'Identifier') {
+ if (node.id.name.includes('DEFAULT_OPTIONS')) {
+ if (isConstantsFile) {
+ definedOptions.add(node.id.name)
+ } else {
+ context.report({
+ node: node.id,
+ message: `Option '${node.id.name}' must be included in 'src/constants.ts#defaultOptionsList', so it can be globally overridden.`
+ })
+ }
+ }
+ if (isConstantsFile && node.id.name === 'defaultOptionsList' && node.init && node.init.type === 'ArrayExpression') {
+ listNode = node
+ node.init.elements.forEach(el => {
+ if (el && el.type === 'Identifier') {
+ listElements.add(el.name)
+ }
+ })
+ }
+ }
+ },
+ 'Program:exit'() {
+ if (!listNode) {
+ return
+ }
+
+ definedOptions.forEach(opt => {
+ if (!listElements.has(opt)) {
+ context.report({
+ node: listNode,
+ message: `Option '${opt}' must be included in 'defaultOptionsList', so it can be globally overridden in 'src/constants.ts'.`
+ })
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ rules: {
+ 'local/enforce-options-list': 'error'
+ }
}
])
diff --git a/package.json b/package.json
index 48675f2f7..da16ff30c 100644
--- a/package.json
+++ b/package.json
@@ -72,7 +72,8 @@
"watch": "npm run compile -- --watch",
"prepare": "husky install",
"playgrounds:setup": "for dir in playgrounds/*/; do cd \"$dir\" && npm install && cd ../..; done",
- "playgrounds:checks:all": "for dir in playgrounds/*/; do cd \"$dir\" && npm run checks:all && cd ../..; done"
+ "playgrounds:checks:all": "for dir in playgrounds/*/; do cd \"$dir\" && npm run checks:all && cd ../..; done",
+ "playgrounds:snapshots:update": "cd playgrounds/mocha && npm run snapshots:update && cd ../jest && npm run snapshots:update && cd ../.."
},
"dependencies": {
"@vitest/snapshot": "^4.0.16",
diff --git a/playgrounds/jasmine/package-lock.json b/playgrounds/jasmine/package-lock.json
index f791ec4fd..d0672a0e8 100644
--- a/playgrounds/jasmine/package-lock.json
+++ b/playgrounds/jasmine/package-lock.json
@@ -24,7 +24,6 @@
}
},
"../..": {
- "name": "expect-webdriverio",
"version": "5.6.4",
"dev": true,
"license": "MIT",
diff --git a/playgrounds/jest/package-lock.json b/playgrounds/jest/package-lock.json
index 2b7c2dab9..cf9bbf7ad 100644
--- a/playgrounds/jest/package-lock.json
+++ b/playgrounds/jest/package-lock.json
@@ -24,7 +24,6 @@
}
},
"../..": {
- "name": "expect-webdriverio",
"version": "5.6.4",
"dev": true,
"license": "MIT",
diff --git a/playgrounds/jest/package.json b/playgrounds/jest/package.json
index dc7ab4ab0..fbf402289 100644
--- a/playgrounds/jest/package.json
+++ b/playgrounds/jest/package.json
@@ -8,7 +8,8 @@
"typecheck": "tsc --noEmit",
"test": "wdio run wdio.conf.ts",
"lint": "eslint .",
- "checks:all": "npm run typecheck && npm run lint && npm test"
+ "checks:all": "npm run typecheck && npm run lint && npm test",
+ "snapshots:update": "npm run test -- --spec snapshot.test.ts --updateSnapshots"
},
"devDependencies": {
"@types/jest": "^30.0.0",
diff --git a/playgrounds/jest/test/specs/__snapshots__/snapshot.test.ts.snap b/playgrounds/jest/test/specs/__snapshots__/snapshot.test.ts.snap
index f4d9ec4da..c43152608 100644
--- a/playgrounds/jest/test/specs/__snapshots__/snapshot.test.ts.snap
+++ b/playgrounds/jest/test/specs/__snapshots__/snapshot.test.ts.snap
@@ -15,7 +15,7 @@ exports[`DOM snapshots > should match command result snapshot 1`] = `
exports[`DOM snapshots > should match element outerHTML snapshot 1`] = `
"
-

+
"
`;
diff --git a/playgrounds/mocha/package-lock.json b/playgrounds/mocha/package-lock.json
index 53f856e16..358be4919 100644
--- a/playgrounds/mocha/package-lock.json
+++ b/playgrounds/mocha/package-lock.json
@@ -21,7 +21,6 @@
}
},
"../..": {
- "name": "expect-webdriverio",
"version": "5.6.4",
"dev": true,
"license": "MIT",
diff --git a/playgrounds/mocha/package.json b/playgrounds/mocha/package.json
index bc5dbc45a..c68cc0f91 100644
--- a/playgrounds/mocha/package.json
+++ b/playgrounds/mocha/package.json
@@ -8,7 +8,8 @@
"typecheck": "tsc --noEmit",
"test": "wdio run wdio.conf.ts",
"lint": "eslint .",
- "checks:all": "npm run typecheck && npm run lint && npm test"
+ "checks:all": "npm run typecheck && npm run lint && npm test",
+ "snapshots:update": "npm run test -- --spec snapshot.test.ts --updateSnapshots"
},
"devDependencies": {
"@wdio/cli": "^9.4.0",
diff --git a/playgrounds/mocha/test/specs/__snapshots__/snapshot.test.ts.snap b/playgrounds/mocha/test/specs/__snapshots__/snapshot.test.ts.snap
index f4d9ec4da..c43152608 100644
--- a/playgrounds/mocha/test/specs/__snapshots__/snapshot.test.ts.snap
+++ b/playgrounds/mocha/test/specs/__snapshots__/snapshot.test.ts.snap
@@ -15,7 +15,7 @@ exports[`DOM snapshots > should match command result snapshot 1`] = `
exports[`DOM snapshots > should match element outerHTML snapshot 1`] = `
"
-

+
"
`;
diff --git a/playgrounds/mocha/test/specs/options.test.ts b/playgrounds/mocha/test/specs/options.test.ts
new file mode 100644
index 000000000..e266133ca
--- /dev/null
+++ b/playgrounds/mocha/test/specs/options.test.ts
@@ -0,0 +1,31 @@
+import { $ } from '@wdio/globals'
+import { setOptions, getConfig } from 'expect-webdriverio'
+
+describe('Global Options', () => {
+ const defaultWait = getConfig().wait
+
+ before(() => {
+ setOptions({ wait: 1 })
+ })
+
+ it('should set global wait option', () => {
+ expect(getConfig().wait).toBe(1)
+ expect(getConfig().wait).not.toBe(defaultWait)
+ expect(defaultWait).toBe(10000)
+ })
+
+ it('should allow setting and using global wait option', async () => {
+ const start = Date.now()
+
+ // Should fail immediately (wait: 1ms)
+ await expect(expect($('non-existent-element-' + Date.now())).toBeDisplayed()).rejects.toThrow()
+ const duration = Date.now() - start
+
+ // Ensure failure was fast (< 500ms) compared to default timeout
+ expect(duration).toBeLessThan(500)
+ })
+
+ after(() => {
+ setOptions({ wait: defaultWait })
+ })
+})
diff --git a/src/constants.ts b/src/constants.ts
index f84c62eb4..fb8627e22 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -4,3 +4,16 @@ export const DEFAULT_OPTIONS: Required = {
beforeAssertion: async () => {},
afterAssertion: async () => {},
}
+
+export const DEFAULT_OPTIONS_TO_BE_DISPLAYED: Required> = {
+ ...DEFAULT_OPTIONS,
+ withinViewport: false,
+ contentVisibilityAuto: true,
+ opacityProperty: true,
+ visibilityProperty: true
+}
+
+export const defaultOptionsList = [
+ DEFAULT_OPTIONS,
+ DEFAULT_OPTIONS_TO_BE_DISPLAYED
+]
diff --git a/src/index.ts b/src/index.ts
index c88bd5c10..c245f2455 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,7 +2,7 @@
import { expect as expectLib } from 'expect'
import type { WdioMatchersObject } from './types.js'
import * as wdioMatchers from './matchers.js'
-import { DEFAULT_OPTIONS } from './constants.js'
+import { DEFAULT_OPTIONS, defaultOptionsList } from './constants.js'
import createSoftExpect from './softExpect.js'
import { SoftAssertService } from './softAssert.js'
@@ -51,15 +51,22 @@ Object.defineProperty(expectWithSoft, 'clearSoftFailures', {
export const expect = expectWithSoft
+// TODO one day to rename to something more aligned with setDefaultOptions
export const getConfig = (): ExpectWebdriverIO.DefaultOptions => DEFAULT_OPTIONS
-export const setDefaultOptions = (options = {}): void => {
+export const setDefaultOptions = (options: Partial): void => {
Object.entries(options).forEach(([key, value]) => {
- if (key in DEFAULT_OPTIONS) {
- // @ts-ignore
- DEFAULT_OPTIONS[key] = value
- }
+ defaultOptionsList.forEach((option) => {
+ if (key in option) {
+ // @ts-ignore
+ option[key] = value
+ }
+ })
})
}
+
+/**
+ * @deprecated use setDefaultOptions instead
+ */
export const setOptions = setDefaultOptions
/**
diff --git a/src/matchers/element/toBeDisplayed.ts b/src/matchers/element/toBeDisplayed.ts
index 16b0352e3..ec06f1c24 100644
--- a/src/matchers/element/toBeDisplayed.ts
+++ b/src/matchers/element/toBeDisplayed.ts
@@ -1,18 +1,10 @@
import { executeCommandBe } from '../../utils.js'
-import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
-
-const DEFAULT_OPTIONS_DISPLAYED: ExpectWebdriverIO.ToBeDisplayedOptions = {
- ...DEFAULT_OPTIONS,
- withinViewport: false,
- contentVisibilityAuto: true,
- opacityProperty: true,
- visibilityProperty: true
-}
+import { DEFAULT_OPTIONS_TO_BE_DISPLAYED } from '../../constants.js'
export async function toBeDisplayed(
received: WdioElementMaybePromise,
- options: ExpectWebdriverIO.ToBeDisplayedOptions = DEFAULT_OPTIONS_DISPLAYED,
+ options: ExpectWebdriverIO.ToBeDisplayedOptions = DEFAULT_OPTIONS_TO_BE_DISPLAYED,
) {
this.expectation = this.expectation || 'displayed'
@@ -27,7 +19,7 @@ export async function toBeDisplayed(
opacityProperty,
visibilityProperty,
...commandOptions
- } = { ...DEFAULT_OPTIONS_DISPLAYED, ...options }
+ } = { ...DEFAULT_OPTIONS_TO_BE_DISPLAYED, ...options }
const result = await executeCommandBe.call(this, received, el => el?.isDisplayed({
withinViewport,
diff --git a/test/matchers/beMatchers.test.ts b/test/matchers/beMatchers.test.ts
index c2eba4864..6d20fa856 100644
--- a/test/matchers/beMatchers.test.ts
+++ b/test/matchers/beMatchers.test.ts
@@ -1,7 +1,10 @@
-import { vi, test, describe, expect } from 'vitest'
+import { vi, test, describe, expect, afterEach, beforeEach } from 'vitest'
import { $ } from '@wdio/globals'
import { matcherLastWordName } from '../__fixtures__/utils.js'
import * as Matchers from '../../src/matchers.js'
+import { setOptions } from '../../src/index.js'
+import { DEFAULT_OPTIONS } from '../../src/constants.js'
+import { executeCommandBe } from '../../src/utils.js'
vi.mock('@wdio/globals')
@@ -17,6 +20,15 @@ const beMatchers = {
'toBeSelected': 'isSelected',
} satisfies Partial>
+vi.mock('../../src/utils.js', async (importOriginal) => {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ executeCommandBe: vi.fn(actual.executeCommandBe)
+ }
+})
+
describe('be* matchers', () => {
describe('Ensure all toBe matchers are covered', () => {
@@ -135,6 +147,35 @@ Expected: "${matcherLastWordName(matcherName)}"
Received: "not ${matcherLastWordName(matcherName)}"`
)
})
+
+ describe('global options', () => {
+ const defaultOptions = { ...DEFAULT_OPTIONS }
+
+ beforeEach(() => {
+ // Set global options to custom values before each test
+ setOptions({ wait: 99, interval: 101 })
+ })
+
+ afterEach(() => {
+ // Reset options after each test to avoid side effects
+ setOptions(defaultOptions)
+ expect(DEFAULT_OPTIONS.wait).not.toBe(99)
+ })
+
+ test('should use globally set default options', async () => {
+ const el = await $('sel')
+ el.isDisplayed = vi.fn().mockResolvedValue(true)
+
+ await matcherFn.call({}, el)
+
+ expect(DEFAULT_OPTIONS.wait).toBe(99)
+ expect(executeCommandBe).toHaveBeenCalledWith(
+ el,
+ expect.anything(),
+ expect.objectContaining({ wait: 99, interval: 101 })
+ )
+ })
+ })
})
})
})
diff --git a/test/matchers/element/toBeDisplayed.test.ts b/test/matchers/element/toBeDisplayed.test.ts
index 250ead0bd..39f13780a 100644
--- a/test/matchers/element/toBeDisplayed.test.ts
+++ b/test/matchers/element/toBeDisplayed.test.ts
@@ -1,9 +1,11 @@
-import { vi, test, describe, expect } from 'vitest'
+import { vi, test, describe, expect, afterEach, beforeEach } from 'vitest'
import { $ } from '@wdio/globals'
import { toBeDisplayed } from '../../../src/matchers/element/toBeDisplayed.js'
import { executeCommandBe } from '../../../src/utils.js'
import { DEFAULT_OPTIONS } from '../../../src/constants.js'
+import { setDefaultOptions } from '../../../src/index.js'
+import type { ChainablePromiseElement } from 'webdriverio'
vi.mock('@wdio/globals')
vi.mock('../../../src/utils.js', async (importOriginal) => {
@@ -15,7 +17,7 @@ vi.mock('../../../src/utils.js', async (importOriginal) => {
}
})
-describe('toBeDisplayed', () => {
+describe(toBeDisplayed, () => {
/**
* result is inverted for toBeDisplayed because it inverts isEnabled result
* `!await el.isEnabled()`
@@ -189,4 +191,50 @@ Expected: "displayed"
Received: "not displayed"`
)
})
+
+ describe('global options', () => {
+ const defaultOptions = { ...DEFAULT_OPTIONS }
+
+ let el: ChainablePromiseElement
+
+ beforeEach(async () => {
+ setDefaultOptions({ wait: 99, interval: 101 })
+ el = await $('sel')
+ el.isDisplayed = vi.fn().mockResolvedValue(true)
+
+ })
+
+ afterEach(() => {
+ setDefaultOptions(defaultOptions)
+ })
+
+ test('should use globally set default options with executeCommandBe', async () => {
+ await toBeDisplayed.call({}, el)
+
+ expect(executeCommandBe).toHaveBeenCalledWith(
+ el,
+ expect.anything(),
+ expect.objectContaining({ wait: 99, interval: 101 })
+ )
+ })
+
+ test('should use globally set default options with isDisplayed', async () => {
+
+ await toBeDisplayed.call({}, el)
+
+ expect(executeCommandBe).toHaveBeenCalledWith(
+ el,
+ expect.anything(),
+ expect.objectContaining({ wait: 99, interval: 101 })
+ )
+ expect(el.isDisplayed).toHaveBeenCalledWith(
+ {
+ withinViewport: false,
+ contentVisibilityAuto: true,
+ opacityProperty: true,
+ visibilityProperty: true
+ }
+ )
+ })
+ })
})
diff --git a/test/options.test.ts b/test/options.test.ts
new file mode 100644
index 000000000..1daba5d48
--- /dev/null
+++ b/test/options.test.ts
@@ -0,0 +1,37 @@
+import { test, expect, describe, afterEach } from 'vitest'
+import { setDefaultOptions, setOptions } from '../src/index.js'
+import { DEFAULT_OPTIONS, DEFAULT_OPTIONS_TO_BE_DISPLAYED } from '../src/constants.js'
+
+describe('Default Options', () => {
+ const defaultOptions = { ...DEFAULT_OPTIONS }
+ afterEach(() => {
+ setDefaultOptions(defaultOptions)
+ })
+
+ describe(setOptions, () => {
+ test('legacy setOptions should update both DEFAULT_OPTIONS_TO_BE_DISPLAYED and DEFAULT_OPTIONS', () => {
+ expect(DEFAULT_OPTIONS_TO_BE_DISPLAYED.wait).not.toBe(98)
+ expect(DEFAULT_OPTIONS.wait).not.toBe(98)
+
+ setOptions({ wait: 98 })
+
+ expect(DEFAULT_OPTIONS_TO_BE_DISPLAYED.wait).toBe(98)
+ expect(DEFAULT_OPTIONS.wait).toBe(98)
+ })
+ })
+
+ describe(setDefaultOptions, () => {
+
+ test('setDefaultOptions should update both DEFAULT_OPTIONS_TO_BE_DISPLAYED and DEFAULT_OPTIONS', () => {
+ expect(DEFAULT_OPTIONS_TO_BE_DISPLAYED.wait).not.toBe(1234)
+ expect(DEFAULT_OPTIONS.wait).not.toBe(1234)
+
+ setDefaultOptions({ wait: 1234 })
+
+ expect(DEFAULT_OPTIONS_TO_BE_DISPLAYED.wait).toBe(1234)
+ expect(DEFAULT_OPTIONS.wait).toBe(1234)
+ })
+ })
+
+})
+