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)}
/>
);
},
};