Skip to content

Commit e94ad20

Browse files
authored
fix: Jasmine augmentation withContext and add Playgrounds for Jasmine, Jest & Mocha (#2010)
* Add mocha playgrounds - expect-wdio releases breaks easily other integration since we just test with unit tests - Adding playground will help testing further before releasing and also helps troubleshooting existing problem inside the project * Add jasmine & jest playgrounds * Code reviews * Add visual snapshot to playgrounds * Document Jasmine quicks + fix type + Add withContext problem example * Remove soft from Jasmine since it useless * Add eslint and no-floating-promise to playgrounds * Fix Jasmine withContext + use project global instead of wdio one * Use proper folder for visual test + review jasmine docs quicks * Review * fix temp missing ignore * Have stable e2e * Review doc * More concise doc * Remove snapshot from Jasmine, update snapshot for others
1 parent 4f8e9b5 commit e94ad20

64 files changed

Lines changed: 25856 additions & 25 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/API.md

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ expect.clearSoftFailures();
6363

6464
### Integration with Test Frameworks
6565

66-
The soft assertions feature integrates with WebdriverIO's test runner automatically. By default, it will report all soft assertion failures at the end of each test (Mocha/Jasmine) or step (Cucumber).
66+
The soft assertions feature integrates with WebdriverIO's test runner automatically. By default, it will report all soft assertion failures at the end of each test (Mocha) or step (Cucumber).
6767

6868
To use with WebdriverIO, add the SoftAssertionService to your services list:
6969

@@ -75,7 +75,7 @@ export const config = {
7575
// ...
7676
services: [
7777
// ...other services
78-
[SoftAssertionService]
78+
[SoftAssertionService, {}]
7979
],
8080
// ...
8181
}
@@ -113,8 +113,7 @@ This is useful if you want full control over when soft assertions are verified o
113113

114114
### Known limitations
115115

116-
For Jasmine, using `wdio-jasmine-framework` will give a better plug-and-play experiences, else without it, the soft assertion service and custom matchers might not work/be registered correctly.
117-
Moreover, if Jasmine augmentation is used, the soft assertion function are not exposed in the typing, but could still work depending of your configuration. See [this issue](https://github.com/webdriverio/expect-webdriverio/issues/1893) for more details.
116+
The soft assertions service is not supported under Jasmine (e.g. `@wdio/jasmine-framework`) using the global import because Jasmine is already designed to provide similar behavior out of the box.
118117

119118
## Default Options
120119

@@ -905,7 +904,72 @@ await expect(elem).toHaveElementClass(/Container/i)
905904

906905
## Default Matchers
907906

908-
In addition to the `expect-webdriverio` matchers you can use builtin Jest's [expect](https://jestjs.io/docs/expect) assertions or [expect/expectAsync](https://jasmine.github.io/api/edge/global.html#expect) for Jasmine.
907+
In addition to the WebdriverIO matchers, `expect-webdriverio` also provides basic matchers from Jest's [expect](https://jestjs.io/docs/expect) library.
908+
909+
```ts
910+
describe('Expect matchers', () => {
911+
test('Basic matchers', async () => {
912+
// Equality
913+
expect(2 + 2).toBe(4);
914+
expect({a: 1}).toEqual({a: 1});
915+
expect([1, 2, 3]).toStrictEqual([1, 2, 3]);
916+
expect(2 + 2).not.toBe(5);
917+
918+
// Truthiness
919+
expect(null).toBeNull();
920+
expect(undefined).toBeUndefined();
921+
expect(0).toBeFalsy();
922+
expect(1).toBeTruthy();
923+
expect(NaN).toBeNaN();
924+
925+
// Numbers
926+
expect(4).toBeGreaterThan(3);
927+
expect(4).toBeGreaterThanOrEqual(4);
928+
expect(4).toBeLessThan(5);
929+
expect(4).toBeLessThanOrEqual(4);
930+
expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
931+
932+
// Strings
933+
expect('team').toMatch(/team/);
934+
expect('Christoph').toContain('stop');
935+
936+
// Arrays and iterables
937+
expect([1, 2, 3]).toContain(2);
938+
expect([{a: 1}, {b: 2}]).toContainEqual({a: 1});
939+
expect([1, 2, 3]).toHaveLength(3);
940+
941+
// Objects
942+
expect({a: 1, b: 2}).toHaveProperty('a');
943+
expect({a: {b: 2}}).toHaveProperty('a.b', 2);
944+
945+
// Errors
946+
expect(() => { throw new Error('error!') }).toThrow('error!');
947+
expect(() => { throw new TypeError('wrong type') }).toThrow(TypeError);
948+
949+
// Asymmetric matchers
950+
expect({foo: 'bar', baz: 1}).toEqual(expect.objectContaining({foo: expect.any(String)}));
951+
expect([1, 2, 3]).toEqual(expect.arrayContaining([2]));
952+
expect('abc').toEqual(expect.stringContaining('b'));
953+
expect('abc').toEqual(expect.stringMatching(/b/));
954+
expect(123).toEqual(expect.any(Number));
955+
956+
// Others
957+
expect(new Set([1, 2, 3])).toContain(2);
958+
959+
// .resolves / .rejects (async)
960+
await expect(Promise.resolve(42)).resolves.toBe(42);
961+
await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
962+
});
963+
});
964+
```
965+
966+
### Jasmine
967+
968+
For Jasmine, see the official documentation for [expect/expectAsync](https://jasmine.github.io/api/edge/global.html#expect), [matchers](https://jasmine.github.io/tutorials/your_first_suite#section-Matchers), and [async-matchers](https://jasmine.github.io/api/edge/async-matchers.html).
969+
970+
**Note:**
971+
- With the global import in @wdio/jasmine-framework, only WebdriverIO custom matchers are registered on expectAsync (assigned to global expect), so all matchers are always async, even those that are normally synchronous.
972+
- Default matchers are still available if you import `expect` directly from `expect-webdriverio` instead of using the global.
909973

910974
## Modifiers
911975

@@ -928,7 +992,7 @@ await expect(element).not.toBeDisplayed({ wait: 0 })
928992
await expect(browser).not.toHaveTitle('some title', { wait: 0 })
929993
```
930994

931-
Note: You can pair `.not` with asymmetric matchers, but to enable the wait-until behavior, `.not` must be used directly on the `expect()` call.
995+
**Note:** You can pair `.not` with asymmetric matchers, but to enable the wait-until behavior, `.not` must be used directly on the `expect()` call.
932996

933997
## Asymmetric Matchers
934998

@@ -943,3 +1007,21 @@ or
9431007
```ts
9441008
await expect(browser).toHaveTitle(expect.not.stringContaining('some title'))
9451009
```
1010+
1011+
### Jasmine
1012+
1013+
Even under `@wdio/jasmine-framework`, Jasmine asymmetric matchers do not work with WebdriverIO matchers.
1014+
1015+
```ts
1016+
// DOES NOT work
1017+
await expect(browser).toHaveTitle(jasmine.stringContaining('some title'))
1018+
1019+
// Use expect
1020+
await expect(browser).toHaveTitle(expect.stringContaining('some title'))
1021+
```
1022+
1023+
However, when using Jasmine original matchers, both works.
1024+
```ts
1025+
await expect(url).toEqual(jasmine.stringMatching(/^https:\/\//))
1026+
await expect(url).toEqual(expect.stringMatching(/^https:\/\//))
1027+
```

docs/Framework.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ See also this [documentation](https://webdriver.io/docs/assertion/#migrating-fro
146146
### Jasmine
147147
When paired with [Jasmine](https://jasmine.github.io/), [`@wdio/jasmine-framework`](https://www.npmjs.com/package/@wdio/jasmine-framework) is also required to configure it correctly, as it needs to force `expect` to be `expectAsync` and also register the WDIO matchers with `addAsyncMatcher` since `expect-webdriverio` only supports the Jest-style `expect.extend` version.
148148

149+
Jasmine differs from other assertion libraries in two key ways:
150+
1. Jasmine performs soft assertions by default, collecting failures and only failing the test at the end. Because of this, the SoftAssertion service is not needed or supported.
151+
2. Forcing `expectAsync` as `expect` (by `@wdio/jasmine-framework`) makes even basic matchers asynchronous. However, since Jasmine handles all promises at the end of the test, assertions appear to work properly—unlike in other frameworks, where using `await` is mandatory for correct behavior.
152+
- Note: This goes against [this recommendation](https://jasmine.github.io/api/edge/async-matchers) and could cause unexpected issues.
153+
149154
The types `expect-webdriverio/jasmine` are still offered but are subject to removal or being moved into `@wdio/jasmine-framework`. The usage of `expectAsync` is also subject to future removal.
150155

151156
#### Jasmine `expectAsync`
@@ -173,14 +178,14 @@ Expected in `tsconfig.json`:
173178
```
174179

175180
#### Global `expectAsync` force as `expect`
176-
When the global ambiant is the `expect` of wdio but forced to be `expectAsync` under the hood, like when using `@wdio/jasmine-framework`, then even the basic matchers need to be awaited
181+
When the global ambient `expect` is actually `expectAsync` under the hood (as with `@wdio/jasmine-framework`), it is recommended to `await` even basic matchers, even though Jasmine will handle any un-awaited assertions at the end of the test.
177182

178183
```ts
179184
describe('My tests', async () => {
180185
it('should verify my browser to have the expected url', async () => {
181186
await expect(browser).toHaveUrl('https://example.com')
182187

183-
// Even basic matchers requires expect since they are promises underneath
188+
// Even basic matchers should have `await` since they are promises underneath
184189
await expect(true).toBe(true)
185190
})
186191
})
@@ -249,4 +254,4 @@ It is recommended to build your project using this approach instead of relying o
249254

250255
### Cucumber
251256

252-
More details to come. In short, when paired with `@wdio/cucumber-framework`, you can use WDIO's expect with Cucumber and even [Gherkin](https://www.npmjs.com/package/@cucumber/gherkin).
257+
More details to come. In short, when paired with `@wdio/cucumber-framework`, you can use WDIO's expect with Cucumber and even [Gherkin](https://www.npmjs.com/package/@cucumber/gherkin).

eslint.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export default wdioEslint.config([
44
{
55
ignores: [
66
'lib',
7-
'**/*/dist'
7+
'**/*/dist',
8+
'playgrounds/**'
89
]
910
},
1011
/**

jasmine-wdio-expect-async.d.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,37 @@
11
/// <reference types="./types/expect-webdriverio.d.ts"/>
22

3+
declare namespace jasmine {
4+
5+
/**
6+
* Async matchers for Jasmine to allow the typing of `expectAsync` with WebDriverIO matchers.
7+
* T is the type of the actual value
8+
* U is the type of the expected value
9+
* Both T,U must stay named as they are to override the default `AsyncMatchers` type from Jasmine.
10+
*
11+
* We force Matchers to return a `Promise<void>` since Jasmine's `expectAsync` expects a promise in all cases (different from Jest)
12+
*/
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14+
interface AsyncMatchers<T, U> extends Omit<ExpectWebdriverIO.Matchers<Promise<void>, T>, 'toMatchSnapshot' | 'toMatchInlineSnapshot'> {
15+
/**
16+
* snapshot matcher
17+
* @param label optional snapshot label
18+
*/
19+
toMatchSnapshot(label?: string): Promise<void>;
20+
/**
21+
* inline snapshot matcher
22+
* @param snapshot snapshot string (autogenerated if not specified)
23+
* @param label optional snapshot label
24+
*/
25+
toMatchInlineSnapshot(snapshot?: string, label?: string): Promise<void>;
26+
}
27+
}
28+
329
/**
4-
* Overrides the default wdio `expect` for Jasmine case specifically since the `expect` is now completely asynchronous which is not the case under Jest or standalone.
30+
* Overrides the default WDIO expect specifically for Jasmine, since `expectAsync` is forced into `expect`, making all matchers fully asynchronous. This is not the case under Jest or Mocha.
31+
* Using `jasmine.AsyncMatchers` pull on WdioMatchers above but also allow to using Jasmine's built-in matchers and also `withContext` matcher.
532
*/
633
declare namespace ExpectWebdriverIO {
7-
interface Expect extends ExpectWebdriverIO.AsymmetricMatchers, ExpectLibInverse<ExpectWebdriverIO.InverseAsymmetricMatchers>, WdioExpect {
34+
interface Expect {
835
/**
936
* The `expect` function is used every time you want to test a value.
1037
* You will rarely call `expect` by itself.
@@ -17,6 +44,6 @@ declare namespace ExpectWebdriverIO {
1744
*
1845
* @param actual The value to apply matchers against.
1946
*/
20-
<T = unknown>(actual: T): ExpectWebdriverIO.MatchersAndInverse<Promise<void>, T>
47+
<T = unknown>(actual: T): jasmine.AsyncMatchers<T, void>
2148
}
22-
}
49+
}

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
"types": "./jasmine.d.ts"
3939
}
4040
],
41+
"./jasmine-wdio-expect-async": [
42+
{
43+
"types": "./jasmine-wdio-expect-async.d.ts"
44+
}
45+
],
4146
"./types": "./types/expect-global.d.ts",
4247
"./expect-global": "./types/expect-global.d.ts"
4348
},
@@ -65,7 +70,9 @@
6570
"ts:jasmine-async": "cd test-types/jasmine-async && tsc --project ./tsconfig.json",
6671
"checks:all": "npm run build && npm run compile && npm run tsc:root-types && npm run test && npm run ts",
6772
"watch": "npm run compile -- --watch",
68-
"prepare": "husky install"
73+
"prepare": "husky install",
74+
"playgrounds:setup": "for dir in playgrounds/*/; do cd \"$dir\" && npm install && cd ../..; done",
75+
"playgrounds:checks:all": "for dir in playgrounds/*/; do cd \"$dir\" && npm run checks:all && cd ../..; done"
6976
},
7077
"dependencies": {
7178
"@vitest/snapshot": "^4.0.16",

playgrounds/jasmine/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
2+
import tsParser from '@typescript-eslint/parser';
3+
4+
export default {
5+
files: ['**/*.ts', '**/*.js'],
6+
languageOptions: {
7+
parser: tsParser,
8+
parserOptions: {
9+
project: './tsconfig.json',
10+
sourceType: 'module',
11+
ecmaVersion: 2021,
12+
},
13+
globals: {
14+
NodeJS: true,
15+
require: true,
16+
module: true,
17+
__dirname: true,
18+
process: true,
19+
},
20+
},
21+
plugins: {
22+
'@typescript-eslint': tsEslintPlugin,
23+
},
24+
rules: {
25+
...tsEslintPlugin.configs['recommended'].rules,
26+
'@typescript-eslint/no-floating-promises': 'error',
27+
},
28+
};

0 commit comments

Comments
 (0)