Skip to content

Commit fed785d

Browse files
committed
Ensure mocks used represented wdio framework reality
fix bad command Review `toHaveText` test case for current behaviour and inconsistencies Fix rebase Review toHaveText.test.ts Enrich toHaveText with more test cases Add command validating the build is under the right path Fix mock - Missing parent for $$ selector name in failure message - Better support of filtered ElementArray - `elementArray.props.length` is not a thing length is not directly on ElementArray Better factory + fix missing element's parent Final mock version with everything + toHaveText more tests
1 parent ce50d4f commit fed785d

File tree

8 files changed

+748
-306
lines changed

8 files changed

+748
-306
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,5 @@ jobs:
2121
node-version: ${{ matrix.node-version }}
2222
- name: Install Dependencies
2323
run: npm install --force
24-
- name: Build
25-
run: npm run build
26-
- name: Run Tests
27-
run: npm test
24+
- name: Run All Checks
25+
run: npm run checks:all

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
"build": "run-s clean compile",
5151
"clean": "run-p clean:*",
5252
"clean:build": "rimraf ./lib",
53-
"compile": "tsc --build tsconfig.build.json",
53+
"compile": "run-s compile:*",
54+
"compile:lib": "tsc --build tsconfig.build.json",
55+
"compile:check": "if [ ! -f lib/index.js ] || [ $(find lib -type f | wc -l) -le 30 ]; then echo 'File structure under lib is broken'; exit 1; fi",
5456
"tsc:root-types": "node types-checks-filter-out-node_modules.js",
5557
"test": "run-s test:*",
5658
"test:tsc": "tsc --project tsconfig.json --noEmit --rootDir .",

src/matchers/element/toHaveText.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async function condition(el: WebdriverIO.Element | WebdriverIO.ElementArray, tex
3838
}
3939

4040
export async function toHaveText(
41-
received: ChainablePromiseElement | ChainablePromiseArray,
41+
received: ChainablePromiseElement | ChainablePromiseArray | WebdriverIO.Element | WebdriverIO.ElementArray,
4242
expectedValue: string | RegExp | WdioAsymmetricMatcher<string> | Array<string | RegExp>,
4343
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
4444
) {

test/__mocks__/@wdio/globals.ts

Lines changed: 156 additions & 31 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'>
@@ -20,46 +20,172 @@ const getElementMethods = () => ({
2020
getHTML: vi.spyOn({ getHTML: async () => { return '<Html/>' } }, 'getHTML'),
2121
getComputedLabel: vi.spyOn({ getComputedLabel: async () => 'Computed Label' }, 'getComputedLabel'),
2222
getComputedRole: vi.spyOn({ getComputedRole: async () => 'Computed Role' }, 'getComputedRole'),
23+
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'),
2326
getSize: vi.spyOn({ getSize: async (prop?: 'width' | 'height') => {
2427
if (prop === 'width') { return 100 }
2528
if (prop === 'height') { return 50 }
2629
return { width: 100, height: 50 } satisfies Size
27-
} }, 'getSize') as unknown as WebdriverIO.Element['getSize'],
28-
getAttribute: vi.spyOn({ getAttribute: async (_attr: string) => 'some attribute' }, 'getAttribute'),
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

31-
function $(_selector: string) {
32-
const element = {
37+
export const elementFactory = (_selector: string, index?: number, parent: WebdriverIO.Browser | WebdriverIO.Element = browser): WebdriverIO.Element => {
38+
const partialElement = {
3339
selector: _selector,
3440
...getElementMethods(),
41+
index,
42+
$,
43+
$$,
44+
parent
45+
} satisfies Partial<WebdriverIO.Element>
46+
47+
const element = partialElement as unknown as WebdriverIO.Element
48+
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+
53+
return element
54+
}
55+
56+
export const notFoundElementFactory = (_selector: string, index?: number, parent: WebdriverIO.Browser | WebdriverIO.Element = browser): WebdriverIO.Element => {
57+
const partialElement = {
58+
selector: _selector,
59+
index,
3560
$,
36-
$$
37-
} satisfies Partial<WebdriverIO.Element> as unknown as WebdriverIO.Element
38-
element.getElement = async () => Promise.resolve(element)
39-
return element as unknown as ChainablePromiseElement
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
4091
}
4192

42-
function $$(selector: string) {
43-
const length = (this)?._length || 2
44-
const elements = Array(length).fill(null).map((_, index) => {
45-
const element = {
46-
selector,
47-
index,
48-
...getElementMethods(),
49-
$,
50-
$$
51-
} satisfies Partial<WebdriverIO.Element> as unknown as WebdriverIO.Element
52-
element.getElement = async () => Promise.resolve(element)
53-
return element
54-
}) satisfies WebdriverIO.Element[] as unknown as WebdriverIO.ElementArray
55-
56-
elements.foundWith = '$$'
57-
elements.props = []
58-
elements.props.length = length
59-
elements.selector = selector
60-
elements.getElements = async () => elements
61-
elements.length = length
62-
return elements as unknown as ChainablePromiseArray
93+
const $ = vi.fn((_selector: string) => {
94+
const element = elementFactory(_selector)
95+
96+
// Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
97+
const chainablePromiseElement = Promise.resolve(element) as unknown as ChainablePromiseElement
98+
99+
// Ensure `'getElement' in chainableElement` is false while allowing to use `await chainableElement.getElement()`
100+
const runtimeChainableElement = new Proxy(chainablePromiseElement, {
101+
get(target, prop) {
102+
if (prop in element) {
103+
return element[prop as keyof WebdriverIO.Element]
104+
}
105+
const value = Reflect.get(target, prop)
106+
return typeof value === 'function' ? value.bind(target) : value
107+
}
108+
})
109+
return runtimeChainableElement
110+
})
111+
112+
const $$ = vi.fn((selector: string) => {
113+
const length = (this as any)?._length || 2
114+
return chainableElementArrayFactory(selector, length)
115+
})
116+
117+
export function elementArrayFactory(selector: string, length?: number): WebdriverIO.ElementArray {
118+
const elements: WebdriverIO.Element[] = Array(length).fill(null).map((_, index) => elementFactory(selector, index))
119+
120+
const elementArray = elements as unknown as WebdriverIO.ElementArray
121+
122+
elementArray.foundWith = '$$'
123+
elementArray.props = []
124+
elementArray.selector = selector
125+
elementArray.getElements = vi.fn().mockResolvedValue(elementArray)
126+
elementArray.filter = async <T>(fn: (element: WebdriverIO.Element, index: number, array: T[]) => boolean | Promise<boolean>) => {
127+
const results = await Promise.all(elements.map((el, i) => fn(el, i, elements as unknown as T[])))
128+
return Array.prototype.filter.call(elements, (_, i) => results[i])
129+
}
130+
elementArray.parent = browser
131+
132+
// TODO Verify if we need to implement other array methods
133+
// [Symbol.iterator]: array[Symbol.iterator].bind(array)
134+
// filter: vi.fn().mockReturnThis(),
135+
// map: vi.fn().mockReturnThis(),
136+
// find: vi.fn().mockReturnThis(),
137+
// forEach: vi.fn(),
138+
// some: vi.fn(),
139+
// every: vi.fn(),
140+
// slice: vi.fn().mockReturnThis(),
141+
// toArray: vi.fn().mockReturnThis(),
142+
// getElements: vi.fn().mockResolvedValue(array)
143+
144+
return elementArray
145+
}
146+
147+
export function chainableElementArrayFactory(selector: string, length: number) {
148+
const elementArray = elementArrayFactory(selector, length)
149+
150+
// Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
151+
const chainablePromiseArray = Promise.resolve(elementArray) as unknown as ChainablePromiseArray
152+
153+
// Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
154+
const runtimeChainablePromiseArray = new Proxy(chainablePromiseArray, {
155+
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+
}
180+
if (elementArray && prop in elementArray) {
181+
return elementArray[prop as keyof WebdriverIO.ElementArray]
182+
}
183+
const value = Reflect.get(target, prop)
184+
return typeof value === 'function' ? value.bind(target) : value
185+
}
186+
})
187+
188+
return runtimeChainablePromiseArray
63189
}
64190

65191
export const browser = {
@@ -71,4 +197,3 @@ export const browser = {
71197
getTitle: vi.spyOn({ getTitle: async () => 'Example Domain' }, 'getTitle'),
72198
call(fn: Function) { return fn() },
73199
} satisfies Partial<WebdriverIO.Browser> as unknown as WebdriverIO.Browser
74-

0 commit comments

Comments
 (0)