summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/tile-layout-store.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/workspaces/tile-layout-store.test.ts')
-rw-r--r--src/features/workspaces/tile-layout-store.test.ts77
1 files changed, 61 insertions, 16 deletions
diff --git a/src/features/workspaces/tile-layout-store.test.ts b/src/features/workspaces/tile-layout-store.test.ts
index f8b30f7..be6a14e 100644
--- a/src/features/workspaces/tile-layout-store.test.ts
+++ b/src/features/workspaces/tile-layout-store.test.ts
@@ -29,12 +29,21 @@ function tile(id: string, order: number, colSpan = 1, rowSpan = 1): TileLayoutEn
}
describe("createTileLayoutStore", () => {
- it("loads the persisted document at creation", () => {
+ it("loads the persisted document at creation (v1 migrates to v2 in memory)", () => {
const persistence = memoryStore({ version: 1, tiles: [tile("a", 0, 2, 2)] });
const store = createTileLayoutStore(persistence);
expect(store.entries).toEqual([tile("a", 0, 2, 2)]);
});
+ it("loads a v2 document with anchors intact", () => {
+ const persistence = memoryStore({
+ version: 2,
+ tiles: [{ ...tile("a", 0), col: 2, row: 3 }],
+ });
+ const store = createTileLayoutStore(persistence);
+ expect(store.entries).toEqual([{ ...tile("a", 0), col: 2, row: 3 }]);
+ });
+
it("sanitizes a corrupt persisted document to empty", () => {
const persistence = memoryStore(null);
// The adapter's load is untyped at the boundary — simulate garbage.
@@ -57,11 +66,26 @@ describe("createTileLayoutStore", () => {
expect(persistence.saved).toHaveLength(1);
// The max order counts KEPT entries (the dropped one doesn't reserve a slot).
expect(persistence.saved[0]).toEqual({
- version: 1,
+ version: 2,
tiles: [tile("a", 0), { id: "b", colSpan: 1, rowSpan: 1, order: 1 }],
});
});
+ it("sync keeps pinned anchors (a deleted tile's cells become gaps)", () => {
+ const persistence = memoryStore({
+ version: 2,
+ tiles: [
+ { ...tile("a", 0), col: 1, row: 1 },
+ { ...tile("gone", 1), col: 1, row: 2 },
+ ],
+ });
+ const store = createTileLayoutStore(persistence);
+
+ store.sync(["a"]);
+
+ expect(store.entries).toEqual([{ ...tile("a", 0), col: 1, row: 1 }]);
+ });
+
it("sync is a no-op (no write) when the merge changes nothing", () => {
const persistence = memoryStore({ version: 1, tiles: [tile("a", 0)] });
const store = createTileLayoutStore(persistence);
@@ -82,20 +106,34 @@ describe("createTileLayoutStore", () => {
expect(persistence.saved.at(-1)?.tiles).toEqual([tile("a", 0, 4, 2)]);
});
- it("move persists the reordered layout", () => {
+ it("placeAtCell commits the displacement and pins EVERY tile", () => {
const persistence = memoryStore({
version: 1,
- tiles: [tile("a", 0), tile("b", 1), tile("c", 2)],
+ tiles: [tile("a", 0), tile("b", 1)],
});
const store = createTileLayoutStore(persistence);
- store.move("c", 0);
+ // a@(1,1), b@(1,2) → drag a to (2,1): a pins there, b keeps its cell.
+ store.placeAtCell("a", { col: 2, row: 1 }, WIDE);
+
+ const entries = store.entries;
+ expect(entries.find((t) => t.id === "a")).toMatchObject({ col: 2, row: 1 });
+ expect(entries.find((t) => t.id === "b")).toMatchObject({ col: 1, row: 2 });
+ // Written as v2.
+ expect(persistence.saved.at(-1)?.version).toBe(2);
+ });
+
+ it("placeAtCell with an unknown id is a no-op (no write)", () => {
+ const persistence = memoryStore({ version: 1, tiles: [tile("a", 0)] });
+ const store = createTileLayoutStore(persistence);
+
+ store.placeAtCell("nope", { col: 1, row: 1 }, WIDE);
- expect(store.entries.map((t) => t.id)).toEqual(["c", "a", "b"]);
- expect(store.entries.map((t) => t.order)).toEqual([0, 1, 2]);
+ expect(persistence.saved).toHaveLength(0);
+ expect(store.entries).toEqual([tile("a", 0)]);
});
- it("placeFirstEmpty puts a new id in the first empty slot's fill position", () => {
+ it("placeFirstEmpty PINS the new id at the first empty slot", () => {
const persistence = memoryStore({
version: 1,
tiles: [tile("a", 0, 1, 2), tile("b", 1)],
@@ -104,27 +142,33 @@ describe("createTileLayoutStore", () => {
store.placeFirstEmpty("new", WIDE);
- // a packs rows 1-2, b row 3 → the first empty slot is (col1,row4) → appended.
- expect(store.entries.map((t) => t.id)).toEqual(["a", "b", "new"]);
+ // a packs rows 1-2, b row 3 → the first empty slot is (col1,row4) → pinned.
+ expect(store.entries.find((t) => t.id === "new")).toMatchObject({ col: 1, row: 4 });
expect(persistence.saved.at(-1)?.tiles.map((t) => t.id)).toEqual(["a", "b", "new"]);
});
it("placeFirstEmpty is idempotent for an id sync already appended (the refresh race)", () => {
const persistence = memoryStore({ version: 1, tiles: [tile("a", 0)] });
const store = createTileLayoutStore(persistence);
- store.sync(["a", "x"]); // the refresh appended the new workspace at the end
- const before = store.entries;
+ store.sync(["a", "x"]); // the refresh appended the new workspace (unpinned)
store.placeFirstEmpty("x", WIDE);
+ const pinned = store.entries;
+ store.placeFirstEmpty("x", WIDE); // the create flow's re-call must be stable
- expect(store.entries).toEqual(before);
+ expect(store.entries).toEqual(pinned);
expect(store.entries.filter((t) => t.id === "x")).toHaveLength(1);
+ // x is PINNED at the first empty slot: a@(1,1) → (1,2).
+ expect(store.entries.find((t) => t.id === "x")).toMatchObject({ col: 1, row: 2 });
});
- it("sortByRecentActivity re-applies recent-activity order and persists", () => {
+ it("sortByRecentActivity re-applies recent-activity order, CLEARs pins, persists", () => {
const persistence = memoryStore({
- version: 1,
- tiles: [tile("a", 0), tile("b", 1)],
+ version: 2,
+ tiles: [
+ { ...tile("a", 0), col: 1, row: 1 },
+ { ...tile("b", 1), col: 2, row: 3 },
+ ],
});
const store = createTileLayoutStore(persistence);
@@ -136,5 +180,6 @@ describe("createTileLayoutStore", () => {
);
expect(store.entries.map((t) => t.id)).toEqual(["b", "a"]);
+ expect(store.entries.every((t) => t.col === undefined && t.row === undefined)).toBe(true);
});
});