expect-webdriverio 5.6.9 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -14
- package/jasmine-wdio-expect-async.d.ts +58 -18
- package/jasmine.d.ts +9 -16
- package/lib/constants.js +4 -0
- package/lib/index.js +30 -18
- package/lib/jasmineUtils.js +3 -2
- package/lib/matchers/asymmetrics/asymmetricsUtils.js +24 -0
- package/lib/matchers/asymmetrics/oneOf.js +51 -0
- package/lib/matchers/browser/toHaveClipboardText.js +8 -9
- package/lib/matchers/browser/toHaveLocalStorageItem.js +22 -14
- package/lib/matchers/browser/toHaveTitle.js +9 -10
- package/lib/matchers/browser/toHaveUrl.js +8 -9
- package/lib/matchers/element/toBeExisting.js +8 -3
- package/lib/matchers/element/toBeSelected.js +6 -13
- package/lib/matchers/element/toHaveAttribute.js +46 -45
- package/lib/matchers/element/toHaveChildren.js +30 -32
- package/lib/matchers/element/toHaveComputedLabel.js +21 -20
- package/lib/matchers/element/toHaveComputedRole.js +21 -20
- package/lib/matchers/element/toHaveElementClass.js +57 -0
- package/lib/matchers/element/toHaveElementProperty.js +40 -32
- package/lib/matchers/element/toHaveHTML.js +22 -23
- package/lib/matchers/element/toHaveHeight.js +23 -28
- package/lib/matchers/element/toHaveSize.js +16 -14
- package/lib/matchers/element/toHaveStyle.js +20 -14
- package/lib/matchers/element/toHaveText.js +26 -44
- package/lib/matchers/element/toHaveWidth.js +23 -28
- package/lib/matchers/elements/toBeElementsArrayOfSize.js +25 -30
- package/lib/matchers/mock/toBeRequestedTimes.js +12 -17
- package/lib/matchers/mock/toBeRequestedWith.js +13 -23
- package/lib/matchers/modifiers/some.js +15 -0
- package/lib/matchers.js +1 -1
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/lib/util/commandOptionsUtils.js +6 -0
- package/lib/util/elementsUtil.js +74 -4
- package/lib/util/executeCommand.js +94 -6
- package/lib/util/formatMessage.js +87 -32
- package/lib/util/numberOptionsUtil.js +92 -0
- package/lib/util/refetchElements.js +30 -5
- package/lib/util/stringUtil.js +12 -0
- package/lib/util/waitUntil.js +44 -0
- package/lib/utils.js +128 -105
- package/package.json +13 -11
- package/types/expect-global.d.ts +2 -1
- package/types/expect-webdriverio.d.ts +628 -136
- package/lib/matchers/element/toHaveClass.js +0 -61
package/lib/utils.js
CHANGED
|
@@ -1,69 +1,68 @@
|
|
|
1
1
|
import deepEql from 'deep-eql';
|
|
2
2
|
import { expect } from 'expect';
|
|
3
|
-
import { DEFAULT_OPTIONS } from './constants.js';
|
|
4
3
|
import { wrapExpectedWithArray } from './util/elementsUtil.js';
|
|
5
|
-
import {
|
|
6
|
-
import { enhanceError, enhanceErrorBe
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import { executeCommandWithStrategy } from './util/executeCommand.js';
|
|
5
|
+
import { enhanceError, enhanceErrorBe } from './util/formatMessage.js';
|
|
6
|
+
import { waitUntil } from './util/waitUntil.js';
|
|
7
|
+
import { DEFAULT_FEATURE_FLAGS } from './constants.js';
|
|
8
|
+
import { OneOfMatcher } from './matchers/asymmetrics/oneOf.js';
|
|
9
|
+
export function isJasmineStringAsymmetricMatcher(expected) {
|
|
10
|
+
return isAsymmetricMatcher(expected) && !('toAsymmetricMatcher' in expected) && 'jasmineToString' in expected && typeof expected.jasmineToString === 'function';
|
|
11
|
+
}
|
|
11
12
|
export function isAsymmetricMatcher(expected) {
|
|
12
13
|
return (typeof expected === 'object' &&
|
|
13
|
-
expected &&
|
|
14
|
-
'$$typeof' in expected &&
|
|
14
|
+
!!expected &&
|
|
15
15
|
'asymmetricMatch' in expected &&
|
|
16
|
-
expected
|
|
17
|
-
Boolean(expected.asymmetricMatch));
|
|
16
|
+
!!expected.asymmetricMatch);
|
|
18
17
|
}
|
|
19
|
-
function
|
|
20
|
-
return
|
|
18
|
+
export function isStringContainingMatcherLike(expected) {
|
|
19
|
+
return !!expected && expected.constructor.name === 'StringContaining';
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const start = Date.now();
|
|
29
|
-
while (true) {
|
|
30
|
-
if (Date.now() - start > wait) {
|
|
31
|
-
throw new Error('timeout');
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
const result = isNot !== (await condition());
|
|
35
|
-
error = undefined;
|
|
36
|
-
if (result) {
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
await sleep(interval);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
error = err;
|
|
43
|
-
await sleep(interval);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (error) {
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
return !isNot;
|
|
21
|
+
export function isInversedStringContainingMatcher(expected) {
|
|
22
|
+
return isStringContainingMatcherLike(expected) && expected.inverse === true;
|
|
23
|
+
}
|
|
24
|
+
export function getStringAsymmetricMatcherValue(expected) {
|
|
25
|
+
if ('expected' in expected) {
|
|
26
|
+
return expected.expected;
|
|
50
27
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
return isNot;
|
|
28
|
+
else if ('regexp' in expected) {
|
|
29
|
+
return expected.regexp;
|
|
56
30
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
31
|
+
else if ('sample' in expected) {
|
|
32
|
+
return expected.sample;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Could not extract value from asymmetric matcher: ${expected}. Please report this issue to the expect-webdriverio maintainers.`);
|
|
35
|
+
}
|
|
36
|
+
export function getAsymmetricMatcherValue(expected) {
|
|
37
|
+
if ('expected' in expected) {
|
|
38
|
+
return expected.expected;
|
|
39
|
+
}
|
|
40
|
+
else if ('expectedObject' in expected) {
|
|
41
|
+
return expected.expectedObject;
|
|
42
|
+
}
|
|
43
|
+
else if ('regexp' in expected) {
|
|
44
|
+
return expected.regexp;
|
|
45
|
+
}
|
|
46
|
+
else if ('sample' in expected) {
|
|
47
|
+
return expected.sample;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
async function executeCommandBe(received, command, options = {}) {
|
|
52
|
+
const { isNot, verb = 'be', allowEmptyElements = false } = this;
|
|
53
|
+
const { success: pass, actual, subject, context: { isSome } = {} } = await waitUntil(async (iteration) => {
|
|
54
|
+
return await executeCommandWithStrategy({
|
|
55
|
+
unresolvedElements: received,
|
|
56
|
+
expectedValues: true,
|
|
57
|
+
singleElementCompare: async (element) => {
|
|
58
|
+
const result = await command(element);
|
|
59
|
+
return { success: result, actual: result };
|
|
60
|
+
},
|
|
61
|
+
context: { isNot, iteration },
|
|
62
|
+
strictConfiguration: { allowEmptyElements },
|
|
63
|
+
});
|
|
64
|
+
}, isNot, { wait: options.wait, interval: options.interval });
|
|
65
|
+
const message = enhanceErrorBe(subject, actual, { ...this, verb, isSome }, options);
|
|
67
66
|
return {
|
|
68
67
|
pass,
|
|
69
68
|
message: () => message,
|
|
@@ -84,11 +83,30 @@ const compareNumbers = (actual, options = {}) => {
|
|
|
84
83
|
}
|
|
85
84
|
return false;
|
|
86
85
|
};
|
|
86
|
+
export const compareTextOrArray = (actualText, expectedTexts, options) => {
|
|
87
|
+
if (expectedTexts === undefined) {
|
|
88
|
+
return { actual: actualText, success: false };
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(expectedTexts)) {
|
|
91
|
+
if (expectedTexts.some((expected) => expected instanceof OneOfMatcher)) {
|
|
92
|
+
throw new Error('OneOf is not supported in array under legacy behavior. Please enable `useToHaveTextStrictMultiElementsCompareStrategy` feature flag to use the new strict index based matching strategy with `expect.oneOf()`.');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.warn('Array of expected values is deprecated. Please use `expect.oneOf()` asymmetric matcher to compare against multiple expected values. This will be removed in v8.0.0.');
|
|
96
|
+
expectedTexts = new OneOfMatcher(...expectedTexts).withOptions(options);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (expectedTexts instanceof OneOfMatcher) {
|
|
100
|
+
return { success: expectedTexts.asymmetricMatch(actualText), actual: actualText };
|
|
101
|
+
}
|
|
102
|
+
const compareResults = compareText(actualText, expectedTexts, options);
|
|
103
|
+
return { success: compareResults.success, actual: actualText };
|
|
104
|
+
};
|
|
87
105
|
export const compareText = (actual, expected, { ignoreCase = false, trim = true, containing = false, atStart = false, atEnd = false, atIndex, replace, }) => {
|
|
88
|
-
if (typeof actual !== 'string') {
|
|
106
|
+
if (typeof actual !== 'string' || expected === null || expected === undefined) {
|
|
89
107
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
actual,
|
|
109
|
+
success: false,
|
|
92
110
|
};
|
|
93
111
|
}
|
|
94
112
|
if (trim) {
|
|
@@ -102,59 +120,60 @@ export const compareText = (actual, expected, { ignoreCase = false, trim = true,
|
|
|
102
120
|
if (typeof expected === 'string') {
|
|
103
121
|
expected = expected.toLowerCase();
|
|
104
122
|
}
|
|
105
|
-
else if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
else if (isStringContainingMatcherLike(expected)) {
|
|
124
|
+
const sample = getStringAsymmetricMatcherValue(expected).toString().toLocaleLowerCase();
|
|
125
|
+
expected = (isInversedStringContainingMatcher(expected)
|
|
126
|
+
? expect.not.stringContaining(sample)
|
|
127
|
+
: expect.stringContaining(sample));
|
|
109
128
|
}
|
|
110
129
|
}
|
|
111
130
|
if (isAsymmetricMatcher(expected)) {
|
|
112
131
|
const result = expected.asymmetricMatch(actual);
|
|
113
132
|
return {
|
|
114
|
-
|
|
115
|
-
result
|
|
133
|
+
actual,
|
|
134
|
+
success: result
|
|
116
135
|
};
|
|
117
136
|
}
|
|
118
137
|
if (expected instanceof RegExp) {
|
|
119
138
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
actual,
|
|
140
|
+
success: !!actual.match(expected),
|
|
122
141
|
};
|
|
123
142
|
}
|
|
124
143
|
if (containing) {
|
|
125
144
|
return {
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
actual,
|
|
146
|
+
success: actual.includes(expected),
|
|
128
147
|
};
|
|
129
148
|
}
|
|
130
149
|
if (atStart) {
|
|
131
150
|
return {
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
actual,
|
|
152
|
+
success: actual.startsWith(expected),
|
|
134
153
|
};
|
|
135
154
|
}
|
|
136
155
|
if (atEnd) {
|
|
137
156
|
return {
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
actual,
|
|
158
|
+
success: actual.endsWith(expected),
|
|
140
159
|
};
|
|
141
160
|
}
|
|
142
161
|
if (atIndex) {
|
|
143
162
|
return {
|
|
144
|
-
|
|
145
|
-
|
|
163
|
+
actual,
|
|
164
|
+
success: actual.substring(atIndex, actual.length).startsWith(expected),
|
|
146
165
|
};
|
|
147
166
|
}
|
|
148
167
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
actual,
|
|
169
|
+
success: actual === expected,
|
|
151
170
|
};
|
|
152
171
|
};
|
|
153
172
|
export const compareTextWithArray = (actual, expectedArray, { ignoreCase = false, trim = false, containing = false, atStart = false, atEnd = false, atIndex, replace, }) => {
|
|
154
173
|
if (typeof actual !== 'string') {
|
|
155
174
|
return {
|
|
156
|
-
|
|
157
|
-
|
|
175
|
+
actual,
|
|
176
|
+
success: false,
|
|
158
177
|
};
|
|
159
178
|
}
|
|
160
179
|
if (trim) {
|
|
@@ -169,15 +188,16 @@ export const compareTextWithArray = (actual, expectedArray, { ignoreCase = false
|
|
|
169
188
|
if (typeof item === 'string') {
|
|
170
189
|
return item.toLowerCase();
|
|
171
190
|
}
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
if (isStringContainingMatcherLike(item)) {
|
|
192
|
+
const sample = getStringAsymmetricMatcherValue(item).toString().toLocaleLowerCase();
|
|
193
|
+
return (isInversedStringContainingMatcher(item)
|
|
194
|
+
? expect.not.stringContaining(sample)
|
|
195
|
+
: expect.stringContaining(sample));
|
|
176
196
|
}
|
|
177
197
|
return item;
|
|
178
198
|
});
|
|
179
199
|
}
|
|
180
|
-
const
|
|
200
|
+
const hasFoundTextInArray = expectedArray.some((expected) => {
|
|
181
201
|
if (expected instanceof RegExp) {
|
|
182
202
|
return !!actual.match(expected);
|
|
183
203
|
}
|
|
@@ -199,24 +219,24 @@ export const compareTextWithArray = (actual, expectedArray, { ignoreCase = false
|
|
|
199
219
|
return actual === expected;
|
|
200
220
|
});
|
|
201
221
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
actual,
|
|
223
|
+
success: hasFoundTextInArray,
|
|
204
224
|
};
|
|
205
225
|
};
|
|
206
226
|
export const compareObject = (actual, expected) => {
|
|
207
227
|
if (typeof actual !== 'object' || Array.isArray(actual)) {
|
|
208
228
|
return {
|
|
209
|
-
|
|
210
|
-
|
|
229
|
+
actual,
|
|
230
|
+
success: false,
|
|
211
231
|
};
|
|
212
232
|
}
|
|
213
233
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
234
|
+
actual,
|
|
235
|
+
success: deepEql(actual, expected),
|
|
216
236
|
};
|
|
217
237
|
};
|
|
218
238
|
export const compareStyle = async (actualEl, style, { ignoreCase = false, trim = true, containing = false, atStart = false, atEnd = false, atIndex, replace, }) => {
|
|
219
|
-
let
|
|
239
|
+
let success = true;
|
|
220
240
|
const actual = {};
|
|
221
241
|
for (const key in style) {
|
|
222
242
|
const css = await actualEl.getCSSProperty(key);
|
|
@@ -231,42 +251,37 @@ export const compareStyle = async (actualEl, style, { ignoreCase = false, trim =
|
|
|
231
251
|
expectedVal = expectedVal.toLowerCase();
|
|
232
252
|
}
|
|
233
253
|
if (containing) {
|
|
234
|
-
|
|
254
|
+
success = actualVal.includes(expectedVal);
|
|
235
255
|
actual[key] = actualVal;
|
|
236
256
|
}
|
|
237
257
|
else if (atStart) {
|
|
238
|
-
|
|
258
|
+
success = actualVal.startsWith(expectedVal);
|
|
239
259
|
actual[key] = actualVal;
|
|
240
260
|
}
|
|
241
261
|
else if (atEnd) {
|
|
242
|
-
|
|
262
|
+
success = actualVal.endsWith(expectedVal);
|
|
243
263
|
actual[key] = actualVal;
|
|
244
264
|
}
|
|
245
265
|
else if (atIndex) {
|
|
246
|
-
|
|
266
|
+
success = actualVal.substring(atIndex, actualVal.length).startsWith(expectedVal);
|
|
247
267
|
actual[key] = actualVal;
|
|
248
268
|
}
|
|
249
269
|
else if (replace) {
|
|
250
270
|
const replacedActual = replaceActual(replace, actualVal);
|
|
251
|
-
|
|
271
|
+
success = replacedActual === expectedVal;
|
|
252
272
|
actual[key] = replacedActual;
|
|
253
273
|
}
|
|
254
274
|
else {
|
|
255
|
-
|
|
275
|
+
success = success && actualVal === expectedVal;
|
|
256
276
|
actual[key] = css.value;
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
return {
|
|
260
|
-
|
|
261
|
-
|
|
280
|
+
actual,
|
|
281
|
+
success,
|
|
262
282
|
};
|
|
263
283
|
};
|
|
264
|
-
|
|
265
|
-
this.verb = verb;
|
|
266
|
-
this.expectation = expectation;
|
|
267
|
-
return fn.apply(this, args);
|
|
268
|
-
}
|
|
269
|
-
export { aliasFn, compareNumbers, enhanceError, executeCommand, executeCommandBe, numberError, waitUntil, wrapExpectedWithArray };
|
|
284
|
+
export { compareNumbers, enhanceError, executeCommandBe, waitUntil, wrapExpectedWithArray };
|
|
270
285
|
function replaceActual(replace, actual) {
|
|
271
286
|
const hasMultipleReplacers = replace.every((r) => Array.isArray(r));
|
|
272
287
|
const replacers = hasMultipleReplacers
|
|
@@ -281,3 +296,11 @@ function replaceActual(replace, actual) {
|
|
|
281
296
|
}
|
|
282
297
|
return actual;
|
|
283
298
|
}
|
|
299
|
+
export const getFeatureFlagValue = ({ featureFlags }, featureFlag) => {
|
|
300
|
+
const providedFeatureFlagValue = featureFlags?.[featureFlag];
|
|
301
|
+
if (providedFeatureFlagValue !== undefined) {
|
|
302
|
+
return providedFeatureFlagValue;
|
|
303
|
+
}
|
|
304
|
+
return DEFAULT_FEATURE_FLAGS[featureFlag] ?? false;
|
|
305
|
+
};
|
|
306
|
+
export const toArray = (value) => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expect-webdriverio",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"author": "Mykola Grybyk <mykola.grybyk@gmail.com>",
|
|
5
5
|
"description": "WebdriverIO Assertion Library",
|
|
6
6
|
"license": "MIT",
|
|
@@ -77,29 +77,31 @@
|
|
|
77
77
|
"playgrounds:snapshots:update": "cd playgrounds && npm run snapshots:update"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@vitest/snapshot": "^4.1.
|
|
80
|
+
"@vitest/snapshot": "^4.1.10",
|
|
81
81
|
"deep-eql": "^5.0.2",
|
|
82
82
|
"expect": "^30.4.1",
|
|
83
83
|
"jest-matcher-utils": "^30.4.1"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
+
"@jest/globals": "^30.4.1",
|
|
86
87
|
"@types/debug": "^4.1.13",
|
|
87
88
|
"@types/jasmine": "^5.1.15",
|
|
88
89
|
"@types/jest": "^30.0.0",
|
|
89
|
-
"@types/node": "^24.12.4",
|
|
90
90
|
"@types/mocha": "^10.0.10",
|
|
91
|
-
"@
|
|
92
|
-
"@vitest/coverage-v8": "^4.1.
|
|
91
|
+
"@types/node": "^24.13.3",
|
|
92
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
93
|
+
"@vitest/eslint-plugin": "^1.6.26",
|
|
93
94
|
"@wdio/eslint": "^0.1.3",
|
|
94
|
-
"@wdio/types": "^9.
|
|
95
|
-
"eslint": "^9.39.
|
|
95
|
+
"@wdio/types": "^9.30.1",
|
|
96
|
+
"eslint": "^9.39.5",
|
|
96
97
|
"husky": "^9.1.7",
|
|
97
|
-
"npm-run-all2": "^9.0.
|
|
98
|
-
"release-it": "^20.2.
|
|
98
|
+
"npm-run-all2": "^9.0.3",
|
|
99
|
+
"release-it": "^20.2.1",
|
|
99
100
|
"rimraf": "^6.1.3",
|
|
101
|
+
"strip-ansi": "^7.2.0",
|
|
100
102
|
"typescript": "^5.9.3",
|
|
101
|
-
"vitest": "^4.1.
|
|
102
|
-
"webdriverio": "^9.
|
|
103
|
+
"vitest": "^4.1.10",
|
|
104
|
+
"webdriverio": "^9.30.1"
|
|
103
105
|
},
|
|
104
106
|
"peerDependencies": {
|
|
105
107
|
"@wdio/globals": "^9.0.0",
|
package/types/expect-global.d.ts
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* Required when used in standalone mode (mocha) or to override the one of Jasmine
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass!
|
|
9
9
|
declare const expect: ExpectWebdriverIO.Expect
|
|
10
10
|
|
|
11
11
|
declare namespace NodeJS {
|
|
12
12
|
interface Global {
|
|
13
|
+
// @ts-expect-error: Both wdio & Jasmine augmentation declare different global expect explaining why TS is complaining here
|
|
13
14
|
expect: ExpectWebdriverIO.Expect
|
|
14
15
|
}
|
|
15
16
|
}
|