expect-webdriverio 6.0.7 → 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.7",
3
+ "version": "6.0.8",
4
4
  "author": "Mykola Grybyk <mykola.grybyk@gmail.com>",
5
5
  "description": "WebdriverIO Assertion Library",
6
6
  "license": "MIT",
@@ -113,7 +113,7 @@
113
113
  "peerDependencies": {
114
114
  "@wdio/globals": "^9.0.0",
115
115
  "@wdio/logger": "^9.0.0",
116
- "webdriverio": "^9.0.0"
116
+ "webdriverio": "^9.28.0"
117
117
  },
118
118
  "peerDependenciesMeta": {
119
119
  "@wdio/globals": {
@@ -1236,11 +1236,17 @@ declare namespace ExpectWebdriverIO {
1236
1236
  | ((headers: Record<string, string>) => boolean)
1237
1237
  postData?:
1238
1238
  | string
1239
+ | boolean
1240
+ | number
1241
+ | null
1239
1242
  | ExpectWebdriverIO.JsonCompatible
1240
1243
  | ExpectWebdriverIO.PartialMatcher<string | ExpectWebdriverIO.JsonCompatible>
1241
1244
  | ((postData: string | undefined) => boolean)
1242
1245
  response?:
1243
1246
  | string
1247
+ | boolean
1248
+ | number
1249
+ | null
1244
1250
  | ExpectWebdriverIO.JsonCompatible
1245
1251
  | ExpectWebdriverIO.PartialMatcher<string | ExpectWebdriverIO.JsonCompatible>
1246
1252
  | ((response: unknown) => boolean)