@@ -198,6 +198,21 @@ def capture(label: str, description: str = "") -> Path:
198198 return capture
199199
200200
201+ def _await_stable_screenshot (page , check : str , * , arg = None ) -> None :
202+ """Wait for a screenshot-readiness predicate, settle briefly, then re-verify.
203+
204+ Dashboard pages render structural content only after their first RPC resolves,
205+ and an SPA route swap (lazy-imported components) can leave the previous page's
206+ DOM mounted while the next chunk loads. The settle + re-verify catches a
207+ predicate that passes on such a transient state and then flips back, so the
208+ screenshot lands on the stable loaded page. Timing lives here so both detail
209+ pages share one cadence.
210+ """
211+ page .wait_for_function (check , arg = arg , timeout = 15000 )
212+ page .wait_for_timeout (250 )
213+ page .wait_for_function (check , arg = arg , timeout = 5000 )
214+
215+
201216def _wait_for_worker_detail_screenshot_ready (page , worker_id : str ) -> None :
202217 # WorkerDetail.vue uniquely nulls `data` in its workerId watch, so a late
203218 # re-fire can flip the page back to the "Loading worker..." overlay after a
@@ -217,9 +232,7 @@ def _wait_for_worker_detail_screenshot_ready(page, worker_id: str) -> None:
217232 && headings.includes("task history");
218233 }
219234 """
220- page .wait_for_function (check , arg = worker_id , timeout = 15000 )
221- page .wait_for_timeout (250 )
222- page .wait_for_function (check , arg = worker_id , timeout = 5000 )
235+ _await_stable_screenshot (page , check , arg = worker_id )
223236
224237
225238def _wait_for_job_detail_screenshot_ready (page , job_id : str ) -> None :
@@ -496,20 +509,36 @@ def test_dashboard_worker_detail(smoke_cluster, smoke_page, smoke_screenshot, ca
496509 )
497510
498511
512+ def _wait_for_autoscaler_screenshot_ready (page ) -> None :
513+ # AutoscalerTab.vue shows only a "Loading autoscaler status…" spinner until its
514+ # first RPC resolves. Route components are lazy-imported, so during the SPA swap
515+ # the previously-viewed page (e.g. worker detail, which renders "Scale Group" +
516+ # the "local-cpu" group name) is still mounted while the autoscaler chunk loads.
517+ # A body-text wait keyed on those strings false-positives on that stale DOM, so
518+ # the screenshot then catches the autoscaler spinner. Anchor on the route hash
519+ # plus the section headings that only render in the loaded (v-else) branch so the
520+ # match can't be satisfied by another page.
521+ check = """
522+ () => {
523+ const text = document.body.textContent || "";
524+ const routeReady = decodeURIComponent(window.location.hash) === "#/autoscaler";
525+ const headings = Array.from(document.querySelectorAll("h3"))
526+ .map((heading) => (heading.textContent || "").trim().toLowerCase());
527+ return routeReady
528+ && !text.includes("Loading autoscaler status")
529+ && headings.includes("waterfall routing")
530+ && headings.includes("recent actions")
531+ && headings.includes("autoscaler logs");
532+ }
533+ """
534+ _await_stable_screenshot (page , check )
535+
536+
499537def test_dashboard_autoscaler_tab (smoke_cluster , smoke_page , smoke_screenshot ):
500538 """Autoscaler tab shows scale groups."""
501539 dashboard_goto (smoke_page , f"{ smoke_cluster .url } /autoscaler" )
502540 wait_for_dashboard_ready (smoke_page )
503- # Wait for actual scale group content. The strict !Loading-autoscaler-status check
504- # blocks on the AutoscalerTab.vue placeholder so the screenshot isn't taken
505- # during a refetch cycle.
506- smoke_page .wait_for_function (
507- "() => !document.body.textContent.includes('Loading autoscaler status') && "
508- "(document.body.textContent.includes('Scale Group') || "
509- "document.body.textContent.includes('scale group') || "
510- "document.body.textContent.includes('local-cpu'))" ,
511- timeout = 15000 ,
512- )
541+ _wait_for_autoscaler_screenshot_ready (smoke_page )
513542 smoke_screenshot ("autoscaler-tab" , "Autoscaler tab showing scale group configuration" )
514543
515544
0 commit comments