diff --git a/docs/API.md b/docs/API.md index 9f09e3803..7f9ca71ba 100644 --- a/docs/API.md +++ b/docs/API.md @@ -880,6 +880,29 @@ await expect(elem).toHaveElementClass(/Container/i) In addition to the `expect-webdriverio` matchers you can use builtin Jest's [expect](https://jestjs.io/docs/expect) assertions or [expect/expectAsync](https://jasmine.github.io/api/edge/global.html#expect) for Jasmine. +## Modifiers + +WebdriverIO supports usage of modifiers as `.not` and it will wait until the reverse condition is meet + +```ts +// Wait until the element is no longer present +await expect(element).not.toBeDisplayed() + +// Wait until the text is no more 'some title' +await expect(browser).not.toHaveTitle('some title') +``` + +In case immediate assertion is required, use `{ wait: 0 }` +```ts +// Ensure element is not present right now +await expect(element).not.toBeDisplayed({ wait: 0 }) + +// Ensure the text is not 'some title' right now +await expect(browser).not.toHaveTitle('some title', { wait: 0 }) +``` + +Note: You can pair `.not` with asymmetric matchers, but to enable the wait-until behavior, `.not` must be used directly on the `expect()` call. + ## Asymmetric Matchers WebdriverIO supports usage of asymmetric matchers wherever you compare text values, e.g.: diff --git a/src/matchers/browser/toHaveClipboardText.ts b/src/matchers/browser/toHaveClipboardText.ts index bcf863e7d..00b023408 100644 --- a/src/matchers/browser/toHaveClipboardText.ts +++ b/src/matchers/browser/toHaveClipboardText.ts @@ -10,6 +10,7 @@ export async function toHaveClipboardText( expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'clipboard text', verb = 'have' } = this await options.beforeAssertion?.({ @@ -27,7 +28,7 @@ export async function toHaveClipboardText( .catch((err) => log.warn(`Couldn't set clipboard permissions: ${err}`)) actual = await browser.execute(() => window.navigator.clipboard.readText()) return compareText(actual, expectedValue, options).result - }, options) + }, isNot, options) const message = enhanceError('browser', expectedValue, actual, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { diff --git a/src/matchers/browser/toHaveTitle.ts b/src/matchers/browser/toHaveTitle.ts index 236c55d4d..4c18dd7f8 100644 --- a/src/matchers/browser/toHaveTitle.ts +++ b/src/matchers/browser/toHaveTitle.ts @@ -6,6 +6,7 @@ export async function toHaveTitle( expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'title', verb = 'have' } = this await options.beforeAssertion?.({ @@ -19,7 +20,7 @@ export async function toHaveTitle( actual = await browser.getTitle() return compareText(actual, expectedValue, options).result - }, options) + }, isNot, options) const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { diff --git a/src/matchers/browser/toHaveUrl.ts b/src/matchers/browser/toHaveUrl.ts index 3dcc50195..06719ac5d 100644 --- a/src/matchers/browser/toHaveUrl.ts +++ b/src/matchers/browser/toHaveUrl.ts @@ -6,6 +6,7 @@ export async function toHaveUrl( expectedValue: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'url', verb = 'have' } = this await options.beforeAssertion?.({ @@ -19,7 +20,7 @@ export async function toHaveUrl( actual = await browser.getUrl() return compareText(actual, expectedValue, options).result - }, options) + }, isNot, options) const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { diff --git a/src/matchers/element/toHaveAttribute.ts b/src/matchers/element/toHaveAttribute.ts index c6ca5ac9d..9d42293e7 100644 --- a/src/matchers/element/toHaveAttribute.ts +++ b/src/matchers/element/toHaveAttribute.ts @@ -27,6 +27,7 @@ async function conditionAttrAndValue(el: WebdriverIO.Element, attribute: string, } export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attribute: string, value: string | RegExp | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) { + const isNot = this.isNot const { expectation = 'attribute', verb = 'have' } = this let el = await received?.getElement() @@ -37,7 +38,7 @@ export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attr = result.values return result.success - }, options) + }, isNot, options) const expected = wrapExpectedWithArray(el, attr, value) const message = enhanceError(el, expected, attr, this, verb, expectation, attribute, options) @@ -48,8 +49,9 @@ export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, } as ExpectWebdriverIO.AssertionResult } -async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: string, options: ExpectWebdriverIO.StringOptions) { - const { expectation = 'attribute', verb = 'have', isNot } = this +async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: string) { + const isNot = this.isNot + const { expectation = 'attribute', verb = 'have' } = this let el = await received?.getElement() @@ -58,9 +60,9 @@ async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: s el = result.el as WebdriverIO.Element return result.success - }, options) + }, isNot, {}) - const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, options) + const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, {}) return { pass, @@ -84,7 +86,7 @@ export async function toHaveAttribute( // Name and value is passed in e.g. el.toHaveAttribute('attr', 'value', (opts)) ? await toHaveAttributeAndValue.call(this, received, attribute, value, options) // Only name is passed in e.g. el.toHaveAttribute('attr') - : await toHaveAttributeFn.call(this, received, attribute, options) + : await toHaveAttributeFn.call(this, received, attribute) await options.afterAssertion?.({ matcherName: 'toHaveAttribute', diff --git a/src/matchers/element/toHaveChildren.ts b/src/matchers/element/toHaveChildren.ts index 712070e55..f9b035965 100644 --- a/src/matchers/element/toHaveChildren.ts +++ b/src/matchers/element/toHaveChildren.ts @@ -35,6 +35,7 @@ export async function toHaveChildren( expectedValue?: number | ExpectWebdriverIO.NumberOptions, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'children', verb = 'have' } = this await options.beforeAssertion?.({ @@ -55,7 +56,7 @@ export async function toHaveChildren( children = result.values return result.success - }, { ...numberOptions, ...options }) + }, isNot, { ...numberOptions, ...options }) const error = numberError(numberOptions) const expectedArray = wrapExpectedWithArray(el, children, error) diff --git a/src/matchers/element/toHaveClass.ts b/src/matchers/element/toHaveClass.ts index 0c7d53b9d..864d4ad04 100644 --- a/src/matchers/element/toHaveClass.ts +++ b/src/matchers/element/toHaveClass.ts @@ -42,6 +42,7 @@ export async function toHaveElementClass( expectedValue: string | RegExp | Array | WdioAsymmetricMatcher, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'class', verb = 'have' } = this await options.beforeAssertion?.({ @@ -61,7 +62,7 @@ export async function toHaveElementClass( attr = result.values return result.success - }, options) + }, isNot, options) const message = enhanceError(el, wrapExpectedWithArray(el, attr, expectedValue), attr, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { diff --git a/src/matchers/element/toHaveComputedLabel.ts b/src/matchers/element/toHaveComputedLabel.ts index f5729919c..50e2a9324 100644 --- a/src/matchers/element/toHaveComputedLabel.ts +++ b/src/matchers/element/toHaveComputedLabel.ts @@ -26,6 +26,7 @@ export async function toHaveComputedLabel( expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'computed label', verb = 'have' } = this await options.beforeAssertion?.({ @@ -45,6 +46,7 @@ export async function toHaveComputedLabel( return result.success }, + isNot, options ) diff --git a/src/matchers/element/toHaveComputedRole.ts b/src/matchers/element/toHaveComputedRole.ts index 72209acb4..916506b97 100644 --- a/src/matchers/element/toHaveComputedRole.ts +++ b/src/matchers/element/toHaveComputedRole.ts @@ -26,6 +26,7 @@ export async function toHaveComputedRole( expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'computed role', verb = 'have' } = this await options.beforeAssertion?.({ @@ -45,6 +46,7 @@ export async function toHaveComputedRole( return result.success }, + isNot, options ) diff --git a/src/matchers/element/toHaveElementProperty.ts b/src/matchers/element/toHaveElementProperty.ts index 68456901a..2c67f38d2 100644 --- a/src/matchers/element/toHaveElementProperty.ts +++ b/src/matchers/element/toHaveElementProperty.ts @@ -42,7 +42,8 @@ export async function toHaveElementProperty( value?: string | RegExp | WdioAsymmetricMatcher | null, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { - const { expectation = 'property', verb = 'have', isNot } = this + const isNot = this.isNot + const { expectation = 'property', verb = 'have' } = this await options.beforeAssertion?.({ matcherName: 'toHaveElementProperty', @@ -60,6 +61,7 @@ export async function toHaveElementProperty( return result.success }, + isNot, options ) diff --git a/src/matchers/element/toHaveHTML.ts b/src/matchers/element/toHaveHTML.ts index d8ceafb52..1f7b976e2 100644 --- a/src/matchers/element/toHaveHTML.ts +++ b/src/matchers/element/toHaveHTML.ts @@ -22,6 +22,7 @@ export async function toHaveHTML( expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.HTMLOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'HTML', verb = 'have' } = this await options.beforeAssertion?.({ @@ -43,7 +44,7 @@ export async function toHaveHTML( actualHTML = result.values return result.success - }, options) + }, isNot, options) const message = enhanceError(el, wrapExpectedWithArray(el, actualHTML, expectedValue), actualHTML, this, verb, expectation, '', options) diff --git a/src/matchers/element/toHaveHeight.ts b/src/matchers/element/toHaveHeight.ts index 5666b3338..0905d2183 100644 --- a/src/matchers/element/toHaveHeight.ts +++ b/src/matchers/element/toHaveHeight.ts @@ -22,6 +22,7 @@ export async function toHaveHeight( expectedValue: number | ExpectWebdriverIO.NumberOptions, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'height', verb = 'have' } = this await options.beforeAssertion?.({ @@ -52,6 +53,7 @@ export async function toHaveHeight( return result.success }, + isNot, { ...numberOptions, ...options } ) diff --git a/src/matchers/element/toHaveSize.ts b/src/matchers/element/toHaveSize.ts index 0976e7cc7..a2d2cc80a 100644 --- a/src/matchers/element/toHaveSize.ts +++ b/src/matchers/element/toHaveSize.ts @@ -19,6 +19,7 @@ export async function toHaveSize( expectedValue: { height: number; width: number }, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'size', verb = 'have' } = this await options.beforeAssertion?.({ @@ -39,6 +40,7 @@ export async function toHaveSize( return result.success }, + isNot, options ) diff --git a/src/matchers/element/toHaveStyle.ts b/src/matchers/element/toHaveStyle.ts index d13c032dc..5d61baf41 100644 --- a/src/matchers/element/toHaveStyle.ts +++ b/src/matchers/element/toHaveStyle.ts @@ -17,6 +17,7 @@ export async function toHaveStyle( expectedValue: { [key: string]: string; }, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'style', verb = 'have' } = this await options.beforeAssertion?.({ @@ -34,7 +35,7 @@ export async function toHaveStyle( actualStyle = result.values return result.success - }, options) + }, isNot, options) const message = enhanceError(el, wrapExpectedWithArray(el, actualStyle, expectedValue), actualStyle, this, verb, expectation, '', options) diff --git a/src/matchers/element/toHaveText.ts b/src/matchers/element/toHaveText.ts index edcab174e..82d4450c7 100644 --- a/src/matchers/element/toHaveText.ts +++ b/src/matchers/element/toHaveText.ts @@ -42,6 +42,7 @@ export async function toHaveText( expectedValue: string | RegExp | WdioAsymmetricMatcher | Array, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'text', verb = 'have' } = this await options.beforeAssertion?.({ @@ -63,7 +64,7 @@ export async function toHaveText( actualText = result.values return result.success - }, options) + }, isNot, options) const message = enhanceError(el, wrapExpectedWithArray(el, actualText, expectedValue), actualText, this, verb, expectation, '', options) const result: ExpectWebdriverIO.AssertionResult = { diff --git a/src/matchers/element/toHaveWidth.ts b/src/matchers/element/toHaveWidth.ts index 42eb8c0ad..6f706ffcb 100644 --- a/src/matchers/element/toHaveWidth.ts +++ b/src/matchers/element/toHaveWidth.ts @@ -22,6 +22,7 @@ export async function toHaveWidth( expectedValue: number | ExpectWebdriverIO.NumberOptions, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'width', verb = 'have' } = this await options.beforeAssertion?.({ @@ -52,6 +53,7 @@ export async function toHaveWidth( return result.success }, + isNot, { ...numberOptions, ...options } ) diff --git a/src/matchers/elements/toBeElementsArrayOfSize.ts b/src/matchers/elements/toBeElementsArrayOfSize.ts index 9e3621a49..39cd07ccd 100644 --- a/src/matchers/elements/toBeElementsArrayOfSize.ts +++ b/src/matchers/elements/toBeElementsArrayOfSize.ts @@ -8,6 +8,7 @@ export async function toBeElementsArrayOfSize( expectedValue: number | ExpectWebdriverIO.NumberOptions, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot const { expectation = 'elements array of size', verb = 'be' } = this await options.beforeAssertion?.({ @@ -37,7 +38,7 @@ export async function toBeElementsArrayOfSize( } elements = await refetchElements(elements, numberOptions.wait, true) return false - }, { ...numberOptions, ...options }) + }, isNot, { ...numberOptions, ...options }) if (Array.isArray(received) && pass) { for (let index = originalLength; index < elements.length; index++) { diff --git a/src/matchers/mock/toBeRequestedTimes.ts b/src/matchers/mock/toBeRequestedTimes.ts index e6953dd22..0a50b3af7 100644 --- a/src/matchers/mock/toBeRequestedTimes.ts +++ b/src/matchers/mock/toBeRequestedTimes.ts @@ -7,6 +7,7 @@ export async function toBeRequestedTimes( expectedValue: number | ExpectWebdriverIO.NumberOptions = {}, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS ) { + const isNot = this.isNot || false const { expectation = `called${typeof expectedValue === 'number' ? ' ' + expectedValue : '' } time${expectedValue !== 1 ? 's' : ''}`, verb = 'be' } = this await options.beforeAssertion?.({ @@ -24,7 +25,7 @@ export async function toBeRequestedTimes( const pass = await waitUntil(async () => { actual = received.calls.length return compareNumbers(actual, numberOptions) - }, { ...numberOptions, ...options }) + }, isNot, { ...numberOptions, ...options }) const error = numberError(numberOptions) const message = enhanceError('mock', error, actual, this, verb, expectation, '', numberOptions) diff --git a/src/matchers/mock/toBeRequestedWith.ts b/src/matchers/mock/toBeRequestedWith.ts index c735c4057..338fb83df 100644 --- a/src/matchers/mock/toBeRequestedWith.ts +++ b/src/matchers/mock/toBeRequestedWith.ts @@ -24,7 +24,8 @@ export async function toBeRequestedWith( expectedValue: ExpectWebdriverIO.RequestedWith = {}, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS ) { - const { expectation = 'called with', verb = 'be', isNot } = this + const isNot = this.isNot || false + const { expectation = 'called with', verb = 'be' } = this await options.beforeAssertion?.({ matcherName: 'toBeRequestedWith', @@ -53,6 +54,7 @@ export async function toBeRequestedWith( return false }, + isNot, { ...options, wait: isNot ? 0 : options.wait } ) diff --git a/src/utils.ts b/src/utils.ts index 8720b86cc..3987241ab 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,32 +39,49 @@ function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetri */ const waitUntil = async ( condition: () => Promise, + isNot = false, { wait = DEFAULT_OPTIONS.wait, interval = DEFAULT_OPTIONS.interval } = {} ): Promise => { - const start = Date.now() - let error: unknown - let result = false - - do { - try { - result = await condition() - error = undefined - if (result) { - break + // single attempt + if (wait === 0) { + return await condition() + } + + let error: Error | undefined + + // wait for condition to be truthy + try { + const start = Date.now() + while (true) { + if (Date.now() - start > wait) { + throw new Error('timeout') + } + + try { + const result = isNot !== (await condition()) + error = undefined + if (result) { + break + } + await sleep(interval) + } catch (err) { + error = err + await sleep(interval) } - } catch (err) { - error = err } - // No need to sleep again if time is already over - if (Date.now() - start < wait) { - await sleep(interval) + if (error) { + throw error } - } while (Date.now() - start < wait) - if (error) { throw error } + return !isNot + } catch { + if (error) { + throw error + } - return result + return isNot + } } async function executeCommandBe( @@ -72,7 +89,7 @@ async function executeCommandBe( command: (el: WebdriverIO.Element) => Promise, options: ExpectWebdriverIO.CommandOptions ): ExpectWebdriverIO.AsyncAssertionResult { - const { expectation, verb = 'be' } = this + const { isNot, expectation, verb = 'be' } = this let el = await received?.getElement() const pass = await waitUntil( @@ -86,6 +103,7 @@ async function executeCommandBe( el = result.el as WebdriverIO.Element return result.success }, + isNot, options ) diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 7cb19b8aa..a1534d539 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -402,7 +402,8 @@ Received: 100`) }) - describe('Matcher eventually passing', async () => { + // Skipped since even though logically correct, this is not too user-friendly and breaks today's current expected behaviour, see https://github.com/webdriverio/expect-webdriverio/issues/2013 + describe.skip('Matcher eventually passing', async () => { test('when element eventually is displayed, matcher and .not matcher should be consistent', async () => { const el = await $('selector') @@ -454,4 +455,30 @@ Received: "not displayed"`) expect(el.isDisplayed).toHaveBeenCalledTimes(6) }) }) + + describe('Matchers should cover real life scenarios', async () => { + test('Using toBeDisplayed and not.toBeDisplayed before and after a component is being discarded should work easily', async () => { + const el = await $('selector') + + // Element takes time to display + vi.mocked(el.isDisplayed) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true) + + // Passes when element becomes displayed + await expectLib(el).toBeDisplayed() + + // The element ok button is clicked and the component is discarded... + + // ...but the element takes time to be removed from the DOM (below 500 ms in real life) + vi.mocked(el.isDisplayed) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + + // We should be able to assert that the element is no longer displayed by default without additional code or configuration + await expectLib(el).not.toBeDisplayed() + }) + }) }) diff --git a/test/utils.test.ts b/test/utils.test.ts index cba27af79..377628621 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -159,94 +159,189 @@ describe('utils', () => { }) }) - describe('waitUntil', () => { + describe(waitUntil, () => { - describe('should be pass=true for normal success and pass=true for `isNot` failure', () => { - test('should return true when condition is met', async () => { - const condition = vi.fn().mockResolvedValue(true) + describe('given we should wait for the condition to be met (modifier `.not` is not used)', () => { + const isNot = undefined + describe('should be pass=true for normal success', () => { + test('should return true when condition is met', async () => { + const condition = vi.fn().mockResolvedValue(true) - const result = await waitUntil(condition, { wait: 1000, interval: 100 }) + const result = await waitUntil(condition, isNot, { wait: 1000, interval: 100 }) - expect(result).toBe(true) - }) + expect(result).toBe(true) + }) - test('should return true with wait 0', async () => { - const condition = vi.fn().mockResolvedValue(true) + test('should return true with wait 0', async () => { + const condition = vi.fn().mockResolvedValue(true) - const result = await waitUntil(condition, { wait: 0 }) + const result = await waitUntil(condition, isNot, { wait: 0 }) - expect(result).toBe(true) - }) + expect(result).toBe(true) + }) + + test('should return true when condition is met within wait time', async () => { + const condition = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValueOnce(true) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) - test('should return true when condition is met within wait time', async () => { - const condition = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValueOnce(true) + test('should return true when condition errors but still is met within wait time', async () => { + const condition = vi.fn().mockRejectedValueOnce(new Error('Test error')).mockRejectedValueOnce(new Error('Test error')).mockResolvedValueOnce(true) - const result = await waitUntil(condition, { wait: 1000, interval: 50 }) + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) - expect(result).toBe(true) - expect(condition).toBeCalledTimes(3) + expect(result).toBe(true) + expect(condition).toBeCalledTimes(3) + }) + + test('should use default options when not provided', async () => { + const condition = vi.fn().mockResolvedValue(true) + + const result = await waitUntil(condition) + + expect(result).toBe(true) + }) }) - test('should return true when condition errors but still is met within wait time', async () => { - const condition = vi.fn().mockRejectedValueOnce(new Error('Test error')).mockRejectedValueOnce(new Error('Test error')).mockResolvedValueOnce(true) + describe('should be pass=false for normal failure', () => { + + test('should return false when condition is not met within wait time', async () => { + const condition = vi.fn().mockResolvedValue(false) - const result = await waitUntil(condition, { wait: 1000, interval: 50 }) + const result = await waitUntil(condition, isNot, { wait: 200, interval: 50 }) - expect(result).toBe(true) - expect(condition).toBeCalledTimes(3) + expect(result).toBe(false) + }) + + test('should return false when condition is not met and wait is 0', async () => { + const condition = vi.fn().mockResolvedValue(false) + + const result = await waitUntil(condition, isNot, { wait: 0 }) + + expect(result).toBe(false) + }) + + test('should return false if condition throws but still return false', async () => { + const condition = vi.fn().mockRejectedValueOnce(new Error('Always failing')).mockRejectedValueOnce(new Error('Always failing')).mockResolvedValue(false) + + const result = await waitUntil(condition, isNot, { wait: 180, interval: 50 }) + + expect(result).toBe(false) + expect(condition).toBeCalledTimes(4) + }) }) - test('should use default options when not provided', async () => { - const condition = vi.fn().mockResolvedValue(true) + describe('when condition throws', () => { + const error = new Error('failing') + + test('should throw with wait', async () => { + const condition = vi.fn().mockRejectedValue(error) + + await expect(() => waitUntil(condition, isNot, { wait: 200, interval: 50 })).rejects.toThrowError('failing') + }) + + test('should throw with wait 0', async () => { + const condition = vi.fn().mockRejectedValue(error) - const result = await waitUntil(condition) + await expect(() => waitUntil(condition, isNot, { wait: 0 })).rejects.toThrowError('failing') - expect(result).toBe(true) + }) }) }) - describe('should be pass=false for normal failure or pass=false for `isNot` success', () => { + describe('given we should wait for the reverse condition to meet since element state can take time to update (modifier `.not` is true to for reverse condition)', () => { + const isNot = true + describe('should be pass=false for normal success', () => { + test('should return false when condition is met', async () => { + const condition = vi.fn().mockResolvedValue(false) - test('should return false when condition is not met within wait time', async () => { - const condition = vi.fn().mockResolvedValue(false) + const result = await waitUntil(condition, isNot, { wait: 1000, interval: 100 }) - const result = await waitUntil(condition, { wait: 200, interval: 50 }) + expect(result).toBe(false) + }) - expect(result).toBe(false) - }) + test('should return false with wait 0', async () => { + const condition = vi.fn().mockResolvedValue(false) - test('should return false when condition is not met and wait is 0', async () => { - const condition = vi.fn().mockResolvedValue(false) + const result = await waitUntil(condition, isNot, { wait: 0 }) - const result = await waitUntil(condition, { wait: 0 }) + expect(result).toBe(false) + }) - expect(result).toBe(false) - }) + test('should return false when condition is met within wait time', async () => { + const condition = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(true).mockResolvedValueOnce(false) + + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) + + expect(result).toBe(false) // success for .not, boolean is inverted later by jest's expect library + expect(condition).toBeCalledTimes(3) + }) + + test('should return false when condition errors but still is met within wait time', async () => { + const condition = vi.fn().mockRejectedValueOnce(new Error('Test error')).mockRejectedValueOnce(new Error('Test error')).mockResolvedValueOnce(false) - test('should return false if condition throws but still return false', async () => { - const condition = vi.fn().mockRejectedValueOnce(new Error('Always failing')).mockRejectedValueOnce(new Error('Always failing')).mockResolvedValue(false) + const result = await waitUntil(condition, isNot, { wait: 990, interval: 50 }) - const result = await waitUntil(condition, { wait: 200, interval: 50 }) + expect(result).toBe(false) + expect(condition).toBeCalledTimes(3) + }) - expect(result).toBe(false) - expect(condition).toBeCalledTimes(4) + test('should use default options when not provided', async () => { + const condition = vi.fn().mockResolvedValue(false) + + const result = await waitUntil(condition, isNot) + + expect(result).toBe(false) + }) }) - }) - describe('when condition throws', () => { - const error = new Error('failing') + describe('should be pass=true for normal failure', () => { + + test('should return true when condition is not met within wait time', async () => { + const condition = vi.fn().mockResolvedValue(true) - test('should throw with wait', async () => { - const condition = vi.fn().mockRejectedValue(error) + const result = await waitUntil(condition, isNot, { wait: 200, interval: 50 }) - await expect(() => waitUntil(condition, { wait: 200, interval: 50 })).rejects.toThrowError('failing') + expect(result).toBe(true) + }) + + test('should return true when condition is not met and wait is 0', async () => { + const condition = vi.fn().mockResolvedValue(true) + + const result = await waitUntil(condition, isNot, { wait: 0 }) + + expect(result).toBe(true) + }) + + test('should return true if condition throws but still return true', async () => { + const condition = vi.fn().mockRejectedValueOnce(new Error('Always failing')).mockRejectedValueOnce(new Error('Always failing')).mockResolvedValue(true) + + const result = await waitUntil(condition, isNot, { wait: 190, interval: 50 }) + + expect(result).toBe(true) + expect(condition).toBeCalledTimes(4) + }) }) - test('should throw with wait 0', async () => { - const condition = vi.fn().mockRejectedValue(error) + describe('when condition throws', () => { + const error = new Error('failing') + + test('should throw with wait', async () => { + const condition = vi.fn().mockRejectedValue(error) + + await expect(() => waitUntil(condition, isNot, { wait: 200, interval: 50 })).rejects.toThrowError('failing') + }) + + test('should throw with wait 0', async () => { + const condition = vi.fn().mockRejectedValue(error) - await expect(() => waitUntil(condition, { wait: 0 })).rejects.toThrowError('failing') + await expect(() => waitUntil(condition, isNot, { wait: 0 })).rejects.toThrowError('failing') + }) }) }) })