Newer
Older
smart-home-server / webclient-vue / src / stores / __tests__ / areas.spec.js
import { describe, it, expect, vi, beforeEach } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { useAreasStore } from "../areas.js";

vi.mock("../../api/modules/areas.js", () => ({
  areasApi: {
    list: vi.fn(),
    newArea: vi.fn(),
    updateDisplayName: vi.fn(),
    remove: vi.fn(),
    unassign: vi.fn(),
    placeInArea: vi.fn(),
  },
}));

import { areasApi } from "../../api/modules/areas.js";

describe("useAreasStore", () => {
  beforeEach(() => {
    setActivePinia(createPinia());
    vi.clearAllMocks();
  });

  describe("areaTree getter", () => {
    it("returns flat list for root-only areas", () => {
      const store = useAreasStore();
      store.areas = [
        { id: 1, parent_id: 0, display_name: "Kitchen" },
        { id: 2, parent_id: 0, display_name: "Hall" },
      ];

      const tree = store.areaTree;

      expect(tree).toHaveLength(2);
      expect(tree[0].display_name).toBe("Kitchen");
      expect(tree[1].display_name).toBe("Hall");
    });

    it("nests children under parents", () => {
      const store = useAreasStore();
      store.areas = [
        { id: 1, parent_id: 0, display_name: "House" },
        { id: 2, parent_id: 1, display_name: "Kitchen" },
        { id: 3, parent_id: 1, display_name: "Bedroom" },
      ];

      const tree = store.areaTree;

      expect(tree).toHaveLength(1);
      expect(tree[0].children).toHaveLength(2);
      expect(tree[0].children[0].display_name).toBe("Kitchen");
      expect(tree[0].children[1].display_name).toBe("Bedroom");
    });

    it("skips self-referencing areas", () => {
      const store = useAreasStore();
      store.areas = [
        { id: 1, parent_id: 1, display_name: "Self" },
      ];

      const tree = store.areaTree;

      expect(tree).toHaveLength(1);
      expect(tree[0].children).toEqual([]);
    });

    it("treats missing parent as root", () => {
      const store = useAreasStore();
      store.areas = [
        { id: 1, parent_id: 0, display_name: "Root" },
        { id: 2, parent_id: 99, display_name: "Orphan" },
      ];

      const tree = store.areaTree;

      expect(tree).toHaveLength(2);
    });
  });

  describe("loadAreas", () => {
    it("sets areas on success", async () => {
      areasApi.list.mockResolvedValue({
        ok: true,
        data: { data: { areas: [{ id: 1, display_name: "Kitchen" }] } },
      });

      const store = useAreasStore();
      await store.loadAreas();

      expect(store.areas).toEqual([{ id: 1, display_name: "Kitchen" }]);
      expect(store.isLoading).toBe(false);
      expect(store.error).toBeNull();
    });

    it("sets error on failure", async () => {
      areasApi.list.mockResolvedValue({
        ok: false,
        error: { message: "Network error" },
      });

      const store = useAreasStore();
      await store.loadAreas();

      expect(store.error).toEqual({ message: "Network error" });
      expect(store.isLoading).toBe(false);
    });
  });

  describe("createArea", () => {
    it("adds new area to list on success", async () => {
      areasApi.newArea.mockResolvedValue({
        ok: true,
        data: { data: { area: { id: 5, display_name: "New" } } },
      });

      const store = useAreasStore();
      store.areas = [{ id: 1, display_name: "Old" }];

      await store.createArea({ type: "room", alias: "new", display_name: "New" });

      expect(store.areas).toHaveLength(2);
      expect(store.areas[1].display_name).toBe("New");
    });
  });

  describe("renameArea", () => {
    it("updates display_name in place", async () => {
      areasApi.updateDisplayName.mockResolvedValue({ ok: true });

      const store = useAreasStore();
      store.areas = [{ id: 1, display_name: "Old" }];

      await store.renameArea(1, "New");

      expect(store.areas[0].display_name).toBe("New");
    });
  });

  describe("removeArea", () => {
    it("removes area from list on success", async () => {
      areasApi.remove.mockResolvedValue({ ok: true });

      const store = useAreasStore();
      store.areas = [{ id: 1 }, { id: 2 }];

      await store.removeArea(1);

      expect(store.areas).toHaveLength(1);
      expect(store.areas[0].id).toBe(2);
    });
  });

  describe("unassignArea", () => {
    it("sets parent_id to 0 on success", async () => {
      areasApi.unassign.mockResolvedValue({ ok: true });

      const store = useAreasStore();
      store.areas = [{ id: 1, parent_id: 2 }];

      await store.unassignArea(1);

      expect(store.areas[0].parent_id).toBe(0);
    });
  });

  describe("expandedNodeIds", () => {
    it("restores expanded nodes from localStorage on init", () => {
      localStorage.setItem("sh:areas:expandedNodes", JSON.stringify([1, 3]));
      setActivePinia(createPinia());

      const store = useAreasStore();
      expect(store.isNodeExpanded(1)).toBe(true);
      expect(store.isNodeExpanded(3)).toBe(true);
      expect(store.isNodeExpanded(2)).toBe(false);
    });

    it("persists toggled nodes to localStorage", async () => {
      const store = useAreasStore();
      store.toggleNode(5);
      await new Promise((r) => setTimeout(r, 0));

      const raw = localStorage.getItem("sh:areas:expandedNodes");
      expect(JSON.parse(raw)).toContain(5);
    });

    it("removes collapsed nodes from localStorage", async () => {
      const store = useAreasStore();
      store.toggleNode(5);
      await new Promise((r) => setTimeout(r, 0));
      store.toggleNode(5);
      await new Promise((r) => setTimeout(r, 0));

      const raw = localStorage.getItem("sh:areas:expandedNodes");
      expect(JSON.parse(raw)).not.toContain(5);
    });
  });

  describe("assignToArea", () => {
    it("sets parent_id on success", async () => {
      areasApi.placeInArea.mockResolvedValue({ ok: true });

      const store = useAreasStore();
      store.areas = [
        { id: 1, parent_id: 0 },
        { id: 2, parent_id: 0 },
      ];

      await store.assignToArea(1, 2);

      expect(store.areas[0].parent_id).toBe(2);
    });

    it("rejects assigning the last root area", async () => {
      const store = useAreasStore();
      store.areas = [{ id: 1, parent_id: 0, display_name: "OnlyRoot" }];

      const result = await store.assignToArea(1, 2);

      expect(result.ok).toBe(false);
      expect(result.error?.message).toContain("last root");
      expect(store.areas[0].parent_id).toBe(0);
    });

    it("allows assigning a root area when it is not the last one", async () => {
      areasApi.placeInArea.mockResolvedValue({ ok: true });

      const store = useAreasStore();
      store.areas = [
        { id: 1, parent_id: 0, display_name: "Root1" },
        { id: 2, parent_id: 0, display_name: "Root2" },
      ];

      const result = await store.assignToArea(1, 3);

      expect(result.ok).toBe(true);
      expect(store.areas[0].parent_id).toBe(3);
    });
  });
});