Skip to content

Commit 5151227

Browse files
committed
Final mock version with everything + toHaveText more tests
1 parent 2c0769d commit 5151227

3 files changed

Lines changed: 171 additions & 4 deletions

File tree

test/__mocks__/@wdio/globals.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* This file exist for better typed mock implementation, so that we can follow wdio/globals API updates more easily.
44
*/
55
import { vi } from 'vitest'
6-
import type { ChainablePromiseArray, ChainablePromiseElement } from 'webdriverio'
6+
import type { ChainablePromiseArray, ChainablePromiseElement, ParsedCSSValue } from 'webdriverio'
77

88
import type { RectReturn } from '@wdio/protocols'
99
export type Size = Pick<RectReturn, 'width' | 'height'>
@@ -21,11 +21,17 @@ const getElementMethods = () => ({
2121
getComputedLabel: vi.spyOn({ getComputedLabel: async () => 'Computed Label' }, 'getComputedLabel'),
2222
getComputedRole: vi.spyOn({ getComputedRole: async () => 'Computed Role' }, 'getComputedRole'),
2323
getAttribute: vi.spyOn({ getAttribute: async (_attr: string) => 'some attribute' }, 'getAttribute'),
24+
getCSSProperty: vi.spyOn({ getCSSProperty: async (_prop: string, _pseudo?: string) =>
25+
({ value: 'colorValue', parsed: {} } satisfies ParsedCSSValue) }, 'getCSSProperty'),
2426
getSize: vi.spyOn({ getSize: async (prop?: 'width' | 'height') => {
2527
if (prop === 'width') { return 100 }
2628
if (prop === 'height') { return 50 }
2729
return { width: 100, height: 50 } satisfies Size
28-
} }, 'getSize') as unknown as WebdriverIO.Element['getSize'],
30+
} },
31+
// Force wrong size & number typing, fixed by https://github.com/webdriverio/webdriverio/pull/15003
32+
'getSize') as unknown as WebdriverIO.Element['getSize'],
33+
$,
34+
$$,
2935
} satisfies Partial<WebdriverIO.Element>)
3036

3137
export const elementFactory = (_selector: string, index?: number, parent: WebdriverIO.Browser | WebdriverIO.Element = browser): WebdriverIO.Element => {
@@ -40,9 +46,50 @@ export const elementFactory = (_selector: string, index?: number, parent: Webdri
4046

4147
const element = partialElement as unknown as WebdriverIO.Element
4248
element.getElement = vi.fn().mockResolvedValue(element)
49+
50+
// Note: an element found has element.elementId while a not found has element.error
51+
element.elementId = `${_selector}${index ? '-' + index : ''}`
52+
4353
return element
4454
}
4555

56+
export const notFoundElementFactory = (_selector: string, index?: number, parent: WebdriverIO.Browser | WebdriverIO.Element = browser): WebdriverIO.Element => {
57+
const partialElement = {
58+
selector: _selector,
59+
index,
60+
$,
61+
$$,
62+
isExisting: vi.fn().mockResolvedValue(false),
63+
parent
64+
} satisfies Partial<WebdriverIO.Element>
65+
66+
const element = partialElement as unknown as WebdriverIO.Element
67+
68+
// Note: an element found has element.elementId while a not found has element.error
69+
const elementId = `${_selector}${index ? '-' + index : ''}`
70+
const error = (functionName: string) => new Error(`Can't call ${functionName} on element with selector ${elementId} because element wasn't found`)
71+
72+
// Mimic element not found by throwing error on any method call beisde isExisting
73+
const notFoundElement = new Proxy(element, {
74+
get(target, prop) {
75+
if (prop in element) {
76+
const value = element[prop as keyof WebdriverIO.Element]
77+
return value
78+
}
79+
if (['then', 'catch', 'toStringTag'].includes(prop as string) || typeof prop === 'symbol') {
80+
const value = Reflect.get(target, prop)
81+
return typeof value === 'function' ? value.bind(target) : value
82+
}
83+
element.error = error(prop as string)
84+
return () => { throw element.error }
85+
}
86+
})
87+
88+
element.getElement = vi.fn().mockResolvedValue(notFoundElement)
89+
90+
return notFoundElement
91+
}
92+
4693
const $ = vi.fn((_selector: string) => {
4794
const element = elementFactory(_selector)
4895

@@ -106,6 +153,30 @@ export function chainableElementArrayFactory(selector: string, length: number) {
106153
// Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
107154
const runtimeChainablePromiseArray = new Proxy(chainablePromiseArray, {
108155
get(target, prop) {
156+
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
157+
const index = parseInt(prop, 10)
158+
if (index >= length) {
159+
const error = new Error(`Index out of bounds! $$(${selector}) returned only ${length} elements.`)
160+
return new Proxy(Promise.resolve(), {
161+
get(target, prop) {
162+
if (prop === 'then') {
163+
return (resolve: any, reject: any) => reject(error)
164+
}
165+
// Allow resolving methods like 'catch', 'finally' normally from the promise if needed,
166+
// but usually we want any interaction to fail?
167+
// Actually, standard promise methods might be accessed.
168+
// But the user requirements says: `$$('foo')[3].getText()` should return a promise (that rejects).
169+
170+
// If accessing a property that exists on Promise (like catch, finally, Symbol.toStringTag), maybe we should be careful.
171+
// However, the test expects `el` (the proxy) to be a Promise instance.
172+
// And `el.getText()` to return a promise.
173+
174+
// If I return a function that returns a rejected promise for everything else:
175+
return () => Promise.reject(error)
176+
}
177+
})
178+
}
179+
}
109180
if (elementArray && prop in elementArray) {
110181
return elementArray[prop as keyof WebdriverIO.ElementArray]
111182
}
@@ -126,4 +197,3 @@ export const browser = {
126197
getTitle: vi.spyOn({ getTitle: async () => 'Example Domain' }, 'getTitle'),
127198
call(fn: Function) { return fn() },
128199
} satisfies Partial<WebdriverIO.Browser> as unknown as WebdriverIO.Browser
129-

test/globals_mock.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect, vi } from 'vitest'
22
import { $, $$ } from '@wdio/globals'
3+
import { notFoundElementFactory } from './__mocks__/@wdio/globals.js'
34

45
vi.mock('@wdio/globals')
56

@@ -92,7 +93,6 @@ describe('globals mock', () => {
9293

9394
it('should allow iterating if awaited', async () => {
9495
const els = await $$('foo')
95-
9696
// map is available on the resolved array
9797
const selectors = els.map(el => el.selector)
9898
expect(selectors).toEqual(['foo', 'foo'])
@@ -111,5 +111,57 @@ describe('globals mock', () => {
111111

112112
expect(text).toBe(' Valid Text ')
113113
})
114+
115+
it('should returns ElementArray on getElements', async () => {
116+
const els = await $$('foo')
117+
118+
expect(await els.getElements()).toEqual(els)
119+
})
120+
121+
it('should return a promise-like object when accessing index out of bounds', () => {
122+
const el = $$('foo')[3]
123+
// It shouldn't throw synchronously
124+
expect(el).toBeDefined()
125+
expect(el).toBeInstanceOf(Promise)
126+
127+
// Methods should return a Promise
128+
const getEl = el.getElement()
129+
expect(getEl).toBeInstanceOf(Promise)
130+
// catch unhandled rejection to avoid warnings
131+
getEl.catch(() => {})
132+
133+
const getText = el.getText()
134+
expect(getText).toBeInstanceOf(Promise)
135+
// catch unhandled rejection to avoid warnings
136+
getText.catch(() => {})
137+
})
138+
139+
it('should throw "Index out of bounds" when awaiting index out of bounds', async () => {
140+
await expect(async () => await $$('foo')[3]).rejects.toThrow('Index out of bounds! $$(foo) returned only 2 elements.')
141+
await expect(async () => await $$('foo')[3].getElement()).rejects.toThrow('Index out of bounds! $$(foo) returned only 2 elements.')
142+
await expect(async () => await $$('foo')[3].getText()).rejects.toThrow('Index out of bounds! $$(foo) returned only 2 elements.')
143+
})
144+
})
145+
146+
describe('notFoundElementFactory', () => {
147+
it('should return false for isExisting', async () => {
148+
const el = notFoundElementFactory('not-found')
149+
expect(await el.isExisting()).toBe(false)
150+
})
151+
152+
it('should resolve to itself when calling getElement', async () => {
153+
const el = notFoundElementFactory('not-found')
154+
expect(await el.getElement()).toBe(el)
155+
})
156+
157+
it('should throw error on method calls', async () => {
158+
const el = notFoundElementFactory('not-found')
159+
expect(() => el.click()).toThrow("Can't call click on element with selector not-found because element wasn't found")
160+
})
161+
162+
it('should throw error when awaiting a method call (sync throw)', async () => {
163+
const el = notFoundElementFactory('not-found')
164+
expect(() => el.getText()).toThrow("Can't call getText on element with selector not-found because element wasn't found")
165+
})
114166
})
115167
})

test/matchers/element/toHaveText.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { $, $$ } from '@wdio/globals'
22
import { beforeEach, describe, expect, test, vi } from 'vitest'
33
import { toHaveText } from '../../../src/matchers/element/toHaveText.js'
44
import type { ChainablePromiseArray } from 'webdriverio'
5+
import { notFoundElementFactory } from '../../__mocks__/@wdio/globals.js'
56

67
vi.mock('@wdio/globals')
78

@@ -484,4 +485,48 @@ Expect ${selectorName} to have text
484485
})
485486
})
486487
})
488+
489+
describe('Edge cases', () => {
490+
test('should have pass false with proper error message when actual is an empty array of elements', async () => {
491+
// @ts-ignore
492+
const result = await thisContext.toHaveText([], 'webdriverio')
493+
494+
expect(result.pass).toBe(true)
495+
})
496+
497+
// TODO view later to handle this case more gracefully
498+
test('given element is not found then it throws error when an element does not exists', async () => {
499+
const element: WebdriverIO.Element = notFoundElementFactory('sel')
500+
501+
await expect(thisContext.toHaveText(element, 'webdriverio')).rejects.toThrow("Can't call getText on element with selector sel because element wasn't found")
502+
})
503+
504+
// TODO view later to handle this case more gracefully
505+
test('given element from out of bound ChainableArray, then it throws error when an element does not exists', async () => {
506+
const element: ChainablePromiseElement = $$('elements')[3]
507+
508+
await expect(thisContext.toHaveText(element, 'webdriverio')).rejects.toThrow('Index out of bounds! $$(elements) returned only 2 elements.')
509+
})
510+
511+
// Throws with wierd and differrent error message!
512+
test.skip.for([
513+
{ actual: undefined, selectorName: 'undefined' },
514+
{ actual: null, selectorName: 'null' },
515+
{ actual: true, selectorName: 'true' },
516+
{ actual: 5, selectorName: '5' },
517+
{ actual: 'test', selectorName: 'test' },
518+
{ actual: Promise.resolve(true), selectorName: 'true' },
519+
{ actual: {}, selectorName: '{}' },
520+
{ actual: ['1', '2'], selectorName: '["1","2"]' },
521+
])('should have pass false with proper error message when actual is unsupported type of $actual', async ({ actual, selectorName }) => {
522+
const result = await thisContext.toHaveText(actual as any, 'webdriverio')
523+
524+
expect(result.pass).toBe(false)
525+
expect(result.message()).toEqual(`\
526+
Expect ${selectorName} to have text
527+
528+
Expected: "webdriverio"
529+
Received: undefined`)
530+
})
531+
})
487532
})

0 commit comments

Comments
 (0)