import { describe, expect, it } from "vitest"; import type { ComposerInit } from "../components/compose/Composer"; import { collapseExpanded, type ComposeWin, layoutDock, minimizeAll, minimizeWindow, openWindow, restoreWindow, toggleExpand, } from "../components/compose/windows"; const init = (draftId?: string): ComposerInit => ({ accountId: "a1", kind: "new", draftId }); let clock = 0; const tick = () => (clock += 1); /** Open `n` windows the way repeated Compose clicks would. */ function opened(n: number): ComposeWin[] { clock = 0; let wins: ComposeWin[] = []; for (let i = 1; i <= n; i += 1) wins = openWindow(wins, `c${i}`, init(), false, tick()); return wins; } const ids = (wins: { id: string }[]) => wins.map((w) => w.id); const byId = (wins: ComposeWin[], id: string) => wins.find((w) => w.id === id)!; const openIds = (wins: ComposeWin[]) => ids(wins.filter((w) => !w.minimized)); describe("compose window list", () => { it("parks the open window instead of replacing it", () => { const wins = opened(3); expect(ids(wins)).toEqual(["c1", "c2", "c3"]); expect(wins.map((w) => w.minimized)).toEqual([true, true, false]); }); it("keeps every window's identity as the list grows", () => { const first = opened(1)[0]; const later = openWindow( openWindow([first], "c2", init(), false, tick()), "c3", init(), false, tick(), ); // Same object identity for c1's init: nothing was rebuilt underneath it. expect(later[0].init).toBe(first.init); }); it("reopening a draft restores its window rather than adding a second", () => { const wins = openWindow(opened(2), "c3", init("draft-1"), false, tick()); const again = openWindow(wins, "c4", init("draft-1"), false, tick()); expect(again).toHaveLength(3); expect(openIds(again)).toEqual(["c3"]); }); it("restoring a window leaves every other window as it was", () => { // c3 open, c1 and c2 collapsed; restoring c1 must not park c3. const wins = restoreWindow(opened(3), "c1", tick()); expect(openIds(wins)).toEqual(["c1", "c3"]); expect(ids(wins)).toEqual(["c1", "c2", "c3"]); // and nothing moved }); it("restoring marks the window as the most recently reached for", () => { const wins = restoreWindow(opened(3), "c1", tick()); const newest = [...wins].sort((a, b) => b.activated - a.activated)[0]; expect(newest.id).toBe("c1"); }); it("expanding claims the row; collapsing out of it leaves the others alone", () => { const big = toggleExpand(opened(2), "c1", tick()); expect(byId(big, "c1")).toMatchObject({ minimized: false, expanded: true }); expect(byId(big, "c2").minimized).toBe(true); const back = restoreWindow(big, "c2", tick()); const small = toggleExpand(back, "c1", tick()); expect(openIds(small).sort()).toEqual(["c1", "c2"]); }); it("expanding moves the window to the end of the row", () => { // The row is right-anchored: a bar after an expanded window would shove it // left, off the reading pane and across the list. const wins = toggleExpand(opened(3), "c1", tick()); expect(ids(wins)).toEqual(["c2", "c3", "c1"]); expect(byId(wins, "c1").expanded).toBe(true); }); it("a window already last is not shuffled, and collapsing moves nothing", () => { const wins = opened(3); const big = toggleExpand(wins, "c3", tick()); expect(ids(big)).toEqual(["c1", "c2", "c3"]); // And it stays put on the way back down. expect(ids(toggleExpand(big, "c3", tick()))).toEqual(["c1", "c2", "c3"]); expect(ids(toggleExpand(toggleExpand(wins, "c1", tick()), "c1", tick()))).toEqual([ "c2", "c3", "c1", ]); }); it("a folder switch parks an expanded window without closing it", () => { const big = toggleExpand(opened(2), "c1", tick()); const after = collapseExpanded(big); expect(byId(after, "c1")).toMatchObject({ minimized: true, expanded: false }); // Nothing expanded -> the same array, so an effect calling this can't loop. expect(collapseExpanded(after)).toBe(after); }); it("minimize and Escape park windows without dropping them", () => { expect(minimizeWindow(opened(2), "c2").every((w) => w.minimized)).toBe(true); const all = minimizeAll(opened(3)); expect(all).toHaveLength(3); expect(all.every((w) => w.minimized)).toBe(true); }); }); describe("dock layout", () => { it("never reorders the row", () => { const wins = restoreWindow(opened(4), "c1", tick()); expect(ids(layoutDock(1600, 950, wins).items.map((i) => i.win))).toEqual(["c1", "c2", "c3", "c4"]); }); it("draws several open windows side by side when they fit", () => { const wins = restoreWindow(opened(3), "c1", tick()); const { items } = layoutDock(1600, 950, wins); expect(items.filter((i) => i.open).map((i) => i.win.id)).toEqual(["c1", "c3"]); }); it("parks the least recently reached-for window when they stop fitting", () => { // Three open windows can't fit in 1000px at 420 apiece. let wins = opened(3); wins = restoreWindow(wins, "c1", tick()); wins = restoreWindow(wins, "c2", tick()); expect(openIds(wins)).toEqual(["c1", "c2", "c3"]); const { items } = layoutDock(1000, 950, wins); // c3 was activated first of the three, so it is the one drawn as a bar — // its state is untouched, so it reopens when there is room again. expect(items.filter((i) => i.open).map((i) => i.win.id)).toEqual(["c1", "c2"]); expect(layoutDock(2400, 950, wins).items.every((i) => i.open)).toBe(true); }); it("moves the oldest bars into the overflow menu when the row fills up", () => { const { items, overflow } = layoutDock(1600, 950, opened(6)); expect(overflow).toBeGreaterThan(0); expect(overflow).toBeLessThan(5); // some bars still on screen expect(items.filter((i) => !i.open && !i.hidden)[0].width).toBeGreaterThanOrEqual(256); }); it("never lets the row run off the left edge", () => { for (const vw of [520, 760, 1024, 1280, 1600, 2560]) { for (const n of [1, 2, 3, 6, 12]) { let wins = opened(n); // Worst case: the user has restored everything, so every window wants // to be a full composer. for (const w of wins) wins = restoreWindow(wins, w.id, tick()); const { items, overflow } = layoutDock(vw, 950, wins); const visible = items.filter((i) => !i.hidden); const chip = overflow > 0 ? 56 + 8 : 0; const width = chip + visible.reduce((n2, i) => n2 + i.width + 8, 0) - (visible.length ? 8 : 0); expect(width, `vw=${vw} n=${n}`).toBeLessThanOrEqual(vw - 32); } } }); it("an expanded window matches the reading pane", () => { const wins = toggleExpand(opened(2), "c2", tick()); for (const paneW of [640, 980, 1240]) { const { items } = layoutDock(1600, 950, wins, paneW); expect(items.find((i) => i.win.id === "c2")?.width).toBe(paneW); } // Unmeasured (first paint, or no pane at all) falls back to a fixed width // rather than collapsing the window to nothing. expect(layoutDock(1600, 950, wins).items.find((i) => i.win.id === "c2")?.width).toBe(1112); }); it("an expanded window is the rightmost thing on the row", () => { // What the alignment in ComposeDock assumes: it slides the last item over // to sit on the pane, which is only correct if nothing follows it. for (const id of ["c1", "c2", "c3"]) { const { items } = layoutDock(1600, 950, toggleExpand(opened(3), id, tick()), 900); expect(items[items.length - 1].win.id, `expanded ${id}`).toBe(id); expect(items[items.length - 1].win.expanded).toBe(true); } }); it("an expanded window still fits the row when the pane is wider than it", () => { const wins = toggleExpand(opened(2), "c1", tick()); const { items } = layoutDock(900, 950, wins, 2000); expect(items.find((i) => i.win.id === "c1")?.width).toBeLessThanOrEqual(900 - 32); }); it("gives every open window a height, so a long body scrolls instead of growing", () => { const { items } = layoutDock(1600, 950, opened(2)); expect(items.find((i) => i.open)?.height).toBe(520); // A title bar is as tall as its own header; the dock doesn't size it. expect(items.find((i) => !i.open)?.height).toBeNull(); }); it("an expanded window takes the viewport, less a sliver of desktop", () => { const { items } = layoutDock(1600, 950, toggleExpand(opened(1), "c1", tick())); expect(items[0].height).toBe(942); }); it("never stands taller than the screen", () => { for (const vh of [320, 480, 700, 950, 1440]) { const plain = layoutDock(1600, vh, opened(1)).items[0].height ?? 0; const big = layoutDock(1600, vh, toggleExpand(opened(1), "c1", tick())).items[0].height ?? 0; expect(plain, `vh=${vh}`).toBeLessThanOrEqual(vh - 8); expect(big, `vh=${vh}`).toBeLessThanOrEqual(vh - 8); } }); });