Files
orca/tests/e2e/sidebar-folder-keyboard-navigation.spec.ts
T
Jinwoo HongandClaude 19e45b7921 feat(sidebar): the desktop window lists main's sidebar inputs behind the build switch (dark) (#30461)
* feat(sidebar): the desktop window lists main's sidebar inputs, with its own writes shown until main does (dark)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(sidebar): a newer write wins in the pending layer, created rows hold the stamped record, writes name their host (dark)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(sidebar): move the focus-read allowance with the window list model it moved into (dark)

* perf(sidebar): main's sidebar feed publishes a change after a quiet spell at once, not 50 ms later (dark)

* test(e2e): the desktop reads-main spec skips on a switch-off build and waits for a frame after collapse

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 22:49:43 -04:00

137 lines
5.5 KiB
TypeScript

import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
test('cycles visible folder workspaces with shortcuts and focused list arrows', async ({
orcaPage,
electronApp,
registerPostElectronShutdownCleanup
}, testInfo) => {
await waitForSessionReady(orcaPage)
await orcaPage.emulateMedia({ reducedMotion: 'reduce' })
const folderPath = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'orca-folder-navigation-')))
registerPostElectronShutdownCleanup(async () =>
rmSync(folderPath, { recursive: true, force: true })
)
const ids = await orcaPage.evaluate(async (parentPath) => {
const store = window.__store!
const group = await window.api.projectGroups.create({
name: 'Folder project',
parentPath,
createdFrom: 'folder-scan'
})
await store.getState().fetchProjectGroups({ runtimeEnvironmentId: null, throwOnError: true })
const folder = await store.getState().createFolderWorkspace(
{
projectGroupId: group.id,
name: 'Folder workspace',
folderPath: parentPath
},
{ runtimeEnvironmentId: null }
)
const repo = store.getState().repos[0]
const [first, last] = repo ? (store.getState().worktreesByRepo[repo.id] ?? []) : []
if (!folder || !repo || !first || !last) {
throw new Error('Expected two git worktrees and one folder workspace')
}
await store.getState().updateFolderWorkspace(folder.id, { workspaceStatus: 'in-progress' })
// Through the metadata writes, so main's copy of the catalog agrees with the window's.
await store.getState().updateWorktreeMeta(first.id, { workspaceStatus: 'todo' })
await store.getState().updateWorktreeMeta(last.id, { workspaceStatus: 'completed' })
store.getState().setGroupBy('workspace-status')
store.setState({ collapsedGroups: new Set() })
return [first.id, `folder:${folder.id}`, last.id]
}, folderPath)
expect(
await electronApp.evaluate(({ BrowserWindow }) =>
BrowserWindow.getAllWindows().every((window) => !window.isVisible())
)
).toBe(true)
const sidebar = orcaPage.locator('[data-worktree-sidebar]')
const row = (id: string) =>
sidebar.locator(`[role="option"][data-worktree-id=${JSON.stringify(id)}]`)
await expect(sidebar.locator('[role="option"]')).toHaveCount(3)
await expect
.poll(() =>
sidebar
.locator('[role="option"]')
.evaluateAll((rows) => rows.map((row) => row.getAttribute('data-worktree-id')))
)
.toEqual(ids)
const [first, folder, last] = ids
if (!first || !folder || !last) {
throw new Error('Missing navigation targets')
}
const mod = await orcaPage.evaluate(() =>
navigator.userAgent.includes('Mac') ? 'Meta' : 'Control'
)
const transitions = [
{ from: first, key: 'ArrowDown', to: folder },
{ from: last, key: 'ArrowUp', to: folder },
{ from: folder, key: 'ArrowDown', to: last },
{ from: folder, key: 'ArrowUp', to: first },
{ from: last, key: 'ArrowDown', to: first },
{ from: first, key: 'ArrowUp', to: last }
]
const observed: { mode: string; from: string; key: string; to: string; current: string[] }[] = []
// Clicking each starting row proves the folder already activates before keyboard cycling.
for (const mode of ['shortcut', 'focused-list']) {
for (const { from, key, to } of transitions) {
await row(from).click()
await expect(row(from)).toHaveAttribute('aria-current', 'page')
if (mode === 'focused-list') {
await orcaPage.keyboard.press(`${mod}+Shift+0`)
await expect(sidebar).toBeFocused()
}
await orcaPage.keyboard.press(mode === 'shortcut' ? `${mod}+Shift+${key}` : key)
await expect.soft(row(to)).toHaveAttribute('aria-current', 'page', { timeout: 3000 })
observed.push({
mode,
from,
key,
to,
current: await sidebar
.locator('[aria-current="page"]')
.evaluateAll((rows) => rows.map((row) => row.getAttribute('data-worktree-id') ?? ''))
})
if (mode === 'focused-list') {
await expect.soft(sidebar).toBeFocused()
}
if (to === folder) {
const proofPath = testInfo.outputPath(`${mode}-${key}.png`)
await sidebar.screenshot({ path: proofPath })
await testInfo.attach(`${mode}-${key}`, { path: proofPath, contentType: 'image/png' })
}
}
}
await testInfo.attach('navigation-transitions', {
body: JSON.stringify(observed, null, 2),
contentType: 'application/json'
})
const folderSection = sidebar.getByRole('button', { name: /^In progress/ })
await folderSection.click()
await expect(row(folder)).toHaveCount(0)
await row(first).click()
await orcaPage.keyboard.press(`${mod}+Shift+ArrowDown`)
await expect(row(last)).toHaveAttribute('aria-current', 'page')
await folderSection.click()
await expect(row(folder)).toHaveCount(1)
await row(first).click()
await orcaPage.setViewportSize({ width: 1000, height: 400 })
await expect.poll(() => sidebar.evaluate((element) => element.clientHeight)).toBeGreaterThan(50)
await sidebar.evaluate((element) => {
element.scrollTop = 0
})
await orcaPage.keyboard.press(`${mod}+Shift+ArrowDown`)
await expect(row(folder)).toHaveAttribute('aria-current', 'page')
await expect(row(folder)).toBeInViewport({ ratio: 1 })
await expect.poll(() => sidebar.evaluate((element) => element.scrollTop)).toBeGreaterThan(0)
})