217 lines
8.8 KiB
TypeScript
217 lines
8.8 KiB
TypeScript
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);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|