diff --git a/package.json b/package.json index d293e1d19..dbc81bba3 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "ts:mocha": "cd test-types/mocha && tsc --project ./tsconfig.json", "ts:jasmine": "cd test-types/jasmine && tsc --project ./tsconfig.json", "ts:jasmine-async": "cd test-types/jasmine-async && tsc --project ./tsconfig.json", + "checks:all": "npm run build && npm run compile && npm run tsc:root-types && npm run test && npm run ts", "watch": "npm run compile -- --watch", "prepare": "husky install" }, diff --git a/src/matchers/elements/toBeElementsArrayOfSize.ts b/src/matchers/elements/toBeElementsArrayOfSize.ts index 3b8f65a45..39cd07ccd 100644 --- a/src/matchers/elements/toBeElementsArrayOfSize.ts +++ b/src/matchers/elements/toBeElementsArrayOfSize.ts @@ -1,7 +1,7 @@ import { waitUntil, enhanceError, compareNumbers, numberError } from '../../utils.js' import { refetchElements } from '../../util/refetchElements.js' import { DEFAULT_OPTIONS } from '../../constants.js' -import type { WdioElementsMaybePromise } from '../../types.js' +import type { WdioElements, WdioElementsMaybePromise } from '../../types.js' export async function toBeElementsArrayOfSize( received: WdioElementsMaybePromise, @@ -17,17 +17,16 @@ export async function toBeElementsArrayOfSize( options, }) - // type check let numberOptions: ExpectWebdriverIO.NumberOptions if (typeof expectedValue === 'number') { - numberOptions = { eq: expectedValue } as ExpectWebdriverIO.NumberOptions + numberOptions = { eq: expectedValue } satisfies ExpectWebdriverIO.NumberOptions } else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) { throw new Error('Invalid params passed to toBeElementsArrayOfSize.') } else { numberOptions = expectedValue } - let elements = await received as WebdriverIO.ElementArray + let elements = await received as WdioElements const originalLength = elements.length const pass = await waitUntil(async () => { /** diff --git a/src/types.ts b/src/types.ts index d74569d50..7fb7d2420 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,12 +5,14 @@ export type WdioElementMaybePromise = WebdriverIO.Element | ChainablePromiseElement +export type WdioElements = WebdriverIO.ElementArray | WebdriverIO.Element[] + export type WdioElementsMaybePromise = - WebdriverIO.ElementArray | + WdioElements | ChainablePromiseArray export type RawMatcherFn = { (this: Context, actual: unknown, ...expected: unknown[]): ExpectationResult; } -export type WdioMatchersObject = Map \ No newline at end of file +export type WdioMatchersObject = Map diff --git a/src/util/elementsUtil.ts b/src/util/elementsUtil.ts index 5c844d9c3..0c4b0f8f6 100644 --- a/src/util/elementsUtil.ts +++ b/src/util/elementsUtil.ts @@ -11,3 +11,7 @@ export const wrapExpectedWithArray = (el: WebdriverIO.Element | WebdriverIO.Elem } return expected } + +export const isElementArray = (obj: unknown): obj is WebdriverIO.ElementArray => { + return obj !== null && typeof obj === 'object' && 'selector' in obj && 'foundWith' in obj && 'parent' in obj +} diff --git a/src/util/formatMessage.ts b/src/util/formatMessage.ts index 631bb55ec..d5bf2b665 100644 --- a/src/util/formatMessage.ts +++ b/src/util/formatMessage.ts @@ -1,5 +1,7 @@ import { printDiffOrStringify, printExpected, printReceived } from 'jest-matcher-utils' import { equals } from '../jasmineUtils.js' +import type { WdioElements } from '../types.js' +import { isElementArray } from './elementsUtil.js' const EXPECTED_LABEL = 'Expected' const RECEIVED_LABEL = 'Received' @@ -15,15 +17,15 @@ export const getSelector = (el: WebdriverIO.Element | WebdriverIO.ElementArray) return result } -export const getSelectors = (el: WebdriverIO.Element | WebdriverIO.ElementArray) => { +export const getSelectors = (el: WebdriverIO.Element | WdioElements) => { const selectors = [] - let parent: WebdriverIO.Element | WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | undefined + let parent: WebdriverIO.ElementArray['parent'] | undefined - if (Array.isArray(el)) { - selectors.push(`${(el as WebdriverIO.ElementArray).foundWith}(\`${getSelector(el)}\`)`) + if (isElementArray(el)) { + selectors.push(`${(el).foundWith}(\`${getSelector(el)}\`)`) parent = el.parent - } else { - parent = el as WebdriverIO.Element + } else if (!Array.isArray(el)) { + parent = el } while (parent && 'selector' in parent) { @@ -42,7 +44,7 @@ export const not = (isNot: boolean): string => { } export const enhanceError = ( - subject: string | WebdriverIO.Element | WebdriverIO.ElementArray, + subject: string | WebdriverIO.Element | WdioElements, expected: unknown, actual: unknown, context: { isNot: boolean }, diff --git a/src/util/refetchElements.ts b/src/util/refetchElements.ts index be195d90c..2ee14a228 100644 --- a/src/util/refetchElements.ts +++ b/src/util/refetchElements.ts @@ -1,19 +1,19 @@ import { DEFAULT_OPTIONS } from '../constants.js' +import type { WdioElements } from '../types.js' +import { isElementArray } from './elementsUtil.js' /** - * refetch elements array - * @param elements WebdriverIO.ElementArray + * Refetch elements array or return when elements is not of type WebdriverIO.ElementArray + * @param elements WebdriverIO.ElementArray | WebdriverIO.Element[] */ export const refetchElements = async ( - elements: WebdriverIO.ElementArray, + elements: WdioElements, wait = DEFAULT_OPTIONS.wait, full = false -): Promise => { - if (elements) { - if (wait > 0 && (elements.length === 0 || full)) { - // @ts-ignore - elements = (await elements.parent[elements.foundWith](elements.selector, ...elements.props) as WebdriverIO.ElementArray) - } +): Promise => { + if (elements && wait > 0 && (elements.length === 0 || full) && isElementArray(elements) && elements.parent && elements.foundWith && elements.foundWith in elements.parent) { + const fetchFunction = elements.parent[elements.foundWith as keyof typeof elements.parent] as Function + elements = await fetchFunction(elements.selector, ...elements.props) } return elements } diff --git a/test-types/mocha/types-mocha.test.ts b/test-types/mocha/types-mocha.test.ts index 4d5ebd886..877ecdddf 100644 --- a/test-types/mocha/types-mocha.test.ts +++ b/test-types/mocha/types-mocha.test.ts @@ -7,6 +7,7 @@ describe('type assertions', () => { const element: WebdriverIO.Element = {} as unknown as WebdriverIO.Element const elementArray: WebdriverIO.ElementArray = [] as unknown as WebdriverIO.ElementArray + const elements: WebdriverIO.Element[] = [] as unknown as WebdriverIO.Element[] const networkMock: WebdriverIO.Mock = {} as unknown as WebdriverIO.Mock @@ -323,6 +324,26 @@ describe('type assertions', () => { expectVoid = expect(chainableArray).toBeElementsArrayOfSize({ lte: 10 }) }) + it('should work correctly when actual is element array', async () => { + expectPromiseVoid = expect(elementArray).toBeElementsArrayOfSize(5) + expectPromiseVoid = expect(elementArray).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = expect(elementArray).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expect(elementArray).toBeElementsArrayOfSize({ lte: 10 }) + }) + + it('should work correctly when actual is element[]', async () => { + expectPromiseVoid = expect(elements).toBeElementsArrayOfSize(5) + expectPromiseVoid = expect(elements).toBeElementsArrayOfSize({ lte: 10 }) + + // @ts-expect-error + expectVoid = expect(elements).toBeElementsArrayOfSize(5) + // @ts-expect-error + expectVoid = expect(elements).toBeElementsArrayOfSize({ lte: 10 }) + }) + it('should not work when actual is not chainableArray', async () => { // @ts-expect-error await expect(chainableElement).toBeElementsArrayOfSize(5) diff --git a/test/matchers/elements/toBeElementsArrayOfSize.test.ts b/test/matchers/elements/toBeElementsArrayOfSize.test.ts index e3b26bcbc..1af391110 100644 --- a/test/matchers/elements/toBeElementsArrayOfSize.test.ts +++ b/test/matchers/elements/toBeElementsArrayOfSize.test.ts @@ -34,132 +34,232 @@ vi.mock('@wdio/globals', () => ({ })) describe('toBeElementsArrayOfSize', () => { - let els: WebdriverIO.ElementArray + describe('given an elements of type WebdriverIO.ElementArray', () => { + let els: WebdriverIO.ElementArray - beforeEach(() => { - els = $$('parent') as unknown as WebdriverIO.ElementArray - }) + beforeEach(() => { + els = $$('parent') as unknown as WebdriverIO.ElementArray + }) - describe('success', () => { - test('array of size 2', async () => { - const beforeAssertion = vi.fn() - const afterAssertion = vi.fn() - const result = await toBeElementsArrayOfSize.call({}, els, 2, { beforeAssertion, afterAssertion }) - expect(result.pass).toBe(true) - expect(beforeAssertion).toBeCalledWith({ - matcherName: 'toBeElementsArrayOfSize', - expectedValue: 2, - options: { beforeAssertion, afterAssertion } - }) - expect(afterAssertion).toBeCalledWith({ - matcherName: 'toBeElementsArrayOfSize', - expectedValue: 2, - options: { beforeAssertion, afterAssertion }, - result + describe('success', () => { + test('array of size 2', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + const result = await toBeElementsArrayOfSize.call({}, els, 2, { beforeAssertion, afterAssertion }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeElementsArrayOfSize', + expectedValue: 2, + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeElementsArrayOfSize', + expectedValue: 2, + options: { beforeAssertion, afterAssertion }, + result + }) + }) + test('array of size 5', async () => { + els = createMockElementArray(5) + const result = await toBeElementsArrayOfSize.call({}, els, 5, {}) + expect(result.pass).toBe(true) }) }) - test('array of size 5', async () => { - els = createMockElementArray(5) - const result = await toBeElementsArrayOfSize.call({}, els, 5, {}) - expect(result.pass).toBe(true) + + describe('failure', () => { + let result: any + + beforeEach(async () => { + result = await toBeElementsArrayOfSize.call({}, els, 5, {}) + }) + + test('fails', () => { + expect(result.pass).toBe(false) + }) + + describe('message shows correctly', () => { + test('expect message', () => { + expect(getExpectMessage(result.message())).toContain('to be elements array of size') + }) + test('expected message', () => { + expect(getExpected(result.message())).toContain('5') + }) + test('received message', () => { + expect(getReceived(result.message())).toContain('2') + }) + }) }) - }) - describe('failure', () => { - let result: any + describe('error catching', () => { + test('throws error with incorrect size param', async () => { + await expect(toBeElementsArrayOfSize.call({}, els, '5' as any)).rejects.toThrow('Invalid params passed to toBeElementsArrayOfSize.') + }) - beforeEach(async () => { - result = await toBeElementsArrayOfSize.call({}, els, 5, {}) + test('works if size contains options', async () => { + const result = await toBeElementsArrayOfSize.call({}, els, { lte: 5 }) + expect(result.pass).toBe(true) + }) }) - test('fails', () => { - expect(result.pass).toBe(false) + describe('number options', () => { + test.each([ + ['lte', 10, true], + ['lte', 1, false], + ['gte', 1, true], + ['gte', 10, false], + ['gte and lte', { gte: 1, lte: 10 }, true], + ['not gte but is lte', { gte: 10, lte: 10 }, false], + ['not lte but is gte', { gte: 1, lte: 1 }, false], + ])('should handle %s correctly', async (_, option, expected) => { + const result = await toBeElementsArrayOfSize.call({}, els, typeof option === 'object' ? option : { [_ as string]: option }) + expect(result.pass).toBe(expected) + }) }) - describe('message shows correctly', () => { - test('expect message', () => { - expect(getExpectMessage(result.message())).toContain('to be elements array of size') + describe('array update', () => { + test('updates the received array when assertion passes', async () => { + const receivedArray = createMockElementArray(2); + (receivedArray.parent as any)._length = 5; + (receivedArray.parent as any).$$ = vi.fn().mockReturnValue(createMockElementArray(5)) + + const result = await toBeElementsArrayOfSize.call({}, receivedArray, 5) + + expect(result.pass).toBe(true) + expect(receivedArray.length).toBe(5) }) - test('expected message', () => { - expect(getExpected(result.message())).toContain('5') + + test('does not update the received array when assertion fails', async () => { + const receivedArray = createMockElementArray(2) + + const result = await toBeElementsArrayOfSize.call({}, receivedArray, 10) + + expect(result.pass).toBe(false) + expect(receivedArray.length).toBe(2) }) - test('received message', () => { - expect(getReceived(result.message())).toContain('2') + + test('does not modify non-array received values', async () => { + const nonArrayEls = { + selector: 'parent', + length: 2, + parent: { + $: vi.fn(), + $$: vi.fn().mockReturnValue(createMockElementArray(5)), + }, + foundWith: '$$', + props: [], + } as unknown as WebdriverIO.ElementArray + + const result = await toBeElementsArrayOfSize.call({}, nonArrayEls, 5) + + expect(result.pass).toBe(true) + expect(nonArrayEls.length).toBe(2) }) - }) - }) - describe('error catching', () => { - test('throws error with incorrect size param', async () => { - await expect(toBeElementsArrayOfSize.call({}, els, '5' as any)).rejects.toThrow('Invalid params passed to toBeElementsArrayOfSize.') - }) + test('does not alter the array when checking', async () => { + const receivedArray = createMockElementArray(2) + const result = await toBeElementsArrayOfSize.call({}, receivedArray, 2) - test('works if size contains options', async () => { - const result = await toBeElementsArrayOfSize.call({}, els, { lte: 5 }) - expect(result.pass).toBe(true) + expect(result.pass).toBe(true) + expect(receivedArray.length).toBe(2) + }) }) }) - describe('number options', () => { - test.each([ - ['lte', 10, true], - ['lte', 1, false], - ['gte', 1, true], - ['gte', 10, false], - ['gte and lte', { gte: 1, lte: 10 }, true], - ['not gte but is lte', { gte: 10, lte: 10 }, false], - ['not lte but is gte', { gte: 1, lte: 1 }, false], - ])('should handle %s correctly', async (_, option, expected) => { - const result = await toBeElementsArrayOfSize.call({}, els, typeof option === 'object' ? option : { [_ as string]: option }) - expect(result.pass).toBe(expected) - }) - }) + describe('given an elements of type WebdriverIO.Element[]', () => { + describe('when elements is empty array', () => { + const elements: WebdriverIO.Element[] = [] + describe('success', () => { + test('array of size 0', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + const result = await toBeElementsArrayOfSize.call({}, elements, 0, { beforeAssertion, afterAssertion }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeElementsArrayOfSize', + expectedValue: 0, + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeElementsArrayOfSize', + expectedValue: 0, + options: { beforeAssertion, afterAssertion }, + result + }) + }) + }) - describe('array update', () => { - test('updates the received array when assertion passes', async () => { - const receivedArray = createMockElementArray(2); - (receivedArray.parent as any)._length = 5; - (receivedArray.parent as any).$$ = vi.fn().mockReturnValue(createMockElementArray(5)) + describe('failure', () => { + let result: any - const result = await toBeElementsArrayOfSize.call({}, receivedArray, 5) + beforeEach(async () => { + result = await toBeElementsArrayOfSize.call({}, elements, 5, {}) + }) - expect(result.pass).toBe(true) - expect(receivedArray.length).toBe(5) - }) + test('fails', () => { + expect(result.pass).toBe(false) + }) - test('does not update the received array when assertion fails', async () => { - const receivedArray = createMockElementArray(2) + describe('message shows correctly', () => { + test('expect message', () => { + expect(getExpectMessage(result.message())).toContain('to be elements array of size') + }) + test('expected message', () => { + expect(getExpected(result.message())).toContain('5') + }) + test('received message', () => { + expect(getReceived(result.message())).toContain('0') + }) + }) + }) + }) - const result = await toBeElementsArrayOfSize.call({}, receivedArray, 10) + describe('when elements is not empty array', () => { + const elements: WebdriverIO.Element[] = [{ + elementId: 'element-1' + } satisfies Partial as WebdriverIO.Element,] + describe('success', () => { + test('array of size 1', async () => { + const beforeAssertion = vi.fn() + const afterAssertion = vi.fn() + const result = await toBeElementsArrayOfSize.call({}, elements, 1, { beforeAssertion, afterAssertion }) + expect(result.pass).toBe(true) + expect(beforeAssertion).toBeCalledWith({ + matcherName: 'toBeElementsArrayOfSize', + expectedValue: 1, + options: { beforeAssertion, afterAssertion } + }) + expect(afterAssertion).toBeCalledWith({ + matcherName: 'toBeElementsArrayOfSize', + expectedValue: 1, + options: { beforeAssertion, afterAssertion }, + result + }) + }) + }) - expect(result.pass).toBe(false) - expect(receivedArray.length).toBe(2) - }) + describe('failure', () => { + let result: any - test('does not modify non-array received values', async () => { - const nonArrayEls = { - selector: 'parent', - length: 2, - parent: { - $: vi.fn(), - $$: vi.fn().mockReturnValue(createMockElementArray(5)), - }, - foundWith: '$$', - props: [], - } as unknown as WebdriverIO.ElementArray - - const result = await toBeElementsArrayOfSize.call({}, nonArrayEls, 5) - - expect(result.pass).toBe(true) - expect(nonArrayEls.length).toBe(2) - }) + beforeEach(async () => { + result = await toBeElementsArrayOfSize.call({}, elements, 5, {}) + }) - test('does not alter the array when checking', async () => { - const receivedArray = createMockElementArray(2) - const result = await toBeElementsArrayOfSize.call({}, receivedArray, 2) + test('fails', () => { + expect(result.pass).toBe(false) + }) - expect(result.pass).toBe(true) - expect(receivedArray.length).toBe(2) + describe('message shows correctly', () => { + test('expect message', () => { + expect(getExpectMessage(result.message())).toContain('to be elements array of size') + }) + test('expected message', () => { + expect(getExpected(result.message())).toContain('5') + }) + test('received message', () => { + expect(getReceived(result.message())).toContain('1') + }) + }) + }) }) }) }) diff --git a/test/util/refetchElements.test.ts b/test/util/refetchElements.test.ts index 6e82a8321..0f50e682b 100644 --- a/test/util/refetchElements.test.ts +++ b/test/util/refetchElements.test.ts @@ -3,24 +3,62 @@ import { $$ } from '@wdio/globals' import { refetchElements } from '../../src/util/refetchElements.js' -vi.mock('@wdio/globals') +const createMockElementArray = (length: number): WebdriverIO.ElementArray => { + const array = Array.from({ length }, () => ({})) + const mockArray = { + selector: 'parent', + get length() { return array.length }, + set length(newLength: number) { array.length = newLength }, + parent: { + $: vi.fn(), + $$: vi.fn().mockReturnValue(array), + }, + foundWith: '$$', + props: [], + [Symbol.iterator]: array[Symbol.iterator].bind(array), + filter: vi.fn().mockReturnThis(), + map: vi.fn().mockReturnThis(), + find: vi.fn().mockReturnThis(), + forEach: vi.fn(), + some: vi.fn(), + every: vi.fn(), + slice: vi.fn().mockReturnThis(), + toArray: vi.fn().mockReturnThis(), + } + return Object.assign(array, mockArray) as unknown as WebdriverIO.ElementArray +} + +vi.mock('@wdio/globals', () => ({ + $$: vi.fn().mockImplementation(() => createMockElementArray(5)) +})) describe('refetchElements', () => { - let els: WebdriverIO.ElementArray + describe('given WebdriverIO.ElementArray type', () => { + let elements: WebdriverIO.ElementArray - beforeEach(async () => { - els = (await $$('parent')) as unknown as WebdriverIO.ElementArray - // @ts-ignore - els.parent._length = 5 - }) + beforeEach(async () => { + elements = (await $$('parent')) as unknown as WebdriverIO.ElementArray + // @ts-ignore + elements.parent._length = 5 + }) - test('default', async () => { - const actual = await refetchElements(els, 5, true) - expect(actual.length).toBe(5) + test('default', async () => { + const actual = await refetchElements(elements, 5, true) + expect(actual.length).toBe(5) + }) + + test('wait is 0', async () => { + const actual = await refetchElements(elements, 0, true) + expect(actual).toEqual(elements) + }) }) - test('wait is 0', async () => { - const actual = await refetchElements(els, 0, true) - expect(actual).toEqual(els) + describe('given WebdriverIO.Element[] type', () => { + const elements: WebdriverIO.Element[] = [] as unknown as WebdriverIO.Element[] + + test('default', async () => { + const actual = await refetchElements(elements, 0) + expect(actual).toEqual([]) + }) }) }) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 429c12c32..15dabadcc 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -54,7 +54,7 @@ type WdioOnlyMaybePromiseLike = ElementPromise | ElementArrayPromise | Chainable */ type ElementOrArrayLike = ElementLike | ElementArrayLike type ElementLike = WebdriverIO.Element | ChainablePromiseElement -type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray +type ElementArrayLike = WebdriverIO.ElementArray | ChainablePromiseArray | WebdriverIO.Element[] type MockPromise = Promise /**