Skip to content

fix: Better support of existence for toHaveAttribute & toHaveElementProperty#2128

Open
dprevost-LMI wants to merge 11 commits into
webdriverio:mainfrom
dprevost-LMI:better-support-of-existence
Open

fix: Better support of existence for toHaveAttribute & toHaveElementProperty#2128
dprevost-LMI wants to merge 11 commits into
webdriverio:mainfrom
dprevost-LMI:better-support-of-existence

Conversation

@dprevost-LMI

@dprevost-LMI dprevost-LMI commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Ease the signature when checking the existence of an attribute so that we can do

await expect(el).toHaveAttribute(el, 'attribute_name', { wait: 0 })
await expect(elem).toHaveElementProperty('height', { wait: 0 })

instead of

await expect(el).toHaveAttribute(el, 'attribute_name', undefined, { wait: 0 })
await expect(elem).toHaveElementProperty('height', undefined, { wait: 0 })

@dprevost-LMI dprevost-LMI marked this pull request as ready for review June 28, 2026 17:56
@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR improves the ergonomics of toHaveAttribute and toHaveElementProperty by allowing callers to omit the value argument and pass options directly (e.g. toHaveAttribute('attr', { wait: 0 })) instead of needing an explicit undefined placeholder. A new isStringOptions utility discriminates between a value argument and an options object at runtime.

  • Adds overloaded function signatures for both matchers so passing options in the third position is now a first-class pattern, while the old (attr, undefined, options) form is deprecated.
  • Introduces src/util/commandOptionsUtils.ts with an isStringOptions type-guard that uses a whitelist of known option keys to classify the third argument at runtime.
  • Fixes a long-standing bug where toHaveAttributeFn (existence check) always used default timing by passing {} to waitUntil; it now forwards the caller-supplied wait/interval.

Confidence Score: 5/5

The change is safe to merge — the new dispatch logic is well-tested, waitUntil only consumes wait/interval so the options-slicing approach is correct, and all existing behavior is preserved.

The core logic is straightforward: a type-guard classifies the third argument and routes to the existing helper functions with the right options. The fix to forward wait/interval from user-supplied options to waitUntil in the existence-check path is correct and verified by the expanded test suite. No existing call signatures are broken, and the new overloads are fully covered by type tests and unit tests.

src/util/commandOptionsUtils.ts warrants a second look for the some vs every predicate (discussed in prior review thread).

Important Files Changed

Filename Overview
src/util/commandOptionsUtils.ts New type-guard utility; uses some predicate on a whitelist — any object with at least one matching key is treated as options (discussed in prior thread). Orphan comment numbers (// 3, // 4 with no // 1 or // 2) suggest incomplete cleanup.
src/matchers/element/toHaveAttribute.ts Adds overloads and runtime dispatch via isStringOptions; fixes existence check timing; waitUntil now correctly receives caller-provided wait/interval instead of empty object.
src/matchers/element/toHaveElementProperty.ts Mirrors toHaveAttribute pattern; fixes the long-standing undefined-expectedValue bug by extending the null check to also cover undefined; adds correct message branching for existence vs value assertions.
types/expect-webdriverio.d.ts Expands toHaveAttribute and toHaveElementProperty type declarations with overloaded call signatures; also adds number to the property value union and deprecation annotations.
test/matchers/element/toHaveAttribute.test.ts Substantially expanded test suite covering existence, value-match, deprecated overloads, and error message snapshots; switches from .call({}) to a structured thisContext pattern.
test/util/commandOptionsUtils.test.ts New unit tests for isStringOptions covering empty objects, all whitelisted keys, primitives, RegExps, arrays, and asymmetric matchers.
docs/API.md Removes the toHaveAttr section entirely and adds new existence-check usage examples without sub-headings, making the section harder to navigate; no deprecation notice for toHaveAttr remains.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["toHaveAttribute(el, attr, valueOrOptions, options)"] --> B{isStringOptions check}
    B -- "true - e.g. wait: 0" --> C[options = valueOrOptions\nvalue = undefined]
    B -- "false - string/RegExp/undefined" --> D[value = valueOrOptions\noptions = 4th arg or DEFAULT_OPTIONS]
    C --> E{value defined?}
    D --> E
    E -- yes --> F[toHaveAttributeAndValue\nwaitUntil with wait+interval]
    E -- no --> G[toHaveAttributeFn\nwaitUntil with wait+interval]
    F --> H[compareText result]
    G --> I[existence check result]

    J["toHaveElementProperty(el, prop, valueOrOptions, options)"] --> K{isStringOptions check}
    K -- "true" --> L[options = valueOrOptions\nvalue = undefined]
    K -- "false" --> M[value = valueOrOptions\noptions = 4th arg or DEFAULT]
    L --> N{value undefined?}
    M --> N
    N -- "yes - existence message format" --> O[condition: null/undefined expectedValue triggers existence path]
    N -- "no - value message format" --> O
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["toHaveAttribute(el, attr, valueOrOptions, options)"] --> B{isStringOptions check}
    B -- "true - e.g. wait: 0" --> C[options = valueOrOptions\nvalue = undefined]
    B -- "false - string/RegExp/undefined" --> D[value = valueOrOptions\noptions = 4th arg or DEFAULT_OPTIONS]
    C --> E{value defined?}
    D --> E
    E -- yes --> F[toHaveAttributeAndValue\nwaitUntil with wait+interval]
    E -- no --> G[toHaveAttributeFn\nwaitUntil with wait+interval]
    F --> H[compareText result]
    G --> I[existence check result]

    J["toHaveElementProperty(el, prop, valueOrOptions, options)"] --> K{isStringOptions check}
    K -- "true" --> L[options = valueOrOptions\nvalue = undefined]
    K -- "false" --> M[value = valueOrOptions\noptions = 4th arg or DEFAULT]
    L --> N{value undefined?}
    M --> N
    N -- "yes - existence message format" --> O[condition: null/undefined expectedValue triggers existence path]
    N -- "no - value message format" --> O
Loading

Reviews (6): Last reviewed commit: "Fix doc" | Re-trigger Greptile

Comment thread types-checks-filter-out-node_modules.js Outdated
Comment on lines +41 to +43
// Condition B: At least one key in the object exists in our whitelist
return objKeys.some(key => allowedKeys.has(key))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "At least one matching key" heuristic can produce false positives

isStringOptions returns true as long as the object contains any key from the allowlist. An object like { wait: 1, href: '/path' } (e.g. someone incorrectly passing a WebdriverIO link object) would be classified as StringOptions, causing toHaveAttribute to skip the value check entirely and fall through to the attribute-existence path. A stricter rule — e.g. returning true only when all keys are in the allowlist — would prevent this class of silent misrouting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All keys are optional so I cannot do so. Do you have other suggestions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion isn't about requiring all allowlisted keys to be present — it's the reverse: requiring that all keys that are present must be in the allowlist. You'd change some to every:

Suggested change
// Condition B: At least one key in the object exists in our whitelist
return objKeys.some(key => allowedKeys.has(key))
}
return objKeys.every(key => allowedKeys.has(key))
  • {}true (vacuously passes, as before)
  • { wait: 1 }true
  • { wait: 1, href: '/path' }false (href isn't in the allowlist)

This closes the false-positive gap without requiring any specific key to be present, which respects the fully-optional nature of the interface.

…ndefined

- Deprecate `toHaveAttr`, there is no gain to have it since `toHaveAttribute` is clearer
- Allow passing options to `toHaveAttribute` without needing `undefined`
- All case passing `undefined` as value is deprecated.
- Add some refactoring done during support of $$ to ease changes
-Fix options not passed when checking prop existences
@dprevost-LMI dprevost-LMI force-pushed the better-support-of-existence branch from 41a7811 to 832a822 Compare June 29, 2026 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant