Skip to content

Commit cd3bd6c

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/picomatch-2.3.2
2 parents e0a6773 + 733c86d commit cd3bd6c

14 files changed

Lines changed: 1708 additions & 1448 deletions

File tree

.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-lock.json

Lines changed: 937 additions & 1105 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
"build": "run-s clean compile",
5656
"clean": "run-p clean:*",
5757
"clean:build": "rimraf ./lib",
58-
"compile": "tsc --build tsconfig.build.json",
58+
"compile": "run-s compile:*",
59+
"compile:lib": "tsc --build tsconfig.build.json",
60+
"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",
5961
"tsc:root-types": "node types-checks-filter-out-node_modules.js",
6062
"test": "run-s test:*",
6163
"test:tsc": "tsc --project tsconfig.json --noEmit --rootDir .",

playgrounds/jasmine/package-lock.json

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playgrounds/jest/package-lock.json

Lines changed: 14 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playgrounds/mocha/package-lock.json

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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-types/mocha/types-mocha.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ describe('type assertions', () => {
8383
})
8484
})
8585

86+
describe('toHaveHTML', () => {
87+
it('should allow using HTMLOptions', async () => {
88+
expectPromiseVoid = expect(element).toHaveHTML('html', { includeSelectorTag: true })
89+
})
90+
})
91+
8692
describe('element', () => {
8793

8894
describe('toBeDisabled', () => {

test/__mocks__/@wdio/globals.ts

Lines changed: 135 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,151 @@ 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,
3542
$,
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
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
4054
}
4155

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
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+
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+
return elementArray
133+
}
134+
135+
export function chainableElementArrayFactory(selector: string, length: number) {
136+
const elementArray = elementArrayFactory(selector, length)
137+
138+
// Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
139+
const chainablePromiseArray = Promise.resolve(elementArray) as unknown as ChainablePromiseArray
140+
141+
// Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
142+
const runtimeChainablePromiseArray = new Proxy(chainablePromiseArray, {
143+
get(target, prop) {
144+
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
145+
// Simulate index out of bounds error when asking for an element outside the array length
146+
const index = parseInt(prop, 10)
147+
if (index >= length) {
148+
const error = new Error(`Index out of bounds! $$(${selector}) returned only ${length} elements.`)
149+
return new Proxy(Promise.resolve(), {
150+
get(target, prop) {
151+
if (prop === 'then') {
152+
return (resolve: any, reject: any) => reject(error)
153+
}
154+
return () => Promise.reject(error)
155+
}
156+
})
157+
}
158+
}
159+
if (elementArray && prop in elementArray) {
160+
return elementArray[prop as keyof WebdriverIO.ElementArray]
161+
}
162+
const value = Reflect.get(target, prop)
163+
return typeof value === 'function' ? value.bind(target) : value
164+
}
165+
})
166+
167+
return runtimeChainablePromiseArray
63168
}
64169

65170
export const browser = {
@@ -71,4 +176,3 @@ export const browser = {
71176
getTitle: vi.spyOn({ getTitle: async () => 'Example Domain' }, 'getTitle'),
72177
call(fn: Function) { return fn() },
73178
} satisfies Partial<WebdriverIO.Browser> as unknown as WebdriverIO.Browser
74-

0 commit comments

Comments
 (0)