fix: exclude mutating actions from the expectation retry loop - #249
Open
ibrasho wants to merge 1 commit into
Open
fix: exclude mutating actions from the expectation retry loop#249ibrasho wants to merge 1 commit into
ibrasho wants to merge 1 commit into
Conversation
waitForExpectation() re-runs the whole call under a 1s cap per attempt. For an assertion that is the right tool. For an action it re-fires a side effect: a click whose first attempt needs more than 1s has already opened its dialog, and the retry then aims at a button the overlay now covers. The test burns the whole timeout and fails on a click that worked. A retried Enter double-submits a form; a retried drag starts from an element the first attempt already moved. typeSlowly is already excluded for exactly this reason. Extend the same treatment to the other methods that repeat their effect when re-fired. The excluded branch calls the method directly, so the action gets the full timeout in one attempt — and Playwright's actionability check (attached, visible, stable, receives events, enabled) waits inside that one call, so nothing fires twice.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
AwaitableWebpage::__call()routes every page method throughExecution::waitForExpectation(), which re-runs the whole call under a hard-coded 1s cap per attempt until the configured timeout runs out, then makes one final uncapped attempt. Every Playwright error surfaces asExpectationFailedException, so a timeout is retried exactly like a failed assertion.That is correct for an assertion and wrong for an action. A click changes the page it clicks. When the first attempt needs more than 1s to complete — routine on a 2-core CI runner with a heavy SPA page — the click has already fired and a dialog is open, but the attempt is judged failed. The retry then aims at a button the dialog's overlay now covers, which can never become actionable. The test spends the whole timeout plus one uncapped attempt and fails with
Timeout Nms exceededon a click that succeeded.In practice this shows up as browser suites that are green locally going red on CI on a shifting set of tests (whichever click crossed 1s that run), and raising the timeout makes each failure take longer instead of fixing it. Related report: pestphp/pest#1511.
The change
typeSlowlyis already excluded from the retry loop, with the comment "Retrying this action would append the value to what was already typed." This PR extends the same treatment to the other methods that repeat their effect when re-fired:click/rightClick— the retry aims at a page the first attempt already changeddrag— the retry starts from an element the first attempt already movedkeys/press/pressAndWaitFor/withKeyDown— a re-sent Enter submits the form again; a multi-key sequence is replayed from the startappend— the identical hazardtypeSlowlywas excluded forDeliberately NOT excluded: the state-setting inputs (
fill,type,clear,check,uncheck,radio,select,attach,hover), where a retry is harmless, and every assertion, where retry-until-timeout is exactly right.The excluded branch calls the method directly, so the action gets the full configured timeout in one attempt. That is also the stronger wait: Playwright's actionability check (attached → visible → stable → receives events → enabled) runs inside the one call, so it waits without ever firing twice.
Verification
The included regression test simulates a slow main thread: the button's first
pointerdownbusy-loops 1.2s, and the click shows an overlay covering the button.window.downs === 1, overlay open.pintandphpstanclean.No signatures change. Configurations with a timeout ≤ 1000ms already bypass the loop entirely, so they see no behavior change.
If you would prefer a smaller first step,
click+dragalone fixes the loudest failure mode — the keyboard methods andappendare the same hazard class, just quieter. Happy to adjust.