@blotch/dolan 22.2.3 → 22.2.5
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/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +38 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +258 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +425 -11
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +105 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +194 -50
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +570 -3
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +17 -0
- package/dist/lockfile.js +56 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +65 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +119 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +61 -6
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +27 -0
- package/dist/test-utils.js +45 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
- package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
- package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +14 -15
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +646 -2
- package/dist/ui/components/button/button.ts +443 -64
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +528 -2
- package/dist/ui/components/button-group/button-group.ts +428 -70
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +97 -61
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +14 -7
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1031 -0
- package/dist/ui/components/chart/chart.ts +66 -46
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +25 -24
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +21 -15
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +35 -29
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +142 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +752 -0
- package/dist/ui/components/dock/dock.ts +1438 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +376 -0
- package/dist/ui/components/glass/glass.ts +430 -0
- package/dist/ui/components/glass/index.ts +19 -0
- package/dist/ui/components/icon/icon-theme.test.ts +64 -0
- package/dist/ui/components/icon/icon-theme.ts +70 -16
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +17 -13
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +636 -0
- package/dist/ui/components/menu/menu-popup.ts +565 -0
- package/dist/ui/components/menu/menu.test.ts +523 -0
- package/dist/ui/components/menu/menu.ts +455 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +54 -55
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +26 -25
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +19 -12
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +151 -11
- package/dist/ui/components/sheet/sheet.ts +68 -5
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +534 -48
- package/dist/ui/components/switch/switch.ts +613 -48
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +484 -150
- package/dist/ui/components/tabs/tabs.ts +344 -149
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +16 -13
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +559 -146
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +19 -0
- package/dist/utils/analyze.js +16 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +25 -1
- package/dist/utils/installed.js +33 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +55 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +45 -0
- package/dist/utils/tailwind.js +90 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +134 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { Component, type Provider, signal, viewChildren } from '@angular/core';
|
|
2
|
+
import { Router, RouterOutlet, type Routes } from '@angular/router';
|
|
3
|
+
import {
|
|
4
|
+
fireEvent,
|
|
5
|
+
render,
|
|
6
|
+
waitForUpdate,
|
|
7
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
8
|
+
import {
|
|
9
|
+
LYNX_ELEMENTS,
|
|
10
|
+
LynxAccessibilitySettings,
|
|
11
|
+
LynxSystemInfo,
|
|
12
|
+
provideRouter,
|
|
13
|
+
} from '@blotch/angular-lynx';
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
15
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
16
|
+
import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
|
|
17
|
+
import { UiIcon } from '../icon/icon';
|
|
18
|
+
import { provideGlass } from '../glass/glass-design';
|
|
19
|
+
import { UiDock, UiDockAccessory, UiDockAction, UiDockItem } from './dock';
|
|
20
|
+
import { UiDockState } from './dock-scroll';
|
|
21
|
+
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'home-page',
|
|
24
|
+
imports: [LYNX_ELEMENTS],
|
|
25
|
+
template: `<text>Home page</text>`,
|
|
26
|
+
})
|
|
27
|
+
class HomePage {}
|
|
28
|
+
|
|
29
|
+
@Component({
|
|
30
|
+
selector: 'inbox-page',
|
|
31
|
+
imports: [LYNX_ELEMENTS],
|
|
32
|
+
template: `<text>Inbox page</text>`,
|
|
33
|
+
})
|
|
34
|
+
class InboxPage {}
|
|
35
|
+
|
|
36
|
+
@Component({
|
|
37
|
+
selector: 'search-page',
|
|
38
|
+
imports: [LYNX_ELEMENTS],
|
|
39
|
+
template: `<text>Search page</text>`,
|
|
40
|
+
})
|
|
41
|
+
class SearchPage {}
|
|
42
|
+
|
|
43
|
+
const routes: Routes = [
|
|
44
|
+
{ path: '', component: HomePage },
|
|
45
|
+
{ path: 'inbox', component: InboxPage },
|
|
46
|
+
{ path: 'profile', component: HomePage },
|
|
47
|
+
{ path: 'search', component: SearchPage },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
@Component({
|
|
51
|
+
imports: [
|
|
52
|
+
LYNX_ELEMENTS,
|
|
53
|
+
RouterOutlet,
|
|
54
|
+
UiDock,
|
|
55
|
+
UiDockAccessory,
|
|
56
|
+
UiDockAction,
|
|
57
|
+
UiDockItem,
|
|
58
|
+
UiIcon,
|
|
59
|
+
],
|
|
60
|
+
template: `
|
|
61
|
+
<router-outlet />
|
|
62
|
+
<ui-dock [class]="dockClass()" [expandLabel]="expandLabel()">
|
|
63
|
+
@if (accessory()) {
|
|
64
|
+
<view ui-dock-accessory class="flex-row">
|
|
65
|
+
<text class="text-glass-foreground">Now playing</text>
|
|
66
|
+
<ui-icon name="play" />
|
|
67
|
+
</view>
|
|
68
|
+
}
|
|
69
|
+
<ui-dock-item
|
|
70
|
+
route="/"
|
|
71
|
+
icon="home"
|
|
72
|
+
label="Home"
|
|
73
|
+
[routerLinkActiveOptions]="{ exact: true }"
|
|
74
|
+
/>
|
|
75
|
+
<ui-dock-item
|
|
76
|
+
route="/inbox"
|
|
77
|
+
icon="inbox"
|
|
78
|
+
label="Inbox"
|
|
79
|
+
[badge]="badge()"
|
|
80
|
+
[class]="inboxClass()"
|
|
81
|
+
/>
|
|
82
|
+
<ui-dock-item
|
|
83
|
+
route="/profile"
|
|
84
|
+
icon="user"
|
|
85
|
+
label="Profile"
|
|
86
|
+
[queryParams]="{ tab: 'posts' }"
|
|
87
|
+
/>
|
|
88
|
+
@if (action()) {
|
|
89
|
+
<ui-dock-action route="/search" icon="search" label="Search" />
|
|
90
|
+
}
|
|
91
|
+
</ui-dock>
|
|
92
|
+
`,
|
|
93
|
+
})
|
|
94
|
+
class DockHost {
|
|
95
|
+
readonly dockClass = signal('');
|
|
96
|
+
readonly expandLabel = signal('Show tab bar');
|
|
97
|
+
readonly accessory = signal(true);
|
|
98
|
+
readonly action = signal(true);
|
|
99
|
+
readonly badge = signal<string | number | null | undefined>(undefined);
|
|
100
|
+
readonly inboxClass = signal('');
|
|
101
|
+
readonly items = viewChildren(UiDockItem);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A dock with only an action of the app's own, and no items.
|
|
106
|
+
*/
|
|
107
|
+
@Component({
|
|
108
|
+
imports: [LYNX_ELEMENTS, UiDock, UiDockAction],
|
|
109
|
+
template: `
|
|
110
|
+
<ui-dock>
|
|
111
|
+
<ui-dock-action
|
|
112
|
+
icon="plus"
|
|
113
|
+
label="New message"
|
|
114
|
+
(pressed)="taps.set(taps() + 1)"
|
|
115
|
+
/>
|
|
116
|
+
</ui-dock>
|
|
117
|
+
`,
|
|
118
|
+
})
|
|
119
|
+
class ActionOnlyHost {
|
|
120
|
+
readonly taps = signal(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
124
|
+
const settings = {
|
|
125
|
+
reduceMotion: signal(false),
|
|
126
|
+
reduceTransparency: signal(false),
|
|
127
|
+
increaseContrast: signal(false),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
131
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
132
|
+
const ANDROID = { platform: 'Android', osVersion: '15' };
|
|
133
|
+
|
|
134
|
+
/** The theme's light colors, as icon-theme.ts resolves them for an SVG. */
|
|
135
|
+
const PRIMARY = 'rgba(24, 24, 27, 1)';
|
|
136
|
+
const PRIMARY_FOREGROUND = 'rgba(250, 250, 250, 1)';
|
|
137
|
+
const MUTED_FOREGROUND = 'rgba(113, 113, 122, 1)';
|
|
138
|
+
const GLASS_FOREGROUND = 'rgba(9, 9, 11, 1)';
|
|
139
|
+
|
|
140
|
+
/** How wide the dock lays out in these tests, in px. */
|
|
141
|
+
const WIDTH = 370;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Runs change detection until `done` holds: a navigation takes a few ticks,
|
|
145
|
+
* and RouterLinkActive reports the result in a microtask after it.
|
|
146
|
+
*/
|
|
147
|
+
const until = async (done: () => boolean): Promise<void> => {
|
|
148
|
+
for (let i = 0; i < 20 && !done(); i++) {
|
|
149
|
+
await waitForUpdate();
|
|
150
|
+
}
|
|
151
|
+
expect(done()).toBe(true);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The platform, as `SystemInfo` reports it. Provided rather than stubbed as
|
|
156
|
+
* a global: the router's setup switches the testing library's threads, which
|
|
157
|
+
* puts back its own SystemInfo before the dock reads it.
|
|
158
|
+
*/
|
|
159
|
+
const onPlatform = (platform: { platform: string; osVersion: string }) => ({
|
|
160
|
+
provide: LynxSystemInfo,
|
|
161
|
+
useValue: platform,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const renderDock = async (
|
|
165
|
+
platform: { platform: string; osVersion: string },
|
|
166
|
+
providers: Provider[] = [],
|
|
167
|
+
) => {
|
|
168
|
+
const result = await render(DockHost, {
|
|
169
|
+
providers: [
|
|
170
|
+
provideRouter(routes),
|
|
171
|
+
onPlatform(platform),
|
|
172
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
173
|
+
...providers,
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
const { container } = result;
|
|
177
|
+
const host = result.componentRef.instance as DockHost;
|
|
178
|
+
const injector = result.componentRef.injector;
|
|
179
|
+
const router = injector.get(Router);
|
|
180
|
+
const state = injector.get(UiDockState);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* An item's host, the accessibility element named by its label.
|
|
184
|
+
*/
|
|
185
|
+
const item = (label: string) =>
|
|
186
|
+
container.querySelector<HTMLElement>(`[accessibility-label^="${label}"]`)!;
|
|
187
|
+
const isSelected = (label: string) =>
|
|
188
|
+
item(label).getAttribute('accessibility-traits') === 'button,selected';
|
|
189
|
+
/**
|
|
190
|
+
* The glass group, and the glass shapes in it.
|
|
191
|
+
*/
|
|
192
|
+
const stage = () => container.querySelector<HTMLElement>('[ui-glass-group]')!;
|
|
193
|
+
/**
|
|
194
|
+
* The dock's host, the group's parent.
|
|
195
|
+
*/
|
|
196
|
+
const dock = () => stage().parentElement!;
|
|
197
|
+
const shapes = () =>
|
|
198
|
+
Array.from(stage().children).filter((child) =>
|
|
199
|
+
child.hasAttribute('ui-glass'),
|
|
200
|
+
) as HTMLElement[];
|
|
201
|
+
const bar = () => row().parentElement!;
|
|
202
|
+
const row = () =>
|
|
203
|
+
container.querySelector<HTMLElement>('[accessibility-traits="tabbar"]')!;
|
|
204
|
+
const accessory = () =>
|
|
205
|
+
shapes().find((shape) => shape.querySelector('[ui-dock-accessory]'));
|
|
206
|
+
/**
|
|
207
|
+
* The action's glass, and the button in it.
|
|
208
|
+
*/
|
|
209
|
+
const action = () => shapes().find((shape) => shape.contains(button()));
|
|
210
|
+
const button = () =>
|
|
211
|
+
container.querySelector<HTMLElement>('[accessibility-label="Search"]');
|
|
212
|
+
const capsule = () => row().firstElementChild as HTMLElement;
|
|
213
|
+
const restore = () =>
|
|
214
|
+
bar().querySelector<HTMLElement>('[accessibility-label="Show tab bar"]');
|
|
215
|
+
const iconOf = (element: Element) =>
|
|
216
|
+
element.querySelector('svg')!.getAttribute('content') ?? '';
|
|
217
|
+
const labelOf = (label: string) =>
|
|
218
|
+
Array.from(item(label).querySelectorAll('text')).find(
|
|
219
|
+
(text) => text.textContent === label,
|
|
220
|
+
)!;
|
|
221
|
+
const page = () =>
|
|
222
|
+
Array.from(container.querySelectorAll('text'))
|
|
223
|
+
.map((text) => text.textContent ?? '')
|
|
224
|
+
.find((text) => text.endsWith(' page'));
|
|
225
|
+
/**
|
|
226
|
+
* Lays the dock out, as Lynx reports its width after the first frame.
|
|
227
|
+
*/
|
|
228
|
+
const layOut = async (width = WIDTH) => {
|
|
229
|
+
fireEvent.layoutchange(dock().firstElementChild!, {
|
|
230
|
+
detail: { width, height: 0 },
|
|
231
|
+
});
|
|
232
|
+
await waitForUpdate();
|
|
233
|
+
};
|
|
234
|
+
const minimize = async (minimized = true) => {
|
|
235
|
+
state.minimized.set(minimized);
|
|
236
|
+
await waitForUpdate();
|
|
237
|
+
};
|
|
238
|
+
await until(() => isSelected('Home'));
|
|
239
|
+
return {
|
|
240
|
+
...result,
|
|
241
|
+
host,
|
|
242
|
+
router,
|
|
243
|
+
state,
|
|
244
|
+
item,
|
|
245
|
+
isSelected,
|
|
246
|
+
dock,
|
|
247
|
+
stage,
|
|
248
|
+
shapes,
|
|
249
|
+
bar,
|
|
250
|
+
row,
|
|
251
|
+
accessory,
|
|
252
|
+
action,
|
|
253
|
+
button,
|
|
254
|
+
capsule,
|
|
255
|
+
restore,
|
|
256
|
+
iconOf,
|
|
257
|
+
labelOf,
|
|
258
|
+
page,
|
|
259
|
+
layOut,
|
|
260
|
+
minimize,
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const style = (element: HTMLElement, name: string) =>
|
|
265
|
+
element.style.getPropertyValue(name);
|
|
266
|
+
|
|
267
|
+
// Item, page and icon selectors aren't Lynx elements, so the renderer falls
|
|
268
|
+
// back to a <view> for each and warns.
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
afterEach(() => {
|
|
274
|
+
vi.restoreAllMocks();
|
|
275
|
+
settings.reduceMotion.set(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('UiDock on iOS 26 and later', () => {
|
|
279
|
+
it('floats a glass bar, the action beside it and the accessory above, in one glass group', async () => {
|
|
280
|
+
const { dock, stage, shapes, bar, accessory, action } =
|
|
281
|
+
await renderDock(IOS_26);
|
|
282
|
+
|
|
283
|
+
expect(dock().classList).toContain('absolute');
|
|
284
|
+
expect(dock().classList).toContain('bottom-0');
|
|
285
|
+
// It clears the home indicator, floating a little above it.
|
|
286
|
+
expect(dock().classList).toContain('pb-safe-2');
|
|
287
|
+
expect(stage().getAttribute('blur-effect')).toBe('glass-container');
|
|
288
|
+
expect(stage().getAttribute('spacing')).toBe('8');
|
|
289
|
+
|
|
290
|
+
// The accessory, the bar and the action are the group's glass shapes.
|
|
291
|
+
expect(shapes()).toHaveLength(3);
|
|
292
|
+
for (const shape of shapes()) {
|
|
293
|
+
expect(shape.getAttribute('blur-effect')).toBe('glass');
|
|
294
|
+
expect(shape.getAttribute('glass-interactive')).toBe('true');
|
|
295
|
+
}
|
|
296
|
+
expect(bar().classList).toContain('rounded-full');
|
|
297
|
+
expect(action()?.classList).toContain('rounded-full');
|
|
298
|
+
|
|
299
|
+
// Bottom up: the bar and the action share a row, the accessory above.
|
|
300
|
+
expect(style(stage(), 'height')).toBe('120px');
|
|
301
|
+
expect(style(bar(), 'right')).toBe('72px');
|
|
302
|
+
expect(style(bar(), 'height')).toBe('62px');
|
|
303
|
+
expect(style(action()!, 'width')).toBe('62px');
|
|
304
|
+
expect(style(action()!, 'bottom')).toBe('0px');
|
|
305
|
+
expect(style(accessory()!, 'bottom')).toBe('72px');
|
|
306
|
+
expect(style(accessory()!, 'height')).toBe('48px');
|
|
307
|
+
expect(style(accessory()!, 'left')).toBe('0px');
|
|
308
|
+
expect(style(accessory()!, 'right')).toBe('0px');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('puts the accessory and the action in their glass, and draws icons there in the glass foreground', async () => {
|
|
312
|
+
const { accessory, action, iconOf } = await renderDock(IOS_26);
|
|
313
|
+
expect(accessory()?.textContent).toContain('Now playing');
|
|
314
|
+
expect(iconOf(accessory()!)).toContain(GLASS_FOREGROUND);
|
|
315
|
+
expect(action()).toBeTruthy();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('leaves out the accessory and the action without them', async () => {
|
|
319
|
+
const { host, shapes, stage, bar, accessory, action } =
|
|
320
|
+
await renderDock(IOS_26);
|
|
321
|
+
host.accessory.set(false);
|
|
322
|
+
host.action.set(false);
|
|
323
|
+
await waitForUpdate();
|
|
324
|
+
|
|
325
|
+
expect(shapes()).toHaveLength(1);
|
|
326
|
+
expect(accessory()).toBeUndefined();
|
|
327
|
+
expect(action()).toBeUndefined();
|
|
328
|
+
expect(style(stage(), 'height')).toBe('62px');
|
|
329
|
+
expect(style(bar(), 'right')).toBe('0px');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('marks the selected item with a capsule that slides to the item tapped', async () => {
|
|
333
|
+
const { item, capsule, isSelected, page, labelOf, iconOf } =
|
|
334
|
+
await renderDock(IOS_26);
|
|
335
|
+
|
|
336
|
+
// Three items share the row, so the capsule is a third of it, and moves
|
|
337
|
+
// by its own width per item.
|
|
338
|
+
expect(style(capsule(), 'width')).toBe('33.333333333333336%');
|
|
339
|
+
expect(style(capsule(), 'transform')).toBe('translateX(0%)');
|
|
340
|
+
expect(style(capsule(), 'opacity')).toBe('0.1');
|
|
341
|
+
expect(style(capsule(), 'transition')).toContain('transform');
|
|
342
|
+
expect(labelOf('Home').classList).toContain('text-primary');
|
|
343
|
+
expect(iconOf(item('Home'))).toContain(PRIMARY);
|
|
344
|
+
expect(labelOf('Inbox').classList).toContain('text-glass-foreground');
|
|
345
|
+
expect(iconOf(item('Inbox'))).toContain(GLASS_FOREGROUND);
|
|
346
|
+
|
|
347
|
+
fireEvent.tap(item('Inbox'));
|
|
348
|
+
await until(() => page() === 'Inbox page');
|
|
349
|
+
await until(() => isSelected('Inbox'));
|
|
350
|
+
expect(style(capsule(), 'transform')).toBe('translateX(100%)');
|
|
351
|
+
expect(isSelected('Home')).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('selects the action while its route is active, and hides the capsule', async () => {
|
|
355
|
+
const { button, capsule, isSelected, page, iconOf } =
|
|
356
|
+
await renderDock(IOS_26);
|
|
357
|
+
expect(iconOf(button()!)).toContain(GLASS_FOREGROUND);
|
|
358
|
+
|
|
359
|
+
fireEvent.tap(button()!);
|
|
360
|
+
await until(() => page() === 'Search page');
|
|
361
|
+
await until(
|
|
362
|
+
() =>
|
|
363
|
+
button()!.getAttribute('accessibility-traits') === 'button,selected',
|
|
364
|
+
);
|
|
365
|
+
expect(iconOf(button()!)).toContain(PRIMARY);
|
|
366
|
+
expect(isSelected('Home')).toBe(false);
|
|
367
|
+
expect(style(capsule(), 'opacity')).toBe('0');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('reads to a screen reader as a tab bar, the current item selected', async () => {
|
|
371
|
+
const { getByRole, queryByRole } = await renderDock(IOS_26);
|
|
372
|
+
expect(getByRole('tablist')).toBeTruthy();
|
|
373
|
+
expect(getByRole('button', { name: 'Home', selected: true })).toBeTruthy();
|
|
374
|
+
expect(queryByRole('button', { name: 'Inbox', selected: true })).toBeNull();
|
|
375
|
+
expect(getByRole('button', { name: 'Search' })).toBeTruthy();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('keeps the bar whole until it knows its width, then lays the row out at it', async () => {
|
|
379
|
+
const { row, minimize, layOut, restore } = await renderDock(IOS_26);
|
|
380
|
+
expect(style(row(), 'width')).toBe('');
|
|
381
|
+
|
|
382
|
+
// Minimizing needs the width to shrink from.
|
|
383
|
+
await minimize();
|
|
384
|
+
expect(restore()).toBeNull();
|
|
385
|
+
expect(style(row(), 'opacity')).toBe('1');
|
|
386
|
+
|
|
387
|
+
await layOut();
|
|
388
|
+
// The bar's width less its padding: 370 - 62 - 10 - 2 × 4.
|
|
389
|
+
expect(style(row(), 'width')).toBe('290px');
|
|
390
|
+
expect(restore()).toBeTruthy();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('minimizes to the selected icon, with the accessory between it and the action', async () => {
|
|
394
|
+
const {
|
|
395
|
+
stage,
|
|
396
|
+
bar,
|
|
397
|
+
row,
|
|
398
|
+
accessory,
|
|
399
|
+
action,
|
|
400
|
+
restore,
|
|
401
|
+
iconOf,
|
|
402
|
+
minimize,
|
|
403
|
+
layOut,
|
|
404
|
+
state,
|
|
405
|
+
} = await renderDock(IOS_26);
|
|
406
|
+
await layOut();
|
|
407
|
+
expect(style(row(), 'transition')).toContain('opacity');
|
|
408
|
+
|
|
409
|
+
await minimize();
|
|
410
|
+
expect(style(stage(), 'height')).toBe('48px');
|
|
411
|
+
// The bar shrinks to a 48px circle at the left…
|
|
412
|
+
expect(style(bar(), 'right')).toBe(`${WIDTH - 48}px`);
|
|
413
|
+
expect(style(bar(), 'height')).toBe('48px');
|
|
414
|
+
// …the action to one at the right…
|
|
415
|
+
expect(style(action()!, 'width')).toBe('48px');
|
|
416
|
+
expect(style(action()!, 'height')).toBe('48px');
|
|
417
|
+
// …and the accessory moves down between them.
|
|
418
|
+
expect(style(accessory()!, 'left')).toBe('58px');
|
|
419
|
+
expect(style(accessory()!, 'right')).toBe('58px');
|
|
420
|
+
expect(style(accessory()!, 'bottom')).toBe('0px');
|
|
421
|
+
expect(style(accessory()!, 'height')).toBe('48px');
|
|
422
|
+
// The shapes move and resize with a transition, which the glass follows.
|
|
423
|
+
expect(style(bar(), 'transition')).toContain('right');
|
|
424
|
+
|
|
425
|
+
// The items fade out of the way, and out of a screen reader's reach.
|
|
426
|
+
expect(style(row(), 'opacity')).toBe('0');
|
|
427
|
+
expect(row().getAttribute('accessibility-elements-hidden')).toBe('true');
|
|
428
|
+
// The minimized bar is one button that brings the dock back.
|
|
429
|
+
expect(iconOf(restore()!)).toContain(PRIMARY);
|
|
430
|
+
expect(restore()!.getAttribute('accessibility-value')).toBe('Home');
|
|
431
|
+
|
|
432
|
+
fireEvent.tap(restore()!);
|
|
433
|
+
await waitForUpdate();
|
|
434
|
+
expect(state.minimized()).toBe(false);
|
|
435
|
+
expect(restore()).toBeNull();
|
|
436
|
+
expect(style(bar(), 'height')).toBe('62px');
|
|
437
|
+
expect(style(row(), 'opacity')).toBe('1');
|
|
438
|
+
expect(row().hasAttribute('accessibility-elements-hidden')).toBe(false);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('minimizes without an action or accessory, the bar alone at the left', async () => {
|
|
442
|
+
const { host, accessory, bar, minimize, layOut } = await renderDock(IOS_26);
|
|
443
|
+
host.action.set(false);
|
|
444
|
+
await waitForUpdate();
|
|
445
|
+
await layOut();
|
|
446
|
+
await minimize();
|
|
447
|
+
expect(style(bar(), 'right')).toBe(`${WIDTH - 48}px`);
|
|
448
|
+
|
|
449
|
+
host.accessory.set(true);
|
|
450
|
+
await waitForUpdate();
|
|
451
|
+
expect(style(accessory()!, 'right')).toBe('0px');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('shows the first item on the minimized bar while the action is selected', async () => {
|
|
455
|
+
const { button, restore, iconOf, minimize, layOut, page } =
|
|
456
|
+
await renderDock(IOS_26);
|
|
457
|
+
fireEvent.tap(button()!);
|
|
458
|
+
await until(() => page() === 'Search page');
|
|
459
|
+
await layOut();
|
|
460
|
+
await minimize();
|
|
461
|
+
await until(() => !restore()!.hasAttribute('accessibility-value'));
|
|
462
|
+
// The home icon, in the selected color.
|
|
463
|
+
expect(iconOf(restore()!)).toContain(PRIMARY);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('fades the minimized icon in, but not under Reduce Motion', async () => {
|
|
467
|
+
const { minimize, layOut, capsule, bar, row } = await renderDock(IOS_26);
|
|
468
|
+
const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
469
|
+
/**
|
|
470
|
+
* How many animations started: `__ElementAnimate`'s 0 starts one.
|
|
471
|
+
*/
|
|
472
|
+
const started = () =>
|
|
473
|
+
animate.mock.calls.filter(([, args]) => args[0] === 0).length;
|
|
474
|
+
await layOut();
|
|
475
|
+
await minimize();
|
|
476
|
+
expect(started()).toBe(1);
|
|
477
|
+
|
|
478
|
+
await minimize(false);
|
|
479
|
+
settings.reduceMotion.set(true);
|
|
480
|
+
await waitForUpdate();
|
|
481
|
+
await minimize();
|
|
482
|
+
expect(started()).toBe(1);
|
|
483
|
+
// Nothing slides, shrinks or fades either: it changes at once.
|
|
484
|
+
expect(style(bar(), 'transition')).toBe('none');
|
|
485
|
+
expect(style(row(), 'transition')).toBe('none');
|
|
486
|
+
expect(style(capsule(), 'transition')).toBe('none');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("uses the dock's class and expand label", async () => {
|
|
490
|
+
const { host, dock, restore, bar, minimize, layOut } =
|
|
491
|
+
await renderDock(IOS_26);
|
|
492
|
+
host.dockClass.set('px-6');
|
|
493
|
+
host.expandLabel.set('Show tabs');
|
|
494
|
+
await waitForUpdate();
|
|
495
|
+
expect(dock().classList).toContain('px-6');
|
|
496
|
+
expect(dock().classList).not.toContain('px-4');
|
|
497
|
+
|
|
498
|
+
await layOut();
|
|
499
|
+
await minimize();
|
|
500
|
+
expect(restore()).toBeNull();
|
|
501
|
+
expect(
|
|
502
|
+
bar().querySelector('[accessibility-label="Show tabs"]'),
|
|
503
|
+
).toBeTruthy();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe('UiDock on iOS 15 to 25', () => {
|
|
508
|
+
it("runs edge to edge on the bars' chrome material, over the home indicator", async () => {
|
|
509
|
+
const { dock, stage, bar, row, action, accessory, capsule } =
|
|
510
|
+
await renderDock(IOS_18);
|
|
511
|
+
|
|
512
|
+
expect(dock().classList).not.toContain('px-4');
|
|
513
|
+
expect(stage().getAttribute('blur-effect')).toBe('light');
|
|
514
|
+
expect(bar().getAttribute('blur-effect')).toBe('extra-light');
|
|
515
|
+
for (const shape of [bar(), action()!, accessory()!]) {
|
|
516
|
+
expect(shape.getAttribute('blur-material')).toBe('chrome');
|
|
517
|
+
}
|
|
518
|
+
expect(bar().classList).toContain('pb-safe');
|
|
519
|
+
expect(action()?.classList).toContain('pb-safe');
|
|
520
|
+
expect(row().classList).toContain('h-[49px]');
|
|
521
|
+
// A hairline along the top of the bar and the action.
|
|
522
|
+
expect(bar().querySelector('.bg-border')).toBeTruthy();
|
|
523
|
+
expect(action()?.querySelector('.bg-border')).toBeTruthy();
|
|
524
|
+
expect(style(bar(), 'right')).toBe('64px');
|
|
525
|
+
expect(style(action()!, 'width')).toBe('64px');
|
|
526
|
+
expect(style(accessory()!, 'left')).toBe('8px');
|
|
527
|
+
expect(style(accessory()!, 'border-radius')).toBe('12px');
|
|
528
|
+
// No capsule: the selected item takes the primary color.
|
|
529
|
+
expect(style(capsule(), 'opacity')).toBe('0');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('marks the selected item in the primary color and the others gray', async () => {
|
|
533
|
+
const { item, labelOf, iconOf, action } = await renderDock(IOS_18);
|
|
534
|
+
expect(labelOf('Home').classList).toContain('text-primary');
|
|
535
|
+
expect(iconOf(item('Home'))).toContain(PRIMARY);
|
|
536
|
+
expect(labelOf('Inbox').classList).toContain('text-muted-foreground');
|
|
537
|
+
expect(iconOf(item('Inbox'))).toContain(MUTED_FOREGROUND);
|
|
538
|
+
// The action is a tinted bar button.
|
|
539
|
+
expect(iconOf(action()!)).toContain(PRIMARY);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("doesn't minimize, as Apple's tab bars didn't", async () => {
|
|
543
|
+
const { host, stage, minimize, layOut, restore, row } =
|
|
544
|
+
await renderDock(IOS_18);
|
|
545
|
+
await layOut();
|
|
546
|
+
await minimize();
|
|
547
|
+
expect(restore()).toBeNull();
|
|
548
|
+
expect(style(row(), 'opacity')).toBe('1');
|
|
549
|
+
expect(style(row(), 'width')).toBe('');
|
|
550
|
+
|
|
551
|
+
host.accessory.set(false);
|
|
552
|
+
host.action.set(false);
|
|
553
|
+
await waitForUpdate();
|
|
554
|
+
expect(stage().getAttribute('style')).toContain('height');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('UiDock on Android', () => {
|
|
559
|
+
it('floats a Material toolbar, with an indicator behind the selected icon and a floating action button', async () => {
|
|
560
|
+
const { bar, action, accessory, item, labelOf, iconOf } =
|
|
561
|
+
await renderDock(ANDROID);
|
|
562
|
+
|
|
563
|
+
// Solid surfaces with an elevation shadow, not glass.
|
|
564
|
+
expect(bar().classList).toContain('bg-secondary');
|
|
565
|
+
expect(style(bar(), 'box-shadow')).not.toBe('');
|
|
566
|
+
expect(bar().classList).toContain('rounded-full');
|
|
567
|
+
expect(style(bar(), 'height')).toBe('64px');
|
|
568
|
+
// The button is filled with the primary color, and centered on the bar.
|
|
569
|
+
// (jsdom writes the theme's rgba(…, 1) as rgb().)
|
|
570
|
+
expect(style(action()!, 'background-color')).toBe('rgb(24, 24, 27)');
|
|
571
|
+
expect(style(action()!, 'border-radius')).toBe('16px');
|
|
572
|
+
expect(style(action()!, 'bottom')).toBe('4px');
|
|
573
|
+
expect(iconOf(action()!)).toContain(PRIMARY_FOREGROUND);
|
|
574
|
+
expect(style(accessory()!, 'border-radius')).toBe('16px');
|
|
575
|
+
|
|
576
|
+
const indicator = (label: string) =>
|
|
577
|
+
item(label).querySelector<HTMLElement>('.bg-primary')!;
|
|
578
|
+
expect(style(indicator('Home'), 'opacity')).toBe('1');
|
|
579
|
+
expect(style(indicator('Home'), 'transform')).toBe('scaleX(1)');
|
|
580
|
+
expect(iconOf(item('Home'))).toContain(PRIMARY_FOREGROUND);
|
|
581
|
+
expect(labelOf('Home').classList).toContain('text-foreground');
|
|
582
|
+
expect(labelOf('Home').classList).toContain('text-xs');
|
|
583
|
+
expect(style(indicator('Inbox'), 'opacity')).toBe('0');
|
|
584
|
+
expect(style(indicator('Inbox'), 'transform')).toBe('scaleX(0.5)');
|
|
585
|
+
expect(iconOf(item('Inbox'))).toContain(MUTED_FOREGROUND);
|
|
586
|
+
expect(labelOf('Inbox').classList).toContain('text-muted-foreground');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('widens the indicator on the item tapped, at once under Reduce Motion', async () => {
|
|
590
|
+
const { item, page, isSelected } = await renderDock(ANDROID);
|
|
591
|
+
const indicator = (label: string) =>
|
|
592
|
+
item(label).querySelector<HTMLElement>('.bg-primary')!;
|
|
593
|
+
fireEvent.tap(item('Inbox'));
|
|
594
|
+
await until(() => page() === 'Inbox page');
|
|
595
|
+
await until(() => isSelected('Inbox'));
|
|
596
|
+
expect(style(indicator('Inbox'), 'transition')).toContain('transform');
|
|
597
|
+
// It leaves the item it moves from at once.
|
|
598
|
+
expect(style(indicator('Home'), 'transition')).toBe('none');
|
|
599
|
+
|
|
600
|
+
settings.reduceMotion.set(true);
|
|
601
|
+
await waitForUpdate();
|
|
602
|
+
expect(style(indicator('Inbox'), 'transition')).toBe('none');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('slides the toolbar and the accessory away when minimized, and keeps the button', async () => {
|
|
606
|
+
const { bar, accessory, action, stage, minimize, layOut } =
|
|
607
|
+
await renderDock(ANDROID);
|
|
608
|
+
await layOut();
|
|
609
|
+
expect(style(bar(), 'transform')).toBe('translateY(0px)');
|
|
610
|
+
|
|
611
|
+
await minimize();
|
|
612
|
+
expect(style(bar(), 'transform')).toBe('translateY(128px)');
|
|
613
|
+
expect(style(accessory()!, 'transform')).toBe('translateY(196px)');
|
|
614
|
+
expect(style(action()!, 'width')).toBe('56px');
|
|
615
|
+
expect(style(stage(), 'height')).toBe('132px');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('draws glass when the app opts in', async () => {
|
|
619
|
+
const { bar } = await renderDock(ANDROID, [
|
|
620
|
+
provideGlass({ android: 'glass' }),
|
|
621
|
+
]);
|
|
622
|
+
expect(bar().getAttribute('blur-effect')).toBe('glass');
|
|
623
|
+
expect(bar().classList).not.toContain('bg-secondary');
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe('UiDockItem', () => {
|
|
628
|
+
it("navigates with the item's RouterLink options", async () => {
|
|
629
|
+
const { router, item, isSelected } = await renderDock(IOS_26);
|
|
630
|
+
fireEvent.tap(item('Profile'));
|
|
631
|
+
await until(() => isSelected('Profile'));
|
|
632
|
+
expect(router.url).toBe('/profile?tab=posts');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('emits pressed for every tap, the selected item too', async () => {
|
|
636
|
+
const { item, router, host } = await renderDock(IOS_26);
|
|
637
|
+
const pressed = vi.fn();
|
|
638
|
+
host.items()[0].pressed.subscribe(pressed);
|
|
639
|
+
fireEvent.tap(item('Home'));
|
|
640
|
+
await waitForUpdate();
|
|
641
|
+
expect(pressed).toHaveBeenCalledTimes(1);
|
|
642
|
+
expect(router.url).toBe('/');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("shows a badge over the icon, and says it after the item's name", async () => {
|
|
646
|
+
const { host, item } = await renderDock(IOS_26);
|
|
647
|
+
const badge = () =>
|
|
648
|
+
Array.from(item('Inbox').querySelectorAll('text')).find(
|
|
649
|
+
(text) => text.textContent !== 'Inbox',
|
|
650
|
+
);
|
|
651
|
+
expect(badge()).toBeUndefined();
|
|
652
|
+
|
|
653
|
+
host.badge.set(3);
|
|
654
|
+
await waitForUpdate();
|
|
655
|
+
expect(badge()?.textContent).toBe('3');
|
|
656
|
+
expect(badge()?.parentElement?.classList).toContain('bg-destructive');
|
|
657
|
+
expect(item('Inbox').getAttribute('accessibility-label')).toBe('Inbox, 3');
|
|
658
|
+
|
|
659
|
+
for (const empty of [0, '', null, undefined]) {
|
|
660
|
+
host.badge.set(empty);
|
|
661
|
+
await waitForUpdate();
|
|
662
|
+
expect(badge()).toBeUndefined();
|
|
663
|
+
expect(item('Inbox').getAttribute('accessibility-label')).toBe('Inbox');
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('shrinks an item while it is pressed', async () => {
|
|
668
|
+
const { item } = await renderDock(IOS_26);
|
|
669
|
+
const inbox = item('Inbox');
|
|
670
|
+
const scale = () => inbox.style.getPropertyValue('transform');
|
|
671
|
+
|
|
672
|
+
fireEvent.touchstart(inbox);
|
|
673
|
+
expect(scale()).toBe('scale(0.97)');
|
|
674
|
+
fireEvent.touchend(inbox);
|
|
675
|
+
expect(scale()).toBe('scale(1)');
|
|
676
|
+
fireEvent.touchstart(inbox);
|
|
677
|
+
fireEvent.touchcancel(inbox);
|
|
678
|
+
expect(scale()).toBe('scale(1)');
|
|
679
|
+
fireEvent.mousedown(inbox);
|
|
680
|
+
expect(scale()).toBe('scale(0.97)');
|
|
681
|
+
fireEvent.mouseup(inbox);
|
|
682
|
+
expect(scale()).toBe('scale(1)');
|
|
683
|
+
fireEvent.mousedown(inbox);
|
|
684
|
+
fireLynxEvent(inbox, 'mouseleave');
|
|
685
|
+
expect(scale()).toBe('scale(1)');
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("merges an item's class into its content, the given class winning", async () => {
|
|
689
|
+
const { host, item } = await renderDock(IOS_26);
|
|
690
|
+
const content = () => item('Inbox').querySelector('view')!;
|
|
691
|
+
expect(content().classList).toContain('gap-0.5');
|
|
692
|
+
|
|
693
|
+
host.inboxClass.set('gap-2');
|
|
694
|
+
await waitForUpdate();
|
|
695
|
+
expect(content().classList).toContain('gap-2');
|
|
696
|
+
expect(content().classList).not.toContain('gap-0.5');
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
describe('UiDockAction', () => {
|
|
701
|
+
it('emits pressed for an action without a route, and shrinks while pressed', async () => {
|
|
702
|
+
const { container, componentRef } = await render(ActionOnlyHost, {
|
|
703
|
+
providers: [
|
|
704
|
+
provideRouter(routes),
|
|
705
|
+
onPlatform(IOS_26),
|
|
706
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
707
|
+
],
|
|
708
|
+
});
|
|
709
|
+
const host = componentRef.instance as ActionOnlyHost;
|
|
710
|
+
const button = container.querySelector<HTMLElement>(
|
|
711
|
+
'[accessibility-label="New message"]',
|
|
712
|
+
)!;
|
|
713
|
+
fireEvent.tap(button);
|
|
714
|
+
await waitForUpdate();
|
|
715
|
+
expect(host.taps()).toBe(1);
|
|
716
|
+
|
|
717
|
+
const scale = () => button.style.getPropertyValue('transform');
|
|
718
|
+
fireEvent.touchstart(button);
|
|
719
|
+
expect(scale()).toBe('scale(0.97)');
|
|
720
|
+
fireEvent.touchend(button);
|
|
721
|
+
expect(scale()).toBe('scale(1)');
|
|
722
|
+
fireEvent.mousedown(button);
|
|
723
|
+
fireEvent.mouseup(button);
|
|
724
|
+
expect(scale()).toBe('scale(1)');
|
|
725
|
+
fireEvent.touchcancel(button);
|
|
726
|
+
fireLynxEvent(button, 'mouseleave');
|
|
727
|
+
expect(scale()).toBe('scale(1)');
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('minimizes to an empty bar with no items', async () => {
|
|
731
|
+
const { container, componentRef } = await render(ActionOnlyHost, {
|
|
732
|
+
providers: [
|
|
733
|
+
provideRouter(routes),
|
|
734
|
+
onPlatform(IOS_26),
|
|
735
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
736
|
+
],
|
|
737
|
+
});
|
|
738
|
+
fireEvent.layoutchange(
|
|
739
|
+
container.querySelector('[ui-glass-group]')!.previousElementSibling!,
|
|
740
|
+
{ detail: { width: WIDTH, height: 0 } },
|
|
741
|
+
);
|
|
742
|
+
componentRef.injector.get(UiDockState).minimized.set(true);
|
|
743
|
+
await waitForUpdate();
|
|
744
|
+
const restore = container.querySelector(
|
|
745
|
+
'[accessibility-label="Show tab bar"]',
|
|
746
|
+
)!;
|
|
747
|
+
expect(restore.querySelector('svg')?.getAttribute('content') ?? '').toBe(
|
|
748
|
+
'',
|
|
749
|
+
);
|
|
750
|
+
expect(restore.hasAttribute('accessibility-value')).toBe(false);
|
|
751
|
+
});
|
|
752
|
+
});
|