@sovereignfs/ui 0.45.0 → 0.83.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/dist/Accordion.module.css +1 -1
- package/dist/Alert.module.css +1 -1
- package/dist/AppsLauncher.module.css +96 -0
- package/dist/Avatar.module.css +1 -1
- package/dist/Badge.module.css +30 -7
- package/dist/BalanceChip.module.css +1 -1
- package/dist/Button.module.css +18 -2
- package/dist/Card.module.css +1 -1
- package/dist/CardTile.module.css +181 -0
- package/dist/CodeTextarea.module.css +1 -1
- package/dist/Collapsible.module.css +1 -1
- package/dist/ColorPicker.module.css +132 -0
- package/dist/Combobox.module.css +2 -2
- package/dist/Command.module.css +1 -1
- package/dist/ConfirmDialog.module.css +2 -2
- package/dist/ConsentPrompt.module.css +21 -0
- package/dist/DatePicker.module.css +1 -1
- package/dist/Dialog.module.css +130 -21
- package/dist/Drawer.module.css +36 -7
- package/dist/Header.module.css +62 -0
- package/dist/HeaderFooterLayout.module.css +21 -0
- package/dist/IconPicker.module.css +1 -1
- package/dist/Input.module.css +1 -1
- package/dist/Kbd.module.css +1 -1
- package/dist/LegalLinks.module.css +24 -0
- package/dist/Markdown.module.css +73 -0
- package/dist/Menu.module.css +1 -0
- package/dist/Message.module.css +1 -1
- package/dist/MessageScroller.module.css +12 -1
- package/dist/MobileAppsDrawer.module.css +82 -0
- package/dist/MobileFooter.module.css +73 -0
- package/dist/MobileHeader.module.css +76 -0
- package/dist/NavList.module.css +130 -0
- package/dist/NavTabs.module.css +1 -1
- package/dist/NotificationsPanel.module.css +258 -0
- package/dist/OverlayHeader.module.css +1 -1
- package/dist/PageContainer.module.css +82 -0
- package/dist/PageHeader.module.css +54 -0
- package/dist/PageLayout.module.css +51 -0
- package/dist/Popover.module.css +1 -1
- package/dist/QuantityStepper.module.css +1 -1
- package/dist/Resizable.module.css +3 -3
- package/dist/RootLayout.module.css +12 -0
- package/dist/SegmentedControl.module.css +1 -1
- package/dist/Select.module.css +1 -1
- package/dist/Sheet.module.css +4 -1
- package/dist/SplitPane.module.css +4 -4
- package/dist/StatusBadge.module.css +1 -1
- package/dist/StepStrip.module.css +53 -0
- package/dist/SuggestionInput.module.css +2 -2
- package/dist/SwipableMobileCarousel.module.css +64 -0
- package/dist/SwipableMobileCarouselDots.module.css +64 -0
- package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/dist/SwipeStack.module.css +101 -0
- package/dist/SystemBanner.module.css +10 -0
- package/dist/Table.module.css +2 -2
- package/dist/Tabs.module.css +1 -1
- package/dist/TagInput.module.css +2 -2
- package/dist/Textarea.module.css +9 -1
- package/dist/ThreeColumnLayout.module.css +35 -0
- package/dist/Toast.module.css +62 -3
- package/dist/Tooltip.module.css +1 -1
- package/dist/UserMenu.module.css +90 -0
- package/dist/index.d.ts +1680 -30
- package/dist/index.js +4618 -1443
- package/dist/tokens/primitives.css +36 -0
- package/dist/tokens/semantic.css +32 -0
- package/dist/tokens/theme-presets.css +33 -0
- package/package.json +32 -12
- package/src/__tests__/package-json.test.ts +8 -0
- package/src/components/Accordion/Accordion.module.css +1 -1
- package/src/components/Alert/Alert.module.css +1 -1
- package/src/components/AppsLauncher/AppsLauncher.module.css +96 -0
- package/src/components/AppsLauncher/AppsLauncher.stories.tsx +58 -0
- package/src/components/AppsLauncher/AppsLauncher.tsx +117 -0
- package/src/components/AppsLauncher/__tests__/AppsLauncher.test.tsx +52 -0
- package/src/components/Avatar/Avatar.module.css +1 -1
- package/src/components/Badge/Badge.module.css +30 -7
- package/src/components/Badge/Badge.stories.tsx +16 -4
- package/src/components/Badge/Badge.tsx +8 -11
- package/src/components/Badge/__tests__/Badge.test.tsx +4 -2
- package/src/components/BalanceChip/BalanceChip.module.css +1 -1
- package/src/components/Button/Button.module.css +18 -2
- package/src/components/Card/Card.module.css +1 -1
- package/src/components/CardTile/CardTile.module.css +181 -0
- package/src/components/CardTile/CardTile.stories.tsx +86 -0
- package/src/components/CardTile/CardTile.tsx +128 -0
- package/src/components/CardTile/__tests__/CardTile.test.tsx +56 -0
- package/src/components/CodeTextarea/CodeTextarea.module.css +1 -1
- package/src/components/Collapsible/Collapsible.module.css +1 -1
- package/src/components/ColorPicker/ColorPicker.module.css +132 -0
- package/src/components/ColorPicker/ColorPicker.stories.tsx +80 -0
- package/src/components/ColorPicker/ColorPicker.tsx +157 -0
- package/src/components/ColorPicker/__tests__/ColorPicker.test.tsx +117 -0
- package/src/components/Combobox/Combobox.module.css +2 -2
- package/src/components/Command/Command.module.css +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.module.css +2 -2
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +28 -2
- package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +71 -3
- package/src/components/ConsentPrompt/ConsentPrompt.module.css +21 -0
- package/src/components/ConsentPrompt/ConsentPrompt.stories.tsx +31 -0
- package/src/components/ConsentPrompt/ConsentPrompt.tsx +66 -0
- package/src/components/ContextMenu/ContextMenu.tsx +1 -1
- package/src/components/DatePicker/DatePicker.module.css +1 -1
- package/src/components/DeviceOnlyGate/DeviceOnlyGate.tsx +58 -0
- package/src/components/DeviceOnlyGate/__tests__/DeviceOnlyGate.test.tsx +45 -0
- package/src/components/DeviceStorageKeyGate/DeviceStorageKeyGate.tsx +100 -0
- package/src/components/DeviceStorageKeyGate/__tests__/DeviceStorageKeyGate.test.tsx +119 -0
- package/src/components/Dialog/Dialog.module.css +130 -21
- package/src/components/Dialog/Dialog.stories.tsx +179 -2
- package/src/components/Dialog/Dialog.tsx +137 -94
- package/src/components/Dialog/__tests__/Dialog.test.tsx +190 -9
- package/src/components/DragHandleRow/DragHandleRow.tsx +2 -21
- package/src/components/Drawer/Drawer.module.css +36 -7
- package/src/components/Drawer/Drawer.stories.tsx +44 -0
- package/src/components/Drawer/Drawer.tsx +27 -66
- package/src/components/Drawer/__tests__/Drawer.test.tsx +55 -6
- package/src/components/GripIcon/GripIcon.tsx +27 -0
- package/src/components/Header/Header.module.css +62 -0
- package/src/components/Header/Header.stories.tsx +452 -0
- package/src/components/Header/Header.tsx +102 -0
- package/src/components/Header/__tests__/Header.test.tsx +104 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.module.css +21 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.stories.tsx +111 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.tsx +54 -0
- package/src/components/HeaderFooterLayout/__tests__/HeaderFooterLayout.test.tsx +58 -0
- package/src/components/Icon/icons/arrow-left-right.tsx +23 -0
- package/src/components/Icon/icons/bold.tsx +20 -0
- package/src/components/Icon/icons/calendar.tsx +4 -4
- package/src/components/Icon/icons/carrot.tsx +3 -3
- package/src/components/Icon/icons/circle-chevron-left.tsx +21 -0
- package/src/components/Icon/icons/code.tsx +21 -0
- package/src/components/Icon/icons/copy.tsx +21 -0
- package/src/components/Icon/icons/credit-card.tsx +21 -0
- package/src/components/Icon/icons/database.tsx +22 -0
- package/src/components/Icon/icons/download.tsx +22 -0
- package/src/components/Icon/icons/file-text.tsx +24 -0
- package/src/components/Icon/icons/folder-closed.tsx +21 -0
- package/src/components/Icon/icons/folder-open.tsx +20 -0
- package/src/components/Icon/icons/folder.tsx +20 -0
- package/src/components/Icon/icons/folders.tsx +21 -0
- package/src/components/Icon/icons/hat-glasses.tsx +24 -0
- package/src/components/Icon/icons/history.tsx +22 -0
- package/src/components/Icon/icons/inbox.tsx +21 -0
- package/src/components/Icon/icons/index.ts +169 -1
- package/src/components/Icon/icons/italic.tsx +22 -0
- package/src/components/Icon/icons/layers.tsx +22 -0
- package/src/components/Icon/icons/layout-dashboard.tsx +23 -0
- package/src/components/Icon/icons/layout-grid.tsx +23 -0
- package/src/components/Icon/icons/link.tsx +21 -0
- package/src/components/Icon/icons/list-ordered.tsx +25 -0
- package/src/components/Icon/icons/list.tsx +25 -0
- package/src/components/Icon/icons/luggage.tsx +24 -0
- package/src/components/Icon/icons/map-pin.tsx +21 -0
- package/src/components/Icon/icons/menu.tsx +22 -0
- package/src/components/Icon/icons/minus.tsx +20 -0
- package/src/components/Icon/icons/paint-bucket.tsx +23 -0
- package/src/components/Icon/icons/panel-left.tsx +21 -0
- package/src/components/Icon/icons/pin.tsx +21 -0
- package/src/components/Icon/icons/rectangle-ellipsis.tsx +23 -0
- package/src/components/Icon/icons/refresh-cw.tsx +23 -0
- package/src/components/Icon/icons/rotate-cw.tsx +21 -0
- package/src/components/Icon/icons/route.tsx +22 -0
- package/src/components/Icon/icons/send.tsx +21 -0
- package/src/components/Icon/icons/share-2.tsx +24 -0
- package/src/components/Icon/icons/sheet.tsx +24 -0
- package/src/components/Icon/icons/smartphone.tsx +21 -0
- package/src/components/Icon/icons/square-kanban.tsx +23 -0
- package/src/components/Icon/icons/table.tsx +23 -0
- package/src/components/Icon/icons/user-round-plus.tsx +23 -0
- package/src/components/Icon/icons/users.tsx +23 -0
- package/src/components/IconPicker/IconPicker.module.css +1 -1
- package/src/components/Input/Input.module.css +1 -1
- package/src/components/Input/Input.tsx +8 -2
- package/src/components/Kbd/Kbd.module.css +1 -1
- package/src/components/LegalLinks/LegalLinks.module.css +24 -0
- package/src/components/LegalLinks/LegalLinks.stories.tsx +22 -0
- package/src/components/LegalLinks/LegalLinks.tsx +40 -0
- package/src/components/Markdown/Markdown.module.css +73 -0
- package/src/components/Markdown/Markdown.stories.tsx +40 -0
- package/src/components/Markdown/Markdown.tsx +305 -0
- package/src/components/Markdown/__tests__/Markdown.test.tsx +144 -0
- package/src/components/Menu/Menu.module.css +1 -0
- package/src/components/Menu/Menu.tsx +72 -22
- package/src/components/Menu/__tests__/Menu.test.tsx +30 -0
- package/src/components/Menubar/Menubar.tsx +1 -1
- package/src/components/Message/Message.module.css +1 -1
- package/src/components/MessageScroller/MessageScroller.module.css +12 -1
- package/src/components/MessageScroller/MessageScroller.tsx +12 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +82 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.tsx +78 -0
- package/src/components/MobileAppsDrawer/__tests__/MobileAppsDrawer.test.tsx +80 -0
- package/src/components/MobileFooter/MobileFooter.module.css +73 -0
- package/src/components/MobileFooter/MobileFooter.stories.tsx +47 -0
- package/src/components/MobileFooter/MobileFooter.tsx +122 -0
- package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +129 -0
- package/src/components/MobileHeader/MobileHeader.module.css +76 -0
- package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
- package/src/components/MobileHeader/MobileHeader.tsx +90 -0
- package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +113 -0
- package/src/components/NavList/NavList.module.css +130 -0
- package/src/components/NavList/NavList.tsx +136 -0
- package/src/components/NavList/__tests__/NavList.test.tsx +109 -0
- package/src/components/NavTabs/NavTabs.module.css +1 -1
- package/src/components/NavTabs/NavTabs.tsx +39 -11
- package/src/components/NotificationsPanel/NotificationsPanel.module.css +258 -0
- package/src/components/NotificationsPanel/NotificationsPanel.stories.tsx +61 -0
- package/src/components/NotificationsPanel/NotificationsPanel.tsx +187 -0
- package/src/components/NotificationsPanel/__tests__/NotificationsPanel.test.tsx +78 -0
- package/src/components/OfflineGate/OfflineGate.tsx +44 -0
- package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
- package/src/components/OverlayHeader/OverlayHeader.module.css +1 -1
- package/src/components/OverlayHeader/OverlayHeader.tsx +19 -4
- package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +18 -0
- package/src/components/PageContainer/PageContainer.module.css +82 -0
- package/src/components/PageContainer/PageContainer.tsx +64 -0
- package/src/components/PageContainer/__tests__/PageContainer.test.tsx +70 -0
- package/src/components/PageHeader/PageHeader.module.css +54 -0
- package/src/components/PageHeader/PageHeader.tsx +157 -2
- package/src/components/PageHeader/__tests__/PageHeader.test.tsx +103 -0
- package/src/components/PageLayout/PageLayout.module.css +51 -0
- package/src/components/PageLayout/PageLayout.stories.tsx +73 -0
- package/src/components/PageLayout/PageLayout.tsx +51 -0
- package/src/components/PageLayout/__tests__/PageLayout.test.tsx +34 -0
- package/src/components/Popover/Popover.module.css +1 -1
- package/src/components/Popover/Popover.tsx +86 -4
- package/src/components/Popover/__tests__/Popover.test.tsx +165 -0
- package/src/components/QuantityStepper/QuantityStepper.module.css +1 -1
- package/src/components/Resizable/Resizable.module.css +3 -3
- package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
- package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
- package/src/components/RootLayout/RootLayout.module.css +12 -0
- package/src/components/RootLayout/RootLayout.stories.tsx +125 -0
- package/src/components/RootLayout/RootLayout.tsx +185 -0
- package/src/components/RootLayout/__tests__/RootLayout.test.tsx +161 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +1 -1
- package/src/components/Select/Select.module.css +1 -1
- package/src/components/Sheet/Sheet.module.css +4 -1
- package/src/components/Sheet/Sheet.tsx +14 -56
- package/src/components/SplitPane/SplitPane.module.css +4 -4
- package/src/components/StatusBadge/StatusBadge.module.css +1 -1
- package/src/components/StepStrip/StepStrip.module.css +53 -0
- package/src/components/StepStrip/StepStrip.tsx +74 -0
- package/src/components/SuggestionInput/SuggestionInput.module.css +2 -2
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +64 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +243 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlide.tsx +35 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody.tsx +42 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter.tsx +18 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader.tsx +21 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +214 -0
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarouselDevGuard.test.tsx +61 -0
- package/src/components/SwipableMobileCarousel/context.ts +13 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +64 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +73 -0
- package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
- package/src/components/SwipeStack/SwipeStack.module.css +101 -0
- package/src/components/SwipeStack/SwipeStack.tsx +199 -0
- package/src/components/SwipeStack/SwipeStackCard.tsx +22 -0
- package/src/components/SwipeStack/__tests__/SwipeStack.test.tsx +169 -0
- package/src/components/SystemBanner/SystemBanner.module.css +10 -0
- package/src/components/Table/Table.module.css +2 -2
- package/src/components/Tabs/Tabs.module.css +1 -1
- package/src/components/TagInput/TagInput.module.css +2 -2
- package/src/components/Textarea/Textarea.module.css +9 -1
- package/src/components/Textarea/Textarea.tsx +28 -5
- package/src/components/Textarea/__tests__/Textarea.test.tsx +23 -1
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.module.css +35 -0
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.stories.tsx +152 -0
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.tsx +87 -0
- package/src/components/ThreeColumnLayout/__tests__/ThreeColumnLayout.test.tsx +78 -0
- package/src/components/Toast/Toast.module.css +62 -3
- package/src/components/Toast/Toast.stories.tsx +1 -1
- package/src/components/Tooltip/Tooltip.module.css +1 -1
- package/src/components/UserMenu/UserMenu.module.css +90 -0
- package/src/components/UserMenu/UserMenu.stories.tsx +49 -0
- package/src/components/UserMenu/UserMenu.tsx +132 -0
- package/src/components/UserMenu/__tests__/UserMenu.test.tsx +77 -0
- package/src/hooks/__tests__/useCarouselRouteSync.test.tsx +126 -0
- package/src/hooks/__tests__/useIsOffline.test.tsx +53 -0
- package/src/hooks/__tests__/useOfflineTileState.test.tsx +69 -0
- package/src/hooks/__tests__/usePublishShellChromeHeight.test.tsx +118 -0
- package/src/hooks/__tests__/useReorderSensors.test.ts +26 -0
- package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
- package/src/hooks/__tests__/useSnapCarousel.test.tsx +175 -0
- package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
- package/src/hooks/__tests__/useSwipeStack.test.tsx +155 -0
- package/src/hooks/index.ts +27 -0
- package/src/hooks/useCarouselRouteSync.ts +92 -0
- package/src/hooks/useIsOffline.ts +41 -0
- package/src/hooks/useOfflineTileState.ts +45 -0
- package/src/hooks/usePublishShellChromeHeight.ts +96 -0
- package/src/hooks/useReorderSensors.ts +88 -0
- package/src/hooks/useResponsiveLayout.ts +36 -0
- package/src/hooks/useSnapCarousel.ts +284 -0
- package/src/hooks/useSwipeReveal.ts +107 -0
- package/src/hooks/useSwipeStack.ts +235 -0
- package/src/index.ts +124 -3
- package/src/overlay-shell.ts +159 -0
- package/src/stories/DesignSystemOverview.stories.tsx +933 -6
- package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
- package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
- package/src/stories/GripIcon.stories.tsx +29 -0
- package/src/stories/InteractionHooks.stories.tsx +483 -0
- package/src/stories/LayoutSystem.stories.tsx +639 -0
- package/src/stories/MenuEntries.stories.tsx +86 -0
- package/src/stories/MobilePatterns.stories.tsx +247 -2
- package/src/stories/NavList.stories.tsx +147 -0
- package/src/stories/NavTabs.stories.tsx +18 -0
- package/src/stories/OfflineGate.stories.tsx +42 -0
- package/src/stories/PageContainer.stories.tsx +82 -0
- package/src/stories/PageHeader.stories.tsx +65 -1
- package/src/stories/ResponsiveSurface.stories.tsx +47 -0
- package/src/stories/StepStrip.stories.tsx +76 -0
- package/src/stories/SwipableMobileCarousel.stories.tsx +362 -0
- package/src/stories/SwipeStack.stories.tsx +200 -0
- package/src/stories/Textarea.stories.tsx +12 -0
- package/src/stories/TokenGallery.stories.tsx +115 -0
- package/src/tokens/primitives.css +36 -0
- package/src/tokens/semantic.css +32 -0
- package/src/tokens/theme-presets.css +33 -0
- package/src/tokens/theme-presets.ts +72 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { cleanup, render } from '@testing-library/react';
|
|
4
|
+
import { useOfflineTileState } from '../useOfflineTileState';
|
|
5
|
+
|
|
6
|
+
function Harness({
|
|
7
|
+
offline,
|
|
8
|
+
deviceOnlyAvailable,
|
|
9
|
+
}: {
|
|
10
|
+
offline: 'offline-first' | 'device-only' | undefined;
|
|
11
|
+
deviceOnlyAvailable: boolean;
|
|
12
|
+
}) {
|
|
13
|
+
const state = useOfflineTileState(offline, deviceOnlyAvailable);
|
|
14
|
+
return <span data-testid="out">{String(state)}</span>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('useOfflineTileState', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.stubGlobal('navigator', { ...window.navigator, onLine: true });
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.unstubAllGlobals();
|
|
23
|
+
cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('is null for a no-tier app while online', () => {
|
|
27
|
+
const { getByTestId } = render(<Harness offline={undefined} deviceOnlyAvailable={false} />);
|
|
28
|
+
expect(getByTestId('out').textContent).toBe('null');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('connectivity-dims a no-tier app while offline', () => {
|
|
32
|
+
vi.stubGlobal('navigator', { ...window.navigator, onLine: false });
|
|
33
|
+
const { getByTestId } = render(<Harness offline={undefined} deviceOnlyAvailable={false} />);
|
|
34
|
+
expect(getByTestId('out').textContent).toBe('connectivity-dimmed');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('never dims an offline-first app, online or offline', () => {
|
|
38
|
+
const online = render(<Harness offline="offline-first" deviceOnlyAvailable={false} />);
|
|
39
|
+
expect(online.getByTestId('out').textContent).toBe('null');
|
|
40
|
+
cleanup();
|
|
41
|
+
|
|
42
|
+
vi.stubGlobal('navigator', { ...window.navigator, onLine: false });
|
|
43
|
+
const offline = render(<Harness offline="offline-first" deviceOnlyAvailable={false} />);
|
|
44
|
+
expect(offline.getByTestId('out').textContent).toBe('null');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('capability-restricts a device-only app when unavailable, regardless of connectivity', () => {
|
|
48
|
+
const online = render(<Harness offline="device-only" deviceOnlyAvailable={false} />);
|
|
49
|
+
expect(online.getByTestId('out').textContent).toBe('capability-restricted');
|
|
50
|
+
cleanup();
|
|
51
|
+
|
|
52
|
+
vi.stubGlobal('navigator', { ...window.navigator, onLine: false });
|
|
53
|
+
const offline = render(<Harness offline="device-only" deviceOnlyAvailable={false} />);
|
|
54
|
+
expect(offline.getByTestId('out').textContent).toBe('capability-restricted');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('is not restricted for a device-only app when available, and not dimmed offline either', () => {
|
|
58
|
+
const online = render(<Harness offline="device-only" deviceOnlyAvailable={true} />);
|
|
59
|
+
expect(online.getByTestId('out').textContent).toBe('null');
|
|
60
|
+
cleanup();
|
|
61
|
+
|
|
62
|
+
// Availability is a capability signal, not a connectivity one — a
|
|
63
|
+
// device-only app that IS available behaves like offline-first once the
|
|
64
|
+
// capability question is settled.
|
|
65
|
+
vi.stubGlobal('navigator', { ...window.navigator, onLine: false });
|
|
66
|
+
const offline = render(<Harness offline="device-only" deviceOnlyAvailable={true} />);
|
|
67
|
+
expect(offline.getByTestId('out').textContent).toBe('null');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
4
|
+
import { useRef } from 'react';
|
|
5
|
+
import { usePublishShellChromeHeight } from '../usePublishShellChromeHeight';
|
|
6
|
+
|
|
7
|
+
function Harness() {
|
|
8
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
9
|
+
usePublishShellChromeHeight(ref, '--sv-shell-footer-height');
|
|
10
|
+
return <div ref={ref} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function HeaderHarness() {
|
|
14
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
15
|
+
usePublishShellChromeHeight(ref, '--sv-shell-header-height');
|
|
16
|
+
return <div ref={ref} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stubHeight(height: number) {
|
|
20
|
+
return vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
21
|
+
height,
|
|
22
|
+
width: 0,
|
|
23
|
+
top: 0,
|
|
24
|
+
left: 0,
|
|
25
|
+
right: 0,
|
|
26
|
+
bottom: 0,
|
|
27
|
+
x: 0,
|
|
28
|
+
y: 0,
|
|
29
|
+
toJSON() {},
|
|
30
|
+
} as DOMRect);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('usePublishShellChromeHeight', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup();
|
|
36
|
+
document.getElementById('sv-app-shell')?.remove();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('re-measures on window resize', () => {
|
|
41
|
+
const shell = document.createElement('div');
|
|
42
|
+
shell.id = 'sv-app-shell';
|
|
43
|
+
document.body.appendChild(shell);
|
|
44
|
+
const rectSpy = stubHeight(50);
|
|
45
|
+
|
|
46
|
+
render(<Harness />);
|
|
47
|
+
expect(shell.style.getPropertyValue('--sv-shell-footer-height')).toBe('50px');
|
|
48
|
+
|
|
49
|
+
rectSpy.mockReturnValue({
|
|
50
|
+
height: 80,
|
|
51
|
+
width: 0,
|
|
52
|
+
top: 0,
|
|
53
|
+
left: 0,
|
|
54
|
+
right: 0,
|
|
55
|
+
bottom: 0,
|
|
56
|
+
x: 0,
|
|
57
|
+
y: 0,
|
|
58
|
+
toJSON() {},
|
|
59
|
+
} as DOMRect);
|
|
60
|
+
act(() => window.dispatchEvent(new Event('resize')));
|
|
61
|
+
|
|
62
|
+
expect(shell.style.getPropertyValue('--sv-shell-footer-height')).toBe('80px');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('removes its resize listener on unmount', () => {
|
|
66
|
+
const shell = document.createElement('div');
|
|
67
|
+
shell.id = 'sv-app-shell';
|
|
68
|
+
document.body.appendChild(shell);
|
|
69
|
+
stubHeight(50);
|
|
70
|
+
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
71
|
+
|
|
72
|
+
const { unmount } = render(<Harness />);
|
|
73
|
+
unmount();
|
|
74
|
+
|
|
75
|
+
expect(removeSpy).toHaveBeenCalledWith('resize', expect.any(Function));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('is a no-op when #sv-app-shell does not exist', () => {
|
|
79
|
+
stubHeight(50);
|
|
80
|
+
expect(() => render(<Harness />)).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('also mirrors --sv-shell-header-height onto --sv-dialog-inset-top on :root', () => {
|
|
84
|
+
const shell = document.createElement('div');
|
|
85
|
+
shell.id = 'sv-app-shell';
|
|
86
|
+
document.body.appendChild(shell);
|
|
87
|
+
stubHeight(69);
|
|
88
|
+
|
|
89
|
+
render(<HeaderHarness />);
|
|
90
|
+
|
|
91
|
+
expect(shell.style.getPropertyValue('--sv-shell-header-height')).toBe('69px');
|
|
92
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('69px');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('removes --sv-dialog-inset-top from :root on unmount', () => {
|
|
96
|
+
const shell = document.createElement('div');
|
|
97
|
+
shell.id = 'sv-app-shell';
|
|
98
|
+
document.body.appendChild(shell);
|
|
99
|
+
stubHeight(69);
|
|
100
|
+
|
|
101
|
+
const { unmount } = render(<HeaderHarness />);
|
|
102
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('69px');
|
|
103
|
+
|
|
104
|
+
unmount();
|
|
105
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('never touches --sv-dialog-inset-top for the footer variable', () => {
|
|
109
|
+
const shell = document.createElement('div');
|
|
110
|
+
shell.id = 'sv-app-shell';
|
|
111
|
+
document.body.appendChild(shell);
|
|
112
|
+
stubHeight(50);
|
|
113
|
+
|
|
114
|
+
render(<Harness />);
|
|
115
|
+
|
|
116
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { shouldHandleDndEvent } from '../useReorderSensors';
|
|
4
|
+
|
|
5
|
+
describe('shouldHandleDndEvent', () => {
|
|
6
|
+
it('refuses an element nested inside a data-no-dnd ancestor', () => {
|
|
7
|
+
const wrapper = document.createElement('div');
|
|
8
|
+
wrapper.setAttribute('data-no-dnd', '');
|
|
9
|
+
const child = document.createElement('button');
|
|
10
|
+
wrapper.appendChild(child);
|
|
11
|
+
document.body.appendChild(wrapper);
|
|
12
|
+
|
|
13
|
+
expect(shouldHandleDndEvent(child)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('allows an element with no data-no-dnd ancestor', () => {
|
|
17
|
+
const el = document.createElement('div');
|
|
18
|
+
document.body.appendChild(el);
|
|
19
|
+
|
|
20
|
+
expect(shouldHandleDndEvent(el)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('allows a non-Element target', () => {
|
|
24
|
+
expect(shouldHandleDndEvent(null)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
4
|
+
import { useResponsiveLayout } from '../useResponsiveLayout';
|
|
5
|
+
|
|
6
|
+
// Same matchMedia mocking approach as useIsMobile.test.tsx, since this hook
|
|
7
|
+
// is a thin wrapper over it and must not diverge in SSR/breakpoint behavior.
|
|
8
|
+
let changeHandler: ((e: MediaQueryListEvent) => void) | null = null;
|
|
9
|
+
let matches = false;
|
|
10
|
+
let lastQuery = '';
|
|
11
|
+
|
|
12
|
+
function installMatchMedia() {
|
|
13
|
+
changeHandler = null;
|
|
14
|
+
vi.stubGlobal(
|
|
15
|
+
'matchMedia',
|
|
16
|
+
vi.fn().mockImplementation((query: string) => {
|
|
17
|
+
lastQuery = query;
|
|
18
|
+
return {
|
|
19
|
+
matches,
|
|
20
|
+
media: query,
|
|
21
|
+
addEventListener: (_: string, cb: (e: MediaQueryListEvent) => void) => {
|
|
22
|
+
changeHandler = cb;
|
|
23
|
+
},
|
|
24
|
+
removeEventListener: vi.fn(),
|
|
25
|
+
addListener: vi.fn(),
|
|
26
|
+
removeListener: vi.fn(),
|
|
27
|
+
dispatchEvent: vi.fn(),
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Harness({ breakpointPx }: { breakpointPx?: number }) {
|
|
34
|
+
const { isMobile, value } = useResponsiveLayout({
|
|
35
|
+
web: 'web-tree',
|
|
36
|
+
mobile: 'mobile-tree',
|
|
37
|
+
breakpointPx,
|
|
38
|
+
});
|
|
39
|
+
return (
|
|
40
|
+
<span data-testid="out" data-is-mobile={String(isMobile)}>
|
|
41
|
+
{value}
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('useResponsiveLayout', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
matches = false;
|
|
49
|
+
installMatchMedia();
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.unstubAllGlobals();
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('defaults to the web value before/without a mobile match (SSR-safe default)', () => {
|
|
57
|
+
matches = false;
|
|
58
|
+
installMatchMedia();
|
|
59
|
+
const { getByTestId } = render(<Harness />);
|
|
60
|
+
expect(getByTestId('out').textContent).toBe('web-tree');
|
|
61
|
+
expect(getByTestId('out').dataset.isMobile).toBe('false');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('picks the mobile value once matchMedia reports a match', () => {
|
|
65
|
+
matches = true;
|
|
66
|
+
installMatchMedia();
|
|
67
|
+
const { getByTestId } = render(<Harness />);
|
|
68
|
+
expect(getByTestId('out').textContent).toBe('mobile-tree');
|
|
69
|
+
expect(getByTestId('out').dataset.isMobile).toBe('true');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('swaps live when the viewport crosses the breakpoint', () => {
|
|
73
|
+
matches = false;
|
|
74
|
+
installMatchMedia();
|
|
75
|
+
const { getByTestId } = render(<Harness />);
|
|
76
|
+
expect(getByTestId('out').textContent).toBe('web-tree');
|
|
77
|
+
act(() => changeHandler?.({ matches: true } as MediaQueryListEvent));
|
|
78
|
+
expect(getByTestId('out').textContent).toBe('mobile-tree');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('passes a custom breakpointPx straight through to useIsMobile', () => {
|
|
82
|
+
matches = false;
|
|
83
|
+
installMatchMedia();
|
|
84
|
+
render(<Harness breakpointPx={640} />);
|
|
85
|
+
expect(lastQuery).toBe('(max-width: 640px)');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { act, cleanup, fireEvent, render } from '@testing-library/react';
|
|
4
|
+
import { useSnapCarousel, type UseSnapCarouselOptions } from '../useSnapCarousel';
|
|
5
|
+
|
|
6
|
+
// jsdom doesn't implement scrollTo on elements.
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
if (!Element.prototype.scrollTo) {
|
|
10
|
+
Element.prototype.scrollTo = vi.fn();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.runOnlyPendingTimers();
|
|
16
|
+
vi.useRealTimers();
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function setLayout(el: HTMLElement, clientWidth: number, scrollLeft: number) {
|
|
21
|
+
Object.defineProperty(el, 'clientWidth', { value: clientWidth, configurable: true });
|
|
22
|
+
Object.defineProperty(el, 'scrollLeft', { value: scrollLeft, configurable: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Harness(
|
|
26
|
+
props: UseSnapCarouselOptions & { onScrollTo?: (fn: (i: number) => void) => void },
|
|
27
|
+
) {
|
|
28
|
+
const { onScrollTo, ...options } = props;
|
|
29
|
+
const { scrollRef, scrollToIndex } = useSnapCarousel(options);
|
|
30
|
+
onScrollTo?.(scrollToIndex);
|
|
31
|
+
return <div data-testid="scroller" ref={scrollRef} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('useSnapCarousel', () => {
|
|
35
|
+
it('calls onSettle with the new index after the debounce window', () => {
|
|
36
|
+
const onSettle = vi.fn();
|
|
37
|
+
const { getByTestId } = render(<Harness itemCount={3} onSettle={onSettle} debounceMs={120} />);
|
|
38
|
+
const el = getByTestId('scroller');
|
|
39
|
+
fireEvent.touchStart(el);
|
|
40
|
+
setLayout(el, 300, 300); // scrollLeft 300 / width 300 -> index 1
|
|
41
|
+
fireEvent.scroll(el);
|
|
42
|
+
expect(onSettle).not.toHaveBeenCalled();
|
|
43
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
44
|
+
expect(onSettle).toHaveBeenCalledWith(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('debounces repeated scroll events into a single settle call', () => {
|
|
48
|
+
const onSettle = vi.fn();
|
|
49
|
+
const { getByTestId } = render(<Harness itemCount={3} onSettle={onSettle} debounceMs={120} />);
|
|
50
|
+
const el = getByTestId('scroller');
|
|
51
|
+
fireEvent.touchStart(el);
|
|
52
|
+
setLayout(el, 300, 0);
|
|
53
|
+
fireEvent.scroll(el);
|
|
54
|
+
act(() => void vi.advanceTimersByTime(60));
|
|
55
|
+
setLayout(el, 300, 300);
|
|
56
|
+
fireEvent.scroll(el);
|
|
57
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
58
|
+
expect(onSettle).toHaveBeenCalledOnce();
|
|
59
|
+
expect(onSettle).toHaveBeenCalledWith(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not call onSettle again for the same index', () => {
|
|
63
|
+
const onSettle = vi.fn();
|
|
64
|
+
const { getByTestId } = render(<Harness itemCount={3} onSettle={onSettle} debounceMs={120} />);
|
|
65
|
+
const el = getByTestId('scroller');
|
|
66
|
+
fireEvent.touchStart(el);
|
|
67
|
+
setLayout(el, 300, 300);
|
|
68
|
+
fireEvent.scroll(el);
|
|
69
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
70
|
+
fireEvent.touchStart(el);
|
|
71
|
+
fireEvent.scroll(el);
|
|
72
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
73
|
+
expect(onSettle).toHaveBeenCalledOnce();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('clamps the settled index within [0, itemCount - 1]', () => {
|
|
77
|
+
const onSettle = vi.fn();
|
|
78
|
+
const { getByTestId } = render(<Harness itemCount={2} onSettle={onSettle} debounceMs={120} />);
|
|
79
|
+
const el = getByTestId('scroller');
|
|
80
|
+
fireEvent.touchStart(el);
|
|
81
|
+
setLayout(el, 300, 900); // rounds to index 3, clamps to itemCount - 1 = 1
|
|
82
|
+
fireEvent.scroll(el);
|
|
83
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
84
|
+
expect(onSettle).toHaveBeenCalledWith(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ignores a scroll event with no active gesture (drift, not a settle)', () => {
|
|
88
|
+
// Regression test: reproduced live via a user screen recording — a
|
|
89
|
+
// slide's content changing size while still loading can nudge
|
|
90
|
+
// scrollLeft enough to round to a different index. Previously any such
|
|
91
|
+
// stray `scroll` event was enough to fire a second, contradicting
|
|
92
|
+
// onSettle, silently "auto-swiping" the carousel back to the wrong
|
|
93
|
+
// slide with no further touch input.
|
|
94
|
+
const onSettle = vi.fn();
|
|
95
|
+
const { getByTestId } = render(<Harness itemCount={10} onSettle={onSettle} debounceMs={120} />);
|
|
96
|
+
const el = getByTestId('scroller');
|
|
97
|
+
fireEvent.touchStart(el);
|
|
98
|
+
setLayout(el, 300, 1800); // settles cleanly on index 6
|
|
99
|
+
fireEvent.scroll(el);
|
|
100
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
101
|
+
expect(onSettle).toHaveBeenCalledExactlyOnceWith(6);
|
|
102
|
+
|
|
103
|
+
// Well outside the grace window and no further touch/wheel/pointer
|
|
104
|
+
// activity — this scroll event is drift, not a resettle.
|
|
105
|
+
act(() => void vi.advanceTimersByTime(1000));
|
|
106
|
+
setLayout(el, 300, 1620); // would round to index 5
|
|
107
|
+
fireEvent.scroll(el);
|
|
108
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
109
|
+
expect(onSettle).toHaveBeenCalledExactlyOnceWith(6);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('still settles on long-running momentum/snap-correction scroll with no further touch input', () => {
|
|
113
|
+
// Regression test for a bug in the FIRST version of the drift fix above:
|
|
114
|
+
// gating on "was there a touch/wheel/pointer event within a fixed
|
|
115
|
+
// debounceMs * 2 slack of settle-check time" dropped real settles once
|
|
116
|
+
// native momentum/snap-correction ran longer than that slack — which it
|
|
117
|
+
// routinely does for a fast flick. That left activeIndex stuck on the
|
|
118
|
+
// old slide while the container had already scrolled to the new one,
|
|
119
|
+
// and since the mount window is keyed off activeIndex, the slide the
|
|
120
|
+
// user was actually looking at could get unmounted — a blank carousel,
|
|
121
|
+
// worse than the bug being fixed. The chain stays trusted for as long as
|
|
122
|
+
// scroll events keep arriving without a debounceMs-or-longer gap,
|
|
123
|
+
// regardless of how long that run lasts or how few touch events back it.
|
|
124
|
+
const onSettle = vi.fn();
|
|
125
|
+
const { getByTestId } = render(<Harness itemCount={10} onSettle={onSettle} debounceMs={120} />);
|
|
126
|
+
const el = getByTestId('scroller');
|
|
127
|
+
fireEvent.touchStart(el);
|
|
128
|
+
setLayout(el, 300, 1700);
|
|
129
|
+
fireEvent.scroll(el); // starts the run — trusted (touchstart just happened)
|
|
130
|
+
fireEvent.touchEnd(el);
|
|
131
|
+
// Momentum keeps producing scroll events, each within debounceMs of the
|
|
132
|
+
// last (so the run never goes quiet), for far longer than any fixed
|
|
133
|
+
// slack window would tolerate — no further touch input at all.
|
|
134
|
+
for (const scrollLeft of [1720, 1740, 1760, 1780, 1800]) {
|
|
135
|
+
act(() => void vi.advanceTimersByTime(80));
|
|
136
|
+
setLayout(el, 300, scrollLeft);
|
|
137
|
+
fireEvent.scroll(el);
|
|
138
|
+
}
|
|
139
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
140
|
+
expect(onSettle).toHaveBeenCalledExactlyOnceWith(6);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('settles correctly on two independent real swipes fired back-to-back', () => {
|
|
144
|
+
// Regression test: two genuine swipes with essentially no gap between
|
|
145
|
+
// them (an impatient fast double-swipe) must not corrupt or drop either
|
|
146
|
+
// settle — each is its own trusted run.
|
|
147
|
+
const onSettle = vi.fn();
|
|
148
|
+
const { getByTestId } = render(<Harness itemCount={10} onSettle={onSettle} debounceMs={120} />);
|
|
149
|
+
const el = getByTestId('scroller');
|
|
150
|
+
fireEvent.touchStart(el);
|
|
151
|
+
setLayout(el, 300, 300);
|
|
152
|
+
fireEvent.scroll(el);
|
|
153
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
154
|
+
expect(onSettle).toHaveBeenCalledExactlyOnceWith(1);
|
|
155
|
+
|
|
156
|
+
fireEvent.touchStart(el);
|
|
157
|
+
setLayout(el, 300, 600);
|
|
158
|
+
fireEvent.scroll(el);
|
|
159
|
+
act(() => void vi.advanceTimersByTime(120));
|
|
160
|
+
expect(onSettle).toHaveBeenCalledTimes(2);
|
|
161
|
+
expect(onSettle).toHaveBeenLastCalledWith(2);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('scrollToIndex scrolls the container to the target slide', () => {
|
|
165
|
+
let scrollToIndex: ((index: number) => void) | undefined;
|
|
166
|
+
const { getByTestId } = render(
|
|
167
|
+
<Harness itemCount={3} onScrollTo={(fn) => (scrollToIndex = fn)} />,
|
|
168
|
+
);
|
|
169
|
+
const el = getByTestId('scroller');
|
|
170
|
+
setLayout(el, 400, 0);
|
|
171
|
+
const spy = vi.spyOn(el, 'scrollTo');
|
|
172
|
+
scrollToIndex?.(2);
|
|
173
|
+
expect(spy).toHaveBeenCalledWith({ left: 800, behavior: 'smooth' });
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { cleanup, fireEvent, render } from '@testing-library/react';
|
|
4
|
+
import { useSwipeReveal, type UseSwipeRevealOptions } from '../useSwipeReveal';
|
|
5
|
+
|
|
6
|
+
// jsdom doesn't implement pointer capture — stub it as a no-op so
|
|
7
|
+
// `e.currentTarget.setPointerCapture(...)` doesn't throw.
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
if (!HTMLElement.prototype.setPointerCapture) {
|
|
10
|
+
HTMLElement.prototype.setPointerCapture = vi.fn();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
|
|
16
|
+
function Harness(props: UseSwipeRevealOptions) {
|
|
17
|
+
const { rowRef, handlers } = useSwipeReveal(props);
|
|
18
|
+
return (
|
|
19
|
+
<div data-testid="row" ref={rowRef} {...handlers}>
|
|
20
|
+
row
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pointer(x: number, y: number) {
|
|
26
|
+
return { clientX: x, clientY: y, pointerId: 1 };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('useSwipeReveal', () => {
|
|
30
|
+
it('calls onOpen when a horizontal drag crosses the halfway point', () => {
|
|
31
|
+
const onOpen = vi.fn();
|
|
32
|
+
const onClose = vi.fn();
|
|
33
|
+
const { getByTestId } = render(
|
|
34
|
+
<Harness revealWidth={100} open={false} onOpen={onOpen} onClose={onClose} />,
|
|
35
|
+
);
|
|
36
|
+
const el = getByTestId('row');
|
|
37
|
+
fireEvent.pointerDown(el, pointer(0, 0));
|
|
38
|
+
fireEvent.pointerMove(el, pointer(-60, 0));
|
|
39
|
+
fireEvent.pointerUp(el, pointer(-60, 0));
|
|
40
|
+
expect(onOpen).toHaveBeenCalledOnce();
|
|
41
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('calls onClose when the drag does not cross the halfway point', () => {
|
|
45
|
+
const onOpen = vi.fn();
|
|
46
|
+
const onClose = vi.fn();
|
|
47
|
+
const { getByTestId } = render(
|
|
48
|
+
<Harness revealWidth={100} open={false} onOpen={onOpen} onClose={onClose} />,
|
|
49
|
+
);
|
|
50
|
+
const el = getByTestId('row');
|
|
51
|
+
fireEvent.pointerDown(el, pointer(0, 0));
|
|
52
|
+
fireEvent.pointerMove(el, pointer(-20, 0));
|
|
53
|
+
fireEvent.pointerUp(el, pointer(-20, 0));
|
|
54
|
+
expect(onClose).toHaveBeenCalledOnce();
|
|
55
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('locks to vertical and ignores a mostly-vertical drag', () => {
|
|
59
|
+
const onOpen = vi.fn();
|
|
60
|
+
const onClose = vi.fn();
|
|
61
|
+
const { getByTestId } = render(
|
|
62
|
+
<Harness revealWidth={100} open={false} onOpen={onOpen} onClose={onClose} />,
|
|
63
|
+
);
|
|
64
|
+
const el = getByTestId('row');
|
|
65
|
+
fireEvent.pointerDown(el, pointer(0, 0));
|
|
66
|
+
fireEvent.pointerMove(el, pointer(2, 60));
|
|
67
|
+
fireEvent.pointerUp(el, pointer(2, 60));
|
|
68
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
69
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('does nothing when disabled', () => {
|
|
73
|
+
const onOpen = vi.fn();
|
|
74
|
+
const onClose = vi.fn();
|
|
75
|
+
const { getByTestId } = render(
|
|
76
|
+
<Harness revealWidth={100} open={false} onOpen={onOpen} onClose={onClose} disabled />,
|
|
77
|
+
);
|
|
78
|
+
const el = getByTestId('row');
|
|
79
|
+
fireEvent.pointerDown(el, pointer(0, 0));
|
|
80
|
+
fireEvent.pointerMove(el, pointer(-60, 0));
|
|
81
|
+
fireEvent.pointerUp(el, pointer(-60, 0));
|
|
82
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
83
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('closes from an already-open state when dragged back past halfway', () => {
|
|
87
|
+
const onOpen = vi.fn();
|
|
88
|
+
const onClose = vi.fn();
|
|
89
|
+
const { getByTestId } = render(
|
|
90
|
+
<Harness revealWidth={100} open onOpen={onOpen} onClose={onClose} />,
|
|
91
|
+
);
|
|
92
|
+
const el = getByTestId('row');
|
|
93
|
+
fireEvent.pointerDown(el, pointer(0, 0));
|
|
94
|
+
fireEvent.pointerMove(el, pointer(60, 0));
|
|
95
|
+
fireEvent.pointerUp(el, pointer(60, 0));
|
|
96
|
+
expect(onClose).toHaveBeenCalledOnce();
|
|
97
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('ignores pointerup without a preceding pointerdown', () => {
|
|
101
|
+
const onOpen = vi.fn();
|
|
102
|
+
const onClose = vi.fn();
|
|
103
|
+
const { getByTestId } = render(
|
|
104
|
+
<Harness revealWidth={100} open={false} onOpen={onOpen} onClose={onClose} />,
|
|
105
|
+
);
|
|
106
|
+
fireEvent.pointerUp(getByTestId('row'), pointer(-60, 0));
|
|
107
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
108
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|