Problem
Multi-step click interactions flake when intermediate states are slow or transient — clicking a button to open a dropdown, then clicking an item inside it. toPass reruns the whole block blindly, which causes toggle-button problems (clicking again closes the thing you just opened).
Design
clickFor (internal primitive)
await clickFor(trigger, reveals, async (revealed) => {
await revealed.getByRole('menuitem', { name: 'Delete' }).click();
});
Retry logic:
- If
reveals is not yet visible → click trigger
- Wait for
reveals to become visible (short timeout)
- If still not visible → retry from top
- Run callback
- If callback fails and
reveals is no longer visible → retry from top
- If callback fails and
reveals is still visible → throw (genuine error, not a flake)
Key insight: check visibility before clicking so retries don't toggle the trigger closed. A slow dropdown that's already open gets acted on directly without re-clicking.
No state revert on retry — clickFor targets single-action interactions. Multi-step rollback belongs in .cleanup().
clickMenu (public shorthand)
// Locator item
await clickMenu(trigger, page.getByRole('menuitem', { name: 'Delete' }));
// String shorthand
await clickMenu(trigger, 'Delete');
Built on clickFor — trigger click + wait for menu + click item, with the retry logic above.
Relationship to hoverMenu
hoverMenu already has per-step retry (re-hover current step if next doesn't appear) plus the L-shaped mouse path for diagonal-move safety. They solve similar flake problems but the mechanics are different enough to keep them separate. Both could share the "check before acting" principle though.
Open questions
- What should the default retry count and reveal timeout be?
- Should
clickFor be exported or stay internal? Current thinking: internal only.
- Are there other natural
clickFor wrappers beyond clickMenu? (clickToSelect for custom dropdowns/comboboxes was floated)
- Should
hoverMenu be refactored to use the same "check visibility before hovering" pattern on retries?
Related
- Closes or relates to the broader DX theme of eliminating
toPass boilerplate for common interaction patterns
Problem
Multi-step click interactions flake when intermediate states are slow or transient — clicking a button to open a dropdown, then clicking an item inside it.
toPassreruns the whole block blindly, which causes toggle-button problems (clicking again closes the thing you just opened).Design
clickFor(internal primitive)Retry logic:
revealsis not yet visible → click triggerrevealsto become visible (short timeout)revealsis no longer visible → retry from toprevealsis still visible → throw (genuine error, not a flake)Key insight: check visibility before clicking so retries don't toggle the trigger closed. A slow dropdown that's already open gets acted on directly without re-clicking.
No state revert on retry —
clickFortargets single-action interactions. Multi-step rollback belongs in.cleanup().clickMenu(public shorthand)Built on
clickFor— trigger click + wait for menu + click item, with the retry logic above.Relationship to
hoverMenuhoverMenualready has per-step retry (re-hover current step if next doesn't appear) plus the L-shaped mouse path for diagonal-move safety. They solve similar flake problems but the mechanics are different enough to keep them separate. Both could share the "check before acting" principle though.Open questions
clickForbe exported or stay internal? Current thinking: internal only.clickForwrappers beyondclickMenu? (clickToSelectfor custom dropdowns/comboboxes was floated)hoverMenube refactored to use the same "check visibility before hovering" pattern on retries?Related
toPassboilerplate for common interaction patterns