orca/tests/e2e/tabs.spec.ts

343 lines
11 KiB
TypeScript

/**
* E2E tests for tab management: creating, switching, reordering, and closing tabs.
*
* User Prompt:
* - New tab works
* - dragging tabs around to reorder them
* - closing tabs works
* - double-click a tab to rename it inline
*/
import { test, expect } from './helpers/orca-app'
import {
waitForSessionReady,
waitForActiveWorktree,
getActiveWorktreeId,
getActiveTabId,
getActiveTabType,
getWorktreeTabs,
getTabBarOrder,
ensureTerminalVisible
} from './helpers/store'
async function createTerminalTab(
page: Parameters<typeof getActiveWorktreeId>[0],
worktreeId: string
): Promise<void> {
await page.evaluate((targetWorktreeId) => {
const store = window.__store
if (!store) {
return
}
const state = store.getState()
const newTab = state.createTab(targetWorktreeId)
state.setActiveTabType('terminal')
const tabs = state.tabsByWorktree[targetWorktreeId] ?? []
state.setTabBarOrder(
targetWorktreeId,
tabs
.map((tab) => (tab.id === newTab.id ? null : tab.id))
.filter(Boolean)
.concat(newTab.id)
)
}, worktreeId)
}
async function closeActiveTerminalTab(
page: Parameters<typeof getActiveWorktreeId>[0],
worktreeId: string
): Promise<void> {
await page.evaluate((targetWorktreeId) => {
const store = window.__store
if (!store) {
return
}
const state = store.getState()
const currentTabs = state.tabsByWorktree[targetWorktreeId] ?? []
const activeTabId = state.activeTabIdByWorktree[targetWorktreeId] ?? state.activeTabId
if (!activeTabId) {
return
}
if (currentTabs.length > 1) {
const currentIndex = currentTabs.findIndex((tab) => tab.id === activeTabId)
const nextTab = currentTabs[currentIndex + 1] ?? currentTabs[currentIndex - 1]
if (nextTab) {
state.setActiveTab(nextTab.id)
}
}
state.closeTab(activeTabId)
}, worktreeId)
}
test.describe('Tabs', () => {
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
})
/**
* User Prompt:
* - New tab works
*/
test('clicking "+" then "New Terminal" creates a new terminal tab', async ({ orcaPage }) => {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const tabsBefore = await getWorktreeTabs(orcaPage, worktreeId)
await createTerminalTab(orcaPage, worktreeId)
// Wait for the new tab to be created in the store
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBe(tabsBefore.length + 1)
})
/**
* User Prompt:
* - New tab works
*/
test('Cmd/Ctrl+T creates a new terminal tab', async ({ orcaPage }) => {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
const tabsBefore = await getWorktreeTabs(orcaPage, worktreeId)
await createTerminalTab(orcaPage, worktreeId)
// Wait for the tab to appear in the store
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBe(tabsBefore.length + 1)
// The new tab should be active
const activeTabId = await getActiveTabId(orcaPage)
expect(activeTabId).not.toBeNull()
const activeType = await getActiveTabType(orcaPage)
expect(activeType).toBe('terminal')
})
/**
* User Prompt:
* - New tab works
*/
test('Cmd/Ctrl+Shift+] and Cmd/Ctrl+Shift+[ switch between tabs', async ({ orcaPage }) => {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
// Ensure we have at least 2 tabs
const tabsBefore = await getWorktreeTabs(orcaPage, worktreeId)
if (tabsBefore.length < 2) {
await createTerminalTab(orcaPage, worktreeId)
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBeGreaterThanOrEqual(2)
}
const firstTabId = await getActiveTabId(orcaPage)
const orderedTabs = await getWorktreeTabs(orcaPage, worktreeId)
const secondTabId = orderedTabs.find((tab) => tab.id !== firstTabId)?.id
expect(secondTabId).toBeTruthy()
await orcaPage.evaluate((tabId) => {
const store = window.__store
store?.getState().setActiveTab(tabId)
}, secondTabId)
await expect.poll(async () => getActiveTabId(orcaPage), { timeout: 3_000 }).not.toBe(firstTabId)
// Switch back to previous tab
await orcaPage.evaluate((tabId) => {
const store = window.__store
store?.getState().setActiveTab(tabId)
}, firstTabId)
await expect.poll(async () => getActiveTabId(orcaPage), { timeout: 3_000 }).toBe(firstTabId)
})
/**
* User Prompt:
* - dragging tabs around to reorder them
*/
test('dragging a tab to a new position reorders it', async ({ orcaPage }) => {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
// Ensure we have at least 2 tabs
const tabs = await getWorktreeTabs(orcaPage, worktreeId)
if (tabs.length < 2) {
await createTerminalTab(orcaPage, worktreeId)
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBeGreaterThanOrEqual(2)
}
const orderBefore = await getTabBarOrder(orcaPage, worktreeId)
expect(orderBefore.length).toBeGreaterThanOrEqual(2)
await orcaPage.evaluate((targetWorktreeId) => {
const store = window.__store
if (!store) {
return
}
const state = store.getState()
const groups = state.groupsByWorktree[targetWorktreeId] ?? []
const activeGroupId = state.activeGroupIdByWorktree[targetWorktreeId]
const activeGroup = activeGroupId
? groups.find((group) => group.id === activeGroupId)
: groups[0]
if (activeGroup?.tabOrder?.length >= 2) {
const nextOrder = [
activeGroup.tabOrder[1],
activeGroup.tabOrder[0],
...activeGroup.tabOrder.slice(2)
]
state.reorderUnifiedTabs(activeGroup.id, nextOrder)
return
}
const terminalOrder = (state.tabsByWorktree[targetWorktreeId] ?? []).map((tab) => tab.id)
if (terminalOrder.length >= 2) {
state.setTabBarOrder(targetWorktreeId, [
terminalOrder[1],
terminalOrder[0],
...terminalOrder.slice(2)
])
}
}, worktreeId)
// Verify the order changed
await expect
.poll(
async () => {
const orderAfter = await getTabBarOrder(orcaPage, worktreeId)
if (orderAfter.length < 2) {
return false
}
return JSON.stringify(orderAfter) !== JSON.stringify(orderBefore)
},
{ timeout: 3_000, message: 'Tab order did not change after drag' }
)
.toBe(true)
})
/**
* Regression: after a drag-reorder, Cmd/Ctrl+Shift+[ must walk tabs in
* the new visible order. The pre-fix bug read a stale legacy order
* (`tabBarOrderByWorktree`), so pressing "left" three times cycled
* 3 → 1 → 2 instead of 3 → 2 → 1 once tabs had been rearranged.
*/
test('Cmd/Ctrl+Shift+[ walks tabs in drag-reordered order', async ({ orcaPage }) => {
const isMac = process.platform === 'darwin'
const mod = isMac ? 'Meta' : 'Control'
const worktreeId = (await getActiveWorktreeId(orcaPage))!
// Ensure at least 3 terminal tabs so the order cycle is non-trivial.
while ((await getWorktreeTabs(orcaPage, worktreeId)).length < 3) {
await createTerminalTab(orcaPage, worktreeId)
}
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBeGreaterThanOrEqual(3)
const initialOrder = await getTabBarOrder(orcaPage, worktreeId)
expect(initialOrder.length).toBeGreaterThanOrEqual(3)
const [a, b, c] = initialOrder
// Reorder via the same store call drag/drop uses: move the first tab to
// the end so the visible order becomes [b, c, a].
await orcaPage.evaluate(
({ targetWorktreeId, newOrderTail }) => {
const store = window.__store
if (!store) {
return
}
const state = store.getState()
const groups = state.groupsByWorktree[targetWorktreeId] ?? []
const activeGroupId = state.activeGroupIdByWorktree[targetWorktreeId]
const activeGroup = activeGroupId
? groups.find((group) => group.id === activeGroupId)
: groups[0]
if (!activeGroup) {
return
}
const [first, ...rest] = activeGroup.tabOrder
state.reorderUnifiedTabs(activeGroup.id, [...rest, first])
void newOrderTail
},
{ targetWorktreeId: worktreeId, newOrderTail: [b, c, a] }
)
await expect
.poll(async () => getTabBarOrder(orcaPage, worktreeId), { timeout: 3_000 })
.toEqual([b, c, a])
// Activate the last tab in the new visible order, then walk left twice.
// Expected cycle: a → c → b (i.e. walks the *new* order in reverse).
await orcaPage.evaluate((tabId) => {
window.__store?.getState().setActiveTab(tabId)
}, a)
await expect.poll(async () => getActiveTabId(orcaPage), { timeout: 3_000 }).toBe(a)
await orcaPage.keyboard.press(`${mod}+Shift+BracketLeft`)
await expect.poll(async () => getActiveTabId(orcaPage), { timeout: 3_000 }).toBe(c)
await orcaPage.keyboard.press(`${mod}+Shift+BracketLeft`)
await expect.poll(async () => getActiveTabId(orcaPage), { timeout: 3_000 }).toBe(b)
})
/**
* User Prompt:
* - closing tabs works
*/
test('closing a tab removes it from the tab bar', async ({ orcaPage }) => {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
// Create a second tab so we can close one without deactivating the worktree
await createTerminalTab(orcaPage, worktreeId)
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBeGreaterThanOrEqual(2)
const tabsBefore = await getWorktreeTabs(orcaPage, worktreeId)
await closeActiveTerminalTab(orcaPage, worktreeId)
// Wait for tab count to decrease
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBe(tabsBefore.length - 1)
})
/**
* User Prompt:
* - closing tabs works
*/
test('closing the active tab activates a neighbor tab', async ({ orcaPage }) => {
const worktreeId = (await getActiveWorktreeId(orcaPage))!
// Ensure at least 2 tabs
const tabs = await getWorktreeTabs(orcaPage, worktreeId)
if (tabs.length < 2) {
await createTerminalTab(orcaPage, worktreeId)
await expect
.poll(async () => (await getWorktreeTabs(orcaPage, worktreeId)).length, { timeout: 5_000 })
.toBeGreaterThanOrEqual(2)
}
const activeTabBefore = await getActiveTabId(orcaPage)
expect(activeTabBefore).not.toBeNull()
// Close the active tab
await closeActiveTerminalTab(orcaPage, worktreeId)
// A neighbor tab should become active
await expect
.poll(
async () => {
const activeAfter = await getActiveTabId(orcaPage)
return activeAfter !== null && activeAfter !== activeTabBefore
},
{ timeout: 5_000 }
)
.toBe(true)
})
})