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.
Files changed (45) hide show
  1. package/README.md +16 -14
  2. package/jasmine-wdio-expect-async.d.ts +58 -18
  3. package/jasmine.d.ts +9 -16
  4. package/lib/constants.js +4 -0
  5. package/lib/index.js +30 -18
  6. package/lib/jasmineUtils.js +3 -2
  7. package/lib/matchers/asymmetrics/asymmetricsUtils.js +24 -0
  8. package/lib/matchers/asymmetrics/oneOf.js +51 -0
  9. package/lib/matchers/browser/toHaveClipboardText.js +8 -9
  10. package/lib/matchers/browser/toHaveLocalStorageItem.js +22 -14
  11. package/lib/matchers/browser/toHaveTitle.js +9 -10
  12. package/lib/matchers/browser/toHaveUrl.js +8 -9
  13. package/lib/matchers/element/toBeExisting.js +8 -3
  14. package/lib/matchers/element/toBeSelected.js +6 -13
  15. package/lib/matchers/element/toHaveAttribute.js +46 -45
  16. package/lib/matchers/element/toHaveChildren.js +30 -32
  17. package/lib/matchers/element/toHaveComputedLabel.js +21 -20
  18. package/lib/matchers/element/toHaveComputedRole.js +21 -20
  19. package/lib/matchers/element/toHaveElementClass.js +57 -0
  20. package/lib/matchers/element/toHaveElementProperty.js +40 -32
  21. package/lib/matchers/element/toHaveHTML.js +22 -23
  22. package/lib/matchers/element/toHaveHeight.js +23 -28
  23. package/lib/matchers/element/toHaveSize.js +16 -14
  24. package/lib/matchers/element/toHaveStyle.js +20 -14
  25. package/lib/matchers/element/toHaveText.js +26 -44
  26. package/lib/matchers/element/toHaveWidth.js +23 -28
  27. package/lib/matchers/elements/toBeElementsArrayOfSize.js +25 -30
  28. package/lib/matchers/mock/toBeRequestedTimes.js +12 -17
  29. package/lib/matchers/mock/toBeRequestedWith.js +13 -23
  30. package/lib/matchers/modifiers/some.js +15 -0
  31. package/lib/matchers.js +1 -1
  32. package/lib/tsconfig.build.tsbuildinfo +1 -1
  33. package/lib/util/commandOptionsUtils.js +6 -0
  34. package/lib/util/elementsUtil.js +74 -4
  35. package/lib/util/executeCommand.js +94 -6
  36. package/lib/util/formatMessage.js +87 -32
  37. package/lib/util/numberOptionsUtil.js +92 -0
  38. package/lib/util/refetchElements.js +30 -5
  39. package/lib/util/stringUtil.js +12 -0
  40. package/lib/util/waitUntil.js +44 -0
  41. package/lib/utils.js +128 -105
  42. package/package.json +13 -11
  43. package/types/expect-global.d.ts +2 -1
  44. package/types/expect-webdriverio.d.ts +628 -136
  45. 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 { executeCommand } from './util/executeCommand.js';
6
- import { enhanceError, enhanceErrorBe, numberError } from './util/formatMessage.js';
7
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
- const asymmetricMatcher = typeof Symbol === 'function' && Symbol.for
9
- ? Symbol.for('jest.asymmetricMatcher')
10
- : 0x13_57_a5;
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.$$typeof === asymmetricMatcher &&
17
- Boolean(expected.asymmetricMatch));
16
+ !!expected.asymmetricMatch);
18
17
  }
19
- function isStringContainingMatcher(expected) {
20
- return isAsymmetricMatcher(expected) && ['StringContaining', 'StringNotContaining'].includes(expected.toString());
18
+ export function isStringContainingMatcherLike(expected) {
19
+ return !!expected && expected.constructor.name === 'StringContaining';
21
20
  }
22
- const waitUntil = async (condition, isNot = false, { wait = DEFAULT_OPTIONS.wait, interval = DEFAULT_OPTIONS.interval } = {}) => {
23
- if (wait === 0) {
24
- return await condition();
25
- }
26
- let error;
27
- try {
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
- catch {
52
- if (error) {
53
- throw error;
54
- }
55
- return isNot;
28
+ else if ('regexp' in expected) {
29
+ return expected.regexp;
56
30
  }
57
- };
58
- async function executeCommandBe(received, command, options) {
59
- const { isNot, verb = 'be' } = this;
60
- let el = await received?.getElement();
61
- const pass = await waitUntil(async () => {
62
- const result = await executeCommand.call(this, el, async (element) => ({ result: await command(element) }), options);
63
- el = result.el;
64
- return result.success;
65
- }, isNot, options);
66
- const message = enhanceErrorBe(el, { ...this, verb }, options);
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
- value: actual,
91
- result: false,
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 (isStringContainingMatcher(expected)) {
106
- expected = (expected.toString() === 'StringContaining'
107
- ? expect.stringContaining(expected.sample?.toString().toLowerCase())
108
- : expect.not.stringContaining(expected.sample?.toString().toLowerCase()));
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
- value: actual,
115
- result
133
+ actual,
134
+ success: result
116
135
  };
117
136
  }
118
137
  if (expected instanceof RegExp) {
119
138
  return {
120
- value: actual,
121
- result: !!actual.match(expected),
139
+ actual,
140
+ success: !!actual.match(expected),
122
141
  };
123
142
  }
124
143
  if (containing) {
125
144
  return {
126
- value: actual,
127
- result: actual.includes(expected),
145
+ actual,
146
+ success: actual.includes(expected),
128
147
  };
129
148
  }
130
149
  if (atStart) {
131
150
  return {
132
- value: actual,
133
- result: actual.startsWith(expected),
151
+ actual,
152
+ success: actual.startsWith(expected),
134
153
  };
135
154
  }
136
155
  if (atEnd) {
137
156
  return {
138
- value: actual,
139
- result: actual.endsWith(expected),
157
+ actual,
158
+ success: actual.endsWith(expected),
140
159
  };
141
160
  }
142
161
  if (atIndex) {
143
162
  return {
144
- value: actual,
145
- result: actual.substring(atIndex, actual.length).startsWith(expected),
163
+ actual,
164
+ success: actual.substring(atIndex, actual.length).startsWith(expected),
146
165
  };
147
166
  }
148
167
  return {
149
- value: actual,
150
- result: actual === expected,
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
- value: actual,
157
- result: false,
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 (isStringContainingMatcher(item)) {
173
- return (item.toString() === 'StringContaining'
174
- ? expect.stringContaining(item.sample?.toString().toLowerCase())
175
- : expect.not.stringContaining(item.sample?.toString().toLowerCase()));
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 textInArray = expectedArray.some((expected) => {
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
- value: actual,
203
- result: textInArray,
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
- value: actual,
210
- result: false,
229
+ actual,
230
+ success: false,
211
231
  };
212
232
  }
213
233
  return {
214
- value: actual,
215
- result: deepEql(actual, expected),
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 result = true;
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
- result = actualVal.includes(expectedVal);
254
+ success = actualVal.includes(expectedVal);
235
255
  actual[key] = actualVal;
236
256
  }
237
257
  else if (atStart) {
238
- result = actualVal.startsWith(expectedVal);
258
+ success = actualVal.startsWith(expectedVal);
239
259
  actual[key] = actualVal;
240
260
  }
241
261
  else if (atEnd) {
242
- result = actualVal.endsWith(expectedVal);
262
+ success = actualVal.endsWith(expectedVal);
243
263
  actual[key] = actualVal;
244
264
  }
245
265
  else if (atIndex) {
246
- result = actualVal.substring(atIndex, actualVal.length).startsWith(expectedVal);
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
- result = replacedActual === expectedVal;
271
+ success = replacedActual === expectedVal;
252
272
  actual[key] = replacedActual;
253
273
  }
254
274
  else {
255
- result = result && actualVal === expectedVal;
275
+ success = success && actualVal === expectedVal;
256
276
  actual[key] = css.value;
257
277
  }
258
278
  }
259
279
  return {
260
- value: actual,
261
- result,
280
+ actual,
281
+ success,
262
282
  };
263
283
  };
264
- function aliasFn(fn, { verb, expectation, } = {}, ...args) {
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": "5.6.9",
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.7",
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
- "@jest/globals": "^30.4.1",
92
- "@vitest/coverage-v8": "^4.1.7",
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.27.2",
95
- "eslint": "^9.39.4",
95
+ "@wdio/types": "^9.30.1",
96
+ "eslint": "^9.39.5",
96
97
  "husky": "^9.1.7",
97
- "npm-run-all2": "^9.0.1",
98
- "release-it": "^20.2.0",
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.7",
102
- "webdriverio": "^9.27.2"
103
+ "vitest": "^4.1.10",
104
+ "webdriverio": "^9.30.1"
103
105
  },
104
106
  "peerDependencies": {
105
107
  "@wdio/globals": "^9.0.0",
@@ -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
- //// @ts-expect-error: IDE might flags this one but just does be concerned by it. This way the `tsc:root-types` can pass!
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
  }