Files
webmail-client/frontend/src/test/composeWindows.test.ts
T

217 lines
8.8 KiB
TypeScript
Raw Normal View History

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);
}
});
});