From 41fab042b3b3807e415419488f6f145f8b88173d Mon Sep 17 00:00:00 2001 From: dprevost-perso Date: Sun, 28 Dec 2025 15:18:00 -0500 Subject: [PATCH 1/2] fix: use only 1 options object and consider command options + fix typing - Merge toBeDisplayed options with command options as done for other kind of option (see NumberOptions or StringOptions) - Ensure default options are always applied when passing only 1 options - Fix typing not considering the `ToBeDisplayedOptions` --- __mocks__/@wdio/globals.ts | 21 ++++- src/matchers/element/toBeDisplayed.ts | 45 +++++------ test/matchers/element/toBeDisplayed.test.ts | 87 +++++++++++++++++++-- types/expect-webdriverio.d.ts | 28 ++++++- vitest.config.ts | 9 ++- 5 files changed, 150 insertions(+), 40 deletions(-) diff --git a/__mocks__/@wdio/globals.ts b/__mocks__/@wdio/globals.ts index 0ba7f9754..9f096ee79 100644 --- a/__mocks__/@wdio/globals.ts +++ b/__mocks__/@wdio/globals.ts @@ -1,3 +1,5 @@ +import { vi } from 'vitest' + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) function beFn() { @@ -32,9 +34,7 @@ function getSizeFn(property?: 'height' | 'width') { return this._size ? this._size(property) : undefined } -const element = { - $, - $$, +const elementMethods = { isDisplayed: beFn, isDisplayedInViewport: beFn, isExisting: beFn, @@ -50,11 +50,20 @@ const element = { getSize: getSizeFn, } +const element = { + $, + $$, + ...elementMethods +} + export function $(selector) { const el: any = { ...element, selector } + for (const [prop, method] of Object.entries(elementMethods)) { + el[prop] = vi.fn(method) + } el.getElement = async () => el return el } @@ -62,11 +71,15 @@ export function $(selector) { export function $$(selector) { const length = this?._length || 2 const els: any = Array(length).map((_, index) => { - return { + const el = { ...element, selector, index } + for (const [prop, method] of Object.entries(elementMethods)) { + el[prop] = vi.fn(method) + } + return el }) // Required to refetch const parent: any = element diff --git a/src/matchers/element/toBeDisplayed.ts b/src/matchers/element/toBeDisplayed.ts index f1b5e5e6f..16b0352e3 100644 --- a/src/matchers/element/toBeDisplayed.ts +++ b/src/matchers/element/toBeDisplayed.ts @@ -2,33 +2,17 @@ import { executeCommandBe } from '../../utils.js' import { DEFAULT_OPTIONS } from '../../constants.js' import type { WdioElementMaybePromise } from '../../types.js' -interface ToBeDisplayedOptions { - /** - * `true` to check if the element is within the viewport. false by default. - */ - withinViewport?: boolean - /** - * `true` to check if the element content-visibility property has (or inherits) the value auto, - * and it is currently skipping its rendering. `true` by default. - * @default true - */ - contentVisibilityAuto?: boolean - /** - * `true` to check if the element opacity property has (or inherits) a value of 0. `true` by default. - * @default true - */ - opacityProperty?: boolean - /** - * `true` to check if the element is invisible due to the value of its visibility property. `true` by default. - * @default true - */ - visibilityProperty?: boolean +const DEFAULT_OPTIONS_DISPLAYED: ExpectWebdriverIO.ToBeDisplayedOptions = { + ...DEFAULT_OPTIONS, + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true } export async function toBeDisplayed( received: WdioElementMaybePromise, - { withinViewport = false, contentVisibilityAuto = true, opacityProperty = true, visibilityProperty = true }: ToBeDisplayedOptions = {}, - options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS + options: ExpectWebdriverIO.ToBeDisplayedOptions = DEFAULT_OPTIONS_DISPLAYED, ) { this.expectation = this.expectation || 'displayed' @@ -37,7 +21,20 @@ export async function toBeDisplayed( options, }) - const result = await executeCommandBe.call(this, received, el => el?.isDisplayed({ withinViewport, contentVisibilityAuto, opacityProperty, visibilityProperty }), options) + const { + withinViewport, + contentVisibilityAuto, + opacityProperty, + visibilityProperty, + ...commandOptions + } = { ...DEFAULT_OPTIONS_DISPLAYED, ...options } + + const result = await executeCommandBe.call(this, received, el => el?.isDisplayed({ + withinViewport, + contentVisibilityAuto, + opacityProperty, + visibilityProperty + }), commandOptions) await options.afterAssertion?.({ matcherName: 'toBeDisplayed', diff --git a/test/matchers/element/toBeDisplayed.test.ts b/test/matchers/element/toBeDisplayed.test.ts index f5c11ce06..845adbf3c 100644 --- a/test/matchers/element/toBeDisplayed.test.ts +++ b/test/matchers/element/toBeDisplayed.test.ts @@ -3,8 +3,18 @@ import { $ } from '@wdio/globals' import { getExpectMessage, getReceived } from '../../__fixtures__/utils.js' import { toBeDisplayed } from '../../../src/matchers/element/toBeDisplayed.js' +import { executeCommandBe } from '../../../src/utils.js' +import { DEFAULT_OPTIONS } from '../../../src/constants.js' vi.mock('@wdio/globals') +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('toBeDisplayed', () => { /** @@ -24,7 +34,21 @@ describe('toBeDisplayed', () => { const beforeAssertion = vi.fn() const afterAssertion = vi.fn() - const result = await toBeDisplayed.call({}, el, {}, { beforeAssertion, afterAssertion }) + + const result = await toBeDisplayed.call({}, el, { beforeAssertion, afterAssertion }) + + expect(el.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ + wait: DEFAULT_OPTIONS.wait, + interval: DEFAULT_OPTIONS.interval + })) expect(result.pass).toBe(true) expect(el._attempts).toBe(0) expect(beforeAssertion).toBeCalledWith({ @@ -38,6 +62,28 @@ describe('toBeDisplayed', () => { }) }) + test('success with ToBeDisplayed and command options', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + return true + } + const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 1, withinViewport: true }) + + expect(el.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: true, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ + wait: 1, + interval: DEFAULT_OPTIONS.interval + })) + expect(result.pass).toBe(true) + }) + test('wait but failure', async () => { const el: any = await $('sel') el._value = function (): boolean { @@ -69,7 +115,8 @@ describe('toBeDisplayed', () => { return false } - const result = await toBeDisplayed.call({}, el, {}, { wait: 0 }) + const result = await toBeDisplayed.call({}, el, { wait: 0 }) + expect(result.pass).toBe(false) expect(el._attempts).toBe(1) }) @@ -82,7 +129,21 @@ describe('toBeDisplayed', () => { return true } - const result = await toBeDisplayed.call({}, el, {}, { wait: 0 }) + const result = await toBeDisplayed.call({}, el, { wait: 0 }) + + expect(el.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ + wait: 0, + interval: DEFAULT_OPTIONS.interval + })) + expect(result.pass).toBe(true) expect(el._attempts).toBe(1) }) @@ -92,7 +153,7 @@ describe('toBeDisplayed', () => { el._value = function (): boolean { return true } - const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 0 }) + const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 0 }) const received = getReceived(result.message()) expect(received).not.toContain('not') @@ -104,7 +165,7 @@ describe('toBeDisplayed', () => { el._value = function (): boolean { return false } - const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 0 }) + const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 0 }) const received = getReceived(result.message()) expect(received).toContain('not') @@ -116,7 +177,7 @@ describe('toBeDisplayed', () => { el._value = function (): boolean { return true } - const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 1 }) + const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 1 }) const received = getReceived(result.message()) expect(received).not.toContain('not') @@ -128,9 +189,21 @@ describe('toBeDisplayed', () => { el._value = function (): boolean { return false } - const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 1 }) + const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 1 }) const received = getReceived(result.message()) + expect(el.isDisplayed).toHaveBeenCalledWith( + { + withinViewport: false, + contentVisibilityAuto: true, + opacityProperty: true, + visibilityProperty: true + } + ) + expect(executeCommandBe).toHaveBeenCalledWith(el, expect.anything(), expect.objectContaining({ + wait: 1, + interval: DEFAULT_OPTIONS.interval + })) expect(received).toContain('not') expect(result.pass).toBe(false) }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 429c12c32..1d7649c88 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -122,7 +122,7 @@ interface WdioElementOrArrayMatchers<_R, ActualT = unknown> { /** * `WebdriverIO.Element` -> `isDisplayed` */ - toBeDisplayed: FnWhenElementOrArrayLike Promise> + toBeDisplayed: FnWhenElementOrArrayLike Promise> /** * `WebdriverIO.Element` -> `isExisting` @@ -702,6 +702,32 @@ declare namespace ExpectWebdriverIO { gte?: number } + interface ToBeDisplayedOptions extends CommandOptions { + /** + * `true` to check if the element is within the viewport. false by default. + */ + withinViewport?: boolean + + /** + * `true` to check if the element content-visibility property has (or inherits) the value auto, + * and it is currently skipping its rendering. `true` by default. + * @default true + */ + contentVisibilityAuto?: boolean + + /** + * `true` to check if the element opacity property has (or inherits) a value of 0. `true` by default. + * @default true + */ + opacityProperty?: boolean + + /** + * `true` to check if the element is invisible due to the value of its visibility property. `true` by default. + * @default true + */ + visibilityProperty?: boolean + } + type RequestedWith = { url?: string | ExpectWebdriverIO.PartialMatcher| ((url: string) => boolean) method?: string | Array diff --git a/vitest.config.ts b/vitest.config.ts index e85e599a4..a4a5028ac 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ exclude: [ '**/build/**', '**/__fixtures__/**', + '**/__mocks__/**', '**/*.test.ts', 'lib', 'test-types', @@ -29,10 +30,10 @@ export default defineConfig({ 'types-checks-filter-out-node_modules.js', ], thresholds: { - lines: 87.3, - functions: 85.8, - statements: 87, - branches: 78.6, + lines: 88.4, + functions: 86.9, + statements: 88.3, + branches: 79.4, } } } From db6e33efea03ee115b8f216bd53b0b7cd2b5473c Mon Sep 17 00:00:00 2001 From: dprevost-perso Date: Sun, 28 Dec 2025 15:25:43 -0500 Subject: [PATCH 2/2] fix unwanted isNot in UT --- test/matchers/element/toBeDisplayed.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/matchers/element/toBeDisplayed.test.ts b/test/matchers/element/toBeDisplayed.test.ts index 845adbf3c..1f99f27db 100644 --- a/test/matchers/element/toBeDisplayed.test.ts +++ b/test/matchers/element/toBeDisplayed.test.ts @@ -67,7 +67,7 @@ describe('toBeDisplayed', () => { el._value = function (): boolean { return true } - const result = await toBeDisplayed.call({ isNot: true }, el, { wait: 1, withinViewport: true }) + const result = await toBeDisplayed.call({}, el, { wait: 1, withinViewport: true }) expect(el.isDisplayed).toHaveBeenCalledWith( {