import { useState, MouseEvent, ReactElement } from 'react'; import { userEvent, within, expect, waitFor } from 'storybook/test'; import { Meta, StoryObj } from '@storybook/react-webpack5'; import { action } from 'storybook/actions'; import { Star } from '@transferwise/icons'; import { InfoPrompt, type InfoPromptProps } from './InfoPrompt'; import { allModes } from '../../../.storybook/modes'; import { withVariantConfig } from '../../../.storybook/helpers'; const withComponentGrid = ({ gap = '1rem' } = {}) => (Story: () => ReactElement) => (
); const meta = { title: 'Prompts/InfoPrompt/Tests', component: InfoPrompt, tags: ['!autodocs', '!manifest', 'new'], args: { sentiment: 'neutral', description: 'This is an informational message.', }, } satisfies Meta; export default meta; type Story = StoryObj; const wait = async (duration = 500) => new Promise((resolve) => { setTimeout(resolve, duration); }); /** * Test that clicking the dismiss button removes the prompt. */ export const DismissInteraction: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify prompt is visible', async () => { await waitFor(async () => expect(canvas.getByText('This message can be dismissed.')).toBeInTheDocument(), ); }); await step('Click the dismiss button', async () => { const dismissButton = canvas.getByRole('button', { name: /close/i, hidden: true }); await userEvent.click(dismissButton); }); await step('Verify prompt is removed', async () => { await waitFor(async () => expect(canvas.queryByText('This message can be dismissed.')).not.toBeInTheDocument(), ); }); }, render: function Render(args: InfoPromptProps) { const [isVisible, setIsVisible] = useState(true); if (!isVisible) { return
Prompt dismissed
; } return ( setIsVisible(false)} /> ); }, }; /** * Test keyboard accessibility - dismiss prompt via Enter key. */ export const DismissViaKeyboard: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Tab to the dismiss button', async () => { await wait(); await userEvent.tab(); }); await step('Press Enter to dismiss', async () => { await userEvent.keyboard('{Enter}'); }); await step('Verify prompt is removed', async () => { await waitFor(async () => expect(canvas.queryByText('Press Enter to dismiss.')).not.toBeInTheDocument(), ); }); }, render: function Render(args: InfoPromptProps) { const [isVisible, setIsVisible] = useState(true); if (!isVisible) { return
Prompt dismissed
; } return ( setIsVisible(false)} /> ); }, }; /** * Test action link click interaction. */ export const ActionClickInteraction: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify prompt with action is visible', async () => { await waitFor(async () => expect(canvas.getByText('Click the action link.')).toBeInTheDocument(), ); }); await step('Click the action link', async () => { const actionLink = await waitFor(() => canvas.getByRole('link', { name: 'Learn more' })); await userEvent.click(actionLink); }); await step('Verify action was triggered', async () => { await waitFor(async () => expect(canvas.getByText('Action clicked!')).toBeInTheDocument()); }); }, render: function Render(args: InfoPromptProps) { const [actionClicked, setActionClicked] = useState(false); return ( <> { e.preventDefault(); setActionClicked(true); }, }} /> {actionClicked &&
Action clicked!
} ); }, }; /** * Test multiple prompts with different sentiments can be dismissed independently. */ export const MultipleDismissInteraction: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify all prompts are visible', async () => { await waitFor(async () => { await expect(canvas.getByText('Success message')).toBeInTheDocument(); await expect(canvas.getByText('Warning message')).toBeInTheDocument(); await expect(canvas.getByText('Negative message')).toBeInTheDocument(); }); }); await step('Dismiss the warning prompt', async () => { // Click the second dismiss button (warning) const dismissButtons = canvas.getAllByRole('button', { name: /close/i, hidden: true }); await userEvent.click(dismissButtons[1]); }); await step('Verify only warning prompt is removed', async () => { await waitFor(async () => { await expect(canvas.getByText('Success message')).toBeInTheDocument(); await expect(canvas.queryByText('Warning message')).not.toBeInTheDocument(); await expect(canvas.getByText('Negative message')).toBeInTheDocument(); }); }); }, render: function Render() { const [prompts, setPrompts] = useState([ { id: 1, sentiment: 'success' as const, description: 'Success message' }, { id: 2, sentiment: 'warning' as const, description: 'Warning message' }, { id: 3, sentiment: 'negative' as const, description: 'Negative message' }, ]); const handleDismiss = (id: number) => { setPrompts((prev) => prev.filter((p) => p.id !== id)); }; return (
{prompts.map((prompt) => ( handleDismiss(prompt.id)} /> ))}
); }, }; /** * Test that touch interactions work for navigation (mobile). */ export const TouchInteraction: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify prompt with action is visible', async () => { await waitFor(async () => expect(canvas.getByText('Tap the prompt.')).toBeInTheDocument()); }); await step('Click the action (simulating touch)', async () => { const actionLink = await waitFor(() => canvas.getByRole('link', { name: 'Navigate' })); await userEvent.click(actionLink); }); await step('Verify navigation was triggered', async () => { await waitFor(async () => expect(canvas.getByText('Navigated!')).toBeInTheDocument()); }); }, render: function Render(args: InfoPromptProps) { const [navigated, setNavigated] = useState(false); return ( <> { e.preventDefault(); setNavigated(true); }, }} /> {navigated &&
Navigated!
} ); }, }; export const AllThemesAndSentiments: Story = { render: () => ( <> {(['success', 'warning', 'negative', 'neutral', 'proposition'] as const).map((sentiment) => ( } : undefined} action={{ label: 'Action', onClick: action('action') }} onDismiss={action('dismiss')} /> ))} ), decorators: [withComponentGrid({ gap: '1.5rem' })], parameters: { padding: '16px', variants: ['default', 'dark', 'bright-green', 'forest-green'], chromatic: { dark: allModes.dark, brightGreen: allModes.brightGreen, forestGreen: allModes.forestGreen, }, }, }; export const TinyScreen: Story = { render: () => ( ), ...withVariantConfig(['400%']), }; /** * Test that the default LiveRegion renders with role="status" and aria-live="polite". */ export const LiveRegionPoliteDefault: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify live region with role="status" exists', async () => { await waitFor(async () => { const liveRegion = canvas.getByRole('status'); await expect(liveRegion).toBeInTheDocument(); await waitFor(async () => expect(liveRegion).toHaveAttribute('aria-live', 'polite')); await expect(liveRegion).toHaveAttribute('aria-atomic', 'true'); }); }); await step('Verify prompt content is inside the live region', async () => { const liveRegion = canvas.getByRole('status'); await expect(within(liveRegion).getByText('Polite announcement')).toBeInTheDocument(); }); }, args: { description: 'Polite announcement', }, }; /** * Test that aria-live="assertive" renders with role="alert". */ export const LiveRegionAssertive: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify live region with role="alert" exists', async () => { await waitFor(async () => { const liveRegion = canvas.getByRole('alert'); await expect(liveRegion).toBeInTheDocument(); await waitFor(async () => expect(liveRegion).toHaveAttribute('aria-live', 'assertive')); await expect(liveRegion).toHaveAttribute('aria-atomic', 'true'); }); }); await step('Verify prompt content is inside the live region', async () => { const liveRegion = canvas.getByRole('alert'); await expect(within(liveRegion).getByText('Payment failed')).toBeInTheDocument(); }); }, args: { sentiment: 'negative', description: 'Payment failed', 'aria-live': 'assertive', }, }; /** * Test that a polite InfoPrompt added to the DOM gets a delayed live region. */ export const LiveRegionPoliteWhenAddedToDom: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify prompt is not in the DOM before trigger', async () => { await expect(canvas.queryByText('Polite prompt added later')).not.toBeInTheDocument(); await expect(canvas.queryByRole('status')).not.toBeInTheDocument(); }); await step('Add two prompts to the DOM', async () => { const addButton = canvas.getByRole('button', { name: 'Add polite prompt' }); await userEvent.click(addButton); await userEvent.click(addButton); }); await step('Verify delayed polite live regions appear with content', async () => { await expect(canvas.getByText('Polite prompt added later 1')).toBeInTheDocument(); await expect(canvas.getByText('Polite prompt added later 2')).toBeInTheDocument(); const initialLiveRegions = canvas.queryAllByRole('status'); await expect(initialLiveRegions).toHaveLength(2); await expect(initialLiveRegions[0].firstElementChild).toHaveAttribute('aria-hidden', 'true'); await expect(initialLiveRegions[1].firstElementChild).toHaveAttribute('aria-hidden', 'true'); await waitFor(async () => { const liveRegions = canvas.getAllByRole('status'); await expect(liveRegions).toHaveLength(2); await expect(liveRegions[0]).toHaveAttribute('aria-live', 'polite'); await expect(liveRegions[1]).toHaveAttribute('aria-live', 'polite'); await expect(liveRegions[0]).toHaveAttribute('aria-atomic', 'true'); await expect(liveRegions[1]).toHaveAttribute('aria-atomic', 'true'); await expect(liveRegions[0].firstElementChild).not.toHaveAttribute('aria-hidden'); await expect(liveRegions[1].firstElementChild).not.toHaveAttribute('aria-hidden'); await expect( within(liveRegions[0]).getByText('Polite prompt added later 1'), ).toBeInTheDocument(); await expect( within(liveRegions[1]).getByText('Polite prompt added later 2'), ).toBeInTheDocument(); }); }); }, render: function Render(args: InfoPromptProps) { const [promptIndexes, setPromptIndexes] = useState([]); const addPrompt = () => { setPromptIndexes((previousIndexes) => [...previousIndexes, previousIndexes.length + 1]); }; return ( <> {promptIndexes.map((promptIndex) => ( ))} ); }, }; /** * Test that an assertive InfoPrompt added to the DOM gets a delayed live region. */ export const LiveRegionAssertiveWhenAddedToDom: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify prompt is not in the DOM before trigger', async () => { await expect(canvas.queryByText('Assertive prompt added later')).not.toBeInTheDocument(); await expect(canvas.queryByRole('alert')).not.toBeInTheDocument(); }); await step('Add two prompts to the DOM', async () => { const addButton = canvas.getByRole('button', { name: 'Add assertive prompt' }); await userEvent.click(addButton); await userEvent.click(addButton); }); await step('Verify delayed assertive live regions appear with content', async () => { await expect(canvas.getByText('Assertive prompt added later 1')).toBeInTheDocument(); await expect(canvas.getByText('Assertive prompt added later 2')).toBeInTheDocument(); const initialLiveRegions = canvas.queryAllByRole('alert'); await expect(initialLiveRegions).toHaveLength(2); await expect(initialLiveRegions[0].firstElementChild).toHaveAttribute('aria-hidden', 'true'); await expect(initialLiveRegions[1].firstElementChild).toHaveAttribute('aria-hidden', 'true'); await waitFor(async () => { const liveRegions = canvas.getAllByRole('alert'); await expect(liveRegions).toHaveLength(2); await expect(liveRegions[0]).toHaveAttribute('aria-live', 'assertive'); await expect(liveRegions[1]).toHaveAttribute('aria-live', 'assertive'); await expect(liveRegions[0]).toHaveAttribute('aria-atomic', 'true'); await expect(liveRegions[1]).toHaveAttribute('aria-atomic', 'true'); await expect(liveRegions[0].firstElementChild).not.toHaveAttribute('aria-hidden'); await expect(liveRegions[1].firstElementChild).not.toHaveAttribute('aria-hidden'); await expect( within(liveRegions[0]).getByText('Assertive prompt added later 1'), ).toBeInTheDocument(); await expect( within(liveRegions[1]).getByText('Assertive prompt added later 2'), ).toBeInTheDocument(); }); }); }, render: function Render(args: InfoPromptProps) { const [promptIndexes, setPromptIndexes] = useState([]); const addPrompt = () => { setPromptIndexes((previousIndexes) => [...previousIndexes, previousIndexes.length + 1]); }; return ( <> {promptIndexes.map((promptIndex) => ( ))} ); }, }; /** * Test that aria-live="off" renders without a live region wrapper. */ export const LiveRegionOff: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify no live region wrapper exists', async () => { await waitFor(async () => { await expect(canvas.queryByRole('status')).not.toBeInTheDocument(); await expect(canvas.queryByRole('alert')).not.toBeInTheDocument(); }); }); await step('Verify prompt content still renders', async () => { await expect(canvas.getByText('Static info')).toBeInTheDocument(); }); }, args: { description: 'Static info', 'aria-live': 'off', }, }; /** * Test that a dismissed prompt also removes the live region. */ export const LiveRegionDismiss: Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Verify live region exists before dismiss', async () => { await waitFor(async () => { await expect(canvas.getByRole('status')).toBeInTheDocument(); }); }); await step('Dismiss the prompt', async () => { const dismissButton = canvas.getByRole('button', { name: /close/i, hidden: true }); await userEvent.click(dismissButton); }); await step('Verify live region is removed', async () => { await waitFor(async () => { await expect(canvas.queryByRole('status')).not.toBeInTheDocument(); }); }); }, render: function Render(args: InfoPromptProps) { const [isVisible, setIsVisible] = useState(true); if (!isVisible) { return
Prompt dismissed
; } return ( setIsVisible(false)} /> ); }, };