Boost: Replace brittle E2E delays with network-level synchronization#48066
Boost: Replace brittle E2E delays with network-level synchronization#48066LiamSarsfield wants to merge 6 commits intotrunkfrom
Conversation
…le delays Address the two main root causes of Boost E2E flakiness (auth setup timeouts and connection flow timeouts) along with several test-level timing issues identified from analysis of 100 recent trunk CI failures.
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Boost plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
Code Coverage SummaryThis PR did not change code coverage! That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷 |
|
@manzoorwanijk I remember you mentioning this and had it in my todo 📝 |
|
Note on the debounce test change ( |
There was a problem hiding this comment.
Pull request overview
Reduces recurring Jetpack Boost E2E flakiness by increasing key UI wait timeouts and replacing brittle fixed delays with retry/polling-style assertions.
Changes:
- Increase timeouts for critical visibility checks in shared auth setup and Boost connection flow.
- Replace hard-coded sleeps with “wait until state appears” assertions and add retry blocks for transient UI states.
- Extend
.toPass()timeouts in a few slower CI-sensitive assertions.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/e2e-commons/setup-specs/auth.setup.ts | Extends wp.com “Howdy, user” visibility timeout during shared auth verification. |
| projects/plugins/boost/tests/e2e/lib/pages/jetpack-boost-page.ts | Extends post-connection “Refresh” button visibility timeout. |
| projects/plugins/boost/tests/e2e/specs/modules/speed-score.test.ts | Adds retry logic to catch transient loading state after “Refresh”. |
| projects/plugins/boost/tests/e2e/specs/modules/score-auto-refresh.test.ts | Replaces some fixed delays with state-based waits around debounce/refresh behavior. |
| projects/plugins/boost/tests/e2e/specs/lcp-optimization/lcp-optimization.test.ts | Adds retries and longer waits for async analysis/pending transitions. |
| projects/plugins/boost/tests/e2e/specs/image-guide/image-guide.test.ts | Extends .toPass() timeout for script presence check. |
| projects/plugins/boost/tests/e2e/specs/concatenate/concatenate.test.ts | Extends .toPass() timeouts for concatenation/exclusion assertions. |
| projects/plugins/boost/changelog/fix-boost-e2e-flaky-test-stability | Adds a changelog entry file for the stability improvements. |
Comments suppressed due to low confidence (1)
projects/plugins/boost/changelog/fix-boost-e2e-flaky-test-stability:4
- This changelog entry file only contains headers (and an optional Comment) but no actual entry text after the blank line. Changelogger treats everything after the blank line as the entry body, so this will produce an empty changelog entry (or may fail validation). Add a short, user-facing entry line after the blank line (and keep/omit the Comment header as appropriate).
Significance: patch
Type: fixed
Comment: Improve E2E test stability by increasing timeouts and replacing brittle hard-coded delays.
manzoorwanijk
left a comment
There was a problem hiding this comment.
I don't see this PR doing anything more than increasing the timeouts, which is not the right fix in my humble opinion. I think we should find the root causes as I pointed out (just a guess) in the inline comments.
| 'User is logged in and username is visible' | ||
| ) | ||
| .toBeVisible(); | ||
| .toBeVisible( { timeout: 40 * 1000 } ); |
There was a problem hiding this comment.
This setup is used by all other tests, but they don't fail. I don't think we should need to increase the timeout here.
There was a problem hiding this comment.
I think the problem might be these hard-coded wait times, without a deterministic way to wait for something real. For example, when a toggle is clicked, it must be making some kind of API call or something which we should wait for instead of a random 1 second wait.
| await test.step( 'Wait for score refresh after 2 second delay and verify score is visible', async () => { | ||
| await new Promise( resolve => setTimeout( resolve, 2100 ) ); | ||
| await test.step( 'Wait for score refresh to start after debounce and verify score is visible', async () => { | ||
| // The score refresh triggers after a 2-second debounce. Rather than relying on a | ||
| // hard-coded delay, wait for the loading state to appear (indicating the refresh started), | ||
| // then wait for the score to become visible again. | ||
| await expect( | ||
| jetpackBoostPage.page.getByRole( 'heading', { name: 'Loading…' } ), | ||
| 'Score refresh should start after debounce' | ||
| ).toBeVisible( { timeout: 10 * 1000 } ); |
There was a problem hiding this comment.
Same here, adding a random increased time out is just a band-aid to the problem. We should instead waitForResponse or something similar here after the toggle is clicked.
- Fix missing await in toggleModule() causing tests to proceed before toggle settled - Add waitForScoreRefreshRequest() to wait for the actual API request instead of sleeping - Rework score-auto-refresh and speed-score tests to use waitForResponse on /speed-scores/refresh - Revert shared auth.setup.ts timeout change (not needed per reviewer feedback)
|
@manzoorwanijk Thanks for the feedback, I pushed a rework that takes a different approach per your suggestions.
Worth noting: The |
Align with monorepo convention where all other plugins and e2e-commons use literal millisecond values (e.g., 60000) instead of multiplication expressions (e.g., 60 * 1000). Converts all 19 instances across 5 files.
manzoorwanijk
left a comment
There was a problem hiding this comment.
TBH, I am still not happy with how we are trying to solve this. Let us aim to get rid all these band-aid timeouts which are not the right fix. I have added some inline suggestions/ideas, let us strive to aim for those. Please feel free to reach out if you need help getting this through.
| await page.getByRole( 'link', { name: 'Go to Jetpack Boost' } ).click(); | ||
|
|
||
| await expect( | ||
| page.getByTestId( 'critical-css-meta' ), | ||
| 'Critical CSS meta information should be visible' | ||
| ).toBeVisible( { timeout: 60 * 1000 } ); | ||
| ).toBeVisible( { timeout: 60000 } ); |
There was a problem hiding this comment.
What does the above link click do? Does it result in some navigation or an API call? If yes, then it's better to wait for that navigation instead of random timeout.
| await page.getByRole( 'button', { name: 'Regenerate' } ).click(); | ||
|
|
||
| await expect( | ||
| page.getByTestId( 'critical-css-meta' ), | ||
| 'Critical CSS meta information should be visible' | ||
| ).toBeVisible( { timeout: 60 * 1000 } ); | ||
| ).toBeVisible( { timeout: 60000 } ); |
There was a problem hiding this comment.
Same here, what does the button click do? Does it make an API call or something? May be wait for that response instead of the timeout?
| await page.getByRole( 'button', { name: 'Go back' } ).click(); | ||
| await expect( | ||
| page.getByTestId( 'critical-css-meta' ), | ||
| 'Critical CSS meta information should be visible' | ||
| ).toBeVisible( { timeout: 60 * 1000 } ); | ||
| ).toBeVisible( { timeout: 60000 } ); |
There was a problem hiding this comment.
Same here. These timeouts are just band-aids. Let us get rid of all of them and fix the root cause.
| await jetpackBoostPage.visit(); | ||
| await expect( | ||
| page.getByTestId( 'critical-css-meta' ), | ||
| 'Critical CSS meta information should be visible' | ||
| ).toBeVisible( { timeout: 60 * 1000 } ); | ||
| ).toBeVisible( { timeout: 60000 } ); |
There was a problem hiding this comment.
I guess the .visit() results in navigation, which we should wait for, instead of adding a timeout to the assertion below it.
| await jetpackBoostPage.visit(); | ||
|
|
||
| await expect( | ||
| page.getByTestId( 'critical-css-meta' ), | ||
| 'Critical CSS meta information should be visible' | ||
| ).toBeVisible( { timeout: 60 * 1000 } ); | ||
| ).toBeVisible( { timeout: 60000 } ); |
There was a problem hiding this comment.
Likewise here about getting rid of the timeout and waiting for the navigation instead.
| await page.goto( '/' ); | ||
| } ); | ||
|
|
||
| await expect( async () => { | ||
| // jQuery is enqueued by a helper plugin. | ||
| const count = await page.locator( '#jquery-core-js' ).count(); | ||
| expect( count, 'jQuery should not be concatenated' ).toBeGreaterThan( 0 ); | ||
| } ).toPass( { timeout: 10000 } ); | ||
| } ).toPass( { timeout: 20000 } ); |
There was a problem hiding this comment.
Since page.goto() results in navigation, we should wait for that navigation to finish, and maybe also wait for some UI element to be visible before making the assertions, instead of these timeouts.
| await page.goto( '/' ); | ||
| } ); | ||
|
|
||
| await expect( async () => { | ||
| // e2e-script-one and e2e-script-two are enqueued by a helper plugin. When concatenation is enabled, | ||
| // they should be concatenated into a single script. | ||
| const count = await page | ||
| .locator( '[data-handles*="e2e-script-one"][data-handles*="e2e-script-two"]' ) | ||
| .count(); | ||
| expect( count, 'JS Concatenation occurs when module is active' ).toBeGreaterThan( 0 ); | ||
| } ).toPass( { timeout: 10000 } ); | ||
| } ).toPass( { timeout: 20000 } ); |
There was a problem hiding this comment.
Same here, let us get rid all these timeouts please.
| await admin.visitAdminPage( 'admin.php', 'page=jetpack-boost' ); | ||
| await expect( | ||
| page.locator( '.jb-critical-css-progress' ), | ||
| 'Critical CSS generation progress indicator should be visible' | ||
| ).toBeVisible(); | ||
| await expect( | ||
| page.getByTestId( 'critical-css-meta' ), | ||
| 'Critical CSS meta information should be visible' | ||
| ).toBeVisible( { timeout: 4 * 60 * 1000 } ); | ||
| ).toBeVisible( { timeout: 240000 } ); |
There was a problem hiding this comment.
Visiting the page, waiting for the navigation (which admin.visitAdminPage already does IIRC), then waiting for some UI element to be visible if it's loaded client-side, then making the assertion is the right approach instead of the timeout.
Not sure I understand what is not pure E2E practice here. |
|
Took a close look at this. A few thoughts on the timeout discussion and the broader approach. The score/debounce test changes are solid. The On the "band-aid timeouts" feedback -- I think there's a distinction worth drawing. The critical-css and concatenate timeout changes aren't new timeouts. They're reformatting pre-existing values from A few things that still need work:
|
I don’t agree with that if there is some API call which we can wait for or if the elements load asynchronously, we can instead wait for them to be visible. The point is to be more deterministic rather than adding a random timeout which can still be flakey when those requests or elements take longer than that. Playwright is good at handling timeouts automatically, and by default, retries/waits 3 times if the element or response is not available yet. |
Proposed changes
Addresses persistent Boost E2E flakiness by replacing timing-dependent assertions with deterministic network-based synchronization, fixing a real async bug, and improving transient state handling.
awaitintoggleModule(): TheexpectNoticeToBeVisible()call was not awaited, so tests proceeded before the toggle had settled. Other methods in the same file (addCornerstonePage,togglePrerenderOption) correctly useawait.waitForScoreRefreshRequest()helper: Waits for the actualPOST /speed-scores/refreshnetwork request instead of checking transient UI state or sleeping. Follows the existingwaitForResponsepattern fromchooseFreePlan().setTimeoutdelays andLoading…heading checks withwaitForResponseon the score refresh endpoint. The debounce test retains its timing delays (structurally necessary to verify "nothing happened yet") but uses the network listener to confirm the refresh eventually fires..toPass()wrapper on transientLoading…heading withwaitForResponsebefore asserting scores are visible..toPass()retry blocks for LCP transient states: The pending optimization state can transition too fast for a plain.toBeVisible()— wrapping in.toPass()with explicit timeouts handles this correctly..toPass()timeouts in concatenate/image-guide: 10s to 20s for CI environments where WordPress page rendering is slower.actionTimeout).60 * 1000) to literal millisecond values (e.g.,60000) across 5 files to align with the monorepo convention used by all other plugins ande2e-commons.What was reverted from the initial approach
auth.setup.tstimeout increase — removed per reviewer feedback. This is shared infrastructure used by all E2E suites, and other suites don't fail there.Does this pull request change what data or activity we track or use?
No
Testing instructions
cd projects/plugins/boost/tests/e2e && pnpm run test:e2e