diff --git a/tests/e2e/helpers/orca-app.ts b/tests/e2e/helpers/orca-app.ts index 434a3297df3..73c6659e13e 100644 --- a/tests/e2e/helpers/orca-app.ts +++ b/tests/e2e/helpers/orca-app.ts @@ -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({ // 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({ message: 'seeded e2e worktrees did not load' } ) - .toBeGreaterThanOrEqual(2) + .toBeGreaterThanOrEqual(minimumSeededWorktreeCount) // Wait for workspaceSessionReady to become true await page.waitForFunction( diff --git a/tests/e2e/worktree-active-delete-scroll-position.spec.ts b/tests/e2e/worktree-active-delete-scroll-position.spec.ts index 6e5629c2b72..1b9d4bc3283 100644 --- a/tests/e2e/worktree-active-delete-scroll-position.spec.ts +++ b/tests/e2e/worktree-active-delete-scroll-position.spec.ts @@ -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 { +async function prepareScrolledActiveRow(page: Page, targetId: string): Promise { 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 element.scrollTop) } async function startRowRemovalSampling( @@ -169,10 +170,7 @@ async function startRowRemovalSampling( await page.evaluate( ({ belowId, maxRemovalWaitFrames, postRemovalSampleFrames, targetId }) => { const sample = async (): Promise => { - const frames: RowRemovalFrame[] = [] - let framesAfterRemoval = 0 - for (let index = 0; index < maxRemovalWaitFrames + postRemovalSampleFrames; index += 1) { - await new Promise((resolve) => requestAnimationFrame(() => resolve())) + const readFrame = (): RowRemovalFrame => { const scroller = document.querySelector('[data-worktree-sidebar]') const below = document.querySelector( `[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((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)}]`)