Port the full-page and navigation tiles into the Vue editor (for 0.2.74)
Sizes single, wide and full in web/src/model/layout.ts (a full tile covers the six cells of a page and starts it; packing, fitting, arranging and page counting know it), the full card on the mockup, Size with Full page and its firmware hint in the tile drawer, controls defaulting to none on a full tile, navigation tiles (screen.page_1..8: Goes to page, size normal or double, icon and colour only, "Page n ›" on the card), the tile limit of 48 from firmware 0.2.62, the "Screen" domain label. Vitest and the Python source tests cover it. The add-on side of this round lives on the peer branch fullpage (4be6153). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -118,3 +118,16 @@ describe("CommandPalette", () => {
|
||||
expect(state.palette).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full-page and navigation tiles on the mockup", () => {
|
||||
it("draws a full tile as one big card and a navigation tile with its page", () => {
|
||||
state.liveStates["light.a"] = { state: "on", word: "On", a: {} };
|
||||
const full = placed({ entity: "light.a", name: "", slot: 0, options: { size: "full" } });
|
||||
expect(full.classes()).toContain("full");
|
||||
expect(full.classes()).not.toContain("wide");
|
||||
expect(full.find(".ic").classes()).toContain("lit");
|
||||
expect(full.find(".tog").exists()).toBe(false);
|
||||
const nav = placed({ entity: "screen.page_3", name: "Go to page 3", slot: 6 });
|
||||
expect(nav.find(".goto").text()).toBe("Page 3 ›");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// The grid rules: the same ones the add-on applies (core.pack_slots, validate_layout) and the firmware draws.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
arrange, controlsLabel, defaultOptions, effectiveControls, firstFree, fits, hasGaps, MAX_PAGES, MAX_SLOTS, nearestFree,
|
||||
newTile, normalize, occupied, packSlots, pageCount, rowStart, SLOTS_PER_PAGE, tileLimit, versionAtLeast,
|
||||
arrange, cellsOf, controlsLabel, defaultOptions, effectiveControls, firstFree, fits, hasGaps, MAX_PAGES, MAX_SLOTS, nearestFree,
|
||||
newTile, normalize, occupied, packSlots, pageCount, pageStart, pageTarget, rowStart, sizeOf, SLOTS_PER_PAGE, spanOf, tileLimit, versionAtLeast,
|
||||
} from "../src/model/layout";
|
||||
import type { Inventory, Layout, Tile } from "../src/types";
|
||||
|
||||
@@ -109,3 +109,48 @@ describe("defaults, controls and versions", () => {
|
||||
expect(tileLimit(undefined)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full-page tiles and navigation tiles (firmware 0.2.62+)", () => {
|
||||
it("knows the three sizes and what they cover", () => {
|
||||
expect(sizeOf(tile("a", 0))).toBe("single");
|
||||
expect(sizeOf(tile("a", 0, { size: "wide" }))).toBe("wide");
|
||||
expect(sizeOf(tile("a", 0, { size: "full" }))).toBe("full");
|
||||
expect(sizeOf(tile("a", 0, { size: "huge" }))).toBe("single");
|
||||
expect(spanOf("full")).toBe(6);
|
||||
expect(cellsOf(8, "full")).toEqual([6, 7, 8, 9, 10, 11]);
|
||||
expect(cellsOf(3, true)).toEqual([3, 4]);
|
||||
expect(pageStart(11)).toBe(6);
|
||||
});
|
||||
it("packs a full tile at the start of a page and lets only a page start fit it", () => {
|
||||
expect(packSlots([tile("a", -1), tile("f", -1, { size: "full" }), tile("b", -1)])).toEqual([0, 6, 12]);
|
||||
const taken = occupied(entries([tile("a", 0)]));
|
||||
expect(fits(taken, 0, "full")).toBe(false);
|
||||
expect(fits(taken, 6, "full")).toBe(true);
|
||||
expect(fits(taken, 8, "full")).toBe(false);
|
||||
expect(firstFree(taken, "full")).toBe(6);
|
||||
expect(firstFree(taken, "single", 6)).toBe(6);
|
||||
expect(pageCount(entries([tile("f", 6, { size: "full" })]))).toBe(2);
|
||||
});
|
||||
it("drops a full tile on its page and moves what was there to the next free cells", () => {
|
||||
const a = tile("a", 0), b = tile("b", 3), f = tile("f", 6, { size: "full" });
|
||||
const result = arrange([a, b, f], f, 2)!;
|
||||
const by = Object.fromEntries(result.map((e) => [e.tile.entity, e.slot]));
|
||||
expect(by.f).toBe(0);
|
||||
// The displaced tiles take the cells the full tile left, in order.
|
||||
expect(by.a).toBe(6);
|
||||
expect(by.b).toBe(7);
|
||||
});
|
||||
it("knows a navigation tile and gives it no default card", () => {
|
||||
expect(pageTarget("screen.page_3")).toBe(3);
|
||||
expect(pageTarget("screen.page_9")).toBe(0);
|
||||
expect(pageTarget("screen.clock")).toBe(0);
|
||||
expect(defaultOptions("screen.page_2")).toEqual({});
|
||||
});
|
||||
it("holds 48 tiles from firmware 0.2.62 and shows no control on a full card without a choice", () => {
|
||||
expect(tileLimit("0.2.62")).toBe(48);
|
||||
expect(tileLimit("0.2.61")).toBe(20);
|
||||
const inventory = { screens: [], entities: [], controls: { light: { default: "toggle", choices: [{ key: "toggle", label: "On/off" }, { key: "none", label: "None" }] } } } as unknown as Inventory;
|
||||
expect(effectiveControls(tile("light.a", 0, { size: "full" }), inventory)).toBeNull();
|
||||
expect(effectiveControls(tile("light.a", 0, { size: "full", controls: "toggle" }), inventory)).toBe("toggle");
|
||||
});
|
||||
});
|
||||
|
||||
+33
-1
@@ -1,7 +1,7 @@
|
||||
// The store: selecting a screen, editing its layout, what's new, progress, copy and import.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
addTile, canAlert, copyLayoutFrom, importLayout, layoutJson, liveOf, PHASES, removeTile, select, setTileOption, state, tileLimit,
|
||||
addTile, canAlert, copyLayoutFrom, importLayout, layoutJson, liveOf, PHASES, removeTile, retargetPageTile, select, setTileOption, state, tileLimit,
|
||||
topbarItems, topbarView, updateProgress, whatsNew,
|
||||
} from "../src/store";
|
||||
import type { Inventory, Screen } from "../src/types";
|
||||
@@ -156,3 +156,35 @@ describe("updates with content", () => {
|
||||
expect(canAlert(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full-page and navigation tiles", () => {
|
||||
it("moves the other tiles of the page behind a tile that grows to the whole page", () => {
|
||||
select("living");
|
||||
const [lamp, sensor] = state.layout!.tiles;
|
||||
setTileOption(lamp, "size", "full");
|
||||
expect(lamp.slot).toBe(0);
|
||||
expect(sensor.slot).toBe(6);
|
||||
expect(state.layout!.pages).toBe(2);
|
||||
expect(state.toast).toBeNull();
|
||||
});
|
||||
it("takes the first empty page when the others cannot move, and gives up with a toast when none is free", () => {
|
||||
select("living");
|
||||
state.layout!.tiles = Array.from({ length: 48 }, (_, i) => ({ entity: `light.l${i}`, name: "", slot: i }));
|
||||
const first = state.layout!.tiles[0];
|
||||
setTileOption(first, "size", "full");
|
||||
expect(first.options?.size).toBe("single");
|
||||
expect(state.toast?.message).toMatch(/No page is free/);
|
||||
});
|
||||
it("lets a navigation tile point at another page, once per page", () => {
|
||||
select("living");
|
||||
addTile("screen.page_2");
|
||||
const nav = state.layout!.tiles.find((t) => t.entity === "screen.page_2")!;
|
||||
expect(nav.options).toBeUndefined();
|
||||
expect(retargetPageTile(nav, 3)).toBe(true);
|
||||
expect(nav.entity).toBe("screen.page_3");
|
||||
addTile("screen.page_4");
|
||||
expect(retargetPageTile(nav, 4)).toBe(false);
|
||||
expect(state.toast?.message).toMatch(/already has a tile that goes to page 4/);
|
||||
expect(retargetPageTile(nav, 9)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user