summaryrefslogtreecommitdiffhomepage
path: root/src/features/layout/logic/registry.test.ts
blob: e5e947c8ed1e1f4dd5474478a4788dbb6e0b15dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import type { Component } from "svelte";
import { afterEach, describe, expect, it } from "vitest";
import {
  getView,
  listPanelViews,
  listViews,
  registerPluginView,
  registerView,
  unregisterView,
  unregisterViewsByPlugin,
  type ViewRegistration,
  viewRegistry,
} from "./registry";

const fakeComponent: Component = () => {
  throw new Error("fixture component should never be constructed");
};

function registration(overrides?: Partial<ViewRegistration>): ViewRegistration {
  return { id: "test-view", label: "Test View", component: fakeComponent, ...overrides };
}

const registeredIds = (): string[] => listViews().map((v) => v.id);

afterEach(() => {
  for (const id of registeredIds()) unregisterView(id);
});

describe("view registry", () => {
  it("registers a view and exposes it via getView/listViews", () => {
    registerView(registration());
    expect(getView("test-view")?.label).toBe("Test View");
    expect(registeredIds()).toContain("test-view");
  });

  it("throws loudly on a duplicate id", () => {
    registerView(registration());
    expect(() => registerView(registration({ label: "Other" }))).toThrow(/duplicate view id/);
  });

  it("normalizes panelEligible to true by default and respects false", () => {
    registerView(registration({ id: "a" }));
    registerView(registration({ id: "b", panelEligible: false }));
    expect(getView("a")?.panelEligible).toBe(true);
    expect(getView("b")?.panelEligible).toBe(false);
    expect(listPanelViews().map((v) => v.id)).toContain("a");
    expect(listPanelViews().map((v) => v.id)).not.toContain("b");
  });

  it("keeps pluginId when given", () => {
    registerView(registration({ id: "plugin-view", pluginId: "my-plugin" }));
    expect(getView("plugin-view")?.pluginId).toBe("my-plugin");
  });

  it("unregisterView removes the view (idempotent for unknown ids)", () => {
    registerView(registration({ id: "gone" }));
    unregisterView("gone");
    expect(getView("gone")).toBeUndefined();
    expect(() => unregisterView("never-registered")).not.toThrow();
  });

  it("is reactive: subscribers see registrations and unregistrations", () => {
    const seen: string[][] = [];
    const unsubscribe = viewRegistry.subscribe((views) => seen.push(views.map((v) => v.id)));
    try {
      registerView(registration({ id: "reactive-a" }));
      registerView(registration({ id: "reactive-b" }));
      unregisterView("reactive-a");
    } finally {
      unsubscribe();
    }
    expect(seen[0]).toEqual([]);
    expect(seen.at(-1)).toEqual(["reactive-b"]);
  });

  it("preserves registration order in listViews", () => {
    registerView(registration({ id: "z-first" }));
    registerView(registration({ id: "a-second" }));
    const ids = registeredIds();
    expect(ids.indexOf("z-first")).toBeLessThan(ids.indexOf("a-second"));
  });
});

// ── Plugin-view registration (plan 03) — forced attribution + the sweep ─────

describe("registerPluginView / unregisterViewsByPlugin (plan 03)", () => {
  it("forces the importing plugin's pluginId attribution", () => {
    const dispose = registerPluginView(
      { id: "abi-view", label: "ABI View", component: fakeComponent },
      "my-plugin",
    );
    expect(getView("abi-view")?.pluginId).toBe("my-plugin");
    dispose();
    expect(getView("abi-view")).toBeUndefined();
  });

  it("returns a working per-view disposer (the plugin deactivate contract)", () => {
    const dispose = registerPluginView(
      { id: "abi-view-2", label: "ABI View", component: fakeComponent },
      "my-plugin",
    );
    dispose();
    dispose(); // idempotent
    expect(getView("abi-view-2")).toBeUndefined();
  });

  it("the sweep removes exactly that plugin's views (registry attribution)", () => {
    registerPluginView({ id: "p1-a", label: "A", component: fakeComponent }, "plugin-1");
    registerPluginView({ id: "p1-b", label: "B", component: fakeComponent }, "plugin-1");
    registerPluginView({ id: "p2-a", label: "C", component: fakeComponent }, "plugin-2");
    registerView(registration({ id: "builtin" })); // no pluginId — never swept

    unregisterViewsByPlugin("plugin-1");
    expect(getView("p1-a")).toBeUndefined();
    expect(getView("p1-b")).toBeUndefined();
    expect(getView("p2-a")).not.toBeUndefined();
    expect(getView("builtin")).not.toBeUndefined();

    unregisterViewsByPlugin("plugin-2");
    expect(getView("p2-a")).toBeUndefined();
    expect(getView("builtin")).not.toBeUndefined();
  });

  it("the sweep is idempotent and a no-op for an unknown plugin", () => {
    unregisterViewsByPlugin("nobody");
    registerPluginView({ id: "p3-a", label: "A", component: fakeComponent }, "plugin-3");
    unregisterViewsByPlugin("plugin-3");
    expect(() => unregisterViewsByPlugin("plugin-3")).not.toThrow();
  });

  it("a duplicate id from a plugin throws LOUDLY (the driver marks it failed)", () => {
    registerView(registration({ id: "taken" }));
    expect(() =>
      registerPluginView({ id: "taken", label: "Taken", component: fakeComponent }, "my-plugin"),
    ).toThrow(/duplicate view id/);
  });

  it("sweep removals are visible to registry subscribers (reactive)", () => {
    const seen: string[][] = [];
    const unsubscribe = viewRegistry.subscribe((views) => {
      seen.push(views.map((v) => v.id));
    });
    registerPluginView({ id: "reactive", label: "R", component: fakeComponent }, "p");
    unregisterViewsByPlugin("p");
    unsubscribe();
    expect(seen.at(-1)).not.toContain("reactive");
  });
});