gentle-shell-bridge 0.3.0 → 0.3.2
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/LICENSE +202 -0
- package/README.md +91 -722
- package/docs/contract.md +92 -35
- package/package.json +13 -1
- package/src/api.ts +1 -1
- package/.codegraph/.gitignore +0 -5
- package/PRD.md +0 -144
- package/examples/bash-card.ts +0 -1694
- package/gentle-shell-bridge-0.3.0.tgz +0 -0
- package/odd/tasks/palette-sections.md +0 -30
- package/odd/tasks/plugin-keymap.md +0 -32
- package/odd/tasks/plugin-surface-contract.md +0 -55
- package/test/api.test.ts +0 -395
- package/test/apply-on-move.test.ts +0 -477
- package/test/arm.test.ts +0 -220
- package/test/bridge-ready.test.ts +0 -309
- package/test/bus.test.ts +0 -165
- package/test/button-rows.test.ts +0 -409
- package/test/capture.test.ts +0 -356
- package/test/card-style-rows.test.ts +0 -510
- package/test/card-style-transform.test.ts +0 -379
- package/test/card-style-wiring.test.ts +0 -258
- package/test/card-styles-e2e.test.ts +0 -491
- package/test/card-styles.test.ts +0 -324
- package/test/card-transcript-path.test.ts +0 -178
- package/test/config.test.ts +0 -93
- package/test/e2e.test.ts +0 -443
- package/test/example-bash-identity.test.ts +0 -372
- package/test/example-bash-matrix.test.ts +0 -856
- package/test/example-designs.test.ts +0 -734
- package/test/example-neon-transform.test.ts +0 -283
- package/test/example-plain-style.test.ts +0 -1268
- package/test/extension.test.ts +0 -160
- package/test/fake-events.ts +0 -58
- package/test/fake-host.ts +0 -211
- package/test/fake-sidebar.ts +0 -516
- package/test/fake-transcript.ts +0 -486
- package/test/frame-telemetry.test.ts +0 -417
- package/test/hook.test.ts +0 -196
- package/test/manifest.test.ts +0 -105
- package/test/new-category.test.ts +0 -979
- package/test/notices.test.ts +0 -328
- package/test/plugin-integration-lines.test.ts +0 -979
- package/test/plugin-surface.test.ts +0 -152
- package/test/presence.test.ts +0 -459
- package/test/rail-surfaces.test.ts +0 -457
- package/test/rail-width.test.ts +0 -423
- package/test/regression.test.ts +0 -483
- package/test/repaint-api.test.ts +0 -200
- package/test/reserved-categories.test.ts +0 -361
- package/test/row-labels.test.ts +0 -284
- package/test/row-names.test.ts +0 -473
- package/test/row-origin.test.ts +0 -395
- package/test/section-interaction.test.ts +0 -364
- package/test/section-panel.test.ts +0 -403
- package/test/sections-rows.test.ts +0 -291
- package/test/sidebar-backend.test.ts +0 -471
- package/test/sidebar-e2e.test.ts +0 -331
- package/test/sidebar-hover.test.ts +0 -276
- package/test/sidebar-independent.test.ts +0 -349
- package/test/sidebar-render.test.ts +0 -281
- package/test/sidebar-sections.test.ts +0 -245
- package/test/status-card-styles.test.ts +0 -201
- package/test/status-section-frame.test.ts +0 -181
- package/test/status-sections.test.ts +0 -243
- package/test/status.test.ts +0 -209
- package/test/store.test.ts +0 -192
- package/test/surface-repaint-cost.test.ts +0 -520
- package/test/tool-design-chain.test.ts +0 -483
- package/test/tool-design-cost.test.ts +0 -473
- package/test/tool-design-rows.test.ts +0 -359
- package/test/tool-designs.test.ts +0 -745
- package/test/transform-pipeline.test.ts +0 -465
- package/test/transforms-bus.test.ts +0 -233
- package/test/verifier-width-regressions.test.ts +0 -164
- package/tsconfig.json +0 -18
|
@@ -1,734 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* T10: `examples/bash-card.ts` as a PLUGIN — the handshake, the sidebar section, and the surfaces
|
|
3
|
-
* it contributes.
|
|
4
|
-
*
|
|
5
|
-
* The example is what a plugin author copies, so what is proven here is its whole contract:
|
|
6
|
-
*
|
|
7
|
-
* - it reaches the bridge through the symbol and the `gentle-shell-bridge:ready` announcement, with
|
|
8
|
-
* no path, no environment variable and no import (both load orders work);
|
|
9
|
-
* - it registers its design for `bash`, its `plain` card style and its sidebar section, and its own
|
|
10
|
-
* `session_start` handler is the only entry point (the demo row provider is covered here too);
|
|
11
|
-
* - the design's `Cards` line appears with no extra wiring, and the section reads the live selection;
|
|
12
|
-
* - disposing the load leaves neither surface behind;
|
|
13
|
-
* - nothing is ever written to stdout/stderr.
|
|
14
|
-
*
|
|
15
|
-
* The `plain` card style's own coverage lives in `test/example-plain-style.test.ts`, restored from
|
|
16
|
-
* the two suites that were deleted with the style (`5c0c123:test/example-transform.test.ts`,
|
|
17
|
-
* `5c0c123:test/example-late-load.test.ts`) and extended with the integration matrix.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import assert from "node:assert/strict";
|
|
21
|
-
import { spawnSync } from "node:child_process";
|
|
22
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
23
|
-
import { tmpdir } from "node:os";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import test from "node:test";
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
BRIDGE_API_KEY,
|
|
29
|
-
BRIDGE_READY_EVENT,
|
|
30
|
-
announceBridgeReady,
|
|
31
|
-
bridgeApi,
|
|
32
|
-
publishBridgeApi,
|
|
33
|
-
resetBridgeApiForTests,
|
|
34
|
-
} from "../src/api.ts";
|
|
35
|
-
import { resetBusForTests, resetSectionsForTests, snapshotSections } from "../src/bus.ts";
|
|
36
|
-
import gentleShellBridge, { type BridgeExtensionApi } from "../src/extension.ts";
|
|
37
|
-
import { resetNoticesForTests } from "../src/notices.ts";
|
|
38
|
-
import { resetPluginLinesForTests } from "../src/plugin-lines.ts";
|
|
39
|
-
import { buildRows, resetRowBuildStateForTests } from "../src/rows.ts";
|
|
40
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
41
|
-
import {
|
|
42
|
-
TOOL_DESIGN_STORE_ID,
|
|
43
|
-
getToolDesignSelection,
|
|
44
|
-
resetToolDesignsForTests,
|
|
45
|
-
setToolDesignSelection,
|
|
46
|
-
snapshotToolDesigns,
|
|
47
|
-
} from "../src/tool-designs.ts";
|
|
48
|
-
import { resetToolDesignRowStateForTests, installToolDesignRows } from "../src/tool-design-rows.ts";
|
|
49
|
-
import { resetToolDesignWiringForTests } from "../src/tool-design-wiring.ts";
|
|
50
|
-
import type { CustomizeRow, InlineHelpers, SectionRow, SidebarSection } from "../src/types.ts";
|
|
51
|
-
import { createFakeEvents, type FakeEvents } from "./fake-events.ts";
|
|
52
|
-
|
|
53
|
-
type SessionHandler = (event: unknown, ctx: { hasUI?: boolean; ui?: unknown }) => void;
|
|
54
|
-
|
|
55
|
-
interface FakePi {
|
|
56
|
-
on?: (event: string, handler: SessionHandler) => void;
|
|
57
|
-
events?: FakeEvents;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type ExampleInit = (pi: FakePi) => () => void;
|
|
61
|
-
|
|
62
|
-
/** One fresh module instance per scenario: the example keeps nothing on `globalThis`. */
|
|
63
|
-
const loadExample = async (tag: string): Promise<ExampleInit> => {
|
|
64
|
-
const specifier = new URL(`../examples/bash-card.ts?${tag}`, import.meta.url).href;
|
|
65
|
-
return ((await import(specifier)) as { default: ExampleInit }).default;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const hostSlot = (): Record<symbol, unknown> => globalThis as unknown as Record<symbol, unknown>;
|
|
69
|
-
|
|
70
|
-
const recordingPi = (events?: FakeEvents): { pi: FakePi; sessionHandlers: SessionHandler[] } => {
|
|
71
|
-
const sessionHandlers: SessionHandler[] = [];
|
|
72
|
-
const pi: FakePi = {
|
|
73
|
-
on(_event, handler) {
|
|
74
|
-
sessionHandlers.push(handler);
|
|
75
|
-
},
|
|
76
|
-
...(events === undefined ? {} : { events }),
|
|
77
|
-
};
|
|
78
|
-
return { pi, sessionHandlers };
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/** The package loading in the same process: publishing, then announcing itself. */
|
|
82
|
-
const loadBridge = (events: FakeEvents): void => {
|
|
83
|
-
const pi: BridgeExtensionApi = { on() {}, registerCommand() {}, events };
|
|
84
|
-
gentleShellBridge(pi);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const notifySurface = (): {
|
|
88
|
-
ui: { notify: (message: string, level?: string) => void };
|
|
89
|
-
calls: Array<[string, string | undefined]>;
|
|
90
|
-
} => {
|
|
91
|
-
const calls: Array<[string, string | undefined]> = [];
|
|
92
|
-
return { ui: { notify: (message, level) => calls.push([message, level]) }, calls };
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** How many of `plain` and of the section the example owns right now (the design is checked too). */
|
|
96
|
-
const surfaceCounts = (): number[] => [
|
|
97
|
-
snapshotToolDesigns("bash").filter((design) => design.id === "bash-card.bash-heading").length,
|
|
98
|
-
snapshotSections().filter((section) => section.id === "bash-card.status").length,
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const DESIGN_AND_SECTION = [1, 1];
|
|
102
|
-
|
|
103
|
-
/** Every terminal write the process would show: the example must produce none. */
|
|
104
|
-
const captureConsole = (): { writes: string[]; restore: () => void } => {
|
|
105
|
-
const writes: string[] = [];
|
|
106
|
-
const sink = (globalThis as unknown as { console: { log: unknown; warn: unknown; error: unknown } })
|
|
107
|
-
.console;
|
|
108
|
-
const original = { log: sink.log, warn: sink.warn, error: sink.error };
|
|
109
|
-
const record =
|
|
110
|
-
(method: string) =>
|
|
111
|
-
(...args: unknown[]): void => {
|
|
112
|
-
writes.push(`${method}: ${args.map((arg) => String(arg)).join(" ")}`);
|
|
113
|
-
};
|
|
114
|
-
sink.log = record("log");
|
|
115
|
-
sink.warn = record("warn");
|
|
116
|
-
sink.error = record("error");
|
|
117
|
-
return {
|
|
118
|
-
writes,
|
|
119
|
-
restore: () => {
|
|
120
|
-
sink.log = original.log;
|
|
121
|
-
sink.warn = original.warn;
|
|
122
|
-
sink.error = original.error;
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const sectionOf = (): SidebarSection => {
|
|
128
|
-
const section = snapshotSections().find((candidate) => candidate.id === "bash-card.status");
|
|
129
|
-
assert.ok(section !== undefined, "the example registers CARD BASH");
|
|
130
|
-
return section;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const context = (): { theme: unknown; configHome: string } => ({ theme: undefined, configHome });
|
|
134
|
-
|
|
135
|
-
/** The inline helpers a demo row's action is handed; none of them are used by the trivial demo. */
|
|
136
|
-
const inlineHelpers: InlineHelpers = {
|
|
137
|
-
input: async () => undefined,
|
|
138
|
-
confirm: async () => true,
|
|
139
|
-
capture: async () => undefined,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
let configHome: string;
|
|
143
|
-
let previousConfigHome: string | undefined;
|
|
144
|
-
let previousModuleEnv: string | undefined;
|
|
145
|
-
|
|
146
|
-
test.before(() => {
|
|
147
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-example-designs-"));
|
|
148
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
149
|
-
previousModuleEnv = process.env.GENTLE_SHELL_BRIDGE_MODULE;
|
|
150
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
151
|
-
delete process.env.GENTLE_SHELL_BRIDGE_MODULE;
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test.after(() => {
|
|
155
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
156
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
157
|
-
if (previousModuleEnv === undefined) delete process.env.GENTLE_SHELL_BRIDGE_MODULE;
|
|
158
|
-
else process.env.GENTLE_SHELL_BRIDGE_MODULE = previousModuleEnv;
|
|
159
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test.beforeEach(() => {
|
|
163
|
-
rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
|
|
164
|
-
resetBridgeApiForTests();
|
|
165
|
-
resetBusForTests();
|
|
166
|
-
resetSectionsForTests();
|
|
167
|
-
resetNoticesForTests();
|
|
168
|
-
resetPluginLinesForTests();
|
|
169
|
-
resetRowBuildStateForTests();
|
|
170
|
-
resetStoreStateForTests();
|
|
171
|
-
resetToolDesignsForTests();
|
|
172
|
-
resetToolDesignRowStateForTests();
|
|
173
|
-
resetToolDesignWiringForTests();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------------------------
|
|
177
|
-
// The handshake: absent, present, and loaded-first
|
|
178
|
-
// ---------------------------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
test("with no bridge installed the example adds nothing and says so once", async () => {
|
|
181
|
-
const captured = captureConsole();
|
|
182
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
183
|
-
try {
|
|
184
|
-
const init = await loadExample("no-bridge");
|
|
185
|
-
assert.doesNotThrow(() => init(pi));
|
|
186
|
-
} finally {
|
|
187
|
-
captured.restore();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
assert.deepEqual(captured.writes, [], "nothing lands in the terminal");
|
|
191
|
-
assert.equal(sessionHandlers.length, 1, "a session_start handler carries the answer");
|
|
192
|
-
|
|
193
|
-
const { ui, calls } = notifySurface();
|
|
194
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
195
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
196
|
-
assert.equal(calls.length, 1, "one notice, delivered exactly once");
|
|
197
|
-
assert.equal(calls[0][1], "warning");
|
|
198
|
-
assert.match(calls[0][0], /no compatible gentle-shell-bridge is installed/u);
|
|
199
|
-
assert.match(calls[0][0], /no cross-extension event bus/u);
|
|
200
|
-
assert.match(calls[0][0], /adds nothing/u);
|
|
201
|
-
assert.deepEqual(surfaceCounts(), [0, 0]);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("the absent notice waits for a UI and is delivered exactly once", async () => {
|
|
205
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
206
|
-
(await loadExample("absent-waits-for-ui"))(pi);
|
|
207
|
-
|
|
208
|
-
const { ui, calls } = notifySurface();
|
|
209
|
-
sessionHandlers[0]({}, { hasUI: false, ui });
|
|
210
|
-
sessionHandlers[0]({}, {});
|
|
211
|
-
assert.deepEqual(calls, [], "a session without a UI shows nothing");
|
|
212
|
-
|
|
213
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
214
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
215
|
-
assert.equal(calls.length, 1, "the buffered message waits for a UI that can show it");
|
|
216
|
-
assert.match(calls[0][0], /no compatible gentle-shell-bridge is installed/u);
|
|
217
|
-
assert.deepEqual(surfaceCounts(), [0, 0]);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("an old or foreign bridge is treated as absent, and only through notify", async () => {
|
|
221
|
-
hostSlot()[BRIDGE_API_KEY] = { schema: "gentle-shell-bridge/api", version: 0 };
|
|
222
|
-
const captured = captureConsole();
|
|
223
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
224
|
-
try {
|
|
225
|
-
(await loadExample("old-bridge"))(pi);
|
|
226
|
-
} finally {
|
|
227
|
-
captured.restore();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
assert.deepEqual(captured.writes, [], "nothing lands in the terminal");
|
|
231
|
-
const { ui, calls } = notifySurface();
|
|
232
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
233
|
-
assert.equal(calls.length, 1);
|
|
234
|
-
assert.match(calls[0][0], /no compatible gentle-shell-bridge is installed/u);
|
|
235
|
-
assert.deepEqual(surfaceCounts(), [0, 0]);
|
|
236
|
-
assert.equal(hostSlot()[BRIDGE_API_KEY] !== undefined, true, "the slot it cannot use is left alone");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("the session handler does nothing without a UI, then registers on the next one", async () => {
|
|
240
|
-
publishBridgeApi();
|
|
241
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
242
|
-
(await loadExample("no-ui"))(pi);
|
|
243
|
-
|
|
244
|
-
const { ui, calls } = notifySurface();
|
|
245
|
-
sessionHandlers[0]({}, { hasUI: false, ui });
|
|
246
|
-
sessionHandlers[0]({}, {});
|
|
247
|
-
assert.deepEqual(calls, []);
|
|
248
|
-
assert.deepEqual(surfaceCounts(), [0, 0]);
|
|
249
|
-
|
|
250
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
251
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("two session_start events leave one design and one section, not two", async () => {
|
|
255
|
-
publishBridgeApi();
|
|
256
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
257
|
-
(await loadExample("twice"))(pi);
|
|
258
|
-
const { ui } = notifySurface();
|
|
259
|
-
|
|
260
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
261
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
262
|
-
|
|
263
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("the disposer removes both the design and the section", async () => {
|
|
267
|
-
publishBridgeApi();
|
|
268
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
269
|
-
const dispose = (await loadExample("dispose"))(pi);
|
|
270
|
-
const { ui } = notifySurface();
|
|
271
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
272
|
-
|
|
273
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
274
|
-
dispose();
|
|
275
|
-
assert.deepEqual(surfaceCounts(), [0, 0]);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("an older bridge object without registerToolDesign keeps the section and says absent", async () => {
|
|
279
|
-
const published = bridgeApi() ?? publishBridgeApi();
|
|
280
|
-
// First-wins: a mixed process can hand back an object that predates the design bus.
|
|
281
|
-
hostSlot()[BRIDGE_API_KEY] = {
|
|
282
|
-
schema: published.schema,
|
|
283
|
-
version: published.version,
|
|
284
|
-
VERSION: published.VERSION,
|
|
285
|
-
registerRows: published.registerRows,
|
|
286
|
-
registerSection: published.registerSection,
|
|
287
|
-
createStore: published.createStore,
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
291
|
-
const dispose = (await loadExample("old-object"))(pi);
|
|
292
|
-
const { ui, calls } = notifySurface();
|
|
293
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
294
|
-
|
|
295
|
-
assert.deepEqual(calls, [], "a surface the bridge never promised is not warned about");
|
|
296
|
-
assert.deepEqual(snapshotToolDesigns("bash"), [], "and no design is registered");
|
|
297
|
-
assert.deepEqual(surfaceCounts(), [0, 1], "the section still registers");
|
|
298
|
-
assert.equal(sectionOf().lines(context())[0], "resolver absent", "the resolver report is now the first body row");
|
|
299
|
-
|
|
300
|
-
dispose();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// ---------------------------------------------------------------------------------------------
|
|
304
|
-
// The rows and the section: what the user reads
|
|
305
|
-
// ---------------------------------------------------------------------------------------------
|
|
306
|
-
|
|
307
|
-
test("the Cards line appears with no extra wiring, and the section reads the live selection", async () => {
|
|
308
|
-
publishBridgeApi();
|
|
309
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
310
|
-
(await loadExample("live-state"))(pi);
|
|
311
|
-
const { ui } = notifySurface();
|
|
312
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
313
|
-
|
|
314
|
-
installToolDesignRows();
|
|
315
|
-
const rows: CustomizeRow[] = buildRows({ theme: undefined, configHome }).rows;
|
|
316
|
-
const designLine = rows.filter((row) => isBashDesignSetting(row.choice?.setting));
|
|
317
|
-
assert.deepEqual(
|
|
318
|
-
designLine.map((row) => row.choice?.value),
|
|
319
|
-
["native", "bash heading"],
|
|
320
|
-
"the tool's own card is the default, the design is the other value",
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
const section = sectionOf();
|
|
324
|
-
assert.equal(section.title, "CARD BASH");
|
|
325
|
-
assert.equal(section.rowLabel, "card bash");
|
|
326
|
-
assert.equal(section.meta?.(context()), "native", "the header count is the live selection");
|
|
327
|
-
|
|
328
|
-
const lines = section.lines(context());
|
|
329
|
-
assert.equal(
|
|
330
|
-
lines.some((line) => typeof line !== "string" && line.label === "selected"),
|
|
331
|
-
false,
|
|
332
|
-
"the live selection rides the header meta, never a duplicate body row",
|
|
333
|
-
);
|
|
334
|
-
assert.equal(lines[0], "resolver registered", "a plain string row: the design is on the bus");
|
|
335
|
-
const presses = lines.find(
|
|
336
|
-
(line): line is SectionRow => typeof line !== "string" && line.label === "demo presses",
|
|
337
|
-
);
|
|
338
|
-
assert.equal(presses?.onClick === undefined, false, "the section's one action row is the demo");
|
|
339
|
-
|
|
340
|
-
setToolDesignSelection("bash", "bash-card.bash-heading");
|
|
341
|
-
|
|
342
|
-
assert.equal(section.meta?.(context()), "bash heading", "the same section object reads the value");
|
|
343
|
-
assert.equal(
|
|
344
|
-
section
|
|
345
|
-
.lines(context())
|
|
346
|
-
.some((line) => typeof line !== "string" && line.label === "selected"),
|
|
347
|
-
false,
|
|
348
|
-
"and selecting a design still adds no second copy of the value",
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
// The selection lives in the BRIDGE's own file, not in one of the example's.
|
|
352
|
-
const stored = JSON.parse(
|
|
353
|
-
readFileSync(join(configHome, "gentle-shell-bridge", `${TOOL_DESIGN_STORE_ID}.json`), "utf8"),
|
|
354
|
-
);
|
|
355
|
-
assert.deepEqual(stored.values.selections, { bash: "bash-card.bash-heading" });
|
|
356
|
-
assert.equal(getToolDesignSelection("bash"), "bash-card.bash-heading");
|
|
357
|
-
|
|
358
|
-
const settings = rows.map((row) => row.choice?.setting).filter((setting) => setting !== undefined);
|
|
359
|
-
assert.equal(
|
|
360
|
-
settings.some((setting) => /bash-card\./u.test(setting)),
|
|
361
|
-
false,
|
|
362
|
-
"no dotted id reaches a row name",
|
|
363
|
-
);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// ---------------------------------------------------------------------------------------------
|
|
367
|
-
// The demo category: a toggle and a button in a category the plugin invents
|
|
368
|
-
// ---------------------------------------------------------------------------------------------
|
|
369
|
-
|
|
370
|
-
const DEMO_CATEGORY = "Card bash";
|
|
371
|
-
|
|
372
|
-
/** The tool-design line's setting, old bridge (`bash`) and current bridge (`Card bash`). */
|
|
373
|
-
const isBashDesignSetting = (setting: string | undefined): boolean =>
|
|
374
|
-
setting === "bash" || setting === "Card bash";
|
|
375
|
-
|
|
376
|
-
/** The category's rows, through the real provider and the real build. */
|
|
377
|
-
const demoRows = (): CustomizeRow[] =>
|
|
378
|
-
buildRows({ theme: undefined, configHome }).rows.filter((row) => row.category === DEMO_CATEGORY);
|
|
379
|
-
|
|
380
|
-
const sectionStrings = (): string[] =>
|
|
381
|
-
sectionOf()
|
|
382
|
-
.lines(context())
|
|
383
|
-
.filter((line): line is string => typeof line === "string");
|
|
384
|
-
|
|
385
|
-
/** The `demo presses` ACTION row's live meta, read through the real section. */
|
|
386
|
-
const pressesMeta = (): string | undefined =>
|
|
387
|
-
sectionOf()
|
|
388
|
-
.lines(context())
|
|
389
|
-
.find(
|
|
390
|
-
(line): line is SectionRow => typeof line !== "string" && line.label === "demo presses",
|
|
391
|
-
)?.meta;
|
|
392
|
-
|
|
393
|
-
const demoStoreValues = (): Record<string, unknown> => {
|
|
394
|
-
const document = JSON.parse(
|
|
395
|
-
readFileSync(join(configHome, "gentle-shell-bridge", "bash-card.json"), "utf8"),
|
|
396
|
-
) as { values: Record<string, unknown> };
|
|
397
|
-
return document.values;
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
test("a plugin adds its OWN category with a toggle and a button, through one provider", async () => {
|
|
401
|
-
publishBridgeApi();
|
|
402
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
403
|
-
(await loadExample("demo-category"))(pi);
|
|
404
|
-
const { ui } = notifySurface();
|
|
405
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
406
|
-
|
|
407
|
-
const rows = demoRows();
|
|
408
|
-
assert.deepEqual(
|
|
409
|
-
rows.map((row) => row.label),
|
|
410
|
-
["Demo toggle", "Demo toggle", "Demo button"],
|
|
411
|
-
"a two-value toggle (one line) and a plain button, through one provider",
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const [on, off, button] = rows;
|
|
415
|
-
assert.equal(on?.choice?.setting, "Demo toggle");
|
|
416
|
-
assert.equal(off?.choice?.setting, "Demo toggle");
|
|
417
|
-
assert.deepEqual([on?.choice?.value, off?.choice?.value], ["on", "off"]);
|
|
418
|
-
assert.equal(button?.choice, undefined, "the button carries no choice");
|
|
419
|
-
assert.equal(typeof button?.action, "function", "Enter can run it");
|
|
420
|
-
|
|
421
|
-
// The category is created by the row, exactly once, and the default is `off`.
|
|
422
|
-
const categories = [...new Set(buildRows({ theme: undefined, configHome }).rows.map((row) => row.category))];
|
|
423
|
-
assert.equal(categories.filter((category) => category === DEMO_CATEGORY).length, 1);
|
|
424
|
-
assert.equal(on?.choice?.current(), false, "the stored default is off");
|
|
425
|
-
assert.equal(off?.choice?.current(), true);
|
|
426
|
-
|
|
427
|
-
for (const row of rows) {
|
|
428
|
-
const preview = row.preview?.();
|
|
429
|
-
assert.ok(preview !== undefined, "every demo row depicts itself");
|
|
430
|
-
assert.equal([...preview.title].length < 40, true, "a short literal title");
|
|
431
|
-
for (const line of preview.sample.split("\n")) {
|
|
432
|
-
assert.equal([...line].length < 40, true, "a short literal sample row");
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const section = sectionStrings();
|
|
437
|
-
assert.equal(section.includes("demo toggle off"), true, "the sidebar shows the live value");
|
|
438
|
-
assert.equal(
|
|
439
|
-
pressesMeta(),
|
|
440
|
-
"0",
|
|
441
|
-
"the clickable `demo presses` row carries the live count as its right-aligned meta",
|
|
442
|
-
);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("choosing the demo toggle persists it and the sidebar shows the new value", async () => {
|
|
446
|
-
publishBridgeApi();
|
|
447
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
448
|
-
(await loadExample("demo-toggle"))(pi);
|
|
449
|
-
const { ui } = notifySurface();
|
|
450
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
451
|
-
|
|
452
|
-
const onRow = demoRows().find(
|
|
453
|
-
(row) => row.choice?.setting === "Demo toggle" && row.choice.value === "on",
|
|
454
|
-
);
|
|
455
|
-
assert.ok(onRow?.action !== undefined);
|
|
456
|
-
await onRow.action(inlineHelpers);
|
|
457
|
-
|
|
458
|
-
assert.equal(onRow.choice?.current(), true, "the same row reads the new value");
|
|
459
|
-
assert.equal(sectionStrings().includes("demo toggle on"), true, "the sidebar shows it live");
|
|
460
|
-
assert.equal(demoStoreValues().demoToggle, "on", "and it is persisted in the example's own store");
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
test("the demo button counts its presses in the section, through the example's store", async () => {
|
|
464
|
-
publishBridgeApi();
|
|
465
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
466
|
-
(await loadExample("demo-button"))(pi);
|
|
467
|
-
const { ui } = notifySurface();
|
|
468
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
469
|
-
|
|
470
|
-
const button = demoRows().find((row) => row.label === "Demo button");
|
|
471
|
-
assert.ok(button?.action !== undefined);
|
|
472
|
-
await button.action(inlineHelpers);
|
|
473
|
-
await button.action(inlineHelpers);
|
|
474
|
-
|
|
475
|
-
assert.equal(pressesMeta(), "2", "Enter increments the counter");
|
|
476
|
-
assert.equal(demoStoreValues().demoPresses, 2, "and the count is persisted");
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test("a second registration replaces the demo provider instead of stacking a category", async () => {
|
|
480
|
-
publishBridgeApi();
|
|
481
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
482
|
-
(await loadExample("demo-reload-1"))(pi);
|
|
483
|
-
const { ui } = notifySurface();
|
|
484
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
485
|
-
|
|
486
|
-
const onRow = demoRows().find(
|
|
487
|
-
(row) => row.choice?.setting === "Demo toggle" && row.choice.value === "on",
|
|
488
|
-
);
|
|
489
|
-
const button = demoRows().find((row) => row.label === "Demo button");
|
|
490
|
-
assert.ok(onRow?.action !== undefined && button?.action !== undefined);
|
|
491
|
-
await onRow.action(inlineHelpers);
|
|
492
|
-
await button.action(inlineHelpers);
|
|
493
|
-
|
|
494
|
-
// A `/reload` loads the file again: the same provider id must replace, not duplicate.
|
|
495
|
-
(await loadExample("demo-reload-2"))(pi);
|
|
496
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
497
|
-
|
|
498
|
-
assert.equal(demoRows().length, 3, "one category, one toggle line, one button");
|
|
499
|
-
const section = sectionStrings();
|
|
500
|
-
assert.equal(section.includes("demo toggle on"), true, "the stored toggle survives the reload");
|
|
501
|
-
assert.equal(pressesMeta(), "1", "and so does the press count");
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
test("an older bridge without registerRows keeps the design and section, and adds no category", async () => {
|
|
505
|
-
const published = bridgeApi() ?? publishBridgeApi();
|
|
506
|
-
hostSlot()[BRIDGE_API_KEY] = {
|
|
507
|
-
schema: published.schema,
|
|
508
|
-
version: published.version,
|
|
509
|
-
VERSION: published.VERSION,
|
|
510
|
-
registerToolDesign: published.registerToolDesign,
|
|
511
|
-
registerSection: published.registerSection,
|
|
512
|
-
createStore: published.createStore,
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
516
|
-
// `registerRows` is absent from this object: the example must degrade, not throw.
|
|
517
|
-
const dispose = (await loadExample("demo-old-bridge"))(pi);
|
|
518
|
-
const { ui } = notifySurface();
|
|
519
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
520
|
-
|
|
521
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "the promised surfaces still register");
|
|
522
|
-
assert.deepEqual(demoRows(), [], "a surface the bridge never promised adds nothing");
|
|
523
|
-
dispose();
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
test("the design's own preview depicts the card in that design", async () => {
|
|
527
|
-
publishBridgeApi();
|
|
528
|
-
const { pi, sessionHandlers } = recordingPi();
|
|
529
|
-
(await loadExample("preview"))(pi);
|
|
530
|
-
const { ui } = notifySurface();
|
|
531
|
-
sessionHandlers[0]({}, { hasUI: true, ui });
|
|
532
|
-
|
|
533
|
-
const [design] = snapshotToolDesigns("bash");
|
|
534
|
-
assert.ok(design !== undefined);
|
|
535
|
-
const preview = design.preview?.();
|
|
536
|
-
assert.ok(preview !== undefined, "the design depicts itself");
|
|
537
|
-
assert.match(preview.title, /bash heading/u);
|
|
538
|
-
assert.match(preview.sample, /╭─ bash npm test/u, "the sample heads the card with the word bash");
|
|
539
|
-
|
|
540
|
-
installToolDesignRows();
|
|
541
|
-
const row = buildRows({ theme: undefined, configHome }).rows.find(
|
|
542
|
-
(candidate) => candidate.choice?.value === "bash heading",
|
|
543
|
-
);
|
|
544
|
-
assert.deepEqual(row?.preview?.(), preview, "the value shows the design's own preview");
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
// ---------------------------------------------------------------------------------------------
|
|
548
|
-
// The load order Pi really produces: the plugin runs first
|
|
549
|
-
// ---------------------------------------------------------------------------------------------
|
|
550
|
-
|
|
551
|
-
test("a plugin loaded before the bridge registers when the announcement arrives", async () => {
|
|
552
|
-
const bus = createFakeEvents();
|
|
553
|
-
const { pi, sessionHandlers } = recordingPi(bus);
|
|
554
|
-
(await loadExample("late-announcement"))(pi);
|
|
555
|
-
|
|
556
|
-
assert.deepEqual(surfaceCounts(), [0, 0], "nothing registers at module load");
|
|
557
|
-
assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1, "the early loader is listening");
|
|
558
|
-
|
|
559
|
-
loadBridge(bus);
|
|
560
|
-
|
|
561
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "the announcement served the early loader");
|
|
562
|
-
assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 0, "and it stopped listening afterwards");
|
|
563
|
-
|
|
564
|
-
const { ui } = notifySurface();
|
|
565
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
566
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "a reload adds no second copy");
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test("the announcement alone is enough: a late bridge registers with no session_start", async () => {
|
|
570
|
-
const bus = createFakeEvents();
|
|
571
|
-
const { pi, sessionHandlers } = recordingPi(bus);
|
|
572
|
-
(await loadExample("late-no-session"))(pi);
|
|
573
|
-
|
|
574
|
-
publishBridgeApi();
|
|
575
|
-
announceBridgeReady(bus);
|
|
576
|
-
|
|
577
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
578
|
-
assert.equal(sessionHandlers.length, 1, "the handler is still there for the next session");
|
|
579
|
-
|
|
580
|
-
const { ui, calls } = notifySurface();
|
|
581
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
582
|
-
assert.deepEqual(calls, [], "no notice: nothing is pending");
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test("session_start before and after the announcement leaves exactly one of each", async () => {
|
|
586
|
-
const bus = createFakeEvents();
|
|
587
|
-
const { pi, sessionHandlers } = recordingPi(bus);
|
|
588
|
-
(await loadExample("late-both"))(pi);
|
|
589
|
-
const { ui } = notifySurface();
|
|
590
|
-
|
|
591
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
592
|
-
assert.deepEqual(surfaceCounts(), [0, 0], "no bridge yet: nothing to register with");
|
|
593
|
-
|
|
594
|
-
loadBridge(bus);
|
|
595
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
596
|
-
|
|
597
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
598
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION, "the later session_start adds no duplicate");
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
test("a plugin loaded after the bridge registers from session_start and leaves no listener", async () => {
|
|
602
|
-
const bus = createFakeEvents();
|
|
603
|
-
loadBridge(bus);
|
|
604
|
-
|
|
605
|
-
const { pi, sessionHandlers } = recordingPi(bus);
|
|
606
|
-
(await loadExample("early-bridge"))(pi);
|
|
607
|
-
|
|
608
|
-
assert.equal(
|
|
609
|
-
bus.listenerCount(BRIDGE_READY_EVENT),
|
|
610
|
-
0,
|
|
611
|
-
"the API was already published: nothing to wait for, so nothing is subscribed",
|
|
612
|
-
);
|
|
613
|
-
assert.deepEqual(surfaceCounts(), [0, 0], "and still nothing at module load");
|
|
614
|
-
|
|
615
|
-
const { ui, calls } = notifySurface();
|
|
616
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
617
|
-
assert.deepEqual(calls, []);
|
|
618
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test("disposing before the announcement drops the listener and registers nothing", async () => {
|
|
622
|
-
const bus = createFakeEvents();
|
|
623
|
-
const { pi } = recordingPi(bus);
|
|
624
|
-
const dispose = (await loadExample("late-dispose"))(pi);
|
|
625
|
-
|
|
626
|
-
assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1);
|
|
627
|
-
dispose();
|
|
628
|
-
assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 0, "a disposed load hears nothing");
|
|
629
|
-
|
|
630
|
-
loadBridge(bus);
|
|
631
|
-
assert.deepEqual(surfaceCounts(), [0, 0]);
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
test("an announcement backed by no published object registers nothing", async () => {
|
|
635
|
-
const bus = createFakeEvents();
|
|
636
|
-
const { pi } = recordingPi(bus);
|
|
637
|
-
(await loadExample("late-foreign"))(pi);
|
|
638
|
-
|
|
639
|
-
bus.emit(BRIDGE_READY_EVENT, { schema: "someone-else/api", version: 1 });
|
|
640
|
-
|
|
641
|
-
assert.deepEqual(surfaceCounts(), [0, 0], "the symbol decides, never the payload");
|
|
642
|
-
assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1, "and the example is still waiting");
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// ---------------------------------------------------------------------------------------------
|
|
646
|
-
// The two honest notices
|
|
647
|
-
// ---------------------------------------------------------------------------------------------
|
|
648
|
-
|
|
649
|
-
test("with a bus but no bridge yet the notice says it is waiting for the announcement", async () => {
|
|
650
|
-
const bus = createFakeEvents();
|
|
651
|
-
const { pi, sessionHandlers } = recordingPi(bus);
|
|
652
|
-
(await loadExample("notice-waiting"))(pi);
|
|
653
|
-
|
|
654
|
-
const { ui, calls } = notifySurface();
|
|
655
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
656
|
-
|
|
657
|
-
assert.equal(calls.length, 1);
|
|
658
|
-
const message = calls[0][0];
|
|
659
|
-
assert.match(message, /waiting for the "gentle-shell-bridge:ready" announcement/u);
|
|
660
|
-
assert.match(message, /registers the bash design/u);
|
|
661
|
-
assert.match(message, /the CARD BASH sidebar section/u);
|
|
662
|
-
assert.match(message, /if it is not installed, or the published object is incompatible, nothing arrives/u);
|
|
663
|
-
assert.doesNotMatch(message, /no compatible gentle-shell-bridge is installed/u);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test("a waiting notice is withdrawn once the announcement registers the surfaces", async () => {
|
|
667
|
-
const bus = createFakeEvents();
|
|
668
|
-
const { pi, sessionHandlers } = recordingPi(bus);
|
|
669
|
-
(await loadExample("notice-withdrawn"))(pi);
|
|
670
|
-
|
|
671
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: false });
|
|
672
|
-
const { ui, calls } = notifySurface();
|
|
673
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: false, ui });
|
|
674
|
-
assert.deepEqual(calls, [], "a session without a UI shows nothing");
|
|
675
|
-
|
|
676
|
-
loadBridge(bus);
|
|
677
|
-
assert.deepEqual(surfaceCounts(), DESIGN_AND_SECTION);
|
|
678
|
-
|
|
679
|
-
for (const handler of sessionHandlers) handler({}, { hasUI: true, ui });
|
|
680
|
-
assert.deepEqual(calls, [], "the surfaces registered, so the waiting notice is no longer true");
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// ---------------------------------------------------------------------------------------------
|
|
684
|
-
// Nothing outside the TUI
|
|
685
|
-
// ---------------------------------------------------------------------------------------------
|
|
686
|
-
|
|
687
|
-
test("the example's runtime path resolves no module of the bridge", () => {
|
|
688
|
-
const source = readFileSync(new URL("../examples/bash-card.ts", import.meta.url), "utf8");
|
|
689
|
-
|
|
690
|
-
assert.equal(/GENTLE_SHELL_BRIDGE_MODULE/u.test(source), false, "no environment-variable override");
|
|
691
|
-
assert.equal(/bridgeCandidates/u.test(source), false, "no path probing");
|
|
692
|
-
assert.equal(/await\s+import\s*\(/u.test(source), false, "nothing is imported at runtime");
|
|
693
|
-
assert.equal(
|
|
694
|
-
/from\s+["']gentle-shell-bridge["']/u.test(source),
|
|
695
|
-
false,
|
|
696
|
-
"the package is never imported, not even for types",
|
|
697
|
-
);
|
|
698
|
-
assert.match(source, /Symbol\.for\("gentle-shell-bridge\/api\/v1"\)/u);
|
|
699
|
-
assert.equal(/console\./u.test(source), false, "no console write survives in the example");
|
|
700
|
-
assert.match(source, /pi\.on\("session_start"/u, "registration happens in session_start");
|
|
701
|
-
assert.match(source, /gentle-shell-bridge:ready/u, "a later bridge is heard on the announcement");
|
|
702
|
-
assert.match(source, /pi\.events/u, "the channel comes from Pi's own bus, not a module");
|
|
703
|
-
assert.match(source, /registerToolDesign/u, "the design is the surface it registers");
|
|
704
|
-
assert.equal(/registerTransform/u.test(source), false, "there is no second transcript grammar");
|
|
705
|
-
assert.equal(/registerToolRenderer\s*\(/u.test(source), false, "the example claims no resolver slot");
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
test("the absent case writes zero bytes to stdout and stderr", () => {
|
|
709
|
-
const specifier = new URL("../examples/bash-card.ts?zero-bytes", import.meta.url).href;
|
|
710
|
-
const script = `
|
|
711
|
-
import example from ${JSON.stringify(specifier)};
|
|
712
|
-
const handlers = [];
|
|
713
|
-
const pi = {
|
|
714
|
-
on(event, handler) { handlers.push(handler); },
|
|
715
|
-
};
|
|
716
|
-
example(pi);
|
|
717
|
-
for (const handler of handlers) handler({}, { hasUI: true, ui: { notify() {} } });
|
|
718
|
-
`;
|
|
719
|
-
const captured = captureConsole();
|
|
720
|
-
let result: ReturnType<typeof spawnSync>;
|
|
721
|
-
try {
|
|
722
|
-
result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
|
|
723
|
-
cwd: new URL("..", import.meta.url),
|
|
724
|
-
encoding: "utf8",
|
|
725
|
-
env: { ...process.env, GENTLE_PI_CONFIG_HOME: configHome },
|
|
726
|
-
});
|
|
727
|
-
} finally {
|
|
728
|
-
captured.restore();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
assert.equal(result.status, 0, `the example ran (stderr: ${String(result.stderr)})`);
|
|
732
|
-
assert.equal(result.stdout, "", "no stdout bytes");
|
|
733
|
-
assert.equal(result.stderr, "", "no stderr bytes");
|
|
734
|
-
});
|