Skip to content

Commit 2a52080

Browse files
committed
Fixing jasmine prob, once for all
1 parent 3996fe7 commit 2a52080

13 files changed

Lines changed: 268 additions & 29 deletions

File tree

docs/CustomMatchers.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1+
# Custom Matchers
2+
3+
`expect-webdriverio` registers WebdriverIO custom matchers out of the box for a seamless experience.
4+
5+
To use WebdriverIO custom matchers (except asymmetric matchers) directly in:
6+
- **Jest**: Register matchers manually with `expect.extend`.
7+
- **Jasmine**: Register matchers manually with `jasmine.addAsyncMatchers`, then they will be available on `expectAsync`.
8+
- Using `@wdio/jasmine-framework` provides a similar out-of-the-box experience.
9+
- **Types**: Type augmentation for custom matchers is provided. See [Types.md](Types.md) for details.
10+
111
## Adding your own matchers
212

3-
Similar to how `expect-webdriverio` extends Jasmine/Jest matchers it's possible to add custom matchers.
13+
Similar to how `expect-webdriverio` provide custom matchers it's possible to add your own custom matchers.
414

515
- [Jasmine](https://jasmine.github.io/) see [custom matchers](https://jasmine.github.io/tutorials/custom_matchers) doc
616
- Everyone else see [Jest's expect.extend](https://jestjs.io/docs/expect#expectextendmatchers)

jasmine-wdio-expect-async.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ declare namespace jasmine {
1414

1515
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- U is required to properly override Jasmine's AsyncMatchers
1616
interface AsyncMatchers<T, U> extends ExpectWebdriverIO.CustomMatchers<Promise<void>, T> {}
17+
18+
// Needed to reference it below for the withContext method
19+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
20+
interface Matchers<T> {}
1721
}
1822

1923
declare namespace ExpectWebdriverIO {
@@ -39,12 +43,12 @@ declare namespace ExpectWebdriverIO {
3943
* @param actual The value to apply matchers against.
4044
*/
4145
<T = unknown>(actual: T): {
42-
withContext(message: string): jasmine.AsyncMatchers<T, U> & jasmine.Matchers<T>;
43-
} & jasmine.AsyncMatchers<T, void> & jasmine.Matchers<T>
46+
withContext(message: string): jasmine.AsyncMatchers<T, Promise<void>> & jasmine.Matchers<T>;
47+
} & jasmine.AsyncMatchers<T, Promise<void>> & jasmine.Matchers<T>
4448
}
4549
}
4650

47-
//// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass!
51+
// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass!
4852
declare const expect: ExpectWebdriverIO.JasmineExpect
4953
declare namespace NodeJS {
5054
interface Global {

package-lock.json

Lines changed: 68 additions & 0 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
@@ -99,7 +99,9 @@
9999
"shelljs": "^0.10.0",
100100
"typescript": "^5.9.3",
101101
"vitest": "^4.0.16",
102-
"webdriverio": "^9.21.0"
102+
"webdriverio": "^9.21.0",
103+
"jasmine": "^5.1.2",
104+
"jasmine-core": "^5.1.2"
103105
},
104106
"peerDependencies": {
105107
"@wdio/globals": "^9.0.0",

playgrounds/jasmine/test/specs/globalImport/jasmine-specific.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,18 @@ describe('Jasmine-Specific Features', () => {
119119

120120
// failing on jasmine.stringContaining not working properly with wdio matchers
121121
describe('Browser state validation', () => {
122-
xit('should validate browser properties with asymmetric matchers', async () => {
122+
fit('should validate browser properties with asymmetric matchers', async () => {
123123
const title = await browser.getTitle()
124124
const url = await browser.getUrl()
125125

126126
await expect(title).toEqual(jasmine.stringMatching(/WebdriverIO/i))
127127
await expect(url).toEqual(jasmine.stringContaining('webdriver.io'))
128128

129129
// Combined with WebdriverIO matchers
130-
await expect(browser).toHaveUrl(jasmine.stringContaining('webdriver.io'))
131-
await expect(browser).toHaveTitle(jasmine.stringContaining('WebdriverIO'))
130+
// await expect(browser).toHaveUrl(jasmine.stringContaining('webdriver.io'))
131+
// await expect(browser).toHaveTitle(jasmine.stringContaining('WebdriverIO'))
132+
// await expect(browser).toHaveUrl(jasmine.stringContaining('WEBDRIVER.io'),{ignoreCase: true})
133+
await expect(browser).toHaveTitle(jasmine.stringContaining('WEBDRIVERIO'), {ignoreCase: true})
132134
})
133135
})
134136

playgrounds/jasmine/wdio.conf.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export const config: WebdriverIO.Config = {
1717
specs: [
1818
'./test/specs/**/*.test.ts'
1919
// './test/specs/**/jasmine-specific.test.ts',
20+
// './test/specs/expectWdioImport/basic-matchers.test.ts',
2021
// './test/specs/expectWdioImport/wdio-matchers.test.ts'
22+
'./test/specs/globalImport/jasmine-specific.test.ts',
2123
],
2224

2325
//

playgrounds/mocha/test/specs/basic-matchers.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,61 @@ describe('Basic Expect Matchers', () => {
55
await browser.url('https://webdriver.io')
66
})
77

8+
describe('Expect matchers', () => {
9+
test('Basic matchers', async () => {
10+
// Equality
11+
expect(2 + 2).toBe(4);
12+
expect({a: 1}).toEqual({a: 1});
13+
expect([1, 2, 3]).toStrictEqual([1, 2, 3]);
14+
expect(2 + 2).not.toBe(5);
15+
16+
// Truthiness
17+
expect(null).toBeNull();
18+
expect(undefined).toBeUndefined();
19+
expect(0).toBeFalsy();
20+
expect(1).toBeTruthy();
21+
expect(NaN).toBeNaN();
22+
23+
// Numbers
24+
expect(4).toBeGreaterThan(3);
25+
expect(4).toBeGreaterThanOrEqual(4);
26+
expect(4).toBeLessThan(5);
27+
expect(4).toBeLessThanOrEqual(4);
28+
expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
29+
30+
// Strings
31+
expect('team').toMatch(/team/);
32+
expect('Christoph').toContain('stop');
33+
34+
// Arrays and iterables
35+
expect([1, 2, 3]).toContain(2);
36+
expect([{a: 1}, {b: 2}]).toContainEqual({a: 1});
37+
expect([1, 2, 3]).toHaveLength(3);
38+
39+
// Objects
40+
expect({a: 1, b: 2}).toHaveProperty('a');
41+
expect({a: {b: 2}}).toHaveProperty('a.b', 2);
42+
43+
// Errors
44+
expect(() => { throw new Error('error!') }).toThrow('error!');
45+
expect(() => { throw new TypeError('wrong type') }).toThrow(TypeError);
46+
47+
// Asymmetric matchers
48+
expect({foo: 'bar', baz: 1}).toEqual(expect.objectContaining({foo: expect.any(String)}));
49+
expect([1, 2, 3]).toEqual(expect.arrayContaining([2]));
50+
expect('abc').toEqual(expect.stringContaining('b'));
51+
expect('abc').toEqual(expect.stringMatching(/b/));
52+
expect(123).toEqual(expect.any(Number));
53+
54+
// Others
55+
expect(new Set([1, 2, 3])).toContain(2);
56+
57+
// .resolves / .rejects (async)
58+
await expect(Promise.resolve(42)).resolves.toBe(42);
59+
await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
60+
});
61+
});
62+
863
describe('Boolean matchers', () => {
964
it('should verify truthy values', async () => {
1065
const element = await $('.navbar')

src/matchers/browser/toHaveUrl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export async function toHaveUrl(
1919
const pass = await waitUntil(async () => {
2020
actual = await browser.getUrl()
2121

22+
console.log('toHaveUrl', { actual, expectedValue })
2223
return compareText(actual, expectedValue, options).result
2324
}, isNot, options)
2425

src/utils.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,46 @@ import { enhanceError, enhanceErrorBe, numberError } from './util/formatMessage.
1111

1212
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
1313

14-
const asymmetricMatcher =
15-
typeof Symbol === 'function' && Symbol.for
16-
? Symbol.for('jest.asymmetricMatcher')
17-
: 0x13_57_a5
14+
export function isWdioAsymmetricMatcher<T>(expected: unknown): expected is WdioAsymmetricMatcher<T> {
15+
return isAsymmetricMatcher(expected) && 'sample' in expected
16+
}
17+
18+
export function isJasmineAsymmetricMatcher<T>(expected: unknown): expected is JasmineAsymmetricMatcher<T> {
19+
return isAsymmetricMatcher(expected) && 'expected' in expected
20+
}
1821

19-
export function isAsymmetricMatcher(expected: unknown): expected is WdioAsymmetricMatcher<unknown> {
22+
export function isAsymmetricMatcher(expected: unknown): expected is WdioAsymmetricMatcher<unknown> | JasmineAsymmetricMatcher<unknown> {
2023
return (
2124
typeof expected === 'object' &&
22-
expected &&
23-
'$$typeof' in expected &&
25+
!!expected &&
2426
'asymmetricMatch' in expected &&
25-
expected.$$typeof === asymmetricMatcher &&
2627
Boolean(expected.asymmetricMatch)
27-
) as boolean
28+
)
29+
}
30+
31+
export function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetricMatcher<unknown> | JasmineAsymmetricMatcher<unknown> {
32+
return !!expected && expected.constructor.name === 'StringContaining'
2833
}
2934

30-
function isStringContainingMatcher(expected: unknown): expected is WdioAsymmetricMatcher<unknown> {
31-
return isAsymmetricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString())
35+
export function isStrictStringContainingMatcher(expected: unknown): expected is WdioAsymmetricMatcher<unknown> | JasmineAsymmetricMatcher<unknown> {
36+
if (isStringContainingMatcher(expected)) {
37+
if (isWdioAsymmetricMatcher(expected)) {
38+
return expected.toString().includes('StringContaining')
39+
}
40+
return true
41+
}
42+
return false
43+
}
44+
45+
export function getValueFromAsymmetricMatcher<T>(
46+
expected: WdioAsymmetricMatcher<T> | JasmineAsymmetricMatcher<T>
47+
): T {
48+
if ('expected' in expected) {
49+
return expected.expected
50+
} else if ('sample' in expected) {
51+
return expected.sample
52+
}
53+
throw new Error('Could not extract value from asymmetric matcher: ' + expected)
3254
}
3355

3456
/**
@@ -141,7 +163,7 @@ const compareNumbers = (actual: number, options: ExpectWebdriverIO.NumberOptions
141163

142164
export const compareText = (
143165
actual: string,
144-
expected: string | RegExp | WdioAsymmetricMatcher<string>,
166+
expected: string | RegExp | WdioAsymmetricMatcher<string> | JasmineAsymmetricMatcher<string>,
145167
{
146168
ignoreCase = false,
147169
trim = true,
@@ -170,9 +192,10 @@ export const compareText = (
170192
if (typeof expected === 'string') {
171193
expected = expected.toLowerCase()
172194
} else if (isStringContainingMatcher(expected)) {
173-
expected = (expected.toString() === 'StringContaining'
174-
? expect.stringContaining(expected.sample?.toString().toLowerCase())
175-
: expect.not.stringContaining(expected.sample?.toString().toLowerCase())) as WdioAsymmetricMatcher<string>
195+
const sample = getValueFromAsymmetricMatcher<string>(expected).toString().toLocaleLowerCase()
196+
expected = (isStrictStringContainingMatcher(expected)
197+
? expect.stringContaining(sample)
198+
: expect.not.stringContaining(sample)) as WdioAsymmetricMatcher<string>
176199
}
177200
}
178201

test-types/jasmine/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
"types": [
99
"@types/jasmine",
1010
"../../jasmine.d.ts",
11-
"../../jasmine-wdio-expect-async.d.ts"
1211
]
1312
}
14-
}
13+
}

0 commit comments

Comments
 (0)