expect-webdriverio 5.7.0 → 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 (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
package/README.md CHANGED
@@ -7,15 +7,20 @@
7
7
 
8
8
  ## Key Features
9
9
 
10
- - [waits](#default-options) for expectation to succeed
11
- - detailed [error messages](#error-messages)
12
- - works in Mocha, Cucumber, Jest, Jasmine
13
- - builtin [types](docs/Types.md) for TypeScript and JS autocompletion
10
+ - [Waits](#default-options) for expectation to succeed
11
+ - Support single element & multi-elements
12
+ - Detailed [error messages](#error-messages)
13
+ - Works in Mocha, Cucumber, Jest, Jasmine
14
+ - Builtin [types](docs/Types.md) for TypeScript and JS autocompletion
14
15
 
15
16
  ## Installation
16
17
 
17
- 1. `npm install expect` (**Jasmine** and **Jest** users should skip this step)
18
- 2. `npm install expect-webdriverio`
18
+ 1. `npm install expect-webdriverio`
19
+ 2. Install your test framework adapter (if not already set up via WDIO testrunner):
20
+ - **Mocha** (default): `npm install @wdio/mocha-framework`
21
+ - **Jasmine**: `npm install @wdio/jasmine-framework`
22
+ - **Cucumber**: `npm install @wdio/cucumber-framework`
23
+ - **Jest**: No adapter needed — see [Jest Framework section](docs/Framework.md#jest)
19
24
 
20
25
  NOTE: [WebdriverIO](https://github.com/webdriverio/webdriverio) `v9.0.0` or higher is required!
21
26
 
@@ -26,15 +31,15 @@ NOTE: [WebdriverIO](https://github.com/webdriverio/webdriverio) `v9.0.0` or high
26
31
  If you run your tests through the [WDIO testrunner](https://webdriver.io/docs/clioptions) no additional setup is needed. WebdriverIO initialises `expect-webdriverio` and makes `expect` available in the global scope. So you can use it directly in your tests:
27
32
 
28
33
  ```js
29
- const $button = await $('button')
30
- await expect($button).toBeDisplayed()
34
+ const button = await $('button')
35
+ await expect(button).toBeDisplayed()
31
36
  ```
32
37
 
33
38
  See more [Examples](docs/Examples.md)
34
39
 
35
40
  ### Using in a standalone script
36
41
 
37
- If you embed WebdriverIO in a standalone script, make sure you import `expect-webdriverio` before you use it anywhere.
42
+ If you embed WebdriverIO in a standalone (without `@wdio/globals`), make sure you import `expect-webdriverio` before you use it anywhere.
38
43
 
39
44
  ```js
40
45
  import { remote } from 'webdriverio'
@@ -77,8 +82,5 @@ First of all, **feel free to raise an issue with your suggestions or help with P
77
82
 
78
83
  ### Planned
79
84
 
80
- - css matcher
81
- - size matcher
82
- - cookie / localStorage matchers?
83
- - text regex matchers
84
- - multiremote support (if requested)
85
+ - cookie
86
+ - multiremote support (coming)
package/lib/constants.js CHANGED
@@ -1,8 +1,12 @@
1
+ export const DEFAULT_FEATURE_FLAGS = {
2
+ useToHaveTextStrictMultiElementsCompareStrategy: false
3
+ };
1
4
  export const DEFAULT_OPTIONS = {
2
5
  wait: 2000,
3
6
  interval: 100,
4
7
  beforeAssertion: async () => { },
5
8
  afterAssertion: async () => { },
9
+ featureFlags: DEFAULT_FEATURE_FLAGS
6
10
  };
7
11
  export const DEFAULT_OPTIONS_TO_BE_DISPLAYED = {
8
12
  ...DEFAULT_OPTIONS,
package/lib/index.js CHANGED
@@ -3,41 +3,45 @@ import * as wdioMatchers from './matchers.js';
3
3
  import { DEFAULT_OPTIONS, defaultOptionsList } from './constants.js';
4
4
  import createSoftExpect from './softExpect.js';
5
5
  import { SoftAssertService } from './softAssert.js';
6
+ import { oneOf } from './matchers/asymmetrics/oneOf.js';
7
+ import { some as wdioSome } from './matchers/modifiers/some.js';
6
8
  export const wdioCustomMatchers = {};
7
9
  export const matchers = new Map();
8
- const filteredMatchers = {};
9
10
  const extend = expectLib.extend;
10
- Object.keys(wdioMatchers).forEach(matcher => {
11
- if (typeof wdioMatchers[matcher] === 'function') {
12
- filteredMatchers[matcher] = wdioMatchers[matcher];
13
- }
14
- });
15
- expectLib.extend = (m) => {
16
- if (!m || typeof m !== 'object') {
11
+ expectLib.extend = (extendedMatchers) => {
12
+ if (!extendedMatchers || typeof extendedMatchers !== 'object') {
17
13
  return;
18
14
  }
19
- Object.entries(m).forEach(([name, matcher]) => {
15
+ Object.entries(extendedMatchers).forEach(([name, matcher]) => {
20
16
  wdioCustomMatchers[name] = matcher;
21
17
  matchers.set(name, matcher);
22
18
  });
23
- return extend(m);
19
+ return extend(extendedMatchers);
24
20
  };
25
- expectLib.extend(filteredMatchers);
26
- const expectWithSoft = expectLib;
27
- Object.defineProperty(expectWithSoft, 'soft', {
21
+ const filteredWdioMatchers = {};
22
+ Object.entries(wdioMatchers).forEach(([matcher, value]) => {
23
+ if (typeof value === 'function') {
24
+ filteredWdioMatchers[matcher] = value;
25
+ }
26
+ });
27
+ const wdioExpect = expectLib;
28
+ wdioExpect.extend(filteredWdioMatchers);
29
+ wdioExpect.oneOf = oneOf;
30
+ Object.defineProperty(wdioExpect, 'soft', {
28
31
  value: (actual) => createSoftExpect(actual)
29
32
  });
30
- Object.defineProperty(expectWithSoft, 'getSoftFailures', {
33
+ Object.defineProperty(wdioExpect, 'getSoftFailures', {
31
34
  value: (testId) => SoftAssertService.getInstance().getFailures(testId)
32
35
  });
33
- Object.defineProperty(expectWithSoft, 'assertSoftFailures', {
36
+ Object.defineProperty(wdioExpect, 'assertSoftFailures', {
34
37
  value: (testId) => SoftAssertService.getInstance().assertNoFailures(testId)
35
38
  });
36
- Object.defineProperty(expectWithSoft, 'clearSoftFailures', {
39
+ Object.defineProperty(wdioExpect, 'clearSoftFailures', {
37
40
  value: (testId) => SoftAssertService.getInstance().clearFailures(testId)
38
41
  });
39
- export const expect = expectWithSoft;
40
- export const getConfig = () => DEFAULT_OPTIONS;
42
+ export const expect = wdioExpect;
43
+ export const some = wdioSome;
44
+ export const getDefaultOptions = () => DEFAULT_OPTIONS;
41
45
  export const setDefaultOptions = (options) => {
42
46
  Object.entries(options).forEach(([key, value]) => {
43
47
  defaultOptionsList.forEach((option) => {
@@ -47,7 +51,15 @@ export const setDefaultOptions = (options) => {
47
51
  });
48
52
  });
49
53
  };
54
+ export const setFeatureFlags = (featureFlags) => {
55
+ Object.entries(featureFlags).forEach(([ffName, ffValue]) => {
56
+ defaultOptionsList.forEach((option) => {
57
+ option.featureFlags[ffName] = ffValue;
58
+ });
59
+ });
60
+ };
50
61
  export const setOptions = setDefaultOptions;
62
+ export const getConfig = getDefaultOptions;
51
63
  export { SnapshotService } from './snapshot.js';
52
64
  export { SoftAssertService } from './softAssert.js';
53
65
  export { SoftAssertionService } from './softAssertService.js';
@@ -0,0 +1,24 @@
1
+ import { AsymmetricMatcher } from 'expect';
2
+ import { isAsymmetricMatcher } from '../../utils.js';
3
+ export const buildWdioAsymmetricMatchersWithOptions = (expectedValue, options) => {
4
+ if (options) {
5
+ if (Array.isArray(expectedValue)) {
6
+ return expectedValue.map((value) => buildOneAsymmetricMatcherWithOptions(value, options));
7
+ }
8
+ return buildOneAsymmetricMatcherWithOptions(expectedValue, options);
9
+ }
10
+ return expectedValue;
11
+ };
12
+ const buildOneAsymmetricMatcherWithOptions = (expectedValue, options) => {
13
+ if (options) {
14
+ if (isAsymmetricMatcher(expectedValue) && 'withOptions' in expectedValue && typeof expectedValue.withOptions === 'function') {
15
+ return expectedValue.withOptions(options);
16
+ }
17
+ }
18
+ return expectedValue;
19
+ };
20
+ export class WdioAsymmetricMatchers extends AsymmetricMatcher {
21
+ toString() {
22
+ return this.toAsymmetricMatcher();
23
+ }
24
+ }
@@ -0,0 +1,51 @@
1
+ import { compareTextWithArray } from '../../utils.js';
2
+ import { WdioAsymmetricMatchers } from './asymmetricsUtils.js';
3
+ export class OneOfMatcher extends WdioAsymmetricMatchers {
4
+ options = {};
5
+ constructor(...sample) {
6
+ super(sample);
7
+ }
8
+ setOptions(options) {
9
+ this.options = options;
10
+ return this;
11
+ }
12
+ asymmetricMatch(actual) {
13
+ if (actual === null) {
14
+ return this.sample.includes(null);
15
+ }
16
+ else if (typeof actual !== 'string') {
17
+ return false;
18
+ }
19
+ return compareTextWithArray(actual, this.sample.filter(s => s !== null), this.options).success;
20
+ }
21
+ withOptions(options) {
22
+ return new OneOfMatcher(...this.sample).setOptions(options);
23
+ }
24
+ toAsymmetricMatcher() {
25
+ let prefix = '';
26
+ if (this.options.containing) {
27
+ prefix = 'containing';
28
+ }
29
+ else if (this.options.atStart) {
30
+ prefix = 'startingWith';
31
+ }
32
+ else if (this.options.atEnd) {
33
+ prefix = 'endingWith';
34
+ }
35
+ else if (typeof this.options.atIndex === 'number') {
36
+ prefix = `matchingAtIndex<${this.options.atIndex}>`;
37
+ }
38
+ const isArray = Array.isArray(this.sample);
39
+ const samples = isArray ? this.sample : [this.sample];
40
+ const formattedSamples = samples
41
+ .map((s) => (s instanceof RegExp ? s.toString() : `"${s}"`))
42
+ .join(', ');
43
+ if (isArray) {
44
+ prefix = prefix ? prefix + 'OneOf' : 'oneOf';
45
+ }
46
+ return prefix ? `${prefix}<${formattedSamples}>` : formattedSamples;
47
+ }
48
+ }
49
+ export function oneOf(...sample) {
50
+ return new OneOfMatcher(...sample);
51
+ }
@@ -3,27 +3,26 @@ import { waitUntil, enhanceError, compareText } from '../../utils.js';
3
3
  import { DEFAULT_OPTIONS } from '../../constants.js';
4
4
  const log = logger('expect-webdriverio');
5
5
  export async function toHaveClipboardText(browser, expectedValue, options = DEFAULT_OPTIONS) {
6
- const isNot = this.isNot;
7
- const { expectation = 'clipboard text', verb = 'have' } = this;
6
+ const { expectation = 'clipboard text', verb = 'have', isNot, matcherName = 'toHaveClipboardText' } = this;
8
7
  await options.beforeAssertion?.({
9
- matcherName: 'toHaveClipboardText',
8
+ matcherName,
10
9
  expectedValue,
11
10
  options,
12
11
  });
13
- let actual;
14
- const pass = await waitUntil(async () => {
12
+ const { actual, success: pass } = await waitUntil(async () => {
15
13
  await browser.setPermissions({ name: 'clipboard-read' }, 'granted')
16
14
  .catch((err) => log.warn(`Couldn't set clipboard permissions: ${err}`));
17
- actual = await browser.execute(() => window.navigator.clipboard.readText());
18
- return compareText(actual, expectedValue, options).result;
19
- }, isNot, options);
15
+ const actual = await browser.execute(() => window.navigator.clipboard.readText());
16
+ const compareResult = compareText(actual, expectedValue, options);
17
+ return { actual, success: compareResult.success, subject: browser };
18
+ }, isNot, { wait: options.wait, interval: options.interval });
20
19
  const message = enhanceError('browser', expectedValue, actual, this, verb, expectation, '', options);
21
20
  const result = {
22
21
  pass,
23
22
  message: () => message
24
23
  };
25
24
  await options.afterAssertion?.({
26
- matcherName: 'toHaveClipboardText',
25
+ matcherName,
27
26
  expectedValue,
28
27
  options,
29
28
  result
@@ -1,33 +1,41 @@
1
1
  import { waitUntil, enhanceError, compareText } from '../../utils.js';
2
2
  import { DEFAULT_OPTIONS } from '../../constants.js';
3
+ import { expect } from '../../index.js';
3
4
  export async function toHaveLocalStorageItem(browser, key, expectedValue, options = DEFAULT_OPTIONS) {
4
- const isNot = this.isNot;
5
- const { expectation = 'localStorage item', verb = 'have' } = this;
5
+ const { expectation = 'localStorage item', verb = 'have', isNot, matcherName = 'toHaveLocalStorageItem' } = this;
6
6
  await options.beforeAssertion?.({
7
- matcherName: 'toHaveLocalStorageItem',
7
+ matcherName,
8
8
  expectedValue: expectedValue ? [key, expectedValue] : key,
9
9
  options,
10
10
  });
11
- let actual;
12
- const pass = await waitUntil(async () => {
13
- actual = await browser.execute((storageKey) => {
11
+ const paramsCount = arguments.length;
12
+ let expected;
13
+ if (expectedValue === undefined) {
14
+ if (paramsCount > 2) {
15
+ console.warn('[DEPRECATION] Using toHaveLocalStorageItem with undefined is deprecated in favor of expect.anything().');
16
+ }
17
+ expected = expect.anything();
18
+ }
19
+ else {
20
+ expected = expectedValue;
21
+ }
22
+ const { actual, success: pass } = await waitUntil(async () => {
23
+ const actual = await browser.execute((storageKey) => {
14
24
  return localStorage.getItem(storageKey);
15
25
  }, key);
16
- if (expectedValue === undefined) {
17
- return actual !== null;
18
- }
19
26
  if (actual === null) {
20
- return false;
27
+ return { actual, success: false, subject: browser };
21
28
  }
22
- return compareText(actual, expectedValue, options).result;
23
- }, isNot, options);
24
- const message = enhanceError('browser', expectedValue !== undefined ? expectedValue : `localStorage item "${key}"`, actual, this, verb, expectation, key, options);
29
+ const compareResult = compareText(actual, expected, options);
30
+ return { actual, success: compareResult.success, subject: browser };
31
+ }, isNot, { wait: options.wait, interval: options.interval });
32
+ const message = enhanceError('browser', expected, actual, this, verb, expectation, key, options);
25
33
  const result = {
26
34
  pass,
27
35
  message: () => message
28
36
  };
29
37
  await options.afterAssertion?.({
30
- matcherName: 'toHaveLocalStorageItem',
38
+ matcherName,
31
39
  expectedValue: expectedValue ? [key, expectedValue] : key,
32
40
  options,
33
41
  result
@@ -1,25 +1,24 @@
1
1
  import { waitUntil, enhanceError, compareText } from '../../utils.js';
2
2
  import { DEFAULT_OPTIONS } from '../../constants.js';
3
3
  export async function toHaveTitle(browser, expectedValue, options = DEFAULT_OPTIONS) {
4
- const isNot = this.isNot;
5
- const { expectation = 'title', verb = 'have' } = this;
4
+ const { expectation = 'title', verb = 'have', isNot, matcherName = 'toHaveTitle' } = this;
6
5
  await options.beforeAssertion?.({
7
- matcherName: 'toHaveTitle',
6
+ matcherName,
8
7
  expectedValue,
9
8
  options,
10
9
  });
11
- let actual;
12
- const pass = await waitUntil(async () => {
13
- actual = await browser.getTitle();
14
- return compareText(actual, expectedValue, options).result;
15
- }, isNot, options);
10
+ const { actual, success } = await waitUntil(async () => {
11
+ const actual = await browser.getTitle();
12
+ const compareResult = compareText(actual, expectedValue, options);
13
+ return { actual, success: compareResult.success, subject: browser };
14
+ }, isNot, { wait: options.wait, interval: options.interval });
16
15
  const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options);
17
16
  const result = {
18
- pass,
17
+ pass: success,
19
18
  message: () => message
20
19
  };
21
20
  await options.afterAssertion?.({
22
- matcherName: 'toHaveTitle',
21
+ matcherName,
23
22
  expectedValue,
24
23
  options,
25
24
  result
@@ -1,25 +1,24 @@
1
1
  import { waitUntil, enhanceError, compareText } from '../../utils.js';
2
2
  import { DEFAULT_OPTIONS } from '../../constants.js';
3
3
  export async function toHaveUrl(browser, expectedValue, options = DEFAULT_OPTIONS) {
4
- const isNot = this.isNot;
5
- const { expectation = 'url', verb = 'have' } = this;
4
+ const { expectation = 'url', verb = 'have', isNot, matcherName = 'toHaveUrl' } = this;
6
5
  await options.beforeAssertion?.({
7
- matcherName: 'toHaveUrl',
6
+ matcherName,
8
7
  expectedValue,
9
8
  options,
10
9
  });
11
- let actual;
12
- const pass = await waitUntil(async () => {
13
- actual = await browser.getUrl();
14
- return compareText(actual, expectedValue, options).result;
15
- }, isNot, options);
10
+ const { success: pass, actual } = await waitUntil(async () => {
11
+ const actual = await browser.getUrl();
12
+ const compareResult = compareText(actual, expectedValue, options);
13
+ return { actual, success: compareResult.success, subject: browser };
14
+ }, isNot, { wait: options.wait, interval: options.interval });
16
15
  const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options);
17
16
  const result = {
18
17
  pass,
19
18
  message: () => message
20
19
  };
21
20
  await options.afterAssertion?.({
22
- matcherName: 'toHaveUrl',
21
+ matcherName,
23
22
  expectedValue,
24
23
  options,
25
24
  result
@@ -1,8 +1,9 @@
1
- import { executeCommandBe, aliasFn } from '../../utils.js';
1
+ import { executeCommandBe } from '../../utils.js';
2
2
  import { DEFAULT_OPTIONS } from '../../constants.js';
3
3
  export async function toExist(received, options = DEFAULT_OPTIONS) {
4
4
  this.expectation = this.expectation || 'exist';
5
5
  this.verb = this.verb || '';
6
+ this.allowEmptyElements = true;
6
7
  await options.beforeAssertion?.({
7
8
  matcherName: 'toExist',
8
9
  options,
@@ -16,8 +17,12 @@ export async function toExist(received, options = DEFAULT_OPTIONS) {
16
17
  return result;
17
18
  }
18
19
  export function toBeExisting(el, options) {
19
- return aliasFn.call(this, toExist, { verb: 'be', expectation: 'existing' }, el, options);
20
+ this.expectation = 'existing';
21
+ this.verb = 'be';
22
+ return toExist.call(this, el, options);
20
23
  }
21
24
  export function toBePresent(el, options) {
22
- return aliasFn.call(this, toExist, { verb: 'be', expectation: 'present' }, el, options);
25
+ this.expectation = 'present';
26
+ this.verb = 'be';
27
+ return toExist.call(this, el, options);
23
28
  }
@@ -2,29 +2,22 @@ import { executeCommandBe } from '../../utils.js';
2
2
  import { DEFAULT_OPTIONS } from '../../constants.js';
3
3
  export async function toBeSelected(received, options = DEFAULT_OPTIONS) {
4
4
  this.expectation = this.expectation || 'selected';
5
+ this.matcherName = this.matcherName || 'toBeSelected';
5
6
  await options.beforeAssertion?.({
6
- matcherName: 'toBeSelected',
7
+ matcherName: this.matcherName,
7
8
  options,
8
9
  });
9
10
  const result = await executeCommandBe.call(this, received, el => el?.isSelected(), options);
10
11
  await options.afterAssertion?.({
11
- matcherName: 'toBeSelected',
12
+ matcherName: this.matcherName,
12
13
  options,
13
14
  result
14
15
  });
15
16
  return result;
16
17
  }
17
- export async function toBeChecked(el, options = DEFAULT_OPTIONS) {
18
+ export async function toBeChecked(received, options = DEFAULT_OPTIONS) {
18
19
  this.expectation = 'checked';
19
- await options.beforeAssertion?.({
20
- matcherName: 'toBeChecked',
21
- options,
22
- });
23
- const result = await toBeSelected.call(this, el, options);
24
- await options.afterAssertion?.({
25
- matcherName: 'toBeChecked',
26
- options,
27
- result
28
- });
20
+ this.matcherName = 'toBeChecked';
21
+ const result = await toBeSelected.call(this, received, options);
29
22
  return result;
30
23
  }
@@ -1,63 +1,64 @@
1
1
  import { DEFAULT_OPTIONS } from '../../constants.js';
2
- import { compareText, enhanceError, executeCommand, waitUntil, wrapExpectedWithArray } from '../../utils.js';
3
- async function conditionAttr(el, attribute) {
4
- const attr = await el.getAttribute(attribute);
5
- if (typeof attr !== 'string') {
6
- return { result: false, value: attr };
2
+ import { executeCommandWithStrategy } from '../../util/executeCommand.js';
3
+ import { compareText, enhanceError, isAsymmetricMatcher, waitUntil, wrapExpectedWithArray } from '../../utils.js';
4
+ import { expect as wdioExpect } from '../../index.js';
5
+ import { buildWdioAsymmetricMatchersWithOptions } from '../asymmetrics/asymmetricsUtils.js';
6
+ import { OneOfMatcher } from '../asymmetrics/oneOf.js';
7
+ async function conditionAttributeValueMatchWithExpected(el, attribute, expectedValue, options) {
8
+ const attributeValue = await el.getAttribute(attribute);
9
+ if (typeof attributeValue !== 'string' || expectedValue === undefined || expectedValue === null) {
10
+ if (isAsymmetricMatcher(expectedValue)) {
11
+ return { success: expectedValue.asymmetricMatch(attributeValue), actual: attributeValue };
12
+ }
13
+ return { success: attributeValue === expectedValue, actual: attributeValue };
7
14
  }
8
- return { result: true, value: attr };
9
- }
10
- async function conditionAttrAndValue(el, attribute, value, options) {
11
- const attr = await el.getAttribute(attribute);
12
- if (typeof attr !== 'string') {
13
- return { result: false, value: attr };
15
+ else if (expectedValue instanceof OneOfMatcher) {
16
+ return { success: expectedValue.asymmetricMatch(attributeValue), actual: attributeValue };
14
17
  }
15
- return compareText(attr, value, options);
16
- }
17
- export async function toHaveAttributeAndValue(received, attribute, value, options = DEFAULT_OPTIONS) {
18
- const isNot = this.isNot;
19
- const { expectation = 'attribute', verb = 'have' } = this;
20
- let el = await received?.getElement();
21
- let attr;
22
- const pass = await waitUntil(async () => {
23
- const result = await executeCommand.call(this, el, conditionAttrAndValue, options, [attribute, value, options]);
24
- el = result.el;
25
- attr = result.values;
26
- return result.success;
27
- }, isNot, options);
28
- const expected = wrapExpectedWithArray(el, attr, value);
29
- const message = enhanceError(el, expected, attr, this, verb, expectation, attribute, options);
30
- return {
31
- pass,
32
- message: () => message
33
- };
18
+ return compareText(attributeValue, expectedValue, options);
34
19
  }
35
- async function toHaveAttributeFn(received, attribute) {
36
- const isNot = this.isNot;
37
- const { expectation = 'attribute', verb = 'have' } = this;
38
- let el = await received?.getElement();
39
- const pass = await waitUntil(async () => {
40
- const result = await executeCommand.call(this, el, conditionAttr, {}, [attribute]);
41
- el = result.el;
42
- return result.success;
43
- }, isNot, {});
44
- const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, {});
20
+ export async function toHaveAttributeAndValue(received, attribute, expectedValue, options = DEFAULT_OPTIONS) {
21
+ const { expectation = 'attribute', verb = 'have', isNot } = this;
22
+ expectedValue = buildWdioAsymmetricMatchersWithOptions(expectedValue, options);
23
+ const { success: pass, actual: attr, subject: el, context: { isSome } = {} } = await waitUntil(async (iteration) => {
24
+ return await executeCommandWithStrategy({
25
+ unresolvedElements: received,
26
+ expectedValues: expectedValue,
27
+ singleElementCompare: (element, values) => {
28
+ return conditionAttributeValueMatchWithExpected(element, attribute, values, options);
29
+ },
30
+ context: { isNot, iteration },
31
+ strategy: 'NewStrictMultipleElements',
32
+ });
33
+ }, isNot, { wait: options.wait, interval: options.interval });
34
+ const expected = wrapExpectedWithArray(el, attr, expectedValue);
35
+ const message = enhanceError(el, expected, attr, { isNot, isSome }, verb, expectation, attribute, options);
45
36
  return {
46
37
  pass,
47
38
  message: () => message
48
39
  };
49
40
  }
50
41
  export async function toHaveAttribute(received, attribute, value, options = DEFAULT_OPTIONS) {
42
+ const matcherName = 'toHaveAttribute';
43
+ const paramsCount = arguments.length;
51
44
  await options.beforeAssertion?.({
52
- matcherName: 'toHaveAttribute',
45
+ matcherName,
53
46
  expectedValue: [attribute, value],
54
47
  options,
55
48
  });
56
- const result = value !== undefined
57
- ? await toHaveAttributeAndValue.call(this, received, attribute, value, options)
58
- : await toHaveAttributeFn.call(this, received, attribute);
49
+ let expectedValue;
50
+ if (value === undefined) {
51
+ if (paramsCount > 2) {
52
+ console.warn('Using undefined or null as value for toHaveAttribute is deprecated and will be removed in v6.0.0. Please omit the third argument entirely or use toHaveAttribute(el, attribute, wdioExpect.anything(), options).');
53
+ }
54
+ expectedValue = wdioExpect.anything();
55
+ }
56
+ else {
57
+ expectedValue = value;
58
+ }
59
+ const result = await toHaveAttributeAndValue.call(this, received, attribute, expectedValue, options);
59
60
  await options.afterAssertion?.({
60
- matcherName: 'toHaveAttribute',
61
+ matcherName,
61
62
  expectedValue: [attribute, value],
62
63
  options,
63
64
  result
@@ -1,49 +1,47 @@
1
1
  import { DEFAULT_OPTIONS } from '../../constants.js';
2
- import { compareNumbers, enhanceError, executeCommand, numberError, waitUntil, wrapExpectedWithArray } from '../../utils.js';
3
- async function condition(el, options) {
2
+ import { executeCommandWithStrategy } from '../../util/executeCommand.js';
3
+ import { isEmptyOrLegacyNumberOptions, validateNumberArrayAndExtractOptions } from '../../util/numberOptionsUtil.js';
4
+ import { enhanceError, waitUntil, wrapExpectedWithArray } from '../../utils.js';
5
+ async function condition(el, expectedValue) {
4
6
  const children = await el.$$('./*').getElements();
5
- if (typeof options.lte !== 'number' &&
6
- typeof options.gte !== 'number' &&
7
- typeof options.eq !== 'number') {
8
- return {
9
- result: children.length > 0,
10
- value: children?.length
11
- };
7
+ if (expectedValue === undefined) {
8
+ return { success: false, actual: children?.length };
12
9
  }
13
10
  return {
14
- result: compareNumbers(children?.length, options),
15
- value: children?.length
11
+ success: expectedValue?.asymmetricMatch(children?.length) ?? false,
12
+ actual: children?.length
16
13
  };
17
14
  }
18
- export async function toHaveChildren(received, expectedValue, options = DEFAULT_OPTIONS) {
19
- const isNot = this.isNot;
20
- const { expectation = 'children', verb = 'have' } = this;
15
+ export async function toHaveChildren(received, expectedValueOrOptions, options = DEFAULT_OPTIONS) {
16
+ const { expectation = 'children', verb = 'have', isNot, matcherName = 'toHaveChildren' } = this;
17
+ const paramsCount = arguments.length;
18
+ if (paramsCount > 1 && (expectedValueOrOptions === undefined || isEmptyOrLegacyNumberOptions(expectedValueOrOptions))) {
19
+ console.warn('Passing undefined or NumberOptions as the second argument to toHaveChildren is deprecated. Use a NumberMatcher instead. For example, `expect(el).toHaveChildren({ gte: 1 }, options)`');
20
+ }
21
21
  await options.beforeAssertion?.({
22
- matcherName: 'toHaveChildren',
23
- expectedValue,
22
+ matcherName,
23
+ expectedValue: expectedValueOrOptions,
24
24
  options,
25
25
  });
26
- const numberOptions = typeof expectedValue === 'number'
27
- ? { eq: expectedValue }
28
- : expectedValue || {};
29
- let el = await received?.getElement();
30
- let children;
31
- const pass = await waitUntil(async () => {
32
- const result = await executeCommand.call(this, el, condition, numberOptions, [numberOptions]);
33
- el = result.el;
34
- children = result.values;
35
- return result.success;
36
- }, isNot, { ...numberOptions, ...options });
37
- const error = numberError(numberOptions);
38
- const expectedArray = wrapExpectedWithArray(el, children, error);
39
- const message = enhanceError(el, expectedArray, children, this, verb, expectation, '', numberOptions);
26
+ const { numberMatcher: expectedNumber, commandOptions } = validateNumberArrayAndExtractOptions(expectedValueOrOptions, options, { supportDefaultAsGteThen1: true });
27
+ const { success: pass, actual: children, subject, context: { isSome } = {} } = await waitUntil(async (iteration) => {
28
+ return await executeCommandWithStrategy({
29
+ unresolvedElements: received,
30
+ expectedValues: expectedNumber,
31
+ singleElementCompare: (element, expectedValue) => condition(element, expectedValue),
32
+ context: { isNot, iteration },
33
+ strategy: 'NewStrictMultipleElements',
34
+ });
35
+ }, isNot, { wait: commandOptions.wait, interval: commandOptions.interval });
36
+ const expectedArray = wrapExpectedWithArray(subject, children, expectedNumber);
37
+ const message = enhanceError(subject, expectedArray, children, { isNot, isSome }, verb, expectation, '', commandOptions);
40
38
  const result = {
41
39
  pass,
42
40
  message: () => message
43
41
  };
44
42
  await options.afterAssertion?.({
45
- matcherName: 'toHaveChildren',
46
- expectedValue,
43
+ matcherName,
44
+ expectedValue: expectedValueOrOptions,
47
45
  options,
48
46
  result
49
47
  });