Skip to content
Source
Sign inSign up

layout.test.ts

224 lines8.0 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import {
4 alignListPaneToHinge,
5 deriveLayout,
6 deriveThreadArrangementSizing,
7 deriveThreadWorkLogSizing,
8 deriveWorkspacePaneLayout,
9 nativeInspectorColumnWidth,
10 isPrimarySidebarPinned,
11} from "./layout";
12​
13describe("thread arrangement text sizing", () => {
14 it("fits two title lines and section labels with combined app and system text scaling", () => {
15 const sizing = deriveThreadArrangementSizing({ baseFontSize: 22, fontScale: 1.3 });
16 expect(sizing.rowHeight).toBeGreaterThanOrEqual(2 * 32 * 1.3 + 10);
17 expect(sizing.headerHeight).toBeGreaterThanOrEqual(26 * 1.3 + 24);
18 expect(deriveThreadArrangementSizing({ baseFontSize: 16, fontScale: 1 })).toEqual({
19 rowHeight: 56,
20 headerHeight: 48,
21 });
22 });
23});
24​
25describe("thread work-log text sizing", () => {
26 it.each([
27 { baseFontSize: 16, fontScale: 1.25, estimatedRowHeight: 28 },
28 { baseFontSize: 22, fontScale: 2, estimatedRowHeight: 52 },
29 ])(
30 "measures accessibility text instead of locking it to the estimate: %j",
31 ({ estimatedRowHeight, ...settings }) => {
32 expect(deriveThreadWorkLogSizing(settings)).toMatchObject({
33 estimatedRowHeight,
34 fixedRowHeight: undefined,
35 });
36 },
37 );
38​
39 it("invalidates native text measurements even when the minimum row height is unchanged", () => {
40 const original = deriveThreadWorkLogSizing({ baseFontSize: 16, fontScale: 1 });
41 for (const settings of [
42 { baseFontSize: 16, fontScale: 1.1 },
43 { baseFontSize: 17, fontScale: 1 },
44 ]) {
45 const resized = deriveThreadWorkLogSizing(settings);
46 expect(resized.estimatedRowHeight).toBe(original.estimatedRowHeight);
47 expect(resized.textSizeKey).not.toBe(original.textSizeKey);
48 }
49 });
50});
51​
52describe("native workspace sizing", () => {
53 it.each([
54 { width: 669, height: 951, hasHinge: true, nativeWorkspace: true, overlay: true },
55 { width: 951, height: 669, hasHinge: true, nativeWorkspace: true, overlay: false },
56 { width: 669, height: 951, hasHinge: false, nativeWorkspace: true, overlay: false },
57 { width: 669, height: 951, hasHinge: true, nativeWorkspace: false, overlay: false },
58 ])(
59 "overlays only a native unfolded portrait Duo sidebar: %j",
60 ({ overlay, nativeWorkspace, ...size }) => {
61 const nativeMetrics = {
62 ...size,
63 horizontalSizeClass: "regular" as const,
64 verticalBarEdge: "none" as const,
65 safeArea: { top: 24, bottom: 20, left: 0, right: 0 },
66 reservedRegions: [],
67 };
68 const layout = deriveLayout({ ...size, nativeMetrics, nativeWorkspace });
69 expect(layout.variant === "medium").toBe(overlay);
70 for (const primarySidebarPreferredVisible of [true, false]) {
71 expect(
72 deriveWorkspacePaneLayout({
73 layout,
74 viewportWidth: size.width,
75 primarySidebarPreferredVisible,
76 auxiliaryPanePreferredVisible: false,
77 }).contentPaneWidth,
78 ).toBe(
79 !overlay && primarySidebarPreferredVisible
80 ? size.width - (layout.listPaneWidth ?? 0)
81 : size.width,
82 );
83 }
84 },
85 );
86​
87 it("keeps the detail mounted in a narrow iPad window while UIKit still has two columns", () => {
88 const nativeMetrics = {
89 width: 715,
90 height: 1013.5,
91 hasHinge: false,
92 horizontalSizeClass: "regular" as const,
93 verticalBarEdge: "none" as const,
94 safeArea: { top: 32, bottom: 20, left: 0, right: 0 },
95 reservedRegions: [],
96 };
97 expect(deriveLayout({ ...nativeMetrics, nativeMetrics, nativeWorkspace: true })).toMatchObject({
98 usesSplitView: true,
99 listPaneWidth: 280,
100 });
101 });
102​
103 it.each([
104 { width: 600, horizontalSizeClass: "regular" as const, split: true },
105 { width: 599, horizontalSizeClass: "regular" as const, split: false },
106 { width: 715, horizontalSizeClass: "compact" as const, split: false },
107 ])("respects UIKit collapse and native column minimums: %j", ({ split, ...size }) => {
108 const nativeMetrics = {
109 ...size,
110 height: 800,
111 verticalBarEdge: "none" as const,
112 safeArea: { top: 32, bottom: 20, left: 0, right: 0 },
113 reservedRegions: [],
114 };
115 expect(
116 deriveLayout({ ...nativeMetrics, nativeMetrics, nativeWorkspace: true }).usesSplitView,
117 ).toBe(split);
118 });
119});
120​
121describe("width classes", () => {
122 it.each([
123 { width: 599, height: 900, variant: "compact" },
124 { width: 600, height: 900, variant: "medium" },
125 { width: 719, height: 900, variant: "medium" },
126 { width: 720, height: 900, variant: "expanded" },
127 { width: 900, height: 599, variant: "compact" },
128 ] as const)("classes a window without native columns by size: %j", ({ variant, ...size }) => {
129 expect(deriveLayout(size).variant).toBe(variant);
130 });
131});
132​
133describe("isPrimarySidebarPinned", () => {
134 // regression: #20406 sidebar toggle works on iPad and iPhone Duo
135 it("keeps compact native roots pinned so search cannot hide the sidebar for a later split", () => {
136 expect(
137 isPrimarySidebarPinned({ pathname: "/", nativeWorkspace: true, usesSplitView: false }),
138 ).toBe(true);
139 });
140});
141​
142describe("deriveWorkspacePaneLayout", () => {
143 it.each([
144 { width: 1_194, height: 834, horizontalSafeArea: 0, fraction: 0.4, suppressed: false },
145 { width: 1_032, height: 1_376, horizontalSafeArea: 0, fraction: 0.4, suppressed: true },
146 // The unfolded Duo's system bar leaves no room for the sidebar beside even the narrowest inspector.
147 { width: 951, height: 669, horizontalSafeArea: 76, fraction: 0, suppressed: true },
148 ])(
149 "steps the sidebar aside for the native inspector only when the page gets too narrow ($width wide)",
150 ({ width, height, horizontalSafeArea, fraction, suppressed }) => {
151 const layout = deriveLayout({ width, height });
152 for (const primarySidebarPreferredVisible of [true, false]) {
153 expect(
154 deriveWorkspacePaneLayout({
155 layout,
156 viewportWidth: width,
157 horizontalSafeArea,
158 nativeWorkspace: true,
159 primarySidebarPreferredVisible,
160 auxiliaryPanePreferredVisible: true,
161 nativeInspectorWidth: nativeInspectorColumnWidth({
162 viewportWidth: width,
163 horizontalSafeArea,
164 fraction,
165 }),
166 }),
167 ).toMatchObject({
168 primarySidebarVisible: primarySidebarPreferredVisible && !suppressed,
169 primarySidebarSuppressedByAuxiliary: primarySidebarPreferredVisible && suppressed,
170 auxiliaryPaneVisible: true,
171 });
172 }
173 },
174 );
175​
176 it.each([
177 { viewportWidth: 1_376, horizontalSafeArea: 0 },
178 { viewportWidth: 951, horizontalSafeArea: 76 },
179 ])(
180 "never widens the native inspector past the page's minimum ($viewportWidth wide)",
181 ({ viewportWidth, horizontalSafeArea }) => {
182 const width = nativeInspectorColumnWidth({ viewportWidth, horizontalSafeArea, fraction: 1 });
183 expect(viewportWidth - horizontalSafeArea - width).toBeGreaterThanOrEqual(320);
184 },
185 );
186​
187 it("keeps an explicitly hidden thread sidebar hidden when the file inspector is visible", () => {
188 const layout = deriveLayout({ width: 1_024, height: 1_366 });
189​
190 expect(
191 deriveWorkspacePaneLayout({
192 layout,
193 viewportWidth: 1_024,
194 primarySidebarPreferredVisible: false,
195 auxiliaryPanePreferredVisible: true,
196 }),
197 ).toMatchObject({
198 primarySidebarVisible: false,
199 primarySidebarSuppressedByAuxiliary: false,
200 auxiliaryPaneVisible: true,
201 });
202 });
203});
204​
205describe("alignListPaneToHinge", () => {
206 const layout = deriveLayout({ width: 852, height: 880 });
207​
208 it("keeps the default split when either side of the hinge would be too narrow", () => {
209 const narrowList = {
210 layout,
211 hinge: { x: 300, width: 0 },
212 viewportWidth: 852,
213 leadingWidth: 96,
214 };
215 const narrowDetail = {
216 layout,
217 hinge: { x: 600, width: 0 },
218 viewportWidth: 852,
219 leadingWidth: 0,
220 };
221 expect(alignListPaneToHinge(narrowList)).toBe(layout);
222 expect(alignListPaneToHinge(narrowDetail)).toBe(layout);
223 });
224});