expect-webdriverio 6.1.0-alpha.0 → 7.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 (39) hide show
  1. package/README.md +16 -5
  2. package/jest.d.ts +4 -2
  3. package/lib/api/index.js +2 -0
  4. package/lib/index.js +2 -0
  5. package/lib/jasmineUtils.js +4 -1
  6. package/lib/matchers/asymmetrics/asymmetricsUtils.js +10 -0
  7. package/lib/matchers/asymmetrics/multiRemote.js +36 -0
  8. package/lib/matchers/browser/toHaveClipboardText.js +18 -8
  9. package/lib/matchers/browser/toHaveLocalStorageItem.js +22 -11
  10. package/lib/matchers/browser/toHaveTitle.js +16 -6
  11. package/lib/matchers/browser/toHaveUrl.js +16 -6
  12. package/lib/matchers/element/toHaveAttribute.js +3 -2
  13. package/lib/matchers/element/toHaveChildren.js +4 -3
  14. package/lib/matchers/element/toHaveComputedLabel.js +5 -4
  15. package/lib/matchers/element/toHaveComputedRole.js +5 -4
  16. package/lib/matchers/element/toHaveElementClass.js +5 -4
  17. package/lib/matchers/element/toHaveElementProperty.js +7 -5
  18. package/lib/matchers/element/toHaveHTML.js +5 -4
  19. package/lib/matchers/element/toHaveHeight.js +2 -2
  20. package/lib/matchers/element/toHaveSize.js +3 -3
  21. package/lib/matchers/element/toHaveStyle.js +3 -3
  22. package/lib/matchers/element/toHaveText.js +9 -5
  23. package/lib/matchers/element/toHaveValue.js +1 -1
  24. package/lib/matchers/element/toHaveWidth.js +2 -2
  25. package/lib/matchers/elements/toBeElementsArrayOfSize.js +70 -3
  26. package/lib/matchers/mock/toBeRequestedTimes.js +28 -5
  27. package/lib/matchers/mock/toBeRequestedWith.js +40 -12
  28. package/lib/matchers/snapshot.js +38 -7
  29. package/lib/tsconfig.build.tsbuildinfo +1 -1
  30. package/lib/util/elementsUtil.js +37 -8
  31. package/lib/util/executeBrowserCommand.js +37 -0
  32. package/lib/util/executeCommand.js +139 -22
  33. package/lib/util/formatMessage.js +82 -8
  34. package/lib/util/multiRemoteUtils.js +85 -0
  35. package/lib/util/numberOptionsUtil.js +6 -0
  36. package/lib/util/refetchElements.js +24 -2
  37. package/lib/utils.js +20 -2
  38. package/package.json +3 -3
  39. package/types/expect-webdriverio.d.ts +386 -40
@@ -1,19 +1,74 @@
1
+ import { equals } from '../jasmineUtils.js';
2
+ import { isArrayContainingMatcher } from '../utils.js';
1
3
  import { isSomeWrapper } from '../matchers/modifiers/some.js';
2
- import { awaitElementOrArray, isElement, isStrictlyElementArray } from './elementsUtil.js';
4
+ import { awaitElementOrArray, isElement, isMultiRemoteElement, isMultiRemoteElementArray, isMultiRemoteElementLike, isMultiRemoteElements, isMultiRemoteElementsLike, isStrictlyElementArray } from './elementsUtil.js';
5
+ import { getElementsPerInstance, getPerInstanceValues, hasSameInstanceNames } from './multiRemoteUtils.js';
3
6
  import { refreshElementArray } from './refetchElements.js';
4
- export async function executeCommandWithStrategy({ unresolvedElements, expectedValues, singleElementCompare, context: { isNot, iteration }, strategy = 'NewStrictMultipleElements', strictConfiguration = { allowEmptyElements: false, allowArrayWithSingleElement: false } }) {
7
+ export async function executeCommandWithStrategy({ unresolvedElements, expectedValues, singleElementCompare, context: { isNot, iteration }, strategy = 'NewStrictMultipleElements', supportsArrayContaining = false, strictConfiguration = { allowEmptyElements: false, allowArrayWithSingleElement: false } }) {
5
8
  const isSome = isSomeWrapper(unresolvedElements);
9
+ const actualReceived = isSome ? unresolvedElements.elements : unresolvedElements;
10
+ if (supportsArrayContaining && !isSome && isArrayContainingMatcher(expectedValues)) {
11
+ const { selector, elements, other } = await awaitElementOrArray(unresolvedElements);
12
+ if (isMultiRemoteElementsLike(elements)) {
13
+ return multiRemoteArrayContainingStrategy(elements, expectedValues, singleElementCompare, iteration);
14
+ }
15
+ if (elements) {
16
+ if (iteration > 0 && isStrictlyElementArray(elements)) {
17
+ await refreshElementArray(elements);
18
+ }
19
+ const settled = await Promise.allSettled(Array.from(elements).map(async (element, index) => {
20
+ return singleElementCompare(element, undefined, index);
21
+ }));
22
+ const actual = settled.map((result) => {
23
+ if (result.status === 'rejected') {
24
+ throw result.reason;
25
+ }
26
+ return result.value.actual;
27
+ });
28
+ return {
29
+ subject: elements,
30
+ actual,
31
+ success: equals(actual, expectedValues),
32
+ abort: elements.length === 0 && !isStrictlyElementArray(elements),
33
+ };
34
+ }
35
+ if (!isElement(selector) || supportsArrayContaining === 'arrayOnly') {
36
+ return { subject: selector ?? other, actual: undefined, success: !!isNot, abort: true };
37
+ }
38
+ return { subject: selector, ...await singleElementCompare(selector, expectedValues) };
39
+ }
6
40
  if (strategy === 'LegacyLooseMultipleElements') {
7
41
  if (isSome) {
8
42
  throw new Error('some(elements) works only when enabling `useToHaveTextStrictMultiElementsCompareStrategy`');
9
43
  }
10
- return legacyMultipleElementResultsStrategy(unresolvedElements, expectedValues, singleElementCompare, isNot);
44
+ return legacyMultipleElementResultsStrategy(actualReceived, expectedValues, singleElementCompare, isNot);
11
45
  }
12
- const actualReceived = isSome ? unresolvedElements.elements : unresolvedElements;
13
46
  return multipleElementResultsStrategy(actualReceived, expectedValues, singleElementCompare, { isNot, isSome, iteration }, strictConfiguration);
14
47
  }
48
+ const multiRemoteArrayContainingStrategy = async (elements, expectedValues, singleElementCompare, iteration) => {
49
+ const currentElements = iteration > 0 ? await refreshElementArray(elements) : elements;
50
+ const multiRemoteElements = currentElements;
51
+ if (multiRemoteElements.length === 0) {
52
+ return { subject: elements, actual: undefined, success: false, abort: !isMultiRemoteElementArray(elements) && !isMultiRemoteElements(elements) };
53
+ }
54
+ const { instances } = multiRemoteElements[0];
55
+ const elementsPerInstance = getElementsPerInstance(multiRemoteElements, instances);
56
+ const actual = Object.fromEntries(await Promise.all(instances.map(async (instance) => {
57
+ const results = await Promise.all(elementsPerInstance[instance].map((element, index) => singleElementCompare(element, undefined, index)));
58
+ return [instance, results.map((result) => result.actual)];
59
+ })));
60
+ return {
61
+ subject: elements,
62
+ actual,
63
+ success: instances.every((instance) => equals(actual[instance], expectedValues)),
64
+ expected: Object.fromEntries(instances.map((instance) => [instance, expectedValues])),
65
+ };
66
+ };
15
67
  export const legacyMultipleElementResultsStrategy = async (unresolvedElements, expectedValues, singleElementCompare, _isNot) => {
16
68
  const { selector, other, isEmptyElements } = await awaitElementOrArray(unresolvedElements);
69
+ if (isMultiRemoteElementLike(selector)) {
70
+ throw new Error('Multi-remote elements works only when enabling `useToHaveTextStrictMultiElementsCompareStrategy`');
71
+ }
17
72
  const subject = selector ?? other;
18
73
  if (!selector || isEmptyElements) {
19
74
  return {
@@ -42,29 +97,41 @@ export const legacyMultipleElementResultsStrategy = async (unresolvedElements, e
42
97
  actual: results.map(({ actual: value }) => value),
43
98
  };
44
99
  };
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);
100
+ export const multipleElementResultsStrategy = async (unresolvedElements, expectedValues, singleElementCompare, { isNot, isSome, iteration }, { allowEmptyElements = false, allowArrayWithSingleElement = false, allowObjectExpectedValue = false } = {}) => {
101
+ const { selector, other, multiRemoteSelector } = await awaitElementOrArray(unresolvedElements);
102
+ const isRefetchable = isStrictlyElementArray(selector) || isMultiRemoteElementArray(selector) || isMultiRemoteElements(selector);
103
+ let currentElements = selector;
104
+ if (iteration > 0 && (isStrictlyElementArray(selector) || isMultiRemoteElementsLike(selector))) {
105
+ currentElements = await refreshElementArray(selector);
49
106
  }
50
- const subject = selector ?? other;
51
- if (!selector || (Array.isArray(selector) && selector.length === 0)) {
107
+ const subject = multiRemoteSelector ?? selector ?? other;
108
+ if (!currentElements || (Array.isArray(currentElements) && currentElements.length === 0)) {
52
109
  return {
53
110
  subject,
54
111
  success: isNot ? !allowEmptyElements : false,
55
112
  actual: undefined,
56
- abort: !allowEmptyElements && !isStrictlyElementArray(selector),
113
+ abort: !allowEmptyElements && !isRefetchable,
57
114
  context: { isSome },
58
115
  };
59
116
  }
117
+ const isUnexpectedPerInstanceValues = !multiRemoteSelector && !isMultiRemoteElementsLike(selector)
118
+ && getPerInstanceValues(expectedValues, { allowObjectExpectedValue }) !== undefined;
60
119
  if (isElement(selector)) {
61
- const forceFailure = !allowArrayWithSingleElement && Array.isArray(expectedValues);
120
+ const forceFailure = (!allowArrayWithSingleElement && Array.isArray(expectedValues)) || isUnexpectedPerInstanceValues;
62
121
  const compareResult = await singleElementCompare(selector, forceFailure ? undefined : expectedValues);
63
122
  const success = forceFailure ? !!isNot : compareResult.success;
64
123
  return { subject, success, actual: compareResult.actual, abort: forceFailure, context: { isSome } };
65
124
  }
66
- const lengthMismatch = Array.isArray(expectedValues) && expectedValues.length !== selector.length;
67
- const settled = await Promise.allSettled(Array.from(selector).map(async (element, index) => {
125
+ if (multiRemoteSelector || isMultiRemoteElementsLike(selector)) {
126
+ return multiRemoteElementsResultsStrategy(subject, multiRemoteSelector ?? selector, expectedValues, singleElementCompare, { isNot, isSome }, { allowArrayWithSingleElement, allowObjectExpectedValue });
127
+ }
128
+ const elementsSelector = selector;
129
+ const lengthMismatch = Array.isArray(expectedValues) && expectedValues.length !== elementsSelector.length;
130
+ if (isUnexpectedPerInstanceValues) {
131
+ const results = await Promise.all(Array.from(elementsSelector).map((element, index) => singleElementCompare(element, undefined, index)));
132
+ return { subject, success: !!isNot, abort: true, actual: results.map(({ actual }) => actual), context: { isSome } };
133
+ }
134
+ const settled = await Promise.allSettled(Array.from(elementsSelector).map(async (element, index) => {
68
135
  const indexedExpected = Array.isArray(expectedValues) ? expectedValues[index] : expectedValues;
69
136
  const forceElementFailure = Array.isArray(indexedExpected)
70
137
  || (lengthMismatch && Array.isArray(expectedValues) && index >= expectedValues.length);
@@ -76,19 +143,69 @@ export const multipleElementResultsStrategy = async (unresolvedElements, expecte
76
143
  throw firstRejection.reason;
77
144
  }
78
145
  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 }));
146
+ if (Array.isArray(expectedValues) && expectedValues.length > elementsSelector.length) {
147
+ results.push(...Array(expectedValues.length - elementsSelector.length).fill({ success: false, actual: undefined }));
81
148
  }
149
+ const actual = results.map(({ actual }) => actual);
82
150
  if (lengthMismatch) {
83
- return { subject, success: !!isNot, actual: results.map(({ actual }) => actual), context: { isSome } };
151
+ return { subject, success: !!isNot, actual, context: { isSome } };
84
152
  }
85
- const isNotEmpty = results.length > 0;
153
+ return { subject, success: computeSuccess([results], { isNot, isSome }), actual, context: { isSome } };
154
+ };
155
+ const multiRemoteElementsResultsStrategy = async (subject, multiRemoteSelector, expectedValues, singleElementCompare, { isNot, isSome }, { allowArrayWithSingleElement, allowObjectExpectedValue }) => {
156
+ const isSingleElement = isMultiRemoteElement(multiRemoteSelector);
157
+ const multiRemoteElements = isSingleElement ? [multiRemoteSelector] : multiRemoteSelector;
158
+ const { instances } = multiRemoteElements[0];
159
+ const elementsPerInstance = getElementsPerInstance(multiRemoteElements, instances);
160
+ const perInstanceValues = getPerInstanceValues(expectedValues, { allowObjectExpectedValue });
161
+ const expectedPerInstance = perInstanceValues ?? Object.fromEntries(instances.map((name) => [name, expectedValues]));
162
+ const instanceNamesMismatch = !!perInstanceValues && !hasSameInstanceNames(perInstanceValues, instances);
163
+ const unsupportedArray = isSingleElement && !allowArrayWithSingleElement && Object.values(expectedPerInstance).some(Array.isArray);
164
+ const lengthMismatch = !isSingleElement && instances.some((name) => {
165
+ const value = expectedPerInstance[name];
166
+ return Array.isArray(value) && value.length !== elementsPerInstance[name].length;
167
+ });
168
+ const actualPerInstance = {};
169
+ const resultsPerInstance = await Promise.all(instances.map(async (instance) => {
170
+ const isExpected = instance in expectedPerInstance;
171
+ const instanceValue = expectedPerInstance[instance];
172
+ const elements = elementsPerInstance[instance];
173
+ const results = await Promise.all(elements.map(async (element, index) => {
174
+ const indexedExpected = isSingleElement || !Array.isArray(instanceValue) ? instanceValue : instanceValue[index];
175
+ const forceElementFailure = !isExpected || unsupportedArray
176
+ || (!isSingleElement && Array.isArray(instanceValue) && (index >= instanceValue.length || Array.isArray(indexedExpected)));
177
+ const elementExpected = forceElementFailure ? undefined : indexedExpected;
178
+ const result = isSingleElement ? await singleElementCompare(element, elementExpected) : await singleElementCompare(element, elementExpected, index);
179
+ return forceElementFailure ? { success: false, actual: result.actual } : result;
180
+ }));
181
+ const actuals = results.map(({ actual }) => actual);
182
+ if (isSingleElement) {
183
+ actualPerInstance[instance] = actuals[0];
184
+ }
185
+ else {
186
+ const expectedLength = Array.isArray(instanceValue) ? instanceValue.length : 0;
187
+ actualPerInstance[instance] = [...actuals, ...Array(Math.max(expectedLength - actuals.length, 0)).fill(undefined)];
188
+ }
189
+ return results;
190
+ }));
191
+ const expected = isSingleElement ? expectedPerInstance : Object.fromEntries(Object.entries(expectedPerInstance).map(([name, value]) => {
192
+ const count = elementsPerInstance[name]?.length ?? 0;
193
+ return [name, Array.isArray(value) || isArrayContainingMatcher(value) ? value : Array(Math.max(count, 1)).fill(value)];
194
+ }));
195
+ if (instanceNamesMismatch || unsupportedArray) {
196
+ return { subject, success: !!isNot, abort: true, actual: actualPerInstance, context: { isSome }, expected };
197
+ }
198
+ if (lengthMismatch) {
199
+ return { subject, success: !!isNot, actual: actualPerInstance, context: { isSome }, expected };
200
+ }
201
+ return { subject, success: computeSuccess(resultsPerInstance, { isNot, isSome }), actual: actualPerInstance, context: { isSome }, expected };
202
+ };
203
+ const computeSuccess = (groups, { isNot, isSome }) => {
86
204
  const checkFn = isSome ? isAtLeastOneTrue : isAllTrue;
87
205
  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 } };
206
+ const isNotEmpty = groups.length > 0 && groups.every((results) => results.length > 0);
207
+ const assertionPasses = isNotEmpty && groups.every((results) => isNot ? checkNotFn(results) : checkFn(results));
208
+ return isNot ? !assertionPasses : assertionPasses;
92
209
  };
93
210
  const isAllTrue = (results) => results.every((res) => res.success === true);
94
211
  const isAllFalse = (results) => results.every((res) => res.success === false);
@@ -1,8 +1,9 @@
1
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 { isArrayOfElement, isElementArrayLike, isElementOrArrayLike, isStrictlyElementArray } from './elementsUtil.js';
3
+ import { isArrayOfElement, isElementArrayLike, isElementOrArrayLike, isElementOrArrayOrMultiRemoteElementLike, isMultiRemoteElement, isMultiRemoteElementArray, isMultiRemoteElementLike, isMultiRemoteElementsLike, isStrictlyElementArray } from './elementsUtil.js';
4
4
  import { toJsonString } from './stringUtil.js';
5
5
  import { isJasmineStringAsymmetricMatcher, toArray } from '../utils.js';
6
+ import { isBrowser } from './multiRemoteUtils.js';
6
7
  export const isDefined = (value) => value !== null && value !== undefined;
7
8
  export const getSelector = (el) => {
8
9
  let result = typeof el.selector === 'string' ? el.selector : '<fn>';
@@ -17,7 +18,17 @@ export const getSelectors = (el) => {
17
18
  }
18
19
  const selectors = [];
19
20
  let parent;
20
- if (isStrictlyElementArray(el)) {
21
+ if (isMultiRemoteElement(el)) {
22
+ const subject = formatMultiRemoteInstanceNames(el.instances);
23
+ return `${subject}.$(\`${getSelector(el)}\`)`;
24
+ }
25
+ else if (isMultiRemoteElementsLike(el)) {
26
+ const instances = isMultiRemoteElementArray(el) ? el.parent.instances : el[0].instances ?? [];
27
+ const selector = isMultiRemoteElementArray(el) ? getSelector(el) : el[0] ? getSelector(el[0]) : '';
28
+ const subject = formatMultiRemoteInstanceNames(instances);
29
+ return `${subject}.$$(\`${selector}\`)`;
30
+ }
31
+ else if (isStrictlyElementArray(el)) {
21
32
  selectors.push(`${(el).foundWith}(\`${getSelector(el)}\`)`);
22
33
  parent = el.parent;
23
34
  }
@@ -38,7 +49,23 @@ export const getSelectors = (el) => {
38
49
  const not = (isNot) => `${isNot ? 'not ' : ''}`;
39
50
  export const enhanceError = (subject, expected, actual, context, verb, expectation, expectedValueArgument2 = '', { message = '', containing = false } = {}) => {
40
51
  const { isNot, useNotInLabel = true } = context;
41
- let subjectStr = (isElementOrArrayLike(subject) ? getSelectors(subject) : toJsonString(subject));
52
+ if ((isBrowser(subject) && subject.isMultiremote) || isMultiRemoteElementLike(subject)) {
53
+ expected = labelMultiRemoteValues(expected);
54
+ actual = labelMultiRemoteValues(actual);
55
+ }
56
+ if (isBrowser(subject)) {
57
+ if (subject.isMultiremote) {
58
+ subject = formatMultiRemoteInstanceNames(subject.instances);
59
+ }
60
+ else if (subject.isMobile) {
61
+ subject = context.browserTargetType === 'window' ? 'mobile screen' : 'mobile';
62
+ }
63
+ else {
64
+ const prefix = subject.requestedCapabilities?.browserName ?? 'browser';
65
+ subject = context.browserTargetType === 'window' ? `${prefix}'s window` : prefix;
66
+ }
67
+ }
68
+ let subjectStr = (isElementOrArrayOrMultiRemoteElementLike(subject) ? getSelectors(subject) : toJsonString(subject));
42
69
  if (subjectStr.length > 100) {
43
70
  subjectStr = `${subjectStr.substring(0, 100)}...`;
44
71
  }
@@ -56,10 +83,10 @@ export const enhanceError = (subject, expected, actual, context, verb, expectati
56
83
  };
57
84
  let diffString = '';
58
85
  if (isJasmineStringAsymmetricMatcher(expected)) {
59
- expected = expected.jasmineToString();
86
+ expected = expected.jasmineToString(stringify);
60
87
  }
61
88
  else if (isElementOrArrayLike(subject) && Array.isArray(expected)) {
62
- expected = expected.map(item => isJasmineStringAsymmetricMatcher(item) ? item.jasmineToString() : item);
89
+ expected = expected.map(item => isJasmineStringAsymmetricMatcher(item) ? item.jasmineToString(stringify) : item);
63
90
  }
64
91
  if (isNotInLabel && isElementOrArrayLike(subject) && Array.isArray(expected) && Array.isArray(actual) && expected.length === actual.length) {
65
92
  const { expectedFormatted, receivedFormatted } = printArrayWithMatchingItemInRed(expected, actual);
@@ -114,15 +141,47 @@ const printArrayWithMatchingItemInRed = (expectedArray, actualArray) => {
114
141
  .join(', ')}]`;
115
142
  return { expectedFormatted, receivedFormatted };
116
143
  };
117
- export const enhanceErrorBe = (subject, results, context, options) => {
144
+ export const enhanceErrorBe = (subject, actuals, context, options) => {
118
145
  const { isNot, verb, expectation } = context;
119
146
  let expected;
120
147
  let actual;
121
148
  const expectedValue = `${not(isNot)}${expectation}`;
122
149
  const actualValue = `${not(!isNot)}${expectation}`;
123
- if (isElementArrayLike(subject)) {
150
+ if (isMultiRemoteElementLike(subject)) {
151
+ if (isMultiRemoteElement(subject)) {
152
+ const typedActuals = actuals;
153
+ actual = subject.instances.reduce((acc, instance) => {
154
+ acc[instance] = isSuccess(isNot, typedActuals[instance]) ? `${not(isNot)}${expectation}` : `${not(!isNot)}${expectation}`;
155
+ return acc;
156
+ }, {});
157
+ expected = subject.instances.reduce((acc, instance) => {
158
+ acc[instance] = expectedValue;
159
+ return acc;
160
+ }, {});
161
+ }
162
+ else if (isMultiRemoteElementsLike(subject) && subject.length === 0) {
163
+ expected = 'at least one result';
164
+ actual = actualValue;
165
+ }
166
+ else if (isMultiRemoteElementsLike(subject)) {
167
+ const { instances } = subject[0];
168
+ const typedActuals = actuals;
169
+ actual = instances.reduce((acc, instance) => {
170
+ acc[instance] = typedActuals[instance].map(actual => isSuccess(isNot, actual) ? `${not(isNot)}${expectation}` : `${not(!isNot)}${expectation}`);
171
+ return acc;
172
+ }, {});
173
+ expected = instances.reduce((acc, instance) => {
174
+ acc[instance] = Array(typedActuals[instance].length).fill(expectedValue);
175
+ return acc;
176
+ }, {});
177
+ }
178
+ else {
179
+ throw new Error('Unsupported Multi-remote object type for enhanceErrorBe');
180
+ }
181
+ }
182
+ else if (isElementArrayLike(subject)) {
124
183
  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}`);
184
+ actual = toArray(actuals).map(actual => isSuccess(isNot, actual) ? `${not(isNot)}${expectation}` : `${not(!isNot)}${expectation}`);
126
185
  }
127
186
  else {
128
187
  expected = expectedValue;
@@ -133,3 +192,18 @@ export const enhanceErrorBe = (subject, results, context, options) => {
133
192
  const isSuccess = (isNot, success) => {
134
193
  return isNot ? !success : success;
135
194
  };
195
+ class MultiRemoteValuesLabel {
196
+ }
197
+ Object.defineProperty(MultiRemoteValuesLabel, 'name', { value: 'Multi-remote values' });
198
+ export const labelMultiRemoteValues = (value) => {
199
+ const isPlainObject = typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype;
200
+ return isPlainObject ? Object.assign(new MultiRemoteValuesLabel(), value) : value;
201
+ };
202
+ export const formatMultiRemoteMocks = ({ names, isNamedByInstance }) => {
203
+ return isNamedByInstance ? `${formatMultiRemoteInstanceNames(names)} mocks` : 'mocks';
204
+ };
205
+ const formatMultiRemoteInstanceNames = (instances) => {
206
+ let instanceNames = instances.join(', ');
207
+ instanceNames = instanceNames.length > 50 ? `${instanceNames.substring(0, 50)}...` : instanceNames;
208
+ return `multi-remote<${instanceNames}>`;
209
+ };
@@ -0,0 +1,85 @@
1
+ import { isAsymmetricMatcher } from '../utils.js';
2
+ export const isMultiRemoteValues = (value, existingInstanceNames) => {
3
+ if (value && typeof value === 'object' && !Array.isArray(value) && !isAsymmetricMatcher(value) && !(value instanceof RegExp) && Object.keys(value).length > 0) {
4
+ if (existingInstanceNames) {
5
+ return existingInstanceNames?.some(name => Object.keys(value).includes(name));
6
+ }
7
+ return true;
8
+ }
9
+ return false;
10
+ };
11
+ export const hasSameInstanceNames = (expected, instances) => {
12
+ const names = Object.keys(expected);
13
+ return names.length === instances.length && instances.every((name) => names.includes(name));
14
+ };
15
+ export const MULTI_REMOTE_MATCHER_SYMBOL = Symbol.for('expect-webdriverio.multiRemote');
16
+ export const isMultiRemoteMatcher = (value) => {
17
+ return !!value && typeof value === 'object' && value[MULTI_REMOTE_MATCHER_SYMBOL] === true;
18
+ };
19
+ export const getPerInstanceValues = (value, { allowObjectExpectedValue = false } = {}) => {
20
+ if (isMultiRemoteMatcher(value)) {
21
+ return value.sample;
22
+ }
23
+ return !allowObjectExpectedValue && isMultiRemoteValues(value) ? value : undefined;
24
+ };
25
+ export const getElementsPerInstance = (multiRemoteElements, instances) => {
26
+ return Object.fromEntries(instances.map((name) => {
27
+ const elements = [];
28
+ for (let index = 0; index < multiRemoteElements.length; index++) {
29
+ const element = getInstanceOrUndefined(multiRemoteElements[index], name);
30
+ if (element) {
31
+ elements.push(element);
32
+ }
33
+ }
34
+ return [name, elements];
35
+ }));
36
+ };
37
+ const getInstanceOrUndefined = (element, name) => {
38
+ try {
39
+ return element.getInstance(name) || undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ };
45
+ export const getGlobalMultiRemoteInstanceNames = () => {
46
+ try {
47
+ const instances = typeof multiRemoteBrowser === 'undefined' ? undefined : multiRemoteBrowser.instances;
48
+ return Array.isArray(instances) ? instances : undefined;
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ };
54
+ export const isMock = (obj) => {
55
+ return typeof obj === 'object' && obj !== null && Array.isArray(obj.calls);
56
+ };
57
+ export const isMockArray = (obj) => {
58
+ return Array.isArray(obj) && obj.length > 0 && obj.every(isMock);
59
+ };
60
+ export const awaitMocks = async (received) => {
61
+ const mocks = received instanceof Promise ? await received : received;
62
+ if (Array.isArray(mocks) && mocks.length === 0) {
63
+ throw new Error('Expected a mock or a non-empty array of mocks, received an empty array');
64
+ }
65
+ return mocks;
66
+ };
67
+ export const getMockInstanceNames = (mocks) => {
68
+ const instances = getGlobalMultiRemoteInstanceNames();
69
+ if (instances && instances.length === mocks.length) {
70
+ return { names: instances, isNamedByInstance: true };
71
+ }
72
+ return { names: mocks.map((_, index) => `mocks[${index}]`), isNamedByInstance: false };
73
+ };
74
+ export const isGlobalBrowserSingleRemote = () => {
75
+ try {
76
+ return typeof browser !== 'undefined' && isBrowser(browser) && !browser.isMultiremote;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ };
82
+ export const isBrowser = (obj) => {
83
+ const name = obj?.constructor?.name?.replace(/^bound /, '');
84
+ return name === 'Browser' || !!name?.endsWith('MultiRemoteDriver');
85
+ };
@@ -1,5 +1,6 @@
1
1
  import { AsymmetricMatcher } from 'expect';
2
2
  import { isDefinedPlainObject } from './commandOptionsUtils.js';
3
+ import { isMultiRemoteMatcher } from './multiRemoteUtils.js';
3
4
  export const isNumber = (value) => typeof value === 'number' && !isNaN(value);
4
5
  export const isDefinedNotNumber = (value) => value !== undefined && !isNumber(value);
5
6
  export const isDefinedNumberOrNonEmptyObject = (value) => typeof value === 'number' || (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0);
@@ -25,6 +26,11 @@ export function validateNumberAndExtractOptions(expectedValue, commandOptions, {
25
26
  };
26
27
  }
27
28
  export function validateNumberArrayAndExtractOptions(expectedValues, commandOptions, { supportDefaultAsGteThen1 } = {}) {
29
+ if (isMultiRemoteMatcher(expectedValues)) {
30
+ const perInstanceValues = expectedValues.sample;
31
+ const numberMatcher = Object.fromEntries(Object.entries(perInstanceValues).map(([instance, value]) => [instance, validateNumberArrayAndExtractOptions(value, commandOptions).numberMatcher]));
32
+ return { numberMatcher, commandOptions };
33
+ }
28
34
  if (Array.isArray(expectedValues)) {
29
35
  const allNumbers = expectedValues.map((value) => validateNumberAndExtractOptions(value, commandOptions, { supportDefaultAsGteThen1 }));
30
36
  return { numberMatcher: allNumbers.map(({ numberMatcher }) => numberMatcher), commandOptions };
@@ -1,14 +1,32 @@
1
- import { isStrictlyElementArray } from './elementsUtil.js';
1
+ import { isMultiRemoteElementArray, isMultiRemoteElements, isStrictlyElementArray } from './elementsUtil.js';
2
2
  export const refetchElements = async (elements) => {
3
3
  if (elements
4
- && isStrictlyElementArray(elements)
4
+ && (isStrictlyElementArray(elements) || isMultiRemoteElementArray(elements))
5
5
  && elements.parent && elements.foundWith && elements.foundWith in elements.parent) {
6
6
  const browser = elements.parent;
7
7
  const $$ = browser[elements.foundWith];
8
8
  return await $$.call(browser, elements.selector, ...elements.props);
9
9
  }
10
+ else if (isMultiRemoteElements(elements)) {
11
+ const selector = elements.find((element) => !!element.selector)?.selector;
12
+ if (selector) {
13
+ warnMultiRemoteRefetchFallbackOnce();
14
+ if (typeof multiRemoteBrowser === 'undefined') {
15
+ return elements;
16
+ }
17
+ return await multiRemoteBrowser.$$(selector);
18
+ }
19
+ }
10
20
  return elements;
11
21
  };
22
+ let hasWarnedMultiRemoteRefetchFallback = false;
23
+ const warnMultiRemoteRefetchFallbackOnce = () => {
24
+ if (hasWarnedMultiRemoteRefetchFallback) {
25
+ return;
26
+ }
27
+ hasWarnedMultiRemoteRefetchFallback = true;
28
+ console.warn('expect-webdriverio: refetching multi-remote elements between retries is best effort: they are re-queried from the global `multiRemoteBrowser`, ignoring any parent element or `select()` scope. Set `WDIO_ENABLE_MULTI_REMOTE_ELEMENT_ARRAY=true` for reliable retries.');
29
+ };
12
30
  export const syncronizeElements = async (subject, refetchedElements) => {
13
31
  if (subject instanceof Promise) {
14
32
  await syncronizeChainableElementArray(subject, refetchedElements);
@@ -31,5 +49,9 @@ export const synchronizeElementArray = (subject, refetchedElements) => {
31
49
  };
32
50
  export const refreshElementArray = async (subject) => {
33
51
  const refetchedElements = await refetchElements(subject);
52
+ if (refetchedElements.length === 0 && isMultiRemoteElements(subject)) {
53
+ return refetchedElements;
54
+ }
34
55
  synchronizeElementArray(subject, refetchedElements);
56
+ return subject;
35
57
  };
package/lib/utils.js CHANGED
@@ -15,6 +15,19 @@ export function isAsymmetricMatcher(expected) {
15
15
  'asymmetricMatch' in expected &&
16
16
  !!expected.asymmetricMatch);
17
17
  }
18
+ export function isArrayContainingMatcher(expected) {
19
+ if (!isAsymmetricMatcher(expected) || typeof expected.asymmetricMatch !== 'function') {
20
+ return false;
21
+ }
22
+ if (typeof expected.toString === 'function' && /^Array(Not)?Containing$/.test(expected.toString())) {
23
+ return true;
24
+ }
25
+ if ('jasmineToString' in expected && typeof expected.jasmineToString === 'function') {
26
+ const description = expected.jasmineToString(() => '');
27
+ return description === '<jasmine.arrayContaining()>';
28
+ }
29
+ return false;
30
+ }
18
31
  export function isStringContainingMatcherLike(expected) {
19
32
  return !!expected && expected.constructor.name === 'StringContaining';
20
33
  }
@@ -89,7 +102,7 @@ const compareNumbers = (actual, options = {}) => {
89
102
  }
90
103
  return false;
91
104
  };
92
- export const compareTextOrArray = (actualText, expectedTexts, options) => {
105
+ export const compareTextOrOneOf = (actualText, expectedTexts, options) => {
93
106
  if (expectedTexts === undefined) {
94
107
  return { actual: actualText, success: false };
95
108
  }
@@ -106,7 +119,7 @@ export const compareTextOrArray = (actualText, expectedTexts, options) => {
106
119
  return { success: expectedTexts.asymmetricMatch(actualText), actual: actualText };
107
120
  }
108
121
  const compareResults = compareText(actualText, expectedTexts, options);
109
- return { success: compareResults.success, actual: actualText };
122
+ return { ...compareResults, actual: actualText };
110
123
  };
111
124
  export const compareText = (actual, expected, { ignoreCase = false, trim = true, containing = false, atStart = false, atEnd = false, atIndex, replace, }) => {
112
125
  if (typeof actual !== 'string' || expected === null || expected === undefined) {
@@ -276,6 +289,11 @@ export const compareStyle = async (actualEl, style, { ignoreCase = false, trim =
276
289
  const css = await actualEl.getCSSProperty(key);
277
290
  let actualVal = String(css.value || '');
278
291
  let expectedVal = style[key];
292
+ if (typeof expectedVal !== 'string') {
293
+ actual[key] = css.value;
294
+ success = false;
295
+ continue;
296
+ }
279
297
  if (trim) {
280
298
  actualVal = actualVal.trim();
281
299
  expectedVal = expectedVal.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expect-webdriverio",
3
- "version": "6.1.0-alpha.0",
3
+ "version": "7.0.0",
4
4
  "author": "Mykola Grybyk <mykola.grybyk@gmail.com>",
5
5
  "description": "WebdriverIO Assertion Library",
6
6
  "license": "MIT",
@@ -114,9 +114,9 @@
114
114
  "webdriverio": "^9.32.0"
115
115
  },
116
116
  "peerDependencies": {
117
- "@wdio/globals": "^9.0.0",
117
+ "@wdio/globals": "^9.23.0",
118
118
  "@wdio/logger": "^9.0.0",
119
- "webdriverio": "^9.28.0"
119
+ "webdriverio": "^9.31.5"
120
120
  },
121
121
  "peerDependenciesMeta": {
122
122
  "@wdio/globals": {