expect-webdriverio 6.1.0 → 8.0.0-alpha.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.
- package/README.md +22 -7
- package/jasmine-wdio-expect-async.d.ts +1 -1
- package/jest.d.ts +4 -2
- package/lib/api/index.js +2 -0
- package/lib/constants.js +0 -6
- package/lib/index.js +3 -13
- package/lib/jasmineUtils.js +2 -2
- package/lib/matchers/asymmetrics/asymmetricsUtils.js +10 -0
- package/lib/matchers/asymmetrics/multiRemote.js +36 -0
- package/lib/matchers/asymmetrics/oneOf.js +2 -2
- package/lib/matchers/browser/toHaveClipboardText.js +24 -10
- package/lib/matchers/browser/toHaveLocalStorageItem.js +23 -22
- package/lib/matchers/browser/toHaveTitle.js +16 -6
- package/lib/matchers/browser/toHaveUrl.js +16 -6
- package/lib/matchers/element/toBeClickable.js +3 -2
- package/lib/matchers/element/toBeDisabled.js +3 -2
- package/lib/matchers/element/toBeDisplayed.js +3 -2
- package/lib/matchers/element/toBeDisplayedInViewport.js +3 -2
- package/lib/matchers/element/toBeEnabled.js +3 -2
- package/lib/matchers/element/toBeExisting.js +5 -2
- package/lib/matchers/element/toBeFocused.js +3 -2
- package/lib/matchers/element/toBeSelected.js +4 -4
- package/lib/matchers/element/toHaveAttribute.js +4 -16
- package/lib/matchers/element/toHaveChildren.js +9 -14
- package/lib/matchers/element/toHaveComputedLabel.js +4 -6
- package/lib/matchers/element/toHaveComputedRole.js +4 -6
- package/lib/matchers/element/toHaveElementClass.js +5 -10
- package/lib/matchers/element/toHaveElementProperty.js +4 -9
- package/lib/matchers/element/toHaveHTML.js +6 -8
- package/lib/matchers/element/toHaveHeight.js +6 -7
- package/lib/matchers/element/toHaveHref.js +6 -3
- package/lib/matchers/element/toHaveId.js +3 -2
- package/lib/matchers/element/toHaveSize.js +3 -4
- package/lib/matchers/element/toHaveStyle.js +3 -4
- package/lib/matchers/element/toHaveText.js +5 -8
- package/lib/matchers/element/toHaveValue.js +1 -1
- package/lib/matchers/element/toHaveWidth.js +6 -7
- package/lib/matchers/elements/toBeElementsArrayOfSize.js +71 -6
- package/lib/matchers/mock/toBeRequested.js +1 -1
- package/lib/matchers/mock/toBeRequestedTimes.js +30 -7
- package/lib/matchers/mock/toBeRequestedWith.js +40 -15
- package/lib/matchers/snapshot.js +38 -7
- package/lib/matchers.js +32 -32
- package/lib/snapshot.js +1 -8
- package/lib/tsconfig.build.tsbuildinfo +1 -1
- package/lib/util/elementsUtil.js +35 -6
- package/lib/util/executeBrowserCommand.js +37 -0
- package/lib/util/executeCommand.js +101 -52
- package/lib/util/formatMessage.js +81 -7
- package/lib/util/multiRemoteUtils.js +93 -0
- package/lib/util/numberOptionsUtil.js +19 -24
- package/lib/util/refetchElements.js +24 -2
- package/lib/utils.js +16 -111
- package/package.json +37 -40
- package/types/expect-global.d.ts +1 -2
- package/types/expect-webdriverio.d.ts +386 -232
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { equals } from '../jasmineUtils.js';
|
|
2
2
|
import { isArrayContainingMatcher } from '../utils.js';
|
|
3
3
|
import { isSomeWrapper } from '../matchers/modifiers/some.js';
|
|
4
|
-
import { awaitElementOrArray, isElement, isStrictlyElementArray } from './elementsUtil.js';
|
|
4
|
+
import { awaitElementOrArray, isElement, isMultiRemoteElement, isMultiRemoteElementArray, isMultiRemoteElements, isMultiRemoteElementsLike, isStrictlyElementArray } from './elementsUtil.js';
|
|
5
|
+
import { getElementsPerInstance, getPerInstanceValues, hasSameInstanceNames } from './multiRemoteUtils.js';
|
|
5
6
|
import { refreshElementArray } from './refetchElements.js';
|
|
6
|
-
export async function executeCommandWithStrategy({ unresolvedElements, expectedValues, singleElementCompare, context: { isNot, iteration },
|
|
7
|
+
export async function executeCommandWithStrategy({ unresolvedElements, expectedValues, singleElementCompare, context: { isNot = false, iteration }, supportsArrayContaining = false, strictConfiguration = { allowEmptyElements: false, allowArrayWithSingleElement: false } }) {
|
|
7
8
|
const isSome = isSomeWrapper(unresolvedElements);
|
|
8
9
|
const actualReceived = isSome ? unresolvedElements.elements : unresolvedElements;
|
|
9
10
|
if (supportsArrayContaining && !isSome && isArrayContainingMatcher(expectedValues)) {
|
|
10
11
|
const { selector, elements, other } = await awaitElementOrArray(unresolvedElements);
|
|
12
|
+
if (isMultiRemoteElementsLike(elements)) {
|
|
13
|
+
return multiRemoteArrayContainingStrategy(elements, expectedValues, singleElementCompare, iteration);
|
|
14
|
+
}
|
|
11
15
|
if (elements) {
|
|
12
16
|
if (iteration > 0 && isStrictlyElementArray(elements)) {
|
|
13
17
|
await refreshElementArray(elements);
|
|
@@ -33,67 +37,62 @@ export async function executeCommandWithStrategy({ unresolvedElements, expectedV
|
|
|
33
37
|
}
|
|
34
38
|
return { subject: selector, ...await singleElementCompare(selector, expectedValues) };
|
|
35
39
|
}
|
|
36
|
-
if (strategy === 'LegacyLooseMultipleElements') {
|
|
37
|
-
if (isSome) {
|
|
38
|
-
throw new Error('some(elements) works only when enabling `useToHaveTextStrictMultiElementsCompareStrategy`');
|
|
39
|
-
}
|
|
40
|
-
return legacyMultipleElementResultsStrategy(actualReceived, expectedValues, singleElementCompare, isNot);
|
|
41
|
-
}
|
|
42
40
|
return multipleElementResultsStrategy(actualReceived, expectedValues, singleElementCompare, { isNot, isSome, iteration }, strictConfiguration);
|
|
43
41
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return {
|
|
49
|
-
subject: subject,
|
|
50
|
-
success: false,
|
|
51
|
-
actual: undefined,
|
|
52
|
-
abort: true,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
if (isElement(selector)) {
|
|
56
|
-
const compareResult = await singleElementCompare(selector, expectedValues);
|
|
57
|
-
return {
|
|
58
|
-
subject,
|
|
59
|
-
...compareResult,
|
|
60
|
-
};
|
|
42
|
+
const multiRemoteArrayContainingStrategy = async (elements, expectedValues, singleElementCompare, iteration) => {
|
|
43
|
+
const currentElements = iteration > 0 ? await refreshElementArray(elements) : elements;
|
|
44
|
+
const multiRemoteElements = currentElements;
|
|
45
|
+
if (multiRemoteElements.length === 0) {
|
|
46
|
+
return { subject: elements, actual: undefined, success: false, abort: !isMultiRemoteElementArray(elements) && !isMultiRemoteElements(elements) };
|
|
61
47
|
}
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
48
|
+
const { instances } = multiRemoteElements[0];
|
|
49
|
+
const elementsPerInstance = getElementsPerInstance(multiRemoteElements, instances);
|
|
50
|
+
const actual = Object.fromEntries(await Promise.all(instances.map(async (instance) => {
|
|
51
|
+
const results = await Promise.all(elementsPerInstance[instance].map((element, index) => singleElementCompare(element, undefined, index)));
|
|
52
|
+
return [instance, results.map((result) => result.actual)];
|
|
53
|
+
})));
|
|
68
54
|
return {
|
|
69
|
-
subject,
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
subject: elements,
|
|
56
|
+
actual,
|
|
57
|
+
success: instances.every((instance) => equals(actual[instance], expectedValues)),
|
|
58
|
+
expected: Object.fromEntries(instances.map((instance) => [instance, expectedValues])),
|
|
72
59
|
};
|
|
73
60
|
};
|
|
74
|
-
export const multipleElementResultsStrategy = async (unresolvedElements, expectedValues, singleElementCompare, { isNot, isSome, iteration }, { allowEmptyElements = false, allowArrayWithSingleElement = false } = {}) => {
|
|
75
|
-
const { selector, other } = await awaitElementOrArray(unresolvedElements);
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
export const multipleElementResultsStrategy = async (unresolvedElements, expectedValues, singleElementCompare, { isNot, isSome, iteration }, { allowEmptyElements = false, allowArrayWithSingleElement = false, allowObjectExpectedValue = false } = {}) => {
|
|
62
|
+
const { selector, other, multiRemoteSelector } = await awaitElementOrArray(unresolvedElements);
|
|
63
|
+
const isRefetchable = isStrictlyElementArray(selector) || isMultiRemoteElementArray(selector) || isMultiRemoteElements(selector);
|
|
64
|
+
let currentElements = selector;
|
|
65
|
+
if (iteration > 0 && (isStrictlyElementArray(selector) || isMultiRemoteElementsLike(selector))) {
|
|
66
|
+
currentElements = await refreshElementArray(selector);
|
|
78
67
|
}
|
|
79
|
-
const subject = selector ?? other;
|
|
80
|
-
if (!
|
|
68
|
+
const subject = multiRemoteSelector ?? selector ?? other;
|
|
69
|
+
if (!currentElements || (Array.isArray(currentElements) && currentElements.length === 0)) {
|
|
81
70
|
return {
|
|
82
71
|
subject,
|
|
83
72
|
success: isNot ? !allowEmptyElements : false,
|
|
84
73
|
actual: undefined,
|
|
85
|
-
abort: !allowEmptyElements && !
|
|
74
|
+
abort: !allowEmptyElements && !isRefetchable,
|
|
86
75
|
context: { isSome },
|
|
87
76
|
};
|
|
88
77
|
}
|
|
78
|
+
const isUnexpectedPerInstanceValues = !multiRemoteSelector && !isMultiRemoteElementsLike(selector)
|
|
79
|
+
&& getPerInstanceValues(expectedValues, { allowObjectExpectedValue }) !== undefined;
|
|
89
80
|
if (isElement(selector)) {
|
|
90
|
-
const forceFailure = !allowArrayWithSingleElement && Array.isArray(expectedValues);
|
|
81
|
+
const forceFailure = (!allowArrayWithSingleElement && Array.isArray(expectedValues)) || isUnexpectedPerInstanceValues;
|
|
91
82
|
const compareResult = await singleElementCompare(selector, forceFailure ? undefined : expectedValues);
|
|
92
83
|
const success = forceFailure ? !!isNot : compareResult.success;
|
|
93
84
|
return { subject, success, actual: compareResult.actual, abort: forceFailure, context: { isSome } };
|
|
94
85
|
}
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
if (multiRemoteSelector || isMultiRemoteElementsLike(selector)) {
|
|
87
|
+
return multiRemoteElementsResultsStrategy(subject, multiRemoteSelector ?? selector, expectedValues, singleElementCompare, { isNot, isSome }, { allowArrayWithSingleElement, allowObjectExpectedValue });
|
|
88
|
+
}
|
|
89
|
+
const elementsSelector = selector;
|
|
90
|
+
const lengthMismatch = Array.isArray(expectedValues) && expectedValues.length !== elementsSelector.length;
|
|
91
|
+
if (isUnexpectedPerInstanceValues) {
|
|
92
|
+
const results = await Promise.all(Array.from(elementsSelector).map((element, index) => singleElementCompare(element, undefined, index)));
|
|
93
|
+
return { subject, success: !!isNot, abort: true, actual: results.map(({ actual }) => actual), context: { isSome } };
|
|
94
|
+
}
|
|
95
|
+
const settled = await Promise.allSettled(Array.from(elementsSelector).map(async (element, index) => {
|
|
97
96
|
const indexedExpected = Array.isArray(expectedValues) ? expectedValues[index] : expectedValues;
|
|
98
97
|
const forceElementFailure = Array.isArray(indexedExpected)
|
|
99
98
|
|| (lengthMismatch && Array.isArray(expectedValues) && index >= expectedValues.length);
|
|
@@ -105,19 +104,69 @@ export const multipleElementResultsStrategy = async (unresolvedElements, expecte
|
|
|
105
104
|
throw firstRejection.reason;
|
|
106
105
|
}
|
|
107
106
|
const results = settled.map((r) => r.value);
|
|
108
|
-
if (Array.isArray(expectedValues) && expectedValues.length >
|
|
109
|
-
results.push(...Array(expectedValues.length -
|
|
107
|
+
if (Array.isArray(expectedValues) && expectedValues.length > elementsSelector.length) {
|
|
108
|
+
results.push(...Array(expectedValues.length - elementsSelector.length).fill({ success: false, actual: undefined }));
|
|
110
109
|
}
|
|
110
|
+
const actual = results.map(({ actual }) => actual);
|
|
111
111
|
if (lengthMismatch) {
|
|
112
|
-
return { subject, success: !!isNot, actual
|
|
112
|
+
return { subject, success: !!isNot, actual, context: { isSome } };
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
return { subject, success: computeSuccess([results], { isNot, isSome }), actual, context: { isSome } };
|
|
115
|
+
};
|
|
116
|
+
const multiRemoteElementsResultsStrategy = async (subject, multiRemoteSelector, expectedValues, singleElementCompare, { isNot, isSome }, { allowArrayWithSingleElement, allowObjectExpectedValue }) => {
|
|
117
|
+
const isSingleElement = isMultiRemoteElement(multiRemoteSelector);
|
|
118
|
+
const multiRemoteElements = isSingleElement ? [multiRemoteSelector] : multiRemoteSelector;
|
|
119
|
+
const { instances } = multiRemoteElements[0];
|
|
120
|
+
const elementsPerInstance = getElementsPerInstance(multiRemoteElements, instances);
|
|
121
|
+
const perInstanceValues = getPerInstanceValues(expectedValues, { allowObjectExpectedValue });
|
|
122
|
+
const expectedPerInstance = perInstanceValues ?? Object.fromEntries(instances.map((name) => [name, expectedValues]));
|
|
123
|
+
const instanceNamesMismatch = !!perInstanceValues && !hasSameInstanceNames(perInstanceValues, instances);
|
|
124
|
+
const unsupportedArray = isSingleElement && !allowArrayWithSingleElement && Object.values(expectedPerInstance).some(Array.isArray);
|
|
125
|
+
const lengthMismatch = !isSingleElement && instances.some((name) => {
|
|
126
|
+
const value = expectedPerInstance[name];
|
|
127
|
+
return Array.isArray(value) && value.length !== elementsPerInstance[name].length;
|
|
128
|
+
});
|
|
129
|
+
const actualPerInstance = {};
|
|
130
|
+
const resultsPerInstance = await Promise.all(instances.map(async (instance) => {
|
|
131
|
+
const isExpected = instance in expectedPerInstance;
|
|
132
|
+
const instanceValue = expectedPerInstance[instance];
|
|
133
|
+
const elements = elementsPerInstance[instance];
|
|
134
|
+
const results = await Promise.all(elements.map(async (element, index) => {
|
|
135
|
+
const indexedExpected = isSingleElement || !Array.isArray(instanceValue) ? instanceValue : instanceValue[index];
|
|
136
|
+
const forceElementFailure = !isExpected || unsupportedArray
|
|
137
|
+
|| (!isSingleElement && Array.isArray(instanceValue) && (index >= instanceValue.length || Array.isArray(indexedExpected)));
|
|
138
|
+
const elementExpected = forceElementFailure ? undefined : indexedExpected;
|
|
139
|
+
const result = isSingleElement ? await singleElementCompare(element, elementExpected) : await singleElementCompare(element, elementExpected, index);
|
|
140
|
+
return forceElementFailure ? { success: false, actual: result.actual } : result;
|
|
141
|
+
}));
|
|
142
|
+
const actuals = results.map(({ actual }) => actual);
|
|
143
|
+
if (isSingleElement) {
|
|
144
|
+
actualPerInstance[instance] = actuals[0];
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const expectedLength = Array.isArray(instanceValue) ? instanceValue.length : 0;
|
|
148
|
+
actualPerInstance[instance] = [...actuals, ...Array(Math.max(expectedLength - actuals.length, 0)).fill(undefined)];
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}));
|
|
152
|
+
const expected = isSingleElement ? expectedPerInstance : Object.fromEntries(Object.entries(expectedPerInstance).map(([name, value]) => {
|
|
153
|
+
const count = elementsPerInstance[name]?.length ?? 0;
|
|
154
|
+
return [name, Array.isArray(value) || isArrayContainingMatcher(value) ? value : Array(Math.max(count, 1)).fill(value)];
|
|
155
|
+
}));
|
|
156
|
+
if (instanceNamesMismatch || unsupportedArray) {
|
|
157
|
+
return { subject, success: !!isNot, abort: true, actual: actualPerInstance, context: { isSome }, expected };
|
|
158
|
+
}
|
|
159
|
+
if (lengthMismatch) {
|
|
160
|
+
return { subject, success: !!isNot, actual: actualPerInstance, context: { isSome }, expected };
|
|
161
|
+
}
|
|
162
|
+
return { subject, success: computeSuccess(resultsPerInstance, { isNot, isSome }), actual: actualPerInstance, context: { isSome }, expected };
|
|
163
|
+
};
|
|
164
|
+
const computeSuccess = (groups, { isNot, isSome }) => {
|
|
115
165
|
const checkFn = isSome ? isAtLeastOneTrue : isAllTrue;
|
|
116
166
|
const checkNotFn = isSome ? isAtLeastOneFalse : isAllFalse;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return { subject, success, actual: results.map(({ actual }) => actual), context: { isSome } };
|
|
167
|
+
const isNotEmpty = groups.length > 0 && groups.every((results) => results.length > 0);
|
|
168
|
+
const assertionPasses = isNotEmpty && groups.every((results) => isNot ? checkNotFn(results) : checkFn(results));
|
|
169
|
+
return isNot ? !assertionPasses : assertionPasses;
|
|
121
170
|
};
|
|
122
171
|
const isAllTrue = (results) => results.every((res) => res.success === true);
|
|
123
172
|
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, isMultiRemoteBrowser } 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 (
|
|
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
|
-
|
|
52
|
+
if ((isBrowser(subject) && isMultiRemoteBrowser(subject)) || isMultiRemoteElementLike(subject)) {
|
|
53
|
+
expected = labelMultiRemoteValues(expected);
|
|
54
|
+
actual = labelMultiRemoteValues(actual);
|
|
55
|
+
}
|
|
56
|
+
if (isBrowser(subject)) {
|
|
57
|
+
if (isMultiRemoteBrowser(subject)) {
|
|
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
|
}
|
|
@@ -114,15 +141,47 @@ const printArrayWithMatchingItemInRed = (expectedArray, actualArray) => {
|
|
|
114
141
|
.join(', ')}]`;
|
|
115
142
|
return { expectedFormatted, receivedFormatted };
|
|
116
143
|
};
|
|
117
|
-
export const enhanceErrorBe = (subject,
|
|
118
|
-
const { isNot, verb, expectation } = context;
|
|
144
|
+
export const enhanceErrorBe = (subject, actuals, context, options) => {
|
|
145
|
+
const { isNot = false, 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 (
|
|
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(
|
|
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,93 @@
|
|
|
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) && !hasMultiRemoteFlag(browser);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export const hasMultiRemoteFlag = (obj) => {
|
|
83
|
+
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const { isMultiRemote, isMultiremote } = obj;
|
|
87
|
+
return isMultiRemote === true || isMultiremote === true;
|
|
88
|
+
};
|
|
89
|
+
export const isMultiRemoteBrowser = (browser) => hasMultiRemoteFlag(browser);
|
|
90
|
+
export const isBrowser = (obj) => {
|
|
91
|
+
const name = obj?.constructor?.name?.replace(/^bound /, '');
|
|
92
|
+
return name === 'Browser' || !!name?.endsWith('MultiRemoteDriver');
|
|
93
|
+
};
|
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
import { AsymmetricMatcher } from 'expect';
|
|
2
|
-
import {
|
|
2
|
+
import { isMultiRemoteMatcher } from './multiRemoteUtils.js';
|
|
3
3
|
export const isNumber = (value) => typeof value === 'number' && !isNaN(value);
|
|
4
4
|
export const isDefinedNotNumber = (value) => value !== undefined && !isNumber(value);
|
|
5
5
|
export const isDefinedNumberOrNonEmptyObject = (value) => typeof value === 'number' || (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (supportDefaultAsGteThen1 &&
|
|
9
|
-
|
|
6
|
+
const NUMBER_MATCHER_KEYS = ['eq', 'gte', 'lte'];
|
|
7
|
+
export function validateNumberMatcher(expectedValue, { supportDefaultAsGteThen1 } = {}) {
|
|
8
|
+
if (supportDefaultAsGteThen1 && expectedValue === undefined) {
|
|
9
|
+
return new NumberMatcher({ gte: 1 });
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
return
|
|
11
|
+
if (isNumber(expectedValue)) {
|
|
12
|
+
return new NumberMatcher({ eq: expectedValue });
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
if (!isDefinedNumberOrNonEmptyObject(expectedValue)
|
|
15
|
+
|| Object.keys(expectedValue).some((key) => !NUMBER_MATCHER_KEYS.includes(key))
|
|
16
|
+
|| isDefinedNotNumber(expectedValue.eq) || isDefinedNotNumber(expectedValue.gte) || isDefinedNotNumber(expectedValue.lte)
|
|
15
17
|
|| (expectedValue.eq === undefined && expectedValue.gte === undefined && expectedValue.lte === undefined)) {
|
|
16
18
|
throw new Error(`Invalid NumberMatcher. Received: ${JSON.stringify(expectedValue)}`);
|
|
17
19
|
}
|
|
18
|
-
const { eq, gte, lte
|
|
20
|
+
const { eq, gte, lte } = expectedValue;
|
|
19
21
|
if (isNumber(gte) && isNumber(lte) && gte > lte) {
|
|
20
22
|
throw new Error(`Invalid NumberMatcher range: 'gte' (${gte}) cannot be greater than 'lte' (${lte}).`);
|
|
21
23
|
}
|
|
22
|
-
return {
|
|
23
|
-
numberMatcher: defaultExpectedValue ?? new NumberMatcher({ eq, gte, lte }),
|
|
24
|
-
commandOptions: { ...commandOptions, ...restCommandOptions }
|
|
25
|
-
};
|
|
24
|
+
return new NumberMatcher({ eq, gte, lte });
|
|
26
25
|
}
|
|
27
|
-
export function
|
|
26
|
+
export function validateNumberMatcherArray(expectedValues, { supportDefaultAsGteThen1 } = {}) {
|
|
27
|
+
if (isMultiRemoteMatcher(expectedValues)) {
|
|
28
|
+
const perInstanceValues = expectedValues.sample;
|
|
29
|
+
return Object.fromEntries(Object.entries(perInstanceValues).map(([instance, value]) => [instance, validateNumberMatcherArray(value)]));
|
|
30
|
+
}
|
|
28
31
|
if (Array.isArray(expectedValues)) {
|
|
29
|
-
|
|
30
|
-
return { numberMatcher: allNumbers.map(({ numberMatcher }) => numberMatcher), commandOptions };
|
|
32
|
+
return expectedValues.map((value) => validateNumberMatcher(value, { supportDefaultAsGteThen1 }));
|
|
31
33
|
}
|
|
32
|
-
return
|
|
34
|
+
return validateNumberMatcher(expectedValues, { supportDefaultAsGteThen1 });
|
|
33
35
|
}
|
|
34
36
|
export class NumberMatcher extends AsymmetricMatcher {
|
|
35
37
|
sample;
|
|
@@ -83,10 +85,3 @@ export class NumberMatcher extends AsymmetricMatcher {
|
|
|
83
85
|
return this.toString();
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
|
-
export const isEmptyOrLegacyNumberOptions = (value) => {
|
|
87
|
-
if (!isDefinedPlainObject(value)) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
const keys = Object.keys(value);
|
|
91
|
-
return keys.length === 0 || keys.filter((key) => !['eq', 'gte', 'lte'].includes(key)).length > 0;
|
|
92
|
-
};
|
|
@@ -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
|
};
|