From 422fecb69d45bb8af578fc28f1016eb6c619aba8 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:51:21 -0700 Subject: [PATCH] test(activity): stabilize portal readiness latch release under CI load The fixed 9-flip budget could miss enough MutationObserver deliveries under shard load for sibling DOM to still report loading. Drain readiness rAF after each flip and wait until latched unavailable is actually observed. --- ...ty-portal-readiness-loop.react185.test.tsx | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx b/src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx index 50190295daa..1820d5e3399 100644 --- a/src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx +++ b/src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx @@ -15,7 +15,10 @@ import { resolveActivityPortalSwap, type ActivityPortalThreadRef } from './activity-portal-thread-reconciliation' -import type { ActivityPortalReadinessStatus } from './activity-portal-readiness-oscillation' +import { + ACTIVITY_PORTAL_READINESS_MAX_FLIPS, + type ActivityPortalReadinessStatus +} from './activity-portal-readiness-oscillation' const WORKTREE_ID = 'wt-1' const TAB_ID = 'tab-react185' @@ -85,6 +88,26 @@ async function flushPortalFramesUntil( } } +// Drain MutationObserver microtasks and the readiness rAF they schedule. +async function flushPortalReadiness( + frames: ReturnType +): Promise { + for (let attempt = 0; attempt < 8; attempt += 1) { + await act(async () => { + await Promise.resolve() + }) + if (frames.pending() === 0) { + await act(async () => { + await Promise.resolve() + }) + if (frames.pending() === 0) { + return + } + } + await frames.flush() + } +} + // Models the tab-root DOM and sibling hiding emitted by a portaled TerminalPane. function renderPortaledTerminalPane(target: HTMLElement, tabId: string, leafIds: string[]): void { const isolatedLeafId = leafIds[0] @@ -354,23 +377,37 @@ describe('Activity portal pane switching', () => { await act(async () => { root.render() }) + await flushPortalReadiness(frames) await flushPortalFramesUntil(frames, () => statuses.at(-1) === 'unavailable') expect(statuses.at(-1)).toBe('unavailable') - // Feed each observed DOM state separately so MutationObserver cannot coalesce the flips. - for (let flip = 0; flip < 9; flip += 1) { + // Feed each DOM state separately and keep going until sibling DOM reports latched + // unavailable. A fixed 9-flip budget flakes when CI load drops MutationObserver + // deliveries below ACTIVITY_PORTAL_READINESS_MAX_FLIPS transitions. + let sawLatchedSibling = false + for ( + let flip = 0; + flip < ACTIVITY_PORTAL_READINESS_MAX_FLIPS * 4 && !sawLatchedSibling; + flip += 1 + ) { + const mode = flip % 2 === 0 ? 'sibling' : 'hidden' await act(async () => { - buildRoot(flip % 2 === 0 ? 'sibling' : 'hidden') + buildRoot(mode) await Promise.resolve() }) - await frames.flush() + await flushPortalReadiness(frames) + if (mode === 'sibling' && statuses.at(-1) === 'unavailable') { + sawLatchedSibling = true + } } + expect(sawLatchedSibling).toBe(true) expect(statuses.at(-1)).toBe('unavailable') await act(async () => { buildRoot('ready') await Promise.resolve() }) + await flushPortalReadiness(frames) await flushPortalFramesUntil(frames, () => statuses.at(-1) === 'ready') expect(statuses.at(-1)).toBe('ready') })