2026-05-29 22:16:55 +08:00
|
|
|
import { describe, expect, it } from "vitest";
|
2026-05-29 22:56:20 +08:00
|
|
|
import { computeAlbumLayout, type AlbumTile } from "./albumLayout";
|
|
|
|
|
|
|
|
|
|
/** Right/bottom edge of a tile, as fractions of the box. */
|
|
|
|
|
const right = (t: AlbumTile) => t.left + t.width;
|
|
|
|
|
const bottom = (t: AlbumTile) => t.top + t.height;
|
2026-05-29 22:16:55 +08:00
|
|
|
|
|
|
|
|
describe("computeAlbumLayout", () => {
|
|
|
|
|
it("returns null for fewer than 2 images", () => {
|
|
|
|
|
expect(computeAlbumLayout([])).toBeNull();
|
|
|
|
|
expect(computeAlbumLayout([1.5])).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-29 22:56:20 +08:00
|
|
|
it("places two portraits side by side, splitting the full width", () => {
|
2026-05-29 22:16:55 +08:00
|
|
|
const layout = computeAlbumLayout([0.7, 0.8])!;
|
|
|
|
|
expect(layout.aspectRatio).toBeCloseTo(0.7 + 0.8, 4);
|
2026-05-29 22:56:20 +08:00
|
|
|
expect(layout.tiles).toHaveLength(2);
|
|
|
|
|
// Full height each, adjacent columns covering the whole width.
|
|
|
|
|
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, height: 1 });
|
|
|
|
|
expect(layout.tiles[1].top).toBe(0);
|
|
|
|
|
expect(layout.tiles[1].height).toBe(1);
|
|
|
|
|
expect(right(layout.tiles[0])).toBeCloseTo(layout.tiles[1].left, 6);
|
|
|
|
|
expect(right(layout.tiles[1])).toBeCloseTo(1, 6);
|
2026-05-29 22:16:55 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-29 22:56:20 +08:00
|
|
|
it("stacks two landscapes vertically, splitting the full height", () => {
|
2026-05-29 22:16:55 +08:00
|
|
|
const layout = computeAlbumLayout([1.6, 1.4])!;
|
|
|
|
|
expect(layout.aspectRatio).toBeCloseTo(1 / (1 / 1.6 + 1 / 1.4), 4);
|
2026-05-29 22:56:20 +08:00
|
|
|
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, width: 1 });
|
|
|
|
|
expect(layout.tiles[1].left).toBe(0);
|
|
|
|
|
expect(layout.tiles[1].width).toBe(1);
|
|
|
|
|
expect(bottom(layout.tiles[0])).toBeCloseTo(layout.tiles[1].top, 6);
|
|
|
|
|
expect(bottom(layout.tiles[1])).toBeCloseTo(1, 6);
|
2026-05-29 22:16:55 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-29 22:56:20 +08:00
|
|
|
it("portrait primary on the left, rest stacked on the right, fully covering", () => {
|
2026-05-29 22:16:55 +08:00
|
|
|
const layout = computeAlbumLayout([0.6, 1.2, 1.3])!;
|
2026-05-29 22:56:20 +08:00
|
|
|
// Primary occupies the full-height left column.
|
|
|
|
|
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, height: 1 });
|
|
|
|
|
// Secondary tiles share the right column and fill it top-to-bottom.
|
|
|
|
|
expect(layout.tiles[1].left).toBeCloseTo(layout.tiles[0].width, 6);
|
|
|
|
|
expect(layout.tiles[1].top).toBe(0);
|
|
|
|
|
expect(bottom(layout.tiles[1])).toBeCloseTo(layout.tiles[2].top, 6);
|
|
|
|
|
expect(bottom(layout.tiles[2])).toBeCloseTo(1, 6);
|
|
|
|
|
expect(right(layout.tiles[2])).toBeCloseTo(1, 6);
|
2026-05-29 22:16:55 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-29 22:56:20 +08:00
|
|
|
it("landscape primary on top, rest in a bottom row, fully covering", () => {
|
2026-05-29 22:16:55 +08:00
|
|
|
const layout = computeAlbumLayout([1.8, 0.9, 1.1])!;
|
2026-05-29 22:56:20 +08:00
|
|
|
// Primary occupies the full-width top row.
|
|
|
|
|
expect(layout.tiles[0]).toMatchObject({ left: 0, top: 0, width: 1 });
|
|
|
|
|
// Secondary tiles share the bottom row and fill it left-to-right.
|
|
|
|
|
expect(layout.tiles[1].top).toBeCloseTo(layout.tiles[0].height, 6);
|
|
|
|
|
expect(layout.tiles[1].left).toBe(0);
|
|
|
|
|
expect(right(layout.tiles[1])).toBeCloseTo(layout.tiles[2].left, 6);
|
|
|
|
|
expect(right(layout.tiles[2])).toBeCloseTo(1, 6);
|
|
|
|
|
expect(bottom(layout.tiles[2])).toBeCloseTo(1, 6);
|
2026-05-29 22:16:55 +08:00
|
|
|
});
|
|
|
|
|
});
|