@panyam/tsappkit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/page/IslandPage.test.ts +0 -83
- package/src/page/mount.test.ts +0 -73
- package/src/page/spec.test.ts +0 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panyam/tsappkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "TypeScript application toolkit with component lifecycle management, event system, UI utilities, and documentation site components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
24
|
"src",
|
|
25
|
+
"!src/**/*.test.ts",
|
|
25
26
|
"styles"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { LCMComponent } from "../LCMComponent";
|
|
3
|
-
import { IslandPage } from "./IslandPage";
|
|
4
|
-
import type { PageSpec } from "./spec";
|
|
5
|
-
|
|
6
|
-
type Ctx = { built: number };
|
|
7
|
-
type Ext = { things: string[] };
|
|
8
|
-
|
|
9
|
-
const mounted: { name: string; el: HTMLElement; config: unknown; ctx: Ctx }[] = [];
|
|
10
|
-
const contexts: (PageSpec & Ext)[] = [];
|
|
11
|
-
|
|
12
|
-
const island = (name: string) => (el: HTMLElement, spec: { config: unknown }, ctx: Ctx) => {
|
|
13
|
-
mounted.push({ name, el, config: spec.config, ctx });
|
|
14
|
-
return { name } as unknown as LCMComponent;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
class TestPage extends IslandPage<Ctx, Ext> {
|
|
18
|
-
protected registry() {
|
|
19
|
-
return { player: island("player"), chat: island("chat") };
|
|
20
|
-
}
|
|
21
|
-
protected readExtension(raw: Record<string, unknown>): Ext {
|
|
22
|
-
return { things: Array.isArray(raw.things) ? raw.things.map(String) : [] };
|
|
23
|
-
}
|
|
24
|
-
protected makeContext(spec: PageSpec & Ext): Ctx {
|
|
25
|
-
contexts.push(spec);
|
|
26
|
-
return { built: contexts.length };
|
|
27
|
-
}
|
|
28
|
-
mount() {
|
|
29
|
-
return this.initializeSpecificComponents();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function page(spec: string | null, slots: string[]) {
|
|
34
|
-
document.body.innerHTML =
|
|
35
|
-
(spec === null ? "" : `<script type="application/json" id="page-spec">${spec}</script>`) +
|
|
36
|
-
slots.map((s) => `<section data-slot="${s}"></section>`).join("");
|
|
37
|
-
return new TestPage("test");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("IslandPage", () => {
|
|
41
|
-
let warn: ReturnType<typeof vi.spyOn>;
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
// jsdom has no matchMedia, and BasePage's ThemeManager asks it for the colour scheme.
|
|
44
|
-
window.matchMedia ??= ((query: string) => ({ matches: false, media: query, addEventListener() {}, removeEventListener() {} })) as unknown as typeof window.matchMedia;
|
|
45
|
-
mounted.length = 0;
|
|
46
|
-
contexts.length = 0;
|
|
47
|
-
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
48
|
-
});
|
|
49
|
-
afterEach(() => warn.mockRestore());
|
|
50
|
-
|
|
51
|
-
it("mounts each island from the spec into its data-slot, sharing one context", () => {
|
|
52
|
-
const spec = JSON.stringify({
|
|
53
|
-
layout: "drawer",
|
|
54
|
-
islands: [
|
|
55
|
-
{ name: "player", slot: "main", config: { a: 1 } },
|
|
56
|
-
{ name: "nobody", slot: "side" },
|
|
57
|
-
{ name: "chat", slot: "drawer" },
|
|
58
|
-
],
|
|
59
|
-
things: ["x"],
|
|
60
|
-
});
|
|
61
|
-
const out = page(spec, ["main", "side", "drawer"]).mount();
|
|
62
|
-
expect(out).toHaveLength(2);
|
|
63
|
-
expect(mounted.map((m) => [m.name, m.el.dataset.slot, m.config])).toEqual([
|
|
64
|
-
["player", "main", { a: 1 }],
|
|
65
|
-
["chat", "drawer", {}],
|
|
66
|
-
]);
|
|
67
|
-
expect(mounted[0].ctx).toBe(mounted[1].ctx);
|
|
68
|
-
expect(contexts).toHaveLength(1);
|
|
69
|
-
expect(contexts[0].things).toEqual(["x"]);
|
|
70
|
-
expect(warn.mock.calls.flat().join("\n")).toMatch(/nobody/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("mounts nothing, and builds no context, without a readable spec", () => {
|
|
74
|
-
for (const spec of [null, "not json"]) {
|
|
75
|
-
const p = page(spec, ["main"]);
|
|
76
|
-
warn.mockClear();
|
|
77
|
-
expect(p.mount()).toEqual([]);
|
|
78
|
-
expect(warn).toHaveBeenCalledOnce();
|
|
79
|
-
expect(String(warn.mock.calls[0][0])).toMatch(/page-spec/);
|
|
80
|
-
}
|
|
81
|
-
expect(contexts).toHaveLength(0);
|
|
82
|
-
});
|
|
83
|
-
});
|
package/src/page/mount.test.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { mountIslands, type Registry } from "./mount";
|
|
3
|
-
import type { PageSpec } from "./spec";
|
|
4
|
-
|
|
5
|
-
type El = { slot: string };
|
|
6
|
-
const ctx = { tag: "ctx" };
|
|
7
|
-
const bus = { tag: "bus" };
|
|
8
|
-
|
|
9
|
-
const spec: PageSpec = {
|
|
10
|
-
layout: "drawer",
|
|
11
|
-
islands: [
|
|
12
|
-
{ name: "player", slot: "main", presentation: "page", config: { a: 1 } },
|
|
13
|
-
{ name: "unknown", slot: "side", config: {} },
|
|
14
|
-
{ name: "chat", slot: "missing", config: {} },
|
|
15
|
-
{ name: "chat", slot: "drawer", presentation: "drawer", config: {} },
|
|
16
|
-
],
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
describe("mountIslands", () => {
|
|
20
|
-
it("mounts each island in its slot with its config and the page's context, in spec order", () => {
|
|
21
|
-
const calls: unknown[][] = [];
|
|
22
|
-
const registry: Registry<typeof ctx, El, string, typeof bus> = {
|
|
23
|
-
player: (el, island, c, b) => (calls.push([el, island, c, b]), "player-component"),
|
|
24
|
-
chat: (el, island) => (calls.push([el, island.presentation]), "chat-component"),
|
|
25
|
-
};
|
|
26
|
-
const logs: string[] = [];
|
|
27
|
-
const out = mountIslands(spec, registry, (slot) => (slot === "missing" ? null : { slot }), () => ctx, bus, (m) => logs.push(m));
|
|
28
|
-
expect(out).toEqual(["player-component", "chat-component"]);
|
|
29
|
-
expect(calls).toEqual([
|
|
30
|
-
[{ slot: "main" }, spec.islands[0], ctx, bus],
|
|
31
|
-
[{ slot: "drawer" }, "drawer"],
|
|
32
|
-
]);
|
|
33
|
-
// The unknown island and the missing slot are logged and skipped.
|
|
34
|
-
expect(logs).toHaveLength(2);
|
|
35
|
-
expect(logs.join("\n")).toMatch(/unknown/);
|
|
36
|
-
expect(logs.join("\n")).toMatch(/missing/);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("doesn't take an island name from the registry's prototype", () => {
|
|
40
|
-
const logs: string[] = [];
|
|
41
|
-
const out = mountIslands({ layout: "a", islands: [{ name: "toString", slot: "main", config: {} }] }, {}, (slot) => ({ slot }), () => ctx, bus, (m) => logs.push(m));
|
|
42
|
-
expect(out).toEqual([]);
|
|
43
|
-
expect(logs).toHaveLength(1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("carries on when a factory throws", () => {
|
|
47
|
-
const logs: string[] = [];
|
|
48
|
-
const out = mountIslands(
|
|
49
|
-
spec,
|
|
50
|
-
{
|
|
51
|
-
player: () => {
|
|
52
|
-
throw new Error("boom");
|
|
53
|
-
},
|
|
54
|
-
chat: () => "ok",
|
|
55
|
-
},
|
|
56
|
-
(slot) => ({ slot }),
|
|
57
|
-
() => ctx,
|
|
58
|
-
bus,
|
|
59
|
-
(m) => logs.push(m),
|
|
60
|
-
);
|
|
61
|
-
expect(out).toEqual(["ok", "ok"]);
|
|
62
|
-
expect(logs.some((m) => m.includes("boom"))).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("builds the page's context once, and only when there's an island to mount", () => {
|
|
66
|
-
let built = 0;
|
|
67
|
-
const context = () => (built++, ctx);
|
|
68
|
-
mountIslands({ layout: "index", islands: [] }, {}, () => ({ slot: "" }), context, bus, () => {});
|
|
69
|
-
expect(built).toBe(0);
|
|
70
|
-
mountIslands(spec, { player: () => "a", chat: () => "b" }, (slot) => ({ slot }), context, bus, () => {});
|
|
71
|
-
expect(built).toBe(1);
|
|
72
|
-
});
|
|
73
|
-
});
|
package/src/page/spec.test.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { readSpec } from "./spec";
|
|
5
|
-
|
|
6
|
-
describe("readSpec", () => {
|
|
7
|
-
it("reads the spec goapplib's page.Spec writes (page/testdata/spec.json)", () => {
|
|
8
|
-
const text = readFileSync(join(__dirname, "../../../page/testdata/spec.json"), "utf8");
|
|
9
|
-
expect(readSpec(text)).toEqual({
|
|
10
|
-
layout: "drawer",
|
|
11
|
-
islands: [
|
|
12
|
-
{ name: "player", slot: "main", presentation: "page", config: { note: "</script>", urls: ["/a.json"] } },
|
|
13
|
-
{ name: "chat", slot: "side", config: {} },
|
|
14
|
-
],
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("is null when there's no spec or it isn't one", () => {
|
|
19
|
-
for (const bad of [null, undefined, "", "not json", "null", "[]", '{"islands":[]}', '{"layout":"drawer"}', '{"layout":3,"islands":[]}']) {
|
|
20
|
-
expect(readSpec(bad), String(bad)).toBeNull();
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("drops islands it couldn't mount and cleans the rest", () => {
|
|
25
|
-
const spec = readSpec(
|
|
26
|
-
JSON.stringify({
|
|
27
|
-
layout: "drawer",
|
|
28
|
-
islands: [
|
|
29
|
-
{ name: "player", slot: "main" },
|
|
30
|
-
{ slot: "orphan" },
|
|
31
|
-
{ name: "x", slot: 'main"] , body' },
|
|
32
|
-
{ name: "chat", slot: "drawer", presentation: 7, config: "nope" },
|
|
33
|
-
"junk",
|
|
34
|
-
],
|
|
35
|
-
}),
|
|
36
|
-
);
|
|
37
|
-
expect(spec).toEqual({
|
|
38
|
-
layout: "drawer",
|
|
39
|
-
islands: [
|
|
40
|
-
{ name: "player", slot: "main", config: {} },
|
|
41
|
-
{ name: "chat", slot: "drawer", config: {} },
|
|
42
|
-
],
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("leaves an app's own fields out unless it reads them", () => {
|
|
47
|
-
expect(readSpec('{"layout":"a","islands":[],"things":[1]}')).toEqual({ layout: "a", islands: [] });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("merges in what an extension reads, without letting it replace layout or islands", () => {
|
|
51
|
-
const text = JSON.stringify({ layout: "a", islands: [{ name: "p", slot: "main" }], things: [1, "x", 2] });
|
|
52
|
-
const spec = readSpec(text, (raw) => ({
|
|
53
|
-
things: Array.isArray(raw.things) ? raw.things.filter((t): t is number => typeof t === "number") : [],
|
|
54
|
-
layout: "overridden",
|
|
55
|
-
}));
|
|
56
|
-
expect(spec).toEqual({ layout: "a", islands: [{ name: "p", slot: "main", config: {} }], things: [1, 2] });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("is null when the extension throws", () => {
|
|
60
|
-
const spec = readSpec('{"layout":"a","islands":[]}', () => {
|
|
61
|
-
throw new Error("bad things");
|
|
62
|
-
});
|
|
63
|
-
expect(spec).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
});
|