Stabilize active-delete proof timing

This commit is contained in:
Neil
2026-08-23 14:53:06 -07:00
parent 98199bee54
commit 216f29c4a6
2 changed files with 32 additions and 18 deletions
+8 -2
View File
@@ -47,6 +47,8 @@ type OrcaTestFixtures = {
// Why: most E2E specs need a ready project before assertions start. Golden
// first-run specs opt out so they can prove the zero-project onboarding path.
seedTestRepo: boolean
// Synthetic-list specs need only the primary checkout; switching specs keep the two-row default.
minimumSeededWorktreeCount: number
// Why: spec-scoped launch env. Mutating process.env at spec module scope
// leaks into other specs when a worker reloads files without replaying the
// first spec's afterAll; per-test launch env cannot leak.
@@ -273,13 +275,17 @@ export const test = base.extend<OrcaTestFixtures, OrcaWorkerFixtures>({
// Default: dismiss the onboarding overlay so it doesn't intercept clicks.
dismissOnboarding: [true, { option: true }],
seedTestRepo: [true, { option: true }],
minimumSeededWorktreeCount: [2, { option: true }],
launchEnv: [{}, { option: true }],
orcaAppExtraEnv: [{}, { option: true }],
orcaAppExtraArgs: [[], { option: true }],
// Test-scoped: grab the first BrowserWindow, add the test repo, and wait
// until the session is fully ready with a worktree active.
sharedPage: async ({ electronApp, seedTestRepo, testRepoPath }, provideFixture) => {
sharedPage: async (
{ electronApp, minimumSeededWorktreeCount, seedTestRepo, testRepoPath },
provideFixture
) => {
// Why: the Electron app may take a while to create the first window,
// especially on cold start with no prior dev userData. Isolated per-test
// profiles make late-suite launches slower, so use the full test budget.
@@ -379,7 +385,7 @@ export const test = base.extend<OrcaTestFixtures, OrcaWorkerFixtures>({
message: 'seeded e2e worktrees did not load'
}
)
.toBeGreaterThanOrEqual(2)
.toBeGreaterThanOrEqual(minimumSeededWorktreeCount)
// Wait for workspaceSessionReady to become true
await page.waitForFunction(
@@ -8,6 +8,8 @@ const VISUAL_PROOF_PAUSE_MS = 1_200
const POST_REMOVAL_SAMPLE_FRAMES = 20
const MAX_REMOVAL_WAIT_FRAMES = 300
test.use({ minimumSeededWorktreeCount: 1 })
type RowRemovalFrame = {
animationCount: number
belowTop: number | null
@@ -126,7 +128,7 @@ async function seedActiveDeletionRows(page: Page): Promise<{
)
}
async function prepareScrolledActiveRow(page: Page, targetId: string): Promise<number> {
async function prepareScrolledActiveRow(page: Page, targetId: string): Promise<void> {
const target = page.locator(
`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(targetId)}]`
)
@@ -158,7 +160,6 @@ async function prepareScrolledActiveRow(page: Page, targetId: string): Promise<n
})
await expect(target).toBeVisible()
await expect(target).toHaveAttribute('aria-current', 'page')
return scroller.evaluate((element) => element.scrollTop)
}
async function startRowRemovalSampling(
@@ -169,10 +170,7 @@ async function startRowRemovalSampling(
await page.evaluate(
({ belowId, maxRemovalWaitFrames, postRemovalSampleFrames, targetId }) => {
const sample = async (): Promise<RowRemovalFrame[]> => {
const frames: RowRemovalFrame[] = []
let framesAfterRemoval = 0
for (let index = 0; index < maxRemovalWaitFrames + postRemovalSampleFrames; index += 1) {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
const readFrame = (): RowRemovalFrame => {
const scroller = document.querySelector<HTMLElement>('[data-worktree-sidebar]')
const below = document.querySelector<HTMLElement>(
`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(belowId)}]`
@@ -182,16 +180,22 @@ async function startRowRemovalSampling(
`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(targetId)}]`
)
)
const animationCount =
below?.closest('[data-worktree-virtual-row]')?.firstElementChild?.getAnimations()
.length ?? 0
frames.push({
animationCount,
return {
animationCount:
below?.closest('[data-worktree-virtual-row]')?.firstElementChild?.getAnimations()
.length ?? 0,
belowTop: below?.getBoundingClientRect().top ?? null,
scrollTop: scroller?.scrollTop ?? 0,
targetExists
})
framesAfterRemoval = targetExists ? 0 : framesAfterRemoval + 1
}
}
const frames: RowRemovalFrame[] = [readFrame()]
let framesAfterRemoval = 0
for (let index = 0; index < maxRemovalWaitFrames + postRemovalSampleFrames; index += 1) {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
const frame = readFrame()
frames.push(frame)
framesAfterRemoval = frame.targetExists ? 0 : framesAfterRemoval + 1
if (framesAfterRemoval >= postRemovalSampleFrames) {
break
}
@@ -225,7 +229,7 @@ test('deleting the active scrolled worktree preserves position and closes the ro
await waitForSessionReady(orcaPage)
await orcaPage.setViewportSize({ width: 1_200, height: 800 })
const { belowId, successorId, targetId } = await seedActiveDeletionRows(orcaPage)
const initialScrollTop = await prepareScrolledActiveRow(orcaPage, targetId)
await prepareScrolledActiveRow(orcaPage, targetId)
const target = orcaPage.locator(
`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(targetId)}]`
)
@@ -266,16 +270,20 @@ test('deleting the active scrolled worktree preserves position and closes the ro
await pauseForVisualProof(orcaPage)
const mountedTops = frames.flatMap((frame) => (frame.belowTop === null ? [] : [frame.belowTop]))
const firstRemovedFrame = frames.findIndex((frame) => !frame.targetExists)
const scrollTopBeforeDelete = frames[0]?.scrollTop
if (scrollTopBeforeDelete === undefined) {
throw new Error('Row removal sampler recorded no pre-delete frame')
}
expect(firstRemovedFrame).toBeGreaterThan(0)
expect(frames.slice(firstRemovedFrame).every((frame) => !frame.targetExists)).toBe(true)
expect(Math.max(...frames.map((frame) => frame.animationCount))).toBeGreaterThan(0)
expect(Math.max(...mountedTops) - Math.min(...mountedTops)).toBeGreaterThan(30)
expect(Math.max(...frames.map((frame) => frame.scrollTop))).toBeLessThanOrEqual(
initialScrollTop + 1
scrollTopBeforeDelete + 1
)
expect(Math.min(...frames.map((frame) => frame.scrollTop))).toBeGreaterThanOrEqual(
initialScrollTop - 1
scrollTopBeforeDelete - 1
)
await expect(
orcaPage.locator(`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(successorId)}]`)