Fix what a full test round of today's releases found: a ticking single clock, a switch that works in standby, no taps from a swipe (0.2.78)
A test and code-analysis round of 0.2.72 to 0.2.76 (suites at every tag, sanitizers, ESP32 and GitHub builds, host harnesses on both boards, the editor in a browser, best practice checked against ESPHome, LVGL, Home Assistant and Vue) found these, fixed here with Max's choices for the product questions: - Firmware 0.2.65: the second hand of a single analog clock moves again; on a page that is one full on/off tile the waking push also switches it; on the Guition a flick is never a tap and a finger that slides off a tile doesn't tap it; Previous/Next react to a firm press; the same Go to page tile may sit on several pages; a layout the screen has no memory for is refused instead of crashing; 64-bit redraw mask for 48 tiles; Montserrat 14 no longer built. - App: Save refuses a layout too long for one message or too big for eight pages; an offline screen keeps its firmware version from the device registry; HA refusals reach the editor; 128 KB requests; changelog out of the live stream; cached hashed assets; tini as init and a clean stop; changelog parsing without asterisks or crashes. - Editor: clicking the open screen keeps unsaved edits, edits during a save stay unsaved, a Page row and phone dragging to other pages, repeated page tiles, limits from the server, HA's error messages, a type-checked build. - tools/check.sh + GitHub Actions + a CYD flash budget; docs match 48 tiles and the new editor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
// The components that draw the state: a tile with live values, the library's filters, the ⌘K search.
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AppSettingsView from "../src/components/AppSettingsView.vue";
|
||||
import CommandPalette from "../src/components/CommandPalette.vue";
|
||||
import Library from "../src/components/Library.vue";
|
||||
import Sidebar from "../src/components/Sidebar.vue";
|
||||
import TileCard from "../src/components/TileCard.vue";
|
||||
import TileInspector from "../src/components/TileInspector.vue";
|
||||
import { state } from "../src/store";
|
||||
import type { Inventory, Tile } from "../src/types";
|
||||
|
||||
@@ -17,7 +20,10 @@ function inventory(): Inventory {
|
||||
{ id: "sensor.t", name: "Temperature", state: "21.5", area: "Living room" },
|
||||
{ id: "cover.c", name: "Curtains", state: "open", area: "Living room" },
|
||||
],
|
||||
builtin: [{ id: "screen.clock", name: "Clock", device: "Built into the screen", area: "", state: "ok" }],
|
||||
builtin: [
|
||||
{ id: "screen.clock", name: "Clock", device: "Built into the screen", area: "", state: "ok" },
|
||||
{ id: "screen.page_1", name: "Go to page 1", device: "Built into the screen", area: "", state: "ok" },
|
||||
],
|
||||
icons: { groups: [], weather: { partlycloudy: "F0595" }, sun: { below_horizon: "F0594" }, defaults: { light: "F0335", sensor: "F050F", cover: "F1846" }, fallback: "F0335", builtin: { "screen.clock": "F0150" }, controls: { minus: "F0374", plus: "F0415" } },
|
||||
backgrounds: { auto: { label: "Default" }, none: { label: "None" } },
|
||||
controls: { light: { default: "toggle", choices: [{ key: "toggle", label: "On/off" }, { key: "brightness", label: "Brightness" }, { key: "none", label: "None" }] } },
|
||||
@@ -34,6 +40,7 @@ beforeEach(() => {
|
||||
state.search = ""; state.filter = ""; state.room = ""; state.hidePlaced = false;
|
||||
state.palette = false;
|
||||
state.selectedTile = null; state.inspector = null;
|
||||
state.dirty = false; state.tab = "layout";
|
||||
});
|
||||
|
||||
function placed(tile: Tile) {
|
||||
@@ -90,7 +97,7 @@ describe("TileCard", () => {
|
||||
it("opens the tile's settings on a click", async () => {
|
||||
const lamp = placed({ entity: "light.a", name: "", slot: 0 });
|
||||
await lamp.trigger("click");
|
||||
expect(state.selectedTile).toBe("light.a");
|
||||
expect(state.selectedTile?.entity).toBe("light.a");
|
||||
expect(state.inspector).toEqual({ kind: "tile" });
|
||||
expect(lamp.classes()).toContain("chosen");
|
||||
});
|
||||
@@ -101,7 +108,7 @@ describe("Library", () => {
|
||||
state.layout!.tiles.push({ entity: "light.a", name: "", slot: 0 });
|
||||
const library = mount(Library);
|
||||
const names = () => library.findAll(".ent .tx b").map((b) => b.text());
|
||||
expect(names()).toEqual(["Clock", "Lamp A", "Lamp B", "Temperature", "Curtains"]);
|
||||
expect(names()).toEqual(["Clock", "Go to page 1", "Lamp A", "Lamp B", "Temperature", "Curtains"]);
|
||||
expect(library.find('.ent[title="light.a"]').attributes("disabled")).toBeDefined();
|
||||
expect(library.find('.ent[title="light.a"] .av').classes()).toContain("on");
|
||||
expect(library.find('.ent[title="light.b"] .av').classes()).toContain("gone");
|
||||
@@ -110,7 +117,7 @@ describe("Library", () => {
|
||||
expect(names()).toEqual(["Lamp B"]);
|
||||
await library.find("#room").setValue("");
|
||||
await library.find("#hide-placed").trigger("click");
|
||||
expect(names()).toEqual(["Clock", "Lamp B", "Temperature", "Curtains"]);
|
||||
expect(names()).toEqual(["Clock", "Go to page 1", "Lamp B", "Temperature", "Curtains"]);
|
||||
state.filter = "light";
|
||||
await library.vm.$nextTick();
|
||||
expect(names()).toEqual(["Lamp B"]);
|
||||
@@ -146,3 +153,106 @@ describe("full-page and navigation tiles on the mockup", () => {
|
||||
expect(nav.find(".goto").text()).toBe("Page 3 ›");
|
||||
});
|
||||
});
|
||||
|
||||
describe("several tiles that go to the same page in the library (firmware 0.2.65)", () => {
|
||||
it("keeps offering a placed navigation tile when the screen takes several, and adds another copy", async () => {
|
||||
Object.assign(state.inventory.screens[0], { firmware: "0.2.65", page_tiles_repeat: true });
|
||||
state.layout!.tiles.push({ entity: "screen.page_1", name: "", slot: 0 }, { entity: "light.a", name: "", slot: 1 });
|
||||
const library = mount(Library);
|
||||
const row = () => library.find('.ent[title="screen.page_1"]');
|
||||
expect(row().attributes("disabled")).toBeUndefined();
|
||||
expect(row().find(".add").text()).toBe("+");
|
||||
expect(library.find('.ent[title="light.a"]').attributes("disabled")).toBeDefined();
|
||||
await library.find("#hide-placed").trigger("click");
|
||||
expect(row().exists()).toBe(true);
|
||||
await row().trigger("click");
|
||||
expect(state.layout!.tiles.filter((t) => t.entity === "screen.page_1")).toHaveLength(2);
|
||||
});
|
||||
it("marks it placed when the screen takes one per page", () => {
|
||||
Object.assign(state.inventory.screens[0], { page_tiles_repeat: false });
|
||||
state.layout!.tiles.push({ entity: "screen.page_1", name: "", slot: 0 });
|
||||
const library = mount(Library);
|
||||
expect(library.find('.ent[title="screen.page_1"]').attributes("disabled")).toBeDefined();
|
||||
expect(library.find('.ent[title="screen.page_1"] .add').text()).toBe("✓");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TileInspector: pages (app 0.2.78)", () => {
|
||||
const row = (wrapper: ReturnType<typeof mount>, label: string) =>
|
||||
wrapper.findAll(".f").find((f) => f.find(".f-label").exists() && f.find(".f-label").text() === label)!;
|
||||
const choices = (wrapper: ReturnType<typeof mount>, label: string) => row(wrapper, label).findAll(".seg button").map((b) => b.text());
|
||||
function open(tiles: any[], index: number) {
|
||||
state.layout!.tiles.push(...tiles);
|
||||
state.layout!.pages = 1;
|
||||
const tile = state.layout!.tiles[index];
|
||||
return { tile, drawer: mount(TileInspector, { props: { tile } }) };
|
||||
}
|
||||
it("offers the pages the screen has and the empty one after them as targets, and keeps one beyond those", async () => {
|
||||
Object.assign(state.inventory.screens[0], { firmware: "0.2.63", full_page: true });
|
||||
const { tile, drawer } = open([
|
||||
{ entity: "light.a", name: "", slot: 0 }, { entity: "screen.page_2", name: "", slot: 1 }, { entity: "sensor.t", name: "", slot: 6 },
|
||||
], 1);
|
||||
expect(choices(drawer, "Goes to page")).toEqual(["1", "2", "3 (empty)"]);
|
||||
expect(row(drawer, "Goes to page").find('[aria-pressed="true"]').text()).toBe("2");
|
||||
tile.entity = "screen.page_7";
|
||||
await drawer.vm.$nextTick();
|
||||
expect(choices(drawer, "Goes to page")).toEqual(["1", "2", "3 (empty)", "7 (empty)"]);
|
||||
expect(row(drawer, "Goes to page").find("small.warn").text()).toMatch(/no page 7/);
|
||||
await row(drawer, "Goes to page").findAll(".seg button")[2].trigger("click");
|
||||
expect(tile.entity).toBe("screen.page_3");
|
||||
expect(state.layout!.pages).toBe(3);
|
||||
expect(choices(drawer, "Goes to page")).toEqual(["1", "2", "3 (empty)", "4 (empty)"]);
|
||||
expect(row(drawer, "Goes to page").find("small").classes()).not.toContain("warn");
|
||||
});
|
||||
it("moves the tile to another page or a new one with a tap", async () => {
|
||||
const { tile, drawer } = open([{ entity: "light.a", name: "", slot: 0 }, { entity: "sensor.t", name: "", slot: 6 }], 0);
|
||||
expect(choices(drawer, "Page")).toEqual(["1", "2", "New page"]);
|
||||
expect(row(drawer, "Page").find('[aria-pressed="true"]').text()).toBe("1");
|
||||
await row(drawer, "Page").findAll(".seg button")[1].trigger("click");
|
||||
expect(tile.slot).toBe(7);
|
||||
expect(state.dirty).toBe(true);
|
||||
// Alone on the last page now: a new page would only leave this one empty.
|
||||
const sensor = state.layout!.tiles.find((t) => t.entity === "sensor.t")!;
|
||||
await row(drawer, "Page").findAll(".seg button")[0].trigger("click");
|
||||
expect(tile.slot).toBe(0);
|
||||
expect(sensor.slot).toBe(6);
|
||||
const other = mount(TileInspector, { props: { tile: sensor } });
|
||||
expect(choices(other, "Page")).toEqual(["1", "2"]);
|
||||
await row(other, "Page").findAll(".seg button")[0].trigger("click");
|
||||
expect(sensor.slot).toBe(1);
|
||||
// One tile on one page: nowhere to go, so no row.
|
||||
state.layout!.tiles = [];
|
||||
const only = open([{ entity: "light.b", name: "", slot: 0 }], 0).drawer;
|
||||
expect(row(only, "Page")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sidebar", () => {
|
||||
it("marks the open screen with unsaved edits and keeps them when it is chosen again", async () => {
|
||||
state.dirty = true;
|
||||
const layout = state.layout;
|
||||
state.tab = "settings";
|
||||
const sidebar = mount(Sidebar);
|
||||
const item = sidebar.find("#screens .nav-item");
|
||||
expect(item.find(".unsaved").exists()).toBe(true);
|
||||
await item.trigger("click");
|
||||
expect(state.layout).toBe(layout);
|
||||
expect(state.dirty).toBe(true);
|
||||
expect(state.tab).toBe("layout");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppSettingsView", () => {
|
||||
it("shows what the current firmware brings from the changelog of the full inventory (app 0.2.78)", () => {
|
||||
state.inventory.updates = { target: "0.2.65", pending: 0 };
|
||||
state.inventory.changelog = [
|
||||
{ app: "0.2.78", firmware: "0.2.65", lines: ["Several tiles go to the same page."] },
|
||||
{ app: "0.2.76", firmware: "0.2.63", lines: ["Older."] },
|
||||
];
|
||||
const view = mount(AppSettingsView);
|
||||
expect(view.find(".whatsnew summary").text()).toBe("What's new in firmware 0.2.65");
|
||||
expect(view.findAll(".whatsnew li").map((li) => li.text())).toEqual(["Several tiles go to the same page."]);
|
||||
delete state.inventory.changelog;
|
||||
expect(mount(AppSettingsView).find(".whatsnew").exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user