feat: Support $$() withtoHave element's matchers - #1990
Conversation
811216d to
62a200b
Compare
$$ aka ElementArray$$ with all matchers
$$ with all matchers$$() with all matchers
$$() with all matchers$$() with all element's matchers
f8f6ba0 to
78a6cf0
Compare
cd23183 to
aec39c2
Compare
Type-Definition vs. Implementation Analysis1.
|
| Matcher | Issue | Severity |
|---|---|---|
toHaveAttr / toHaveClass |
No FnArray → types hide MaybeArray support for $$() |
Low (deprecated) |
toHaveComputedLabel / toHaveComputedRole |
Inner singleElementCompare uses HTMLOptions instead of StringOptions |
Low (silent) |
toHaveAttribute |
Deprecated undefined overload appears in $$() types but not in impl |
Low (docs) |
matchNumber |
Array OR-branch is unreachable via any public code path | Medium (dead code) |
toHaveChildren condition |
Param typed as MaybeArray<NumberMatcher> when it always receives a scalar |
Low (type accuracy) |
toHaveHTML / toHaveComputedLabel / toHaveComputedRole / toHaveText |
Single-element array overload semantics undocumented in the deprecation notice | Medium (UX clarity) |
Type Definition vs Implementation Analysis
|
| Matcher | Issue | Severity |
|---|---|---|
toHaveComputedLabel & toHaveComputedRole |
Internal singleElementCompare typed as HTMLOptions instead of StringOptions |
Medium – real type error |
toHaveAttribute |
value variable loses MaybeArray type after assignment |
Low – runtime ok, type-unsafe |
toBeElementsArrayOfSize |
Return type Promise<void> & Promise<WebdriverIO.ElementArray> is unsatisfiable |
Medium – misleading .d.ts |
toHaveChildren |
condition() typed as MaybeArray<NumberMatcher> but only ever receives scalar |
Low – overly broad |
toHaveText (strict mode) |
Uses fillSingleExpectedForElementArray instead of wrapExpectedWithArray for error messages |
Medium – inconsistent failure output |
toHaveWidth, toHaveHeight |
Import wrapExpectedWithArray directly from elementsUtil.js instead of via utils.js |
Trivial – cosmetic |
|
Here's a comprehensive analysis of the discrepancies between the type definitions and implementations: 1.
|
| Matcher | Issue | Severity |
|---|---|---|
toHaveWidth / toHaveHeight |
Missing deprecated NumberOptions overload in type def (inconsistent with toHaveChildren) |
Medium |
toBeElementsArrayOfSize |
Return type Promise<void> & Promise<WebdriverIO.ElementArray> doesn't match actual AssertionResult |
Medium |
toHaveComputedLabel / toHaveComputedRole |
Internal helper typed as HTMLOptions but public API is StringOptions |
Low |
toHaveElementProperty |
Stale TODO — MaybeArray already supported via executeCommandWithStrategy |
Low |
toHaveAttr / toHaveClass |
Type hides MaybeArray for $$() even though impl supports it |
Low (deprecated) |
toHaveHref.ts |
Typo in JSDoc | Trivial |
@greptileai, can you review all the above since I fixed a few? |
|
@greptileai, can you expand on those two: why you are flagging them and what the proposed fixes are
|
|
Now I have the full picture. Let me give you specific answers for both. Issue 1: Misleading diff for The problem is a shape mismatch between what In // legacyMultipleElementResultsStrategy
actual: results.length === 1 ? results[0].value : results.map(({ value }) => value),
// ^^ scalar "" when 1 elementBut in // fillSingleExpectedForElementArray
if (isElementArrayLike(subject) && !Array.isArray(value)) {
return Array(fillerlength).fill(value) // ["t"] for 1-element $$()
}So The Fix: Remove the scalar special-case in actual: results.map(({ value }) => value)Or, in Issue 2: On // main branch — src/utils.ts
export {
aliasFn, compareNumbers, enhanceError, executeCommand,
executeCommandBe, waitUntil, wrapExpectedWithArray
}On this PR branch it's dropped entirely — not in import { utils } from 'expect-webdriverio'
utils.executeCommand(...)...will get a runtime error with no type-level warning. The PR description only lists |
@greptileai, First issue: I pushed a fix. Second issue: it does not matter; I consider this one internal and should not be exposed to the public. Let's skip that in your report |
|
The fix looks correct — On |
|
@greptileai, expand on the below and propose some fixes
|
|
Here are all three issues broken down with concrete fixes: Issue 1:
|
| Issue | File | Severity | Impact |
|---|---|---|---|
forceFailure + isNot=true → silent pass (single elem) |
executeCommand.ts |
🔴 Bug | .not on invalid array input passes instead of fails |
forceFailure + isNot=true → full timeout wait (length mismatch) |
executeCommand.ts |
🟡 UX | Correct result, but wastes the full wait duration before failing |
OR-vs-AND runtime flip via allowArrayWithSingleElement |
executeCommand.ts / toHaveHTML.ts |
🟠 Semantic | Same code, different semantics depending on DOM element count at runtime |
MaybeArray cast hiding array type |
toHaveElementProperty.ts |
🟡 Type safety | Compile-time lie; hides future regression risk |
|
@greptileai, please verify the cases you mentioned in your previous comment; I've enumerated them below with answers!
This one should be fixed; please validate again
This one is acknowledged with a todo for later
This is acknowledged with a todo to fix later with the asymmetric matcher oneOf or anyOf
This one is fixed; validate again |
|
I've verified all three items against the current code in Bug A — Silent pass on invalid input (single element branch) ✅ FixedThe fix hinges on how const result = isNot !== (await condition()) // loop breaks when trueOld behavior (presumably
New behavior (
The silent pass is gone. One trade-off to flag: with Issue 3 —
|
| Issue | Status |
|---|---|
| Bug A — silent pass on invalid input | ✅ Fixed (trade-off: isNot=true + invalid input waits full timeout, same class as Bug B) |
| Bug B — full timeout on array length mismatch | ✅ Acknowledged, TODO for shouldAbort |
Issue 2 — OR/AND asymmetry in allowArrayWithSingleElement |
✅ Acknowledged, TODO for oneOf/anyOf |
Issue 3 — MaybeArray cast in toHaveElementProperty |
✅ Fixed |
|
@greptile, what do you mean by the following? How to fix?
|
|
Looking at the type definition in the PR: Before (original, 2 params): type FnWhenElementOrArrayLike<ActualT, Fn> =
ActualT extends ElementArrayLike ? Fn :
ActualT extends ElementLike ? Fn : neverAfter (this PR, 3 params): type FnWhenElementOrArrayLike<ActualT, FnElement, FnArray = FnElement> =
ActualT extends ElementArrayLike ? FnArray :
ActualT extends ElementLike ? FnElement : neverThe "type-level breaking change" is that the conditional type now resolves to two potentially distinct types ( Is this actually a problem in practice? No, for two reasons:
If you want to be conservative, the cleanest fix is to leave the original 2-param type untouched and introduce a separate helper only used internally: // Keep unchanged for backward compat:
type FnWhenElementOrArrayLike<ActualT, Fn> =
ActualT extends ElementArrayLike ? Fn :
ActualT extends ElementLike ? Fn : never
// New discriminated helper used internally by matchers that need different $() vs $$() signatures:
type FnWhenElementOrArrayLikeDiscriminated<ActualT, FnElement, FnArray = FnElement> =
ActualT extends ElementArrayLike ? FnArray :
ActualT extends ElementLike ? FnElement : neverThen matchers like That said, the current approach in the PR is fine — this is a theoretical concern, not a practical one. Adding a |
@greptileai, It resolves to 2 different functions only if you pass 2 different ones; otherwise, it stays like before when passing one, so no breaking. And if you pass 2 FNs, it is because you want them different, so it is totally fine and backward-compatible! Keep a note and update the Greptile Summary comment! |
|
@greptileai, please explain the following with examples and maybe a fix?
|
|
Here's a concrete explanation of the The Type in Question// types/expect-webdriverio.d.ts
type FnWhenElementOrArrayLike<ActualT, FnElement, FnArray = FnElement> =
ActualT extends ElementArrayLike ? FnArray :
ActualT extends ElementLike ? FnElement :
neverThis is a distributive conditional type — when The Widening ProblemScenario 1 — Union
|
Current (FnWhenElementOrArrayLike per-member) |
Conservative (two-interface) | |
|---|---|---|
Union ActualT |
Widens to FnElement | FnArray — both overloads callable |
Resolves to a clean single interface |
| API-extractor output | May serialize each member as the widened union | Serializes as the concrete interface name |
| IDE intellisense | Shows both overloads for $() elements |
Shows only the correct overloads |
| Maintenance | Each new matcher needs careful FnWhenElementOrArrayLike<> wrapping |
Add to one or both interfaces |
If API-extractor is not part of your release pipeline (i.e., you ship the source .d.ts directly), the current approach is acceptable and simpler to maintain. The two-interface approach is only strictly necessary if the generated rollup .d.ts is what gets published to npm.
Partially fixes #512.
Fixes #1717.
Summary
Adds official
$$()(element array/elements) support totoHavematchers (toBedone in #2149). Previously, TypeScript signatures allowed arrays (by mistake in this PR), but the implementation didn't support them properly.Example:
Current Behaviour Issues
toHaveTextandtoBeElementsArrayOfSizeofficially support elementstoHaveHTMLpartially implemented support for elements but failed to do so properly$$()or filtered$$().filter()throws errorstoHaveTextwith empty elements incorrectly passestoHaveTextdoesn't trim text for multiple elements (inconsistent with single element behaviour)toHaveTextdoesn't do strict and index-based comparison but only loose comparison (kept)toHaveText,toHaveHTML,toHaveElementClass,toHaveComputedLabel&toHaveComputedRolesupport an array of expected values with a single element, which must still workError handling
$$returns only one element and we have one expected value, the error message (CHANGED)$$returns only one element and an array of expectations is passed, the error message (CHANGED)Note: All the above have been changed to show all the elements' values and not just those not matching
Official
$$()SupportThis PR adds official support for
toHaveelement matchers.$$()support may incidentally enableexpect()to work with multi-remote, this is not intended and may break at any time. Official multi-remote support is tracked here and is not yet available.Types Support
Behavior
The following must pass when all elements have the HTML; otherwise, it fails.
toHavematchers, you can provide a single expected value or an array; strict array comparison is used.StringOptions,HTMLOptions,ToBeDisplayedOptionsapply to the whole array (not per element).NumberNumbercan be provided as an array, but the formerNumberOptionsis not supported.Array Comparison Behaviour
{ trim: false }).toHaveText(deprecated), elements are not compared to any value in the expected array—only by index.isNot
The following must pass when all elements are not displayed/not have the text; otherwise, it fails.
Edge cases
No elements found
When no elements are found, we fail (nearly) at all times with or without
.not, even if the expected is an empty array.toBeElementsArrayOfSize(0)&toExistsare the only ones supporting empty elements without failuresexpect.arrayContaining
Only
toHaveTextwill do acontainingarray behaviour with the followingWe should consider deprecating the above for
expect.arrayContainingand supporting it, which is not the case at allError handling
Below are examples of colour failures.
toHaveTextandtoBeDisplayedmatchers.nottoBeare handled by addingnotin the valuestoHavematchers, a more complex method was used to highlight those actually matching (red highlight)BREAKING
toHaveClassContainingmatchers.executeCommandremoved; could be brought back if anyone used itFuture Considerations
toBeElementsArrayOfSize.ts, consider updating the array in the non-awaited case by awaiting ittoHaveElementPropertytoHaveAttributesupports properly optional expected value for property existenceanyOf()oroneOf()for theORaka containing behaviour existing on a single element and ontoHaveText.any()for element existence with$$()refreshElementapproach when the elements and expected values length do not match or when there are failures?