diff --git a/web/src/app/pattern-lab/panels/LayersPanel.test.tsx b/web/src/app/pattern-lab/panels/LayersPanel.test.tsx new file mode 100644 index 00000000..ab8c98e5 --- /dev/null +++ b/web/src/app/pattern-lab/panels/LayersPanel.test.tsx @@ -0,0 +1,81 @@ +// Del in the layer stack. There is no undo for it, so a layer with work in it +// asks first; a blank one goes straight away. + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import LayersPanel from "./LayersPanel"; +import { useLabStore } from "@/lib/lab/store"; + +const store = () => useLabStore.getState(); +const names = () => store().layers.map((layer) => layer.name); +const del = () => screen.getByRole("button", { name: "Del" }); + +function drawOnePixel() { + const layer = store().layers.find((entry) => entry.id === store().activeLayerId); + if (!layer || layer.type !== "pixel") throw new Error("active layer is not pixel"); + const data = new Uint8ClampedArray(layer.data); + data.set([255, 77, 0, 255], 0); + store().replacePixelData(layer.id, data); +} + +beforeEach(() => { + store().discardProject(); +}); + +describe("LayersPanel Del", () => { + it("is disabled while the stack has one layer", () => { + render(); + expect(del()).toBeDisabled(); + }); + + it("deletes a pixel layer nobody has drawn on without asking", () => { + store().addPixelLayer(); + const confirm = vi.spyOn(window, "confirm"); + render(); + fireEvent.click(del()); + expect(confirm).not.toHaveBeenCalled(); + expect(names()).toEqual(["Code 1"]); + }); + + it("deletes a code layer still holding the starter pattern without asking", () => { + store().addCodeLayer(); + const confirm = vi.spyOn(window, "confirm"); + render(); + fireEvent.click(del()); + expect(confirm).not.toHaveBeenCalled(); + expect(names()).toEqual(["Code 1"]); + }); + + it("asks before deleting a drawn pixel layer, and Cancel keeps it", () => { + store().addPixelLayer(); + drawOnePixel(); + const confirm = vi.spyOn(window, "confirm").mockReturnValue(false); + render(); + fireEvent.click(del()); + expect(confirm).toHaveBeenCalledTimes(1); + expect(confirm.mock.calls[0][0]).toContain('"Pixel 1"'); + expect(names()).toEqual(["Pixel 1", "Code 1"]); + const kept = store().layers[0]; + expect(kept.type === "pixel" ? kept.data[3] : -1).toBe(255); + }); + + it("deletes the drawn pixel layer once confirmed", () => { + store().addPixelLayer(); + drawOnePixel(); + vi.spyOn(window, "confirm").mockReturnValue(true); + render(); + fireEvent.click(del()); + expect(names()).toEqual(["Code 1"]); + expect(store().activeLayerId).toBe(store().layers[0].id); + }); + + it("asks before deleting a code layer whose code was changed", () => { + store().addPixelLayer(); + store().selectLayer(store().layers[1].id); + const confirm = vi.spyOn(window, "confirm").mockReturnValue(false); + render(); + fireEvent.click(del()); + expect(confirm.mock.calls[0][0]).toContain('"Code 1"'); + expect(names()).toEqual(["Pixel 1", "Code 1"]); + }); +}); diff --git a/web/src/app/pattern-lab/panels/LayersPanel.tsx b/web/src/app/pattern-lab/panels/LayersPanel.tsx index 15d51895..c091f416 100644 --- a/web/src/app/pattern-lab/panels/LayersPanel.tsx +++ b/web/src/app/pattern-lab/panels/LayersPanel.tsx @@ -5,11 +5,22 @@ // The active layer's opacity + blend live at the bottom of the panel. import { useState } from "react"; -import { BLEND_MODES } from "@/lib/lab/types"; +import { BLEND_MODES, type Layer } from "@/lib/lab/types"; import { revealEditor } from "@/lib/lab/editorReveal"; import { useLabStore } from "@/lib/lab/store"; +import { STARTER_CODE } from "@/lib/lab/store/shared"; import dock from "../LabPanels.module.css"; +// A pixel layer nobody has drawn on, or a code layer still holding the +// starter pattern, has nothing to lose, so Del takes it without asking. +function holdsWork(layer: Layer): boolean { + if (layer.type === "code") return layer.code !== STARTER_CODE; + for (let i = 3; i < layer.data.length; i += 4) { + if (layer.data[i] !== 0) return true; + } + return false; +} + export default function LayersPanel() { const layers = useLabStore((state) => state.layers); const activeLayerId = useLabStore((state) => state.activeLayerId); @@ -77,7 +88,18 @@ export default function LayersPanel() {