expect-webdriverio 5.7.0 → 6.0.1

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 (41) hide show
  1. package/README.md +16 -14
  2. package/lib/constants.js +4 -0
  3. package/lib/index.js +30 -18
  4. package/lib/matchers/asymmetrics/asymmetricsUtils.js +24 -0
  5. package/lib/matchers/asymmetrics/oneOf.js +51 -0
  6. package/lib/matchers/browser/toHaveClipboardText.js +8 -9
  7. package/lib/matchers/browser/toHaveLocalStorageItem.js +22 -14
  8. package/lib/matchers/browser/toHaveTitle.js +9 -10
  9. package/lib/matchers/browser/toHaveUrl.js +8 -9
  10. package/lib/matchers/element/toBeExisting.js +8 -3
  11. package/lib/matchers/element/toBeSelected.js +6 -13
  12. package/lib/matchers/element/toHaveAttribute.js +46 -45
  13. package/lib/matchers/element/toHaveChildren.js +30 -32
  14. package/lib/matchers/element/toHaveComputedLabel.js +21 -20
  15. package/lib/matchers/element/toHaveComputedRole.js +21 -20
  16. package/lib/matchers/element/toHaveElementClass.js +57 -0
  17. package/lib/matchers/element/toHaveElementProperty.js +40 -32
  18. package/lib/matchers/element/toHaveHTML.js +22 -23
  19. package/lib/matchers/element/toHaveHeight.js +23 -28
  20. package/lib/matchers/element/toHaveSize.js +16 -14
  21. package/lib/matchers/element/toHaveStyle.js +20 -14
  22. package/lib/matchers/element/toHaveText.js +26 -44
  23. package/lib/matchers/element/toHaveWidth.js +23 -28
  24. package/lib/matchers/elements/toBeElementsArrayOfSize.js +25 -30
  25. package/lib/matchers/mock/toBeRequestedTimes.js +12 -17
  26. package/lib/matchers/mock/toBeRequestedWith.js +8 -9
  27. package/lib/matchers/modifiers/some.js +15 -0
  28. package/lib/matchers.js +1 -1
  29. package/lib/tsconfig.build.tsbuildinfo +1 -1
  30. package/lib/util/commandOptionsUtils.js +6 -0
  31. package/lib/util/elementsUtil.js +74 -4
  32. package/lib/util/executeCommand.js +94 -6
  33. package/lib/util/formatMessage.js +87 -32
  34. package/lib/util/numberOptionsUtil.js +92 -0
  35. package/lib/util/refetchElements.js +30 -5
  36. package/lib/util/stringUtil.js +12 -0
  37. package/lib/util/waitUntil.js +44 -0
  38. package/lib/utils.js +84 -91
  39. package/package.json +11 -10
  40. package/types/expect-webdriverio.d.ts +587 -134
  41. package/lib/matchers/element/toHaveClass.js +0 -61
@@ -1,46 +1,41 @@
1
1
  import { DEFAULT_OPTIONS } from '../../constants.js';
2
- import { compareNumbers, enhanceError, executeCommand, numberError, waitUntil, } from '../../utils.js';
3
- async function condition(el, width, options) {
2
+ import { wrapExpectedWithArray } from '../../util/elementsUtil.js';
3
+ import { executeCommandWithStrategy } from '../../util/executeCommand.js';
4
+ import { validateNumberArrayAndExtractOptions } from '../../util/numberOptionsUtil.js';
5
+ import { enhanceError, waitUntil, } from '../../utils.js';
6
+ async function condition(el, expectedNumber) {
4
7
  const actualWidth = await el.getSize('width');
5
8
  return {
6
- result: compareNumbers(actualWidth, options),
7
- value: actualWidth
9
+ success: expectedNumber?.asymmetricMatch(actualWidth) ?? false,
10
+ actual: actualWidth
8
11
  };
9
12
  }
10
13
  export async function toHaveWidth(received, expectedValue, options = DEFAULT_OPTIONS) {
11
- const isNot = this.isNot;
12
- const { expectation = 'width', verb = 'have' } = this;
14
+ const { expectation = 'width', verb = 'have', isNot, matcherName = 'toHaveWidth' } = this;
13
15
  await options.beforeAssertion?.({
14
- matcherName: 'toHaveWidth',
16
+ matcherName,
15
17
  expectedValue,
16
18
  options,
17
19
  });
18
- let numberOptions;
19
- if (typeof expectedValue === 'number') {
20
- numberOptions = { eq: expectedValue };
21
- }
22
- else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) {
23
- throw new Error('Invalid params passed to toHaveHeight.');
24
- }
25
- else {
26
- numberOptions = expectedValue;
27
- }
28
- let el = await received?.getElement();
29
- let actualWidth;
30
- const pass = await waitUntil(async () => {
31
- const result = await executeCommand.call(this, el, condition, numberOptions, [expectedValue, numberOptions]);
32
- el = result.el;
33
- actualWidth = result.values;
34
- return result.success;
35
- }, isNot, { ...numberOptions, ...options });
36
- const error = numberError(numberOptions);
37
- const message = enhanceError(el, error, actualWidth, this, verb, expectation, '', options);
20
+ const { numberMatcher: expectedNumber, commandOptions } = validateNumberArrayAndExtractOptions(expectedValue, options);
21
+ const { success: pass, actual: actualWidth, subject: elements, context: { isSome } = {} } = await waitUntil(async (iteration) => {
22
+ return await executeCommandWithStrategy({
23
+ unresolvedElements: received,
24
+ expectedValues: expectedNumber,
25
+ singleElementCompare: (element, expectedNumber) => condition(element, expectedNumber),
26
+ context: { isNot, iteration },
27
+ strategy: 'NewStrictMultipleElements',
28
+ strictConfiguration: { allowArrayWithSingleElement: false }
29
+ });
30
+ }, isNot, { wait: commandOptions.wait, interval: commandOptions.interval });
31
+ const expectedValues = wrapExpectedWithArray(elements, actualWidth, expectedNumber);
32
+ const message = enhanceError(elements, expectedValues, actualWidth, { isNot, isSome }, verb, expectation, '', commandOptions);
38
33
  const result = {
39
34
  pass,
40
35
  message: () => message
41
36
  };
42
37
  await options.afterAssertion?.({
43
- matcherName: 'toHaveWidth',
38
+ matcherName,
44
39
  expectedValue,
45
40
  options,
46
41
  result
@@ -1,47 +1,42 @@
1
- import { waitUntil, enhanceError, compareNumbers, numberError } from '../../utils.js';
2
- import { refetchElements } from '../../util/refetchElements.js';
1
+ import { waitUntil, enhanceError, } from '../../utils.js';
2
+ import { refetchElements, syncronizeElements } from '../../util/refetchElements.js';
3
3
  import { DEFAULT_OPTIONS } from '../../constants.js';
4
+ import { validateNumberAndExtractOptions } from '../../util/numberOptionsUtil.js';
5
+ import { awaitElementArray, isStrictlyElementArray } from '../../util/elementsUtil.js';
4
6
  export async function toBeElementsArrayOfSize(received, expectedValue, options = DEFAULT_OPTIONS) {
5
- const isNot = this.isNot;
6
- const { expectation = 'elements array of size', verb = 'be' } = this;
7
+ const { expectation = 'elements array of size', verb = 'be', isNot, matcherName = 'toBeElementsArrayOfSize' } = this;
7
8
  await options.beforeAssertion?.({
8
- matcherName: 'toBeElementsArrayOfSize',
9
+ matcherName,
9
10
  expectedValue,
10
11
  options,
11
12
  });
12
- let numberOptions;
13
- if (typeof expectedValue === 'number') {
14
- numberOptions = { eq: expectedValue };
15
- }
16
- else if (!expectedValue || (typeof expectedValue.eq !== 'number' && typeof expectedValue.gte !== 'number' && typeof expectedValue.lte !== 'number')) {
17
- throw new Error('Invalid params passed to toBeElementsArrayOfSize.');
18
- }
19
- else {
20
- numberOptions = expectedValue;
21
- }
22
- let elements = await received;
23
- const originalLength = elements.length;
24
- const pass = await waitUntil(async () => {
25
- const isPassing = compareNumbers(elements.length, numberOptions);
26
- if (isPassing) {
27
- return isPassing;
13
+ const { numberMatcher: expectedNumber, commandOptions } = validateNumberAndExtractOptions(expectedValue, options);
14
+ let { elements, other } = await awaitElementArray(received);
15
+ const originalLength = elements ? elements.length : undefined;
16
+ const { success: pass } = await waitUntil(async (iteration) => {
17
+ if (!elements) {
18
+ return { success: false, subject: elements, actual: undefined, abort: true };
19
+ }
20
+ else if (iteration > 0) {
21
+ elements = await refetchElements(elements);
28
22
  }
29
- elements = await refetchElements(elements, numberOptions.wait, true);
30
- return false;
31
- }, isNot, { ...numberOptions, ...options });
32
- if (Array.isArray(received) && pass) {
33
- for (let index = originalLength; index < elements.length; index++) {
34
- received.push(elements[index]);
23
+ const isPassing = expectedNumber.asymmetricMatch(elements.length);
24
+ if (isPassing) {
25
+ return { success: isPassing, subject: elements, actual: elements.length };
35
26
  }
27
+ return { success: false, subject: elements, actual: elements.length };
28
+ }, isNot, { wait: commandOptions.wait, interval: commandOptions.interval });
29
+ if (pass && originalLength !== undefined && elements !== received && (isStrictlyElementArray(received) || received instanceof Promise) && isStrictlyElementArray(elements)) {
30
+ await syncronizeElements(received, elements);
36
31
  }
37
- const error = numberError(numberOptions);
38
- const message = enhanceError(elements, error, originalLength, this, verb, expectation, '', numberOptions);
32
+ const actual = originalLength;
33
+ const message = enhanceError(elements ?? other, expectedNumber, actual, this, verb, expectation, '', commandOptions);
39
34
  const result = {
40
35
  pass,
41
36
  message: () => message
42
37
  };
43
38
  await options.afterAssertion?.({
44
- matcherName: 'toBeElementsArrayOfSize',
39
+ matcherName,
45
40
  expectedValue,
46
41
  options,
47
42
  result
@@ -1,30 +1,25 @@
1
- import { waitUntil, enhanceError, compareNumbers } from '../../utils.js';
2
- import { numberError } from '../../util/formatMessage.js';
1
+ import { waitUntil, enhanceError } from '../../utils.js';
3
2
  import { DEFAULT_OPTIONS } from '../../constants.js';
4
- export async function toBeRequestedTimes(received, expectedValue = {}, options = DEFAULT_OPTIONS) {
5
- const isNot = this.isNot || false;
6
- const { expectation = `called${typeof expectedValue === 'number' ? ' ' + expectedValue : ''} time${expectedValue !== 1 ? 's' : ''}`, verb = 'be' } = this;
3
+ import { validateNumberAndExtractOptions } from '../../util/numberOptionsUtil.js';
4
+ export async function toBeRequestedTimes(received, expectedValue, options = DEFAULT_OPTIONS) {
5
+ const { verb = 'be', isNot, matcherName = 'toBeRequestedTimes', expectation = `called${typeof expectedValue === 'number' ? ' ' + expectedValue : ''} time${expectedValue !== 1 ? 's' : ''}`, } = this;
7
6
  await options.beforeAssertion?.({
8
- matcherName: 'toBeRequestedTimes',
7
+ matcherName,
9
8
  expectedValue,
10
9
  options,
11
10
  });
12
- const numberOptions = typeof expectedValue === 'number'
13
- ? { eq: expectedValue }
14
- : expectedValue || {};
15
- let actual;
16
- const pass = await waitUntil(async () => {
17
- actual = received.calls.length;
18
- return compareNumbers(actual, numberOptions);
19
- }, isNot, { ...numberOptions, ...options });
20
- const error = numberError(numberOptions);
21
- const message = enhanceError('mock', error, actual, this, verb, expectation, '', numberOptions);
11
+ const { numberMatcher: expectedNumber, commandOptions } = validateNumberAndExtractOptions(expectedValue, options);
12
+ const { success: pass, actual } = await waitUntil(async () => {
13
+ const actual = received.calls.length;
14
+ return { success: expectedNumber.asymmetricMatch(actual), subject: received, actual };
15
+ }, isNot, { wait: commandOptions.wait, interval: commandOptions.interval });
16
+ const message = enhanceError('mock', expectedNumber, actual, this, verb, expectation, '', commandOptions);
22
17
  const result = {
23
18
  pass,
24
19
  message: () => message
25
20
  };
26
21
  await options.afterAssertion?.({
27
- matcherName: 'toBeRequestedTimes',
22
+ matcherName,
28
23
  expectedValue,
29
24
  options,
30
25
  result
@@ -10,26 +10,25 @@ function reduceHeaders(headers) {
10
10
  }, {});
11
11
  }
12
12
  export async function toBeRequestedWith(received, expectedValue = {}, options = DEFAULT_OPTIONS) {
13
- const isNot = this.isNot || false;
14
- const { expectation = 'called with', verb = 'be' } = this;
13
+ const { expectation = 'called with', verb = 'be', isNot, matcherName = 'toBeRequestedWith' } = this;
15
14
  await options.beforeAssertion?.({
16
- matcherName: 'toBeRequestedWith',
15
+ matcherName,
17
16
  expectedValue,
18
17
  options,
19
18
  });
20
- let actual;
21
- const pass = await waitUntil(async () => {
19
+ const { success: pass, actual } = await waitUntil(async () => {
20
+ let lastCall;
22
21
  for (const call of received.calls) {
23
- actual = call;
22
+ lastCall = call;
24
23
  if (methodMatcher(call.request.method, expectedValue.method) &&
25
24
  statusCodeMatcher(call.response.status, expectedValue.statusCode) &&
26
25
  urlMatcher(call.request.url, expectedValue.url) &&
27
26
  headersMatcher(reduceHeaders(call.request.headers), expectedValue.requestHeaders) &&
28
27
  headersMatcher(reduceHeaders(call.response.headers), expectedValue.responseHeaders)) {
29
- return true;
28
+ return { success: true, subject: call, actual: call };
30
29
  }
31
30
  }
32
- return false;
31
+ return { success: false, subject: lastCall, actual: lastCall };
33
32
  }, isNot, { ...options, wait: isNot ? 0 : options.wait });
34
33
  const message = enhanceError('mock', minifyRequestedWith(expectedValue), minifyRequestMock(actual, expectedValue) || 'was not called', this, verb, expectation, '', options);
35
34
  const result = {
@@ -37,7 +36,7 @@ export async function toBeRequestedWith(received, expectedValue = {}, options =
37
36
  message: () => message
38
37
  };
39
38
  await options.afterAssertion?.({
40
- matcherName: 'toBeRequestedWith',
39
+ matcherName,
41
40
  expectedValue,
42
41
  options,
43
42
  result
@@ -0,0 +1,15 @@
1
+ const SOME_TAG = 'expect-webdriverio.some';
2
+ const SOME_SYMBOL = Symbol.for(SOME_TAG);
3
+ export class SomeElementsWrapper {
4
+ elements;
5
+ [SOME_SYMBOL] = true;
6
+ constructor(elements) {
7
+ this.elements = elements;
8
+ }
9
+ }
10
+ export function some(elements) {
11
+ return new SomeElementsWrapper(elements);
12
+ }
13
+ export function isSomeWrapper(value) {
14
+ return value instanceof SomeElementsWrapper;
15
+ }
package/lib/matchers.js CHANGED
@@ -12,9 +12,9 @@ export * from './matchers/element/toBeFocused.js';
12
12
  export * from './matchers/element/toBeSelected.js';
13
13
  export * from './matchers/element/toHaveAttribute.js';
14
14
  export * from './matchers/element/toHaveChildren.js';
15
- export * from './matchers/element/toHaveClass.js';
16
15
  export * from './matchers/element/toHaveComputedLabel.js';
17
16
  export * from './matchers/element/toHaveComputedRole.js';
17
+ export * from './matchers/element/toHaveElementClass.js';
18
18
  export * from './matchers/element/toHaveElementProperty.js';
19
19
  export * from './matchers/element/toHaveHeight.js';
20
20
  export * from './matchers/element/toHaveHref.js';
@@ -1 +1 @@
1
- {"root":["../src/constants.ts","../src/index.ts","../src/jasmineUtils.ts","../src/matchers.ts","../src/snapshot.ts","../src/softAssert.ts","../src/softAssertService.ts","../src/softExpect.ts","../src/types.ts","../src/utils.ts","../src/matchers/snapshot.ts","../src/matchers/browser/toHaveClipboardText.ts","../src/matchers/browser/toHaveLocalStorageItem.ts","../src/matchers/browser/toHaveTitle.ts","../src/matchers/browser/toHaveUrl.ts","../src/matchers/element/toBeClickable.ts","../src/matchers/element/toBeDisabled.ts","../src/matchers/element/toBeDisplayed.ts","../src/matchers/element/toBeDisplayedInViewport.ts","../src/matchers/element/toBeEnabled.ts","../src/matchers/element/toBeExisting.ts","../src/matchers/element/toBeFocused.ts","../src/matchers/element/toBeSelected.ts","../src/matchers/element/toHaveAttribute.ts","../src/matchers/element/toHaveChildren.ts","../src/matchers/element/toHaveClass.ts","../src/matchers/element/toHaveComputedLabel.ts","../src/matchers/element/toHaveComputedRole.ts","../src/matchers/element/toHaveElementProperty.ts","../src/matchers/element/toHaveHTML.ts","../src/matchers/element/toHaveHeight.ts","../src/matchers/element/toHaveHref.ts","../src/matchers/element/toHaveId.ts","../src/matchers/element/toHaveSize.ts","../src/matchers/element/toHaveStyle.ts","../src/matchers/element/toHaveText.ts","../src/matchers/element/toHaveValue.ts","../src/matchers/element/toHaveWidth.ts","../src/matchers/elements/toBeElementsArrayOfSize.ts","../src/matchers/mock/toBeRequested.ts","../src/matchers/mock/toBeRequestedTimes.ts","../src/matchers/mock/toBeRequestedWith.ts","../src/util/elementsUtil.ts","../src/util/executeCommand.ts","../src/util/formatMessage.ts","../src/util/refetchElements.ts"],"version":"5.9.3"}
1
+ {"root":["../src/constants.ts","../src/index.ts","../src/jasmineUtils.ts","../src/matchers.ts","../src/snapshot.ts","../src/softAssert.ts","../src/softAssertService.ts","../src/softExpect.ts","../src/types.ts","../src/utils.ts","../src/matchers/snapshot.ts","../src/matchers/asymmetrics/asymmetricsUtils.ts","../src/matchers/asymmetrics/oneOf.ts","../src/matchers/browser/toHaveClipboardText.ts","../src/matchers/browser/toHaveLocalStorageItem.ts","../src/matchers/browser/toHaveTitle.ts","../src/matchers/browser/toHaveUrl.ts","../src/matchers/element/toBeClickable.ts","../src/matchers/element/toBeDisabled.ts","../src/matchers/element/toBeDisplayed.ts","../src/matchers/element/toBeDisplayedInViewport.ts","../src/matchers/element/toBeEnabled.ts","../src/matchers/element/toBeExisting.ts","../src/matchers/element/toBeFocused.ts","../src/matchers/element/toBeSelected.ts","../src/matchers/element/toHaveAttribute.ts","../src/matchers/element/toHaveChildren.ts","../src/matchers/element/toHaveComputedLabel.ts","../src/matchers/element/toHaveComputedRole.ts","../src/matchers/element/toHaveElementClass.ts","../src/matchers/element/toHaveElementProperty.ts","../src/matchers/element/toHaveHTML.ts","../src/matchers/element/toHaveHeight.ts","../src/matchers/element/toHaveHref.ts","../src/matchers/element/toHaveId.ts","../src/matchers/element/toHaveSize.ts","../src/matchers/element/toHaveStyle.ts","../src/matchers/element/toHaveText.ts","../src/matchers/element/toHaveValue.ts","../src/matchers/element/toHaveWidth.ts","../src/matchers/elements/toBeElementsArrayOfSize.ts","../src/matchers/mock/toBeRequested.ts","../src/matchers/mock/toBeRequestedTimes.ts","../src/matchers/mock/toBeRequestedWith.ts","../src/matchers/modifiers/some.ts","../src/util/commandOptionsUtils.ts","../src/util/elementsUtil.ts","../src/util/executeCommand.ts","../src/util/formatMessage.ts","../src/util/numberOptionsUtil.ts","../src/util/refetchElements.ts","../src/util/stringUtil.ts","../src/util/waitUntil.ts"],"version":"5.9.3"}
@@ -0,0 +1,6 @@
1
+ export const isDefinedPlainObject = (obj) => {
2
+ if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
3
+ return true;
4
+ }
5
+ return false;
6
+ };
@@ -1,9 +1,79 @@
1
- export const wrapExpectedWithArray = (el, actual, expected) => {
2
- if (Array.isArray(el) && el.length > 1 && Array.isArray(actual)) {
3
- expected = [expected];
1
+ export const wrapExpectedWithArray = (elements, actual, expected) => {
2
+ if (Array.isArray(elements) && Array.isArray(actual) && !Array.isArray(expected)) {
3
+ expected = Array(actual.length).fill(expected);
4
4
  }
5
5
  return expected;
6
6
  };
7
+ export const fillSingleExpectedForElementArray = (subject, value) => {
8
+ if (isElementArrayLike(subject) && !Array.isArray(value)) {
9
+ const fillerlength = subject.length > 0 ? subject.length : 1;
10
+ return Array(fillerlength).fill(value);
11
+ }
12
+ return value;
13
+ };
14
+ export const isArray = (obj) => {
15
+ return Array.isArray(obj);
16
+ };
17
+ export const isSelector = (obj) => {
18
+ return !!obj
19
+ && typeof obj === 'object'
20
+ && 'selector' in obj
21
+ && 'parent' in obj;
22
+ };
7
23
  export const isElementArray = (obj) => {
8
- return obj !== null && typeof obj === 'object' && 'selector' in obj && 'foundWith' in obj && 'parent' in obj;
24
+ return isSelector(obj)
25
+ && 'foundWith' in obj;
26
+ };
27
+ export const isStrictlyElementArray = (obj) => {
28
+ return isElementArray(obj)
29
+ && Array.isArray(obj)
30
+ && 'getElements' in obj;
31
+ };
32
+ export const isElement = (obj) => {
33
+ return isSelector(obj)
34
+ && !Array.isArray(obj)
35
+ && 'getElement' in obj;
36
+ };
37
+ export const isElementArrayLike = (obj) => {
38
+ return !!obj && (isStrictlyElementArray(obj) || (Array.isArray(obj) && obj.every(isElement)));
39
+ };
40
+ export const isArrayOfElement = (obj) => {
41
+ return Array.isArray(obj) && obj.every(isElement);
42
+ };
43
+ export const isElementOrArrayLike = (obj) => {
44
+ return !!obj && (isElement(obj) || isElementArrayLike(obj));
45
+ };
46
+ export const awaitElementOrArray = async (received) => {
47
+ if (!received || typeof received !== 'object') {
48
+ return { other: received };
49
+ }
50
+ let awaitedElements = received;
51
+ if (awaitedElements instanceof Promise) {
52
+ awaitedElements = await awaitedElements;
53
+ }
54
+ if (!isElementOrArrayLike(awaitedElements)) {
55
+ return { other: awaitedElements };
56
+ }
57
+ if ('getElement' in awaitedElements) {
58
+ const element = await awaitedElements.getElement();
59
+ return { selector: element, element };
60
+ }
61
+ if ('getElements' in awaitedElements) {
62
+ const elements = await awaitedElements.getElements();
63
+ return { selector: elements, elements, isEmptyElements: elements.length === 0 };
64
+ }
65
+ return { selector: awaitedElements, elements: awaitedElements, isEmptyElements: awaitedElements.length === 0 };
66
+ };
67
+ export const awaitElementArray = async (received) => {
68
+ let awaitedElements = received;
69
+ if (awaitedElements instanceof Promise) {
70
+ awaitedElements = await awaitedElements;
71
+ }
72
+ if (!isElementArrayLike(awaitedElements)) {
73
+ return { other: awaitedElements };
74
+ }
75
+ if ('getElements' in awaitedElements) {
76
+ return { elements: await awaitedElements.getElements() };
77
+ }
78
+ return { elements: awaitedElements };
9
79
  };
@@ -1,8 +1,96 @@
1
- export async function executeCommand(el, condition, options = {}, params = []) {
2
- const result = await condition(el, ...params, options);
1
+ import { isSomeWrapper } from '../matchers/modifiers/some.js';
2
+ import { awaitElementOrArray, isElement, isStrictlyElementArray } from './elementsUtil.js';
3
+ import { refreshElementArray } from './refetchElements.js';
4
+ export async function executeCommandWithStrategy({ unresolvedElements, expectedValues, singleElementCompare, context: { isNot, iteration }, strategy = 'NewStrictMultipleElements', strictConfiguration = { allowEmptyElements: false, allowArrayWithSingleElement: false } }) {
5
+ const isSome = isSomeWrapper(unresolvedElements);
6
+ if (strategy === 'LegacyLooseMultipleElements') {
7
+ if (isSome) {
8
+ throw new Error('some(elements) works only when enabling `useToHaveTextStrictMultiElementsCompareStrategy`');
9
+ }
10
+ return legacyMultipleElementResultsStrategy(unresolvedElements, expectedValues, singleElementCompare, isNot);
11
+ }
12
+ const actualReceived = isSome ? unresolvedElements.elements : unresolvedElements;
13
+ return multipleElementResultsStrategy(actualReceived, expectedValues, singleElementCompare, { isNot, isSome, iteration }, strictConfiguration);
14
+ }
15
+ export const legacyMultipleElementResultsStrategy = async (unresolvedElements, expectedValues, singleElementCompare, _isNot) => {
16
+ const { selector, other, isEmptyElements } = await awaitElementOrArray(unresolvedElements);
17
+ const subject = selector ?? other;
18
+ if (!selector || isEmptyElements) {
19
+ return {
20
+ subject: subject,
21
+ success: false,
22
+ actual: undefined,
23
+ abort: true,
24
+ };
25
+ }
26
+ if (isElement(selector)) {
27
+ const compareResult = await singleElementCompare(selector, expectedValues);
28
+ return {
29
+ subject,
30
+ ...compareResult,
31
+ };
32
+ }
33
+ const settled = await Promise.allSettled(Array.from(selector).map((element, index) => singleElementCompare(element, expectedValues, index)));
34
+ const firstRejection = settled.find((r) => r.status === 'rejected');
35
+ if (firstRejection) {
36
+ throw firstRejection.reason;
37
+ }
38
+ const results = settled.map((r) => r.value);
3
39
  return {
4
- el,
5
- success: result.result === true,
6
- values: result.value
40
+ subject,
41
+ success: results.length > 0 && results.every((res) => res.success === true),
42
+ actual: results.map(({ actual: value }) => value),
7
43
  };
8
- }
44
+ };
45
+ export const multipleElementResultsStrategy = async (unresolvedElements, expectedValues, singleElementCompare, { isNot, isSome, iteration }, { allowEmptyElements = false, allowArrayWithSingleElement = false } = {}) => {
46
+ const { selector, other } = await awaitElementOrArray(unresolvedElements);
47
+ if (iteration > 0 && isStrictlyElementArray(selector)) {
48
+ await refreshElementArray(selector);
49
+ }
50
+ const subject = selector ?? other;
51
+ if (!selector || (Array.isArray(selector) && selector.length === 0)) {
52
+ return {
53
+ subject,
54
+ success: isNot ? !allowEmptyElements : false,
55
+ actual: undefined,
56
+ abort: !allowEmptyElements && !isStrictlyElementArray(selector),
57
+ context: { isSome },
58
+ };
59
+ }
60
+ if (isElement(selector)) {
61
+ const forceFailure = !allowArrayWithSingleElement && Array.isArray(expectedValues);
62
+ const compareResult = await singleElementCompare(selector, forceFailure ? undefined : expectedValues);
63
+ const success = forceFailure ? !!isNot : compareResult.success;
64
+ return { subject, success, actual: compareResult.actual, abort: forceFailure, context: { isSome } };
65
+ }
66
+ const lengthMismatch = Array.isArray(expectedValues) && expectedValues.length !== selector.length;
67
+ const settled = await Promise.allSettled(Array.from(selector).map(async (element, index) => {
68
+ const indexedExpected = Array.isArray(expectedValues) ? expectedValues[index] : expectedValues;
69
+ const forceElementFailure = Array.isArray(indexedExpected)
70
+ || (lengthMismatch && Array.isArray(expectedValues) && index >= expectedValues.length);
71
+ const compareResult = await singleElementCompare(element, forceElementFailure ? undefined : indexedExpected, index);
72
+ return forceElementFailure ? { success: false, actual: compareResult.actual } : compareResult;
73
+ }));
74
+ const firstRejection = settled.find((r) => r.status === 'rejected');
75
+ if (firstRejection) {
76
+ throw firstRejection.reason;
77
+ }
78
+ const results = settled.map((r) => r.value);
79
+ if (Array.isArray(expectedValues) && expectedValues.length > selector.length) {
80
+ results.push(...Array(expectedValues.length - selector.length).fill({ success: false, actual: undefined }));
81
+ }
82
+ if (lengthMismatch) {
83
+ return { subject, success: !!isNot, actual: results.map(({ actual }) => actual), context: { isSome } };
84
+ }
85
+ const isNotEmpty = results.length > 0;
86
+ const checkFn = isSome ? isAtLeastOneTrue : isAllTrue;
87
+ const checkNotFn = isSome ? isAtLeastOneFalse : isAllFalse;
88
+ const success = isNot
89
+ ? !(isNotEmpty && checkNotFn(results))
90
+ : isNotEmpty && checkFn(results);
91
+ return { subject, success, actual: results.map(({ actual }) => actual), context: { isSome } };
92
+ };
93
+ const isAllTrue = (results) => results.every((res) => res.success === true);
94
+ const isAllFalse = (results) => results.every((res) => res.success === false);
95
+ const isAtLeastOneTrue = (results) => results.some((res) => res.success === true);
96
+ const isAtLeastOneFalse = (results) => results.some((res) => res.success === false);
@@ -1,6 +1,9 @@
1
- import { printDiffOrStringify, printExpected, printReceived } from 'jest-matcher-utils';
1
+ import { printDiffOrStringify, printExpected, printReceived, RECEIVED_COLOR, EXPECTED_COLOR, INVERTED_COLOR, stringify } from 'jest-matcher-utils';
2
2
  import { equals } from '../jasmineUtils.js';
3
- import { isElementArray } from './elementsUtil.js';
3
+ import { isArrayOfElement, isElementArrayLike, isElementOrArrayLike, isStrictlyElementArray } from './elementsUtil.js';
4
+ import { toJsonString } from './stringUtil.js';
5
+ import { isJasmineStringAsymmetricMatcher, toArray } from '../utils.js';
6
+ export const isDefined = (value) => value !== null && value !== undefined;
4
7
  export const getSelector = (el) => {
5
8
  let result = typeof el.selector === 'string' ? el.selector : '<fn>';
6
9
  if (Array.isArray(el) && el.props.length > 0) {
@@ -9,27 +12,36 @@ export const getSelector = (el) => {
9
12
  return result;
10
13
  };
11
14
  export const getSelectors = (el) => {
15
+ if (!el || typeof el !== 'object') {
16
+ return '';
17
+ }
12
18
  const selectors = [];
13
19
  let parent;
14
- if (isElementArray(el)) {
20
+ if (isStrictlyElementArray(el)) {
15
21
  selectors.push(`${(el).foundWith}(\`${getSelector(el)}\`)`);
16
22
  parent = el.parent;
17
23
  }
18
- else if (!Array.isArray(el)) {
24
+ else if (isArrayOfElement(el)) {
25
+ return `[${el.map(getSelectors).join(',')}]`;
26
+ }
27
+ else {
19
28
  parent = el;
20
29
  }
21
- while (parent && 'selector' in parent) {
30
+ while (!!parent && typeof parent === 'object' && 'selector' in parent) {
22
31
  const selector = getSelector(parent);
23
- const index = parent.index ? `[${parent.index}]` : '';
24
- selectors.push(`${parent.index ? '$' : ''}$(\`${selector}\`)${index}`);
32
+ const index = isDefined(parent.index) ? `[${parent.index}]` : '';
33
+ selectors.push(`${isDefined(parent.index) ? '$' : ''}$(\`${selector}\`)${index}`);
25
34
  parent = parent.parent;
26
35
  }
27
36
  return selectors.reverse().join('.');
28
37
  };
29
38
  const not = (isNot) => `${isNot ? 'not ' : ''}`;
30
39
  export const enhanceError = (subject, expected, actual, context, verb, expectation, expectedValueArgument2 = '', { message = '', containing = false } = {}) => {
31
- const { isNot = false, useNotInLabel = true } = context;
32
- subject = typeof subject === 'string' ? subject : getSelectors(subject);
40
+ const { isNot, useNotInLabel = true } = context;
41
+ let subjectStr = (isElementOrArrayLike(subject) ? getSelectors(subject) : toJsonString(subject));
42
+ if (subjectStr.length > 100) {
43
+ subjectStr = `${subjectStr.substring(0, 100)}...`;
44
+ }
33
45
  let contain = '';
34
46
  if (containing) {
35
47
  contain = ' containing';
@@ -37,44 +49,87 @@ export const enhanceError = (subject, expected, actual, context, verb, expectati
37
49
  if (verb) {
38
50
  verb += ' ';
39
51
  }
52
+ const isNotInLabel = useNotInLabel && isNot;
40
53
  const label = {
41
- expected: isNot && useNotInLabel ? 'Expected [not]' : 'Expected',
42
- received: isNot && useNotInLabel ? 'Received ' : 'Received'
54
+ expected: isNotInLabel ? 'Expected [not]' : 'Expected',
55
+ received: isNotInLabel ? 'Received ' : 'Received'
43
56
  };
44
- const diffString = equals(actual, expected) ? `\
57
+ let diffString = '';
58
+ if (isJasmineStringAsymmetricMatcher(expected)) {
59
+ expected = expected.jasmineToString();
60
+ }
61
+ else if (isElementOrArrayLike(subject) && Array.isArray(expected)) {
62
+ expected = expected.map(item => isJasmineStringAsymmetricMatcher(item) ? item.jasmineToString() : item);
63
+ }
64
+ if (isNotInLabel && isElementOrArrayLike(subject) && Array.isArray(expected) && Array.isArray(actual) && expected.length === actual.length) {
65
+ const { expectedFormatted, receivedFormatted } = printArrayWithMatchingItemInRed(expected, actual);
66
+ diffString = `\
67
+ ${label.expected}: ${expectedFormatted}
68
+ ${label.received}: ${receivedFormatted}`;
69
+ }
70
+ else if (equals(actual, expected)) {
71
+ diffString =
72
+ `\
45
73
  ${label.expected}: ${printExpected(expected)}
46
- ${label.received}: ${printReceived(actual)}`
47
- : printDiffOrStringify(expected, actual, label.expected, label.received, true);
74
+ ${label.received}: ${printReceived(actual)}`;
75
+ }
76
+ else {
77
+ diffString = printDiffOrStringify(expected, actual, label.expected, label.received, true);
78
+ }
48
79
  if (message) {
49
80
  message += '\n';
50
81
  }
51
82
  if (expectedValueArgument2) {
52
83
  expectedValueArgument2 = ` ${expectedValueArgument2}`;
53
84
  }
85
+ const some = context.isSome ? 'some of ' : '';
54
86
  const msg = `\
55
- ${message}Expect ${subject} ${not(isNot)}to ${verb}${expectation}${expectedValueArgument2}${contain}
87
+ ${message}Expect ${some}${subjectStr} ${not(isNot)}to ${verb}${expectation}${expectedValueArgument2}${contain}
56
88
 
57
89
  ${diffString}`;
58
90
  return msg;
59
91
  };
60
- export const enhanceErrorBe = (subject, context, options) => {
61
- const { isNot, verb, expectation } = context;
62
- const expected = `${not(isNot)}${expectation}`;
63
- const actual = `${not(!isNot)}${expectation}`;
64
- return enhanceError(subject, expected, actual, { ...context, useNotInLabel: false }, verb, expectation, '', options);
65
- };
66
- export const numberError = (options = {}) => {
67
- if (typeof options.eq === 'number') {
68
- return options.eq;
69
- }
70
- if (options.gte && options.lte) {
71
- return `>= ${options.gte} && <= ${options.lte}`;
92
+ const printArrayWithMatchingItemInRed = (expectedArray, actualArray) => {
93
+ const matchingIndices = [];
94
+ for (let i = 0; i < expectedArray.length; i++) {
95
+ if (equals(expectedArray[i], actualArray[i])) {
96
+ matchingIndices.push(i);
97
+ }
72
98
  }
73
- if (options.gte) {
74
- return `>= ${options.gte}`;
99
+ const expectedFormatted = `[${expectedArray
100
+ .map((item, i) => {
101
+ const stringified = stringify(item);
102
+ return matchingIndices.includes(i)
103
+ ? RECEIVED_COLOR(INVERTED_COLOR(stringified))
104
+ : EXPECTED_COLOR(stringified);
105
+ })
106
+ .join(', ')}]`;
107
+ const receivedFormatted = `[${actualArray
108
+ .map((item, i) => {
109
+ const stringified = stringify(item);
110
+ return matchingIndices.includes(i)
111
+ ? RECEIVED_COLOR(INVERTED_COLOR(stringified))
112
+ : EXPECTED_COLOR(stringified);
113
+ })
114
+ .join(', ')}]`;
115
+ return { expectedFormatted, receivedFormatted };
116
+ };
117
+ export const enhanceErrorBe = (subject, results, context, options) => {
118
+ const { isNot, verb, expectation } = context;
119
+ let expected;
120
+ let actual;
121
+ const expectedValue = `${not(isNot)}${expectation}`;
122
+ const actualValue = `${not(!isNot)}${expectation}`;
123
+ if (isElementArrayLike(subject)) {
124
+ expected = subject.length === 0 ? 'at least one result' : Array(subject.length).fill(expectedValue);
125
+ actual = toArray(results).map(result => isSuccess(isNot, result) ? `${not(isNot)}${expectation}` : `${not(!isNot)}${expectation}`);
75
126
  }
76
- if (options.lte) {
77
- return ` <= ${options.lte}`;
127
+ else {
128
+ expected = expectedValue;
129
+ actual = actualValue;
78
130
  }
79
- return 'no params';
131
+ return enhanceError(subject, expected, actual, { ...context, useNotInLabel: false }, verb, expectation, '', options);
132
+ };
133
+ const isSuccess = (isNot, success) => {
134
+ return isNot ? !success : success;
80
135
  };