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,471 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rail interception (S4, S9), proven against the fake host.
|
|
3
|
-
*
|
|
4
|
-
* The host registers the `footer` part itself (`extensions/gentle-shell.ts:2681`), so our arming
|
|
5
|
-
* may run before OR after it. This suite covers both orders, the pass-through of every field the
|
|
6
|
-
* host's readers use (`digest` for the layout memo, `spend` for the Stats section), the mouse
|
|
7
|
-
* delegation, idempotency across two installs / a rebind / a second package copy, the disable and
|
|
8
|
-
* restore path, and the extension point T4 will use to contribute card groups.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
SIDEBAR_RAIL_KEY,
|
|
16
|
-
disableSidebarBackend,
|
|
17
|
-
getSidebarDiagnostics,
|
|
18
|
-
installSidebarBackend,
|
|
19
|
-
resetSidebarStateForTests,
|
|
20
|
-
setRailContributor,
|
|
21
|
-
} from "../src/sidebar.ts";
|
|
22
|
-
import {
|
|
23
|
-
SIDEBAR_STATE_KEY,
|
|
24
|
-
cardBody,
|
|
25
|
-
cardTitles,
|
|
26
|
-
fakeState,
|
|
27
|
-
fakeTerminal,
|
|
28
|
-
fakeTui,
|
|
29
|
-
registerNativeFooter,
|
|
30
|
-
registerNativeRail,
|
|
31
|
-
renderSidebar,
|
|
32
|
-
resetSidebar,
|
|
33
|
-
type Rail,
|
|
34
|
-
} from "./fake-sidebar.ts";
|
|
35
|
-
|
|
36
|
-
interface NativeOptions {
|
|
37
|
-
title?: string;
|
|
38
|
-
body?: string[];
|
|
39
|
-
render?: (width: number) => string[];
|
|
40
|
-
cardGroups?: Rail["cardGroups"];
|
|
41
|
-
handleMouse?: (event: unknown) => unknown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface NativeRail {
|
|
45
|
-
rail: Rail;
|
|
46
|
-
calls: {
|
|
47
|
-
render: number;
|
|
48
|
-
cardGroups: number;
|
|
49
|
-
digest: number;
|
|
50
|
-
spend: number;
|
|
51
|
-
handleMouse: number;
|
|
52
|
-
open: number;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The host's own footer part: a real rail shape with every field a reader in the host touches. */
|
|
57
|
-
function nativeRail(options: NativeOptions = {}): NativeRail {
|
|
58
|
-
const title = options.title ?? "FOOTER";
|
|
59
|
-
const body = options.body ?? ["native row 1", "native row 2"];
|
|
60
|
-
const calls = {
|
|
61
|
-
render: 0,
|
|
62
|
-
cardGroups: 0,
|
|
63
|
-
digest: 0,
|
|
64
|
-
spend: 0,
|
|
65
|
-
handleMouse: 0,
|
|
66
|
-
open: 0,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const rail: Rail = {
|
|
70
|
-
render: (width) => {
|
|
71
|
-
calls.render += 1;
|
|
72
|
-
return options.render === undefined ? [`▾ ${title}`, ...body] : options.render(width);
|
|
73
|
-
},
|
|
74
|
-
cardGroups: (width) => {
|
|
75
|
-
calls.cardGroups += 1;
|
|
76
|
-
return options.cardGroups === undefined
|
|
77
|
-
? { groups: [{ start: 0, end: body.length + 1 }] }
|
|
78
|
-
: options.cardGroups(width);
|
|
79
|
-
},
|
|
80
|
-
digest: () => {
|
|
81
|
-
calls.digest += 1;
|
|
82
|
-
return "footer:stable";
|
|
83
|
-
},
|
|
84
|
-
surface: "panel",
|
|
85
|
-
summary: () => ({ row: "native summary" }),
|
|
86
|
-
open: () => {
|
|
87
|
-
calls.open += 1;
|
|
88
|
-
},
|
|
89
|
-
alert: () => ["needs you"],
|
|
90
|
-
spend: () => {
|
|
91
|
-
calls.spend += 1;
|
|
92
|
-
return { runs: 2, tokens: 3, cost: 4 };
|
|
93
|
-
},
|
|
94
|
-
invalidate: () => {},
|
|
95
|
-
};
|
|
96
|
-
if (options.handleMouse !== undefined) {
|
|
97
|
-
const handler = options.handleMouse;
|
|
98
|
-
rail.handleMouse = (event) => {
|
|
99
|
-
calls.handleMouse += 1;
|
|
100
|
-
return handler(event);
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return { rail, calls };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const install = () => installSidebarBackend(() => fakeTui());
|
|
107
|
-
const footer = () => fakeState().parts.get(SIDEBAR_RAIL_KEY);
|
|
108
|
-
|
|
109
|
-
test.beforeEach(() => {
|
|
110
|
-
resetSidebar();
|
|
111
|
-
resetSidebarStateForTests();
|
|
112
|
-
setRailContributor(undefined);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("a footer the host registers after the bridge armed is captured", () => {
|
|
116
|
-
fakeState(); // the host created its rail state at startup, with no footer part yet
|
|
117
|
-
const result = install();
|
|
118
|
-
|
|
119
|
-
assert.deepEqual(
|
|
120
|
-
{ installed: result.installed, outcome: result.outcome, rail: result.rail },
|
|
121
|
-
{ installed: true, outcome: "intercepted", rail: "pending" },
|
|
122
|
-
"the map is intercepted even before the part exists",
|
|
123
|
-
);
|
|
124
|
-
assert.equal(footer(), undefined);
|
|
125
|
-
|
|
126
|
-
const native = registerNativeFooter(nativeRail().rail);
|
|
127
|
-
const wrapped = footer();
|
|
128
|
-
|
|
129
|
-
assert.notEqual(wrapped, native, "the map hands out the wrapper, not the host's raw rail");
|
|
130
|
-
assert.equal(wrapped, footer(), "one wrapper: a read never re-wraps");
|
|
131
|
-
assert.deepEqual(wrapped?.render(40), native.render(40), "render delegates to the host's rail");
|
|
132
|
-
assert.deepEqual(cardTitles(), ["FOOTER"]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("an already-registered footer is wrapped on install", () => {
|
|
136
|
-
const { rail } = nativeRail();
|
|
137
|
-
registerNativeFooter(rail);
|
|
138
|
-
|
|
139
|
-
const result = install();
|
|
140
|
-
|
|
141
|
-
assert.equal(result.outcome, "intercepted");
|
|
142
|
-
assert.equal(result.rail, "wrapped");
|
|
143
|
-
assert.notEqual(footer(), rail);
|
|
144
|
-
assert.deepEqual(cardTitles(), ["FOOTER"]);
|
|
145
|
-
assert.deepEqual(cardBody("FOOTER"), ["native row 1", "native row 2"]);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("digest, spend, surface, summary, open, alert and invalidate reach their readers", () => {
|
|
149
|
-
const { rail, calls } = nativeRail();
|
|
150
|
-
registerNativeFooter(rail);
|
|
151
|
-
install();
|
|
152
|
-
|
|
153
|
-
const wrapped = footer();
|
|
154
|
-
assert.ok(wrapped !== undefined);
|
|
155
|
-
// The digest's VALUE is the host's own while nothing contributes rows (N14): the layout memo
|
|
156
|
-
// compares values (`existing.digest === digest`), never function identity, so the wrapper may keep
|
|
157
|
-
// forwarding the host's answer from its own function.
|
|
158
|
-
const hostDigest = rail.digest?.();
|
|
159
|
-
assert.equal(wrapped.digest?.(), hostDigest, "the host's digest value reaches its reader");
|
|
160
|
-
assert.equal(calls.digest, 2, "each read calls the host's own function, once");
|
|
161
|
-
assert.equal(wrapped.spend, rail.spend, "spend keeps its identity");
|
|
162
|
-
assert.deepEqual(wrapped.spend?.(), { runs: 2, tokens: 3, cost: 4 });
|
|
163
|
-
assert.equal(wrapped.surface, "panel");
|
|
164
|
-
assert.equal(wrapped.summary, rail.summary);
|
|
165
|
-
assert.deepEqual(wrapped.summary?.(), { row: "native summary" });
|
|
166
|
-
assert.equal(wrapped.open, rail.open);
|
|
167
|
-
wrapped.open?.();
|
|
168
|
-
assert.equal(calls.open, 1);
|
|
169
|
-
assert.equal(wrapped.alert, rail.alert);
|
|
170
|
-
assert.deepEqual(wrapped.alert?.(40), ["needs you"]);
|
|
171
|
-
|
|
172
|
-
// The host reads spend exactly like this (`gentle-shell.ts:2652`).
|
|
173
|
-
assert.deepEqual(fakeState().parts.get("agents")?.spend?.(), undefined);
|
|
174
|
-
registerNativeRail("agents", { ...rail });
|
|
175
|
-
install();
|
|
176
|
-
assert.deepEqual(fakeState().parts.get(SIDEBAR_RAIL_KEY)?.spend?.(), { runs: 2, tokens: 3, cost: 4 });
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("a throwing host render is rethrown and recorded, never swallowed", () => {
|
|
180
|
-
registerNativeFooter(
|
|
181
|
-
nativeRail({
|
|
182
|
-
render: () => {
|
|
183
|
-
throw new Error("host render exploded");
|
|
184
|
-
},
|
|
185
|
-
}).rail,
|
|
186
|
-
);
|
|
187
|
-
install();
|
|
188
|
-
|
|
189
|
-
assert.throws(() => footer()?.render(40), /host render exploded/);
|
|
190
|
-
|
|
191
|
-
const failures = getSidebarDiagnostics().lastFailures;
|
|
192
|
-
assert.equal(failures.length, 1);
|
|
193
|
-
assert.equal(failures[0].key, SIDEBAR_RAIL_KEY);
|
|
194
|
-
assert.equal(failures[0].phase, "render");
|
|
195
|
-
assert.equal(failures[0].message, "host render exploded");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("a throwing host cardGroups is rethrown and recorded, never swallowed", () => {
|
|
199
|
-
registerNativeFooter(
|
|
200
|
-
nativeRail({
|
|
201
|
-
cardGroups: () => {
|
|
202
|
-
throw new Error("host groups exploded");
|
|
203
|
-
},
|
|
204
|
-
}).rail,
|
|
205
|
-
);
|
|
206
|
-
install();
|
|
207
|
-
|
|
208
|
-
assert.throws(() => footer()?.cardGroups?.(40), /host groups exploded/);
|
|
209
|
-
const failure = getSidebarDiagnostics().lastFailures.at(-1);
|
|
210
|
-
assert.equal(failure?.phase, "cardGroups");
|
|
211
|
-
assert.equal(failure?.message, "host groups exploded");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("a second install on the same tui does not stack a wrapper or duplicate groups", () => {
|
|
215
|
-
const { rail, calls } = nativeRail();
|
|
216
|
-
registerNativeFooter(rail);
|
|
217
|
-
assert.equal(install().outcome, "intercepted");
|
|
218
|
-
const first = footer();
|
|
219
|
-
|
|
220
|
-
const second = install();
|
|
221
|
-
|
|
222
|
-
assert.equal(second.outcome, "already-intercepted");
|
|
223
|
-
assert.equal(footer(), first, "the same wrapper object, not a wrapper of a wrapper");
|
|
224
|
-
|
|
225
|
-
calls.render = 0;
|
|
226
|
-
calls.cardGroups = 0;
|
|
227
|
-
const rows = renderSidebar(40);
|
|
228
|
-
assert.equal(calls.render, 1, "one frame renders the host's rail exactly once");
|
|
229
|
-
assert.equal(calls.cardGroups, 1, "and reads its card groups exactly once");
|
|
230
|
-
assert.deepEqual(
|
|
231
|
-
rows.filter((row) => row.startsWith("╭")).length,
|
|
232
|
-
1,
|
|
233
|
-
"one card, not the same card twice",
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test("a rebind onto a fresh ui object for the same terminal does not re-wrap", () => {
|
|
238
|
-
const { rail } = nativeRail();
|
|
239
|
-
registerNativeFooter(rail);
|
|
240
|
-
install();
|
|
241
|
-
const first = footer();
|
|
242
|
-
|
|
243
|
-
const rebound = installSidebarBackend(() => ({ terminal: fakeTerminal() }));
|
|
244
|
-
|
|
245
|
-
assert.equal(rebound.outcome, "already-intercepted");
|
|
246
|
-
assert.equal(footer(), first);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("a second copy of the package sees the interception and does not stack", async () => {
|
|
250
|
-
const { rail } = nativeRail();
|
|
251
|
-
registerNativeFooter(rail);
|
|
252
|
-
install();
|
|
253
|
-
const first = footer();
|
|
254
|
-
|
|
255
|
-
const secondCopySpecifier = "../src/sidebar.ts?copy=2";
|
|
256
|
-
const copyTwo: typeof import("../src/sidebar.ts") = await import(secondCopySpecifier);
|
|
257
|
-
assert.notEqual(copyTwo, await import("../src/sidebar.ts"));
|
|
258
|
-
|
|
259
|
-
const result = copyTwo.installSidebarBackend(() => fakeTui());
|
|
260
|
-
|
|
261
|
-
assert.equal(result.outcome, "already-intercepted");
|
|
262
|
-
assert.equal(footer(), first, "the marker on the map is process-global, like the slot");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("disableSidebarBackend restores the host rail and the map's own methods", () => {
|
|
266
|
-
const { rail } = nativeRail();
|
|
267
|
-
registerNativeFooter(rail);
|
|
268
|
-
install();
|
|
269
|
-
|
|
270
|
-
const parts = fakeState().parts;
|
|
271
|
-
const wrapped = footer();
|
|
272
|
-
assert.ok(Object.hasOwn(parts, "set"), "the map is intercepted through an own property");
|
|
273
|
-
assert.ok(Object.hasOwn(parts, "get"));
|
|
274
|
-
assert.notEqual(footer(), rail);
|
|
275
|
-
|
|
276
|
-
assert.equal(disableSidebarBackend(), 1);
|
|
277
|
-
|
|
278
|
-
assert.equal(Object.hasOwn(parts, "set"), false, "the prototype's own set/get are in force again");
|
|
279
|
-
assert.equal(Object.hasOwn(parts, "get"), false);
|
|
280
|
-
assert.equal(fakeState().parts.get(SIDEBAR_RAIL_KEY), rail, "the host's rail is back in the map");
|
|
281
|
-
|
|
282
|
-
// A wrapper retained by foreign code must be a plain pass-through now.
|
|
283
|
-
assert.deepEqual(wrapped?.render(40), ["▾ FOOTER", "native row 1", "native row 2"]);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("a foreign wrapper already on the rail is wrapped around, never clobbered", () => {
|
|
287
|
-
const { rail } = nativeRail();
|
|
288
|
-
let foreignCalls = 0;
|
|
289
|
-
const foreign: Rail = {
|
|
290
|
-
...rail,
|
|
291
|
-
render: (width) => {
|
|
292
|
-
foreignCalls += 1;
|
|
293
|
-
return rail.render(width);
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
registerNativeFooter(foreign);
|
|
297
|
-
|
|
298
|
-
install();
|
|
299
|
-
|
|
300
|
-
const wrapped = footer();
|
|
301
|
-
assert.ok(wrapped !== undefined);
|
|
302
|
-
assert.notEqual(wrapped, foreign, "we wrap the foreign wrapper instead of replacing it");
|
|
303
|
-
wrapped.render(40);
|
|
304
|
-
assert.equal(foreignCalls, 1, "the foreign wrapper still runs");
|
|
305
|
-
assert.equal(wrapped.digest?.(), rail.digest?.(), "and its fields still flow through ours");
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("an unknown tui shape is reported, never thrown", () => {
|
|
309
|
-
const noTui = installSidebarBackend(() => undefined);
|
|
310
|
-
assert.deepEqual(
|
|
311
|
-
{ installed: noTui.installed, outcome: noTui.outcome },
|
|
312
|
-
{ installed: false, outcome: "no-sidebar-state" },
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
const noTerminal = installSidebarBackend(() => ({}));
|
|
316
|
-
assert.equal(noTerminal.outcome, "no-sidebar-state");
|
|
317
|
-
|
|
318
|
-
const noState = installSidebarBackend(() => ({ terminal: fakeTerminal(), parts: new Map() }));
|
|
319
|
-
assert.equal(noState.outcome, "no-sidebar-state", "a parts map outside the host's slot is not the host's");
|
|
320
|
-
|
|
321
|
-
const badState = installSidebarBackend(() => ({
|
|
322
|
-
terminal: { [SIDEBAR_STATE_KEY]: { parts: "not a map" } },
|
|
323
|
-
}));
|
|
324
|
-
assert.equal(badState.outcome, "no-sidebar-state");
|
|
325
|
-
|
|
326
|
-
const threw = installSidebarBackend(() => {
|
|
327
|
-
throw new Error("no tui yet");
|
|
328
|
-
});
|
|
329
|
-
assert.equal(threw.installed, false);
|
|
330
|
-
assert.equal(threw.outcome, "resolver-threw");
|
|
331
|
-
assert.equal(threw.reason, "no tui yet");
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("a resolver that starts returning a tui lets a later install succeed", () => {
|
|
335
|
-
let available: { terminal: Record<string | symbol, unknown> } | undefined;
|
|
336
|
-
const resolve = () => available;
|
|
337
|
-
|
|
338
|
-
assert.equal(installSidebarBackend(resolve).outcome, "no-sidebar-state");
|
|
339
|
-
|
|
340
|
-
// The wiring resolved a real tui in between (a widget factory fired, say).
|
|
341
|
-
fakeState();
|
|
342
|
-
registerNativeFooter(nativeRail().rail);
|
|
343
|
-
available = fakeTui();
|
|
344
|
-
|
|
345
|
-
const result = installSidebarBackend(resolve);
|
|
346
|
-
assert.equal(result.outcome, "intercepted");
|
|
347
|
-
assert.notEqual(footer(), undefined);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test("the contributor appends its lines and card groups after the host's own", () => {
|
|
351
|
-
registerNativeFooter(nativeRail().rail);
|
|
352
|
-
install();
|
|
353
|
-
setRailContributor({
|
|
354
|
-
render: () => ({ lines: ["▾ EXTRA", "extra row"], groups: [{ start: 0, end: 2 }] }),
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
const wrapped = footer();
|
|
358
|
-
assert.ok(wrapped !== undefined);
|
|
359
|
-
assert.deepEqual(wrapped.render(40), [
|
|
360
|
-
"▾ FOOTER",
|
|
361
|
-
"native row 1",
|
|
362
|
-
"native row 2",
|
|
363
|
-
"▾ EXTRA",
|
|
364
|
-
"extra row",
|
|
365
|
-
]);
|
|
366
|
-
assert.deepEqual(wrapped.cardGroups?.(40), {
|
|
367
|
-
groups: [
|
|
368
|
-
{ start: 0, end: 3 },
|
|
369
|
-
{ start: 3, end: 5 },
|
|
370
|
-
],
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
assert.deepEqual(cardTitles(), ["FOOTER", "EXTRA"], "the host frames our rows as its own card");
|
|
374
|
-
assert.deepEqual(cardBody("EXTRA"), ["extra row"]);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
test("a host rail without cardGroups gets one synthesized group covering native and contributed rows", () => {
|
|
378
|
-
registerNativeFooter(nativeRail({ cardGroups: undefined, body: ["only row"] }).rail);
|
|
379
|
-
const parts = fakeState().parts;
|
|
380
|
-
const native = parts.get(SIDEBAR_RAIL_KEY);
|
|
381
|
-
assert.ok(native !== undefined);
|
|
382
|
-
delete (native as { cardGroups?: unknown }).cardGroups;
|
|
383
|
-
install();
|
|
384
|
-
setRailContributor({
|
|
385
|
-
render: () => ({ lines: ["▾ EXTRA", "extra row"], groups: [{ start: 0, end: 2 }] }),
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
assert.deepEqual(footer()?.cardGroups?.(40), {
|
|
389
|
-
groups: [
|
|
390
|
-
{ start: 0, end: 2 },
|
|
391
|
-
{ start: 2, end: 4 },
|
|
392
|
-
],
|
|
393
|
-
});
|
|
394
|
-
assert.deepEqual(cardTitles(), ["FOOTER", "EXTRA"]);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test("no host frame means no contribution: an empty render stays empty", () => {
|
|
398
|
-
registerNativeFooter(
|
|
399
|
-
nativeRail({ render: () => [], cardGroups: () => ({ groups: [] }) }).rail,
|
|
400
|
-
);
|
|
401
|
-
install();
|
|
402
|
-
setRailContributor({
|
|
403
|
-
render: () => ({ lines: ["▾ EXTRA", "extra row"], groups: [{ start: 0, end: 2 }] }),
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
assert.deepEqual(footer()?.render(40), [], "the host painted nothing, so neither do we");
|
|
407
|
-
assert.deepEqual(footer()?.cardGroups?.(40), { groups: [] });
|
|
408
|
-
assert.deepEqual(renderSidebar(40), []);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test("mouse dispatch goes to the host rail first; our targets only after it declines", () => {
|
|
412
|
-
const handled = { handled: true, render: true };
|
|
413
|
-
registerNativeFooter(
|
|
414
|
-
nativeRail({ handleMouse: (event) => (event === "hit" ? handled : undefined) }).rail,
|
|
415
|
-
);
|
|
416
|
-
install();
|
|
417
|
-
const ours: unknown[] = [];
|
|
418
|
-
setRailContributor({
|
|
419
|
-
render: () => undefined,
|
|
420
|
-
handleMouse: (event: unknown) => {
|
|
421
|
-
ours.push(event);
|
|
422
|
-
return { handled: true, render: true, ours: true };
|
|
423
|
-
},
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const wrapped = footer();
|
|
427
|
-
assert.ok(wrapped?.handleMouse !== undefined);
|
|
428
|
-
assert.deepEqual(wrapped.handleMouse("hit"), handled);
|
|
429
|
-
assert.deepEqual(ours, [], "the host handled it, so our targets are not consulted");
|
|
430
|
-
|
|
431
|
-
assert.deepEqual(wrapped.handleMouse("miss"), { handled: true, render: true, ours: true }, "our target answers");
|
|
432
|
-
assert.deepEqual(ours, ["miss"]);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
test("our mouse targets work even when the host rail has no mouse handler", () => {
|
|
436
|
-
registerNativeFooter(nativeRail().rail); // no handleMouse on the host's rail
|
|
437
|
-
install();
|
|
438
|
-
setRailContributor({ render: () => undefined, handleMouse: () => ({ ours: true }) });
|
|
439
|
-
|
|
440
|
-
assert.deepEqual(footer()?.handleMouse?.("event"), { ours: true });
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test("a throwing contributor is recorded and leaves every host row rendering", () => {
|
|
444
|
-
registerNativeFooter(nativeRail().rail);
|
|
445
|
-
install();
|
|
446
|
-
setRailContributor({
|
|
447
|
-
render: () => {
|
|
448
|
-
throw new Error("section renderer exploded");
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
const wrapped = footer();
|
|
453
|
-
assert.deepEqual(wrapped?.render(40), ["▾ FOOTER", "native row 1", "native row 2"]);
|
|
454
|
-
assert.deepEqual(cardTitles(), ["FOOTER"], "the host's own card still draws");
|
|
455
|
-
|
|
456
|
-
const failure = getSidebarDiagnostics().lastFailures.at(-1);
|
|
457
|
-
assert.equal(failure?.message, "section renderer exploded");
|
|
458
|
-
assert.equal(failure?.phase, "contribute");
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("the interception only wraps the footer part", () => {
|
|
462
|
-
fakeState();
|
|
463
|
-
install();
|
|
464
|
-
const todo = nativeRail({ title: "TODO" }).rail;
|
|
465
|
-
registerNativeRail("todo", todo);
|
|
466
|
-
registerNativeFooter(nativeRail().rail);
|
|
467
|
-
|
|
468
|
-
assert.equal(fakeState().parts.get("todo"), todo, "another rail keeps its own identity");
|
|
469
|
-
assert.notEqual(footer(), undefined);
|
|
470
|
-
assert.deepEqual(cardTitles(), ["TODO", "FOOTER"]);
|
|
471
|
-
});
|