155 lines
6.5 KiB
TypeScript
155 lines
6.5 KiB
TypeScript
/**
|
|
* E2E tests for the "Create Workspace" flow in Orca.
|
|
*
|
|
* Why: the old 'create-worktree' modal was replaced by the composer modal
|
|
* (`activeModal === 'new-workspace-composer'`) in #710. A prior version of
|
|
* this spec bypassed the UI entirely — it called `state.createWorktree(...)`
|
|
* directly on the store — which is why the #1186 regression (a React #31
|
|
* crash when `StartFromField` rendered the new `getBaseRefDefault` envelope
|
|
* as JSX) shipped despite a green suite.
|
|
*
|
|
* The spec now drives the real user flow: open the composer, type a
|
|
* workspace name, click Create, and assert the worktree actually
|
|
* materialized and became active. See `tests/e2e/AGENTS.md` for the rule
|
|
* that E2E assertions must target the DOM, not the store.
|
|
*
|
|
* Note: the original StartFromField regression guard was removed with #1191
|
|
* (Tabbed Create Workspace), which deleted StartFromField/StartFromPicker
|
|
* entirely. The render-error sweep below still catches any React #31-class
|
|
* crash in whatever replaces it.
|
|
*/
|
|
|
|
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 render errors for the #1186 guard. React logs "Objects are not
|
|
// valid as a React child" via console.error before throwing the
|
|
// minified-production error #31; capture both paths so the test fails
|
|
// loudly whether the build is dev or prod.
|
|
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 {
|
|
// 1. Open the composer through the visible affordance so the lazy modal
|
|
// mount path stays covered along with the composer body.
|
|
await orcaPage.getByRole('button', { name: 'New workspace', exact: true }).click()
|
|
|
|
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
|
|
await expect(dialog).toBeVisible()
|
|
|
|
// Wait for the composer to settle. The card fires several async effects
|
|
// on mount (detected-agent probe, repo combobox autofocus + hydration,
|
|
// setup-hooks fetch). Clicking before those settle can race Radix's
|
|
// FocusScope reparenting.
|
|
await expect(dialog.getByRole('combobox').first()).toBeVisible()
|
|
|
|
// Force the `getBaseRefDefault` IPC to round-trip so any consumer that
|
|
// renders the envelope (e.g. SourceControl) has a chance to crash
|
|
// inside the open modal's React tree — the console/pageerror sweep
|
|
// below is what catches #1186-class regressions now that the
|
|
// StartFromField trigger no longer exists (#1191).
|
|
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)
|
|
|
|
// 3. Type the workspace name into the unified smart-name input.
|
|
// The composer's default mode is 'smart'; its placeholder advertises
|
|
// multiple input shapes ("Type a name, #1234, branch, GitHub or
|
|
// Linear URL"). Plain free-form text is treated as a workspace name
|
|
// by submitQuick, which is what we want here.
|
|
const nameInput = dialog.getByPlaceholder(/Type a name/i)
|
|
await expect(nameInput).toBeVisible()
|
|
await nameInput.fill(workspaceName)
|
|
|
|
// 4. Click Create. This fires the full submitQuick path:
|
|
// createWorktree IPC, applyWorktreeMeta, activateAndRevealWorktree,
|
|
// and closeModal via onCreated.
|
|
const createButton = dialog.getByRole('button', { name: /Create (Workspace|Worktree)/i })
|
|
await expect(createButton).toBeEnabled()
|
|
await createButton.click()
|
|
|
|
// 5. The modal closes once submitQuick completes successfully. If
|
|
// something inside the flow threw (IPC failure, hook error), the modal
|
|
// would stay open with a createError banner — catch that as a fail.
|
|
await expect(dialog).toBeHidden({ timeout: 15_000 })
|
|
|
|
// 6. The new worktree must actually exist on disk and in the store.
|
|
await expect
|
|
.poll(async () => worktreeExists(orcaPage, workspaceName), {
|
|
timeout: 10_000,
|
|
message: `Worktree "${workspaceName}" did not appear in the store`
|
|
})
|
|
.toBe(true)
|
|
|
|
// 7. The new worktree must become active (different from whatever was
|
|
// active before we opened the composer).
|
|
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)
|
|
|
|
// 8. A terminal tab must auto-create for the new worktree. This is
|
|
// the downstream signal that `activateAndRevealWorktree` actually
|
|
// fired, not just that the store row exists.
|
|
await ensureTerminalVisible(orcaPage)
|
|
|
|
// Final render-error sweep. Any render crash during the flow (whether
|
|
// it tore down the modal or bubbled past it) shows up here.
|
|
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 */
|
|
})
|
|
}
|
|
})
|
|
})
|