Files
orca/tests/e2e/worktree.spec.ts
T

458 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { openSidebarWorkspaceComposer } from './helpers/sidebar-project-dialog'
import { isAutoGeneratedCreatureBranchName } from '../../src/shared/branch-name-from-work'
// Drive the real Create Workspace composer: old direct store creation missed #1186’s React #31 render crash after #710’s modal replacement.
// #1191 removed StartFromField, but console/pageerror checks still reject equivalent render crashes; see tests/e2e/AGENTS.md.
import type { ConsoleMessage } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import {
waitForSessionReady,
waitForActiveWorktree,
getActiveWorktreeId,
ensureTerminalVisible,
worktreeExists
} from './helpers/store'
test.describe('Create Workspace', () => {
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
})
test('creates a worktree through the composer UI and activates it', async ({ orcaPage }) => {
const worktreeIdBefore = await getActiveWorktreeId(orcaPage)
// Capture both console’s invalid-React-child message and pageerror #31 for dev/production render failures.
const pageErrors: Error[] = []
orcaPage.on('pageerror', (err) => {
pageErrors.push(err)
})
const consoleErrors: string[] = []
const onConsole = (msg: ConsoleMessage): void => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text())
}
}
orcaPage.on('console', onConsole)
const workspaceName = `e2e-create-${Date.now()}`
try {
// Open via visible affordance to cover lazy modal mount.
await openSidebarWorkspaceComposer(orcaPage)
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
await expect(dialog).toBeVisible()
// Wait for agent probe/name focus/hydration/setup hooks before clicking through Radix FocusScope reparenting.
await expect(dialog.locator('[data-workspace-name-input="true"]')).toBeVisible()
// Round-trip getBaseRefDefault so envelope consumers can trigger #1186-class render errors after #1191 removed StartFromField.
await orcaPage.evaluate(async () => {
const repoId = Object.values(window.__store!.getState().worktreesByRepo).flat()[0]?.repoId
if (!repoId) {
return
}
await window.api.repos.getBaseRefDefault({ repoId })
})
await orcaPage.waitForTimeout(100)
// Plain text in default smart mode submits as workspace name.
const nameInput = dialog.getByPlaceholder(/Type a name/i)
await expect(nameInput).toBeVisible()
await nameInput.fill(workspaceName)
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
await expect(createButton).toBeEnabled()
await createButton.click()
await expect(dialog).toBeHidden({ timeout: 15_000 })
await expect
.poll(async () => worktreeExists(orcaPage, workspaceName), {
timeout: 10_000,
message: `Worktree "${workspaceName}" did not appear in the store`
})
.toBe(true)
await expect
.poll(
async () => {
const id = await getActiveWorktreeId(orcaPage)
return id !== null && id !== worktreeIdBefore
},
{ timeout: 10_000, message: 'New worktree did not become the active worktree' }
)
.toBe(true)
// Require auto-created terminal to prove activateAndRevealWorktree ran beyond creating the store row.
await ensureTerminalVisible(orcaPage)
expect(pageErrors, `pageerror fired: ${pageErrors.map((e) => e.message).join(', ')}`).toEqual(
[]
)
const reactChildErrors = consoleErrors.filter((text) =>
/Objects are not valid as a React child|Minified React error #31/i.test(text)
)
expect(reactChildErrors, `React render error: ${reactChildErrors.join(', ')}`).toEqual([])
} finally {
orcaPage.off('console', onConsole)
// Best-effort close if the test failed mid-flow and left the modal open.
await orcaPage
.evaluate(() => {
window.__store?.getState().closeModal()
})
.catch(() => {
/* page may already be torn down */
})
}
})
for (const autoRenameBranchFromWork of [true, false]) {
test(`creates an emoji-named worktree with automatic branch naming ${autoRenameBranchFromWork ? 'on' : 'off'}`, async ({
orcaPage
}) => {
const workspaceName = autoRenameBranchFromWork ? '✨🚀🧪' : '🚀🧪✨'
const previousSetting = await orcaPage.evaluate(
() => window.__store!.getState().settings!.autoRenameBranchFromWork
)
try {
await orcaPage.evaluate(async (enabled) => {
await window
.__store!.getState()
.updateSettingsOrThrow({ autoRenameBranchFromWork: enabled })
}, autoRenameBranchFromWork)
await openSidebarWorkspaceComposer(orcaPage)
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
await expect(dialog).toBeVisible()
await expect(dialog.locator('[data-workspace-name-input="true"]')).toBeVisible()
const nameInput = dialog.getByPlaceholder(/Type a name/i)
await nameInput.fill(workspaceName)
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
await expect(createButton).toBeEnabled()
await createButton.click()
await expect(dialog).toBeHidden({ timeout: 15_000 })
await expect(orcaPage.getByRole('option', { name: new RegExp(workspaceName) })).toBeVisible(
{
timeout: 10_000
}
)
const worktree = await orcaPage.evaluate((displayName) => {
const worktrees = Object.values(window.__store!.getState().worktreesByRepo).flat()
const created = worktrees.find((worktree) => worktree.displayName === displayName)
return { branch: created?.branch, displayNameMode: created?.displayNameMode }
}, workspaceName)
expect(worktree.displayNameMode).toBe('fixed')
if (autoRenameBranchFromWork) {
expect(worktree.branch).toMatch(/^refs\/heads\//)
expect(
isAutoGeneratedCreatureBranchName(worktree.branch?.replace(/^refs\/heads\//, '') ?? '')
).toBe(true)
} else {
expect(worktree.branch).toBe('refs/heads/rocket-test-tube-sparkles')
}
} finally {
await orcaPage
.evaluate(() => {
window.__store?.getState().closeModal()
})
.catch(() => {
/* page may already be torn down */
})
await orcaPage.evaluate(async (enabled) => {
await window
.__store!.getState()
.updateSettingsOrThrow({ autoRenameBranchFromWork: enabled })
}, previousSetting)
}
})
}
test('enters the Korean flag with the flag_kr shortcode suggestion', async ({ orcaPage }) => {
try {
await openSidebarWorkspaceComposer(orcaPage)
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
const nameInput = dialog.getByPlaceholder(/Type a name/i)
await expect(nameInput).toBeVisible()
await nameInput.pressSequentially('Launch :flag_kr', { delay: 100 })
const emojiSuggestions = orcaPage.locator('[data-workspace-emoji-suggestions="true"]')
const sourceSuggestions = orcaPage.locator('[data-workspace-source-suggestions="true"]')
await expect(emojiSuggestions).toBeVisible()
await expect(emojiSuggestions.getByRole('option', { name: ':flag_kr:' })).toBeVisible()
await expect(emojiSuggestions).toHaveAttribute('data-side', 'top')
await expect(sourceSuggestions).toBeVisible()
await expect(sourceSuggestions).toHaveAttribute('data-side', 'bottom')
// Keep both independently positioned suggestion surfaces visible in proof recordings.
await orcaPage.waitForTimeout(750)
await nameInput.pressSequentially(':')
await expect(nameInput).toHaveValue('Launch 🇰🇷')
await expect(orcaPage.getByRole('option', { name: /:flag_kr:/i })).toHaveCount(0)
await nameInput.pressSequentially(' experiment')
await expect(nameInput).toHaveValue('Launch 🇰🇷 experiment')
// Keep the asserted result visible in retained proof recordings.
await orcaPage.waitForTimeout(750)
} finally {
await orcaPage
.evaluate(() => {
window.__store?.getState().closeModal()
})
.catch(() => {
/* page may already be torn down */
})
}
})
test('shows a failed workspace entry when worktree creation fails', async ({ orcaPage }) => {
await orcaPage.evaluate(() => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const originalCreateWorktree = store.getState().createWorktree
;(
window as unknown as {
__restoreCreateWorktree?: () => void
}
).__restoreCreateWorktree = () => {
store.setState({ createWorktree: originalCreateWorktree })
}
store.setState({
createWorktree: async () => {
throw new Error('could not resolve a default base ref for the E2E fixture')
}
})
})
try {
const workspaceName = `e2e-create-failure-${Date.now()}`
await openSidebarWorkspaceComposer(orcaPage)
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
await expect(dialog).toBeVisible()
await expect(dialog.locator('[data-workspace-name-input="true"]')).toBeVisible()
const nameInput = dialog.getByPlaceholder(/Type a name/i)
await expect(nameInput).toBeVisible()
await nameInput.fill(workspaceName)
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
await expect(createButton).toBeEnabled()
await createButton.click()
await expect(dialog).toBeHidden()
const failedWorkspace = orcaPage.getByRole('button', {
name: new RegExp(`${workspaceName} No base branch found`)
})
await expect(failedWorkspace).toBeVisible()
await expect(orcaPage.getByText('Couldn’t create worktree')).toBeVisible()
await expect(failedWorkspace).toContainText('No base branch found')
await expect(orcaPage.getByRole('button', { name: 'Retry' })).toBeVisible()
} finally {
await orcaPage
.evaluate(() => {
;(
window as unknown as {
__restoreCreateWorktree?: () => void
}
).__restoreCreateWorktree?.()
window.__store?.getState().closeModal()
})
.catch(() => {
/* page may already be torn down */
})
}
})
test('reuses a resolved pasted GitHub URL when quick create submits', async ({
electronApp,
orcaPage
}) => {
const title = `E2E smart URL resolution ${Date.now()}`
const url = 'https://github.com/stablyai/orca/pull/2049'
const linkedWorkspacePattern = new RegExp(title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
try {
await openSidebarWorkspaceComposer(orcaPage)
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
await expect(dialog).toBeVisible()
await expect(dialog.locator('[data-workspace-name-input="true"]')).toBeVisible()
await electronApp.evaluate(
({ ipcMain }, { title, url }) => {
const counters = globalThis as unknown as {
__smartGitHubLookupCount: number
__smartResolvePrBaseCount: number
}
counters.__smartGitHubLookupCount = 0
counters.__smartResolvePrBaseCount = 0
ipcMain.removeHandler('gh:workItemByOwnerRepo')
ipcMain.handle(
'gh:workItemByOwnerRepo',
(
_event: unknown,
args: {
number: number
repoId?: string
}
) => {
counters.__smartGitHubLookupCount += 1
return {
id: `e2e-pr-${args.number}`,
type: 'pr',
number: args.number,
title,
state: 'open',
url,
labels: [],
updatedAt: '2026-05-26T00:00:00.000Z',
author: 'e2e',
repoId: args.repoId ?? 'e2e-repo'
}
}
)
ipcMain.removeHandler('worktrees:resolvePrBase')
// Use HEAD: the remote-less fixture’s default branch depends on host init.defaultBranch.
ipcMain.handle('worktrees:resolvePrBase', () => {
counters.__smartResolvePrBaseCount += 1
return { baseBranch: 'HEAD' }
})
},
{ title, url }
)
const nameInput = dialog.getByPlaceholder(/Type a name/i)
await expect(nameInput).toBeVisible()
await nameInput.fill(url)
await expect
.poll(() =>
electronApp.evaluate(() => {
const counters = globalThis as unknown as {
__smartGitHubLookupCount?: number
__smartResolvePrBaseCount?: number
}
return {
githubLookupCount: counters.__smartGitHubLookupCount ?? -1,
resolvePrBaseCount: counters.__smartResolvePrBaseCount ?? -1
}
})
)
.toEqual({ githubLookupCount: 1, resolvePrBaseCount: 0 })
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
await expect(createButton).toBeEnabled()
await createButton.click()
await expect(dialog).toBeHidden({ timeout: 15_000 })
await expect(orcaPage.getByRole('option', { name: linkedWorkspacePattern })).toBeVisible({
timeout: 10_000
})
await expect(orcaPage.getByRole('option', { name: url })).toHaveCount(0)
await expect(orcaPage.getByText('Linked PR #2049')).toBeVisible()
// Quick create reuses typing’s GitHub lookup; #5733 resolves PR head once at submit, requiring one base resolution.
await expect
.poll(() =>
electronApp.evaluate(() => {
const counters = globalThis as unknown as {
__smartGitHubLookupCount?: number
__smartResolvePrBaseCount?: number
}
return {
githubLookupCount: counters.__smartGitHubLookupCount ?? -1,
resolvePrBaseCount: counters.__smartResolvePrBaseCount ?? -1
}
})
)
.toEqual({ githubLookupCount: 1, resolvePrBaseCount: 1 })
} finally {
await orcaPage
.evaluate(() => {
window.__store?.getState().closeModal()
})
.catch(() => {
/* page may already be torn down */
})
}
})
test('names the workspace after the PR title when the pasted URL suggestion is selected', async ({
electronApp,
orcaPage
}) => {
const title = `E2E selected URL resolution ${Date.now()}`
const url = 'https://github.com/stablyai/orca/pull/2050'
const linkedWorkspacePattern = new RegExp(title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
try {
await openSidebarWorkspaceComposer(orcaPage)
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
await expect(dialog).toBeVisible()
await expect(dialog.locator('[data-workspace-name-input="true"]')).toBeVisible()
await electronApp.evaluate(
({ ipcMain }, { title, url }) => {
ipcMain.removeHandler('gh:workItemByOwnerRepo')
ipcMain.handle(
'gh:workItemByOwnerRepo',
(_event: unknown, args: { number: number; repoId?: string }) => ({
id: `e2e-pr-${args.number}`,
type: 'pr',
number: args.number,
title,
state: 'open',
url,
labels: [],
updatedAt: '2026-05-26T00:00:00.000Z',
author: 'e2e',
repoId: args.repoId ?? 'e2e-repo'
})
)
ipcMain.removeHandler('worktrees:resolvePrBase')
// Use HEAD: the remote-less fixture’s default branch depends on host init.defaultBranch.
ipcMain.handle('worktrees:resolvePrBase', () => ({ baseBranch: 'HEAD' }))
},
{ title, url }
)
const nameInput = dialog.getByPlaceholder(/Type a name/i)
await expect(nameInput).toBeVisible()
await nameInput.fill(url)
// #4900: selecting resolved suggestion must replace the pasted URL as name; locate its portaled row outside the dialog.
const suggestion = orcaPage.getByRole('option', { name: linkedWorkspacePattern })
await expect(suggestion).toBeVisible()
await orcaPage.keyboard.press('Enter')
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
await expect(createButton).toBeEnabled()
await createButton.click()
await expect(dialog).toBeHidden({ timeout: 15_000 })
await expect(orcaPage.getByRole('option', { name: linkedWorkspacePattern })).toBeVisible({
timeout: 10_000
})
await expect(orcaPage.getByRole('option', { name: /https-github/i })).toHaveCount(0)
await expect(orcaPage.getByText('Linked PR #2050')).toBeVisible()
} finally {
await orcaPage
.evaluate(() => {
window.__store?.getState().closeModal()
})
.catch(() => {
/* page may already be torn down */
})
}
})
})