Skip to content

Commit 7174df9

Browse files
committed
Reivew waitUntil and fixes multiple not tests
Code review + make Be/Browser matchers test type safe Mock default option to speed up tests Better way to speed-up tests Add tests around default options Fix $$ mock Ensure matcherName is corectly passed + toExists test correctly Reinforce and test `isElementArray` Add missing coverage for `executeCommand` Migrate test of toBeArraySize + add more robust util - Migrate + add coverage for `toBeElementsArrayOfSize` matcher - Fix wait not correctly considered in `toBeElementsArrayOfSize` - Reinforce isElementArray and similar + add more coverage - By default for `DEFAULT_OPTIONS`use a non 0 wait time - Fix global mock missing element.parent Add note on element not found + add potential case to support Add edge cases Add alternative with parametrized in aPI doc Gracefully fails on invalid element types With selector fixed add back Promise of elements case Code review Code review + add unsupported type to toBe matchers Code review Add unsupported type test coverage Code review + add coverage + better awaitElement mechanism Add asymmetric integration tests Code review & coverage Add more tests for toHaveAttribute Array of array is not supported so adapting test for today Review some TODOs Support better failure msg for multiple results in toBe Matchers Properly handle failure colored message for `.no` for multiple elementst Properly support equal for NumberOptions and .not multiple values failure Code review Review coverage for formatMessage + numberOptions Fix 0 not stringily correctly Add .not elements integration tests Add coverage Review docs Finalize `executeCommandBe` tests Increase coverage More stable tests test Add `toHaveText` non-indexed + non-strict length legacy behavior Test more unknown expected type, but maybe some bug? Use supported type instead of unknown for `toHaveElementProperty` Add case of element not found which throws for single element - Note that for multiple element so ElementArray, there is no exception but an empty array Add missing element case and index out of bound from `$()[x]` Review refresh test after rebase - Ensure we return non modified args elements
1 parent 1566131 commit 7174df9

69 files changed

Lines changed: 5223 additions & 2432 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/API.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,11 @@ await expect(browser).toHaveClipboardText(expect.stringContaining('clipboard tex
256256

257257
### Multiples Elements Support
258258

259-
All element matchers work with arrays of elements (e.g., `$$()` results).
260-
- In short, matchers is applied on each elements and must pass for the entire assertion to succeed, so if one fails, the assertions fails.
261-
- See [MutipleElements.md](MultipleElements.md) for more information.
259+
All element matchers support arrays (e.g., `$$()` results).
260+
261+
- Each element must pass the matcher for the assertion to succeed; if any fail, the assertion fails.
262+
- `toHaveText` differ and keep it's legacy behavior.
263+
- See [MultipleElements.md](MultipleElements.md) for details.
262264

263265
#### Usage
264266

@@ -270,16 +272,20 @@ await expect(await $$('#someElem')).toBeDisplayed()
270272
```ts
271273
const elements = await $$('#someElem')
272274

273-
// Single expected value compare with each element's value
275+
// Single value: checked against every element
274276
await expect(elements).toHaveAttribute('class', 'form-control')
275277

276-
// Multiple expected values for exactly 2 elements having exactly 'control1' & 'control2' as values
278+
// Array: each value checked at corresponding element index (must match length)
277279
await expect(elements).toHaveAttribute('class', ['control1', 'control2'])
278280

279-
// Multiple expected values for exactly 2 elements but with more flexibility for the first element's value
281+
// Use asymmetric matchers for flexible matching
280282
await expect(elements).toHaveAttribute('class', [expect.stringContaining('control1'), 'control2'])
281283

282-
// Filtered array also works
284+
// Use RegEx `i` for case insensitive
285+
await expect(elements).toHaveAttribute('class', [/'Control1'/i, 'control2'])
286+
287+
288+
// Works with filtered arrays too
283289
await expect($$('#someElem').filter(el => el.isDisplayed())).toHaveAttribute('class', ['control1', 'control2'])
284290
```
285291

docs/MultipleElements.md

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
# Multiple Elements Support
22

3-
All element matchers work with arrays of elements (e.g., `$$()` results).
4-
- **Strict Length Matching**: If you provide an array of expected values, the number of values must match the number of elements found. A failure occurs if the lengths differ.
5-
- **Index-based Matching**: When using an array of expected values, each element is compared to the value at the corresponding index.
6-
- **Single Value Matching**: If you provide a single expected value, it is compared against *every* element in the array.
7-
- **Asymmetric Matchers**: Asymmetric matchers can be used within the expected values array for more matching flexibility.
8-
- If no elements exist, a failure occurs (except with `toBeElementsArrayOfSize`).
9-
- Options like `StringOptions` or `HTMLOptions` apply to the entire array (except `NumberOptions`).
10-
- The assertion passes only if **all** elements match the expected value(s).
11-
- Using `.not` applies the negation to each element (e.g., *all* elements must *not* display).
3+
Matchers element array support (e.g., `$$()`):
124

13-
**Note:** Strict length matching does not apply on `toHaveText` to preserve existing behavior.
5+
- **Strict Index-based Matching**: If an array of expected values is provided, it must match the elements' count; each value is checked at its index.
6+
- If a single value is provided, every element is compared to it.
7+
- Asymmetric matchers (e.g., `expect.stringContaining`) work within expected value arrays.
8+
- An error is thrown if no elements are found (except with `toBeElementsArrayOfSize`).
9+
- Options like `StringOptions` or `HTMLOptions` apply to the whole array; `NumberOptions` behaves like any expected provided value.
10+
- The assertion passes only if **all** elements match.
11+
- Using `.not` means all elements must **not** match.
12+
13+
**Note:** Strict Index-based matching does not apply to `toHaveText`, since an existing behavior was already in placed.
1414

1515
## Limitations
16-
- An alternative to using `StringOptions` (like `ignoreCase` or `containing`) for a single expected value is to use RegEx (`/MyExample/i`) or Asymmetric Matchers (`expect.stringContaining('Example')`).
17-
- Passing an array of "containing" values, as previously supported by `toHaveText`, is deprecated and not supported for other matchers.
16+
- Instead of `StringOptions` for a single expected value, use RegExp or asymmetric matchers.
17+
- For `ignoreCase` use RegEx (`/MyExample/i`)
18+
- For `containing` use Asymmetric Matchers (`expect.stringContaining('Example')`)
19+
- Passing an array of "containing" values is deprecated and not supported outside `toHaveText`.
1820

1921
## Supported types
20-
21-
Any of the below element types can be passed to `expect`:
22+
You can pass any of these element types to `expect`:
2223
- `ChainablePromiseArray` (the non-awaited case)
2324
- `ElementArray` (the awaited case)
2425
- `Element[]` (the filtered case)
26+
27+
## Alternative
28+
29+
For more granular or explicit per-element validation, use a parameterized test of your framework.
30+
Example in Mocha:
31+
```ts
32+
describe('Element at index of `$$`', function () {
33+
[ { expectedText: 'one', index: 0 },
34+
{ expectedText: 'two', index: 2 },
35+
{ expectedText: 'four', index: 4 },
36+
].forEach(function ( { expectedText, index } ) {
37+
it("Element at $index of `$$('label')` is $expectedText", function () {
38+
expect($$('label')[index]).toHaveText(expectedText);
39+
});
40+
});
41+
});
42+
```

src/matchers/browser/toHaveClipboardText.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,27 @@ export async function toHaveClipboardText(
1010
expectedValue: string | RegExp | WdioAsymmetricMatcher<string>,
1111
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
1212
) {
13-
const isNot = this.isNot
14-
const { expectation = 'clipboard text', verb = 'have' } = this
13+
const { expectation = 'clipboard text', verb = 'have', matcherName = 'toHaveClipboardText', isNot } = this
1514

1615
await options.beforeAssertion?.({
17-
matcherName: 'toHaveClipboardText',
16+
matcherName,
1817
expectedValue,
1918
options,
2019
})
2120

2221
let actual
23-
const pass = await waitUntil(async () => {
24-
await browser.setPermissions({ name: 'clipboard-read' }, 'granted')
22+
const pass = await waitUntil(
23+
async () => {
24+
await browser.setPermissions({ name: 'clipboard-read' }, 'granted')
2525
/**
2626
* changes are that some browser don't support the clipboard API yet
2727
*/
28-
.catch((err) => log.warn(`Couldn't set clipboard permissions: ${err}`))
29-
actual = await browser.execute(() => window.navigator.clipboard.readText())
30-
return compareText(actual, expectedValue, options).result
31-
}, isNot, options)
28+
.catch((err) => log.warn(`Couldn't set clipboard permissions: ${err}`))
29+
actual = await browser.execute(() => window.navigator.clipboard.readText())
30+
return compareText(actual, expectedValue, options).result
31+
},
32+
isNot,
33+
options)
3234

3335
const message = enhanceError('browser', expectedValue, actual, this, verb, expectation, '', options)
3436
const result: ExpectWebdriverIO.AssertionResult = {
@@ -37,7 +39,7 @@ export async function toHaveClipboardText(
3739
}
3840

3941
await options.afterAssertion?.({
40-
matcherName: 'toHaveClipboardText',
42+
matcherName,
4143
expectedValue,
4244
options,
4345
result

src/matchers/browser/toHaveTitle.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ export async function toHaveTitle(
66
expectedValue: string | RegExp | WdioAsymmetricMatcher<string>,
77
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
88
) {
9-
const isNot = this.isNot
10-
const { expectation = 'title', verb = 'have' } = this
9+
const { expectation = 'title', verb = 'have', matcherName = 'toHaveTitle', isNot } = this
1110

1211
await options.beforeAssertion?.({
13-
matcherName: 'toHaveTitle',
12+
matcherName,
1413
expectedValue,
1514
options,
1615
})
1716

1817
let actual
19-
const pass = await waitUntil(async () => {
20-
actual = await browser.getTitle()
18+
const pass = await waitUntil(
19+
async () => {
20+
actual = await browser.getTitle()
2121

22-
return compareText(actual, expectedValue, options).result
23-
}, isNot, options)
22+
return compareText(actual, expectedValue, options).result
23+
},
24+
isNot,
25+
options
26+
)
2427

2528
const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options)
2629
const result: ExpectWebdriverIO.AssertionResult = {
@@ -29,7 +32,7 @@ export async function toHaveTitle(
2932
}
3033

3134
await options.afterAssertion?.({
32-
matcherName: 'toHaveTitle',
35+
matcherName,
3336
expectedValue,
3437
options,
3538
result

src/matchers/browser/toHaveUrl.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ export async function toHaveUrl(
66
expectedValue: string | RegExp | WdioAsymmetricMatcher<string>,
77
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
88
) {
9-
const isNot = this.isNot
10-
const { expectation = 'url', verb = 'have' } = this
9+
const { expectation = 'url', verb = 'have', matcherName = 'toHaveUrl', isNot } = this
1110

1211
await options.beforeAssertion?.({
13-
matcherName: 'toHaveUrl',
12+
matcherName,
1413
expectedValue,
1514
options,
1615
})
1716

1817
let actual
19-
const pass = await waitUntil(async () => {
20-
actual = await browser.getUrl()
18+
const pass = await waitUntil(
19+
async () => {
20+
actual = await browser.getUrl()
2121

22-
return compareText(actual, expectedValue, options).result
23-
}, isNot, options)
22+
return compareText(actual, expectedValue, options).result
23+
},
24+
isNot,
25+
options
26+
)
2427

2528
const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options)
2629
const result: ExpectWebdriverIO.AssertionResult = {
@@ -29,7 +32,7 @@ export async function toHaveUrl(
2932
}
3033

3134
await options.afterAssertion?.({
32-
matcherName: 'toHaveUrl',
35+
matcherName,
3336
expectedValue,
3437
options,
3538
result

src/matchers/element/toHaveAttribute.ts

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,52 +27,55 @@ async function conditionAttributeValueMatchWithExpected(el: WebdriverIO.Element,
2727
}
2828

2929
export async function toHaveAttributeAndValue(received: WdioElementOrArrayMaybePromise, attribute: string, expectedValue: MaybeArray<string | RegExp | WdioAsymmetricMatcher<string>>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) {
30-
const isNot = this.isNot
31-
const { expectation = 'attribute', verb = 'have' } = this
30+
const { expectation = 'attribute', verb = 'have', isNot } = this
3231

3332
let el
3433
let attr
35-
const pass = await waitUntil(async () => {
36-
const result = await executeCommand(received,
37-
undefined,
38-
(elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, (element, expected) => conditionAttributeValueMatchWithExpected(element, attribute, expected, options))
39-
)
40-
41-
el = result.elementOrArray
42-
attr = result.valueOrArray
43-
44-
return result
45-
}, isNot, { wait: options.wait, interval: options.interval })
34+
const pass = await waitUntil(
35+
async () => {
36+
const result = await executeCommand(received,
37+
undefined,
38+
(elements) => defaultMultipleElementsIterationStrategy(elements, expectedValue, (element, expected) => conditionAttributeValueMatchWithExpected(element, attribute, expected, options))
39+
)
40+
41+
el = result.elementOrArray
42+
attr = result.valueOrArray
43+
44+
return result
45+
},
46+
isNot,
47+
{ wait: options.wait, interval: options.interval }
48+
)
4649

4750
const expected = wrapExpectedWithArray(el, attr, expectedValue)
4851
const message = enhanceError(el, expected, attr, this, verb, expectation, attribute, options)
4952

5053
return {
5154
pass,
5255
message: (): string => message
53-
} as ExpectWebdriverIO.AssertionResult
56+
}
5457
}
5558

5659
async function toHaveAttributeFn(received: WdioElementOrArrayMaybePromise, attribute: string, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS) {
57-
const isNot = this.isNot
58-
const { expectation = 'attribute', verb = 'have' } = this
60+
const { expectation = 'attribute', verb = 'have', isNot } = this
5961

6062
let el
6163

62-
const pass = await waitUntil(async () => {
63-
const result = await executeCommand(
64-
received,
65-
undefined,
66-
(elements) => defaultMultipleElementsIterationStrategy(elements, attribute, (el) => conditionAttributeIsPresent(el, attribute))
67-
)
64+
const pass = await waitUntil(
65+
async () => {
66+
const result = await executeCommand(
67+
received,
68+
undefined,
69+
(elements) => defaultMultipleElementsIterationStrategy(elements, attribute, (el) => conditionAttributeIsPresent(el, attribute))
70+
)
6871

69-
el = result.elementOrArray
72+
el = result.elementOrArray
7073

71-
return result
72-
}, isNot, {
73-
wait: options.wait,
74-
interval: options.interval,
75-
})
74+
return result
75+
},
76+
isNot,
77+
{ wait: options.wait, interval: options.interval }
78+
)
7679

7780
const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, options)
7881

@@ -89,8 +92,10 @@ export async function toHaveAttribute(
8992
value?: MaybeArray<string | RegExp | WdioAsymmetricMatcher<string>>,
9093
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
9194
) {
95+
const { matcherName = 'toHaveAttribute' } = this
96+
9297
await options.beforeAssertion?.({
93-
matcherName: 'toHaveAttribute',
98+
matcherName,
9499
expectedValue: [attribute, value],
95100
options,
96101
})
@@ -102,7 +107,7 @@ export async function toHaveAttribute(
102107
: await toHaveAttributeFn.call(this, received, attribute)
103108

104109
await options.afterAssertion?.({
105-
matcherName: 'toHaveAttribute',
110+
matcherName,
106111
expectedValue: [attribute, value],
107112
options,
108113
result

0 commit comments

Comments
 (0)