From b644f8ada78ea10001caff7f3399dd7db8c434cd Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:07:46 -0700 Subject: [PATCH] Add E2E test for worktree jump-palette host filtering Tests filter interaction via keyboard, filter/project intersection, empty state when filters exclude all results, and ephemeral filter reset on modal close. --- .../e2e/worktree-jump-palette-filter.spec.ts | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 tests/e2e/worktree-jump-palette-filter.spec.ts diff --git a/tests/e2e/worktree-jump-palette-filter.spec.ts b/tests/e2e/worktree-jump-palette-filter.spec.ts new file mode 100644 index 00000000000..dec87aae7da --- /dev/null +++ b/tests/e2e/worktree-jump-palette-filter.spec.ts @@ -0,0 +1,176 @@ +import type { Page } from '@stablyai/playwright-test' +import { expect, test } from './helpers/orca-app' +import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' + +const LOCAL_PROJECT = 'E2E Palette Local Project' +const REMOTE_PROJECT = 'E2E Palette Remote Project' +const REMOTE_WORKSPACE = 'E2E Palette Remote Workspace' +const REMOTE_HOST = 'E2E Palette Builder' + +type PaletteFilterFixture = { localWorktreeId: string; remoteWorktreeId: string } + +async function seedPaletteFilterFixture(page: Page): Promise { + return page.evaluate( + ({ localProject, remoteHost, remoteProject, remoteWorkspace }) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is unavailable') + } + + const state = store.getState() + const sourceRepo = state.repos[0] + const sourceWorktree = Object.values(state.worktreesByRepo) + .flat() + .find((worktree) => worktree.repoId === sourceRepo?.id && !worktree.isArchived) + if (!sourceRepo || !sourceWorktree) { + throw new Error('Palette filter E2E needs the seeded local repository') + } + + const token = crypto.randomUUID() + const remoteConnectionId = `e2e-palette-host-${token}` + const remoteRepoId = `e2e-palette-remote-repo-${token}` + const remoteWorktreeId = `e2e-palette-remote-worktree-${token}` + const remoteRepo = { + ...sourceRepo, + id: remoteRepoId, + path: `${sourceRepo.path}-e2e-palette-remote-${token}`, + displayName: remoteProject, + connectionId: remoteConnectionId, + executionHostId: `ssh:${remoteConnectionId}` + } + const remoteWorktree = { + ...sourceWorktree, + id: remoteWorktreeId, + repoId: remoteRepoId, + path: `${sourceWorktree.path}-e2e-palette-remote-${token}`, + displayName: remoteWorkspace, + title: remoteWorkspace, + branch: 'refs/heads/e2e-palette-remote', + isMainWorktree: false, + isArchived: false, + hostId: `ssh:${remoteConnectionId}` + } + + const sshTargetLabels = new Map(state.sshTargetLabels) + sshTargetLabels.set(remoteConnectionId, remoteHost) + store.setState({ + repos: [ + ...state.repos.map((repo) => + repo.id === sourceRepo.id ? { ...repo, displayName: localProject } : repo + ), + remoteRepo + ], + sshTargetLabels, + worktreesByRepo: { + ...state.worktreesByRepo, + [sourceRepo.id]: (state.worktreesByRepo[sourceRepo.id] ?? []).map((worktree) => + worktree.id === sourceWorktree.id ? { ...worktree, hostId: 'local' } : worktree + ), + [remoteRepoId]: [remoteWorktree] + } + }) + + return { localWorktreeId: sourceWorktree.id, remoteWorktreeId } + }, + { + localProject: LOCAL_PROJECT, + remoteHost: REMOTE_HOST, + remoteProject: REMOTE_PROJECT, + remoteWorkspace: REMOTE_WORKSPACE + } + ) +} + +function worktreeRow(page: Page, worktreeId: string) { + return palette(page).locator(`[cmdk-item][data-value="worktree:${worktreeId}"]`) +} + +function palette(page: Page) { + return page.getByRole('dialog', { name: 'Jump to...' }) +} + +function filterTrigger(page: Page) { + return page.getByRole('button', { name: 'Filter results' }) +} + +async function openPalette(page: Page): Promise { + await page.evaluate(() => window.__store?.getState().openModal('worktree-palette')) + await expect(palette(page)).toBeVisible() +} + +async function searchFixtureWorkspaces(page: Page, fixture: PaletteFilterFixture): Promise { + const input = palette(page).getByPlaceholder('Search worktrees, settings, tabs, and actions...') + await input.fill('E2E Palette') + await expect(worktreeRow(page, fixture.localWorktreeId)).toBeVisible() + await expect(worktreeRow(page, fixture.remoteWorktreeId)).toBeVisible() +} + +async function selectRemoteHost(page: Page, useKeyboard = false): Promise { + if (useKeyboard) { + const input = palette(page).getByPlaceholder('Search worktrees, settings, tabs, and actions...') + await input.press('Tab') + await expect(filterTrigger(page)).toBeFocused() + await filterTrigger(page).click() + } else { + await filterTrigger(page).click() + } + + await expect(palette(page).getByText('Hosts', { exact: true })).toBeVisible() + await palette(page).getByText('Hosts', { exact: true }).click() + const hosts = palette(page).getByRole('listbox', { name: 'Hosts' }) + await expect(hosts.getByRole('option', { name: REMOTE_HOST })).toBeVisible() + await hosts.getByRole('option', { name: REMOTE_HOST }).click() + await filterTrigger(page).click() +} + +test.describe('Worktree jump-palette filters', () => { + test.beforeEach(async ({ orcaPage }) => { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + }) + + test('filters workspace results by host, intersects project selection, and resets on close', async ({ + orcaPage + }) => { + const fixture = await seedPaletteFilterFixture(orcaPage) + await openPalette(orcaPage) + await searchFixtureWorkspaces(orcaPage, fixture) + + // P1: keyboard focus reaches the control; its rendered selection narrows rows. + await selectRemoteHost(orcaPage, true) + await expect(filterTrigger(orcaPage)).toContainText('1') + await expect(palette(orcaPage).getByLabel(`Remove filter ${REMOTE_HOST}`)).toBeVisible() + await expect(worktreeRow(orcaPage, fixture.remoteWorktreeId)).toBeVisible() + await expect(worktreeRow(orcaPage, fixture.localWorktreeId)).toHaveCount(0) + + // P2: host and project fields intersect, with the filter-specific empty state. + await palette(orcaPage) + .getByPlaceholder('Search worktrees, settings, tabs, and actions...') + .fill('') + await filterTrigger(orcaPage).click() + await palette(orcaPage).getByText('Projects', { exact: true }).click() + const projects = palette(orcaPage).getByRole('listbox', { name: 'Projects' }) + const localProject = projects.getByRole('option').filter({ hasNotText: REMOTE_PROJECT }).first() + await expect(localProject).toBeVisible() + await localProject.click() + await filterTrigger(orcaPage).click() + await expect(palette(orcaPage).getByText('No results match the active filter')).toBeVisible() + await expect( + palette(orcaPage).getByText('Clear the filter above, or widen it to more hosts and projects.') + ).toBeVisible() + + // P3: clear restores both rows; closing drops the ephemeral filter. + await filterTrigger(orcaPage).click() + await palette(orcaPage).getByRole('button', { name: 'Clear all' }).last().click() + await filterTrigger(orcaPage).click() + await expect(filterTrigger(orcaPage)).not.toContainText('1') + await searchFixtureWorkspaces(orcaPage, fixture) + + await selectRemoteHost(orcaPage) + await orcaPage.evaluate(() => window.__store?.getState().closeModal()) + await expect(palette(orcaPage)).toBeHidden() + await openPalette(orcaPage) + await searchFixtureWorkspaces(orcaPage, fixture) + await expect(filterTrigger(orcaPage)).not.toContainText('1') + }) +})