mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
test(dify-ui): focus component tests on behavior (#39030)
This commit is contained in:
parent
abb9972e19
commit
5af8f6af4d
@ -1,46 +1,7 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '../index'
|
||||
import { AlertDialog, AlertDialogActions, AlertDialogContent, AlertDialogTitle } from '../index'
|
||||
|
||||
describe('AlertDialog wrapper', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render alert dialog content when dialog is open', async () => {
|
||||
const screen = await render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('Confirm Delete')
|
||||
await expect
|
||||
.element(screen.getByRole('alertdialog'))
|
||||
.toHaveTextContent('This action cannot be undone.')
|
||||
})
|
||||
|
||||
it('should not render content when dialog is closed', async () => {
|
||||
const screen = await render(
|
||||
<AlertDialog open={false}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Hidden Title</AlertDialogTitle>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should apply custom className to popup', async () => {
|
||||
const screen = await render(
|
||||
@ -67,77 +28,18 @@ describe('AlertDialog wrapper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should open and close dialog when trigger and cancel button are clicked', async () => {
|
||||
const screen = await render(
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||
<AlertDialogDescription>Please confirm the action.</AlertDialogDescription>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>Cancel</AlertDialogCancelButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument()
|
||||
|
||||
await screen.getByRole('button', { name: 'Open Dialog' }).click()
|
||||
await expect.element(screen.getByRole('alertdialog')).toHaveTextContent('Action Required')
|
||||
|
||||
await screen.getByRole('button', { name: 'Cancel' }).click()
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Composition Helpers', () => {
|
||||
it('should render actions wrapper and confirm button', async () => {
|
||||
it('should forward className to the actions wrapper', async () => {
|
||||
const screen = await render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||
<AlertDialogActions data-testid="actions" className="custom-actions">
|
||||
<AlertDialogConfirmButton>Confirm</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
<AlertDialogActions data-testid="actions" className="custom-actions" />
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByTestId('actions')).toHaveClass('custom-actions')
|
||||
await expect.element(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep dialog open after confirm click and close via cancel helper', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
const screen = await render(
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>Cancel</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton onClick={onConfirm}>Confirm</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>,
|
||||
)
|
||||
|
||||
await screen.getByRole('button', { name: 'Open Dialog' }).click()
|
||||
await expect.element(screen.getByRole('alertdialog')).toBeInTheDocument()
|
||||
|
||||
await screen.getByRole('button', { name: 'Confirm' }).click()
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
await expect.element(screen.getByRole('alertdialog')).toBeInTheDocument()
|
||||
|
||||
await screen.getByRole('button', { name: 'Cancel' }).click()
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.container.querySelector('[role="alertdialog"]')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -67,6 +67,9 @@ export const Default: Story = {
|
||||
await expect(dialog).toBeVisible()
|
||||
})
|
||||
|
||||
await userEvent.click(body.getByRole('button', { name: 'Delete' }))
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
await userEvent.click(body.getByRole('button', { name: 'Cancel' }))
|
||||
await waitFor(async () => {
|
||||
await expect(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Avatar, AvatarFallback, AvatarImage, AvatarRoot } from '..'
|
||||
import { Avatar } from '..'
|
||||
|
||||
function stubImageLoader() {
|
||||
const originalImage = window.Image
|
||||
@ -27,26 +27,12 @@ function stubImageLoader() {
|
||||
|
||||
describe('Avatar', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should keep the fallback visible when avatar URL is provided before image load', async () => {
|
||||
const screen = await render(
|
||||
<Avatar name="John Doe" avatar="https://example.com/avatar.jpg" />,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render fallback with uppercase initial when avatar is null', async () => {
|
||||
const screen = await render(<Avatar name="alice" avatar={null} />)
|
||||
|
||||
expect(screen.container.querySelector('img')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the fallback when avatar is provided', async () => {
|
||||
const screen = await render(<Avatar name="John" avatar="https://example.com/avatar.jpg" />)
|
||||
|
||||
await expect.element(screen.getByText('J')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('className prop', () => {
|
||||
@ -58,58 +44,13 @@ describe('Avatar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Primitives', () => {
|
||||
it('should support composed avatar usage through exported primitives', async () => {
|
||||
const screen = await render(
|
||||
<AvatarRoot size="sm" data-testid="avatar-root">
|
||||
<AvatarImage src="https://example.com/avatar.jpg" alt="Jane Doe" />
|
||||
<AvatarFallback size="sm" style={{ backgroundColor: 'rgb(1, 2, 3)' }}>
|
||||
J
|
||||
</AvatarFallback>
|
||||
</AvatarRoot>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByTestId('avatar-root')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('J')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('J')).toHaveStyle({ backgroundColor: 'rgb(1, 2, 3)' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string name gracefully', async () => {
|
||||
const screen = await render(<Avatar name="" avatar={null} />)
|
||||
|
||||
expect(screen.container.firstElementChild).toBeInTheDocument()
|
||||
expect(screen.container.textContent).toBe('')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ name: '中文名', expected: '中', label: 'Chinese characters' },
|
||||
{ name: '123User', expected: '1', label: 'number' },
|
||||
])(
|
||||
'should display first character when name starts with $label',
|
||||
async ({ name, expected }) => {
|
||||
const screen = await render(<Avatar name={name} avatar={null} />)
|
||||
|
||||
await expect.element(screen.getByText(expected)).toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
|
||||
it('should handle empty string avatar as falsy value', async () => {
|
||||
const screen = await render(<Avatar name="Test" avatar="" />)
|
||||
|
||||
expect(screen.container.querySelector('img')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByText('T')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onLoadingStatusChange', () => {
|
||||
it('should forward image loading status changes', async () => {
|
||||
it('should show fallback until the image loads and forward status changes', async () => {
|
||||
const { images, restore } = stubImageLoader()
|
||||
const onStatusChange = vi.fn()
|
||||
|
||||
try {
|
||||
await render(
|
||||
const screen = await render(
|
||||
<Avatar
|
||||
name="John"
|
||||
avatar="https://example.com/avatar.jpg"
|
||||
@ -117,6 +58,7 @@ describe('Avatar', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByText('J')).toBeVisible()
|
||||
await vi.waitFor(() => {
|
||||
expect(onStatusChange).toHaveBeenCalledWith('loading')
|
||||
})
|
||||
@ -126,6 +68,7 @@ describe('Avatar', () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(onStatusChange).toHaveBeenCalledWith('loaded')
|
||||
})
|
||||
await expect.element(screen.getByRole('img', { name: 'John' })).toBeVisible()
|
||||
} finally {
|
||||
restore()
|
||||
}
|
||||
|
||||
@ -7,16 +7,6 @@ const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElem
|
||||
|
||||
describe('Button', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders children text', async () => {
|
||||
const screen = await render(<Button>Click me</Button>)
|
||||
await expect.element(screen.getByRole('button')).toHaveTextContent('Click me')
|
||||
})
|
||||
|
||||
it('renders as a native button element by default', async () => {
|
||||
const screen = await render(<Button>Click me</Button>)
|
||||
expect(screen.getByRole('button').element().tagName).toBe('BUTTON')
|
||||
})
|
||||
|
||||
it('defaults to type="button"', async () => {
|
||||
const screen = await render(<Button>Click me</Button>)
|
||||
await expect.element(screen.getByRole('button')).toHaveAttribute('type', 'button')
|
||||
@ -39,51 +29,8 @@ describe('Button', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('loading', () => {
|
||||
it('shows spinner when loading', async () => {
|
||||
const screen = await render(<Button loading>Click me</Button>)
|
||||
expect(
|
||||
screen.getByRole('button').element().querySelector('[aria-hidden="true"]'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides spinner when not loading', async () => {
|
||||
const screen = await render(<Button loading={false}>Click me</Button>)
|
||||
expect(
|
||||
screen.getByRole('button').element().querySelector('[aria-hidden="true"]'),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps loading buttons focusable by default', async () => {
|
||||
const screen = await render(<Button loading>Click me</Button>)
|
||||
const button = screen.getByRole('button').element()
|
||||
|
||||
expect(button).not.toHaveAttribute('disabled')
|
||||
expect((button as HTMLButtonElement).disabled).toBe(false)
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true')
|
||||
|
||||
asHTMLElement(button).focus()
|
||||
expect(button).toHaveFocus()
|
||||
})
|
||||
|
||||
it('does not set aria-busy when loading', async () => {
|
||||
const screen = await render(<Button loading>Click me</Button>)
|
||||
await expect.element(screen.getByRole('button')).not.toHaveAttribute('aria-busy')
|
||||
})
|
||||
|
||||
it('does not set aria-busy when not loading', async () => {
|
||||
const screen = await render(<Button>Click me</Button>)
|
||||
await expect.element(screen.getByRole('button')).not.toHaveAttribute('aria-busy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('disabled', () => {
|
||||
it('disables button when disabled prop is set', async () => {
|
||||
const screen = await render(<Button disabled>Click me</Button>)
|
||||
await expect.element(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('does not keep normal disabled buttons focusable by default', async () => {
|
||||
describe('disabled states', () => {
|
||||
it('uses native disabled semantics for regular disabled buttons', async () => {
|
||||
const screen = await render(<Button disabled>Click me</Button>)
|
||||
const button = screen.getByRole('button').element()
|
||||
|
||||
@ -102,13 +49,6 @@ describe('Button', () => {
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
it('fires onClick when clicked', async () => {
|
||||
const onClick = vi.fn()
|
||||
const screen = await render(<Button onClick={onClick}>Click me</Button>)
|
||||
await screen.getByRole('button').click()
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not implicitly submit a form through a loading submit button', async () => {
|
||||
const onSubmit = vi.fn((event: React.FormEvent<HTMLFormElement>) => event.preventDefault())
|
||||
const screen = await render(
|
||||
|
||||
@ -2,38 +2,6 @@ import { render } from 'vitest-browser-react'
|
||||
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '../index'
|
||||
|
||||
describe('Collapsible wrappers', () => {
|
||||
it('renders the Base UI anatomy with an accessible trigger', async () => {
|
||||
const screen = await render(
|
||||
<Collapsible defaultOpen data-testid="collapsible-root">
|
||||
<CollapsibleTrigger>Recovery keys</CollapsibleTrigger>
|
||||
<CollapsiblePanel>Panel content</CollapsiblePanel>
|
||||
</Collapsible>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByTestId('collapsible-root')).toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'Recovery keys' }))
|
||||
.toHaveAttribute('data-panel-open', '')
|
||||
await expect.element(screen.getByText('Panel content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('toggles open state through the trigger without caller-owned state', async () => {
|
||||
const screen = await render(
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger>Toggle section</CollapsibleTrigger>
|
||||
<CollapsiblePanel>Hidden content</CollapsiblePanel>
|
||||
</Collapsible>,
|
||||
)
|
||||
const trigger = screen.getByRole('button', { name: 'Toggle section' })
|
||||
|
||||
await expect.element(trigger).not.toHaveAttribute('data-panel-open')
|
||||
|
||||
await trigger.click()
|
||||
|
||||
await expect.element(trigger).toHaveAttribute('data-panel-open', '')
|
||||
await expect.element(screen.getByText('Hidden content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('forwards className to every compound part', async () => {
|
||||
const screen = await render(
|
||||
<Collapsible defaultOpen className="custom-root">
|
||||
@ -49,17 +17,18 @@ describe('Collapsible wrappers', () => {
|
||||
expect(screen.container.querySelector('.custom-root')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes Base UI panel props through to the panel', async () => {
|
||||
it('keeps closed panel content mounted when requested', async () => {
|
||||
const screen = await render(
|
||||
<Collapsible defaultOpen>
|
||||
<CollapsibleTrigger>Styled trigger</CollapsibleTrigger>
|
||||
<CollapsiblePanel keepMounted>Styled panel</CollapsiblePanel>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger>Recovery options</CollapsibleTrigger>
|
||||
<CollapsiblePanel keepMounted>Recovery codes</CollapsiblePanel>
|
||||
</Collapsible>,
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'Styled trigger' }))
|
||||
.toHaveAttribute('data-panel-open', '')
|
||||
await expect.element(screen.getByText('Styled panel')).toBeInTheDocument()
|
||||
.element(screen.getByRole('button', { name: 'Recovery options' }))
|
||||
.toHaveAttribute('aria-expanded', 'false')
|
||||
await expect.element(screen.getByText('Recovery codes')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('Recovery codes')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect, waitFor } from 'storybook/test'
|
||||
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '.'
|
||||
import { cn } from '../cn'
|
||||
|
||||
@ -76,6 +77,22 @@ export const DefaultClosed: Story = {
|
||||
<RecoveryKeys />
|
||||
</Collapsible>
|
||||
),
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const trigger = canvas.getByRole('button', { name: 'Recovery keys' })
|
||||
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
await expect(canvas.queryByText('alien-bean-pasta')).not.toBeInTheDocument()
|
||||
|
||||
await userEvent.click(trigger)
|
||||
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
await expect(canvas.getByText('alien-bean-pasta')).toBeVisible()
|
||||
|
||||
await userEvent.click(trigger)
|
||||
await waitFor(async () => {
|
||||
await expect(canvas.queryByText('alien-bean-pasta')).not.toBeInTheDocument()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultOpen: Story = {
|
||||
|
||||
@ -67,25 +67,6 @@ describe('FileTree', () => {
|
||||
await expect.element(selectedFile).toHaveAttribute('data-selected')
|
||||
})
|
||||
|
||||
it('collapses and expands folders with click and native button keyboard behavior', async () => {
|
||||
const screen = await render(<TestFileTree />)
|
||||
const src = screen.getByRole('button', { name: 'src' })
|
||||
|
||||
await src.click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'src' }))
|
||||
.toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.container.textContent).not.toContain('components')
|
||||
|
||||
await src.click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'src' }))
|
||||
.toHaveAttribute('aria-expanded', 'true')
|
||||
await expect.element(screen.getByRole('button', { name: 'components' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('activates file preview buttons without navigation semantics', async () => {
|
||||
const onPreview = vi.fn()
|
||||
const screen = await render(<TestFileTree onPreview={onPreview} />)
|
||||
|
||||
@ -4,19 +4,6 @@ import { Form } from '../../form'
|
||||
import { Input } from '../index'
|
||||
|
||||
describe('Input', () => {
|
||||
it('should render a labelled Base UI input with its value', async () => {
|
||||
const screen = await render(
|
||||
<label>
|
||||
Workspace name
|
||||
<Input name="workspaceName" defaultValue="Dify" />
|
||||
</label>,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'Workspace name' })
|
||||
|
||||
await expect.element(input).toHaveValue('Dify')
|
||||
})
|
||||
|
||||
it('should use Field invalid state', async () => {
|
||||
const screen = await render(
|
||||
<Field name="repositoryUrl" invalid>
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
SelectValue,
|
||||
} from '../index'
|
||||
|
||||
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||
const renderWithSafeViewport = (ui: React.ReactNode) =>
|
||||
render(<div style={{ minHeight: '100vh', minWidth: '100vw', padding: '240px' }}>{ui}</div>)
|
||||
|
||||
@ -258,58 +257,6 @@ describe('Select wrappers', () => {
|
||||
})
|
||||
|
||||
describe('SelectItem', () => {
|
||||
it('should render options when children are provided', async () => {
|
||||
const screen = await renderOpenSelect()
|
||||
|
||||
await expect.element(screen.getByRole('option', { name: 'Seattle' })).toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('option', { name: 'New York' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should navigate items with arrow keys', async () => {
|
||||
const screen = await render(
|
||||
<Select defaultValue="seattle">
|
||||
<SelectTrigger aria-label="city select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent listProps={{ role: 'listbox', 'aria-label': 'select list' }}>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="new-york">
|
||||
<SelectItemText>New York</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="tokyo">
|
||||
<SelectItemText>Tokyo</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
)
|
||||
|
||||
const trigger = asHTMLElement(screen.getByRole('combobox', { name: 'city select' }).element())
|
||||
|
||||
trigger.focus()
|
||||
trigger.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }),
|
||||
)
|
||||
await expect
|
||||
.element(screen.getByRole('option', { name: 'Seattle' }))
|
||||
.toHaveAttribute('data-highlighted')
|
||||
|
||||
const highlightedItem = asHTMLElement(
|
||||
screen.getByRole('option', { name: 'Seattle' }).element(),
|
||||
)
|
||||
highlightedItem.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true }),
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('option', { name: 'New York' }))
|
||||
.toHaveAttribute('data-highlighted')
|
||||
})
|
||||
|
||||
it('should expose disabled item semantics', async () => {
|
||||
const screen = await render(
|
||||
<Select open defaultValue="seattle">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { StatusDot, StatusDotSkeleton } from '../index'
|
||||
import { StatusDot } from '../index'
|
||||
|
||||
describe('StatusDot', () => {
|
||||
it('is hidden from assistive tech by default', async () => {
|
||||
@ -14,10 +14,4 @@ describe('StatusDot', () => {
|
||||
await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-label', 'Active')
|
||||
await expect.element(screen.getByTestId('dot')).not.toHaveAttribute('aria-hidden')
|
||||
})
|
||||
|
||||
it('renders the skeleton placeholder', async () => {
|
||||
const screen = await render(<StatusDotSkeleton data-testid="dot" />)
|
||||
|
||||
await expect.element(screen.getByTestId('dot')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,71 +1,16 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Switch, SwitchSkeleton } from '../index'
|
||||
import { Switch } from '../index'
|
||||
|
||||
const getThumb = (switchElement: HTMLElement | SVGElement) => switchElement.querySelector('span')
|
||||
|
||||
describe('Switch', () => {
|
||||
it('should render in unchecked state when checked is false', async () => {
|
||||
const screen = await render(<Switch checked={false} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toBeInTheDocument()
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
await expect.element(switchElement).not.toHaveAttribute('data-checked')
|
||||
})
|
||||
|
||||
it('should render in checked state when checked is true', async () => {
|
||||
const screen = await render(<Switch checked={true} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'true')
|
||||
await expect.element(switchElement).toHaveAttribute('data-checked', '')
|
||||
})
|
||||
|
||||
it('should call onCheckedChange with next value when clicked', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch checked={false} onCheckedChange={onCheckedChange} />)
|
||||
|
||||
const switchElement = screen.getByRole('switch')
|
||||
await switchElement.click()
|
||||
|
||||
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
||||
expect(onCheckedChange).toHaveBeenCalledTimes(1)
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should work in controlled mode with checked prop', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch checked={false} onCheckedChange={onCheckedChange} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
await switchElement.click()
|
||||
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
await screen.rerender(<Switch checked={true} onCheckedChange={onCheckedChange} />)
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should work in uncontrolled mode with defaultChecked prop', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch defaultChecked={false} onCheckedChange={onCheckedChange} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
await switchElement.click()
|
||||
|
||||
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should not call onCheckedChange when disabled', async () => {
|
||||
it('should expose disabled state and stay out of the tab order', async () => {
|
||||
const screen = await render(<Switch checked={false} disabled />)
|
||||
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toBeDisabled()
|
||||
await expect.element(switchElement).toHaveAttribute('tabindex', '-1')
|
||||
await expect.element(switchElement).toHaveAttribute('data-disabled', '')
|
||||
})
|
||||
|
||||
@ -87,16 +32,6 @@ describe('Switch', () => {
|
||||
expect(getThumb(screen.getByRole('switch').element())).toHaveAttribute('data-checked', '')
|
||||
})
|
||||
|
||||
it('should reflect disabled and checked states together', async () => {
|
||||
const screen = await render(<Switch checked={false} disabled />)
|
||||
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-disabled', '')
|
||||
|
||||
await screen.rerender(<Switch checked={true} disabled />)
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-disabled', '')
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
|
||||
})
|
||||
|
||||
describe('loading state', () => {
|
||||
it('should render as disabled when loading', async () => {
|
||||
const screen = await render(<Switch checked={false} loading />)
|
||||
@ -121,28 +56,5 @@ describe('Switch', () => {
|
||||
await screen.rerender(<Switch checked={false} loading size="sm" />)
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply disabled data-state hooks when loading', async () => {
|
||||
const screen = await render(<Switch checked={false} loading />)
|
||||
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-disabled', '')
|
||||
|
||||
await screen.rerender(<Switch checked={true} loading />)
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-disabled', '')
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SwitchSkeleton', () => {
|
||||
it('should render a plain div without switch role', async () => {
|
||||
const screen = await render(<SwitchSkeleton data-testid="skeleton-switch" />)
|
||||
expect(screen.container.querySelector('[role="switch"]')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByTestId('skeleton-switch')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom className to skeleton', async () => {
|
||||
const screen = await render(<SwitchSkeleton className="custom-class" data-testid="s" />)
|
||||
await expect.element(screen.getByTestId('s')).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,47 +2,6 @@ import { render } from 'vitest-browser-react'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '../index'
|
||||
|
||||
describe('Tabs wrappers', () => {
|
||||
it('renders Base UI tabs with accessible roles', async () => {
|
||||
const screen = await render(
|
||||
<Tabs defaultValue="js">
|
||||
<TabsList>
|
||||
<TabsTab value="js">JavaScript</TabsTab>
|
||||
<TabsTab value="py">Python</TabsTab>
|
||||
</TabsList>
|
||||
<TabsPanel value="js">JS panel</TabsPanel>
|
||||
<TabsPanel value="py">Python panel</TabsPanel>
|
||||
</Tabs>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('tablist')).toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByRole('tab', { name: 'JavaScript' }))
|
||||
.toHaveAttribute('aria-selected', 'true')
|
||||
await expect
|
||||
.element(screen.getByRole('tab', { name: 'Python' }))
|
||||
.toHaveAttribute('aria-selected', 'false')
|
||||
await expect.element(screen.getByText('JS panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onValueChange while leaving controlled value to the caller', async () => {
|
||||
const onValueChange = vi.fn()
|
||||
const screen = await render(
|
||||
<Tabs value="js" onValueChange={onValueChange}>
|
||||
<TabsList>
|
||||
<TabsTab value="js">JavaScript</TabsTab>
|
||||
<TabsTab value="py">Python</TabsTab>
|
||||
</TabsList>
|
||||
</Tabs>,
|
||||
)
|
||||
|
||||
await screen.getByRole('tab', { name: 'Python' }).click()
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith('py', expect.anything())
|
||||
await expect
|
||||
.element(screen.getByRole('tab', { name: 'JavaScript' }))
|
||||
.toHaveAttribute('aria-selected', 'true')
|
||||
})
|
||||
|
||||
it('forwards className to composable parts', async () => {
|
||||
const screen = await render(
|
||||
<Tabs defaultValue="first">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { expect } from 'storybook/test'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '.'
|
||||
|
||||
const meta = {
|
||||
@ -34,4 +35,16 @@ export const Basic: Story = {
|
||||
</TabsPanel>
|
||||
</Tabs>
|
||||
),
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const overviewTab = canvas.getByRole('tab', { name: 'Overview' })
|
||||
const activityTab = canvas.getByRole('tab', { name: 'Activity' })
|
||||
|
||||
await expect(overviewTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(canvas.getByRole('tabpanel', { name: 'Overview' })).toBeVisible()
|
||||
|
||||
await userEvent.click(activityTab)
|
||||
|
||||
await expect(activityTab).toHaveAttribute('aria-selected', 'true')
|
||||
await expect(canvas.getByRole('tabpanel', { name: 'Activity' })).toBeVisible()
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user