Files
orca/src/shared/contextual-tours.test.ts
T

211 lines
8.4 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 { describe, expect, it } from 'vitest'
import {
CONTEXTUAL_TOURS,
normalizeContextualTourIds,
type ContextualTour,
type ContextualTourId
} from './contextual-tours'
describe('contextual tour definitions', () => {
it('defines the required tours with concise visible steps', () => {
const expectedIds: ContextualTourId[] = [
'workspace-board',
'workspace-agent-sessions',
'browser',
'client-hosted-browser',
'tasks',
'automations',
'floating-workspace',
'workspace-creation'
]
expect(CONTEXTUAL_TOURS.map((tour) => tour.id)).toEqual(expectedIds)
for (const tour of CONTEXTUAL_TOURS) {
expect(tour.steps[0]?.requiredForStart).toBe(true)
const stepCount = (tour.steps as readonly unknown[]).length
if (stepCount === 1) {
// A lone step needs a completion path beyond the default Next: either it
// self-completes on the feature interaction, or it carries an explicit
// complete CTA (the client-hosted browser tip's "Got it").
const step = tour.steps[0] as ContextualTour['steps'][number]
expect(
Boolean(step.advanceOnFeatureInteraction) || step.primaryAction?.kind === 'complete'
).toBe(true)
} else {
expect(stepCount).toBeGreaterThanOrEqual(2)
}
expect(stepCount).toBeLessThanOrEqual(tour.id === 'workspace-agent-sessions' ? 5 : 3)
for (const step of tour.steps) {
expect(step.title.length).toBeGreaterThan(0)
expect(step.body.length).toBeGreaterThan(0)
expect(step.body.length).toBeLessThanOrEqual(140)
expect(step.targetSelector).toContain('data-contextual-tour-target')
}
}
})
it('defines the workspace agent sessions value tour as split then create-worktree', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'workspace-agent-sessions') as
| ContextualTour
| undefined
// Two steps only: tasks and orchestration education lives in their own
// page tours, so the in-app tour ends after the worktree CTA.
expect(tour?.steps.map((step) => step.title)).toEqual([
'Split a terminal pane',
'Start another task in parallel'
])
// The opening step teaches the split gesture and offers the convenience button.
expect(tour?.steps[0]).toMatchObject({
requiredForStart: true,
primaryAction: { kind: 'split-terminal-pane', label: 'Split terminal' },
advanceOnFeatureInteraction: 'terminal-pane-split'
})
expect(tour?.steps[0]?.body).toContain('{terminal.splitRight}')
expect(tour?.steps[0]?.targetSelector).toContain('terminal-pane-split-target')
expect(tour?.steps[0]?.targetSelector).not.toContain('terminal-split-control')
expect(tour?.steps[0]?.secondaryAction).toBeUndefined()
// The closing step anchors on the real new-worktree button; the pulse makes
// that button the CTA instead of duplicating it inside the panel.
expect(tour?.steps[1]).toMatchObject({
targetPulse: true,
hidePrimaryAction: true
})
expect(tour?.steps[1]?.targetSelector).toContain('workspace-create-control')
expect(tour?.steps[1]?.primaryAction).toBeUndefined()
expect(tour?.steps[1]?.secondaryAction).toBeUndefined()
})
it('points the workspace board tour at the board center and done lane', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'workspace-board') as
| ContextualTour
| undefined
expect(tour?.steps.map((step) => step.title)).toEqual([
'Plan work on the board',
'Move work through lanes'
])
expect(tour?.steps[0]).toMatchObject({
targetSelector: '[data-contextual-tour-target="workspace-board-center"]',
requiredForStart: true,
preferredPlacement: 'bottom'
})
expect(tour?.steps[1]).toMatchObject({
body: 'Drag workspaces between lanes as their status changes.',
targetSelector:
'[data-contextual-tour-target="workspace-board-done-lane"], [data-contextual-tour-target="workspace-board-lanes"]'
})
})
it('orders the browser tour as grab, annotate, then import cookies', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'browser') as
| ContextualTour
| undefined
expect(tour?.steps.map((step) => step.title)).toEqual([
'Grab page context for agents',
'Mark design feedback in place',
'Stay logged in'
])
expect(tour?.steps[0]).toMatchObject({
targetSelector: '[data-contextual-tour-target="browser-grab-control"]',
preferredPlacement: 'bottom'
})
expect(tour?.steps[1]).toMatchObject({
targetSelector: '[data-contextual-tour-target="browser-annotation-control"]',
preferredPlacement: 'bottom'
})
expect(tour?.steps[2]).toMatchObject({
body: 'Bring your existing logins into Orca to stay signed in immediately.',
// Prefers the always-visible Import button, falling back to the overflow
// menu's Import Cookies row once the hint button is dismissed.
targetSelector:
'[data-contextual-tour-target="browser-import-hint"], [data-contextual-tour-target="browser-import-cookies-control"]',
preferredPlacement: 'bottom'
})
})
it('points the tasks tour at the row workspace action before toolbar fallbacks', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'tasks') as
| ContextualTour
| undefined
const step = tour?.steps[2]
expect(step).toMatchObject({
title: 'Start from work items',
body: 'Use Start or Open on a task, issue, review, or merge request to bring its context into a workspace.'
})
expect(step?.targetSelector.split(', ')).toEqual([
'[data-contextual-tour-target="tasks-start-workspace"]',
'[data-contextual-tour-target="tasks-actions"]',
'[data-contextual-tour-target="tasks-search-presets"]'
])
})
it('orders the automations tour as create, then results', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'automations') as
| ContextualTour
| undefined
expect(tour?.steps.map((step) => step.title)).toEqual([
'What is an automation?',
'Find the results'
])
expect(tour?.steps[0]).toMatchObject({
body: 'Automations run agent work on a schedule. Add an automation by clicking this button.',
requiredForStart: true
})
expect(tour?.steps.map((step) => step.targetSelector)).toEqual([
'[data-contextual-tour-target="automations-create"]',
'[data-contextual-tour-target="automations-runs"]'
])
})
it('defines the floating workspace tour on the action list with a surface fallback', () => {
const tour = CONTEXTUAL_TOURS.find((entry) => entry.id === 'floating-workspace') as
| ContextualTour
| undefined
expect(tour?.steps.map((step) => step.title)).toEqual([
'Run an agent across every repo',
'Or use it as a scratchpad'
])
expect(tour?.steps.map((step) => step.body)).toEqual([
'Agents here run in any folder you choose. Point one at the directory above your services to work across all your repos at once.',
'Open agents, scratch terminals, notes, and browser tabs without cluttering the worktree you’re focused on.'
])
expect(tour?.steps[0]).toMatchObject({
requiredForStart: true,
preferredPlacement: 'left'
})
expect(tour?.steps[1]?.preferredPlacement).toBe('left')
expect(tour?.steps.map((step) => step.targetSelector)).toEqual([
'[data-contextual-tour-target="floating-workspace-new-terminal"], [data-contextual-tour-target="floating-workspace-surface"]',
'[data-contextual-tour-target="floating-workspace-new-markdown"], [data-contextual-tour-target="floating-workspace-surface"]'
])
})
it('allows only workspace creation over its workspace composer modal', () => {
const modalTours = (CONTEXTUAL_TOURS as readonly ContextualTour[]).filter(
(tour) => tour.allowedActiveModals?.length
)
expect(modalTours.map((tour) => tour.id)).toEqual(['workspace-creation'])
expect(modalTours[0]?.allowedActiveModals).toEqual(['new-workspace-composer'])
})
it('normalizes persisted ids by removing unknowns and duplicates', () => {
expect(
normalizeContextualTourIds([
'tasks',
'unknown',
'workspace-agent-sessions',
'browser',
'tasks',
null,
'workspace-creation'
])
).toEqual(['tasks', 'workspace-agent-sessions', 'browser', 'workspace-creation'])
})
})