From 2df2ff6d88c960f53d9e761e1f4cb7a9a0786bcf Mon Sep 17 00:00:00 2001 From: Erwin Heitzman <15839059+erwinheitzman@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:35:34 +0200 Subject: [PATCH 1/3] polish(expect-webdriverio): expose contentVisibilityAuto, opacityProperty and visibilityProperty to toBeDisplayed --- src/matchers/element/toBeDisplayed.ts | 30 ++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/matchers/element/toBeDisplayed.ts b/src/matchers/element/toBeDisplayed.ts index ae27eb050..0f23b9771 100644 --- a/src/matchers/element/toBeDisplayed.ts +++ b/src/matchers/element/toBeDisplayed.ts @@ -2,10 +2,38 @@ 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 +} + export async function toBeDisplayed( received: WdioElementMaybePromise, - options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS + options: ExpectWebdriverIO.CommandOptions & ToBeDisplayedOptions = DEFAULT_OPTIONS ) { + options.withinViewport ??= false; + options.contentVisibilityAuto ??= true; + options.opacityProperty ??= true; + options.visibilityProperty ??= true; + this.expectation = this.expectation || 'displayed' await options.beforeAssertion?.({ From 596deb8e495d6b35bd428de0ecc3e333fde05d7e Mon Sep 17 00:00:00 2001 From: Erwin Heitzman <15839059+erwinheitzman@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:21:21 +0200 Subject: [PATCH 2/3] polish(expect-webdriverio): expose contentVisibilityAuto, opacityProperty and visibilityProperty to toBeDisplayed --- src/matchers/element/toBeDisplayed.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matchers/element/toBeDisplayed.ts b/src/matchers/element/toBeDisplayed.ts index 0f23b9771..37659c032 100644 --- a/src/matchers/element/toBeDisplayed.ts +++ b/src/matchers/element/toBeDisplayed.ts @@ -29,10 +29,10 @@ export async function toBeDisplayed( received: WdioElementMaybePromise, options: ExpectWebdriverIO.CommandOptions & ToBeDisplayedOptions = DEFAULT_OPTIONS ) { - options.withinViewport ??= false; - options.contentVisibilityAuto ??= true; - options.opacityProperty ??= true; - options.visibilityProperty ??= true; + options.withinViewport ??= false + options.contentVisibilityAuto ??= true + options.opacityProperty ??= true + options.visibilityProperty ??= true this.expectation = this.expectation || 'displayed' From dcbd3a15bd5f00d546a79d1f50c96a7954a4b35e Mon Sep 17 00:00:00 2001 From: Erwin Heitzman <15839059+erwinheitzman@users.noreply.github.com> Date: Wed, 23 Jul 2025 01:17:00 +0200 Subject: [PATCH 3/3] polish(expect-webdriverio): expose contentVisibilityAuto, opacityProperty and visibilityProperty to toBeDisplayed --- src/matchers/element/toBeDisplayed.ts | 10 +- test/matchers/beMatchers.test.ts | 2 +- test/matchers/element/toBeDisplayed.test.ts | 146 ++++++++++++++++++++ 3 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 test/matchers/element/toBeDisplayed.test.ts diff --git a/src/matchers/element/toBeDisplayed.ts b/src/matchers/element/toBeDisplayed.ts index 37659c032..f1b5e5e6f 100644 --- a/src/matchers/element/toBeDisplayed.ts +++ b/src/matchers/element/toBeDisplayed.ts @@ -27,13 +27,9 @@ interface ToBeDisplayedOptions { export async function toBeDisplayed( received: WdioElementMaybePromise, - options: ExpectWebdriverIO.CommandOptions & ToBeDisplayedOptions = DEFAULT_OPTIONS + { withinViewport = false, contentVisibilityAuto = true, opacityProperty = true, visibilityProperty = true }: ToBeDisplayedOptions = {}, + options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - options.withinViewport ??= false - options.contentVisibilityAuto ??= true - options.opacityProperty ??= true - options.visibilityProperty ??= true - this.expectation = this.expectation || 'displayed' await options.beforeAssertion?.({ @@ -41,7 +37,7 @@ export async function toBeDisplayed( options, }) - const result = await executeCommandBe.call(this, received, el => el?.isDisplayed(), options) + const result = await executeCommandBe.call(this, received, el => el?.isDisplayed({ withinViewport, contentVisibilityAuto, opacityProperty, visibilityProperty }), options) await options.afterAssertion?.({ matcherName: 'toBeDisplayed', diff --git a/test/matchers/beMatchers.test.ts b/test/matchers/beMatchers.test.ts index c83036b80..1cc948766 100644 --- a/test/matchers/beMatchers.test.ts +++ b/test/matchers/beMatchers.test.ts @@ -5,7 +5,7 @@ import * as Matchers from '../../src/matchers.js' vi.mock('@wdio/globals') -const ignoredMatchers = ['toBeElementsArrayOfSize', 'toBeDisabled', 'toBeRequested', 'toBeRequestedTimes', 'toBeRequestedWithResponse', 'toBeRequestedWith'] +const ignoredMatchers = ['toBeElementsArrayOfSize', 'toBeDisabled', 'toBeDisplayed', 'toBeRequested', 'toBeRequestedTimes', 'toBeRequestedWithResponse', 'toBeRequestedWith'] const beMatchers = [ ...Object.keys(Matchers).filter(name => name.startsWith('toBe') && !ignoredMatchers.includes(name)), 'toExist' diff --git a/test/matchers/element/toBeDisplayed.test.ts b/test/matchers/element/toBeDisplayed.test.ts new file mode 100644 index 000000000..f5c11ce06 --- /dev/null +++ b/test/matchers/element/toBeDisplayed.test.ts @@ -0,0 +1,146 @@ +import { vi, test, describe, expect } from 'vitest' +import { $ } from '@wdio/globals' + +import { getExpectMessage, getReceived } from '../../__fixtures__/utils.js' +import { toBeDisplayed } from '../../../src/matchers/element/toBeDisplayed.js' + +vi.mock('@wdio/globals') + +describe('toBeDisplayed', () => { + /** + * result is inverted for toBeDisplayed because it inverts isEnabled result + * `!await el.isEnabled()` + */ + test('wait for success', async () => { + const el: any = await $('sel') + el._attempts = 2 + el._value = function (): boolean { + if (this._attempts > 0) { + this._attempts-- + return false + } + return true + } + + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + const result = await toBeDisplayed.call({}, el, {}, { beforeAssertion, afterAssertion }) + expect(result.pass).toBe(true) + expect(el._attempts).toBe(0) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeDisplayed', + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeDisplayed', + options: { beforeAssertion, afterAssertion }, + result + }) + }) + + test('wait but failure', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + throw new Error('some error') + } + + await expect(() => toBeDisplayed.call({}, el)) + .rejects.toThrow('some error') + }) + + test('success on the first attempt', async () => { + const el: any = await $('sel') + el._attempts = 0 + el._value = function (): boolean { + this._attempts++ + return true + } + + const result = await toBeDisplayed.call({}, el) + expect(result.pass).toBe(true) + expect(el._attempts).toBe(1) + }) + + test('no wait - failure', async () => { + const el: any = await $('sel') + el._attempts = 0 + el._value = function (): boolean { + this._attempts++ + return false + } + + const result = await toBeDisplayed.call({}, el, {}, { wait: 0 }) + expect(result.pass).toBe(false) + expect(el._attempts).toBe(1) + }) + + test('no wait - success', async () => { + const el: any = await $('sel') + el._attempts = 0 + el._value = function (): boolean { + this._attempts++ + return true + } + + const result = await toBeDisplayed.call({}, el, {}, { wait: 0 }) + expect(result.pass).toBe(true) + expect(el._attempts).toBe(1) + }) + + test('not - failure', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + return true + } + const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 0 }) + const received = getReceived(result.message()) + + expect(received).not.toContain('not') + expect(result.pass).toBe(true) + }) + + test('not - success', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + return false + } + const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 0 }) + const received = getReceived(result.message()) + + expect(received).toContain('not') + expect(result.pass).toBe(false) + }) + + test('not - failure (with wait)', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + return true + } + const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 1 }) + const received = getReceived(result.message()) + + expect(received).not.toContain('not') + expect(result.pass).toBe(true) + }) + + test('not - success (with wait)', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + return false + } + const result = await toBeDisplayed.call({ isNot: true }, el, {}, { wait: 1 }) + const received = getReceived(result.message()) + + expect(received).toContain('not') + expect(result.pass).toBe(false) + }) + + test('message', async () => { + const el: any = await $('sel') + el._value = function (): boolean { + return false + } + const result = await toBeDisplayed.call({}, el) + expect(getExpectMessage(result.message())).toContain('to be displayed') + }) +})