mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 08:02:31 +00:00
Two onboarding-screen bugs: - The final "notifications" step blocked click-off/Escape dismissal, unlike every other step. Remove the notifications-only guard so the skip confirmation opens on all steps; the footer "Skip to project setup" stays hidden there since the primary button already hands off to Add Project. - A skipped "integrations" step (GitHub CLI already installed) still rendered as a dead, disabled stepper dot the user skipped past on Continue. The stepper now drops all skipped steps (integrations + Windows terminal) entirely instead of showing an unreachable dot. Allowing dismissal on the last step let a click-off race the "Add your first project" completion handoff (both call closeWith) and double-write onboarding state / double-fire telemetry. Make closeWith idempotent with a first-wins latch. Also map the displayed step index through resolveStepIndex so a momentarily-skipped resume step can't flash "1 of N". Verified: onboarding unit tests, full onboarding e2e spec (rewritten notifications test locks in the new dismiss behavior), typecheck, lint, and live Electron.
670 lines
27 KiB
TypeScript
670 lines
27 KiB
TypeScript
/**
|
|
* E2E tests for the first-launch Onboarding flow.
|
|
*
|
|
* The onboarding overlay is gated by `OnboardingState.closedAt === null` (see
|
|
* `shouldShowOnboarding` in `should-show-onboarding.ts`). Each test gets a fresh
|
|
* Electron instance + isolated userData dir, so persistence starts clean and
|
|
* the overlay renders on first paint without any setup.
|
|
*/
|
|
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { waitForSessionReady } from './helpers/store'
|
|
import type { Page } from '@stablyai/playwright-test'
|
|
import type { GlobalSettings, TuiAgent } from '../../src/shared/types'
|
|
import { ONBOARDING_FINAL_STEP } from '../../src/shared/constants'
|
|
|
|
type OnboardingState = {
|
|
closedAt: number | null
|
|
outcome: 'completed' | 'dismissed' | null
|
|
lastCompletedStep: number
|
|
checklist: Record<string, boolean>
|
|
}
|
|
|
|
const SKIP_TO_PROJECT_SETUP_BUTTON = /^Skip to project setup$/i
|
|
const TASK_SOURCES_HEADING = /Set up GitHub tasks/i
|
|
const WINDOWS_TERMINAL_HEADING = /Set Windows terminal defaults/i
|
|
const ADD_PROJECT_DIALOG_HEADING = /Add (?:a server project|a project|another project)/i
|
|
|
|
async function getOnboardingState(page: Page): Promise<OnboardingState> {
|
|
return page.evaluate(() => window.api.onboarding.get() as Promise<OnboardingState>)
|
|
}
|
|
|
|
async function getSettings(page: Page): Promise<GlobalSettings> {
|
|
return page.evaluate(() => window.api.settings.get())
|
|
}
|
|
|
|
async function getDocumentThemeClass(page: Page): Promise<'dark' | 'light'> {
|
|
return page.evaluate(() =>
|
|
document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
|
)
|
|
}
|
|
|
|
function onboardingFooter(page: Page) {
|
|
return page
|
|
.locator('footer')
|
|
.filter({
|
|
has: page.getByRole('button', { name: /Back|Continue|Add your first project|Set up|Skip/i })
|
|
})
|
|
.first()
|
|
}
|
|
|
|
function onboardingFooterButton(page: Page, name: RegExp) {
|
|
return onboardingFooter(page).getByRole('button', { name })
|
|
}
|
|
|
|
function onboardingNotificationSoundSelect(page: Page) {
|
|
return page.getByRole('combobox').first()
|
|
}
|
|
|
|
async function expectOnboardingNotificationSoundMenuClosed(page: Page): Promise<void> {
|
|
await expect(page.getByRole('option', { name: /Choose Custom File/i })).toHaveCount(0)
|
|
}
|
|
|
|
async function expectOnboardingSkipConfirmationClosed(page: Page): Promise<void> {
|
|
await expect(page.getByRole('dialog', { name: /Skip onboarding\?/i })).toHaveCount(0)
|
|
}
|
|
|
|
async function expectOnboardingSkipConfirmationOpen(page: Page): Promise<void> {
|
|
await expect(page.getByRole('dialog', { name: /Skip onboarding\?/i })).toBeVisible()
|
|
}
|
|
|
|
async function expectOnboardingNotificationSound(page: Page, name: RegExp): Promise<void> {
|
|
await expect(onboardingNotificationSoundSelect(page)).toContainText(name)
|
|
}
|
|
|
|
async function chooseOnboardingNotificationSound(page: Page, name: RegExp): Promise<void> {
|
|
const soundSelect = onboardingNotificationSoundSelect(page)
|
|
await soundSelect.click()
|
|
const option = page.getByRole('option', { name })
|
|
await expect(option).toBeVisible()
|
|
// Why: the select menu extends over the onboarding footer on small CI
|
|
// viewports; keyboard selection avoids pointer fall-through to Skip.
|
|
await option.press('Enter')
|
|
await expect(soundSelect).toContainText(name)
|
|
await expectOnboardingNotificationSoundMenuClosed(page)
|
|
await expectOnboardingSkipConfirmationClosed(page)
|
|
}
|
|
|
|
async function expectOnboardingCustomSoundOption(page: Page): Promise<void> {
|
|
const soundSelect = onboardingNotificationSoundSelect(page)
|
|
await soundSelect.click()
|
|
await expect(page.getByRole('option', { name: /Choose Custom File/i })).toBeVisible()
|
|
await page.getByRole('option', { selected: true }).press('Enter')
|
|
await expectOnboardingNotificationSoundMenuClosed(page)
|
|
await expectOnboardingSkipConfirmationClosed(page)
|
|
}
|
|
|
|
async function continueOnboarding(page: Page): Promise<void> {
|
|
await onboardingFooterButton(page, /^(Continue|Add your first project)\b/).click()
|
|
}
|
|
|
|
async function expectOnboardingProgress(page: Page, label: RegExp): Promise<void> {
|
|
await expect(page.getByText(label)).toBeVisible()
|
|
}
|
|
|
|
async function expectAddProjectDialog(page: Page): Promise<void> {
|
|
await expect(page.getByRole('heading', { name: ADD_PROJECT_DIALOG_HEADING })).toBeVisible()
|
|
}
|
|
|
|
async function continueFromPostNotificationsToRepo(page: Page): Promise<void> {
|
|
if (await page.getByRole('heading', { name: ADD_PROJECT_DIALOG_HEADING }).isVisible()) {
|
|
return
|
|
}
|
|
await continueThroughOptionalTaskSourcesAndWindowsTerminal(page)
|
|
await expect(page.getByRole('heading', { name: /Set up notifications/i })).toBeVisible()
|
|
await expectOnboardingProgress(page, /^[345] of [345]$/)
|
|
await expect(onboardingFooterButton(page, /^Add your first project\b/)).toBeVisible()
|
|
await continueOnboarding(page)
|
|
await expectAddProjectDialog(page)
|
|
}
|
|
|
|
async function continueThroughOptionalTaskSourcesAndWindowsTerminal(page: Page): Promise<void> {
|
|
const taskSourcesVisible = await page
|
|
.getByRole('heading', { name: TASK_SOURCES_HEADING })
|
|
.waitFor({ state: 'visible', timeout: 1_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
if (taskSourcesVisible) {
|
|
await expectOnboardingProgress(page, /^3 of [45]$/)
|
|
await continueOnboarding(page)
|
|
}
|
|
const windowsTerminalVisible = await page
|
|
.getByRole('heading', { name: WINDOWS_TERMINAL_HEADING })
|
|
.waitFor({ state: 'visible', timeout: 1_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
if (windowsTerminalVisible) {
|
|
await expectOnboardingProgress(page, /^[34] of [45]$/)
|
|
await continueOnboarding(page)
|
|
}
|
|
await expect(page.getByRole('heading', { name: /Set up notifications/i })).toBeVisible()
|
|
}
|
|
|
|
async function continueFromThemeToNotifications(page: Page): Promise<void> {
|
|
await continueOnboarding(page)
|
|
await continueThroughOptionalTaskSourcesAndWindowsTerminal(page)
|
|
}
|
|
|
|
test.describe('Onboarding flow', () => {
|
|
// Why: the shared fixture pre-seeds onboarding as closed so non-onboarding
|
|
// tests don't get blocked by the fullscreen overlay. Opt out here so this
|
|
// spec actually exercises the first-launch flow.
|
|
test.use({ dismissOnboarding: false })
|
|
|
|
test.beforeEach(async ({ orcaPage }) => {
|
|
// Per-test userData is freshly minted by the orcaPage fixture, so persisted
|
|
// onboarding state defaults to `closedAt: null, lastCompletedStep: -1` and
|
|
// the overlay paints on its own once App's bootstrap effect resolves.
|
|
await waitForSessionReady(orcaPage)
|
|
})
|
|
|
|
test('renders on first launch with the agent step active', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await expectOnboardingProgress(orcaPage, /^1 of [345]$/)
|
|
await expect(onboardingFooterButton(orcaPage, /^Continue\b/)).toBeVisible()
|
|
await expect(onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON)).toBeVisible()
|
|
// Why: Back is not rendered on the first step (was previously rendered-but-
|
|
// disabled with `disabled:invisible`, now conditionally mounted).
|
|
await expect(orcaPage.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0)
|
|
// Footer hint shows the platform-correct continue shortcut (⌘ on Mac,
|
|
// Ctrl elsewhere). Match either form so the test runs cross-platform.
|
|
// Why: scope to the footer action so background UI shortcut hints cannot
|
|
// false-positive this assertion.
|
|
await expect(
|
|
onboardingFooterButton(orcaPage, /^Continue\b/)
|
|
.locator('span')
|
|
.filter({ hasText: /⌘|Ctrl/ })
|
|
.first()
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Continue advances steps, persists progress, and applies user-visible settings', async ({
|
|
orcaPage
|
|
}) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
|
|
// --- Step 1: agent ---
|
|
// Force a deterministic, non-default selection so the assertion below
|
|
// proves the wizard actually wrote the user's choice (not just the
|
|
// pre-selected detected agent). Codex sits in the top-6 catalog when no
|
|
// agents are detected, otherwise behind the "Show N more agents" details
|
|
// expander — open it if codex isn't visible.
|
|
const targetAgent: TuiAgent = 'codex'
|
|
const codexButton = orcaPage.getByRole('button', { name: /^Codex\s/ })
|
|
// Why: isVisible() is a one-shot probe — on slow renderer paint it would
|
|
// race the wizard mount and falsely take the "show more agents" branch.
|
|
// waitFor with a small timeout actually retries until the button paints.
|
|
const codexVisible = await codexButton
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 1_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
if (!codexVisible) {
|
|
await orcaPage.getByText(/Show \d+ more agents/).click()
|
|
}
|
|
await codexButton.click()
|
|
|
|
await continueOnboarding(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
await expectOnboardingProgress(orcaPage, /^2 of [345]$/)
|
|
await expect
|
|
.poll(async () => (await getOnboardingState(orcaPage)).lastCompletedStep, {
|
|
timeout: 5_000,
|
|
message: 'lastCompletedStep did not advance to 1 after first Continue'
|
|
})
|
|
.toBe(1)
|
|
// The agent choice must be persisted to settings (the user will see this
|
|
// pre-selected when they later open a new tab / agent picker).
|
|
await expect
|
|
.poll(async () => (await getSettings(orcaPage)).defaultTuiAgent, { timeout: 5_000 })
|
|
.toBe(targetAgent)
|
|
|
|
// --- Step 2: theme ---
|
|
// Default settings.theme is 'system', so the document class can resolve to
|
|
// either 'dark' or 'light' depending on the host. Click the opposite tile
|
|
// so we always observe a live flip — the assertion that proves the wizard
|
|
// applies the choice immediately, not just on Continue.
|
|
// Why: 'system' resolves async on mount, so wait for the class to settle
|
|
// before snapshotting — otherwise startingTheme can be stale.
|
|
await orcaPage.waitForFunction(
|
|
() =>
|
|
document.documentElement.classList.contains('dark') ||
|
|
document.documentElement.classList.contains('light')
|
|
)
|
|
const startingTheme = await getDocumentThemeClass(orcaPage)
|
|
const oppositeTheme: 'dark' | 'light' = startingTheme === 'dark' ? 'light' : 'dark'
|
|
const oppositeTileName = oppositeTheme === 'light' ? /Bright & crisp/ : /Easy on the eyes/
|
|
await orcaPage.getByRole('button', { name: oppositeTileName }).click()
|
|
await expect
|
|
.poll(async () => getDocumentThemeClass(orcaPage), { timeout: 5_000 })
|
|
.toBe(oppositeTheme)
|
|
|
|
await continueOnboarding(orcaPage)
|
|
// Why: the theme Continue persists step 2, then persists *through* any
|
|
// skipped optional steps (integrations is skipped when gh is installed,
|
|
// windows_terminal off macOS), so lastCompletedStep can land at 2, 3, or 4.
|
|
// Key off the settled "theme step committed" lower bound rather than a fixed
|
|
// window that assumed integrations always renders.
|
|
await expect
|
|
.poll(async () => (await getOnboardingState(orcaPage)).lastCompletedStep, {
|
|
timeout: 5_000,
|
|
message: 'lastCompletedStep did not advance past the theme step after second Continue'
|
|
})
|
|
.toBeGreaterThanOrEqual(2)
|
|
await expect
|
|
.poll(async () => (await getSettings(orcaPage)).theme, { timeout: 5_000 })
|
|
.toBe(oppositeTheme)
|
|
await continueThroughOptionalTaskSourcesAndWindowsTerminal(orcaPage)
|
|
await expectOnboardingProgress(orcaPage, /^[345] of [345]$/)
|
|
await expect
|
|
.poll(async () => [3, 4].includes((await getOnboardingState(orcaPage)).lastCompletedStep), {
|
|
timeout: 5_000,
|
|
message: 'lastCompletedStep did not include optional setup progress'
|
|
})
|
|
.toBe(true)
|
|
|
|
// --- Step 3: notifications ---
|
|
await expectOnboardingNotificationSound(orcaPage, /System Default/i)
|
|
await expect(orcaPage.getByRole('button', { name: /Send Test Notification/i })).toBeVisible()
|
|
await expectOnboardingCustomSoundOption(orcaPage)
|
|
|
|
await continueFromPostNotificationsToRepo(orcaPage)
|
|
|
|
// Verify the source defaults land without asking users to configure each
|
|
// source in the onboarding UI.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const s = await getSettings(orcaPage)
|
|
return {
|
|
agentTaskComplete: s.notifications.agentTaskComplete,
|
|
terminalBell: s.notifications.terminalBell,
|
|
enabled: s.notifications.enabled,
|
|
customSoundId: s.notifications.customSoundId
|
|
}
|
|
},
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toEqual({
|
|
agentTaskComplete: true,
|
|
terminalBell: true,
|
|
enabled: true,
|
|
customSoundId: 'system'
|
|
})
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const state = await getOnboardingState(orcaPage)
|
|
return {
|
|
closedAt: state.closedAt === null ? null : 'set',
|
|
outcome: state.outcome,
|
|
addedRepo: state.checklist.addedRepo,
|
|
lastCompletedStep: state.lastCompletedStep
|
|
}
|
|
},
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toEqual({
|
|
closedAt: 'set',
|
|
outcome: 'completed',
|
|
addedRepo: false,
|
|
lastCompletedStep: ONBOARDING_FINAL_STEP
|
|
})
|
|
})
|
|
|
|
test('Cmd/Ctrl+Enter advances steps like Continue', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
|
|
// Why: the OS the renderer reports drives whether Cmd or Ctrl is the
|
|
// accelerator (OnboardingFlow.tsx checks navigator.userAgent).
|
|
const isMac = await orcaPage.evaluate(() => navigator.userAgent.includes('Mac'))
|
|
const accelerator = isMac ? 'Meta+Enter' : 'Control+Enter'
|
|
|
|
// Why: in headless Linux CI the window-level capture-phase listener can
|
|
// miss synthetic keyboard events when no element holds focus. Click an
|
|
// inert area inside the overlay first to anchor focus, then press.
|
|
await orcaPage.locator('footer').click({ position: { x: 1, y: 1 } })
|
|
await orcaPage.keyboard.press(accelerator)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
await expect
|
|
.poll(async () => (await getOnboardingState(orcaPage)).lastCompletedStep, {
|
|
timeout: 5_000
|
|
})
|
|
.toBe(1)
|
|
})
|
|
|
|
test('Skip opens Add Project, saves the selected agent, and completes onboarding', async ({
|
|
orcaPage
|
|
}) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
const codexButton = orcaPage.getByRole('button', { name: /^Codex\s/ })
|
|
const codexVisible = await codexButton
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 1_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
if (!codexVisible) {
|
|
await orcaPage.getByText(/Show \d+ more agents/).click()
|
|
}
|
|
await codexButton.click()
|
|
|
|
await onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON).click()
|
|
|
|
await expectAddProjectDialog(orcaPage)
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const state = await getOnboardingState(orcaPage)
|
|
return {
|
|
closedAt: state.closedAt === null ? null : 'set',
|
|
outcome: state.outcome,
|
|
dismissed: state.checklist.dismissed,
|
|
lastCompletedStep: state.lastCompletedStep
|
|
}
|
|
},
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toEqual({
|
|
closedAt: 'set',
|
|
outcome: 'completed',
|
|
dismissed: false,
|
|
lastCompletedStep: ONBOARDING_FINAL_STEP
|
|
})
|
|
await expect
|
|
.poll(async () => (await getSettings(orcaPage)).defaultTuiAgent, { timeout: 5_000 })
|
|
.toBe('codex')
|
|
})
|
|
|
|
test('Skip from theme restores the entry theme choice', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await continueOnboarding(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
|
|
await orcaPage.waitForFunction(
|
|
() =>
|
|
document.documentElement.classList.contains('dark') ||
|
|
document.documentElement.classList.contains('light')
|
|
)
|
|
const entryTheme = (await getSettings(orcaPage)).theme
|
|
const startingTheme = await getDocumentThemeClass(orcaPage)
|
|
const oppositeTheme: 'dark' | 'light' = startingTheme === 'dark' ? 'light' : 'dark'
|
|
const oppositeTileName = oppositeTheme === 'light' ? /Bright & crisp/ : /Easy on the eyes/
|
|
await orcaPage.getByRole('button', { name: oppositeTileName }).click()
|
|
await expect
|
|
.poll(async () => getDocumentThemeClass(orcaPage), { timeout: 5_000 })
|
|
.toBe(oppositeTheme)
|
|
|
|
await onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON).click()
|
|
|
|
await expectAddProjectDialog(orcaPage)
|
|
await expect
|
|
.poll(async () => (await getSettings(orcaPage)).theme, { timeout: 5_000 })
|
|
.toBe(entryTheme)
|
|
await expect
|
|
.poll(async () => getDocumentThemeClass(orcaPage), { timeout: 5_000 })
|
|
.toBe(startingTheme)
|
|
})
|
|
|
|
test('Skip preserves runtime server project setup UI', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await orcaPage.evaluate(async () => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('window.__store is not available')
|
|
}
|
|
// Why: after #5071 the server-path add step gates on the registered
|
|
// runtime-environment list (store.runtimeEnvironments), not just the
|
|
// activeRuntimeEnvironmentId setting. Seed a redacted environment so the
|
|
// host option exists and the "on host" add UI renders.
|
|
const now = Date.now()
|
|
store.getState().setRuntimeEnvironments([
|
|
{
|
|
id: 'env-e2e',
|
|
name: 'E2E Server',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
lastUsedAt: null,
|
|
runtimeId: null,
|
|
source: 'manual',
|
|
endpoints: [
|
|
{
|
|
id: 'ws-env-e2e',
|
|
kind: 'websocket',
|
|
label: 'WebSocket',
|
|
endpoint: 'wss://e2e.invalid/ws'
|
|
}
|
|
],
|
|
preferredEndpointId: 'ws-env-e2e'
|
|
}
|
|
])
|
|
// Why: a runtime host is only auto-selectable (health 'available') when it
|
|
// has a live, protocol-compatible status; without one it reads
|
|
// 'disconnected' and the Add Project dialog falls back to Local Mac.
|
|
// runtimeProtocolVersion 3 clears MIN_COMPATIBLE_RUNTIME_SERVER_VERSION.
|
|
store.getState().setRuntimeEnvironmentStatus('env-e2e', {
|
|
status: {
|
|
runtimeId: 'env-e2e-runtime',
|
|
rendererGraphEpoch: 0,
|
|
graphStatus: 'ready',
|
|
authoritativeWindowId: null,
|
|
liveTabCount: 0,
|
|
liveLeafCount: 0,
|
|
runtimeProtocolVersion: 3,
|
|
minCompatibleRuntimeClientVersion: 1
|
|
},
|
|
checkedAt: now
|
|
})
|
|
await store.getState().updateSettings({ activeRuntimeEnvironmentId: 'env-e2e' })
|
|
})
|
|
await expect
|
|
.poll(async () => (await getSettings(orcaPage)).activeRuntimeEnvironmentId, {
|
|
timeout: 5_000
|
|
})
|
|
.toBe('env-e2e')
|
|
|
|
await onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON).click()
|
|
|
|
await expectAddProjectDialog(orcaPage)
|
|
// The runtime env is selected as the Add Project host and the browse action
|
|
// is host-scoped, proving the server project-setup UI is preserved on skip.
|
|
await expect(orcaPage.getByText('Existing Git repository or folder on this host')).toBeVisible()
|
|
await expect(orcaPage.getByRole('button', { name: /Browse folder/i })).toBeVisible()
|
|
await expect(orcaPage.getByRole('button', { name: /Clone from URL/i })).toBeVisible()
|
|
await expect(orcaPage.getByRole('button', { name: /Create new project/i })).toBeVisible()
|
|
await expect(onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON)).toHaveCount(0)
|
|
expect((await getOnboardingState(orcaPage)).closedAt).not.toBeNull()
|
|
})
|
|
|
|
test('Skip from notifications does not request permission', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await continueOnboarding(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
await continueFromThemeToNotifications(orcaPage)
|
|
|
|
await orcaPage.evaluate(() => {
|
|
localStorage.removeItem('orca.e2e.notificationPermissionRequested')
|
|
window.api.notifications.requestPermission = async () => {
|
|
localStorage.setItem('orca.e2e.notificationPermissionRequested', '1')
|
|
return { supported: true, platform: 'darwin', requested: true }
|
|
}
|
|
})
|
|
await expectOnboardingNotificationSound(orcaPage, /System Default/i)
|
|
|
|
await expect(onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON)).toHaveCount(0)
|
|
await continueOnboarding(orcaPage)
|
|
|
|
await expectAddProjectDialog(orcaPage)
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
orcaPage.evaluate(() => localStorage.getItem('orca.e2e.notificationPermissionRequested')),
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toBeNull()
|
|
})
|
|
|
|
test('selected agent button reports aria-pressed=true', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
|
|
const codexButton = orcaPage.getByRole('button', { name: /^Codex\s/ })
|
|
const codexVisible = await codexButton
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 1_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
if (!codexVisible) {
|
|
await orcaPage.getByText(/Show \d+ more agents/).click()
|
|
}
|
|
await codexButton.click()
|
|
// Why: AgentButton now sets aria-pressed so screen readers and assistive
|
|
// tech can announce the selection. Verify the attribute reflects state.
|
|
await expect(codexButton).toHaveAttribute('aria-pressed', 'true')
|
|
})
|
|
|
|
test('notification sound choice persists on Continue', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
await continueOnboarding(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
await continueFromThemeToNotifications(orcaPage)
|
|
|
|
await chooseOnboardingNotificationSound(orcaPage, /^Ding$/i)
|
|
|
|
await continueFromPostNotificationsToRepo(orcaPage)
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const s = await getSettings(orcaPage)
|
|
return {
|
|
agentTaskComplete: s.notifications.agentTaskComplete,
|
|
terminalBell: s.notifications.terminalBell,
|
|
customSoundId: s.notifications.customSoundId
|
|
}
|
|
},
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toEqual({ agentTaskComplete: true, terminalBell: true, customSoundId: 'ding' })
|
|
})
|
|
|
|
test('typing in the clone-url input does not hijack Enter as a global shortcut', async ({
|
|
orcaPage
|
|
}) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
// Advance to the Add Project dialog.
|
|
await continueOnboarding(orcaPage)
|
|
await continueOnboarding(orcaPage)
|
|
await continueFromPostNotificationsToRepo(orcaPage)
|
|
await orcaPage.getByRole('button', { name: /Clone from URL/i }).click()
|
|
|
|
// Why: focus the clone-url input and press Cmd/Ctrl+Enter. The capture-
|
|
// phase keydown handler should bail via isEditableTarget, so the dialog
|
|
// should remain visible and the empty clone form must not submit.
|
|
const isMac = await orcaPage.evaluate(() => navigator.userAgent.includes('Mac'))
|
|
const accelerator = isMac ? 'Meta+Enter' : 'Control+Enter'
|
|
const input = orcaPage.getByPlaceholder('https://github.com/user/repo.git')
|
|
await input.click()
|
|
await input.press(accelerator)
|
|
// Brief wait so any (incorrect) handler firing would have already happened.
|
|
await orcaPage.waitForTimeout(250)
|
|
await expect(orcaPage.getByRole('heading', { name: /Clone from URL/i })).toBeVisible()
|
|
await expect(input).toBeVisible()
|
|
expect((await getOnboardingState(orcaPage)).closedAt).not.toBeNull()
|
|
})
|
|
|
|
test('Back returns to the previous step without losing progress', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
|
|
await continueOnboarding(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
await expect
|
|
.poll(async () => (await getOnboardingState(orcaPage)).lastCompletedStep, {
|
|
timeout: 5_000
|
|
})
|
|
.toBe(1)
|
|
|
|
// Why: exact match — the app sidebar also exposes a "Go back" button that
|
|
// would otherwise match this regex.
|
|
await orcaPage.getByRole('button', { name: 'Back', exact: true }).click()
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible()
|
|
await expectOnboardingProgress(orcaPage, /^1 of [345]$/)
|
|
|
|
// Why: "without losing progress" means persisted lastCompletedStep stays
|
|
// at 1 — Back rewinds the visible step but must not roll persistence back.
|
|
// Poll because persistence flushes async via IPC after the Back click.
|
|
await expect
|
|
.poll(async () => (await getOnboardingState(orcaPage)).lastCompletedStep, {
|
|
timeout: 5_000
|
|
})
|
|
.toBe(1)
|
|
})
|
|
|
|
test('final notification step can be dismissed via Escape or click-off', async ({ orcaPage }) => {
|
|
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
|
|
timeout: 15_000
|
|
})
|
|
|
|
// Advance to the final notification step. Its primary button hands off to
|
|
// Add Project, so the footer offers no "Skip to project setup" shortcut —
|
|
// but click-off and Escape must still open the skip-confirmation dialog like
|
|
// every other step, so the modal never feels stuck.
|
|
await continueOnboarding(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Make it feel like home/i })).toBeVisible()
|
|
await continueFromThemeToNotifications(orcaPage)
|
|
|
|
await expect(orcaPage.getByRole('heading', { name: /Set up notifications/i })).toBeVisible()
|
|
await expect(onboardingFooterButton(orcaPage, SKIP_TO_PROJECT_SETUP_BUTTON)).toHaveCount(0)
|
|
|
|
// Escape opens the confirmation; "No, keep going" returns to the step with
|
|
// onboarding still open.
|
|
await orcaPage.keyboard.press('Escape')
|
|
await expectOnboardingSkipConfirmationOpen(orcaPage)
|
|
await orcaPage.getByRole('button', { name: /No, keep going/i }).click()
|
|
await expectOnboardingSkipConfirmationClosed(orcaPage)
|
|
await expect(orcaPage.getByRole('heading', { name: /Set up notifications/i })).toBeVisible()
|
|
expect((await getOnboardingState(orcaPage)).closedAt).toBeNull()
|
|
|
|
// Click-off opens the confirmation; Skip dismisses onboarding outright (no
|
|
// Add Project handoff — that is the primary button's job).
|
|
await orcaPage.locator('[data-onboarding-overlay]').click({ position: { x: 8, y: 40 } })
|
|
await expectOnboardingSkipConfirmationOpen(orcaPage)
|
|
await orcaPage.getByRole('button', { name: /^Skip$/ }).click()
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const state = await getOnboardingState(orcaPage)
|
|
return {
|
|
closedAt: state.closedAt === null ? null : 'set',
|
|
outcome: state.outcome,
|
|
dismissed: state.checklist.dismissed
|
|
}
|
|
},
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toEqual({ closedAt: 'set', outcome: 'dismissed', dismissed: true })
|
|
})
|
|
})
|