fix: scope workspace-creation-project tour target to project picker only

The tour target was previously applied to a container that included both
the project picker and the run target picker below it. Restructure the
layout to scope the target to only the project-related section, and add
a test to verify the tour target does not span into the run target picker.
This commit is contained in:
Jinjing
2026-09-03 11:34:12 -07:00
parent 67e22345da
commit 28cd145fa5
2 changed files with 115 additions and 136 deletions
@@ -104,7 +104,7 @@ vi.mock('@/components/new-workspace/ProjectCombobox', () => ({
value: string | null
onValueChange: (value: string) => void
}) => (
<div data-testid="project-combobox" data-value={value ?? ''}>
<div data-testid="project-combobox" data-project-combobox-root="true" data-value={value ?? ''}>
{options.map((option) => (
<button key={option.id} type="button" onClick={() => onValueChange(option.id)}>
{option.displayName}
@@ -165,32 +165,35 @@ const devboxNeedsSetupHostOption: ProjectHostSetupOption = {
canSetLocation: true
}
const disconnectedDevboxNeedsSetupHostOption: ProjectHostSetupOption = {
kind: 'needs-setup',
id: 'needs-setup:ssh:devbox',
projectId: 'project-group:platform',
hostId: 'ssh:devbox',
label: 'Devbox',
detail: 'Connect this host to set up projects',
isAvailable: false,
attention: false,
canSetLocation: false,
connectAction: { kind: 'ssh', targetId: 'devbox' }
function makeDisconnectedHostOption(targetId: string, label: string): ProjectHostSetupOption {
return {
kind: 'needs-setup',
id: `needs-setup:ssh:${targetId}`,
projectId: 'project-group:platform',
hostId: `ssh:${targetId}`,
label,
detail: 'Connect this host to set up projects',
isAvailable: false,
attention: false,
canSetLocation: false,
connectAction: { kind: 'ssh', targetId }
}
}
const disconnectedBastionNeedsSetupHostOption: ProjectHostSetupOption = {
kind: 'needs-setup',
id: 'needs-setup:ssh:bastion',
projectId: 'project-group:platform',
hostId: 'ssh:bastion',
label: 'Bastion',
detail: 'Connect this host to set up projects',
isAvailable: false,
attention: false,
canSetLocation: false,
connectAction: { kind: 'ssh', targetId: 'bastion' }
const disconnectedDevboxNeedsSetupHostOption = makeDisconnectedHostOption('devbox', 'Devbox')
const disconnectedBastionNeedsSetupHostOption = makeDisconnectedHostOption('bastion', 'Bastion')
const pnpmInstallSetupConfig = {
source: 'yaml' as const,
command: 'pnpm install',
kind: 'setup' as const
}
const vmRecipeHostOptions: ProjectHostSetupOption[] = [
{ kind: 'ready', id: 'setup-local', label: 'Local Mac', path: '/Users/alice/orca' },
{ kind: 'ready', id: 'setup-builder', label: 'Builder', path: '/workspace/orca' }
] as never
function findConnectButton(label: string): HTMLButtonElement | undefined {
const item = findRunTargetItem(label)
return [...(item?.querySelectorAll('button') ?? [])].find((button) =>
@@ -313,6 +316,12 @@ function unmountCurrent(): void {
current?.container.remove()
}
function findWaitSwitch(container: HTMLElement): HTMLButtonElement | null {
return container.querySelector(
'[role="switch"][aria-label="Wait for setup to complete before starting agent"]'
)
}
describe('NewWorkspaceComposerCard folder task source mode', () => {
beforeEach(() => {
;(window as unknown as { api: unknown }).api = {
@@ -378,20 +387,30 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
)
expect(projectSection?.textContent).not.toContain('Task Source')
expect(nameSection?.textContent).toContain("Name or 'Create From'")
expect(
current.container
.querySelector('[aria-label="workspace name"]')
?.getAttribute('data-repo-backed-search-count')
).toBe('2')
expect(
current.container
.querySelector('[aria-label="workspace name"]')
?.getAttribute('data-repo-backed-search-names')
).toBe('Repo A,Repo B')
const nameInput = current.container.querySelector('[aria-label="workspace name"]')
expect(nameInput?.getAttribute('data-repo-backed-search-count')).toBe('2')
expect(nameInput?.getAttribute('data-repo-backed-search-names')).toBe('Repo A,Repo B')
expect(current.container.querySelector('[data-testid="repo-backed-source-trigger"]')).toBeNull()
expect(current.container.querySelectorAll('[data-testid="project-combobox"]')).toHaveLength(1)
})
it('scopes the workspace-creation-project tour target to the project picker rather than the run target picker', () => {
current = renderCard({ projectHostSetupOptions: [localReadyHostOption] })
const projectTourTarget = current.container.querySelector(
'[data-contextual-tour-target="workspace-creation-project"]'
)
expect(projectTourTarget).toBeTruthy()
expect(projectTourTarget?.querySelector('[data-project-combobox-root="true"]')).toBeTruthy()
expect(projectTourTarget?.querySelector('[data-run-target-combobox-root="true"]')).toBeNull()
expect(projectTourTarget?.textContent).not.toContain('Run on')
const runTargetPicker = current.container.querySelector(
'div[data-run-target-combobox-root="true"]'
)
expect(runTargetPicker).toBeTruthy()
})
it('keeps the reuse-branch row collapsed until a local branch is reusable', () => {
// Why: the row stays mounted (for the smooth height transition) but is
// collapsed + aria-hidden when reuse isn't possible.
@@ -467,11 +486,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
current = renderCard({
advancedOpen: true,
setupControlsEnabled: true,
setupConfig: {
source: 'yaml',
command: 'pnpm install',
kind: 'setup'
}
setupConfig: pnpmInstallSetupConfig
})
expect(current.container.textContent).toContain(
'Wait for setup to complete before starting agent'
@@ -484,17 +499,11 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
advancedOpen: true,
setupControlsEnabled: true,
resolvedSetupDecision: 'run',
setupConfig: {
source: 'yaml',
command: 'pnpm install',
kind: 'setup'
},
setupConfig: pnpmInstallSetupConfig,
onSetupAgentStartupPolicyChange: (next) => changes.push(next)
})
const waitSwitch = current.container.querySelector<HTMLButtonElement>(
'[role="switch"][aria-label="Wait for setup to complete before starting agent"]'
)
const waitSwitch = findWaitSwitch(current.container)
expect(waitSwitch).toBeTruthy()
expect(waitSwitch?.disabled).toBe(false)
act(() => waitSwitch?.click())
@@ -507,17 +516,11 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
advancedOpen: true,
setupControlsEnabled: true,
resolvedSetupDecision: 'skip',
setupConfig: {
source: 'yaml',
command: 'pnpm install',
kind: 'setup'
},
setupConfig: pnpmInstallSetupConfig,
onSetupAgentStartupPolicyChange: (next) => changes.push(next)
})
const waitSwitch = current.container.querySelector<HTMLButtonElement>(
'[role="switch"][aria-label="Wait for setup to complete before starting agent"]'
)
const waitSwitch = findWaitSwitch(current.container)
expect(waitSwitch?.disabled).toBe(true)
// Nothing to wait for when setup won't run — clicking is inert.
act(() => waitSwitch?.click())
@@ -799,20 +802,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
const hostChanges: string[] = []
const recipeChanges: (string | null)[] = []
current = renderCard({
projectHostSetupOptions: [
{
kind: 'ready',
id: 'setup-local',
label: 'Local Mac',
path: '/Users/alice/orca'
},
{
kind: 'ready',
id: 'setup-builder',
label: 'Builder',
path: '/workspace/orca'
}
] as never,
projectHostSetupOptions: vmRecipeHostOptions,
selectedProjectHostSetupId: 'setup-local',
onProjectHostSetupChange: (setupId) => hostChanges.push(setupId),
ephemeralVmRecipes: [
@@ -853,20 +843,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
const hostChanges: string[] = []
const recipeChanges: (string | null)[] = []
current = renderCard({
projectHostSetupOptions: [
{
kind: 'ready',
id: 'setup-local',
label: 'Local Mac',
path: '/Users/alice/orca'
},
{
kind: 'ready',
id: 'setup-builder',
label: 'Builder',
path: '/workspace/orca'
}
] as never,
projectHostSetupOptions: vmRecipeHostOptions,
selectedProjectHostSetupId: 'setup-local',
onProjectHostSetupChange: (setupId) => hostChanges.push(setupId),
ephemeralVmRecipes: [
@@ -80,62 +80,64 @@ export function NewWorkspaceComposerProjectSection({
selectedProjectName
}: NewWorkspaceComposerProjectSectionProps): React.JSX.Element {
return (
<div className="space-y-1" data-contextual-tour-target="workspace-creation-project">
<div className="flex items-center justify-between gap-2">
<label className="text-xs font-medium text-muted-foreground">
{projectLabel ??
translate('auto.components.NewWorkspaceComposerCard.969a8bff66', 'Project')}
</label>
{showAddProjectButton ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={onAddProject}
className="size-5 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
aria-label={translate(
'auto.components.NewWorkspaceComposerCard.d6b0a96f32',
'Add project'
)}
>
<FolderPlus className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')}
</TooltipContent>
</Tooltip>
<div className="space-y-1">
<div className="space-y-1" data-contextual-tour-target="workspace-creation-project">
<div className="flex items-center justify-between gap-2">
<label className="text-xs font-medium text-muted-foreground">
{projectLabel ??
translate('auto.components.NewWorkspaceComposerCard.969a8bff66', 'Project')}
</label>
{showAddProjectButton ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
onClick={onAddProject}
className="size-5 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
aria-label={translate(
'auto.components.NewWorkspaceComposerCard.d6b0a96f32',
'Add project'
)}
>
<FolderPlus className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')}
</TooltipContent>
</Tooltip>
) : null}
</div>
<ProjectCombobox
options={projectOptions}
value={selectedProjectId}
onValueChange={onProjectChange}
onValueSelected={focusNameInput}
onAddProject={onAddProject}
placeholder={
projectPlaceholder ??
translate('auto.components.NewWorkspaceComposerCard.dccd26d4e4', 'Choose project')
}
triggerClassName="h-9 w-full border-input text-sm focus:border-ring focus:ring-[3px] focus:ring-ring/50"
invalid={Boolean(projectError)}
describedBy={projectDescriptionId}
/>
{projectError ? (
<p id={projectDescriptionId} className="text-[11px] text-destructive">
{projectError}
</p>
) : projectOptions.length === 0 ? (
<p id={projectDescriptionId} className="text-[11px] text-muted-foreground">
{emptyProjectMessage ??
translate(
'auto.components.NewWorkspaceComposerCard.addProjectBeforeWorkspace',
'Add a project before creating a workspace.'
)}
</p>
) : null}
</div>
<ProjectCombobox
options={projectOptions}
value={selectedProjectId}
onValueChange={onProjectChange}
onValueSelected={focusNameInput}
onAddProject={onAddProject}
placeholder={
projectPlaceholder ??
translate('auto.components.NewWorkspaceComposerCard.dccd26d4e4', 'Choose project')
}
triggerClassName="h-9 w-full border-input text-sm focus:border-ring focus:ring-[3px] focus:ring-ring/50"
invalid={Boolean(projectError)}
describedBy={projectDescriptionId}
/>
{projectError ? (
<p id={projectDescriptionId} className="text-[11px] text-destructive">
{projectError}
</p>
) : projectOptions.length === 0 ? (
<p id={projectDescriptionId} className="text-[11px] text-muted-foreground">
{emptyProjectMessage ??
translate(
'auto.components.NewWorkspaceComposerCard.addProjectBeforeWorkspace',
'Add a project before creating a workspace.'
)}
</p>
) : null}
{shouldShowRunTargetPicker ? (
<div className="space-y-1 pt-3">
<label className="block min-w-0 truncate text-xs font-medium text-muted-foreground">