expect-webdriverio 6.0.6 → 6.0.8

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.
@@ -16,21 +16,41 @@ export async function toBeRequestedWith(received, expectedValue = {}, options =
16
16
  expectedValue,
17
17
  options,
18
18
  });
19
+ const hasPayloadExpectation = expectedValue.postData !== undefined || expectedValue.response !== undefined;
20
+ const waitForPayloadOnNot = isNot && hasPayloadExpectation;
21
+ const parseCache = new Map();
22
+ let payloadNeverCollected = false;
19
23
  const { success: pass, actual } = await waitUntil(async () => {
20
24
  let lastCall;
25
+ let hasPendingPayloadCandidate = false;
21
26
  for (const call of received.calls) {
22
27
  lastCall = call;
23
- if (methodMatcher(call.request.method, expectedValue.method) &&
28
+ const matchesNonPayloadCriteria = methodMatcher(call.request.method, expectedValue.method) &&
24
29
  statusCodeMatcher(call.response.status, expectedValue.statusCode) &&
25
30
  urlMatcher(call.request.url, expectedValue.url) &&
26
31
  headersMatcher(reduceHeaders(call.request.headers), expectedValue.requestHeaders) &&
27
- headersMatcher(reduceHeaders(call.response.headers), expectedValue.responseHeaders)) {
32
+ headersMatcher(reduceHeaders(call.response.headers), expectedValue.responseHeaders);
33
+ if (!matchesNonPayloadCriteria) {
34
+ continue;
35
+ }
36
+ if (bodyMatcher(call.postData, expectedValue.postData, parseCache) &&
37
+ bodyMatcher(call.body, expectedValue.response, parseCache)) {
28
38
  return { success: true, subject: call, actual: call };
29
39
  }
40
+ if ((expectedValue.postData !== undefined && call.postData === undefined) ||
41
+ (expectedValue.response !== undefined && call.body === undefined)) {
42
+ hasPendingPayloadCandidate = true;
43
+ }
30
44
  }
31
- return { success: false, subject: lastCall, actual: lastCall };
32
- }, isNot, { ...options, wait: isNot ? 0 : options.wait });
33
- const message = enhanceError('mock', minifyRequestedWith(expectedValue), minifyRequestMock(actual, expectedValue) || 'was not called', this, verb, expectation, '', options);
45
+ payloadNeverCollected = hasPendingPayloadCandidate;
46
+ return {
47
+ success: false,
48
+ subject: lastCall,
49
+ actual: lastCall,
50
+ abort: waitForPayloadOnNot && !hasPendingPayloadCandidate,
51
+ };
52
+ }, waitForPayloadOnNot ? false : isNot, { ...options, wait: (isNot && !waitForPayloadOnNot) ? 0 : options.wait });
53
+ const message = enhanceError('mock', minifyRequestedWith(expectedValue), minifyRequestMock(actual, expectedValue, parseCache) || 'was not called', this, verb, expectation, '', options) + (!pass && !isNot && payloadNeverCollected ? payloadCollectionHint(expectedValue) : '');
34
54
  const result = {
35
55
  pass,
36
56
  message: () => message
@@ -43,6 +63,18 @@ export async function toBeRequestedWith(received, expectedValue = {}, options =
43
63
  });
44
64
  return result;
45
65
  }
66
+ const payloadCollectionHint = (expectedValue) => {
67
+ const fields = [
68
+ expectedValue.postData !== undefined ? 'postData' : undefined,
69
+ expectedValue.response !== undefined ? 'response' : undefined,
70
+ ].filter(Boolean).join(' and ');
71
+ return `
72
+
73
+ A request matched every other criterion, but its body was never collected, so ${fields} could not be compared.
74
+ This usually means either:
75
+ - webdriverio is older than v9.28.0, which did not populate \`mock.calls[].postData\` (upgrade to v9.28.0 or later), or
76
+ - body collection is turned off via \`maxSpyCollectedBodySize: 0\` in your config (it defaults to 10MB - remove the override or raise it).`;
77
+ };
46
78
  const methodMatcher = (method, expected) => {
47
79
  if (expected === undefined) {
48
80
  return true;
@@ -87,7 +119,69 @@ const headersMatcher = (headers, expected) => {
87
119
  }
88
120
  return equals(headers, expected);
89
121
  };
90
- const minifyRequestMock = (requestMock, requestedWith) => {
122
+ const bodyMatcher = (body, expected, parseCache) => {
123
+ if (typeof expected === 'undefined') {
124
+ return true;
125
+ }
126
+ if (typeof expected === 'function') {
127
+ return expected(body);
128
+ }
129
+ if (typeof body === 'undefined') {
130
+ return false;
131
+ }
132
+ let parsedBody = body;
133
+ if (body instanceof Buffer) {
134
+ parsedBody = body.toString();
135
+ }
136
+ if (typeof parsedBody === 'string' && isExpectedJsonLike(expected)) {
137
+ const parsed = parseJsonOnce(parsedBody, parseCache);
138
+ if (!parsed.ok) {
139
+ return false;
140
+ }
141
+ parsedBody = parsed.value;
142
+ }
143
+ return equals(parsedBody, expected);
144
+ };
145
+ const JSON_LIKE_ANY_CONSTRUCTORS = new Set([Number, Boolean, Array, Object]);
146
+ const isExpectedJsonLike = (expected) => {
147
+ if (typeof expected === 'undefined') {
148
+ return false;
149
+ }
150
+ const actualSample = isAsymmetricMatcher(expected)
151
+ ? getAsymmetricMatcherValue(expected)
152
+ : expected;
153
+ return (actualSample === null ||
154
+ typeof actualSample === 'boolean' ||
155
+ typeof actualSample === 'number' ||
156
+ Array.isArray(actualSample) ||
157
+ JSON_LIKE_ANY_CONSTRUCTORS.has(actualSample) ||
158
+ (typeof actualSample === 'object' &&
159
+ actualSample !== null &&
160
+ actualSample instanceof RegExp === false));
161
+ };
162
+ const parseJsonOnce = (jsonString, cache) => {
163
+ const cached = cache.get(jsonString);
164
+ if (cached) {
165
+ return cached;
166
+ }
167
+ let result;
168
+ try {
169
+ result = { ok: true, value: JSON.parse(jsonString) };
170
+ }
171
+ catch {
172
+ result = { ok: false };
173
+ }
174
+ cache.set(jsonString, result);
175
+ return result;
176
+ };
177
+ const resolveBodyForDisplay = (raw, expectedForField, parseCache) => {
178
+ if (typeof raw !== 'string' || !isExpectedJsonLike(expectedForField)) {
179
+ return raw;
180
+ }
181
+ const parsed = parseJsonOnce(raw, parseCache);
182
+ return parsed.ok ? parsed.value : raw;
183
+ };
184
+ const minifyRequestMock = (requestMock, requestedWith = {}, parseCache = new Map()) => {
91
185
  if (requestMock === undefined) {
92
186
  return requestMock;
93
187
  }
@@ -96,6 +190,8 @@ const minifyRequestMock = (requestMock, requestedWith) => {
96
190
  method: requestMock.request.method,
97
191
  requestHeaders: requestMock.request.headers,
98
192
  responseHeaders: requestMock.response.headers,
193
+ postData: resolveBodyForDisplay(requestMock.postData, requestedWith.postData, parseCache),
194
+ response: resolveBodyForDisplay(requestMock.body, requestedWith.response, parseCache),
99
195
  };
100
196
  deleteUndefinedValues(r, requestedWith);
101
197
  return minifyRequestedWith(r);
package/lib/utils.js CHANGED
@@ -21,6 +21,12 @@ export function isStringContainingMatcherLike(expected) {
21
21
  export function isInversedStringContainingMatcher(expected) {
22
22
  return isStringContainingMatcherLike(expected) && expected.inverse === true;
23
23
  }
24
+ export function isStringMatchingMatcherLike(expected) {
25
+ return !!expected && expected.constructor.name === 'StringMatching';
26
+ }
27
+ export function isInversedStringMatchingMatcher(expected) {
28
+ return isStringMatchingMatcherLike(expected) && expected.inverse === true;
29
+ }
24
30
  export function getStringAsymmetricMatcherValue(expected) {
25
31
  if ('expected' in expected) {
26
32
  return expected.expected;
@@ -116,16 +122,30 @@ export const compareText = (actual, expected, { ignoreCase = false, trim = true,
116
122
  actual = replaceActual(replace, actual);
117
123
  }
118
124
  if (ignoreCase) {
119
- actual = actual.toLowerCase();
120
125
  if (typeof expected === 'string') {
126
+ actual = actual.toLowerCase();
121
127
  expected = expected.toLowerCase();
122
128
  }
129
+ else if (expected instanceof RegExp) {
130
+ expected = withIgnoreCaseFlag(expected);
131
+ }
123
132
  else if (isStringContainingMatcherLike(expected)) {
133
+ actual = actual.toLowerCase();
124
134
  const sample = getStringAsymmetricMatcherValue(expected).toString().toLocaleLowerCase();
125
135
  expected = (isInversedStringContainingMatcher(expected)
126
136
  ? expect.not.stringContaining(sample)
127
137
  : expect.stringContaining(sample));
128
138
  }
139
+ else if (isStringMatchingMatcherLike(expected)) {
140
+ const sample = getStringAsymmetricMatcherValue(expected);
141
+ const caseInsensitiveSample = withIgnoreCaseFlag(sample instanceof RegExp ? sample : new RegExp(sample));
142
+ expected = (isInversedStringMatchingMatcher(expected)
143
+ ? expect.not.stringMatching(caseInsensitiveSample)
144
+ : expect.stringMatching(caseInsensitiveSample));
145
+ }
146
+ else {
147
+ actual = actual.toLowerCase();
148
+ }
129
149
  }
130
150
  if (isAsymmetricMatcher(expected)) {
131
151
  const result = expected.asymmetricMatch(actual);
@@ -158,7 +178,7 @@ export const compareText = (actual, expected, { ignoreCase = false, trim = true,
158
178
  success: actual.endsWith(expected),
159
179
  };
160
180
  }
161
- if (atIndex) {
181
+ if (atIndex !== undefined) {
162
182
  return {
163
183
  actual,
164
184
  success: actual.substring(atIndex, actual.length).startsWith(expected),
@@ -182,24 +202,38 @@ export const compareTextWithArray = (actual, expectedArray, { ignoreCase = false
182
202
  if (Array.isArray(replace)) {
183
203
  actual = replaceActual(replace, actual);
184
204
  }
205
+ const actualOriginalCase = actual;
185
206
  if (ignoreCase) {
186
207
  actual = actual.toLowerCase();
187
208
  expectedArray = expectedArray.map((item) => {
188
209
  if (typeof item === 'string') {
189
210
  return item.toLowerCase();
190
211
  }
212
+ if (item instanceof RegExp) {
213
+ return withIgnoreCaseFlag(item);
214
+ }
191
215
  if (isStringContainingMatcherLike(item)) {
192
216
  const sample = getStringAsymmetricMatcherValue(item).toString().toLocaleLowerCase();
193
217
  return (isInversedStringContainingMatcher(item)
194
218
  ? expect.not.stringContaining(sample)
195
219
  : expect.stringContaining(sample));
196
220
  }
221
+ if (isStringMatchingMatcherLike(item)) {
222
+ const sample = getStringAsymmetricMatcherValue(item);
223
+ const caseInsensitiveSample = withIgnoreCaseFlag(sample instanceof RegExp ? sample : new RegExp(sample));
224
+ return (isInversedStringMatchingMatcher(item)
225
+ ? expect.not.stringMatching(caseInsensitiveSample)
226
+ : expect.stringMatching(caseInsensitiveSample));
227
+ }
197
228
  return item;
198
229
  });
199
230
  }
200
231
  const hasFoundTextInArray = expectedArray.some((expected) => {
201
232
  if (expected instanceof RegExp) {
202
- return !!actual.match(expected);
233
+ return !!actualOriginalCase.match(expected);
234
+ }
235
+ if (isStringMatchingMatcherLike(expected) && getStringAsymmetricMatcherValue(expected) instanceof RegExp) {
236
+ return expected.asymmetricMatch(actualOriginalCase);
203
237
  }
204
238
  if (isAsymmetricMatcher(expected)) {
205
239
  return expected.asymmetricMatch(actual);
@@ -213,13 +247,13 @@ export const compareTextWithArray = (actual, expectedArray, { ignoreCase = false
213
247
  if (atEnd) {
214
248
  return actual.endsWith(expected);
215
249
  }
216
- if (atIndex) {
250
+ if (atIndex !== undefined) {
217
251
  return actual.substring(atIndex, actual.length).startsWith(expected);
218
252
  }
219
253
  return actual === expected;
220
254
  });
221
255
  return {
222
- actual,
256
+ actual: actualOriginalCase,
223
257
  success: hasFoundTextInArray,
224
258
  };
225
259
  };
@@ -250,31 +284,33 @@ export const compareStyle = async (actualEl, style, { ignoreCase = false, trim =
250
284
  actualVal = actualVal.toLowerCase();
251
285
  expectedVal = expectedVal.toLowerCase();
252
286
  }
287
+ let matches;
253
288
  if (containing) {
254
- success = actualVal.includes(expectedVal);
289
+ matches = actualVal.includes(expectedVal);
255
290
  actual[key] = actualVal;
256
291
  }
257
292
  else if (atStart) {
258
- success = actualVal.startsWith(expectedVal);
293
+ matches = actualVal.startsWith(expectedVal);
259
294
  actual[key] = actualVal;
260
295
  }
261
296
  else if (atEnd) {
262
- success = actualVal.endsWith(expectedVal);
297
+ matches = actualVal.endsWith(expectedVal);
263
298
  actual[key] = actualVal;
264
299
  }
265
- else if (atIndex) {
266
- success = actualVal.substring(atIndex, actualVal.length).startsWith(expectedVal);
300
+ else if (atIndex !== undefined) {
301
+ matches = actualVal.substring(atIndex, actualVal.length).startsWith(expectedVal);
267
302
  actual[key] = actualVal;
268
303
  }
269
304
  else if (replace) {
270
305
  const replacedActual = replaceActual(replace, actualVal);
271
- success = replacedActual === expectedVal;
306
+ matches = replacedActual === expectedVal;
272
307
  actual[key] = replacedActual;
273
308
  }
274
309
  else {
275
- success = success && actualVal === expectedVal;
310
+ matches = actualVal === expectedVal;
276
311
  actual[key] = css.value;
277
312
  }
313
+ success = success && matches;
278
314
  }
279
315
  return {
280
316
  actual,
@@ -282,6 +318,13 @@ export const compareStyle = async (actualEl, style, { ignoreCase = false, trim =
282
318
  };
283
319
  };
284
320
  export { compareNumbers, enhanceError, executeCommandBe, waitUntil, wrapExpectedWithArray };
321
+ function withIgnoreCaseFlag(expected) {
322
+ const cloned = expected.ignoreCase
323
+ ? new RegExp(expected.source, expected.flags)
324
+ : new RegExp(expected.source, `${expected.flags}i`);
325
+ cloned.lastIndex = expected.lastIndex;
326
+ return cloned;
327
+ }
285
328
  function replaceActual(replace, actual) {
286
329
  const hasMultipleReplacers = replace.every((r) => Array.isArray(r));
287
330
  const replacers = hasMultipleReplacers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expect-webdriverio",
3
- "version": "6.0.6",
3
+ "version": "6.0.8",
4
4
  "author": "Mykola Grybyk <mykola.grybyk@gmail.com>",
5
5
  "description": "WebdriverIO Assertion Library",
6
6
  "license": "MIT",
@@ -45,7 +45,10 @@
45
45
  ],
46
46
  "./types": "./types/expect-global.d.ts",
47
47
  "./expect-global": "./types/expect-global.d.ts",
48
- "./api": "./lib/api/index.js"
48
+ "./api": {
49
+ "types": "./types/expect-webdriverio.d.ts",
50
+ "import": "./lib/api/index.js"
51
+ }
49
52
  },
50
53
  "types": "./types/expect-webdriverio.d.ts",
51
54
  "typeScriptVersion": "3.8.3",
@@ -70,6 +73,7 @@
70
73
  "ts:jasmine-global-expect-async": "cd test-types/jasmine-global-expect-async && vitest --typecheck --typecheck.tsconfig ./tsconfig.json --run",
71
74
  "ts:jest:@types-jest": "cd test-types/jest-@types_jest && vitest --typecheck --typecheck.tsconfig ./tsconfig.json --run",
72
75
  "ts:mocha": "cd test-types/mocha && vitest --typecheck --typecheck.tsconfig ./tsconfig.json --run",
76
+ "ts:arethetypeswrong": "npx @arethetypeswrong/cli --pack . --profile esm-only",
73
77
  "checks:all": "npm run build && npm run compile && npm run tsc:root-types && npm run test && npm run ts",
74
78
  "watch": "npm run compile -- --watch",
75
79
  "prepare": "husky",
@@ -85,6 +89,7 @@
85
89
  "jest-matcher-utils": "^30.4.1"
86
90
  },
87
91
  "devDependencies": {
92
+ "@arethetypeswrong/cli": "^0.18.5",
88
93
  "@jest/globals": "^30.4.1",
89
94
  "@types/debug": "^4.1.13",
90
95
  "@types/jasmine": "^5.1.15",
@@ -108,7 +113,7 @@
108
113
  "peerDependencies": {
109
114
  "@wdio/globals": "^9.0.0",
110
115
  "@wdio/logger": "^9.0.0",
111
- "webdriverio": "^9.0.0"
116
+ "webdriverio": "^9.28.0"
112
117
  },
113
118
  "peerDependenciesMeta": {
114
119
  "@wdio/globals": {
@@ -22,7 +22,6 @@ type ExpectLibExpectationResult = import('expect').ExpectationResult
22
22
  type ExpectLibMatcherContext = import('expect').MatcherContext
23
23
  type MatchersObject = Parameters<typeof import('expect').expect.extend>[0]
24
24
  type ExpectLibAnything = ReturnType<typeof expect.any> | ReturnType<typeof expect.anything>
25
- type WdioSome<T> = import('expect-webdriverio/api').WdioSome<T>
26
25
 
27
26
  // Extracted from the expect library, this is the type of the matcher function used in the expect library.
28
27
  type RawMatcherFn<Context extends ExpectLibMatcherContext = ExpectLibMatcherContext> = {
@@ -30,6 +29,10 @@ type RawMatcherFn<Context extends ExpectLibMatcherContext = ExpectLibMatcherCont
30
29
  (this: Context, actual: any, ...expected: Array<any>): ExpectLibExpectationResult;
31
30
  }
32
31
 
32
+ interface WdioSome<T> {
33
+ readonly elements: T
34
+ }
35
+
33
36
  /**
34
37
  * Indicates that a value can be either one T or an array of T.
35
38
  */
@@ -1233,11 +1236,17 @@ declare namespace ExpectWebdriverIO {
1233
1236
  | ((headers: Record<string, string>) => boolean)
1234
1237
  postData?:
1235
1238
  | string
1239
+ | boolean
1240
+ | number
1241
+ | null
1236
1242
  | ExpectWebdriverIO.JsonCompatible
1237
1243
  | ExpectWebdriverIO.PartialMatcher<string | ExpectWebdriverIO.JsonCompatible>
1238
1244
  | ((postData: string | undefined) => boolean)
1239
1245
  response?:
1240
1246
  | string
1247
+ | boolean
1248
+ | number
1249
+ | null
1241
1250
  | ExpectWebdriverIO.JsonCompatible
1242
1251
  | ExpectWebdriverIO.PartialMatcher<string | ExpectWebdriverIO.JsonCompatible>
1243
1252
  | ((response: unknown) => boolean)
@@ -1274,9 +1283,6 @@ declare module 'expect-webdriverio' {
1274
1283
  * Required since importing from 'expect-webdriverio' can trigger double initialization.
1275
1284
  */
1276
1285
  declare module 'expect-webdriverio/api' {
1277
- export interface WdioSome<T> {
1278
- readonly elements: T
1279
- }
1280
1286
  /**
1281
1287
  * Quantifier modifier. Wraps a `$$()` result so that the matcher passes
1282
1288
  * when at least one element satisfies the condition (∃ semantics).