1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 constrainAuxiliaryPaneWidth,
5 deriveCenteredContentHorizontalPadding,
6 deriveFileInspectorPaneLayout,
7 deriveLayout,
8 deriveThreadFeedInitialContentInset,
9 deriveThreadWorkLogSizing,
10 deriveWorkspacePaneLayout,
11 SPLIT_LAYOUT_MIN_HEIGHT,
12 SPLIT_LAYOUT_MIN_WIDTH,
13} from "./layout";
14
15describe("thread work-log text sizing", () => {
16 it.each([11, 16, 22])(
17 "keeps exact compact rows at base size %i without OS enlargement",
18 (baseFontSize) => {
19 expect(deriveThreadWorkLogSizing({ baseFontSize, fontScale: 1 })).toMatchObject({
20 estimatedRowHeight: 28,
21 fixedRowHeight: 28,
22 });
23 },
24 );
25
26 it.each([
27 { baseFontSize: 16, fontScale: 1.25, estimatedRowHeight: 28 },
28 { baseFontSize: 16, fontScale: 2, estimatedRowHeight: 38 },
29 { baseFontSize: 22, fontScale: 2, estimatedRowHeight: 52 },
30 ])(
31 "measures accessibility text instead of locking it to the estimate: %j",
32 ({ estimatedRowHeight, ...settings }) => {
33 expect(deriveThreadWorkLogSizing(settings)).toMatchObject({
34 estimatedRowHeight,
35 fixedRowHeight: undefined,
36 });
37 },
38 );
39
40 it("invalidates native text measurements even when the minimum row height is unchanged", () => {
41 const original = deriveThreadWorkLogSizing({ baseFontSize: 16, fontScale: 1 });
42 for (const settings of [
43 { baseFontSize: 16, fontScale: 1.1 },
44 { baseFontSize: 17, fontScale: 1 },
45 ]) {
46 const resized = deriveThreadWorkLogSizing(settings);
47 expect(resized.estimatedRowHeight).toBe(original.estimatedRowHeight);
48 expect(resized.textSizeKey).not.toBe(original.textSizeKey);
49 }
50 });
51});
52
53describe("deriveThreadFeedInitialContentInset", () => {
54 it("seeds Android scroll math with the composer overlay estimate", () => {
55 expect(
56 deriveThreadFeedInitialContentInset({
57 platform: "android",
58 usesNativeAutomaticInsets: false,
59 bottomContentInset: 174,
60 }),
61 ).toEqual({ bottom: 174 });
62 });
63
64 it("does not double native iOS insets", () => {
65 expect(
66 deriveThreadFeedInitialContentInset({
67 platform: "ios",
68 usesNativeAutomaticInsets: true,
69 bottomContentInset: 174,
70 }),
71 ).toBeUndefined();
72 });
73});
74
75describe("resizable pane constraints", () => {
76 it("preserves a useful main pane while constraining a trailing pane", () => {
77 expect(constrainAuxiliaryPaneWidth({ preferredWidth: 440, availableWidth: 1_100 })).toBe(440);
78 expect(constrainAuxiliaryPaneWidth({ preferredWidth: 440, availableWidth: 900 })).toBe(340);
79 expect(constrainAuxiliaryPaneWidth({ preferredWidth: 100, availableWidth: 1_100 })).toBe(260);
80 });
81});
82
83describe("deriveCenteredContentHorizontalPadding", () => {
84 it("keeps the minimum padding while the viewport fits the reading width", () => {
85 expect(
86 deriveCenteredContentHorizontalPadding({
87 viewportWidth: 744,
88 maxContentWidth: 960,
89 minimumPadding: 20,
90 }),
91 ).toBe(20);
92 });
93
94 it("centers only the content inside a wider full-width scroll host", () => {
95 expect(
96 deriveCenteredContentHorizontalPadding({
97 viewportWidth: 1_032,
98 maxContentWidth: 960,
99 minimumPadding: 20,
100 }),
101 ).toBe(56);
102 });
103
104 it("supports unconstrained compact content", () => {
105 expect(
106 deriveCenteredContentHorizontalPadding({
107 viewportWidth: 430,
108 maxContentWidth: null,
109 minimumPadding: 16,
110 }),
111 ).toBe(16);
112 });
113});
114
115describe("deriveLayout", () => {
116 it.each([
117 { name: "small iPhone portrait", width: 375, height: 667 },
118 { name: "large iPhone portrait", width: 430, height: 932 },
119 { name: "small iPhone landscape", width: 667, height: 375 },
120 { name: "large iPhone landscape", width: 932, height: 430 },
121 { name: "short wide window", width: 1_024, height: 599 },
122 { name: "narrow tall window", width: 719, height: 1_024 },
123 ])("keeps a $name in the compact shell", ({ width, height }) => {
124 expect(deriveLayout({ width, height })).toEqual({
125 variant: "compact",
126 usesSplitView: false,
127 listPaneWidth: null,
128 shellPadding: 0,
129 });
130 });
131
132 it.each([
133 { name: "small tablet portrait", width: 744, height: 1_133 },
134 { name: "tablet landscape", width: 1_024, height: 768 },
135 { name: "large resizable window", width: 1_366, height: 1_024 },
136 { name: "foldable-sized window", width: 800, height: 700 },
137 ])("uses the split shell for a $name", ({ width, height }) => {
138 expect(deriveLayout({ width, height })).toMatchObject({
139 variant: "split",
140 usesSplitView: true,
141 });
142 });
143
144 it("switches only after both space requirements are met", () => {
145 expect(
146 deriveLayout({ width: SPLIT_LAYOUT_MIN_WIDTH, height: SPLIT_LAYOUT_MIN_HEIGHT }).variant,
147 ).toBe("split");
148 expect(
149 deriveLayout({ width: SPLIT_LAYOUT_MIN_WIDTH - 1, height: SPLIT_LAYOUT_MIN_HEIGHT }).variant,
150 ).toBe("compact");
151 expect(
152 deriveLayout({ width: SPLIT_LAYOUT_MIN_WIDTH, height: SPLIT_LAYOUT_MIN_HEIGHT - 1 }).variant,
153 ).toBe("compact");
154 });
155
156 it("keeps the sidebar within usable native-column bounds", () => {
157 expect(deriveLayout({ width: 720, height: 1_000 }).listPaneWidth).toBe(280);
158 expect(deriveLayout({ width: 1_024, height: 768 }).listPaneWidth).toBe(328);
159 expect(deriveLayout({ width: 1_600, height: 1_000 }).listPaneWidth).toBe(380);
160 });
161});
162
163describe("deriveWorkspacePaneLayout", () => {
164 it("keeps the auxiliary pane out of a standard iPad detail column", () => {
165 const layout = deriveLayout({ width: 1_194, height: 834 });
166
167 expect(
168 deriveWorkspacePaneLayout({
169 layout,
170 viewportWidth: 1_194,
171 primarySidebarPreferredVisible: true,
172 auxiliaryPanePreferredVisible: true,
173 }),
174 ).toEqual({
175 primarySidebarVisible: true,
176 primarySidebarSuppressedByAuxiliary: false,
177 contentPaneWidth: 814,
178 supportsAuxiliaryPane: false,
179 auxiliaryPaneVisible: false,
180 auxiliaryPaneWidth: null,
181 });
182 });
183
184 it("offers an auxiliary pane when maximizing a standard iPad landscape window", () => {
185 const layout = deriveLayout({ width: 1_194, height: 834 });
186
187 expect(
188 deriveWorkspacePaneLayout({
189 layout,
190 viewportWidth: 1_194,
191 primarySidebarPreferredVisible: false,
192 auxiliaryPanePreferredVisible: true,
193 }),
194 ).toEqual({
195 primarySidebarVisible: false,
196 primarySidebarSuppressedByAuxiliary: false,
197 contentPaneWidth: 1_194,
198 supportsAuxiliaryPane: true,
199 auxiliaryPaneVisible: true,
200 auxiliaryPaneWidth: 320,
201 });
202 });
203
204 it("prioritizes a trailing file inspector over the thread sidebar at medium widths", () => {
205 const layout = deriveLayout({ width: 1_024, height: 1_366 });
206
207 expect(
208 deriveWorkspacePaneLayout({
209 layout,
210 viewportWidth: 1_024,
211 primarySidebarPreferredVisible: true,
212 auxiliaryPanePreferredVisible: true,
213 auxiliaryPaneRole: "inspector",
214 }),
215 ).toEqual({
216 primarySidebarVisible: false,
217 primarySidebarSuppressedByAuxiliary: true,
218 contentPaneWidth: 1_024,
219 supportsAuxiliaryPane: true,
220 auxiliaryPaneVisible: true,
221 auxiliaryPaneWidth: 260,
222 });
223 });
224
225 it("keeps threads, content, and the file inspector visible in a large landscape window", () => {
226 const layout = deriveLayout({ width: 1_366, height: 1_024 });
227
228 expect(
229 deriveWorkspacePaneLayout({
230 layout,
231 viewportWidth: 1_366,
232 primarySidebarPreferredVisible: true,
233 auxiliaryPanePreferredVisible: true,
234 auxiliaryPaneRole: "inspector",
235 }),
236 ).toEqual({
237 primarySidebarVisible: true,
238 primarySidebarSuppressedByAuxiliary: false,
239 contentPaneWidth: 986,
240 supportsAuxiliaryPane: true,
241 auxiliaryPaneVisible: true,
242 auxiliaryPaneWidth: 276,
243 });
244 });
245
246 it("keeps an explicitly hidden thread sidebar hidden when the file inspector is visible", () => {
247 const layout = deriveLayout({ width: 1_024, height: 1_366 });
248
249 expect(
250 deriveWorkspacePaneLayout({
251 layout,
252 viewportWidth: 1_024,
253 primarySidebarPreferredVisible: false,
254 auxiliaryPanePreferredVisible: true,
255 auxiliaryPaneRole: "inspector",
256 }),
257 ).toMatchObject({
258 primarySidebarVisible: false,
259 primarySidebarSuppressedByAuxiliary: false,
260 auxiliaryPaneVisible: true,
261 });
262 });
263
264 it("restores the thread sidebar when the file inspector is hidden", () => {
265 const layout = deriveLayout({ width: 1_024, height: 1_366 });
266
267 expect(
268 deriveWorkspacePaneLayout({
269 layout,
270 viewportWidth: 1_024,
271 primarySidebarPreferredVisible: true,
272 auxiliaryPanePreferredVisible: false,
273 auxiliaryPaneRole: "inspector",
274 }),
275 ).toMatchObject({
276 primarySidebarVisible: true,
277 primarySidebarSuppressedByAuxiliary: false,
278 auxiliaryPaneVisible: false,
279 });
280 });
281
282 it("keeps file navigation on the native stack below the inspector breakpoint", () => {
283 const layout = deriveLayout({ width: 744, height: 1_133 });
284
285 expect(deriveFileInspectorPaneLayout({ layout, viewportWidth: 744 })).toEqual({
286 supported: false,
287 width: null,
288 });
289 expect(
290 deriveWorkspacePaneLayout({
291 layout,
292 viewportWidth: 744,
293 primarySidebarPreferredVisible: true,
294 auxiliaryPanePreferredVisible: true,
295 auxiliaryPaneRole: "inspector",
296 }),
297 ).toMatchObject({
298 primarySidebarVisible: true,
299 supportsAuxiliaryPane: false,
300 auxiliaryPaneVisible: false,
301 });
302 });
303
304 it("supports three visible columns in a sufficiently large window", () => {
305 const layout = deriveLayout({ width: 1_366, height: 1_024 });
306
307 expect(
308 deriveWorkspacePaneLayout({
309 layout,
310 viewportWidth: 1_366,
311 primarySidebarPreferredVisible: true,
312 auxiliaryPanePreferredVisible: true,
313 }),
314 ).toMatchObject({
315 primarySidebarVisible: true,
316 contentPaneWidth: 986,
317 supportsAuxiliaryPane: true,
318 auxiliaryPaneVisible: true,
319 auxiliaryPaneWidth: 276,
320 });
321 });
322
323 it("uses a preferred inspector width when all three panes still fit", () => {
324 const layout = deriveLayout({ width: 1_366, height: 1_024 });
325
326 expect(
327 deriveWorkspacePaneLayout({
328 layout,
329 viewportWidth: 1_366,
330 primarySidebarPreferredVisible: true,
331 auxiliaryPanePreferredVisible: true,
332 auxiliaryPaneRole: "inspector",
333 auxiliaryPanePreferredWidth: 420,
334 }),
335 ).toMatchObject({
336 primarySidebarVisible: true,
337 auxiliaryPaneVisible: true,
338 auxiliaryPaneWidth: 420,
339 });
340 });
341
342 it("clamps a preferred supplementary width before squeezing the main pane", () => {
343 const layout = deriveLayout({ width: 1_366, height: 1_024 });
344
345 expect(
346 deriveWorkspacePaneLayout({
347 layout,
348 viewportWidth: 1_366,
349 primarySidebarPreferredVisible: true,
350 auxiliaryPanePreferredVisible: true,
351 auxiliaryPanePreferredWidth: 460,
352 }).auxiliaryPaneWidth,
353 ).toBe(426);
354 });
355
356 it("respects a hidden auxiliary-pane preference", () => {
357 const layout = deriveLayout({ width: 1_366, height: 1_024 });
358
359 expect(
360 deriveWorkspacePaneLayout({
361 layout,
362 viewportWidth: 1_366,
363 primarySidebarPreferredVisible: true,
364 auxiliaryPanePreferredVisible: false,
365 }).auxiliaryPaneVisible,
366 ).toBe(false);
367 });
368
369 it("never exposes workspace panes in compact layouts", () => {
370 const layout = deriveLayout({ width: 430, height: 932 });
371
372 expect(
373 deriveWorkspacePaneLayout({
374 layout,
375 viewportWidth: 430,
376 primarySidebarPreferredVisible: true,
377 auxiliaryPanePreferredVisible: true,
378 }),
379 ).toMatchObject({
380 primarySidebarVisible: false,
381 supportsAuxiliaryPane: false,
382 auxiliaryPaneVisible: false,
383 auxiliaryPaneWidth: null,
384 });
385 });
386});