Skip to content

Commit 8d487cb

Browse files
committed
Ensure mocks used represented wdio framework reality
1 parent c834809 commit 8d487cb

5 files changed

Lines changed: 162 additions & 45 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 check:all

test/__mocks__/@wdio/globals.ts

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,45 +20,76 @@ 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+
// Null is not part of the type, to fix in wdio one day
24+
getAttribute: vi.spyOn({ getAttribute: async (_attr: string) => null as unknown as string }, 'getAttribute'),
2325
getSize: vi.spyOn({ getSize: async (prop?: 'width' | 'height') => {
2426
if (prop === 'width') { return 100 }
2527
if (prop === 'height') { return 50 }
2628
return { width: 100, height: 50 } satisfies Size
2729
} }, 'getSize') as unknown as WebdriverIO.Element['getSize'],
2830
} satisfies Partial<WebdriverIO.Element>)
2931

30-
function $(_selector: string) {
31-
const element = {
32+
const elementFactory = (_selector: string, index?: number): WebdriverIO.Element => {
33+
const partialElement = {
3234
selector: _selector,
3335
...getElementMethods(),
36+
index,
3437
$,
3538
$$
36-
} satisfies Partial<WebdriverIO.Element> as unknown as WebdriverIO.Element
37-
element.getElement = async () => Promise.resolve(element)
38-
return element as unknown as ChainablePromiseElement
39+
} satisfies Partial<WebdriverIO.Element>
40+
41+
const element = partialElement as unknown as WebdriverIO.Element
42+
element.getElement = vi.fn().mockResolvedValue(element)
43+
return element
44+
}
45+
46+
function $(_selector: string) {
47+
const element = elementFactory(_selector)
48+
49+
// Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
50+
const chainablePromiseElement = Promise.resolve(element) as unknown as ChainablePromiseElement
51+
52+
// Ensure `'getElement' in chainableElement` is false while allowing to use `await chainableElement.getElement()`
53+
const runtimeChainableElement = new Proxy(chainablePromiseElement, {
54+
get(target, prop) {
55+
if (prop in element) {
56+
return element[prop as keyof WebdriverIO.Element]
57+
}
58+
const value = Reflect.get(target, prop)
59+
return typeof value === 'function' ? value.bind(target) : value
60+
}
61+
})
62+
return runtimeChainableElement
3963
}
4064

4165
function $$(selector: string) {
42-
const length = (this)?._length || 2
43-
const elements = Array(length).fill(null).map((_, index) => {
44-
const element = {
45-
selector,
46-
index,
47-
...getElementMethods(),
48-
$,
49-
$$
50-
} satisfies Partial<WebdriverIO.Element> as unknown as WebdriverIO.Element
51-
element.getElement = async () => Promise.resolve(element)
52-
return element
53-
}) satisfies WebdriverIO.Element[] as unknown as WebdriverIO.ElementArray
66+
const length = (this as any)?._length || 2
67+
const elements: WebdriverIO.Element[] = Array(length).fill(null).map((_, index) => elementFactory(selector, index))
68+
69+
const elementArray = elements as unknown as WebdriverIO.ElementArray
70+
71+
elementArray.foundWith = '$$'
72+
elementArray.props = []
73+
elementArray.props.length = length
74+
elementArray.selector = selector
75+
elementArray.getElements = async () => elementArray
76+
elementArray.length = length
77+
78+
// Wdio framework does return a Promise-wrapped element, so we need to mimic this behavior
79+
const chainablePromiseArray = Promise.resolve(elementArray) as unknown as ChainablePromiseArray
80+
81+
// Ensure `'getElements' in chainableElements` is false while allowing to use `await chainableElement.getElements()`
82+
const runtimeChainablePromiseArray = new Proxy(chainablePromiseArray, {
83+
get(target, prop) {
84+
if (elementArray && prop in elementArray) {
85+
return elementArray[prop as keyof WebdriverIO.ElementArray]
86+
}
87+
const value = Reflect.get(target, prop)
88+
return typeof value === 'function' ? value.bind(target) : value
89+
}
90+
})
5491

55-
elements.foundWith = '$$'
56-
elements.props = []
57-
elements.props.length = length
58-
elements.selector = selector
59-
elements.getElements = async () => elements
60-
elements.length = length
61-
return elements as unknown as ChainablePromiseArray
92+
return runtimeChainablePromiseArray
6293
}
6394

6495
export const browser = {

test/globals_mock.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { $, $$ } from '@wdio/globals'
3+
4+
vi.mock('@wdio/globals')
5+
6+
describe('globals mock', () => {
7+
describe($, () => {
8+
it('should return a ChainablePromiseElement', async () => {
9+
const el = $('foo')
10+
11+
// It behaves like a promise
12+
expect(el).toHaveProperty('then')
13+
// @ts-expect-error
14+
expect(typeof el.then).toBe('function')
15+
})
16+
17+
it('should resolve to an element', async () => {
18+
const el = await $('foo')
19+
20+
expect(el.selector).toBe('foo')
21+
// The resolved element should not be the proxy, but the underlying mock
22+
expect(el.getElement).toBeDefined()
23+
})
24+
25+
it('should allow calling getElement on the chainable promise', async () => {
26+
const chainable = $('foo')
27+
28+
// 'getElement' should not be present in the chainable object if checked via `in`
29+
// based on user request logs: 'getElements' in elements false
30+
expect('getElement' in chainable).toBe(false)
31+
32+
// But it should be callable
33+
const el = chainable.getElement()
34+
expect(el).toBeInstanceOf(Promise)
35+
36+
const awaitedEl = await el
37+
expect(awaitedEl.selector).toBe('foo')
38+
expect(awaitedEl.getElement).toBeDefined()
39+
})
40+
41+
it('should allow calling methods like isEnabled on the chainable promise', async () => {
42+
const check = $('foo').isEnabled()
43+
expect(check).toBeInstanceOf(Promise)
44+
45+
const result = await check
46+
expect(result).toBe(true)
47+
})
48+
49+
it('should allow chaining simple methods', async () => {
50+
const text = await $('foo').getText()
51+
52+
expect(text).toBe(' Valid Text ')
53+
})
54+
})
55+
56+
describe('$$', () => {
57+
it('should return a ChainablePromiseArray', async () => {
58+
const els = $$('foo')
59+
expect(els).toHaveProperty('then')
60+
// @ts-expect-error
61+
expect(typeof els.then).toBe('function')
62+
})
63+
64+
it('should resolve to an element array', async () => {
65+
const els = await $$('foo')
66+
expect(Array.isArray(els)).toBe(true)
67+
expect(els).toHaveLength(2) // Default length in mock
68+
expect(els.selector).toBe('foo')
69+
})
70+
71+
it('should allow calling getElements on the chainable promise', async () => {
72+
const chainable = $$('foo')
73+
// 'getElements' should not be present in the chainable object if checked via `in`
74+
expect('getElements' in chainable).toBe(false)
75+
76+
// But it should be callable
77+
const els = await chainable.getElements()
78+
expect(els).toHaveLength(2) // Default length
79+
})
80+
81+
it('should allow iterating if awaited', async () => {
82+
const els = await $$('foo')
83+
// map is available on the resolved array
84+
const selectors = els.map(el => el.selector)
85+
expect(selectors).toEqual(['foo', 'foo'])
86+
})
87+
})
88+
})

test/softAssertions.test.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ describe('Soft Assertions', () => {
1010

1111
beforeEach(async () => {
1212
el = $('sel')
13+
1314
// We need to mock getText() which is what the toHaveText matcher actually calls
14-
el.getText = vi.fn().mockImplementation(() => 'Actual Text')
15+
vi.mocked(el.getText).mockResolvedValue('Actual Text')
16+
1517
// Clear any soft assertion failures before each test
1618
expectWdio.clearSoftFailures()
1719
})
@@ -157,11 +159,13 @@ describe('Soft Assertions', () => {
157159
describe('Different Matcher Types', () => {
158160
beforeEach(async () => {
159161
el = $('sel')
162+
160163
// Mock different methods for different matchers
161-
el.getText = vi.fn().mockImplementation(() => 'Actual Text')
162-
el.isDisplayed = vi.fn().mockImplementation(() => false)
163-
el.getAttribute = vi.fn().mockImplementation(() => 'actual-class')
164-
el.isClickable = vi.fn().mockImplementation(() => false)
164+
vi.mocked(el.getText).mockResolvedValue('Actual Text')
165+
vi.mocked(el.isDisplayed).mockResolvedValue(false)
166+
vi.mocked(el.getAttribute).mockResolvedValue('actual-class')
167+
vi.mocked(el.isClickable).mockResolvedValue(false)
168+
165169
expectWdio.clearSoftFailures()
166170
})
167171

@@ -239,9 +243,9 @@ describe('Soft Assertions', () => {
239243
const softService = SoftAssertService.getInstance()
240244
softService.setCurrentTest('concurrent-test', 'concurrent', 'test file')
241245

242-
el.getText = vi.fn().mockImplementation(() => 'Actual Text')
243-
el.isDisplayed = vi.fn().mockImplementation(() => false)
244-
el.isClickable = vi.fn().mockImplementation(() => false)
246+
vi.mocked(el.getText).mockResolvedValue('Actual Text')
247+
vi.mocked(el.isDisplayed).mockResolvedValue(false)
248+
vi.mocked(el.isClickable).mockResolvedValue(false)
245249

246250
// Fire multiple assertions rapidly
247251
const promises = [
@@ -270,20 +274,14 @@ describe('Soft Assertions', () => {
270274
softService.setCurrentTest('error-test', 'error test', 'test file')
271275

272276
// Mock a matcher that throws a unique error
273-
const originalMethod = el.getText
274-
el.getText = vi.fn().mockImplementation(() => {
275-
throw new TypeError('Weird browser error')
276-
})
277+
vi.mocked(el.getText).mockRejectedValue(new TypeError('Weird browser error'))
277278

278279
await expectWdio.soft(el).toHaveText('Expected Text')
279280

280281
const failures = expectWdio.getSoftFailures()
281282
expect(failures.length).toBe(1)
282283
expect(failures[0].error).toBeInstanceOf(Error)
283284
expect(failures[0].error.message).toContain('Weird browser error')
284-
285-
// Restore
286-
el.getText = originalMethod
287285
})
288286

289287
it('should handle very long error messages', async () => {
@@ -304,7 +302,7 @@ describe('Soft Assertions', () => {
304302

305303
// Test with null/undefined values
306304
await expectWdio.soft(el).toHaveText(null as any)
307-
await expectWdio.soft(el).toHaveAttribute('class', undefined as any)
305+
await expectWdio.soft(el).toHaveAttribute('class')
308306

309307
const failures = expectWdio.getSoftFailures()
310308
expect(failures.length).toBe(2)

vitest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export default defineConfig({
88
'**/node_modules/**'
99
],
1010
testTimeout: 15 * 1000,
11+
clearMocks: true, // clears all mock call histories before each test
12+
restoreMocks: true, // restores the original implementation of spies
1113
coverage: {
1214
enabled: true,
1315
exclude: [
@@ -29,7 +31,7 @@ export default defineConfig({
2931
lines: 88.4,
3032
functions: 86.9,
3133
statements: 88.3,
32-
branches: 79.4,
34+
branches: 79.6,
3335
}
3436
}
3537
}

0 commit comments

Comments
 (0)