Files
supabase/apps/studio/components/ui/SteppedFlow/SteppedFlow.test.tsx
Danny White 53df997425 feat(studio): add SteppedFlow component (#49583)
## What kind of change does this PR introduce?

Feature (shared UI primitive).

## What is the current behavior?

No shared stepped wizard shell in Studio. Upcoming create-pipeline work
needs a reusable step container with header, progress, and
primary/secondary actions.

## What is the new behavior?

Adds `SteppedFlow` and `SteppedFlowHeader` with unit tests: step list,
current step content, next/back, optional first-step cancel, and header
actions slot.

No product callsite yet. Safe to merge on its own; the create-pipeline
wizard PR will consume it.

## To test

Code review + vitest:

```sh
cd apps/studio && pnpm exec vitest run components/ui/SteppedFlow/SteppedFlow.test.tsx
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a reusable stepped-flow interface with progress indicators and
step-specific content.
* Supports optional headers, actions, loading and disabled states,
forms, and configurable button types.
* Added navigation controls for moving forward, going back, cancelling,
and completing the flow.
  * Supports customizable final actions.

* **Tests**
* Added coverage for navigation, cancellation, headers, optional
actions, final actions, and disabled states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-28 09:43:32 +10:00

174 lines
4.8 KiB
TypeScript

import { fireEvent, screen } from '@testing-library/react'
import { describe, expect, test, vi } from 'vitest'
import { SteppedFlow, SteppedFlowHeader } from './SteppedFlow'
import { customRender } from '@/tests/lib/custom-render'
const steps = [
{ id: 'destination', label: 'Destination' },
{ id: 'connection', label: 'Connection' },
{ id: 'review', label: 'Review' },
]
describe('SteppedFlow', () => {
test('does not show Back on the first step', () => {
customRender(
<SteppedFlow steps={steps} currentStep="destination" onStepChange={vi.fn()}>
Step body
</SteppedFlow>
)
expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument()
})
test('shows Cancel on the first step when onCancel is provided', () => {
const onCancel = vi.fn()
customRender(
<SteppedFlow
steps={steps}
currentStep="destination"
onStepChange={vi.fn()}
onCancel={onCancel}
>
Step body
</SteppedFlow>
)
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(onCancel).toHaveBeenCalledOnce()
})
test('does not show Cancel after the first step', () => {
customRender(
<SteppedFlow steps={steps} currentStep="connection" onStepChange={vi.fn()} onCancel={vi.fn()}>
Step body
</SteppedFlow>
)
expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument()
})
test('shows Back after the first step', () => {
const onStepChange = vi.fn()
customRender(
<SteppedFlow steps={steps} currentStep="connection" onStepChange={onStepChange}>
Step body
</SteppedFlow>
)
fireEvent.click(screen.getByRole('button', { name: 'Back' }))
expect(onStepChange).toHaveBeenCalledWith('destination')
})
test('advances to the next step when onNext is omitted', () => {
const onStepChange = vi.fn()
customRender(
<SteppedFlow steps={steps} currentStep="destination" onStepChange={onStepChange}>
Step body
</SteppedFlow>
)
fireEvent.click(screen.getByRole('button', { name: 'Next' }))
expect(onStepChange).toHaveBeenCalledWith('connection')
})
test('shows the final action on the last step instead of Next', () => {
const onFinal = vi.fn()
customRender(
<SteppedFlow
steps={steps}
currentStep="review"
onStepChange={vi.fn()}
onNext={vi.fn()}
finalAction={{ label: 'Create and start pipeline', onClick: onFinal }}
>
Step body
</SteppedFlow>
)
expect(screen.queryByRole('button', { name: /Next/ })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Create and start pipeline' }))
expect(onFinal).toHaveBeenCalledOnce()
})
test('renders a step header heading', () => {
customRender(
<SteppedFlow steps={steps} currentStep="destination" onStepChange={vi.fn()}>
<SteppedFlowHeader title="Choose a destination" description="Where should data go?" />
</SteppedFlow>
)
expect(screen.getByRole('heading', { name: 'Choose a destination' })).toBeInTheDocument()
expect(screen.getByText('Where should data go?')).toBeInTheDocument()
})
test('renders optional header actions', () => {
customRender(
<SteppedFlow steps={steps} currentStep="connection" onStepChange={vi.fn()}>
<SteppedFlowHeader
title="Authorize the destination"
description="Name this pipeline."
actions={
<button type="button" tabIndex={0}>
Docs
</button>
}
/>
</SteppedFlow>
)
expect(screen.getByRole('button', { name: 'Docs' })).toBeInTheDocument()
})
test('disables Back while navigation is locked', () => {
customRender(
<SteppedFlow
steps={steps}
currentStep="review"
onStepChange={vi.fn()}
navigationDisabled
finalAction={{ label: 'Create and start pipeline', loading: true }}
>
Step body
</SteppedFlow>
)
expect(screen.getByRole('button', { name: 'Back' })).toBeDisabled()
})
test('disables Next while navigation is locked', () => {
customRender(
<SteppedFlow
steps={steps}
currentStep="destination"
onStepChange={vi.fn()}
navigationDisabled
>
Step body
</SteppedFlow>
)
expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled()
})
test('disables the final action while navigation is locked', () => {
customRender(
<SteppedFlow
steps={steps}
currentStep="review"
onStepChange={vi.fn()}
navigationDisabled
finalAction={{ label: 'Create and start pipeline' }}
>
Step body
</SteppedFlow>
)
expect(screen.getByRole('button', { name: 'Create and start pipeline' })).toBeDisabled()
})
})