Skip to content

feat(playwright): configurable default timeout and retrying visibility assertions - #208

Closed
Amoifr wants to merge 1 commit into
zenstruck:1.xfrom
Amoifr:feat-199-playwright-timeout-retrying-assertions
Closed

feat(playwright): configurable default timeout and retrying visibility assertions#208
Amoifr wants to merge 1 commit into
zenstruck:1.xfrom
Amoifr:feat-199-playwright-timeout-retrying-assertions

Conversation

@Amoifr

@Amoifr Amoifr commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #199

Both points from the issue:

  • Configurable timeout: a default_timeout option (ms) on PlaywrightBrowser, wired to a BROWSER_DEFAULT_TIMEOUT env var in HasBrowser. It is applied via page->setDefaultTimeout(), so it covers actions and the waitUntil*() methods. When not set, Playwright's 30 second default stays as is.
  • Retrying assertions: assertVisible()/assertNotVisible() now poll client-side (100ms interval, 5 second cap) through Playwright\Testing\Expect, the same shape as Use auto-waiting locator assertions playwright-php/playwright-symfony#35. Failure messages are unchanged, and both still go through session()->assert() first so exception-page detection keeps working.

Tests: the retry is proven against #timeout-box (visible 500ms after load, asserted without any waitUntil*()), and the timeout by asserting a 250ms configured wait fails well under Playwright's 30s default.

As noted in the issue this makes waitUntilVisible()/waitUntilNotVisible() mostly redundant; I left them untouched, happy to deprecate them here or in a follow-up if you prefer.

…y assertions

Two changes from the issue:

- a default_timeout option (BROWSER_DEFAULT_TIMEOUT env var), applied
  as the page's default timeout: it covers actions and the waitUntil*()
  methods, which otherwise use Playwright's 30 second default.
- assertVisible()/assertNotVisible() now retry client-side (100ms poll,
  5 second cap) instead of being one-shot checks, using the same shape
  as playwright-symfony's locator expectations. Failure messages are
  unchanged.

Fixes zenstruck#199
@Amoifr
Amoifr force-pushed the feat-199-playwright-timeout-retrying-assertions branch from c7ba979 to 2c31e50 Compare August 25, 2026 09:42
@Amoifr

Amoifr commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

The two red jobs on the first run were a pre-existing race, not related to this diff: the file-saving tests share the same var/browser/source/source.txt under paratest --functional, so can_save_formatted_json_source and can_save_source_when_exception each read the other's output. I opened #209 with a fix (unique filename per save). Re-triggered CI here in the meantime.

@kbond

kbond commented Aug 29, 2026

Copy link
Copy Markdown
Member

Hmm, my idea for this feature was: every assertion/action "waits" until the selector is available. The wait until's would basically no longer be required because every selector would do this.

@Amoifr

Amoifr commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

That is a better shape than what I did, and the good news is that it is a smaller change than doing it assertion by assertion.

Every selector-based assertion on Browser (assertSeeIn, assertSeeElement, assertElementCount, assertFieldEquals, assertChecked, assertSelected, and the rest) is final, so PlaywrightBrowser cannot override them. But they all funnel through one place: Session::assert() returns Session\Assert, which is a single __call proxy over Mink's WebAssert. Retrying inside that __call, with a budget only when the driver is PlaywrightDriver, makes all of them wait at once, without touching a single final method and without duplicating assertions in PlaywrightBrowser.

Two things I would rather have your call on before reshaping this PR:

Polling the Mink assert, or native Expect per assertion? Retrying __call re-queries the DOM through Mink, so it covers every assertion for free but it is polling rather than Playwright's own waiting. Going through Expect, as this PR does for the two visibility assertions, is native and more precise, but it needs a mapping per assertion and Playwright does not have an Expect shape for all of them. My instinct is the __call retry, precisely because it is uniform and leaves nothing behind.

Negative assertions get slow when they genuinely fail. assertNotSee that is correctly false returns at once, but one that is wrongly false now burns the whole timeout before failing. That is the right behaviour, and it is also why the configurable timeout half of #199 stops being a nicety: 30 seconds per failing assertion would be rough.

Happy to rework this PR into the Session\Assert version, or to keep it as the timeout half and do the waiting in a follow-up, whichever you prefer. And assertVisible / assertNotVisible live on PlaywrightBrowser rather than Browser, so they would fold into the same mechanism instead of keeping their own.

@kbond

kbond commented Aug 30, 2026

Copy link
Copy Markdown
Member

@Amoifr I think I captured this all in #211? WDYT?

@kbond kbond added this to the 1.x milestone Aug 30, 2026
@Amoifr

Amoifr commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Yes, #211 covers everything this PR was after, and the auto wait is in the right place. Thanks for taking it the whole way rather than settling for the two assertions I had reached.

One gap I think is worth a look, since assertVisible() and assertNotVisible() were the two assertions this PR made wait:

  • assertVisible() goes through assert()->elementExists(), so it retries until the element exists, but the Assert::true($element->isVisible(), ...) that follows sits outside the retried closure. An element that is present but still hidden, which is the common Turbo case, fails on the first look instead of waiting for it to become visible.
  • assertNotVisible() calls session()->page()->find() directly rather than assert(), so it never goes through AutoWait at all. The fast pass when the element is absent is right, but the case where it is still visible and about to be hidden does not wait.

The new tests cover actions, assertions, negative assertions and the timeout, but none of them exercise those two, so I may well be reading it wrong. If it holds, folding both into AutoWait::assertion() should be enough, since a failing Assert::true() would need to raise something the retry loop catches.

Everything else lines up: BROWSER_DEFAULT_TIMEOUT, the per test timeout(), and the NOT_RETRYABLE list for the response header assertions, which is a distinction I had missed.

This one is yours to close whenever suits you.

@kbond

kbond commented Sep 1, 2026

Copy link
Copy Markdown
Member

Thanks for pushing this @Amoifr, closing in favour of #211

@kbond kbond closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

PlaywrightBrowser: configurable timeout and waiting assertions

2 participants