@12-apps/ui 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -8
- package/src/button/index.ts +1 -0
- package/src/social-login-button/index.ts +1 -0
- package/src/{social-login-button.tsx → social-login-button/social-login-button.tsx} +5 -5
- package/src/user-avatar/index.ts +1 -0
- package/src/{user-avatar.tsx → user-avatar/user-avatar.tsx} +4 -4
- package/eslint.config.js +0 -89
- package/src/__tests__/data-grid.test.tsx +0 -125
- package/src/__tests__/package-wiring.test.ts +0 -41
- package/src/__tests__/setup.ts +0 -1
- package/src/__tests__/social-login-button.test.tsx +0 -186
- package/src/__tests__/user-avatar.test.tsx +0 -195
- package/src/__tests__/viewport.test.ts +0 -50
- package/src/components/command/__tests__/command.test.tsx +0 -112
- package/src/components/data-display/Alert/Alert.stories.tsx +0 -559
- package/src/components/data-display/Alert/Alert.test.stories.tsx +0 -707
- package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +0 -362
- package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +0 -763
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.stories.tsx +0 -230
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +0 -270
- package/src/components/data-display/Avatar/Avatar.stories.tsx +0 -721
- package/src/components/data-display/Avatar/Avatar.test.stories.tsx +0 -822
- package/src/components/data-display/Badge/Badge.stories.tsx +0 -1134
- package/src/components/data-display/Badge/Badge.test.stories.tsx +0 -1194
- package/src/components/data-display/Banner/Banner.stories.tsx +0 -528
- package/src/components/data-display/Banner/Banner.test.stories.tsx +0 -511
- package/src/components/data-display/Carousel/Carousel.stories.tsx +0 -607
- package/src/components/data-display/Carousel/Carousel.test.stories.tsx +0 -701
- package/src/components/data-display/Chart/Chart.stories.tsx +0 -568
- package/src/components/data-display/Chart/Chart.test.stories.tsx +0 -174
- package/src/components/data-display/Chart/SpecChart.stories.tsx +0 -150
- package/src/components/data-display/Chart/SpecChart.test.stories.tsx +0 -56
- package/src/components/data-display/Chart/__tests__/SpecChart.test.tsx +0 -60
- package/src/components/data-display/Chart/__tests__/chart-format.test.ts +0 -43
- package/src/components/data-display/Chart/__tests__/chart-spec.test.ts +0 -119
- package/src/components/data-display/Chip/Chip.stories.tsx +0 -252
- package/src/components/data-display/Chip/Chip.test.stories.tsx +0 -266
- package/src/components/data-display/DataGrid/DataGrid.stories.tsx +0 -515
- package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +0 -653
- package/src/components/data-display/DataViews/__tests__/DataViewsGrid.test.tsx +0 -271
- package/src/components/data-display/DataViews/__tests__/DataViewsTableBase.test.tsx +0 -134
- package/src/components/data-display/DataViews/__tests__/SaveViewModal.test.tsx +0 -83
- package/src/components/data-display/DataViews/__tests__/data-views-filter.test.ts +0 -115
- package/src/components/data-display/DataViews/__tests__/data-views-preview.test.ts +0 -53
- package/src/components/data-display/DataViews/__tests__/range-pill.test.tsx +0 -114
- package/src/components/data-display/DataViews/base-card.stories.tsx +0 -136
- package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +0 -254
- package/src/components/data-display/EmptyState/EmptyState.stories.tsx +0 -282
- package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +0 -441
- package/src/components/data-display/ErrorState/ErrorState.stories.tsx +0 -110
- package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +0 -133
- package/src/components/data-display/HoverCard/HoverCard.stories.tsx +0 -650
- package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +0 -929
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.stories.tsx +0 -411
- package/src/components/data-display/LazyImage/LazyImage.stories.tsx +0 -387
- package/src/components/data-display/Lightbox/Lightbox.stories.tsx +0 -315
- package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +0 -598
- package/src/components/data-display/LoadingState/LoadingState.stories.tsx +0 -124
- package/src/components/data-display/LoadingState/LoadingState.test.stories.tsx +0 -99
- package/src/components/data-display/MapPreview/MapPreview.stories.tsx +0 -458
- package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +0 -394
- package/src/components/data-display/Popover/Popover.stories.tsx +0 -295
- package/src/components/data-display/Popover/Popover.test.stories.tsx +0 -1162
- package/src/components/data-display/Progress/Progress.stories.tsx +0 -481
- package/src/components/data-display/Progress/Progress.test.stories.tsx +0 -2070
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +0 -160
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.test.stories.tsx +0 -190
- package/src/components/data-display/Sheet/Sheet.stories.tsx +0 -883
- package/src/components/data-display/Sheet/Sheet.test.stories.tsx +0 -1105
- package/src/components/data-display/StatCard/StatCard.stories.tsx +0 -167
- package/src/components/data-display/StatCard/StatCard.test.stories.tsx +0 -240
- package/src/components/data-display/Stepper/Stepper.stories.tsx +0 -540
- package/src/components/data-display/Stepper/Stepper.test.stories.tsx +0 -600
- package/src/components/data-display/Table/Table.stories.tsx +0 -803
- package/src/components/data-display/Table/Table.test.stories.tsx +0 -369
- package/src/components/data-display/Timeline/Timeline.stories.tsx +0 -442
- package/src/components/data-display/Timeline/Timeline.test.stories.tsx +0 -930
- package/src/components/data-display/TimingDiagram/TimingDiagram.stories.tsx +0 -475
- package/src/components/data-display/TimingDiagram/TimingDiagram.test.stories.tsx +0 -734
- package/src/components/data-display/Tooltip/Tooltip.stories.tsx +0 -468
- package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +0 -1154
- package/src/components/feedback/ConfirmAction/ConfirmAction.stories.tsx +0 -146
- package/src/components/feedback/ConfirmAction/ConfirmAction.test.stories.tsx +0 -114
- package/src/components/feedback/ConfirmAction/__tests__/confirm-action.test.tsx +0 -288
- package/src/components/feedback/Dialog/Dialog.stories.tsx +0 -833
- package/src/components/feedback/Dialog/Dialog.test.stories.tsx +0 -964
- package/src/components/feedback/Dialog/__tests__/dialog-content-spacing.test.tsx +0 -66
- package/src/components/feedback/Modal/Modal.stories.tsx +0 -765
- package/src/components/feedback/Modal/Modal.test.stories.tsx +0 -251
- package/src/components/feedback/Sonner/Sonner.stories.tsx +0 -698
- package/src/components/feedback/Sonner/Sonner.test.stories.tsx +0 -896
- package/src/components/feedback/StackedModal/StackedModal.stories.tsx +0 -1783
- package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +0 -973
- package/src/components/feedback/StackedModal/__tests__/stacked-modal-viewport.test.tsx +0 -104
- package/src/components/feedback/Toast/Toast.stories.tsx +0 -548
- package/src/components/feedback/Toast/Toast.test.stories.tsx +0 -834
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.stories.tsx +0 -691
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +0 -634
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +0 -665
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +0 -727
- package/src/components/form/Autocomplete/Autocomplete.stories.tsx +0 -715
- package/src/components/form/Autocomplete/Autocomplete.test.stories.tsx +0 -825
- package/src/components/form/Button/Button.stories.tsx +0 -365
- package/src/components/form/Button/Button.test.stories.tsx +0 -607
- package/src/components/form/Calendar/Calendar.stories.tsx +0 -349
- package/src/components/form/Calendar/Calendar.test.stories.tsx +0 -464
- package/src/components/form/CepField/CepField.stories.tsx +0 -138
- package/src/components/form/CepField/CepField.test.stories.tsx +0 -281
- package/src/components/form/Checkbox/Checkbox.stories.tsx +0 -265
- package/src/components/form/Checkbox/Checkbox.test.stories.tsx +0 -181
- package/src/components/form/CodeEditor/CodeEditor.stories.tsx +0 -871
- package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +0 -574
- package/src/components/form/Command/Command.stories.tsx +0 -580
- package/src/components/form/Command/Command.test.stories.tsx +0 -215
- package/src/components/form/CreatableSelect/CreatableSelect.stories.tsx +0 -112
- package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +0 -134
- package/src/components/form/Form/Form.stories.tsx +0 -687
- package/src/components/form/Form/Form.test.stories.tsx +0 -601
- package/src/components/form/Input/Input.stories.tsx +0 -260
- package/src/components/form/Input/Input.test.stories.tsx +0 -1191
- package/src/components/form/InputOTP/InputOTP.stories.tsx +0 -774
- package/src/components/form/InputOTP/InputOTP.test.stories.tsx +0 -588
- package/src/components/form/Label/Label.stories.tsx +0 -473
- package/src/components/form/Label/Label.test.stories.tsx +0 -915
- package/src/components/form/Menubar/Menubar.stories.tsx +0 -472
- package/src/components/form/Menubar/Menubar.test.stories.tsx +0 -584
- package/src/components/form/PasswordStrength/PasswordStrength.stories.tsx +0 -562
- package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +0 -327
- package/src/components/form/PhoneInput/PhoneInput.stories.tsx +0 -495
- package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +0 -666
- package/src/components/form/RadioGroup/RadioGroup.stories.tsx +0 -518
- package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +0 -685
- package/src/components/form/RichTextEditor/RichTextEditor.stories.tsx +0 -261
- package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +0 -491
- package/src/components/form/SearchPalette/SearchPalette.stories.tsx +0 -135
- package/src/components/form/Select/Select.stories.tsx +0 -364
- package/src/components/form/Select/Select.test.stories.tsx +0 -985
- package/src/components/form/Slider/Slider.stories.tsx +0 -418
- package/src/components/form/Slider/Slider.test.stories.tsx +0 -1042
- package/src/components/form/Switch/Switch.stories.tsx +0 -697
- package/src/components/form/Switch/Switch.test.stories.tsx +0 -597
- package/src/components/form/Textarea/Textarea.stories.tsx +0 -473
- package/src/components/form/Textarea/Textarea.test.stories.tsx +0 -702
- package/src/components/form/Toggle/Toggle.stories.tsx +0 -638
- package/src/components/form/Toggle/Toggle.test.stories.tsx +0 -1159
- package/src/components/form/ToggleGroup/ToggleGroup.stories.tsx +0 -617
- package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +0 -915
- package/src/components/form/UploadButton/UploadButton.stories.tsx +0 -367
- package/src/components/form/UploadButton/UploadButton.test.stories.tsx +0 -573
- package/src/components/form/total-form/total-form.test.tsx +0 -214
- package/src/components/layout/Accordion/Accordion.stories.tsx +0 -694
- package/src/components/layout/Accordion/Accordion.test.stories.tsx +0 -1344
- package/src/components/layout/Card/Card.stories.tsx +0 -591
- package/src/components/layout/Card/Card.test.stories.tsx +0 -1134
- package/src/components/layout/CardGrid/__tests__/CardGrid.test.tsx +0 -92
- package/src/components/layout/Collapsible/Collapsible.stories.tsx +0 -180
- package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +0 -308
- package/src/components/layout/Container/Container.stories.tsx +0 -557
- package/src/components/layout/Container/Container.test.stories.tsx +0 -745
- package/src/components/layout/ContentToolbar/ContentToolbar.stories.tsx +0 -129
- package/src/components/layout/ContentToolbar/ContentToolbar.test.stories.tsx +0 -251
- package/src/components/layout/ContentToolbar/__tests__/ColumnsMenu.test.tsx +0 -39
- package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +0 -131
- package/src/components/layout/ContentToolbar/__tests__/SavedViewsMenu.test.tsx +0 -99
- package/src/components/layout/Dashboard/Dashboard.stories.tsx +0 -104
- package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +0 -190
- package/src/components/layout/Drawer/Drawer.stories.tsx +0 -658
- package/src/components/layout/Drawer/Drawer.test.stories.tsx +0 -1343
- package/src/components/layout/Resizable/Resizable.stories.tsx +0 -1039
- package/src/components/layout/Resizable/Resizable.test.stories.tsx +0 -754
- package/src/components/layout/ScrollArea/ScrollArea.stories.tsx +0 -651
- package/src/components/layout/ScrollArea/ScrollArea.test.stories.tsx +0 -619
- package/src/components/layout/Separator/Separator.stories.tsx +0 -563
- package/src/components/layout/Separator/Separator.test.stories.tsx +0 -518
- package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +0 -192
- package/src/components/layout/SettingsLayout/SettingsLayout.test.stories.tsx +0 -175
- package/src/components/layout/Sidebar/Sidebar.stories.tsx +0 -609
- package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +0 -1324
- package/src/components/layout/Skeleton/Skeleton.stories.tsx +0 -689
- package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +0 -521
- package/src/components/layout/Spacer/Spacer.stories.tsx +0 -502
- package/src/components/layout/Spacer/Spacer.test.stories.tsx +0 -474
- package/src/components/layout/TableFilter/TableFilter.stories.tsx +0 -82
- package/src/components/layout/TableFilter/TableFilter.test.stories.tsx +0 -87
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +0 -638
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +0 -1008
- package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +0 -386
- package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +0 -435
- package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +0 -614
- package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +0 -845
- package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +0 -578
- package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +0 -660
- package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +0 -516
- package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +0 -935
- package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +0 -501
- package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +0 -453
- package/src/components/navigation/Pagination/Pagination.stories.tsx +0 -602
- package/src/components/navigation/Pagination/Pagination.test.stories.tsx +0 -520
- package/src/components/navigation/Tabs/Tabs.stories.tsx +0 -1004
- package/src/components/navigation/Tabs/Tabs.test.stories.tsx +0 -1225
- package/src/components/typography/Blockquote/Blockquote.stories.tsx +0 -359
- package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +0 -432
- package/src/components/typography/Code/Code.stories.tsx +0 -686
- package/src/components/typography/Code/Code.test.stories.tsx +0 -703
- package/src/components/typography/Heading/Heading.stories.tsx +0 -451
- package/src/components/typography/Heading/Heading.test.stories.tsx +0 -830
- package/src/components/typography/Paragraph/Paragraph.stories.tsx +0 -280
- package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +0 -231
- package/src/components/typography/Text/Text.stories.tsx +0 -359
- package/src/components/typography/Text/Text.test.stories.tsx +0 -759
- package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +0 -945
- package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +0 -603
- package/src/components/utility/AspectRatio/AspectRatio.stories.tsx +0 -414
- package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +0 -859
- package/src/components/utility/InfiniteScroll/InfiniteScroll.stories.tsx +0 -620
- package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +0 -361
- package/src/components/utility/InstallPrompt/InstallPrompt.stories.tsx +0 -118
- package/src/components/utility/InstallPrompt/InstallPrompt.test.stories.tsx +0 -83
- package/src/components/utility/InstallPrompt/__tests__/InstallPrompt.ios.test.tsx +0 -65
- package/src/components/utility/InstallPrompt/__tests__/usePwaInstall.test.tsx +0 -271
- package/src/components/utility/Portal/Portal.stories.tsx +0 -340
- package/src/components/utility/Portal/Portal.test.stories.tsx +0 -2086
- package/src/components/utility/Transition/Transition.stories.tsx +0 -493
- package/src/components/utility/Transition/Transition.test.stories.tsx +0 -854
- package/src/components/utility/VirtualList/VirtualList.stories.tsx +0 -1073
- package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +0 -1085
- package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +0 -431
- package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +0 -364
- /package/src/{button.tsx → button/button.tsx} +0 -0
|
@@ -1,2086 +0,0 @@
|
|
|
1
|
-
import { Alert, Box, Button, Paper, TextField,Typography } from '@mui/material';
|
|
2
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { expect, userEvent, waitFor,within } from 'storybook/test';
|
|
5
|
-
|
|
6
|
-
import { Portal } from './Portal';
|
|
7
|
-
|
|
8
|
-
const meta: Meta<typeof Portal> = {
|
|
9
|
-
title: 'Utility/Portal/Tests',
|
|
10
|
-
component: Portal,
|
|
11
|
-
parameters: {
|
|
12
|
-
layout: 'centered',
|
|
13
|
-
chromatic: { disableSnapshot: false },
|
|
14
|
-
},
|
|
15
|
-
tags: ['autodocs', 'test', 'component:Portal'],
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default meta;
|
|
19
|
-
export type Story = StoryObj<typeof meta>;
|
|
20
|
-
|
|
21
|
-
// ================================
|
|
22
|
-
// INTERACTION TESTS
|
|
23
|
-
// ================================
|
|
24
|
-
|
|
25
|
-
export const BasicInteraction: Story = {
|
|
26
|
-
render: () => {
|
|
27
|
-
const BasicInteractionComponent = () => {
|
|
28
|
-
const [showPortal, setShowPortal] = useState(false);
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<Box data-testid="portal-container">
|
|
32
|
-
<Button data-testid="toggle-portal" onClick={() => setShowPortal(!showPortal)}>
|
|
33
|
-
{showPortal ? 'Hide' : 'Show'} Portal
|
|
34
|
-
</Button>
|
|
35
|
-
|
|
36
|
-
{showPortal && (
|
|
37
|
-
<Portal>
|
|
38
|
-
<Box
|
|
39
|
-
data-testid="portal-content"
|
|
40
|
-
sx={{
|
|
41
|
-
position: 'fixed',
|
|
42
|
-
top: 20,
|
|
43
|
-
right: 20,
|
|
44
|
-
p: 2,
|
|
45
|
-
bgcolor: 'primary.main',
|
|
46
|
-
color: 'white',
|
|
47
|
-
borderRadius: 1,
|
|
48
|
-
zIndex: 9999,
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
Portal Content Rendered
|
|
52
|
-
</Box>
|
|
53
|
-
</Portal>
|
|
54
|
-
)}
|
|
55
|
-
</Box>
|
|
56
|
-
);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
return <BasicInteractionComponent />;
|
|
60
|
-
},
|
|
61
|
-
play: async ({ canvasElement }) => {
|
|
62
|
-
const canvas = within(canvasElement);
|
|
63
|
-
const toggleButton = canvas.getByTestId('toggle-portal');
|
|
64
|
-
|
|
65
|
-
// Initially, portal content should not exist
|
|
66
|
-
expect(canvas.queryByTestId('portal-content')).not.toBeInTheDocument();
|
|
67
|
-
|
|
68
|
-
// Click to show portal
|
|
69
|
-
await userEvent.click(toggleButton);
|
|
70
|
-
|
|
71
|
-
// Portal content should now be visible
|
|
72
|
-
await waitFor(() => {
|
|
73
|
-
expect(document.querySelector('[data-testid="portal-content"]')).toBeInTheDocument();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Verify portal content is rendered outside the component tree
|
|
77
|
-
const portalContent = document.querySelector('[data-testid="portal-content"]');
|
|
78
|
-
expect(portalContent?.parentElement).not.toBe(canvasElement);
|
|
79
|
-
|
|
80
|
-
// Click to hide portal
|
|
81
|
-
await userEvent.click(toggleButton);
|
|
82
|
-
|
|
83
|
-
// Portal content should be removed
|
|
84
|
-
await waitFor(() => {
|
|
85
|
-
expect(document.querySelector('[data-testid="portal-content"]')).not.toBeInTheDocument();
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const FormInteraction: Story = {
|
|
91
|
-
render: () => {
|
|
92
|
-
const SimpleInteractionComponent = () => {
|
|
93
|
-
const [showPortal, setShowPortal] = useState(false);
|
|
94
|
-
const [actionTaken, setActionTaken] = useState(false);
|
|
95
|
-
|
|
96
|
-
const handleAction = () => {
|
|
97
|
-
setActionTaken(true);
|
|
98
|
-
setShowPortal(false);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Box>
|
|
103
|
-
<Button data-testid="open-portal" onClick={() => setShowPortal(true)} variant="contained">
|
|
104
|
-
Open Portal
|
|
105
|
-
</Button>
|
|
106
|
-
|
|
107
|
-
{actionTaken && (
|
|
108
|
-
<Alert data-testid="action-success" severity="success" sx={{ mt: 2 }}>
|
|
109
|
-
Portal interaction completed successfully
|
|
110
|
-
</Alert>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{showPortal && (
|
|
114
|
-
<Portal>
|
|
115
|
-
<Box
|
|
116
|
-
data-testid="portal-content"
|
|
117
|
-
sx={{
|
|
118
|
-
position: 'fixed',
|
|
119
|
-
top: '50%',
|
|
120
|
-
left: '50%',
|
|
121
|
-
transform: 'translate(-50%, -50%)',
|
|
122
|
-
bgcolor: 'background.paper',
|
|
123
|
-
p: 3,
|
|
124
|
-
borderRadius: 2,
|
|
125
|
-
boxShadow: 24,
|
|
126
|
-
minWidth: 300,
|
|
127
|
-
zIndex: 9999,
|
|
128
|
-
}}
|
|
129
|
-
>
|
|
130
|
-
<Typography variant="h6" gutterBottom>
|
|
131
|
-
Portal Content
|
|
132
|
-
</Typography>
|
|
133
|
-
<Typography variant="body1" sx={{ mb: 2 }}>
|
|
134
|
-
This content is rendered in a portal
|
|
135
|
-
</Typography>
|
|
136
|
-
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
137
|
-
<Button data-testid="cancel-portal" onClick={() => setShowPortal(false)}>
|
|
138
|
-
Cancel
|
|
139
|
-
</Button>
|
|
140
|
-
<Button data-testid="confirm-portal" onClick={handleAction} variant="contained">
|
|
141
|
-
Confirm
|
|
142
|
-
</Button>
|
|
143
|
-
</Box>
|
|
144
|
-
</Box>
|
|
145
|
-
</Portal>
|
|
146
|
-
)}
|
|
147
|
-
</Box>
|
|
148
|
-
);
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
return <SimpleInteractionComponent />;
|
|
152
|
-
},
|
|
153
|
-
play: async ({ canvasElement }) => {
|
|
154
|
-
const canvas = within(canvasElement);
|
|
155
|
-
|
|
156
|
-
// Open the portal
|
|
157
|
-
const openButton = canvas.getByTestId('open-portal');
|
|
158
|
-
await userEvent.click(openButton);
|
|
159
|
-
|
|
160
|
-
// Wait for portal content to appear
|
|
161
|
-
await waitFor(() => {
|
|
162
|
-
expect(document.querySelector('[data-testid="portal-content"]')).toBeInTheDocument();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Interact with the portal content
|
|
166
|
-
const confirmButton = document.querySelector(
|
|
167
|
-
'[data-testid="confirm-portal"]',
|
|
168
|
-
) as HTMLButtonElement;
|
|
169
|
-
expect(confirmButton).toBeInTheDocument();
|
|
170
|
-
|
|
171
|
-
await userEvent.click(confirmButton);
|
|
172
|
-
|
|
173
|
-
// Wait for portal to close and success message to appear
|
|
174
|
-
await waitFor(
|
|
175
|
-
() => {
|
|
176
|
-
// Check that the portal is closed
|
|
177
|
-
expect(document.querySelector('[data-testid="portal-content"]')).not.toBeInTheDocument();
|
|
178
|
-
// Check that success message appears
|
|
179
|
-
const successElement = canvas.getByTestId('action-success');
|
|
180
|
-
expect(successElement).toBeInTheDocument();
|
|
181
|
-
expect(successElement).toHaveTextContent('Portal interaction completed successfully');
|
|
182
|
-
},
|
|
183
|
-
{ timeout: 3000 },
|
|
184
|
-
);
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export const StateChange: Story = {
|
|
189
|
-
render: () => {
|
|
190
|
-
const StateChangeComponent = () => {
|
|
191
|
-
const [portalMode, setPortalMode] = useState<'disabled' | 'body' | 'custom'>('body');
|
|
192
|
-
const [customContainer, setCustomContainer] = useState<HTMLElement | null>(null);
|
|
193
|
-
const [showContent, setShowContent] = useState(true);
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
|
|
197
|
-
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
198
|
-
<Button
|
|
199
|
-
data-testid="mode-disabled"
|
|
200
|
-
variant={portalMode === 'disabled' ? 'contained' : 'outlined'}
|
|
201
|
-
onClick={() => setPortalMode('disabled')}
|
|
202
|
-
size="small"
|
|
203
|
-
>
|
|
204
|
-
Disabled
|
|
205
|
-
</Button>
|
|
206
|
-
<Button
|
|
207
|
-
data-testid="mode-body"
|
|
208
|
-
variant={portalMode === 'body' ? 'contained' : 'outlined'}
|
|
209
|
-
onClick={() => setPortalMode('body')}
|
|
210
|
-
size="small"
|
|
211
|
-
>
|
|
212
|
-
Body Portal
|
|
213
|
-
</Button>
|
|
214
|
-
<Button
|
|
215
|
-
data-testid="mode-custom"
|
|
216
|
-
variant={portalMode === 'custom' ? 'contained' : 'outlined'}
|
|
217
|
-
onClick={() => setPortalMode('custom')}
|
|
218
|
-
size="small"
|
|
219
|
-
>
|
|
220
|
-
Custom Container
|
|
221
|
-
</Button>
|
|
222
|
-
<Button
|
|
223
|
-
data-testid="toggle-content"
|
|
224
|
-
onClick={() => setShowContent(!showContent)}
|
|
225
|
-
color="secondary"
|
|
226
|
-
size="small"
|
|
227
|
-
>
|
|
228
|
-
{showContent ? 'Hide' : 'Show'} Content
|
|
229
|
-
</Button>
|
|
230
|
-
</Box>
|
|
231
|
-
|
|
232
|
-
<Box sx={{ display: 'flex', gap: 2 }}>
|
|
233
|
-
<Paper sx={{ p: 2, flex: 1 }}>
|
|
234
|
-
<Typography variant="h6">Source Component</Typography>
|
|
235
|
-
<Typography>Portal mode: {portalMode}</Typography>
|
|
236
|
-
</Paper>
|
|
237
|
-
|
|
238
|
-
<Paper
|
|
239
|
-
ref={(el) => setCustomContainer(el)}
|
|
240
|
-
data-testid="custom-container"
|
|
241
|
-
sx={{ p: 2, flex: 1, bgcolor: 'grey.100', position: 'relative' }}
|
|
242
|
-
>
|
|
243
|
-
<Typography variant="h6">Custom Container</Typography>
|
|
244
|
-
<Typography variant="body2" color="text.secondary">
|
|
245
|
-
Target for custom portal
|
|
246
|
-
</Typography>
|
|
247
|
-
</Paper>
|
|
248
|
-
</Box>
|
|
249
|
-
|
|
250
|
-
{showContent && (
|
|
251
|
-
<Portal
|
|
252
|
-
disablePortal={portalMode === 'disabled'}
|
|
253
|
-
container={portalMode === 'custom' ? customContainer : undefined}
|
|
254
|
-
>
|
|
255
|
-
<Box
|
|
256
|
-
data-testid="portal-state-content"
|
|
257
|
-
sx={{
|
|
258
|
-
position: portalMode === 'disabled' ? 'static' : 'absolute',
|
|
259
|
-
top: portalMode === 'disabled' ? 'auto' : 10,
|
|
260
|
-
right: portalMode === 'disabled' ? 'auto' : 10,
|
|
261
|
-
bgcolor: 'primary.main',
|
|
262
|
-
color: 'white',
|
|
263
|
-
p: 1,
|
|
264
|
-
borderRadius: 1,
|
|
265
|
-
fontSize: '0.875rem',
|
|
266
|
-
zIndex: portalMode === 'disabled' ? 'auto' : 1000,
|
|
267
|
-
}}
|
|
268
|
-
>
|
|
269
|
-
Mode: {portalMode}
|
|
270
|
-
</Box>
|
|
271
|
-
</Portal>
|
|
272
|
-
)}
|
|
273
|
-
</Box>
|
|
274
|
-
);
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
return <StateChangeComponent />;
|
|
278
|
-
},
|
|
279
|
-
play: async ({ canvasElement }) => {
|
|
280
|
-
const canvas = within(canvasElement);
|
|
281
|
-
|
|
282
|
-
// Test body portal mode (default)
|
|
283
|
-
let content = document.querySelector('[data-testid="portal-state-content"]');
|
|
284
|
-
expect(content).toBeInTheDocument();
|
|
285
|
-
expect(content?.textContent).toBe('Mode: body');
|
|
286
|
-
|
|
287
|
-
// Switch to disabled mode
|
|
288
|
-
await userEvent.click(canvas.getByTestId('mode-disabled'));
|
|
289
|
-
await waitFor(() => {
|
|
290
|
-
content = canvas.queryByTestId('portal-state-content');
|
|
291
|
-
expect(content).toBeInTheDocument();
|
|
292
|
-
expect(content?.textContent).toBe('Mode: disabled');
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Switch to custom container mode
|
|
296
|
-
await userEvent.click(canvas.getByTestId('mode-custom'));
|
|
297
|
-
await waitFor(() => {
|
|
298
|
-
content = document.querySelector('[data-testid="portal-state-content"]');
|
|
299
|
-
expect(content?.textContent).toBe('Mode: custom');
|
|
300
|
-
|
|
301
|
-
// Verify it's in the custom container
|
|
302
|
-
const customContainer = canvas.getByTestId('custom-container');
|
|
303
|
-
expect(customContainer).toContainElement(content);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// Test content toggle
|
|
307
|
-
await userEvent.click(canvas.getByTestId('toggle-content'));
|
|
308
|
-
await waitFor(() => {
|
|
309
|
-
expect(
|
|
310
|
-
document.querySelector('[data-testid="portal-state-content"]'),
|
|
311
|
-
).not.toBeInTheDocument();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Show content again
|
|
315
|
-
await userEvent.click(canvas.getByTestId('toggle-content'));
|
|
316
|
-
await waitFor(() => {
|
|
317
|
-
expect(document.querySelector('[data-testid="portal-state-content"]')).toBeInTheDocument();
|
|
318
|
-
});
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// ================================
|
|
323
|
-
// ACCESSIBILITY TESTS
|
|
324
|
-
// ================================
|
|
325
|
-
|
|
326
|
-
export const KeyboardNavigation: Story = {
|
|
327
|
-
render: () => {
|
|
328
|
-
const KeyboardNavigationComponent = () => {
|
|
329
|
-
const [showPortal, setShowPortal] = useState(false);
|
|
330
|
-
|
|
331
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
332
|
-
if (e.key === 'Escape') {
|
|
333
|
-
setShowPortal(false);
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<Box>
|
|
339
|
-
<Button
|
|
340
|
-
data-testid="keyboard-trigger"
|
|
341
|
-
onClick={() => setShowPortal(true)}
|
|
342
|
-
onKeyDown={(e) => e.key === 'Enter' && setShowPortal(true)}
|
|
343
|
-
>
|
|
344
|
-
Open Portal (Enter or Click)
|
|
345
|
-
</Button>
|
|
346
|
-
|
|
347
|
-
{showPortal && (
|
|
348
|
-
<Portal>
|
|
349
|
-
<Box
|
|
350
|
-
data-testid="keyboard-portal"
|
|
351
|
-
onKeyDown={handleKeyDown}
|
|
352
|
-
tabIndex={-1}
|
|
353
|
-
sx={{
|
|
354
|
-
position: 'fixed',
|
|
355
|
-
top: '50%',
|
|
356
|
-
left: '50%',
|
|
357
|
-
transform: 'translate(-50%, -50%)',
|
|
358
|
-
bgcolor: 'background.paper',
|
|
359
|
-
p: 3,
|
|
360
|
-
borderRadius: 2,
|
|
361
|
-
boxShadow: 24,
|
|
362
|
-
minWidth: 300,
|
|
363
|
-
zIndex: 9999,
|
|
364
|
-
outline: 'none',
|
|
365
|
-
}}
|
|
366
|
-
>
|
|
367
|
-
<Typography variant="h6" gutterBottom>
|
|
368
|
-
Keyboard Accessible Portal
|
|
369
|
-
</Typography>
|
|
370
|
-
|
|
371
|
-
<Box sx={{ mb: 2 }}>
|
|
372
|
-
<Button data-testid="first-button" variant="outlined" sx={{ mr: 1 }}>
|
|
373
|
-
First Button
|
|
374
|
-
</Button>
|
|
375
|
-
<Button data-testid="second-button" variant="outlined">
|
|
376
|
-
Second Button
|
|
377
|
-
</Button>
|
|
378
|
-
</Box>
|
|
379
|
-
|
|
380
|
-
<Typography variant="body2" color="text.secondary">
|
|
381
|
-
Press Escape to close, Tab to navigate
|
|
382
|
-
</Typography>
|
|
383
|
-
|
|
384
|
-
<Button
|
|
385
|
-
data-testid="close-button"
|
|
386
|
-
onClick={() => setShowPortal(false)}
|
|
387
|
-
sx={{ mt: 2 }}
|
|
388
|
-
variant="contained"
|
|
389
|
-
size="small"
|
|
390
|
-
>
|
|
391
|
-
Close
|
|
392
|
-
</Button>
|
|
393
|
-
</Box>
|
|
394
|
-
</Portal>
|
|
395
|
-
)}
|
|
396
|
-
</Box>
|
|
397
|
-
);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
return <KeyboardNavigationComponent />;
|
|
401
|
-
},
|
|
402
|
-
play: async ({ canvasElement }) => {
|
|
403
|
-
const canvas = within(canvasElement);
|
|
404
|
-
const triggerButton = canvas.getByTestId('keyboard-trigger');
|
|
405
|
-
|
|
406
|
-
// Focus and press Enter to open portal
|
|
407
|
-
triggerButton.focus();
|
|
408
|
-
await userEvent.keyboard('{Enter}');
|
|
409
|
-
|
|
410
|
-
// Wait for portal to appear
|
|
411
|
-
await waitFor(() => {
|
|
412
|
-
expect(document.querySelector('[data-testid="keyboard-portal"]')).toBeInTheDocument();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Test Tab navigation within portal
|
|
416
|
-
await userEvent.keyboard('{Tab}');
|
|
417
|
-
const firstButton = document.querySelector('[data-testid="first-button"]') as HTMLButtonElement;
|
|
418
|
-
expect(firstButton).toHaveFocus();
|
|
419
|
-
|
|
420
|
-
await userEvent.keyboard('{Tab}');
|
|
421
|
-
const secondButton = document.querySelector('[data-testid="second-button"]') as HTMLButtonElement;
|
|
422
|
-
expect(secondButton).toHaveFocus();
|
|
423
|
-
|
|
424
|
-
await userEvent.keyboard('{Tab}');
|
|
425
|
-
const closeButton = document.querySelector('[data-testid="close-button"]') as HTMLButtonElement;
|
|
426
|
-
expect(closeButton).toHaveFocus();
|
|
427
|
-
|
|
428
|
-
// Test Escape key to close
|
|
429
|
-
await userEvent.keyboard('{Escape}');
|
|
430
|
-
|
|
431
|
-
await waitFor(() => {
|
|
432
|
-
expect(document.querySelector('[data-testid="keyboard-portal"]')).not.toBeInTheDocument();
|
|
433
|
-
});
|
|
434
|
-
},
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
export const ScreenReader: Story = {
|
|
438
|
-
render: () => {
|
|
439
|
-
const ScreenReaderComponent = () => {
|
|
440
|
-
const [announcements, setAnnouncements] = useState<string[]>([]);
|
|
441
|
-
const [showPortal, setShowPortal] = useState(false);
|
|
442
|
-
|
|
443
|
-
const announce = (message: string) => {
|
|
444
|
-
setAnnouncements((prev) => [...prev, message]);
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
return (
|
|
448
|
-
<Box>
|
|
449
|
-
<Box sx={{ mb: 2 }}>
|
|
450
|
-
<Typography variant="h6" id="portal-demo-title">
|
|
451
|
-
Screen Reader Portal Demo
|
|
452
|
-
</Typography>
|
|
453
|
-
<Typography variant="body2" color="text.secondary">
|
|
454
|
-
This demo shows proper ARIA attributes and announcements for portals
|
|
455
|
-
</Typography>
|
|
456
|
-
</Box>
|
|
457
|
-
|
|
458
|
-
<Button
|
|
459
|
-
data-testid="sr-trigger"
|
|
460
|
-
onClick={() => {
|
|
461
|
-
setShowPortal(true);
|
|
462
|
-
announce('Portal dialog opened');
|
|
463
|
-
}}
|
|
464
|
-
aria-describedby="portal-demo-title"
|
|
465
|
-
>
|
|
466
|
-
Open Accessible Portal
|
|
467
|
-
</Button>
|
|
468
|
-
|
|
469
|
-
{/* Screen reader announcements */}
|
|
470
|
-
<Box
|
|
471
|
-
data-testid="announcements"
|
|
472
|
-
role="log"
|
|
473
|
-
aria-live="polite"
|
|
474
|
-
aria-label="Screen reader announcements"
|
|
475
|
-
sx={{ position: 'absolute', left: '-10000px' }}
|
|
476
|
-
>
|
|
477
|
-
{announcements.map((msg, index) => (
|
|
478
|
-
<div key={index}>{msg}</div>
|
|
479
|
-
))}
|
|
480
|
-
</Box>
|
|
481
|
-
|
|
482
|
-
{showPortal && (
|
|
483
|
-
<Portal>
|
|
484
|
-
<Box
|
|
485
|
-
data-testid="sr-portal"
|
|
486
|
-
role="dialog"
|
|
487
|
-
aria-modal="true"
|
|
488
|
-
aria-labelledby="portal-title"
|
|
489
|
-
aria-describedby="portal-description"
|
|
490
|
-
sx={{
|
|
491
|
-
position: 'fixed',
|
|
492
|
-
top: '50%',
|
|
493
|
-
left: '50%',
|
|
494
|
-
transform: 'translate(-50%, -50%)',
|
|
495
|
-
bgcolor: 'background.paper',
|
|
496
|
-
p: 3,
|
|
497
|
-
borderRadius: 2,
|
|
498
|
-
boxShadow: 24,
|
|
499
|
-
minWidth: 400,
|
|
500
|
-
zIndex: 9999,
|
|
501
|
-
}}
|
|
502
|
-
>
|
|
503
|
-
<Typography id="portal-title" variant="h6" component="h2" gutterBottom>
|
|
504
|
-
Accessible Portal Dialog
|
|
505
|
-
</Typography>
|
|
506
|
-
|
|
507
|
-
<Typography id="portal-description" variant="body1" sx={{ mb: 2 }}>
|
|
508
|
-
This portal has proper ARIA attributes including role, aria-modal,
|
|
509
|
-
aria-labelledby, and aria-describedby for screen reader accessibility.
|
|
510
|
-
</Typography>
|
|
511
|
-
|
|
512
|
-
<Box sx={{ mb: 2 }}>
|
|
513
|
-
<TextField
|
|
514
|
-
data-testid="portal-input"
|
|
515
|
-
label="Accessible Input"
|
|
516
|
-
fullWidth
|
|
517
|
-
size="small"
|
|
518
|
-
InputProps={{
|
|
519
|
-
'aria-describedby': 'input-help',
|
|
520
|
-
}}
|
|
521
|
-
/>
|
|
522
|
-
<Typography
|
|
523
|
-
id="input-help"
|
|
524
|
-
variant="caption"
|
|
525
|
-
color="text.secondary"
|
|
526
|
-
sx={{ display: 'block', mt: 0.5 }}
|
|
527
|
-
>
|
|
528
|
-
This input is properly labeled and described
|
|
529
|
-
</Typography>
|
|
530
|
-
</Box>
|
|
531
|
-
|
|
532
|
-
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
533
|
-
<Button
|
|
534
|
-
data-testid="sr-cancel"
|
|
535
|
-
onClick={() => {
|
|
536
|
-
setShowPortal(false);
|
|
537
|
-
announce('Portal dialog cancelled and closed');
|
|
538
|
-
}}
|
|
539
|
-
>
|
|
540
|
-
Cancel
|
|
541
|
-
</Button>
|
|
542
|
-
<Button
|
|
543
|
-
data-testid="sr-confirm"
|
|
544
|
-
variant="contained"
|
|
545
|
-
onClick={() => {
|
|
546
|
-
setShowPortal(false);
|
|
547
|
-
announce('Portal dialog confirmed and closed');
|
|
548
|
-
}}
|
|
549
|
-
>
|
|
550
|
-
OK
|
|
551
|
-
</Button>
|
|
552
|
-
</Box>
|
|
553
|
-
</Box>
|
|
554
|
-
</Portal>
|
|
555
|
-
)}
|
|
556
|
-
</Box>
|
|
557
|
-
);
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
return <ScreenReaderComponent />;
|
|
561
|
-
},
|
|
562
|
-
play: async ({ canvasElement }) => {
|
|
563
|
-
const canvas = within(canvasElement);
|
|
564
|
-
|
|
565
|
-
// Open the portal
|
|
566
|
-
const triggerButton = canvas.getByTestId('sr-trigger');
|
|
567
|
-
await userEvent.click(triggerButton);
|
|
568
|
-
|
|
569
|
-
// Wait for portal to appear
|
|
570
|
-
await waitFor(() => {
|
|
571
|
-
expect(document.querySelector('[data-testid="sr-portal"]')).toBeInTheDocument();
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// Verify ARIA attributes
|
|
575
|
-
const portalDialog = document.querySelector('[data-testid="sr-portal"]');
|
|
576
|
-
expect(portalDialog).toHaveAttribute('role', 'dialog');
|
|
577
|
-
expect(portalDialog).toHaveAttribute('aria-modal', 'true');
|
|
578
|
-
expect(portalDialog).toHaveAttribute('aria-labelledby', 'portal-title');
|
|
579
|
-
expect(portalDialog).toHaveAttribute('aria-describedby', 'portal-description');
|
|
580
|
-
|
|
581
|
-
// Verify title and description elements exist
|
|
582
|
-
expect(document.getElementById('portal-title')).toBeInTheDocument();
|
|
583
|
-
expect(document.getElementById('portal-description')).toBeInTheDocument();
|
|
584
|
-
|
|
585
|
-
// Test input accessibility
|
|
586
|
-
const input = document.querySelector('[data-testid="portal-input"] input');
|
|
587
|
-
expect(input).toHaveAttribute('aria-describedby', 'input-help');
|
|
588
|
-
|
|
589
|
-
// Verify announcements area
|
|
590
|
-
const announcements = canvas.getByTestId('announcements');
|
|
591
|
-
expect(announcements).toHaveAttribute('role', 'log');
|
|
592
|
-
expect(announcements).toHaveAttribute('aria-live', 'polite');
|
|
593
|
-
|
|
594
|
-
// Test closing with proper announcement
|
|
595
|
-
const confirmButton = document.querySelector('[data-testid="sr-confirm"]') as HTMLButtonElement;
|
|
596
|
-
await userEvent.click(confirmButton);
|
|
597
|
-
|
|
598
|
-
await waitFor(() => {
|
|
599
|
-
expect(document.querySelector('[data-testid="sr-portal"]')).not.toBeInTheDocument();
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// Verify announcement was made
|
|
603
|
-
expect(announcements).toHaveTextContent('Portal dialog confirmed and closed');
|
|
604
|
-
},
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
export const FocusManagement: Story = {
|
|
608
|
-
render: () => {
|
|
609
|
-
const FocusManagementComponent = () => {
|
|
610
|
-
const [showPortal, setShowPortal] = useState(false);
|
|
611
|
-
const [previousFocus, setPreviousFocus] = useState<HTMLElement | null>(null);
|
|
612
|
-
|
|
613
|
-
const openPortal = () => {
|
|
614
|
-
setPreviousFocus(document.activeElement as HTMLElement);
|
|
615
|
-
setShowPortal(true);
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
const closePortal = () => {
|
|
619
|
-
setShowPortal(false);
|
|
620
|
-
// Restore focus after a brief delay to allow DOM updates
|
|
621
|
-
window.setTimeout(() => {
|
|
622
|
-
previousFocus?.focus();
|
|
623
|
-
}, 10);
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
return (
|
|
627
|
-
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
628
|
-
<TextField data-testid="before-portal" label="Input Before Portal" size="small" />
|
|
629
|
-
|
|
630
|
-
<Button data-testid="focus-trigger" onClick={openPortal} variant="contained">
|
|
631
|
-
Open Focus Managed Portal
|
|
632
|
-
</Button>
|
|
633
|
-
|
|
634
|
-
<TextField data-testid="after-portal" label="Input After Portal" size="small" />
|
|
635
|
-
|
|
636
|
-
{showPortal && (
|
|
637
|
-
<Portal>
|
|
638
|
-
<Box
|
|
639
|
-
data-testid="focus-portal"
|
|
640
|
-
sx={{
|
|
641
|
-
position: 'fixed',
|
|
642
|
-
top: '50%',
|
|
643
|
-
left: '50%',
|
|
644
|
-
transform: 'translate(-50%, -50%)',
|
|
645
|
-
bgcolor: 'background.paper',
|
|
646
|
-
p: 3,
|
|
647
|
-
borderRadius: 2,
|
|
648
|
-
boxShadow: 24,
|
|
649
|
-
minWidth: 400,
|
|
650
|
-
zIndex: 9999,
|
|
651
|
-
}}
|
|
652
|
-
>
|
|
653
|
-
<Typography variant="h6" gutterBottom>
|
|
654
|
-
Focus Management Portal
|
|
655
|
-
</Typography>
|
|
656
|
-
|
|
657
|
-
<Box sx={{ mb: 2 }}>
|
|
658
|
-
<TextField
|
|
659
|
-
data-testid="first-portal-input"
|
|
660
|
-
label="First Input"
|
|
661
|
-
fullWidth
|
|
662
|
-
size="small"
|
|
663
|
-
sx={{ mb: 1 }}
|
|
664
|
-
autoFocus
|
|
665
|
-
/>
|
|
666
|
-
<TextField
|
|
667
|
-
data-testid="second-portal-input"
|
|
668
|
-
label="Second Input"
|
|
669
|
-
fullWidth
|
|
670
|
-
size="small"
|
|
671
|
-
/>
|
|
672
|
-
</Box>
|
|
673
|
-
|
|
674
|
-
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
675
|
-
<Button data-testid="focus-cancel" onClick={closePortal}>
|
|
676
|
-
Cancel
|
|
677
|
-
</Button>
|
|
678
|
-
<Button data-testid="focus-save" variant="contained" onClick={closePortal}>
|
|
679
|
-
Save
|
|
680
|
-
</Button>
|
|
681
|
-
</Box>
|
|
682
|
-
</Box>
|
|
683
|
-
</Portal>
|
|
684
|
-
)}
|
|
685
|
-
</Box>
|
|
686
|
-
);
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
return <FocusManagementComponent />;
|
|
690
|
-
},
|
|
691
|
-
play: async ({ canvasElement }) => {
|
|
692
|
-
const canvas = within(canvasElement);
|
|
693
|
-
|
|
694
|
-
// Focus on the trigger button
|
|
695
|
-
const triggerButton = canvas.getByTestId('focus-trigger');
|
|
696
|
-
triggerButton.focus();
|
|
697
|
-
expect(triggerButton).toHaveFocus();
|
|
698
|
-
|
|
699
|
-
// Open portal
|
|
700
|
-
await userEvent.click(triggerButton);
|
|
701
|
-
|
|
702
|
-
// Wait for portal to appear
|
|
703
|
-
await waitFor(() => {
|
|
704
|
-
expect(document.querySelector('[data-testid="focus-portal"]')).toBeInTheDocument();
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
// Verify focus moved to first input in portal
|
|
708
|
-
await waitFor(() => {
|
|
709
|
-
const firstInput = document.querySelector(
|
|
710
|
-
'[data-testid="first-portal-input"] input',
|
|
711
|
-
) as HTMLInputElement;
|
|
712
|
-
expect(firstInput).toHaveFocus();
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
// Test tab navigation within portal
|
|
716
|
-
await userEvent.keyboard('{Tab}');
|
|
717
|
-
const secondInput = document.querySelector(
|
|
718
|
-
'[data-testid="second-portal-input"] input',
|
|
719
|
-
) as HTMLInputElement;
|
|
720
|
-
expect(secondInput).toHaveFocus();
|
|
721
|
-
|
|
722
|
-
await userEvent.keyboard('{Tab}');
|
|
723
|
-
const cancelButton = document.querySelector(
|
|
724
|
-
'[data-testid="focus-cancel"]',
|
|
725
|
-
) as HTMLButtonElement;
|
|
726
|
-
expect(cancelButton).toHaveFocus();
|
|
727
|
-
|
|
728
|
-
await userEvent.keyboard('{Tab}');
|
|
729
|
-
const saveButton = document.querySelector('[data-testid="focus-save"]') as HTMLButtonElement;
|
|
730
|
-
expect(saveButton).toHaveFocus();
|
|
731
|
-
|
|
732
|
-
// Close portal and verify focus returns to trigger
|
|
733
|
-
await userEvent.click(saveButton);
|
|
734
|
-
|
|
735
|
-
await waitFor(() => {
|
|
736
|
-
expect(document.querySelector('[data-testid="focus-portal"]')).not.toBeInTheDocument();
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// Verify focus returned to trigger button
|
|
740
|
-
await waitFor(
|
|
741
|
-
() => {
|
|
742
|
-
expect(triggerButton).toHaveFocus();
|
|
743
|
-
},
|
|
744
|
-
{ timeout: 200 },
|
|
745
|
-
);
|
|
746
|
-
},
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
// ================================
|
|
750
|
-
// VISUAL TESTS
|
|
751
|
-
// ================================
|
|
752
|
-
|
|
753
|
-
export const ResponsiveDesign: Story = {
|
|
754
|
-
render: () => {
|
|
755
|
-
const ResponsiveComponent = () => {
|
|
756
|
-
const [showPortal, setShowPortal] = useState(true);
|
|
757
|
-
const [viewport, setViewport] = useState('desktop');
|
|
758
|
-
|
|
759
|
-
return (
|
|
760
|
-
<Box>
|
|
761
|
-
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
762
|
-
{['mobile', 'tablet', 'desktop'].map((size) => (
|
|
763
|
-
<Button
|
|
764
|
-
key={size}
|
|
765
|
-
data-testid={`viewport-${size}`}
|
|
766
|
-
variant={viewport === size ? 'contained' : 'outlined'}
|
|
767
|
-
onClick={() => setViewport(size)}
|
|
768
|
-
size="small"
|
|
769
|
-
>
|
|
770
|
-
{size}
|
|
771
|
-
</Button>
|
|
772
|
-
))}
|
|
773
|
-
<Button
|
|
774
|
-
data-testid="toggle-responsive-portal"
|
|
775
|
-
onClick={() => setShowPortal(!showPortal)}
|
|
776
|
-
color="secondary"
|
|
777
|
-
size="small"
|
|
778
|
-
>
|
|
779
|
-
{showPortal ? 'Hide' : 'Show'} Portal
|
|
780
|
-
</Button>
|
|
781
|
-
</Box>
|
|
782
|
-
|
|
783
|
-
{showPortal && (
|
|
784
|
-
<Portal>
|
|
785
|
-
<Box
|
|
786
|
-
data-testid="responsive-portal"
|
|
787
|
-
sx={{
|
|
788
|
-
position: 'fixed',
|
|
789
|
-
top: '50%',
|
|
790
|
-
left: '50%',
|
|
791
|
-
transform: 'translate(-50%, -50%)',
|
|
792
|
-
bgcolor: 'background.paper',
|
|
793
|
-
p: viewport === 'mobile' ? 1 : viewport === 'tablet' ? 2 : 3,
|
|
794
|
-
borderRadius: 1,
|
|
795
|
-
boxShadow: 24,
|
|
796
|
-
width: viewport === 'mobile' ? '90vw' : viewport === 'tablet' ? '60vw' : '40vw',
|
|
797
|
-
maxWidth: viewport === 'mobile' ? 320 : viewport === 'tablet' ? 600 : 800,
|
|
798
|
-
maxHeight: '90vh',
|
|
799
|
-
overflow: 'auto',
|
|
800
|
-
zIndex: 9999,
|
|
801
|
-
}}
|
|
802
|
-
>
|
|
803
|
-
<Typography variant={viewport === 'mobile' ? 'subtitle1' : 'h6'} gutterBottom>
|
|
804
|
-
Responsive Portal Content ({viewport})
|
|
805
|
-
</Typography>
|
|
806
|
-
|
|
807
|
-
<Typography
|
|
808
|
-
variant="body2"
|
|
809
|
-
sx={{
|
|
810
|
-
mb: 2,
|
|
811
|
-
fontSize: viewport === 'mobile' ? '0.75rem' : '0.875rem',
|
|
812
|
-
}}
|
|
813
|
-
>
|
|
814
|
-
This portal adapts its size and padding based on viewport. Content scales
|
|
815
|
-
appropriately for different screen sizes.
|
|
816
|
-
</Typography>
|
|
817
|
-
|
|
818
|
-
<Box
|
|
819
|
-
sx={{
|
|
820
|
-
display: 'grid',
|
|
821
|
-
gridTemplateColumns: viewport === 'mobile' ? '1fr' : 'repeat(2, 1fr)',
|
|
822
|
-
gap: 1,
|
|
823
|
-
mb: 2,
|
|
824
|
-
}}
|
|
825
|
-
>
|
|
826
|
-
<TextField
|
|
827
|
-
label="Field 1"
|
|
828
|
-
size={viewport === 'mobile' ? 'small' : 'medium'}
|
|
829
|
-
fullWidth
|
|
830
|
-
/>
|
|
831
|
-
<TextField
|
|
832
|
-
label="Field 2"
|
|
833
|
-
size={viewport === 'mobile' ? 'small' : 'medium'}
|
|
834
|
-
fullWidth
|
|
835
|
-
/>
|
|
836
|
-
</Box>
|
|
837
|
-
|
|
838
|
-
<Box
|
|
839
|
-
sx={{
|
|
840
|
-
display: 'flex',
|
|
841
|
-
gap: 1,
|
|
842
|
-
justifyContent: 'flex-end',
|
|
843
|
-
flexDirection: viewport === 'mobile' ? 'column' : 'row',
|
|
844
|
-
}}
|
|
845
|
-
>
|
|
846
|
-
<Button size={viewport === 'mobile' ? 'small' : 'medium'}>Cancel</Button>
|
|
847
|
-
<Button variant="contained" size={viewport === 'mobile' ? 'small' : 'medium'}>
|
|
848
|
-
Confirm
|
|
849
|
-
</Button>
|
|
850
|
-
</Box>
|
|
851
|
-
</Box>
|
|
852
|
-
</Portal>
|
|
853
|
-
)}
|
|
854
|
-
</Box>
|
|
855
|
-
);
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
return <ResponsiveComponent />;
|
|
859
|
-
},
|
|
860
|
-
play: async ({ canvasElement }) => {
|
|
861
|
-
const canvas = within(canvasElement);
|
|
862
|
-
|
|
863
|
-
// Test different viewport configurations
|
|
864
|
-
for (const viewport of ['mobile', 'tablet', 'desktop']) {
|
|
865
|
-
await userEvent.click(canvas.getByTestId(`viewport-${viewport}`));
|
|
866
|
-
|
|
867
|
-
await waitFor(() => {
|
|
868
|
-
const portal = document.querySelector('[data-testid="responsive-portal"]');
|
|
869
|
-
expect(portal).toBeInTheDocument();
|
|
870
|
-
|
|
871
|
-
// Verify content adapts to viewport
|
|
872
|
-
expect(portal).toHaveTextContent(`Responsive Portal Content (${viewport})`);
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Test portal visibility toggle
|
|
877
|
-
await userEvent.click(canvas.getByTestId('toggle-responsive-portal'));
|
|
878
|
-
await waitFor(() => {
|
|
879
|
-
expect(document.querySelector('[data-testid="responsive-portal"]')).not.toBeInTheDocument();
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
await userEvent.click(canvas.getByTestId('toggle-responsive-portal'));
|
|
883
|
-
await waitFor(() => {
|
|
884
|
-
expect(document.querySelector('[data-testid="responsive-portal"]')).toBeInTheDocument();
|
|
885
|
-
});
|
|
886
|
-
},
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
export const ThemeVariations: Story = {
|
|
890
|
-
render: () => {
|
|
891
|
-
const ThemeComponent = () => {
|
|
892
|
-
const [theme, setTheme] = useState('light');
|
|
893
|
-
const [showPortal, setShowPortal] = useState(true);
|
|
894
|
-
|
|
895
|
-
return (
|
|
896
|
-
<Box>
|
|
897
|
-
<Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
|
|
898
|
-
{['light', 'dark', 'high-contrast'].map((themeMode) => (
|
|
899
|
-
<Button
|
|
900
|
-
key={themeMode}
|
|
901
|
-
data-testid={`theme-${themeMode}`}
|
|
902
|
-
variant={theme === themeMode ? 'contained' : 'outlined'}
|
|
903
|
-
onClick={() => setTheme(themeMode)}
|
|
904
|
-
size="small"
|
|
905
|
-
>
|
|
906
|
-
{themeMode.charAt(0).toUpperCase() + themeMode.slice(1)}
|
|
907
|
-
</Button>
|
|
908
|
-
))}
|
|
909
|
-
<Button
|
|
910
|
-
data-testid="toggle-theme-portal"
|
|
911
|
-
onClick={() => setShowPortal(!showPortal)}
|
|
912
|
-
color="secondary"
|
|
913
|
-
size="small"
|
|
914
|
-
>
|
|
915
|
-
{showPortal ? 'Hide' : 'Show'} Portal
|
|
916
|
-
</Button>
|
|
917
|
-
</Box>
|
|
918
|
-
|
|
919
|
-
{showPortal && (
|
|
920
|
-
<Portal>
|
|
921
|
-
<Box
|
|
922
|
-
data-testid="theme-portal"
|
|
923
|
-
sx={{
|
|
924
|
-
position: 'fixed',
|
|
925
|
-
top: '50%',
|
|
926
|
-
left: '50%',
|
|
927
|
-
transform: 'translate(-50%, -50%)',
|
|
928
|
-
bgcolor:
|
|
929
|
-
theme === 'dark'
|
|
930
|
-
? 'grey.900'
|
|
931
|
-
: theme === 'high-contrast'
|
|
932
|
-
? '#000'
|
|
933
|
-
: 'background.paper',
|
|
934
|
-
color:
|
|
935
|
-
theme === 'dark'
|
|
936
|
-
? 'grey.100'
|
|
937
|
-
: theme === 'high-contrast'
|
|
938
|
-
? '#fff'
|
|
939
|
-
: 'text.primary',
|
|
940
|
-
p: 3,
|
|
941
|
-
borderRadius: theme === 'high-contrast' ? 0 : 2,
|
|
942
|
-
boxShadow: theme === 'high-contrast' ? 'none' : 24,
|
|
943
|
-
border: theme === 'high-contrast' ? '2px solid #fff' : 'none',
|
|
944
|
-
minWidth: 350,
|
|
945
|
-
zIndex: 9999,
|
|
946
|
-
}}
|
|
947
|
-
>
|
|
948
|
-
<Typography variant="h6" gutterBottom>
|
|
949
|
-
{theme.charAt(0).toUpperCase() + theme.slice(1)} Theme Portal
|
|
950
|
-
</Typography>
|
|
951
|
-
|
|
952
|
-
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
953
|
-
This portal adapts to different theme modes including colors, contrast levels, and
|
|
954
|
-
visual styling.
|
|
955
|
-
</Typography>
|
|
956
|
-
|
|
957
|
-
<Box
|
|
958
|
-
sx={{
|
|
959
|
-
mb: 2,
|
|
960
|
-
p: 1.5,
|
|
961
|
-
bgcolor:
|
|
962
|
-
theme === 'dark'
|
|
963
|
-
? 'grey.800'
|
|
964
|
-
: theme === 'high-contrast'
|
|
965
|
-
? '#333'
|
|
966
|
-
: 'grey.50',
|
|
967
|
-
borderRadius: theme === 'high-contrast' ? 0 : 1,
|
|
968
|
-
border: theme === 'high-contrast' ? '1px solid #fff' : 'none',
|
|
969
|
-
}}
|
|
970
|
-
>
|
|
971
|
-
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
|
|
972
|
-
Current Theme: {theme}
|
|
973
|
-
</Typography>
|
|
974
|
-
<Typography variant="caption">
|
|
975
|
-
Background and text colors adapt automatically
|
|
976
|
-
</Typography>
|
|
977
|
-
</Box>
|
|
978
|
-
|
|
979
|
-
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
980
|
-
<Button
|
|
981
|
-
sx={{
|
|
982
|
-
color: theme === 'high-contrast' ? '#fff' : 'inherit',
|
|
983
|
-
borderColor: theme === 'high-contrast' ? '#fff' : 'inherit',
|
|
984
|
-
'&:hover': {
|
|
985
|
-
bgcolor: theme === 'high-contrast' ? '#333' : 'inherit',
|
|
986
|
-
},
|
|
987
|
-
}}
|
|
988
|
-
>
|
|
989
|
-
Cancel
|
|
990
|
-
</Button>
|
|
991
|
-
<Button
|
|
992
|
-
variant="contained"
|
|
993
|
-
sx={{
|
|
994
|
-
bgcolor: theme === 'high-contrast' ? '#fff' : 'primary.main',
|
|
995
|
-
color: theme === 'high-contrast' ? '#000' : 'primary.contrastText',
|
|
996
|
-
'&:hover': {
|
|
997
|
-
bgcolor: theme === 'high-contrast' ? '#ccc' : 'primary.dark',
|
|
998
|
-
},
|
|
999
|
-
}}
|
|
1000
|
-
>
|
|
1001
|
-
Apply
|
|
1002
|
-
</Button>
|
|
1003
|
-
</Box>
|
|
1004
|
-
</Box>
|
|
1005
|
-
</Portal>
|
|
1006
|
-
)}
|
|
1007
|
-
</Box>
|
|
1008
|
-
);
|
|
1009
|
-
};
|
|
1010
|
-
|
|
1011
|
-
return <ThemeComponent />;
|
|
1012
|
-
},
|
|
1013
|
-
play: async ({ canvasElement }) => {
|
|
1014
|
-
const canvas = within(canvasElement);
|
|
1015
|
-
|
|
1016
|
-
// Test different theme variations
|
|
1017
|
-
for (const theme of ['light', 'dark', 'high-contrast']) {
|
|
1018
|
-
await userEvent.click(canvas.getByTestId(`theme-${theme}`));
|
|
1019
|
-
|
|
1020
|
-
await waitFor(() => {
|
|
1021
|
-
const portal = document.querySelector('[data-testid="theme-portal"]');
|
|
1022
|
-
expect(portal).toBeInTheDocument();
|
|
1023
|
-
expect(portal).toHaveTextContent(
|
|
1024
|
-
`${theme.charAt(0).toUpperCase() + theme.slice(1)} Theme Portal`,
|
|
1025
|
-
);
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Test portal toggle
|
|
1030
|
-
await userEvent.click(canvas.getByTestId('toggle-theme-portal'));
|
|
1031
|
-
await waitFor(() => {
|
|
1032
|
-
expect(document.querySelector('[data-testid="theme-portal"]')).not.toBeInTheDocument();
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
await userEvent.click(canvas.getByTestId('toggle-theme-portal'));
|
|
1036
|
-
await waitFor(() => {
|
|
1037
|
-
expect(document.querySelector('[data-testid="theme-portal"]')).toBeInTheDocument();
|
|
1038
|
-
});
|
|
1039
|
-
},
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
export const VisualStates: Story = {
|
|
1043
|
-
render: () => {
|
|
1044
|
-
const VisualStatesComponent = () => {
|
|
1045
|
-
const [state, setState] = useState<'default' | 'loading' | 'error' | 'success'>('default');
|
|
1046
|
-
const [showPortal, setShowPortal] = useState(true);
|
|
1047
|
-
|
|
1048
|
-
const stateConfig = {
|
|
1049
|
-
default: { color: 'primary', icon: '📄' },
|
|
1050
|
-
loading: { color: 'info', icon: '⏳' },
|
|
1051
|
-
error: { color: 'error', icon: '❌' },
|
|
1052
|
-
success: { color: 'success', icon: '✅' },
|
|
1053
|
-
};
|
|
1054
|
-
|
|
1055
|
-
return (
|
|
1056
|
-
<Box>
|
|
1057
|
-
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
1058
|
-
{Object.keys(stateConfig).map((stateName) => (
|
|
1059
|
-
<Button
|
|
1060
|
-
key={stateName}
|
|
1061
|
-
data-testid={`state-${stateName}`}
|
|
1062
|
-
variant={state === stateName ? 'contained' : 'outlined'}
|
|
1063
|
-
onClick={() => setState(stateName as 'default' | 'loading' | 'error' | 'success')}
|
|
1064
|
-
size="small"
|
|
1065
|
-
>
|
|
1066
|
-
{stateName}
|
|
1067
|
-
</Button>
|
|
1068
|
-
))}
|
|
1069
|
-
<Button
|
|
1070
|
-
data-testid="toggle-visual-portal"
|
|
1071
|
-
onClick={() => setShowPortal(!showPortal)}
|
|
1072
|
-
color="secondary"
|
|
1073
|
-
size="small"
|
|
1074
|
-
>
|
|
1075
|
-
{showPortal ? 'Hide' : 'Show'} Portal
|
|
1076
|
-
</Button>
|
|
1077
|
-
</Box>
|
|
1078
|
-
|
|
1079
|
-
{showPortal && (
|
|
1080
|
-
<Portal>
|
|
1081
|
-
<Box
|
|
1082
|
-
data-testid="visual-states-portal"
|
|
1083
|
-
sx={{
|
|
1084
|
-
position: 'fixed',
|
|
1085
|
-
top: '50%',
|
|
1086
|
-
left: '50%',
|
|
1087
|
-
transform: 'translate(-50%, -50%)',
|
|
1088
|
-
bgcolor: 'background.paper',
|
|
1089
|
-
p: 3,
|
|
1090
|
-
borderRadius: 2,
|
|
1091
|
-
boxShadow: 24,
|
|
1092
|
-
minWidth: 300,
|
|
1093
|
-
zIndex: 9999,
|
|
1094
|
-
border: state === 'error' ? 2 : 1,
|
|
1095
|
-
borderColor: state === 'error' ? 'error.main' : 'divider',
|
|
1096
|
-
}}
|
|
1097
|
-
>
|
|
1098
|
-
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
1099
|
-
<Typography variant="h4" sx={{ mr: 1 }}>
|
|
1100
|
-
{stateConfig[state].icon}
|
|
1101
|
-
</Typography>
|
|
1102
|
-
<Typography variant="h6">
|
|
1103
|
-
Portal State: {state.charAt(0).toUpperCase() + state.slice(1)}
|
|
1104
|
-
</Typography>
|
|
1105
|
-
</Box>
|
|
1106
|
-
|
|
1107
|
-
{state === 'loading' && (
|
|
1108
|
-
<Box sx={{ mb: 2 }}>
|
|
1109
|
-
<Typography variant="body2" color="text.secondary">
|
|
1110
|
-
Content is loading, please wait...
|
|
1111
|
-
</Typography>
|
|
1112
|
-
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center' }}>
|
|
1113
|
-
<Box
|
|
1114
|
-
sx={{
|
|
1115
|
-
width: 20,
|
|
1116
|
-
height: 20,
|
|
1117
|
-
border: '2px solid',
|
|
1118
|
-
borderColor: 'primary.light',
|
|
1119
|
-
borderTopColor: 'primary.main',
|
|
1120
|
-
borderRadius: '50%',
|
|
1121
|
-
animation: 'spin 1s linear infinite',
|
|
1122
|
-
mr: 1,
|
|
1123
|
-
'@keyframes spin': {
|
|
1124
|
-
'0%': { transform: 'rotate(0deg)' },
|
|
1125
|
-
'100%': { transform: 'rotate(360deg)' },
|
|
1126
|
-
},
|
|
1127
|
-
}}
|
|
1128
|
-
/>
|
|
1129
|
-
<Typography variant="caption">Loading...</Typography>
|
|
1130
|
-
</Box>
|
|
1131
|
-
</Box>
|
|
1132
|
-
)}
|
|
1133
|
-
|
|
1134
|
-
{state === 'error' && (
|
|
1135
|
-
<Alert severity="error" sx={{ mb: 2 }}>
|
|
1136
|
-
<Typography variant="body2">
|
|
1137
|
-
An error occurred while processing your request.
|
|
1138
|
-
</Typography>
|
|
1139
|
-
</Alert>
|
|
1140
|
-
)}
|
|
1141
|
-
|
|
1142
|
-
{state === 'success' && (
|
|
1143
|
-
<Alert severity="success" sx={{ mb: 2 }}>
|
|
1144
|
-
<Typography variant="body2">Operation completed successfully!</Typography>
|
|
1145
|
-
</Alert>
|
|
1146
|
-
)}
|
|
1147
|
-
|
|
1148
|
-
{state === 'default' && (
|
|
1149
|
-
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
1150
|
-
This is the default state of the portal with standard styling and no special
|
|
1151
|
-
indicators.
|
|
1152
|
-
</Typography>
|
|
1153
|
-
)}
|
|
1154
|
-
|
|
1155
|
-
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
1156
|
-
{state !== 'loading' && (
|
|
1157
|
-
<Button
|
|
1158
|
-
color={
|
|
1159
|
-
stateConfig[state].color as
|
|
1160
|
-
| 'primary'
|
|
1161
|
-
| 'secondary'
|
|
1162
|
-
| 'error'
|
|
1163
|
-
| 'warning'
|
|
1164
|
-
| 'info'
|
|
1165
|
-
| 'success'
|
|
1166
|
-
}
|
|
1167
|
-
variant={state === 'error' ? 'outlined' : 'contained'}
|
|
1168
|
-
>
|
|
1169
|
-
{state === 'error' ? 'Retry' : state === 'success' ? 'Done' : 'OK'}
|
|
1170
|
-
</Button>
|
|
1171
|
-
)}
|
|
1172
|
-
</Box>
|
|
1173
|
-
</Box>
|
|
1174
|
-
</Portal>
|
|
1175
|
-
)}
|
|
1176
|
-
</Box>
|
|
1177
|
-
);
|
|
1178
|
-
};
|
|
1179
|
-
|
|
1180
|
-
return <VisualStatesComponent />;
|
|
1181
|
-
},
|
|
1182
|
-
play: async ({ canvasElement }) => {
|
|
1183
|
-
const canvas = within(canvasElement);
|
|
1184
|
-
|
|
1185
|
-
// Test each visual state
|
|
1186
|
-
const states = ['default', 'loading', 'error', 'success'];
|
|
1187
|
-
|
|
1188
|
-
for (const state of states) {
|
|
1189
|
-
await userEvent.click(canvas.getByTestId(`state-${state}`));
|
|
1190
|
-
|
|
1191
|
-
await waitFor(() => {
|
|
1192
|
-
const portal = document.querySelector('[data-testid="visual-states-portal"]');
|
|
1193
|
-
expect(portal).toBeInTheDocument();
|
|
1194
|
-
expect(portal).toHaveTextContent(
|
|
1195
|
-
`Portal State: ${state.charAt(0).toUpperCase() + state.slice(1)}`,
|
|
1196
|
-
);
|
|
1197
|
-
|
|
1198
|
-
// Verify state-specific content
|
|
1199
|
-
if (state === 'loading') {
|
|
1200
|
-
expect(portal).toHaveTextContent('Content is loading, please wait...');
|
|
1201
|
-
} else if (state === 'error') {
|
|
1202
|
-
expect(portal?.querySelector('[role="alert"]')).toBeInTheDocument();
|
|
1203
|
-
} else if (state === 'success') {
|
|
1204
|
-
expect(portal?.querySelector('[role="alert"]')).toBeInTheDocument();
|
|
1205
|
-
expect(portal).toHaveTextContent('Operation completed successfully!');
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// Test portal toggle
|
|
1211
|
-
await userEvent.click(canvas.getByTestId('toggle-visual-portal'));
|
|
1212
|
-
await waitFor(() => {
|
|
1213
|
-
expect(
|
|
1214
|
-
document.querySelector('[data-testid="visual-states-portal"]'),
|
|
1215
|
-
).not.toBeInTheDocument();
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
await userEvent.click(canvas.getByTestId('toggle-visual-portal'));
|
|
1219
|
-
await waitFor(() => {
|
|
1220
|
-
expect(document.querySelector('[data-testid="visual-states-portal"]')).toBeInTheDocument();
|
|
1221
|
-
});
|
|
1222
|
-
},
|
|
1223
|
-
};
|
|
1224
|
-
|
|
1225
|
-
// ================================
|
|
1226
|
-
// PERFORMANCE TESTS
|
|
1227
|
-
// ================================
|
|
1228
|
-
|
|
1229
|
-
export const Performance: Story = {
|
|
1230
|
-
render: () => {
|
|
1231
|
-
const PerformanceComponent = () => {
|
|
1232
|
-
const [portalCount, setPortalCount] = useState(1);
|
|
1233
|
-
const [showPortals, setShowPortals] = useState(false);
|
|
1234
|
-
const [renderTime, setRenderTime] = useState<number | null>(null);
|
|
1235
|
-
|
|
1236
|
-
const handleTogglePortals = async () => {
|
|
1237
|
-
const startTime = window.performance.now();
|
|
1238
|
-
setShowPortals(!showPortals);
|
|
1239
|
-
|
|
1240
|
-
// Use requestAnimationFrame to measure after render
|
|
1241
|
-
window.requestAnimationFrame(() => {
|
|
1242
|
-
const endTime = window.performance.now();
|
|
1243
|
-
setRenderTime(endTime - startTime);
|
|
1244
|
-
});
|
|
1245
|
-
};
|
|
1246
|
-
|
|
1247
|
-
const portals = Array.from({ length: portalCount }, (_, i) => i);
|
|
1248
|
-
|
|
1249
|
-
return (
|
|
1250
|
-
<Box>
|
|
1251
|
-
<Box sx={{ mb: 2, display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
1252
|
-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
1253
|
-
<Typography>Portal Count:</Typography>
|
|
1254
|
-
<Button
|
|
1255
|
-
data-testid="decrease-count"
|
|
1256
|
-
size="small"
|
|
1257
|
-
onClick={() => setPortalCount(Math.max(1, portalCount - 1))}
|
|
1258
|
-
disabled={portalCount <= 1}
|
|
1259
|
-
>
|
|
1260
|
-
-
|
|
1261
|
-
</Button>
|
|
1262
|
-
<Typography data-testid="portal-count" sx={{ minWidth: 20, textAlign: 'center' }}>
|
|
1263
|
-
{portalCount}
|
|
1264
|
-
</Typography>
|
|
1265
|
-
<Button
|
|
1266
|
-
data-testid="increase-count"
|
|
1267
|
-
size="small"
|
|
1268
|
-
onClick={() => setPortalCount(Math.min(50, portalCount + 1))}
|
|
1269
|
-
disabled={portalCount >= 50}
|
|
1270
|
-
>
|
|
1271
|
-
+
|
|
1272
|
-
</Button>
|
|
1273
|
-
</Box>
|
|
1274
|
-
|
|
1275
|
-
<Button
|
|
1276
|
-
data-testid="toggle-performance-portals"
|
|
1277
|
-
variant="contained"
|
|
1278
|
-
onClick={handleTogglePortals}
|
|
1279
|
-
>
|
|
1280
|
-
{showPortals ? 'Hide' : 'Show'} {portalCount} Portal{portalCount > 1 ? 's' : ''}
|
|
1281
|
-
</Button>
|
|
1282
|
-
|
|
1283
|
-
{renderTime !== null && (
|
|
1284
|
-
<Typography data-testid="render-time" variant="caption" color="text.secondary">
|
|
1285
|
-
Render time: {renderTime.toFixed(2)}ms
|
|
1286
|
-
</Typography>
|
|
1287
|
-
)}
|
|
1288
|
-
</Box>
|
|
1289
|
-
|
|
1290
|
-
{showPortals &&
|
|
1291
|
-
portals.map((index) => (
|
|
1292
|
-
<Portal key={index}>
|
|
1293
|
-
<Box
|
|
1294
|
-
data-testid={`performance-portal-${index}`}
|
|
1295
|
-
sx={{
|
|
1296
|
-
position: 'fixed',
|
|
1297
|
-
top: 50 + (index % 10) * 60,
|
|
1298
|
-
left: 50 + Math.floor(index / 10) * 250,
|
|
1299
|
-
bgcolor: `hsl(${(index * 360) / portalCount}, 70%, 95%)`,
|
|
1300
|
-
border: `2px solid hsl(${(index * 360) / portalCount}, 70%, 70%)`,
|
|
1301
|
-
p: 1,
|
|
1302
|
-
borderRadius: 1,
|
|
1303
|
-
minWidth: 200,
|
|
1304
|
-
zIndex: 9999 + index,
|
|
1305
|
-
}}
|
|
1306
|
-
>
|
|
1307
|
-
<Typography variant="caption" fontWeight="bold">
|
|
1308
|
-
Portal #{index + 1}
|
|
1309
|
-
</Typography>
|
|
1310
|
-
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
|
1311
|
-
Performance test portal with unique styling and positioning. Z-index:{' '}
|
|
1312
|
-
{9999 + index}
|
|
1313
|
-
</Typography>
|
|
1314
|
-
</Box>
|
|
1315
|
-
</Portal>
|
|
1316
|
-
))}
|
|
1317
|
-
</Box>
|
|
1318
|
-
);
|
|
1319
|
-
};
|
|
1320
|
-
|
|
1321
|
-
return <PerformanceComponent />;
|
|
1322
|
-
},
|
|
1323
|
-
play: async ({ canvasElement }) => {
|
|
1324
|
-
const canvas = within(canvasElement);
|
|
1325
|
-
|
|
1326
|
-
// Test single portal performance
|
|
1327
|
-
const toggleButton = canvas.getByTestId('toggle-performance-portals');
|
|
1328
|
-
await userEvent.click(toggleButton);
|
|
1329
|
-
|
|
1330
|
-
await waitFor(() => {
|
|
1331
|
-
expect(document.querySelector('[data-testid="performance-portal-0"]')).toBeInTheDocument();
|
|
1332
|
-
});
|
|
1333
|
-
|
|
1334
|
-
// Verify render time is recorded
|
|
1335
|
-
await waitFor(() => {
|
|
1336
|
-
const renderTimeEl = canvas.queryByTestId('render-time');
|
|
1337
|
-
if (renderTimeEl) {
|
|
1338
|
-
expect(renderTimeEl).toHaveTextContent(/Render time: \d+\.\d+ms/);
|
|
1339
|
-
}
|
|
1340
|
-
});
|
|
1341
|
-
|
|
1342
|
-
// Test multiple portals
|
|
1343
|
-
const increaseButton = canvas.getByTestId('increase-count');
|
|
1344
|
-
|
|
1345
|
-
// Increase to 5 portals
|
|
1346
|
-
for (let i = 0; i < 4; i++) {
|
|
1347
|
-
await userEvent.click(increaseButton);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// Verify count updated
|
|
1351
|
-
expect(canvas.getByTestId('portal-count')).toHaveTextContent('5');
|
|
1352
|
-
|
|
1353
|
-
// Toggle portals with increased count
|
|
1354
|
-
await userEvent.click(toggleButton);
|
|
1355
|
-
|
|
1356
|
-
await waitFor(() => {
|
|
1357
|
-
expect(
|
|
1358
|
-
document.querySelector('[data-testid="performance-portal-0"]'),
|
|
1359
|
-
).not.toBeInTheDocument();
|
|
1360
|
-
});
|
|
1361
|
-
|
|
1362
|
-
await userEvent.click(toggleButton);
|
|
1363
|
-
|
|
1364
|
-
await waitFor(() => {
|
|
1365
|
-
for (let i = 0; i < 5; i++) {
|
|
1366
|
-
expect(
|
|
1367
|
-
document.querySelector(`[data-testid="performance-portal-${i}"]`),
|
|
1368
|
-
).toBeInTheDocument();
|
|
1369
|
-
}
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
// Test performance with many portals
|
|
1373
|
-
for (let i = 0; i < 5; i++) {
|
|
1374
|
-
await userEvent.click(increaseButton);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
expect(canvas.getByTestId('portal-count')).toHaveTextContent('10');
|
|
1378
|
-
|
|
1379
|
-
// Toggle with 10 portals and verify performance
|
|
1380
|
-
await userEvent.click(toggleButton);
|
|
1381
|
-
await userEvent.click(toggleButton);
|
|
1382
|
-
|
|
1383
|
-
await waitFor(() => {
|
|
1384
|
-
for (let i = 0; i < 10; i++) {
|
|
1385
|
-
expect(
|
|
1386
|
-
document.querySelector(`[data-testid="performance-portal-${i}"]`),
|
|
1387
|
-
).toBeInTheDocument();
|
|
1388
|
-
}
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
// Verify all portals have unique positioning and styling
|
|
1392
|
-
const portal0 = document.querySelector('[data-testid="performance-portal-0"]');
|
|
1393
|
-
const portal5 = document.querySelector('[data-testid="performance-portal-5"]');
|
|
1394
|
-
|
|
1395
|
-
expect(portal0).toHaveStyle('top: 50px');
|
|
1396
|
-
expect(portal5).toHaveStyle('top: 350px'); // 50 + (5 % 10) * 60
|
|
1397
|
-
},
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
// ================================
|
|
1401
|
-
// EDGE CASES TESTS
|
|
1402
|
-
// ================================
|
|
1403
|
-
|
|
1404
|
-
export const EdgeCases: Story = {
|
|
1405
|
-
render: () => {
|
|
1406
|
-
const EdgeCasesComponent = () => {
|
|
1407
|
-
const [testCase, setTestCase] = useState<string>('null-container');
|
|
1408
|
-
const [customContainer, setCustomContainer] = useState<HTMLElement | null>(null);
|
|
1409
|
-
const [showPortal, setShowPortal] = useState(false);
|
|
1410
|
-
const [showContainer, setShowContainer] = useState(true);
|
|
1411
|
-
const [,] = useState<React.ReactNode>('Default content');
|
|
1412
|
-
|
|
1413
|
-
const testCases = {
|
|
1414
|
-
'null-container': {
|
|
1415
|
-
title: 'Null Container',
|
|
1416
|
-
description: 'Portal with explicitly null container should fall back to document.body',
|
|
1417
|
-
},
|
|
1418
|
-
'undefined-container': {
|
|
1419
|
-
title: 'Undefined Container',
|
|
1420
|
-
description: 'Portal with undefined container should create default container',
|
|
1421
|
-
},
|
|
1422
|
-
'invalid-container': {
|
|
1423
|
-
title: 'Invalid Container',
|
|
1424
|
-
description: 'Portal with container that gets removed from DOM',
|
|
1425
|
-
},
|
|
1426
|
-
'empty-children': {
|
|
1427
|
-
title: 'Empty Children',
|
|
1428
|
-
description: 'Portal with no children or null children',
|
|
1429
|
-
},
|
|
1430
|
-
'complex-children': {
|
|
1431
|
-
title: 'Complex Children',
|
|
1432
|
-
description: 'Portal with complex nested React elements',
|
|
1433
|
-
},
|
|
1434
|
-
'rapid-toggle': {
|
|
1435
|
-
title: 'Rapid Toggle',
|
|
1436
|
-
description: 'Rapidly showing and hiding portal to test cleanup',
|
|
1437
|
-
},
|
|
1438
|
-
};
|
|
1439
|
-
|
|
1440
|
-
const renderPortalForTestCase = () => {
|
|
1441
|
-
switch (testCase) {
|
|
1442
|
-
case 'null-container':
|
|
1443
|
-
return (
|
|
1444
|
-
<Portal container={null}>
|
|
1445
|
-
<Box
|
|
1446
|
-
data-testid="edge-portal-content"
|
|
1447
|
-
sx={{
|
|
1448
|
-
position: 'fixed',
|
|
1449
|
-
top: 20,
|
|
1450
|
-
right: 20,
|
|
1451
|
-
p: 2,
|
|
1452
|
-
bgcolor: 'primary.main',
|
|
1453
|
-
color: 'white',
|
|
1454
|
-
borderRadius: 1,
|
|
1455
|
-
}}
|
|
1456
|
-
>
|
|
1457
|
-
Null container test
|
|
1458
|
-
</Box>
|
|
1459
|
-
</Portal>
|
|
1460
|
-
);
|
|
1461
|
-
|
|
1462
|
-
case 'undefined-container':
|
|
1463
|
-
return (
|
|
1464
|
-
<Portal container={undefined}>
|
|
1465
|
-
<Box
|
|
1466
|
-
data-testid="edge-portal-content"
|
|
1467
|
-
sx={{
|
|
1468
|
-
position: 'fixed',
|
|
1469
|
-
top: 20,
|
|
1470
|
-
right: 20,
|
|
1471
|
-
p: 2,
|
|
1472
|
-
bgcolor: 'secondary.main',
|
|
1473
|
-
color: 'white',
|
|
1474
|
-
borderRadius: 1,
|
|
1475
|
-
}}
|
|
1476
|
-
>
|
|
1477
|
-
Undefined container test
|
|
1478
|
-
</Box>
|
|
1479
|
-
</Portal>
|
|
1480
|
-
);
|
|
1481
|
-
|
|
1482
|
-
case 'invalid-container':
|
|
1483
|
-
return (
|
|
1484
|
-
<Portal container={customContainer}>
|
|
1485
|
-
<Box
|
|
1486
|
-
data-testid="edge-portal-content"
|
|
1487
|
-
sx={{
|
|
1488
|
-
position: 'absolute',
|
|
1489
|
-
top: 10,
|
|
1490
|
-
right: 10,
|
|
1491
|
-
p: 2,
|
|
1492
|
-
bgcolor: 'warning.main',
|
|
1493
|
-
color: 'white',
|
|
1494
|
-
borderRadius: 1,
|
|
1495
|
-
}}
|
|
1496
|
-
>
|
|
1497
|
-
Invalid container test
|
|
1498
|
-
</Box>
|
|
1499
|
-
</Portal>
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
|
-
case 'empty-children':
|
|
1503
|
-
return <Portal>{null}</Portal>;
|
|
1504
|
-
|
|
1505
|
-
case 'complex-children':
|
|
1506
|
-
return (
|
|
1507
|
-
<Portal>
|
|
1508
|
-
<Box
|
|
1509
|
-
data-testid="edge-portal-content"
|
|
1510
|
-
sx={{
|
|
1511
|
-
position: 'fixed',
|
|
1512
|
-
top: '50%',
|
|
1513
|
-
left: '50%',
|
|
1514
|
-
transform: 'translate(-50%, -50%)',
|
|
1515
|
-
p: 2,
|
|
1516
|
-
bgcolor: 'background.paper',
|
|
1517
|
-
boxShadow: 24,
|
|
1518
|
-
borderRadius: 2,
|
|
1519
|
-
}}
|
|
1520
|
-
>
|
|
1521
|
-
<Typography variant="h6">Complex Portal Content</Typography>
|
|
1522
|
-
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
|
1523
|
-
<Button size="small" variant="outlined">
|
|
1524
|
-
Button 1
|
|
1525
|
-
</Button>
|
|
1526
|
-
<Button size="small" variant="contained">
|
|
1527
|
-
Button 2
|
|
1528
|
-
</Button>
|
|
1529
|
-
</Box>
|
|
1530
|
-
<TextField label="Input Field" size="small" fullWidth sx={{ mt: 1 }} />
|
|
1531
|
-
<Alert severity="info" sx={{ mt: 1 }}>
|
|
1532
|
-
This is a complex nested structure
|
|
1533
|
-
</Alert>
|
|
1534
|
-
</Box>
|
|
1535
|
-
</Portal>
|
|
1536
|
-
);
|
|
1537
|
-
|
|
1538
|
-
case 'rapid-toggle':
|
|
1539
|
-
return (
|
|
1540
|
-
<Portal>
|
|
1541
|
-
<Box
|
|
1542
|
-
data-testid="edge-portal-content"
|
|
1543
|
-
sx={{
|
|
1544
|
-
position: 'fixed',
|
|
1545
|
-
top: 20,
|
|
1546
|
-
right: 20,
|
|
1547
|
-
p: 2,
|
|
1548
|
-
bgcolor: 'success.main',
|
|
1549
|
-
color: 'white',
|
|
1550
|
-
borderRadius: 1,
|
|
1551
|
-
}}
|
|
1552
|
-
>
|
|
1553
|
-
Rapid toggle test - {Date.now()}
|
|
1554
|
-
</Box>
|
|
1555
|
-
</Portal>
|
|
1556
|
-
);
|
|
1557
|
-
|
|
1558
|
-
default:
|
|
1559
|
-
return null;
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
return (
|
|
1564
|
-
<Box>
|
|
1565
|
-
<Box sx={{ mb: 2 }}>
|
|
1566
|
-
<Typography variant="h6" gutterBottom>
|
|
1567
|
-
Edge Cases Test
|
|
1568
|
-
</Typography>
|
|
1569
|
-
<Typography variant="body2" color="text.secondary">
|
|
1570
|
-
{testCases[testCase as keyof typeof testCases]?.description}
|
|
1571
|
-
</Typography>
|
|
1572
|
-
</Box>
|
|
1573
|
-
|
|
1574
|
-
<Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
1575
|
-
{Object.entries(testCases).map(([key, config]) => (
|
|
1576
|
-
<Button
|
|
1577
|
-
key={key}
|
|
1578
|
-
data-testid={`test-case-${key}`}
|
|
1579
|
-
variant={testCase === key ? 'contained' : 'outlined'}
|
|
1580
|
-
onClick={() => {
|
|
1581
|
-
setTestCase(key);
|
|
1582
|
-
setShowPortal(false); // Reset portal state when changing test cases
|
|
1583
|
-
setShowContainer(true); // Reset container state when changing test cases
|
|
1584
|
-
}}
|
|
1585
|
-
size="small"
|
|
1586
|
-
>
|
|
1587
|
-
{config.title}
|
|
1588
|
-
</Button>
|
|
1589
|
-
))}
|
|
1590
|
-
</Box>
|
|
1591
|
-
|
|
1592
|
-
<Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
|
|
1593
|
-
<Button
|
|
1594
|
-
data-testid="toggle-edge-portal"
|
|
1595
|
-
variant="contained"
|
|
1596
|
-
onClick={() => setShowPortal(!showPortal)}
|
|
1597
|
-
>
|
|
1598
|
-
{showPortal ? 'Hide' : 'Show'} Portal
|
|
1599
|
-
</Button>
|
|
1600
|
-
|
|
1601
|
-
{testCase === 'invalid-container' && (
|
|
1602
|
-
<Button
|
|
1603
|
-
data-testid="remove-container"
|
|
1604
|
-
color="warning"
|
|
1605
|
-
onClick={() => {
|
|
1606
|
-
setShowContainer(false);
|
|
1607
|
-
setCustomContainer(null);
|
|
1608
|
-
}}
|
|
1609
|
-
>
|
|
1610
|
-
Remove Container
|
|
1611
|
-
</Button>
|
|
1612
|
-
)}
|
|
1613
|
-
|
|
1614
|
-
{testCase === 'rapid-toggle' && (
|
|
1615
|
-
<Button
|
|
1616
|
-
data-testid="rapid-toggle"
|
|
1617
|
-
color="secondary"
|
|
1618
|
-
onClick={async () => {
|
|
1619
|
-
// Rapidly toggle portal 10 times
|
|
1620
|
-
for (let i = 0; i < 10; i++) {
|
|
1621
|
-
setShowPortal((prev) => !prev);
|
|
1622
|
-
await new Promise((resolve) => window.setTimeout(resolve, 50));
|
|
1623
|
-
}
|
|
1624
|
-
}}
|
|
1625
|
-
>
|
|
1626
|
-
Rapid Toggle (10x)
|
|
1627
|
-
</Button>
|
|
1628
|
-
)}
|
|
1629
|
-
</Box>
|
|
1630
|
-
|
|
1631
|
-
{/* Container for invalid-container test */}
|
|
1632
|
-
{testCase === 'invalid-container' && showContainer && (
|
|
1633
|
-
<Paper
|
|
1634
|
-
ref={(el) => setCustomContainer(el)}
|
|
1635
|
-
data-testid="removable-container"
|
|
1636
|
-
sx={{ p: 2, mb: 2, bgcolor: 'grey.100', position: 'relative' }}
|
|
1637
|
-
>
|
|
1638
|
-
<Typography variant="subtitle2">Removable Container</Typography>
|
|
1639
|
-
<Typography variant="body2" color="text.secondary">
|
|
1640
|
-
This container can be removed to test invalid container handling
|
|
1641
|
-
</Typography>
|
|
1642
|
-
</Paper>
|
|
1643
|
-
)}
|
|
1644
|
-
|
|
1645
|
-
{showPortal && renderPortalForTestCase()}
|
|
1646
|
-
</Box>
|
|
1647
|
-
);
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
return <EdgeCasesComponent />;
|
|
1651
|
-
},
|
|
1652
|
-
play: async ({ canvasElement }) => {
|
|
1653
|
-
const canvas = within(canvasElement);
|
|
1654
|
-
|
|
1655
|
-
// Test null container case
|
|
1656
|
-
await userEvent.click(canvas.getByTestId('test-case-null-container'));
|
|
1657
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
|
|
1658
|
-
|
|
1659
|
-
await waitFor(() => {
|
|
1660
|
-
const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
|
|
1661
|
-
expect(portalContent).toBeInTheDocument();
|
|
1662
|
-
// Should be appended to document.body since container is null
|
|
1663
|
-
expect(portalContent?.parentElement).toBe(document.body);
|
|
1664
|
-
});
|
|
1665
|
-
|
|
1666
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
|
|
1667
|
-
|
|
1668
|
-
// Test undefined container case
|
|
1669
|
-
await userEvent.click(canvas.getByTestId('test-case-undefined-container'));
|
|
1670
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
|
|
1671
|
-
|
|
1672
|
-
await waitFor(() => {
|
|
1673
|
-
const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
|
|
1674
|
-
expect(portalContent).toBeInTheDocument();
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
|
|
1678
|
-
|
|
1679
|
-
// Test invalid container case
|
|
1680
|
-
await userEvent.click(canvas.getByTestId('test-case-invalid-container'));
|
|
1681
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
|
|
1682
|
-
|
|
1683
|
-
await waitFor(() => {
|
|
1684
|
-
const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
|
|
1685
|
-
expect(portalContent).toBeInTheDocument();
|
|
1686
|
-
|
|
1687
|
-
// Should be in the custom container initially
|
|
1688
|
-
const customContainer = canvas.getByTestId('removable-container');
|
|
1689
|
-
expect(customContainer).toContainElement(portalContent);
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
|
-
// Remove the container and verify portal handles it gracefully
|
|
1693
|
-
await userEvent.click(canvas.getByTestId('remove-container'));
|
|
1694
|
-
|
|
1695
|
-
// Portal should still exist and handle the missing container
|
|
1696
|
-
await waitFor(() => {
|
|
1697
|
-
// The portal content might still exist or be moved to body
|
|
1698
|
-
const _portalContent = document.querySelector('[data-testid="edge-portal-content"]');
|
|
1699
|
-
// We don't expect it to crash, content may or may not still be visible
|
|
1700
|
-
void _portalContent; // Acknowledge the unused variable
|
|
1701
|
-
});
|
|
1702
|
-
|
|
1703
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
|
|
1704
|
-
|
|
1705
|
-
// Test empty children case
|
|
1706
|
-
await userEvent.click(canvas.getByTestId('test-case-empty-children'));
|
|
1707
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
|
|
1708
|
-
|
|
1709
|
-
// Should not crash with empty children
|
|
1710
|
-
await waitFor(() => {
|
|
1711
|
-
// No portal content should be visible
|
|
1712
|
-
expect(document.querySelector('[data-testid="edge-portal-content"]')).not.toBeInTheDocument();
|
|
1713
|
-
});
|
|
1714
|
-
|
|
1715
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
|
|
1716
|
-
|
|
1717
|
-
// Test complex children case
|
|
1718
|
-
await userEvent.click(canvas.getByTestId('test-case-complex-children'));
|
|
1719
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
|
|
1720
|
-
|
|
1721
|
-
await waitFor(() => {
|
|
1722
|
-
const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
|
|
1723
|
-
expect(portalContent).toBeInTheDocument();
|
|
1724
|
-
expect(portalContent).toHaveTextContent('Complex Portal Content');
|
|
1725
|
-
|
|
1726
|
-
// Verify nested elements exist
|
|
1727
|
-
expect(portalContent?.querySelector('button')).toBeInTheDocument();
|
|
1728
|
-
expect(portalContent?.querySelector('input')).toBeInTheDocument();
|
|
1729
|
-
expect(portalContent?.querySelector('[role="alert"]')).toBeInTheDocument();
|
|
1730
|
-
});
|
|
1731
|
-
|
|
1732
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
|
|
1733
|
-
|
|
1734
|
-
// Test rapid toggle case
|
|
1735
|
-
await userEvent.click(canvas.getByTestId('test-case-rapid-toggle'));
|
|
1736
|
-
|
|
1737
|
-
// Test single toggle first
|
|
1738
|
-
await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
|
|
1739
|
-
await waitFor(() => {
|
|
1740
|
-
expect(document.querySelector('[data-testid="edge-portal-content"]')).toBeInTheDocument();
|
|
1741
|
-
});
|
|
1742
|
-
|
|
1743
|
-
// Test rapid toggle functionality
|
|
1744
|
-
await userEvent.click(canvas.getByTestId('rapid-toggle'));
|
|
1745
|
-
|
|
1746
|
-
// Give some time for rapid toggles to complete
|
|
1747
|
-
await new Promise((resolve) => window.setTimeout(resolve, 600));
|
|
1748
|
-
|
|
1749
|
-
// Should not crash and final state should be deterministic
|
|
1750
|
-
// (The exact final state depends on the implementation but shouldn't crash)
|
|
1751
|
-
},
|
|
1752
|
-
};
|
|
1753
|
-
|
|
1754
|
-
// ================================
|
|
1755
|
-
// INTEGRATION TESTS
|
|
1756
|
-
// ================================
|
|
1757
|
-
|
|
1758
|
-
export const Integration: Story = {
|
|
1759
|
-
render: () => {
|
|
1760
|
-
const IntegrationComponent = () => {
|
|
1761
|
-
const [showModal, setShowModal] = useState(false);
|
|
1762
|
-
const [showTooltip, setShowTooltip] = useState(false);
|
|
1763
|
-
const [showDropdown, setShowDropdown] = useState(false);
|
|
1764
|
-
const [notifications, setNotifications] = useState<string[]>([]);
|
|
1765
|
-
|
|
1766
|
-
const addNotification = (message: string) => {
|
|
1767
|
-
setNotifications((prev) => [...prev, message]);
|
|
1768
|
-
window.setTimeout(() => {
|
|
1769
|
-
setNotifications((prev) => prev.slice(1));
|
|
1770
|
-
}, 3000);
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
|
-
return (
|
|
1774
|
-
<Box>
|
|
1775
|
-
<Typography variant="h6" gutterBottom>
|
|
1776
|
-
Multiple Portal Integration
|
|
1777
|
-
</Typography>
|
|
1778
|
-
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
1779
|
-
Test multiple portals working together with proper z-index stacking and interaction.
|
|
1780
|
-
</Typography>
|
|
1781
|
-
|
|
1782
|
-
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
|
|
1783
|
-
<Button
|
|
1784
|
-
data-testid="show-modal"
|
|
1785
|
-
variant="contained"
|
|
1786
|
-
onClick={() => {
|
|
1787
|
-
setShowModal(true);
|
|
1788
|
-
addNotification('Modal opened');
|
|
1789
|
-
}}
|
|
1790
|
-
>
|
|
1791
|
-
Show Modal
|
|
1792
|
-
</Button>
|
|
1793
|
-
|
|
1794
|
-
<Button
|
|
1795
|
-
data-testid="show-tooltip"
|
|
1796
|
-
variant="outlined"
|
|
1797
|
-
onMouseEnter={() => setShowTooltip(true)}
|
|
1798
|
-
onMouseLeave={() => setShowTooltip(false)}
|
|
1799
|
-
>
|
|
1800
|
-
Hover for Tooltip
|
|
1801
|
-
</Button>
|
|
1802
|
-
|
|
1803
|
-
<Button
|
|
1804
|
-
data-testid="show-dropdown"
|
|
1805
|
-
variant="outlined"
|
|
1806
|
-
onClick={() => setShowDropdown(!showDropdown)}
|
|
1807
|
-
>
|
|
1808
|
-
Toggle Dropdown
|
|
1809
|
-
</Button>
|
|
1810
|
-
|
|
1811
|
-
<Button
|
|
1812
|
-
data-testid="add-notification"
|
|
1813
|
-
variant="text"
|
|
1814
|
-
onClick={() => addNotification(`Notification ${Date.now()}`)}
|
|
1815
|
-
>
|
|
1816
|
-
Add Notification
|
|
1817
|
-
</Button>
|
|
1818
|
-
</Box>
|
|
1819
|
-
|
|
1820
|
-
{/* Modal Portal */}
|
|
1821
|
-
{showModal && (
|
|
1822
|
-
<Portal>
|
|
1823
|
-
{/* Backdrop */}
|
|
1824
|
-
<Box
|
|
1825
|
-
data-testid="modal-backdrop"
|
|
1826
|
-
onClick={() => setShowModal(false)}
|
|
1827
|
-
sx={{
|
|
1828
|
-
position: 'fixed',
|
|
1829
|
-
top: 0,
|
|
1830
|
-
left: 0,
|
|
1831
|
-
right: 0,
|
|
1832
|
-
bottom: 0,
|
|
1833
|
-
bgcolor: 'rgba(0, 0, 0, 0.5)',
|
|
1834
|
-
zIndex: 1000,
|
|
1835
|
-
}}
|
|
1836
|
-
/>
|
|
1837
|
-
|
|
1838
|
-
{/* Modal Content */}
|
|
1839
|
-
<Box
|
|
1840
|
-
data-testid="modal-content"
|
|
1841
|
-
sx={{
|
|
1842
|
-
position: 'fixed',
|
|
1843
|
-
top: '50%',
|
|
1844
|
-
left: '50%',
|
|
1845
|
-
transform: 'translate(-50%, -50%)',
|
|
1846
|
-
bgcolor: 'background.paper',
|
|
1847
|
-
p: 3,
|
|
1848
|
-
borderRadius: 2,
|
|
1849
|
-
boxShadow: 24,
|
|
1850
|
-
minWidth: 400,
|
|
1851
|
-
zIndex: 1001,
|
|
1852
|
-
}}
|
|
1853
|
-
>
|
|
1854
|
-
<Typography variant="h6" gutterBottom>
|
|
1855
|
-
Modal Dialog
|
|
1856
|
-
</Typography>
|
|
1857
|
-
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
1858
|
-
This modal is rendered in a portal with high z-index to appear above other
|
|
1859
|
-
content.
|
|
1860
|
-
</Typography>
|
|
1861
|
-
|
|
1862
|
-
<Box sx={{ mb: 2 }}>
|
|
1863
|
-
<TextField label="Input in Modal" fullWidth size="small" />
|
|
1864
|
-
</Box>
|
|
1865
|
-
|
|
1866
|
-
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
|
1867
|
-
<Button data-testid="modal-cancel" onClick={() => setShowModal(false)}>
|
|
1868
|
-
Cancel
|
|
1869
|
-
</Button>
|
|
1870
|
-
<Button
|
|
1871
|
-
data-testid="modal-confirm"
|
|
1872
|
-
variant="contained"
|
|
1873
|
-
onClick={() => {
|
|
1874
|
-
setShowModal(false);
|
|
1875
|
-
addNotification('Modal confirmed');
|
|
1876
|
-
}}
|
|
1877
|
-
>
|
|
1878
|
-
Confirm
|
|
1879
|
-
</Button>
|
|
1880
|
-
</Box>
|
|
1881
|
-
</Box>
|
|
1882
|
-
</Portal>
|
|
1883
|
-
)}
|
|
1884
|
-
|
|
1885
|
-
{/* Tooltip Portal */}
|
|
1886
|
-
{showTooltip && (
|
|
1887
|
-
<Portal>
|
|
1888
|
-
<Box
|
|
1889
|
-
data-testid="tooltip-content"
|
|
1890
|
-
sx={{
|
|
1891
|
-
position: 'absolute',
|
|
1892
|
-
top: '100%',
|
|
1893
|
-
left: '50%',
|
|
1894
|
-
transform: 'translateX(-50%)',
|
|
1895
|
-
bgcolor: 'grey.800',
|
|
1896
|
-
color: 'white',
|
|
1897
|
-
p: 1,
|
|
1898
|
-
borderRadius: 1,
|
|
1899
|
-
fontSize: '0.75rem',
|
|
1900
|
-
zIndex: 1100,
|
|
1901
|
-
mt: 0.5,
|
|
1902
|
-
'&::before': {
|
|
1903
|
-
content: '""',
|
|
1904
|
-
position: 'absolute',
|
|
1905
|
-
top: -4,
|
|
1906
|
-
left: '50%',
|
|
1907
|
-
transform: 'translateX(-50%)',
|
|
1908
|
-
width: 0,
|
|
1909
|
-
height: 0,
|
|
1910
|
-
borderLeft: '4px solid transparent',
|
|
1911
|
-
borderRight: '4px solid transparent',
|
|
1912
|
-
borderBottom: '4px solid',
|
|
1913
|
-
borderBottomColor: 'grey.800',
|
|
1914
|
-
},
|
|
1915
|
-
}}
|
|
1916
|
-
>
|
|
1917
|
-
This is a tooltip rendered in a portal
|
|
1918
|
-
</Box>
|
|
1919
|
-
</Portal>
|
|
1920
|
-
)}
|
|
1921
|
-
|
|
1922
|
-
{/* Dropdown Portal */}
|
|
1923
|
-
{showDropdown && (
|
|
1924
|
-
<Portal>
|
|
1925
|
-
<Box
|
|
1926
|
-
data-testid="dropdown-content"
|
|
1927
|
-
sx={{
|
|
1928
|
-
position: 'fixed',
|
|
1929
|
-
top: 200,
|
|
1930
|
-
left: 300,
|
|
1931
|
-
bgcolor: 'background.paper',
|
|
1932
|
-
border: 1,
|
|
1933
|
-
borderColor: 'divider',
|
|
1934
|
-
borderRadius: 1,
|
|
1935
|
-
boxShadow: 3,
|
|
1936
|
-
py: 1,
|
|
1937
|
-
minWidth: 150,
|
|
1938
|
-
zIndex: 1050,
|
|
1939
|
-
}}
|
|
1940
|
-
>
|
|
1941
|
-
{['Option 1', 'Option 2', 'Option 3'].map((option) => (
|
|
1942
|
-
<Box
|
|
1943
|
-
key={option}
|
|
1944
|
-
data-testid={`dropdown-option-${option.toLowerCase().replace(' ', '-')}`}
|
|
1945
|
-
sx={{
|
|
1946
|
-
px: 2,
|
|
1947
|
-
py: 1,
|
|
1948
|
-
cursor: 'pointer',
|
|
1949
|
-
'&:hover': {
|
|
1950
|
-
bgcolor: 'action.hover',
|
|
1951
|
-
},
|
|
1952
|
-
}}
|
|
1953
|
-
onClick={() => {
|
|
1954
|
-
setShowDropdown(false);
|
|
1955
|
-
addNotification(`Selected: ${option}`);
|
|
1956
|
-
}}
|
|
1957
|
-
>
|
|
1958
|
-
<Typography variant="body2">{option}</Typography>
|
|
1959
|
-
</Box>
|
|
1960
|
-
))}
|
|
1961
|
-
</Box>
|
|
1962
|
-
</Portal>
|
|
1963
|
-
)}
|
|
1964
|
-
|
|
1965
|
-
{/* Notifications Portal */}
|
|
1966
|
-
{notifications.map((notification, index) => (
|
|
1967
|
-
<Portal key={`${notification}-${index}`}>
|
|
1968
|
-
<Box
|
|
1969
|
-
data-testid={`notification-${index}`}
|
|
1970
|
-
sx={{
|
|
1971
|
-
position: 'fixed',
|
|
1972
|
-
top: 20 + index * 60,
|
|
1973
|
-
right: 20,
|
|
1974
|
-
bgcolor: 'success.main',
|
|
1975
|
-
color: 'white',
|
|
1976
|
-
p: 2,
|
|
1977
|
-
borderRadius: 1,
|
|
1978
|
-
boxShadow: 3,
|
|
1979
|
-
maxWidth: 300,
|
|
1980
|
-
zIndex: 1200,
|
|
1981
|
-
animation: 'slideIn 0.3s ease-out',
|
|
1982
|
-
'@keyframes slideIn': {
|
|
1983
|
-
from: {
|
|
1984
|
-
transform: 'translateX(100%)',
|
|
1985
|
-
opacity: 0,
|
|
1986
|
-
},
|
|
1987
|
-
to: {
|
|
1988
|
-
transform: 'translateX(0)',
|
|
1989
|
-
opacity: 1,
|
|
1990
|
-
},
|
|
1991
|
-
},
|
|
1992
|
-
}}
|
|
1993
|
-
>
|
|
1994
|
-
<Typography variant="body2">{notification}</Typography>
|
|
1995
|
-
</Box>
|
|
1996
|
-
</Portal>
|
|
1997
|
-
))}
|
|
1998
|
-
</Box>
|
|
1999
|
-
);
|
|
2000
|
-
};
|
|
2001
|
-
|
|
2002
|
-
return <IntegrationComponent />;
|
|
2003
|
-
},
|
|
2004
|
-
play: async ({ canvasElement }) => {
|
|
2005
|
-
const canvas = within(canvasElement);
|
|
2006
|
-
|
|
2007
|
-
// Test notification system
|
|
2008
|
-
const addNotificationBtn = canvas.getByTestId('add-notification');
|
|
2009
|
-
await userEvent.click(addNotificationBtn);
|
|
2010
|
-
|
|
2011
|
-
await waitFor(() => {
|
|
2012
|
-
expect(document.querySelector('[data-testid="notification-0"]')).toBeInTheDocument();
|
|
2013
|
-
});
|
|
2014
|
-
|
|
2015
|
-
// Test dropdown
|
|
2016
|
-
const dropdownBtn = canvas.getByTestId('show-dropdown');
|
|
2017
|
-
await userEvent.click(dropdownBtn);
|
|
2018
|
-
|
|
2019
|
-
await waitFor(() => {
|
|
2020
|
-
expect(document.querySelector('[data-testid="dropdown-content"]')).toBeInTheDocument();
|
|
2021
|
-
});
|
|
2022
|
-
|
|
2023
|
-
// Select an option from dropdown
|
|
2024
|
-
const option1 = document.querySelector(
|
|
2025
|
-
'[data-testid="dropdown-option-option-1"]',
|
|
2026
|
-
) as HTMLElement;
|
|
2027
|
-
await userEvent.click(option1);
|
|
2028
|
-
|
|
2029
|
-
await waitFor(() => {
|
|
2030
|
-
expect(document.querySelector('[data-testid="dropdown-content"]')).not.toBeInTheDocument();
|
|
2031
|
-
// Should trigger a new notification
|
|
2032
|
-
expect(document.querySelector('[data-testid="notification-1"]')).toBeInTheDocument();
|
|
2033
|
-
});
|
|
2034
|
-
|
|
2035
|
-
// Test modal (highest z-index)
|
|
2036
|
-
const modalBtn = canvas.getByTestId('show-modal');
|
|
2037
|
-
await userEvent.click(modalBtn);
|
|
2038
|
-
|
|
2039
|
-
await waitFor(() => {
|
|
2040
|
-
expect(document.querySelector('[data-testid="modal-backdrop"]')).toBeInTheDocument();
|
|
2041
|
-
expect(document.querySelector('[data-testid="modal-content"]')).toBeInTheDocument();
|
|
2042
|
-
});
|
|
2043
|
-
|
|
2044
|
-
// Test modal interaction
|
|
2045
|
-
const modalInput = document.querySelector(
|
|
2046
|
-
'[data-testid="modal-content"] input',
|
|
2047
|
-
) as HTMLInputElement;
|
|
2048
|
-
await userEvent.type(modalInput, 'Test input in modal');
|
|
2049
|
-
|
|
2050
|
-
expect(modalInput.value).toBe('Test input in modal');
|
|
2051
|
-
|
|
2052
|
-
// Confirm modal
|
|
2053
|
-
const confirmBtn = document.querySelector('[data-testid="modal-confirm"]') as HTMLButtonElement;
|
|
2054
|
-
await userEvent.click(confirmBtn);
|
|
2055
|
-
|
|
2056
|
-
await waitFor(() => {
|
|
2057
|
-
expect(document.querySelector('[data-testid="modal-content"]')).not.toBeInTheDocument();
|
|
2058
|
-
// Should trigger another notification
|
|
2059
|
-
expect(document.querySelector('[data-testid="notification-2"]')).toBeInTheDocument();
|
|
2060
|
-
});
|
|
2061
|
-
|
|
2062
|
-
// Test tooltip (requires hover simulation)
|
|
2063
|
-
const tooltipBtn = canvas.getByTestId('show-tooltip');
|
|
2064
|
-
await userEvent.hover(tooltipBtn);
|
|
2065
|
-
|
|
2066
|
-
await waitFor(() => {
|
|
2067
|
-
expect(document.querySelector('[data-testid="tooltip-content"]')).toBeInTheDocument();
|
|
2068
|
-
});
|
|
2069
|
-
|
|
2070
|
-
await userEvent.unhover(tooltipBtn);
|
|
2071
|
-
|
|
2072
|
-
await waitFor(() => {
|
|
2073
|
-
expect(document.querySelector('[data-testid="tooltip-content"]')).not.toBeInTheDocument();
|
|
2074
|
-
});
|
|
2075
|
-
|
|
2076
|
-
// Verify multiple notifications stack properly
|
|
2077
|
-
await userEvent.click(addNotificationBtn);
|
|
2078
|
-
await userEvent.click(addNotificationBtn);
|
|
2079
|
-
|
|
2080
|
-
await waitFor(() => {
|
|
2081
|
-
// Should have multiple notifications stacked
|
|
2082
|
-
const notifications = document.querySelectorAll('[data-testid^="notification-"]');
|
|
2083
|
-
expect(notifications.length).toBeGreaterThan(3);
|
|
2084
|
-
});
|
|
2085
|
-
},
|
|
2086
|
-
};
|