expect-webdriverio 5.6.9 → 6.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 (45) hide show
  1. package/README.md +16 -14
  2. package/jasmine-wdio-expect-async.d.ts +58 -18
  3. package/jasmine.d.ts +9 -16
  4. package/lib/constants.js +4 -0
  5. package/lib/index.js +30 -18
  6. package/lib/jasmineUtils.js +3 -2
  7. package/lib/matchers/asymmetrics/asymmetricsUtils.js +24 -0
  8. package/lib/matchers/asymmetrics/oneOf.js +51 -0
  9. package/lib/matchers/browser/toHaveClipboardText.js +8 -9
  10. package/lib/matchers/browser/toHaveLocalStorageItem.js +22 -14
  11. package/lib/matchers/browser/toHaveTitle.js +9 -10
  12. package/lib/matchers/browser/toHaveUrl.js +8 -9
  13. package/lib/matchers/element/toBeExisting.js +8 -3
  14. package/lib/matchers/element/toBeSelected.js +6 -13
  15. package/lib/matchers/element/toHaveAttribute.js +46 -45
  16. package/lib/matchers/element/toHaveChildren.js +30 -32
  17. package/lib/matchers/element/toHaveComputedLabel.js +21 -20
  18. package/lib/matchers/element/toHaveComputedRole.js +21 -20
  19. package/lib/matchers/element/toHaveElementClass.js +57 -0
  20. package/lib/matchers/element/toHaveElementProperty.js +40 -32
  21. package/lib/matchers/element/toHaveHTML.js +22 -23
  22. package/lib/matchers/element/toHaveHeight.js +23 -28
  23. package/lib/matchers/element/toHaveSize.js +16 -14
  24. package/lib/matchers/element/toHaveStyle.js +20 -14
  25. package/lib/matchers/element/toHaveText.js +26 -44
  26. package/lib/matchers/element/toHaveWidth.js +23 -28
  27. package/lib/matchers/elements/toBeElementsArrayOfSize.js +25 -30
  28. package/lib/matchers/mock/toBeRequestedTimes.js +12 -17
  29. package/lib/matchers/mock/toBeRequestedWith.js +13 -23
  30. package/lib/matchers/modifiers/some.js +15 -0
  31. package/lib/matchers.js +1 -1
  32. package/lib/tsconfig.build.tsbuildinfo +1 -1
  33. package/lib/util/commandOptionsUtils.js +6 -0
  34. package/lib/util/elementsUtil.js +74 -4
  35. package/lib/util/executeCommand.js +94 -6
  36. package/lib/util/formatMessage.js +87 -32
  37. package/lib/util/numberOptionsUtil.js +92 -0
  38. package/lib/util/refetchElements.js +30 -5
  39. package/lib/util/stringUtil.js +12 -0
  40. package/lib/util/waitUntil.js +44 -0
  41. package/lib/utils.js +128 -105
  42. package/package.json +13 -11
  43. package/types/expect-global.d.ts +2 -1
  44. package/types/expect-webdriverio.d.ts +628 -136
  45. package/lib/matchers/element/toHaveClass.js +0 -61
@@ -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
  };
@@ -0,0 +1,92 @@
1
+ import { AsymmetricMatcher } from 'expect';
2
+ import { isDefinedPlainObject } from './commandOptionsUtils.js';
3
+ export const isNumber = (value) => typeof value === 'number' && !isNaN(value);
4
+ export const isDefinedNotNumber = (value) => value !== undefined && !isNumber(value);
5
+ export const isDefinedNumberOrNonEmptyObject = (value) => typeof value === 'number' || (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0);
6
+ export function validateNumberAndExtractOptions(expectedValue, commandOptions, { supportDefaultAsGteThen1 } = {}) {
7
+ let defaultExpectedValue = undefined;
8
+ if (supportDefaultAsGteThen1 && (expectedValue === undefined || (isDefinedPlainObject(expectedValue) && expectedValue.eq === undefined && expectedValue.gte === undefined && expectedValue.lte === undefined))) {
9
+ defaultExpectedValue = new NumberMatcher({ gte: 1 });
10
+ }
11
+ else if (isNumber(expectedValue)) {
12
+ return { numberMatcher: new NumberMatcher({ eq: expectedValue }), commandOptions };
13
+ }
14
+ else if (!isDefinedNumberOrNonEmptyObject(expectedValue) || isDefinedNotNumber(expectedValue.eq) || isDefinedNotNumber(expectedValue.gte) || isDefinedNotNumber(expectedValue.lte)
15
+ || (expectedValue.eq === undefined && expectedValue.gte === undefined && expectedValue.lte === undefined)) {
16
+ throw new Error(`Invalid NumberMatcher. Received: ${JSON.stringify(expectedValue)}`);
17
+ }
18
+ const { eq, gte, lte, ...restCommandOptions } = expectedValue ?? {};
19
+ if (isNumber(gte) && isNumber(lte) && gte > lte) {
20
+ throw new Error(`Invalid NumberMatcher range: 'gte' (${gte}) cannot be greater than 'lte' (${lte}).`);
21
+ }
22
+ return {
23
+ numberMatcher: defaultExpectedValue ?? new NumberMatcher({ eq, gte, lte }),
24
+ commandOptions: { ...commandOptions, ...restCommandOptions }
25
+ };
26
+ }
27
+ export function validateNumberArrayAndExtractOptions(expectedValues, commandOptions, { supportDefaultAsGteThen1 } = {}) {
28
+ if (Array.isArray(expectedValues)) {
29
+ const allNumbers = expectedValues.map((value) => validateNumberAndExtractOptions(value, commandOptions, { supportDefaultAsGteThen1 }));
30
+ return { numberMatcher: allNumbers.map(({ numberMatcher }) => numberMatcher), commandOptions };
31
+ }
32
+ return validateNumberAndExtractOptions(expectedValues, commandOptions, { supportDefaultAsGteThen1 });
33
+ }
34
+ export class NumberMatcher extends AsymmetricMatcher {
35
+ sample;
36
+ constructor(sample) {
37
+ super(sample);
38
+ this.sample = sample;
39
+ }
40
+ asymmetricMatch(actual) {
41
+ if (actual === undefined) {
42
+ return false;
43
+ }
44
+ if (isNumber(this.sample)) {
45
+ return actual === this.sample;
46
+ }
47
+ if (isNumber(this.sample.eq)) {
48
+ return actual === this.sample.eq;
49
+ }
50
+ if (isNumber(this.sample.gte) && isNumber(this.sample.lte)) {
51
+ return actual >= this.sample.gte && actual <= this.sample.lte;
52
+ }
53
+ if (isNumber(this.sample.gte)) {
54
+ return actual >= this.sample.gte;
55
+ }
56
+ if (isNumber(this.sample.lte)) {
57
+ return actual <= this.sample.lte;
58
+ }
59
+ return false;
60
+ }
61
+ toAsymmetricMatcher() {
62
+ if (isNumber(this.sample)) {
63
+ return `${this.sample}`;
64
+ }
65
+ if (isNumber(this.sample.eq)) {
66
+ return `${this.sample.eq}`;
67
+ }
68
+ if (isNumber(this.sample.gte) && isNumber(this.sample.lte)) {
69
+ return `>= ${this.sample.gte} && <= ${this.sample.lte}`;
70
+ }
71
+ if (isNumber(this.sample.gte)) {
72
+ return `>= ${this.sample.gte}`;
73
+ }
74
+ if (isNumber(this.sample.lte)) {
75
+ return `<= ${this.sample.lte}`;
76
+ }
77
+ return 'Incorrect number options provided';
78
+ }
79
+ toString() {
80
+ return this.toAsymmetricMatcher();
81
+ }
82
+ jasmineToString() {
83
+ return this.toString();
84
+ }
85
+ }
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,10 +1,35 @@
1
- import { DEFAULT_OPTIONS } from '../constants.js';
2
- import { isElementArray } from './elementsUtil.js';
3
- export const refetchElements = async (elements, wait = DEFAULT_OPTIONS.wait, full = false) => {
4
- if (elements && wait > 0 && (elements.length === 0 || full) && isElementArray(elements) && elements.parent && elements.foundWith && elements.foundWith in elements.parent) {
1
+ import { isStrictlyElementArray } from './elementsUtil.js';
2
+ export const refetchElements = async (elements) => {
3
+ if (elements
4
+ && isStrictlyElementArray(elements)
5
+ && elements.parent && elements.foundWith && elements.foundWith in elements.parent) {
5
6
  const browser = elements.parent;
6
7
  const $$ = browser[elements.foundWith];
7
- elements = await $$.call(browser, elements.selector, ...elements.props);
8
+ return await $$.call(browser, elements.selector, ...elements.props);
8
9
  }
9
10
  return elements;
10
11
  };
12
+ export const syncronizeElements = async (subject, refetchedElements) => {
13
+ if (subject instanceof Promise) {
14
+ await syncronizeChainableElementArray(subject, refetchedElements);
15
+ }
16
+ else if (isStrictlyElementArray(subject)) {
17
+ synchronizeElementArray(subject, refetchedElements);
18
+ }
19
+ };
20
+ export const syncronizeChainableElementArray = async (subject, refetchedElements) => {
21
+ const awaitedSubject = await subject;
22
+ if (isStrictlyElementArray(awaitedSubject) && refetchedElements) {
23
+ synchronizeElementArray(await awaitedSubject.getElements(), refetchedElements);
24
+ }
25
+ };
26
+ export const synchronizeElementArray = (subject, refetchedElements) => {
27
+ subject.length = refetchedElements.length;
28
+ for (let index = 0; index < refetchedElements.length; index++) {
29
+ subject[index] = refetchedElements[index];
30
+ }
31
+ };
32
+ export const refreshElementArray = async (subject) => {
33
+ const refetchedElements = await refetchElements(subject);
34
+ synchronizeElementArray(subject, refetchedElements);
35
+ };
@@ -0,0 +1,12 @@
1
+ export const isString = (value) => typeof value === 'string';
2
+ export const toJsonString = (value) => {
3
+ if (isString(value)) {
4
+ return value;
5
+ }
6
+ try {
7
+ return JSON.stringify(value) ?? String(value);
8
+ }
9
+ catch {
10
+ return String(value);
11
+ }
12
+ };
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_OPTIONS } from '../constants.js';
2
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ export const waitUntil = async (condition, isNot = false, { wait = DEFAULT_OPTIONS.wait, interval = DEFAULT_OPTIONS.interval } = {}) => {
4
+ if (wait === 0) {
5
+ return await condition(0);
6
+ }
7
+ let error;
8
+ let result;
9
+ let iteration = 0;
10
+ try {
11
+ const start = Date.now();
12
+ while (true) {
13
+ if (Date.now() - start > wait) {
14
+ throw new Error('timeout');
15
+ }
16
+ try {
17
+ result = await condition(iteration++);
18
+ const passed = isNot !== result?.success;
19
+ error = undefined;
20
+ if (passed) {
21
+ break;
22
+ }
23
+ else if (result?.abort) {
24
+ return { ...result, success: isNot };
25
+ }
26
+ await sleep(interval);
27
+ }
28
+ catch (err) {
29
+ error = err;
30
+ await sleep(interval);
31
+ }
32
+ }
33
+ if (error) {
34
+ throw error;
35
+ }
36
+ return { ...result, success: !isNot };
37
+ }
38
+ catch {
39
+ if (error) {
40
+ throw error;
41
+ }
42
+ return { ...result, subject: result?.subject, success: isNot, actual: result?.actual };
43
+ }
44
+ };