mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 00:02:43 +00:00
feat(sidebar): include folder workspaces in keyboard navigation
Use the rendered sidebar row order and host identity when cycling through folder and Git workspaces. Related: https://github.com/stablyai/orca/pull/10555 Co-authored-by: Neil <neil@stably.ai> Co-authored-by: JeongUk Park <jeongph.dev@gmail.com>
This commit is contained in:
co-authored by
Neil
JeongUk Park
parent
c1d403a47b
commit
7b8498b406
@@ -1,11 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { HostSectionRow } from './host-section-rows'
|
||||
import type { FolderWorkspaceRow } from './worktree-list/grouping/row-types'
|
||||
import {
|
||||
getCyclableRowIdentity,
|
||||
getCyclableWorktreeRows,
|
||||
getCyclableWorktreeIds,
|
||||
getCyclableWorktrees,
|
||||
resolveActiveCycleIdentity,
|
||||
resolveCycledWorktreeId
|
||||
} from './worktree-keyboard-cycle'
|
||||
|
||||
const folderRow: FolderWorkspaceRow = {
|
||||
type: 'folder-workspace',
|
||||
key: 'folder-workspace:folder-1',
|
||||
folderWorkspace: {
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Folder 1',
|
||||
folderPath: '/group-1/folder-1',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 1,
|
||||
lastActivityAt: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
},
|
||||
projectGroup: {
|
||||
id: 'group-1',
|
||||
name: 'Group 1',
|
||||
parentPath: '/group-1',
|
||||
parentGroupId: null,
|
||||
createdFrom: 'folder-scan',
|
||||
tabOrder: 0,
|
||||
isCollapsed: false,
|
||||
color: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
},
|
||||
depth: 0,
|
||||
groupDepth: 0
|
||||
}
|
||||
|
||||
describe('resolveCycledWorktreeId', () => {
|
||||
const worktreeIds = ['a', 'b', 'c']
|
||||
|
||||
@@ -110,22 +148,62 @@ describe('getCyclableWorktreeIds', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves folder workspaces out of the rotation', () => {
|
||||
// Why: their synthetic `folder:` id is not activatable through
|
||||
// activateAndRevealWorktree, so arrowing onto one would be a dead keypress.
|
||||
const rows: HostSectionRow[] = [
|
||||
{
|
||||
type: 'folder-workspace',
|
||||
key: 'folder-workspace:folder-1',
|
||||
folderWorkspace: { id: 'folder-1', projectGroupId: 'group-1' } as never,
|
||||
projectGroup: { id: 'group-1' } as never,
|
||||
depth: 0,
|
||||
groupDepth: 0
|
||||
},
|
||||
worktree('plain-b')
|
||||
it('includes folder workspaces between git worktrees in visible order', () => {
|
||||
const rows = [worktree('a'), folderRow, worktree('b')]
|
||||
|
||||
expect(getCyclableWorktreeIds(rows, 'single-location')).toEqual(['a', 'folder:folder-1', 'b'])
|
||||
})
|
||||
|
||||
it('anchors both directions on the active folder workspace', () => {
|
||||
const rows = getCyclableWorktreeRows(
|
||||
[worktree('a'), folderRow, worktree('b')],
|
||||
'single-location'
|
||||
)
|
||||
const activeWorktreeId = resolveActiveCycleIdentity({
|
||||
rows,
|
||||
activeWorktreeId: 'folder:folder-1',
|
||||
activeWorkspaceExecutionHostId: 'local'
|
||||
})
|
||||
const worktreeIds = rows.map(getCyclableRowIdentity)
|
||||
|
||||
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId, direction: 'up' })).toBe(
|
||||
getCyclableRowIdentity(rows[0])
|
||||
)
|
||||
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId, direction: 'down' })).toBe(
|
||||
getCyclableRowIdentity(rows[2])
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps folder placement while preferring a pinned worktree natural row', () => {
|
||||
const rows = [
|
||||
worktree('dup', true),
|
||||
folderRow,
|
||||
{ ...worktree('dup'), rowKey: 'row:dup-natural' },
|
||||
worktree('b')
|
||||
]
|
||||
|
||||
expect(getCyclableWorktreeIds(rows, 'single-location')).toEqual(['plain-b'])
|
||||
expect(getCyclableWorktreeIds(rows, 'duplicate-in-groups')).toEqual([
|
||||
'folder:folder-1',
|
||||
'dup',
|
||||
'b'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps folder keys distinct from git ids and preserves same-id host ownership', () => {
|
||||
const otherHostFolder = {
|
||||
...folderRow,
|
||||
folderWorkspace: { ...folderRow.folderWorkspace, executionHostId: 'ssh:host-b' as const }
|
||||
}
|
||||
const rows = getCyclableWorktreeRows(
|
||||
[worktree('folder-1'), folderRow, otherHostFolder],
|
||||
'single-location'
|
||||
)
|
||||
|
||||
expect(rows.map(getCyclableRowIdentity)).toEqual([
|
||||
'local|folder-1',
|
||||
'local|folder:folder-1',
|
||||
'ssh:host-b|folder:folder-1'
|
||||
])
|
||||
})
|
||||
|
||||
it('drops worktrees the sidebar elided inside a collapsed host section', () => {
|
||||
|
||||
@@ -2,8 +2,11 @@ import type { HostSectionRow } from './host-section-rows'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { composeWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity'
|
||||
import { getWorktreeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { PinnedWorktreeDisplayPolicy, WorktreeRow } from './worktree-list/grouping/row-types'
|
||||
import { getPreferredWorktreeRows } from './worktree-sidebar-row-preference'
|
||||
import type { PinnedWorktreeDisplayPolicy } from './worktree-list/grouping/row-types'
|
||||
import {
|
||||
getRenderedWorkspaceRowsInSidebarOrder,
|
||||
type RenderedWorkspaceRow
|
||||
} from './worktree-sidebar-row-preference'
|
||||
|
||||
/** Host-resolved identity for a cyclable row.
|
||||
*
|
||||
@@ -11,7 +14,7 @@ import { getPreferredWorktreeRows } from './worktree-sidebar-row-preference'
|
||||
* `hostId` (`withRepoHostOwnership` leaves it unqualified), but every activation
|
||||
* path stores the host it resolved to, so raw and resolved identities never match.
|
||||
*/
|
||||
export function getCyclableRowIdentity(row: Pick<WorktreeRow, 'worktree' | 'repo'>): string {
|
||||
export function getCyclableRowIdentity(row: RenderedWorkspaceRow): string {
|
||||
return composeWorktreeHostIdentity(
|
||||
getWorktreeExecutionHostId(row.worktree, row.repo),
|
||||
row.worktree.id
|
||||
@@ -21,14 +24,13 @@ export function getCyclableRowIdentity(row: Pick<WorktreeRow, 'worktree' | 'repo
|
||||
export function getCyclableWorktreeRows(
|
||||
rows: readonly HostSectionRow[],
|
||||
pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy
|
||||
): WorktreeRow[] {
|
||||
const itemRows = rows.filter((row): row is WorktreeRow => row.type === 'item')
|
||||
return getPreferredWorktreeRows(itemRows, pinnedDisplayPolicy)
|
||||
): RenderedWorkspaceRow[] {
|
||||
return getRenderedWorkspaceRowsInSidebarOrder(rows, pinnedDisplayPolicy)
|
||||
}
|
||||
|
||||
/** Identity that locates the active workspace among the cyclable rows. */
|
||||
export function resolveActiveCycleIdentity(args: {
|
||||
rows: readonly WorktreeRow[]
|
||||
rows: readonly RenderedWorkspaceRow[]
|
||||
activeWorktreeId: string | null
|
||||
activeWorkspaceExecutionHostId: ExecutionHostId | null
|
||||
}): string | null {
|
||||
@@ -44,14 +46,12 @@ export function resolveActiveCycleIdentity(args: {
|
||||
return row ? getCyclableRowIdentity(row) : null
|
||||
}
|
||||
|
||||
/** Worktree ids in sidebar order, taken from the rows the sidebar actually
|
||||
/** Workspace ids in sidebar order, taken from the rows the sidebar actually
|
||||
* rendered, so collapsed groups and collapsed host sections drop out on their own. */
|
||||
export function getCyclableWorktreeIds(
|
||||
rows: readonly HostSectionRow[],
|
||||
pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy
|
||||
): string[] {
|
||||
// Why item-only: folder workspaces render as their own row type and are not
|
||||
// activatable through activateAndRevealWorktree, so cycling has never included them.
|
||||
const ids: string[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const row of getCyclableWorktreeRows(rows, pinnedDisplayPolicy)) {
|
||||
|
||||
+26
-2
@@ -47,7 +47,7 @@ describe('host-qualified reveal lookup finds folder workspaces', () => {
|
||||
projectGroup: PROJECT_GROUP,
|
||||
depth: 0,
|
||||
groupDepth: 0
|
||||
} as RenderRow
|
||||
}
|
||||
]
|
||||
|
||||
const index = findPreferredRenderRowIndexForWorktreeIdentity(
|
||||
@@ -68,7 +68,7 @@ describe('host-qualified reveal lookup finds folder workspaces', () => {
|
||||
projectGroup: PROJECT_GROUP,
|
||||
depth: 0,
|
||||
groupDepth: 0
|
||||
} as RenderRow
|
||||
}
|
||||
]
|
||||
|
||||
expect(
|
||||
@@ -79,4 +79,28 @@ describe('host-qualified reveal lookup finds folder workspaces', () => {
|
||||
)
|
||||
).toBe(-1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['local', 0],
|
||||
['runtime:env', 1],
|
||||
[undefined, 0],
|
||||
['runtime:missing', -1]
|
||||
] as const)('matches the folder row for host %s', (hostId, expectedIndex) => {
|
||||
const rows: RenderRow[] = (['local', 'runtime:env'] as const).map((executionHostId) => ({
|
||||
type: 'folder-workspace',
|
||||
key: `folder-workspace:${executionHostId}:${FOLDER_WORKSPACE.id}`,
|
||||
folderWorkspace: { ...FOLDER_WORKSPACE, executionHostId },
|
||||
projectGroup: PROJECT_GROUP,
|
||||
depth: 0,
|
||||
groupDepth: 0
|
||||
}))
|
||||
|
||||
expect(
|
||||
findPreferredRenderRowIndexForWorktreeIdentity(
|
||||
rows,
|
||||
{ id: folderWorkspaceKey(FOLDER_WORKSPACE.id), hostId },
|
||||
'single-location'
|
||||
)
|
||||
).toBe(expectedIndex)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
|
||||
import { folderWorkspaceToWorktree } from '../../../../../../shared/folder-workspace-worktree'
|
||||
import { getWorktreeExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
@@ -112,7 +113,11 @@ export function findPreferredRenderRowIndexForWorktreeIdentity(
|
||||
// Why: host-qualified reveals are emitted for folder workspaces too, and a
|
||||
// walker that only knows item rows returns -1 so the reveal never lands.
|
||||
if (row.type === 'folder-workspace') {
|
||||
if (folderWorkspaceKey(row.folderWorkspace.id) === worktree.id) {
|
||||
if (
|
||||
folderWorkspaceKey(row.folderWorkspace.id) === worktree.id &&
|
||||
(!worktree.hostId ||
|
||||
getWorktreeHostIdentity(folderWorkspaceToWorktree(row.folderWorkspace)) === identity)
|
||||
) {
|
||||
return index
|
||||
}
|
||||
continue
|
||||
|
||||
+1
@@ -148,6 +148,7 @@ describe('workspace list focus ownership', () => {
|
||||
expect(activate.mock.calls.map(([id]) => id)).toEqual(['b', 'c', 'b'])
|
||||
expect(activate).toHaveBeenLastCalledWith('b', {
|
||||
navigationIntent: 'user-open',
|
||||
revealInSidebar: false,
|
||||
executionHostId: 'ssh:fixture'
|
||||
})
|
||||
expect(terminal.focus).not.toHaveBeenCalled()
|
||||
|
||||
+12
-3
@@ -101,7 +101,10 @@ describe('worktree keyboard cycling with a resolved active host', () => {
|
||||
|
||||
press('down')
|
||||
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledWith('c', { navigationIntent: 'user-open' })
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledWith('c', {
|
||||
navigationIntent: 'user-open',
|
||||
revealInSidebar: false
|
||||
})
|
||||
})
|
||||
|
||||
it('steps to the previous row when the active host resolved to local', () => {
|
||||
@@ -109,7 +112,10 @@ describe('worktree keyboard cycling with a resolved active host', () => {
|
||||
|
||||
press('up')
|
||||
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledWith('a', { navigationIntent: 'user-open' })
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledWith('a', {
|
||||
navigationIntent: 'user-open',
|
||||
revealInSidebar: false
|
||||
})
|
||||
})
|
||||
|
||||
it('still steps normally when the active host is unqualified', () => {
|
||||
@@ -117,6 +123,9 @@ describe('worktree keyboard cycling with a resolved active host', () => {
|
||||
|
||||
press('down')
|
||||
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledWith('c', { navigationIntent: 'user-open' })
|
||||
expect(activateAndRevealWorktree).toHaveBeenCalledWith('c', {
|
||||
navigationIntent: 'user-open',
|
||||
revealInSidebar: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react'
|
||||
import type React from 'react'
|
||||
import type { Virtualizer } from '@tanstack/react-virtual'
|
||||
import { useAppStore } from '@/store'
|
||||
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
|
||||
import { activateWorktreeFromSidebar } from '@/lib/sidebar-worktree-activation'
|
||||
import { focusRuntimeTerminalSurface } from '@/runtime/sync-runtime-graph'
|
||||
import { hasVisibleOverlay } from '@/lib/visible-overlay'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
@@ -88,11 +88,7 @@ export function useWorktreeListKeyboardNavigation(args: {
|
||||
return
|
||||
}
|
||||
|
||||
// Why: keyboard cycling is real navigation; route through the activation helper that records history.
|
||||
activateAndRevealWorktree(nextWorktree.id, {
|
||||
navigationIntent: 'user-open',
|
||||
...(nextWorktree.hostId ? { executionHostId: nextWorktree.hostId } : {})
|
||||
})
|
||||
void activateWorktreeFromSidebar(nextWorktree.id, nextWorktree.hostId)
|
||||
|
||||
const rowIndex = findPreferredRenderRowIndexForWorktreeIdentity(
|
||||
renderRows,
|
||||
|
||||
@@ -44,22 +44,36 @@ export function getPreferredWorktreeRows(
|
||||
return preferredRows
|
||||
}
|
||||
|
||||
export function getRenderedWorktreesInSidebarOrder(
|
||||
export type RenderedWorkspaceRow = Pick<WorktreeRow, 'worktree' | 'repo'>
|
||||
|
||||
export function getRenderedWorkspaceRowsInSidebarOrder(
|
||||
rows: readonly HostSectionRow[],
|
||||
pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy
|
||||
): Worktree[] {
|
||||
): RenderedWorkspaceRow[] {
|
||||
const itemRows = rows.filter((row): row is WorktreeRow => row.type === 'item')
|
||||
const preferredRowKeys = new Set(
|
||||
getPreferredWorktreeRows(itemRows, pinnedDisplayPolicy).map((row) => row.rowKey)
|
||||
)
|
||||
const renderedWorktrees: Worktree[] = []
|
||||
const renderedRows: RenderedWorkspaceRow[] = []
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.type === 'item' && preferredRowKeys.has(row.rowKey)) {
|
||||
renderedWorktrees.push(row.worktree)
|
||||
renderedRows.push({ worktree: row.worktree, repo: row.repo })
|
||||
} else if (row.type === 'folder-workspace') {
|
||||
renderedWorktrees.push(folderWorkspaceToWorktree(row.folderWorkspace))
|
||||
renderedRows.push({
|
||||
worktree: folderWorkspaceToWorktree(row.folderWorkspace),
|
||||
repo: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
return renderedWorktrees
|
||||
return renderedRows
|
||||
}
|
||||
|
||||
export function getRenderedWorktreesInSidebarOrder(
|
||||
rows: readonly HostSectionRow[],
|
||||
pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy
|
||||
): Worktree[] {
|
||||
return getRenderedWorkspaceRowsInSidebarOrder(rows, pinnedDisplayPolicy).map(
|
||||
(row) => row.worktree
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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()
|
||||
const folder = await store.getState().createFolderWorkspace({
|
||||
projectGroupId: group.id,
|
||||
name: 'Folder workspace',
|
||||
folderPath: parentPath
|
||||
})
|
||||
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' })
|
||||
store.getState().setGroupBy('workspace-status')
|
||||
store.setState({
|
||||
collapsedGroups: new Set(),
|
||||
worktreesByRepo: {
|
||||
[repo.id]: [
|
||||
{ ...first, hostId: 'local', displayName: 'Git workspace A', workspaceStatus: 'todo' },
|
||||
{ ...last, hostId: 'local', displayName: 'Git workspace B', workspaceStatus: 'completed' }
|
||||
]
|
||||
}
|
||||
})
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user