@treelocator/runtime 0.4.7 → 0.6.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.
Files changed (166) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/dejitter/recorder.d.ts +7 -1
  41. package/dist/dejitter/recorder.js +64 -1
  42. package/dist/functions/cssRuleInspector.d.ts +83 -0
  43. package/dist/functions/cssRuleInspector.js +608 -0
  44. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  45. package/dist/functions/cssRuleInspector.test.js +439 -0
  46. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  47. package/dist/functions/deduplicateLabels.test.js +178 -0
  48. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  49. package/dist/functions/extractComputedStyles.d.ts +51 -0
  50. package/dist/functions/extractComputedStyles.js +447 -0
  51. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  52. package/dist/functions/extractComputedStyles.test.js +549 -0
  53. package/dist/functions/formatAncestryChain.d.ts +8 -0
  54. package/dist/functions/formatAncestryChain.js +21 -1
  55. package/dist/functions/formatAncestryChain.test.js +18 -0
  56. package/dist/functions/getUsableName.test.d.ts +1 -0
  57. package/dist/functions/getUsableName.test.js +219 -0
  58. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  59. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  60. package/dist/functions/mergeRects.test.js +210 -1
  61. package/dist/functions/namedSnapshots.d.ts +52 -0
  62. package/dist/functions/namedSnapshots.js +161 -0
  63. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  64. package/dist/functions/namedSnapshots.test.js +85 -0
  65. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  66. package/dist/functions/normalizeFilePath.test.js +66 -0
  67. package/dist/functions/parseDataId.test.d.ts +1 -0
  68. package/dist/functions/parseDataId.test.js +101 -0
  69. package/dist/hooks/getStorage.d.ts +3 -0
  70. package/dist/hooks/getStorage.js +17 -0
  71. package/dist/hooks/useEventListeners.d.ts +15 -0
  72. package/dist/hooks/useEventListeners.js +56 -0
  73. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  74. package/dist/hooks/useLocatorStorage.js +41 -0
  75. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  76. package/dist/hooks/useLocatorStorage.test.js +124 -0
  77. package/dist/hooks/useRecordingState.d.ts +43 -0
  78. package/dist/hooks/useRecordingState.js +387 -0
  79. package/dist/hooks/useSettings.d.ts +13 -0
  80. package/dist/hooks/useSettings.js +66 -0
  81. package/dist/index.d.ts +5 -2
  82. package/dist/index.js +4 -2
  83. package/dist/initRuntime.d.ts +3 -1
  84. package/dist/initRuntime.js +4 -1
  85. package/dist/mcpBridge.d.ts +61 -0
  86. package/dist/mcpBridge.js +534 -0
  87. package/dist/mcpBridge.test.d.ts +1 -0
  88. package/dist/mcpBridge.test.js +248 -0
  89. package/dist/output.css +20 -0
  90. package/dist/visualDiff/diff.d.ts +9 -0
  91. package/dist/visualDiff/diff.js +209 -0
  92. package/dist/visualDiff/diff.test.d.ts +1 -0
  93. package/dist/visualDiff/diff.test.js +253 -0
  94. package/dist/visualDiff/settle.d.ts +3 -0
  95. package/dist/visualDiff/settle.js +50 -0
  96. package/dist/visualDiff/settle.test.d.ts +1 -0
  97. package/dist/visualDiff/settle.test.js +65 -0
  98. package/dist/visualDiff/snapshot.d.ts +4 -0
  99. package/dist/visualDiff/snapshot.js +84 -0
  100. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  101. package/dist/visualDiff/snapshot.test.js +245 -0
  102. package/dist/visualDiff/types.d.ts +37 -0
  103. package/dist/visualDiff/types.js +1 -0
  104. package/package.json +2 -2
  105. package/scripts/wrapCSS.js +1 -1
  106. package/scripts/wrapImage.js +1 -1
  107. package/src/_generated_styles.ts +21 -1
  108. package/src/_generated_tree_icon.ts +1 -1
  109. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  110. package/src/adapters/createTreeNode.ts +12 -51
  111. package/src/adapters/detectFramework.test.ts +73 -0
  112. package/src/adapters/detectFramework.ts +28 -0
  113. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  114. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  115. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  116. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  117. package/src/adapters/react/findDebugSource.ts +5 -6
  118. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  119. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  120. package/src/adapters/react/reactAdapter.ts +1 -2
  121. package/src/adapters/resolveAdapter.ts +4 -14
  122. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  123. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  124. package/src/browserApi.test.ts +329 -0
  125. package/src/browserApi.ts +351 -4
  126. package/src/components/RecordingPillButton.tsx +301 -0
  127. package/src/components/RecordingResults.tsx +114 -13
  128. package/src/components/Runtime.tsx +176 -621
  129. package/src/components/SettingsPanel.tsx +339 -0
  130. package/src/consoleCapture.ts +113 -0
  131. package/src/dejitter/recorder.ts +67 -3
  132. package/src/functions/cssRuleInspector.test.ts +517 -0
  133. package/src/functions/cssRuleInspector.ts +708 -0
  134. package/src/functions/deduplicateLabels.test.ts +115 -0
  135. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  136. package/src/functions/extractComputedStyles.test.ts +681 -0
  137. package/src/functions/extractComputedStyles.ts +768 -0
  138. package/src/functions/formatAncestryChain.test.ts +23 -1
  139. package/src/functions/formatAncestryChain.ts +22 -1
  140. package/src/functions/getUsableName.test.ts +242 -0
  141. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  142. package/src/functions/mergeRects.test.ts +111 -1
  143. package/src/functions/namedSnapshots.test.ts +106 -0
  144. package/src/functions/namedSnapshots.ts +232 -0
  145. package/src/functions/normalizeFilePath.test.ts +80 -0
  146. package/src/functions/parseDataId.test.ts +125 -0
  147. package/src/hooks/getStorage.ts +26 -0
  148. package/src/hooks/useEventListeners.ts +97 -0
  149. package/src/hooks/useLocatorStorage.test.ts +127 -0
  150. package/src/hooks/useLocatorStorage.ts +60 -0
  151. package/src/hooks/useRecordingState.ts +516 -0
  152. package/src/hooks/useSettings.ts +83 -0
  153. package/src/index.ts +10 -5
  154. package/src/initRuntime.ts +5 -0
  155. package/src/mcpBridge.test.ts +260 -0
  156. package/src/mcpBridge.ts +677 -0
  157. package/src/visualDiff/diff.test.ts +167 -0
  158. package/src/visualDiff/diff.ts +242 -0
  159. package/src/visualDiff/settle.test.ts +77 -0
  160. package/src/visualDiff/settle.ts +62 -0
  161. package/src/visualDiff/snapshot.test.ts +200 -0
  162. package/src/visualDiff/snapshot.ts +119 -0
  163. package/src/visualDiff/types.ts +40 -0
  164. package/tsconfig.json +3 -1
  165. package/vitest.config.ts +18 -0
  166. package/jest.config.ts +0 -195
@@ -0,0 +1,83 @@
1
+ import { createRoot, createSignal, type Accessor } from "solid-js";
2
+ import { getStorage } from "./getStorage";
3
+
4
+ export type TreelocatorSettings = {
5
+ anomalyTracking: boolean;
6
+ visualDiff: boolean;
7
+ computedStyles: boolean;
8
+ computedStylesIncludeDefaults: boolean;
9
+ sampleRate: number;
10
+ maxDurationMs: number;
11
+ jumpMinAbsolute: number;
12
+ lagMinDelay: number;
13
+ };
14
+
15
+ export const DEFAULT_SETTINGS: TreelocatorSettings = {
16
+ anomalyTracking: true,
17
+ visualDiff: true,
18
+ computedStyles: true,
19
+ computedStylesIncludeDefaults: false,
20
+ sampleRate: 15,
21
+ maxDurationMs: 30000,
22
+ jumpMinAbsolute: 50,
23
+ lagMinDelay: 50,
24
+ };
25
+
26
+ const STORAGE_KEY = "__treelocator_settings__";
27
+
28
+ function loadSettings(): TreelocatorSettings {
29
+ const storage = getStorage();
30
+ if (!storage) return { ...DEFAULT_SETTINGS };
31
+
32
+ try {
33
+ const raw = storage.getItem(STORAGE_KEY);
34
+ if (raw) return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
35
+ } catch {
36
+ // Ignore localStorage errors (SSR, permissions, quota)
37
+ }
38
+ return { ...DEFAULT_SETTINGS };
39
+ }
40
+
41
+ function persistSettings(next: TreelocatorSettings): void {
42
+ const storage = getStorage();
43
+ if (!storage) return;
44
+
45
+ try {
46
+ storage.setItem(STORAGE_KEY, JSON.stringify(next));
47
+ } catch {
48
+ // Ignore localStorage errors (SSR, permissions, quota)
49
+ }
50
+ }
51
+
52
+ type SettingsAPI = {
53
+ settings: Accessor<TreelocatorSettings>;
54
+ setSetting: <K extends keyof TreelocatorSettings>(
55
+ key: K,
56
+ value: TreelocatorSettings[K]
57
+ ) => void;
58
+ resetSettings: () => void;
59
+ };
60
+
61
+ export const { settings, setSetting, resetSettings }: SettingsAPI = createRoot(
62
+ () => {
63
+ const [settings, setSettingsSignal] = createSignal<TreelocatorSettings>(
64
+ loadSettings()
65
+ );
66
+
67
+ function setSetting<K extends keyof TreelocatorSettings>(
68
+ key: K,
69
+ value: TreelocatorSettings[K]
70
+ ) {
71
+ const next = { ...settings(), [key]: value };
72
+ setSettingsSignal(next);
73
+ persistSettings(next);
74
+ }
75
+
76
+ function resetSettings() {
77
+ setSettingsSignal({ ...DEFAULT_SETTINGS });
78
+ persistSettings(DEFAULT_SETTINGS);
79
+ }
80
+
81
+ return { settings, setSetting, resetSettings };
82
+ }
83
+ );
package/src/index.ts CHANGED
@@ -1,18 +1,23 @@
1
1
  import { Target } from "@locator/shared";
2
2
  import { AdapterId } from "./consts";
3
3
  import { initRuntime } from "./initRuntime";
4
+ import type { MCPBridgeConfig } from "./mcpBridge";
4
5
  export * from "./adapters/jsx/runtimeStore";
5
6
 
6
7
  export const MAX_ZINDEX = 2147483647;
7
8
 
9
+ export interface SetupOptions {
10
+ adapter?: AdapterId;
11
+ targets?: { [k: string]: Target | string };
12
+ mcp?: MCPBridgeConfig;
13
+ }
14
+
8
15
  export function setup({
9
16
  adapter,
10
17
  targets,
11
- }: {
12
- adapter?: AdapterId;
13
- targets?: { [k: string]: Target | string };
14
- } = {}) {
15
- setTimeout(() => initRuntime({ adapter, targets }), 0);
18
+ mcp,
19
+ }: SetupOptions = {}) {
20
+ setTimeout(() => initRuntime({ adapter, targets, mcp }), 0);
16
21
  }
17
22
 
18
23
  export default setup;
@@ -3,13 +3,17 @@ import { AdapterId, fontFamily } from "./consts";
3
3
  import generatedStyles from "./_generated_styles";
4
4
  import { MAX_ZINDEX } from "./index";
5
5
  import { installBrowserAPI } from "./browserApi";
6
+ import { startMCPBridge } from "./mcpBridge";
7
+ import type { MCPBridgeConfig } from "./mcpBridge";
6
8
 
7
9
  export function initRuntime({
8
10
  adapter,
9
11
  targets,
12
+ mcp,
10
13
  }: {
11
14
  adapter?: AdapterId;
12
15
  targets?: { [k: string]: Target | string };
16
+ mcp?: MCPBridgeConfig;
13
17
  } = {}) {
14
18
  if (typeof window === "undefined" || typeof document === "undefined") {
15
19
  return;
@@ -21,6 +25,7 @@ export function initRuntime({
21
25
 
22
26
  // Install browser API on window.__treelocator__
23
27
  installBrowserAPI(adapter);
28
+ startMCPBridge(mcp);
24
29
 
25
30
  // add style tag to head
26
31
  const style = document.createElement("style");
@@ -0,0 +1,260 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { executeBridgeCommand } from "./mcpBridge";
3
+ import {
4
+ clearConsoleEntries,
5
+ installConsoleCapture,
6
+ } from "./consoleCapture";
7
+
8
+ function createApiMock() {
9
+ return {
10
+ getPath: vi.fn().mockResolvedValue("path"),
11
+ getAncestry: vi.fn().mockResolvedValue([{ elementName: "button" }]),
12
+ getPathData: vi
13
+ .fn()
14
+ .mockResolvedValue({ path: "path", ancestry: [{ elementName: "button" }] }),
15
+ getStyles: vi.fn().mockReturnValue({ formatted: "styles", snapshot: {} }),
16
+ getCSSRules: vi.fn().mockReturnValue({ properties: [] }),
17
+ getCSSReport: vi.fn().mockReturnValue("report"),
18
+ takeSnapshot: vi.fn().mockReturnValue({
19
+ snapshotId: "baseline",
20
+ selector: "#b",
21
+ index: 0,
22
+ takenAt: "now",
23
+ propertyCount: 42,
24
+ boundingRect: {},
25
+ }),
26
+ getSnapshotDiff: vi.fn().mockReturnValue({
27
+ snapshotId: "baseline",
28
+ selector: "#b",
29
+ index: 0,
30
+ takenAt: "now",
31
+ formatted: "diff",
32
+ changes: [],
33
+ boundingRectChanges: [],
34
+ }),
35
+ clearSnapshot: vi.fn(),
36
+ help: vi.fn().mockReturnValue("help"),
37
+ replay: vi.fn(),
38
+ replayWithRecord: vi.fn().mockResolvedValue(null),
39
+ diff: {
40
+ snapshot: vi.fn().mockReturnValue([]),
41
+ computeDiff: vi.fn(),
42
+ captureDiff: vi.fn(),
43
+ },
44
+ } as any;
45
+ }
46
+
47
+ describe("mcpBridge executeBridgeCommand", () => {
48
+ test("dispatches get_path to browser API", async () => {
49
+ const api = createApiMock();
50
+ const result = await executeBridgeCommand(api, "get_path", {
51
+ selector: "#target",
52
+ });
53
+
54
+ expect(api.getPath).toHaveBeenCalledWith("#target");
55
+ expect(result).toBe("path");
56
+ });
57
+
58
+ test("click uses selector + index", async () => {
59
+ document.body.innerHTML = `
60
+ <button class="target">one</button>
61
+ <button class="target">two</button>
62
+ `;
63
+
64
+ const buttons = Array.from(document.querySelectorAll(".target"));
65
+ const clickSpy = vi.fn();
66
+ buttons[1]?.addEventListener("click", clickSpy);
67
+
68
+ const api = createApiMock();
69
+ const result = await executeBridgeCommand(api, "click", {
70
+ selector: ".target",
71
+ index: 1,
72
+ });
73
+
74
+ expect(result).toEqual({ ok: true });
75
+ expect(clickSpy).toHaveBeenCalledTimes(1);
76
+ });
77
+
78
+ test("hover dispatches mouse events", async () => {
79
+ document.body.innerHTML = `<div id="hover-me"></div>`;
80
+ const element = document.getElementById("hover-me");
81
+ expect(element).not.toBeNull();
82
+
83
+ const events: string[] = [];
84
+ element?.addEventListener("mouseenter", () => events.push("mouseenter"));
85
+ element?.addEventListener("mouseover", () => events.push("mouseover"));
86
+ element?.addEventListener("mousemove", () => events.push("mousemove"));
87
+
88
+ const api = createApiMock();
89
+ await executeBridgeCommand(api, "hover", { selector: "#hover-me" });
90
+
91
+ expect(events).toEqual(["mouseenter", "mouseover", "mousemove"]);
92
+ });
93
+
94
+ test("type updates input value and dispatches input event", async () => {
95
+ document.body.innerHTML = `<input id="field" />`;
96
+ const input = document.getElementById("field") as HTMLInputElement;
97
+ const inputEventSpy = vi.fn();
98
+ input.addEventListener("input", inputEventSpy);
99
+
100
+ const api = createApiMock();
101
+ const result = await executeBridgeCommand(api, "type", {
102
+ selector: "#field",
103
+ text: "hello",
104
+ });
105
+
106
+ expect(input.value).toBe("hello");
107
+ expect(result).toEqual({ value: "hello" });
108
+ expect(inputEventSpy).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ test("take_snapshot forwards selector + snapshotId to browser API", async () => {
112
+ const api = createApiMock();
113
+ const result = await executeBridgeCommand(api, "take_snapshot", {
114
+ selector: "#b",
115
+ snapshotId: "baseline",
116
+ index: 2,
117
+ label: "before fix",
118
+ });
119
+
120
+ expect(api.takeSnapshot).toHaveBeenCalledWith("#b", "baseline", {
121
+ index: 2,
122
+ label: "before fix",
123
+ });
124
+ expect(result).toMatchObject({ snapshotId: "baseline" });
125
+ });
126
+
127
+ test("take_snapshot rejects missing snapshotId", async () => {
128
+ const api = createApiMock();
129
+ await expect(
130
+ executeBridgeCommand(api, "take_snapshot", { selector: "#b" })
131
+ ).rejects.toThrow("snapshotId is required");
132
+ });
133
+
134
+ test("get_snapshot_diff forwards snapshotId to browser API", async () => {
135
+ const api = createApiMock();
136
+ const result = await executeBridgeCommand(api, "get_snapshot_diff", {
137
+ snapshotId: "baseline",
138
+ });
139
+ expect(api.getSnapshotDiff).toHaveBeenCalledWith("baseline");
140
+ expect(result).toMatchObject({ snapshotId: "baseline", formatted: "diff" });
141
+ });
142
+
143
+ test("clear_snapshot forwards snapshotId to browser API", async () => {
144
+ const api = createApiMock();
145
+ const result = await executeBridgeCommand(api, "clear_snapshot", {
146
+ snapshotId: "baseline",
147
+ });
148
+ expect(api.clearSnapshot).toHaveBeenCalledWith("baseline");
149
+ expect(result).toEqual({ ok: true });
150
+ });
151
+
152
+ describe("execute_js", () => {
153
+ test("returns serialized value from async function body", async () => {
154
+ const api = createApiMock();
155
+ const result = await executeBridgeCommand(api, "execute_js", {
156
+ code: "return 1 + 2;",
157
+ });
158
+ expect(result).toEqual({ type: "number", value: 3 });
159
+ });
160
+
161
+ test("awaits returned promise", async () => {
162
+ const api = createApiMock();
163
+ const result = await executeBridgeCommand(api, "execute_js", {
164
+ code: "return await Promise.resolve({ hello: 'world' });",
165
+ });
166
+ expect(result).toEqual({
167
+ type: "object",
168
+ value: { hello: "world" },
169
+ });
170
+ });
171
+
172
+ test("can read from the document", async () => {
173
+ document.body.innerHTML = `<div id="greeting">hi</div>`;
174
+ const api = createApiMock();
175
+ const result = await executeBridgeCommand(api, "execute_js", {
176
+ code: "return document.getElementById('greeting').textContent;",
177
+ });
178
+ expect(result).toEqual({ type: "string", value: "hi" });
179
+ });
180
+
181
+ test("wraps runtime errors with stack details", async () => {
182
+ const api = createApiMock();
183
+ await expect(
184
+ executeBridgeCommand(api, "execute_js", {
185
+ code: "throw new Error('boom');",
186
+ })
187
+ ).rejects.toThrow("boom");
188
+ });
189
+
190
+ test("rejects compile errors", async () => {
191
+ const api = createApiMock();
192
+ await expect(
193
+ executeBridgeCommand(api, "execute_js", { code: "return (" })
194
+ ).rejects.toThrow();
195
+ });
196
+
197
+ test("rejects empty code", async () => {
198
+ const api = createApiMock();
199
+ await expect(
200
+ executeBridgeCommand(api, "execute_js", { code: "" })
201
+ ).rejects.toThrow("code is required");
202
+ });
203
+ });
204
+
205
+ describe("get_console", () => {
206
+ beforeEach(() => {
207
+ installConsoleCapture();
208
+ clearConsoleEntries();
209
+ });
210
+
211
+ test("captures console.log and returns last N entries", async () => {
212
+ console.log("first");
213
+ console.warn("second");
214
+ console.error("third");
215
+
216
+ const api = createApiMock();
217
+ const result = (await executeBridgeCommand(api, "get_console", {
218
+ last: 2,
219
+ })) as { count: number; entries: Array<{ level: string; message: string }> };
220
+
221
+ expect(result.count).toBe(2);
222
+ expect(result.entries.map((e) => e.level)).toEqual(["warn", "error"]);
223
+ expect(result.entries.map((e) => e.message)).toEqual(["second", "third"]);
224
+ });
225
+
226
+ test("returns all entries when last is omitted", async () => {
227
+ console.log("a");
228
+ console.log("b");
229
+
230
+ const api = createApiMock();
231
+ const result = (await executeBridgeCommand(api, "get_console", {})) as {
232
+ count: number;
233
+ };
234
+ expect(result.count).toBe(2);
235
+ });
236
+
237
+ test("formats multiple args joined by spaces", async () => {
238
+ console.log("count is", 42, { ok: true });
239
+
240
+ const api = createApiMock();
241
+ const result = (await executeBridgeCommand(api, "get_console", {
242
+ last: 1,
243
+ })) as { entries: Array<{ message: string }> };
244
+
245
+ expect(result.entries[0]?.message).toBe('count is 42 {"ok":true}');
246
+ });
247
+ });
248
+
249
+ test("type rejects unsupported elements", async () => {
250
+ document.body.innerHTML = `<div id="not-input"></div>`;
251
+ const api = createApiMock();
252
+
253
+ await expect(
254
+ executeBridgeCommand(api, "type", {
255
+ selector: "#not-input",
256
+ text: "hello",
257
+ })
258
+ ).rejects.toThrow("type target must be input, textarea, or contenteditable");
259
+ });
260
+ });