mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 00:02:24 +00:00
Co-authored-by: Kien Le <122910950+kien-ship-it@users.noreply.github.com>
188 lines
7.3 KiB
TypeScript
188 lines
7.3 KiB
TypeScript
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import type { Page } from '@stablyai/playwright-test'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { waitForSessionReady } from './helpers/store'
|
|
import { runProcess } from '../../src/shared/child-process/run-process'
|
|
|
|
test.use({ seedTestRepo: false })
|
|
|
|
type SidebarFixture = {
|
|
groupId: string
|
|
collapsedRepoId: string
|
|
expandedRepoId: string
|
|
worktreeId: string
|
|
}
|
|
|
|
async function readGeometry(page: Page, fixture: SidebarFixture) {
|
|
return page.evaluate((target) => {
|
|
const scroller = document.querySelector<HTMLElement>('[data-worktree-sidebar]')
|
|
const group = scroller?.querySelector<HTMLElement>(
|
|
`[data-project-group-header-id="${target.groupId}"]`
|
|
)
|
|
const collapsed = scroller?.querySelector<HTMLElement>(
|
|
`[data-repo-header-id="${target.collapsedRepoId}"]`
|
|
)
|
|
const expanded = scroller?.querySelector<HTMLElement>(
|
|
`[data-repo-header-id="${target.expandedRepoId}"]`
|
|
)
|
|
const card = Array.from(
|
|
scroller?.querySelectorAll<HTMLElement>('[data-worktree-id]') ?? []
|
|
).find((element) => element.dataset.worktreeId === target.worktreeId)
|
|
if (!scroller || !group || !collapsed || !expanded || !card) {
|
|
throw new Error('Expected fixture sidebar rows to be mounted')
|
|
}
|
|
const groupBounds = group.getBoundingClientRect()
|
|
const hit = document.elementFromPoint(
|
|
groupBounds.left + groupBounds.width / 2,
|
|
groupBounds.top + groupBounds.height / 2
|
|
)
|
|
return {
|
|
scrollTop: scroller.scrollTop,
|
|
scrollHeight: scroller.scrollHeight,
|
|
clientHeight: scroller.clientHeight,
|
|
groupUnobscured: group.contains(hit),
|
|
collapsedGap: collapsed.getBoundingClientRect().top - groupBounds.bottom,
|
|
expandedGap: expanded.getBoundingClientRect().top - collapsed.getBoundingClientRect().bottom,
|
|
cardHeight: card.getBoundingClientRect().height
|
|
}
|
|
}, fixture)
|
|
}
|
|
|
|
async function settledGeometry(page: Page, fixture: SidebarFixture) {
|
|
let previous = ''
|
|
let stableSamples = 0
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const signature = JSON.stringify(await readGeometry(page, fixture))
|
|
stableSamples = signature === previous ? stableSamples + 1 : 0
|
|
previous = signature
|
|
return stableSamples
|
|
},
|
|
{ intervals: [50] }
|
|
)
|
|
.toBeGreaterThanOrEqual(2)
|
|
return readGeometry(page, fixture)
|
|
}
|
|
|
|
test('keeps a collapsed group visible when an idle card below it grows', async ({
|
|
orcaPage,
|
|
registerPostElectronShutdownCleanup
|
|
}, testInfo) => {
|
|
await waitForSessionReady(orcaPage)
|
|
await orcaPage.setViewportSize({ width: 1200, height: 900 })
|
|
const root = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'orca-sidebar-measured-row-')))
|
|
registerPostElectronShutdownCleanup(async () => {
|
|
rmSync(root, { recursive: true, force: true })
|
|
})
|
|
const paths = ['group-member', 'LaunchStack', 'ai-platform'].map((name) => path.join(root, name))
|
|
for (const repoPath of paths) {
|
|
mkdirSync(repoPath)
|
|
writeFileSync(path.join(repoPath, 'seed.txt'), 'seed\n')
|
|
for (const args of [
|
|
['init'],
|
|
['add', '.'],
|
|
[
|
|
'-c',
|
|
'user.name=Test',
|
|
'-c',
|
|
'user.email=test@example.com',
|
|
'-c',
|
|
'commit.gpgsign=false',
|
|
'commit',
|
|
'-m',
|
|
'seed'
|
|
]
|
|
]) {
|
|
const result = await runProcess({ program: 'git', args, cwd: repoPath, timeoutMs: 10_000 })
|
|
expect(result.code, result.stderr).toBe(0)
|
|
}
|
|
}
|
|
|
|
const fixture = await orcaPage.evaluate(async (repoPaths): Promise<SidebarFixture> => {
|
|
const store = window.__store!
|
|
for (const repoPath of repoPaths) {
|
|
await window.api.repos.add({ path: repoPath })
|
|
}
|
|
await store.getState().awaitLocalRepoCatalogSettlement()
|
|
const repos = repoPaths.map((repoPath) => {
|
|
const repo = store.getState().repos.find((candidate) => candidate.path === repoPath)
|
|
if (!repo) {
|
|
throw new Error(`Fixture repo was not registered: ${repoPath}`)
|
|
}
|
|
return repo
|
|
})
|
|
for (const repo of repos) {
|
|
await store.getState().fetchWorktrees(repo.id)
|
|
}
|
|
const [member, collapsed, expanded] = repos
|
|
if (!member || !collapsed || !expanded) {
|
|
throw new Error('Expected three fixture repos')
|
|
}
|
|
const group = await store.getState().createProjectGroup('Research gang')
|
|
if (!group) {
|
|
throw new Error('Fixture group was not created')
|
|
}
|
|
await store.getState().moveProjectToGroup(member.id, group.id)
|
|
const state = store.getState()
|
|
state.setGroupBy('repo')
|
|
state.setProjectOrderBy('manual')
|
|
const collapsedSetup = state.projectHostSetups.find((setup) => setup.repoId === collapsed.id)
|
|
const worktree = state.worktreesByRepo[expanded.id]?.find((entry) => entry.isMainWorktree)
|
|
if (!collapsedSetup || !worktree) {
|
|
throw new Error('Fixture project setup or worktree is missing')
|
|
}
|
|
const collapsedGroups = [`project-group:${group.id}`, `project:${collapsedSetup.projectId}`]
|
|
await window.api.ui.set({ groupBy: 'repo', collapsedGroups })
|
|
store.setState({ collapsedGroups: new Set(collapsedGroups) })
|
|
return {
|
|
groupId: group.id,
|
|
collapsedRepoId: collapsed.id,
|
|
expandedRepoId: expanded.id,
|
|
worktreeId: worktree.id
|
|
}
|
|
}, paths)
|
|
|
|
const sidebar = orcaPage.locator('[data-worktree-sidebar]')
|
|
const group = sidebar.locator(`[data-project-group-header-id="${fixture.groupId}"]`)
|
|
const collapsedRepo = sidebar.locator(`[data-repo-header-id="${fixture.collapsedRepoId}"]`)
|
|
const card = sidebar.locator(`[data-worktree-id=${JSON.stringify(fixture.worktreeId)}]`)
|
|
await expect(group).toHaveCount(1)
|
|
await expect(group).toHaveAttribute('aria-expanded', 'false')
|
|
await expect(collapsedRepo).toHaveAttribute('aria-expanded', 'false')
|
|
await expect(card).toHaveCount(1)
|
|
const before = await settledGeometry(orcaPage, fixture)
|
|
expect(before.groupUnobscured).toBe(true)
|
|
expect(before.scrollTop).toBe(0)
|
|
expect(before.scrollHeight).toBeLessThanOrEqual(before.clientHeight)
|
|
expect(before.collapsedGap).toBeGreaterThan(0)
|
|
expect(before.expandedGap).toBeGreaterThan(0)
|
|
|
|
const beforePath = testInfo.outputPath('before-card-growth.png')
|
|
await sidebar.screenshot({ path: beforePath })
|
|
await testInfo.attach('before-card-growth', { path: beforePath, contentType: 'image/png' })
|
|
|
|
// A real child drives ResizeObserver without changing group data or patching the virtualizer.
|
|
await card.evaluate((element) => {
|
|
const content = document.createElement('div')
|
|
content.style.height = '50px'
|
|
element.appendChild(content)
|
|
})
|
|
await expect
|
|
.poll(async () => (await readGeometry(orcaPage, fixture)).cardHeight)
|
|
.toBeCloseTo(before.cardHeight + 50, 0)
|
|
const after = await settledGeometry(orcaPage, fixture)
|
|
const afterPath = testInfo.outputPath('after-card-growth.png')
|
|
await sidebar.screenshot({ path: afterPath })
|
|
await testInfo.attach('after-card-growth', { path: afterPath, contentType: 'image/png' })
|
|
|
|
expect(after.groupUnobscured).toBe(true)
|
|
expect(after.scrollTop).toBe(0)
|
|
expect(after.scrollHeight).toBeLessThanOrEqual(after.clientHeight)
|
|
expect(after.collapsedGap).toBeCloseTo(before.collapsedGap, 0)
|
|
expect(after.expandedGap).toBeCloseTo(before.expandedGap, 0)
|
|
await expect(group).toHaveCount(1)
|
|
})
|