Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/web-shell/client/components/GitModePopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export function GitModePopover({
align="end"
sideOffset={8}
className={styles.popover}
// The content is portaled out of the composer, but React synthetic
// clicks still bubble through the React tree to the composer
// surface's onClick, which calls core.focus() and steals focus out of
// the popover — Radix then dismisses it via focus-outside. Stop the
// bubble so option clicks keep focus inside (mirrors the composer
// ToolbarPopover pattern in ChatEditor).
onClick={(e) => e.stopPropagation()}
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(e) => {
// The portal container fools Radix's dismissable-layer into
Expand Down
28 changes: 20 additions & 8 deletions packages/web-shell/client/e2e/web-shell.git-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,20 @@ test('git mode chip shows popover with three modes and captures screenshots', as
animations: 'disabled',
});

// Click "New branch" option
const branchOption = popover.getByText('New branch', { exact: false });
await branchOption.click();
// Click "New branch" option (match by role: the option's text is split
// across a name + description span, so getByText('New branch') is ambiguous)
await popover.getByRole('radio', { name: /New branch/ }).click();

// Wait for the branch input to appear
const branchInput = page.locator('[data-testid="git-mode-branch-input"]');
await expect(branchInput).toBeVisible({ timeout: 5_000 });
// Regression guard: clicking an option used to steal focus to the composer
// (via the surface onClick bubbling through the portal), dismissing the
// popover ~100ms after the input flashed visible. Assert it stays open.
await expect(branchInput).toBeVisible();
await page.waitForTimeout(300);
await expect(popover).toBeVisible();
await expect(branchInput).toBeVisible();

// Type a branch name
await branchInput.fill('feat/git-mode-selector');
Expand Down Expand Up @@ -145,13 +152,18 @@ test('git mode chip worktree mode sends worktree intent', async ({
const popover = page.locator('[data-slot="popover-content"]');
await expect(popover).toBeVisible({ timeout: 5_000 });

// Click "Worktree" option
const worktreeOption = popover.getByText('Worktree', { exact: false });
await worktreeOption.click();
// Click "Worktree" option (match by role; see the branch test above)
await popover.getByRole('radio', { name: /Worktree/ }).click();

// Confirm worktree selection
const confirmBtn = page.locator('[data-testid="git-mode-confirm-worktree"]');
await expect(confirmBtn).toBeVisible();
// Regression guard: same focus-steal dismissal as the branch test — the
// confirm button flashed visible, then the popover closed before it could
// be clicked. Assert the popover survives the click.
await page.waitForTimeout(300);
await expect(popover).toBeVisible();
await expect(confirmBtn).toBeVisible();
await confirmBtn.click();

await expect(popover).not.toBeVisible();
Expand Down Expand Up @@ -209,8 +221,8 @@ test('git mode chip clear button resets to current branch', async ({
const popover = page.locator('[data-slot="popover-content"]');
await expect(popover).toBeVisible({ timeout: 5_000 });

// Select branch mode
await popover.getByText('New branch', { exact: false }).click();
// Select branch mode (match by role; see the first branch test)
await popover.getByRole('radio', { name: /New branch/ }).click();
const branchInput = page.locator('[data-testid="git-mode-branch-input"]');
await branchInput.fill('feat/temp');
await page.locator('[data-testid="git-mode-confirm-branch"]').click();
Expand Down
Loading