Skip to content

Commit d4b15ad

Browse files
committed
Ensure mocks used represented wdio framework reality
1 parent 2931c34 commit d4b15ad

5 files changed

Lines changed: 164 additions & 47 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,6 +20,8 @@ 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 }
@@ -28,38 +30,67 @@ const getElementMethods = () => ({
2830
getAttribute: vi.spyOn({ getAttribute: async (_attr: string) => 'some attribute' }, 'getAttribute'),
2931
} satisfies Partial<WebdriverIO.Element>)
3032

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

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

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+
return runtimeChainablePromiseArray
6394
}
6495

6596
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: 13 additions & 15 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

@@ -245,9 +249,9 @@ describe('Soft Assertions', () => {
245249
const softService = SoftAssertService.getInstance()
246250
softService.setCurrentTest('concurrent-test', 'concurrent', 'test file')
247251

248-
el.getText = vi.fn().mockImplementation(() => 'Actual Text')
249-
el.isDisplayed = vi.fn().mockImplementation(() => false)
250-
el.isClickable = vi.fn().mockImplementation(() => false)
252+
vi.mocked(el.getText).mockResolvedValue('Actual Text')
253+
vi.mocked(el.isDisplayed).mockResolvedValue(false)
254+
vi.mocked(el.isClickable).mockResolvedValue(false)
251255

252256
// Fire multiple assertions rapidly
253257
const promises = [
@@ -276,20 +280,14 @@ describe('Soft Assertions', () => {
276280
softService.setCurrentTest('error-test', 'error test', 'test file')
277281

278282
// Mock a matcher that throws a unique error
279-
const originalMethod = el.getText
280-
el.getText = vi.fn().mockImplementation(() => {
281-
throw new TypeError('Weird browser error')
282-
})
283+
vi.mocked(el.getText).mockRejectedValue(new TypeError('Weird browser error'))
283284

284285
await expectWdio.soft(el).toHaveText('Expected Text')
285286

286287
const failures = expectWdio.getSoftFailures()
287288
expect(failures.length).toBe(1)
288289
expect(failures[0].error).toBeInstanceOf(Error)
289290
expect(failures[0].error.message).toContain('Weird browser error')
290-
291-
// Restore
292-
el.getText = originalMethod
293291
})
294292

295293
it('should handle very long error messages', async () => {

vitest.config.ts

Lines changed: 6 additions & 4 deletions
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: [
@@ -26,10 +28,10 @@ export default defineConfig({
2628
'types-checks-filter-out-node_modules.js',
2729
],
2830
thresholds: {
29-
lines: 88,
30-
functions: 86,
31-
statements: 88,
32-
branches: 79,
31+
lines: 88.4,
32+
functions: 86.9,
33+
statements: 88.3,
34+
branches: 79.6,
3335
}
3436
}
3537
}

0 commit comments

Comments
 (0)