preact-missing-hooks 4.0.0 → 4.1.0

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.
@@ -0,0 +1,149 @@
1
+ /** @jsx h */
2
+ import { h } from "preact";
3
+ import { render } from "@testing-library/preact";
4
+ import "@testing-library/jest-dom";
5
+ import { useLLMMetadata } from "../src/useLLMMetadata";
6
+
7
+ const SCRIPT_SELECTOR = 'script[data-llm="true"]';
8
+
9
+ function getLLMScript(): HTMLScriptElement | null {
10
+ return document.querySelector(SCRIPT_SELECTOR);
11
+ }
12
+
13
+ function getLLMPayload(): unknown {
14
+ const script = getLLMScript();
15
+ if (!script?.textContent) return null;
16
+ try {
17
+ return JSON.parse(script.textContent);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ describe("useLLMMetadata", () => {
24
+ beforeEach(() => {
25
+ document.head.querySelectorAll(SCRIPT_SELECTOR).forEach((el) => el.remove());
26
+ });
27
+
28
+ it("injects script with type application/llm+json and data-llm=true", () => {
29
+ function TestComponent() {
30
+ useLLMMetadata({ route: "/", mode: "manual", title: "Home" });
31
+ return <div />;
32
+ }
33
+ render(<TestComponent />);
34
+ const script = getLLMScript();
35
+ expect(script).toBeInTheDocument();
36
+ expect(script?.getAttribute("type")).toBe("application/llm+json");
37
+ expect(script?.getAttribute("data-llm")).toBe("true");
38
+ });
39
+
40
+ it("manual mode uses title, description, tags from config", () => {
41
+ function TestComponent() {
42
+ useLLMMetadata({
43
+ route: "/blog/ai",
44
+ mode: "manual",
45
+ title: "AI Post",
46
+ description: "A short desc",
47
+ tags: ["a", "b"],
48
+ });
49
+ return <div />;
50
+ }
51
+ render(<TestComponent />);
52
+ const payload = getLLMPayload() as Record<string, unknown>;
53
+ expect(payload).not.toBeNull();
54
+ expect(payload.route).toBe("/blog/ai");
55
+ expect(payload.title).toBe("AI Post");
56
+ expect(payload.description).toBe("A short desc");
57
+ expect(payload.tags).toEqual(["a", "b"]);
58
+ expect(payload.generatedAt).toBeDefined();
59
+ });
60
+
61
+ it("removes previous script when route changes", () => {
62
+ function TestComponent({ route }: { route: string }) {
63
+ useLLMMetadata({ route, mode: "manual", title: route });
64
+ return <div />;
65
+ }
66
+ const { rerender } = render(<TestComponent route="/a" />);
67
+ expect(getLLMScript()?.textContent).toContain('"route":"/a"');
68
+ rerender(<TestComponent route="/b" />);
69
+ const script = getLLMScript();
70
+ expect(script).toBeInTheDocument();
71
+ expect(script?.textContent).toContain('"route":"/b"');
72
+ expect(document.querySelectorAll(SCRIPT_SELECTOR).length).toBe(1);
73
+ });
74
+
75
+ it("auto-extract mode uses document.title and builds outline from visible h1/h2", () => {
76
+ document.title = "Page Title";
77
+ const main = document.createElement("main");
78
+ const h1 = document.createElement("h1");
79
+ h1.textContent = "Intro";
80
+ const h2 = document.createElement("h2");
81
+ h2.textContent = "Section";
82
+ main.append(h1, h2);
83
+ document.body.appendChild(main);
84
+
85
+ function TestComponent() {
86
+ useLLMMetadata({ route: "/doc", mode: "auto-extract" });
87
+ return <div />;
88
+ }
89
+ render(<TestComponent />);
90
+ const payload = getLLMPayload() as Record<string, unknown>;
91
+ expect(payload?.title).toBe("Page Title");
92
+ expect(Array.isArray(payload?.outline)).toBe(true);
93
+ expect((payload?.outline as string[]).includes("Intro")).toBe(true);
94
+ expect((payload?.outline as string[]).includes("Section")).toBe(true);
95
+ main.remove();
96
+ });
97
+
98
+ it("cleans up script on unmount", () => {
99
+ function Page() {
100
+ useLLMMetadata({ route: "/x", mode: "manual" });
101
+ return <div>Page</div>;
102
+ }
103
+ const { unmount } = render(<Page />);
104
+ expect(getLLMScript()).toBeInTheDocument();
105
+ unmount();
106
+ expect(getLLMScript()).not.toBeInTheDocument();
107
+ });
108
+
109
+ it("default mode is manual when mode is omitted", () => {
110
+ function TestComponent() {
111
+ useLLMMetadata({ route: "/", title: "T" });
112
+ return <div />;
113
+ }
114
+ render(<TestComponent />);
115
+ const payload = getLLMPayload() as Record<string, unknown>;
116
+ expect(payload?.title).toBe("T");
117
+ expect(payload?.outline).toBeUndefined();
118
+ });
119
+
120
+ it("accepts null config without throwing and injects minimal payload", () => {
121
+ function TestComponent() {
122
+ useLLMMetadata(null);
123
+ return <div />;
124
+ }
125
+ expect(() => render(<TestComponent />)).not.toThrow();
126
+ const payload = getLLMPayload() as Record<string, unknown>;
127
+ expect(payload?.route).toBe("/");
128
+ expect(payload?.generatedAt).toBeDefined();
129
+ });
130
+
131
+ it("includes canonicalUrl, ogType, siteName when provided", () => {
132
+ function TestComponent() {
133
+ useLLMMetadata({
134
+ route: "/page",
135
+ mode: "manual",
136
+ title: "Page",
137
+ canonicalUrl: "https://example.com/page",
138
+ ogType: "article",
139
+ siteName: "My Site",
140
+ });
141
+ return <div />;
142
+ }
143
+ render(<TestComponent />);
144
+ const payload = getLLMPayload() as Record<string, unknown>;
145
+ expect(payload?.canonicalUrl).toBe("https://example.com/page");
146
+ expect(payload?.ogType).toBe("article");
147
+ expect(payload?.siteName).toBe("My Site");
148
+ });
149
+ });
@@ -5,6 +5,7 @@ export default defineConfig({
5
5
  resolve: {
6
6
  alias: {
7
7
  "@": path.resolve(__dirname, "./src"),
8
+ react: path.resolve(__dirname, "./tests/preact-as-react.ts"),
8
9
  },
9
10
  },
10
11
  test: {