Skip to content

Commit a57306c

Browse files
committed
Make SoftAssertService compatible with basic non-promises matchers
- fix `.not` not working for basic matchers - Use soft for all `to` matchers - Use a non async version for basic matchers
1 parent fed785d commit a57306c

File tree

3 files changed

+208
-43
lines changed

3 files changed

+208
-43
lines changed

src/softExpect.ts

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { expect, matchers } from './index.js'
1+
import { expect } from './index.js'
22
import { SoftAssertService } from './softAssert.js'
3+
import type { SyncExpectationResult } from 'expect'
4+
5+
const isPossibleMatcher = (propName: string) => propName.startsWith('to') && propName.length > 2
36

47
/**
58
* Creates a soft assertion wrapper using lazy evaluation
69
* Only creates matchers when they're actually accessed
710
*/
8-
const createSoftExpect = <T = unknown>(actual: T): ExpectWebdriverIO.Matchers<Promise<void>, T> => {
11+
const createSoftExpect = <T = unknown>(actual: T): ExpectWebdriverIO.Matchers<Promise<void> | void, T> => {
912
const softService = SoftAssertService.getInstance()
1013

1114
// Use a simple proxy that creates matchers on-demand
12-
return new Proxy({} as ExpectWebdriverIO.Matchers<Promise<void>, T>, {
15+
return new Proxy({} as ExpectWebdriverIO.Matchers<Promise<void> | void, T>, {
1316
get(target, prop) {
1417
const propName = String(prop)
1518

@@ -23,12 +26,10 @@ const createSoftExpect = <T = unknown>(actual: T): ExpectWebdriverIO.Matchers<Pr
2326
return createSoftChainProxy(actual, propName, softService)
2427
}
2528

26-
// Handle matchers
27-
if (matchers.has(propName)) {
29+
if (isPossibleMatcher(propName)) {
30+
// Support basic & wdio (and more) matchers that start with "to"
2831
return createSoftMatcher(actual, propName, softService)
2932
}
30-
31-
// For any other properties, return undefined
3233
return undefined
3334
}
3435
})
@@ -38,13 +39,10 @@ const createSoftExpect = <T = unknown>(actual: T): ExpectWebdriverIO.Matchers<Pr
3839
* Creates a soft .not proxy
3940
*/
4041
const createSoftNotProxy = <T>(actual: T, softService: SoftAssertService) => {
41-
return new Proxy({} as ExpectWebdriverIO.Matchers<Promise<void>, T>, {
42-
get(target, prop) {
42+
return new Proxy({} as ExpectWebdriverIO.Matchers<Promise<void> | void, T>, {
43+
get(_target, prop) {
4344
const propName = String(prop)
44-
if (matchers.has(propName)) {
45-
return createSoftMatcher(actual, propName, softService, 'not')
46-
}
47-
return undefined
45+
return isPossibleMatcher(propName) ? createSoftMatcher(actual, propName, softService, 'not') : undefined
4846
}
4947
})
5048
}
@@ -53,13 +51,10 @@ const createSoftNotProxy = <T>(actual: T, softService: SoftAssertService) => {
5351
* Creates a soft chain proxy (resolves/rejects)
5452
*/
5553
const createSoftChainProxy = <T>(actual: T, chainType: string, softService: SoftAssertService) => {
56-
return new Proxy({} as ExpectWebdriverIO.Matchers<Promise<void>, T>, {
57-
get(target, prop) {
54+
return new Proxy({} as ExpectWebdriverIO.Matchers<Promise<void> | void, T>, {
55+
get(_target, prop) {
5856
const propName = String(prop)
59-
if (matchers.has(propName)) {
60-
return createSoftMatcher(actual, propName, softService, chainType)
61-
}
62-
return undefined
57+
return isPossibleMatcher(propName) ? createSoftMatcher(actual, propName, softService, chainType) : undefined
6358
}
6459
})
6560
}
@@ -73,7 +68,7 @@ const createSoftMatcher = <T>(
7368
softService: SoftAssertService,
7469
prefix?: string
7570
) => {
76-
return async (...args: unknown[]) => {
71+
return (...args: unknown[]) => {
7772
try {
7873
// Build the expectation chain
7974
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -87,20 +82,31 @@ const createSoftMatcher = <T>(
8782
expectChain = expectChain.rejects
8883
}
8984

90-
return await ((expectChain as unknown) as Record<string, (...args: unknown[]) => ExpectWebdriverIO.AsyncAssertionResult>)[matcherName](...args)
85+
// In case of matchers failures we jump into the catch block below
86+
const assertionResult: ExpectWebdriverIO.AsyncAssertionResult | SyncExpectationResult = expectChain[matcherName](...args)
9187

92-
} catch (error) {
93-
// Record the failure
94-
const fullMatcherName = prefix ? `${prefix}.${matcherName}` : matcherName
95-
softService.addFailure(error as Error, fullMatcherName)
96-
97-
// Return a passing result to continue execution
98-
return {
99-
pass: true,
100-
message: () => `Soft assertion failed: ${fullMatcherName}`
88+
// Handle async matchers, and allow to not be a promise for basic non-async matchers
89+
if ( assertionResult instanceof Promise) {
90+
return assertionResult.catch((error: Error) => handlingMatcherFailure(prefix, matcherName, softService, error))
10191
}
92+
return assertionResult
93+
94+
} catch (error) {
95+
return handlingMatcherFailure(prefix, matcherName, softService, error as Error)
10296
}
10397
}
10498
}
10599

100+
function handlingMatcherFailure(prefix: string | undefined, matcherName: string, softService: SoftAssertService, error: unknown) {
101+
// Record the failure
102+
const fullMatcherName = prefix ? `${prefix}.${matcherName}` : matcherName
103+
softService.addFailure(error as Error, fullMatcherName)
104+
105+
// Return a passing result to continue execution
106+
return {
107+
pass: true,
108+
message: () => `Soft assertion failed: ${fullMatcherName}`
109+
}
110+
}
111+
106112
export default createSoftExpect

test/softAssertions.test.ts

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect, beforeEach, vi } from 'vitest'
2-
import { $ } from '@wdio/globals'
2+
import { $, $$ } from '@wdio/globals'
3+
import expectLib from 'expect'
34
import { expect as expectWdio, SoftAssertionService, SoftAssertService } from '../src/index.js'
45

56
vi.mock('@wdio/globals')
@@ -19,6 +20,77 @@ describe('Soft Assertions', () => {
1920
})
2021

2122
describe('expect.soft', () => {
23+
24+
it('should handle promises properly and return a promise when matchers are used with Promises or Elements', async () => {
25+
const softService = SoftAssertService.getInstance()
26+
softService.setCurrentTest('promise-0', 'test name', 'test file')
27+
28+
expect(expectLib(Promise.resolve(true)).resolves.toBe(true)).toBeInstanceOf(Promise)
29+
expect(expectWdio(Promise.resolve(true)).resolves.toBe(true)).toBeInstanceOf(Promise)
30+
expect(expectWdio(Promise.resolve(true)).resolves.not.toBe(false)).toBeInstanceOf(Promise)
31+
expect(expectWdio.soft(Promise.resolve(true)).resolves.toBe(true)).toBeInstanceOf(Promise)
32+
33+
const elementToHaveText = expectWdio($('element1')).toHaveText('Valid Text')
34+
expect(elementToHaveText).toBeInstanceOf(Promise)
35+
36+
// TODO remove await once $$() support is merged
37+
const elementsToHaveText = expectWdio(await $$('elements2')).toHaveText('Valid Text')
38+
expect(elementsToHaveText).toBeInstanceOf(Promise)
39+
40+
const elementsNotToHaveText = expectWdio(await $$('elements3')).not.toHaveText('Not Valid Text')
41+
expect(elementsNotToHaveText).toBeInstanceOf(Promise)
42+
43+
await Promise.all([elementToHaveText, elementsToHaveText, elementsNotToHaveText])
44+
45+
const elementSoftToHaveText = expectWdio.soft($('element4')).toHaveText('Valid Text')
46+
expect(elementSoftToHaveText).toBeInstanceOf(Promise)
47+
48+
const elementsSoftToHaveText = expectWdio.soft(await $$('elements5')).toHaveText('Valid Text')
49+
expect(elementsSoftToHaveText).toBeInstanceOf(Promise)
50+
51+
const elementsSoftNotToHaveText = expectWdio.soft(await $$('elements6')).not.toHaveText('Not Valid Text')
52+
expect(elementsSoftNotToHaveText).toBeInstanceOf(Promise)
53+
54+
// Ensure all assertions are awaited to avoid conflicts in other tests
55+
await Promise.all([elementSoftToHaveText, elementsSoftToHaveText, elementsSoftNotToHaveText])
56+
})
57+
58+
it('should handle non-promises properly', () => {
59+
const softService = SoftAssertService.getInstance()
60+
softService.setCurrentTest('non-promise-1', 'test name', 'test file')
61+
62+
expect(expectLib(true).toBe(true)).toBeUndefined()
63+
expect(expectLib(true).toBe).toBeInstanceOf(Function)
64+
expect(expectLib(true).toBe(true)).not.toBeInstanceOf(Promise)
65+
expect(expectLib(true).not.toBe(false)).not.toBeInstanceOf(Promise)
66+
67+
expect(expectWdio(true).toBe(true)).toBeUndefined()
68+
expect(expectWdio(true).toBe).toBeInstanceOf(Function)
69+
expect(expectWdio(true).toBe(true)).not.toBeInstanceOf(Promise)
70+
expect(expectWdio(true).not.toBe(false)).not.toBeInstanceOf(Promise)
71+
72+
expect(expectWdio.soft(true).toBe(true)).toBeUndefined()
73+
expect(expectWdio.soft(true).toBe).toBeInstanceOf(Function)
74+
expect(expectWdio.soft(true).toBe).not.toBeInstanceOf(Promise)
75+
expect(expectWdio.soft(true).not.toBe(false)).not.toBeInstanceOf(Promise)
76+
})
77+
78+
it.for([
79+
'',
80+
2,
81+
[],
82+
])('should handle non-promises and return a non-promise target to have the correct runtime type', (actualPromise) => {
83+
const softService = SoftAssertService.getInstance()
84+
softService.setCurrentTest('non-promise-2', 'test name', 'test file')
85+
86+
const wdioExpect = expectWdio(actualPromise)
87+
expect(wdioExpect).not.toBeInstanceOf(Promise)
88+
89+
const softExpect = expectWdio.soft(actualPromise)
90+
expect(softExpect).toBeInstanceOf(Object)
91+
expect(softExpect).not.toBeInstanceOf(Promise)
92+
})
93+
2294
it('should not throw immediately on failure', async () => {
2395
const softService = SoftAssertService.getInstance()
2496
softService.setCurrentTest('test-1', 'test name', 'test file')
@@ -108,20 +180,48 @@ describe('Soft Assertions', () => {
108180
expect(expectWdio.getSoftFailures().length).toBe(0)
109181
})
110182

111-
/**
112-
* TODO: Skipped since soft assertions are currently not supporting basic matchers like toBe or toEqual. To fix one day!
113-
* @see https://github.com/webdriverio/expect-webdriverio/issues/1887
114-
*/
115-
it.skip('should support basic text matching', async () => {
116-
const softService = SoftAssertService.getInstance()
117-
softService.setCurrentTest('test-7', 'test name', 'test file')
118-
const text = await el.getText()
183+
describe('Basic Matchers Support', () => {
184+
it('should support basic matchers failure without await', () => {
185+
const softService = SoftAssertService.getInstance()
186+
softService.setCurrentTest('test-7', 'test name', 'test file')
119187

120-
expectWdio.soft(text).toEqual('!Actual Text')
188+
expectWdio.soft('Actual Text').toEqual('!Actual Text')
121189

122-
const failures = expectWdio.getSoftFailures()
123-
expect(failures.length).toBe(1)
124-
expect(failures[0].matcherName).toBe('toHaveText')
190+
const failures = expectWdio.getSoftFailures()
191+
expect(failures.length).toBe(1)
192+
expect(failures[0].matcherName).toBe('toEqual')
193+
})
194+
195+
it('should support basic matchers success', async () => {
196+
const softService = SoftAssertService.getInstance()
197+
softService.setCurrentTest('test-8', 'test name', 'test file')
198+
199+
expectWdio.soft('Actual Text').toEqual('Actual Text')
200+
201+
const failures = expectWdio.getSoftFailures()
202+
expect(failures.length).toBe(0)
203+
})
204+
205+
it('not - should support basic matchers failure without await', async () => {
206+
const softService = SoftAssertService.getInstance()
207+
softService.setCurrentTest('test-9', 'test name', 'test file')
208+
209+
expectWdio.soft('Actual Text').not.toEqual('Actual Text')
210+
211+
const failures = expectWdio.getSoftFailures()
212+
expect(failures.length).toBe(1)
213+
expect(failures[0].matcherName).toBe('not.toEqual')
214+
})
215+
216+
it.skip('not - should support basic matcher success', async () => {
217+
const softService = SoftAssertService.getInstance()
218+
softService.setCurrentTest('test-10', 'test name', 'test file')
219+
220+
expectWdio.soft('Actual Text').not.toEqual('Not Actual Text')
221+
222+
const failures = expectWdio.getSoftFailures()
223+
expect(failures.length).toBe(0)
224+
})
125225
})
126226

127227
})

test/types.test-d.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, expectTypeOf, test } from 'vitest'
2+
import { $, $$ } from '@wdio/globals'
3+
import type { Matchers, Inverse } from 'expect'
4+
import expectLib from 'expect'
5+
import { expect as expectWdio } from '../src/index.js'
6+
7+
describe('Type test', () => {
8+
9+
test('Jest "expect" lib type tests as baseline', () => {
10+
// Basic matchers
11+
expectTypeOf(expectLib(true)).toExtend<Matchers<void, boolean> & Inverse<Matchers<void, boolean>>>()
12+
expectTypeOf(expectLib(true).toBe(true)).toBeVoid()
13+
expectTypeOf(expectLib(true).toBe(true)).not.toExtend<Promise<void>>()
14+
expectTypeOf(expectLib(Promise.resolve(true)).toBe(expect.any)).toBeVoid()
15+
expectTypeOf(expectLib(Promise.resolve(true)).resolves.toBe(expect.any)).resolves.toBeVoid()
16+
17+
// element matchers are not available in 'expect' lib
18+
expectTypeOf(expectLib($('element')).toBe(expect.any)).toBeVoid()
19+
expectTypeOf(expectLib($('element'))).not.toHaveProperty('toHaveText')
20+
expectTypeOf(expectLib($$('elements')).toBe(expect.any)).toBeVoid()
21+
expectTypeOf(expectLib($$('elements'))).not.toHaveProperty('toHaveText')
22+
})
23+
24+
test('Wdio expect & matchers type tests', () => {
25+
// Basic matchers
26+
expectTypeOf(expectWdio(true)).toExtend<Matchers<void, boolean> & Inverse<Matchers<void, boolean>>>()
27+
expectTypeOf(expectWdio(true).toBe(true)).toBeVoid()
28+
expectTypeOf(expectWdio(true).toBe(true)).not.toExtend<Promise<void>>()
29+
expectTypeOf(expectWdio(Promise.resolve(true)).toBe(true))
30+
expectTypeOf(expectWdio(Promise.resolve(true)).resolves.toBe(true)).resolves.toBeVoid()
31+
32+
// element matchers
33+
expectTypeOf(expectWdio($('element')).toBe(expect.any)).toBeVoid()
34+
expectTypeOf(expectWdio($('element')).toHaveText('test')).not.toBeVoid()
35+
expectTypeOf(expectWdio($('element')).toHaveText('test')).toExtend<Promise<void>>()
36+
expectTypeOf(expectWdio($$('elements')).toBe(expect.any)).toBeVoid()
37+
expectTypeOf(expectWdio($$('elements')).toHaveText('test')).toExtend<Promise<void>>()
38+
expectTypeOf(expectWdio($$('elements')).toHaveText('test')).not.toBeVoid()
39+
40+
})
41+
42+
test('Wdio soft expect & matchers type tests', () => {
43+
// Basic matchers
44+
expectTypeOf(expectWdio.soft(true)).toExtend<Matchers<void, boolean> & Inverse<Matchers<void, boolean>>>()
45+
expectTypeOf(expectWdio.soft(true).toBe(true)).toExtend<void>()
46+
expectTypeOf(expectWdio.soft(true).toBe(true)).not.toExtend<Promise<void>>()
47+
// to fix?
48+
//expectTypeOf(expectWdio.soft(Promise.resolve(true)).toBe(expect.any)).toBeVoid()
49+
expectTypeOf(expectWdio.soft(Promise.resolve(true)).resolves.toBe(expect.any)).toExtend<Promise<void>>()
50+
51+
// element matchers
52+
expectTypeOf(expectWdio($('element')).toBe(expect.any)).toBeVoid()
53+
expectTypeOf(expectWdio($('element')).toHaveText('test')).not.toBeVoid()
54+
expectTypeOf(expectWdio($('element')).toHaveText('test')).toExtend<Promise<void>>()
55+
expectTypeOf(expectWdio($$('elements')).toBe(expect.any)).toBeVoid()
56+
expectTypeOf(expectWdio($$('elements')).toHaveText('test')).toExtend<Promise<void>>()
57+
expectTypeOf(expectWdio($$('elements')).toHaveText('test')).not.toBeVoid()
58+
})
59+
})

0 commit comments

Comments
 (0)