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
package/test/extension.test.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
import type { RowProvider } from "../src/types.ts";
|
|
5
|
-
import { resetBusForTests, registerRows } from "../src/bus.ts";
|
|
6
|
-
import { getCaptureDiagnostics, resetCaptureStateForTests } from "../src/capture.ts";
|
|
7
|
-
import gentleShellBridge, {
|
|
8
|
-
STATUS_COMMAND,
|
|
9
|
-
type BridgeCommandContext,
|
|
10
|
-
type BridgeCommandHandler,
|
|
11
|
-
type BridgeExtensionApi,
|
|
12
|
-
type BridgeSessionContext,
|
|
13
|
-
type BridgeSessionHandler,
|
|
14
|
-
} from "../src/extension.ts";
|
|
15
|
-
import { resetHookStateForTests } from "../src/hook.ts";
|
|
16
|
-
import { resetRowBuildStateForTests } from "../src/rows.ts";
|
|
17
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
18
|
-
import { createFakeHost } from "./fake-host.ts";
|
|
19
|
-
|
|
20
|
-
const HOOK_KEY = Symbol.for("gentle-pi.customize-row-providers");
|
|
21
|
-
const hookSlot = (): Record<symbol, unknown> => globalThis as unknown as Record<symbol, unknown>;
|
|
22
|
-
|
|
23
|
-
test.beforeEach(() => {
|
|
24
|
-
resetBusForTests();
|
|
25
|
-
resetCaptureStateForTests();
|
|
26
|
-
resetRowBuildStateForTests();
|
|
27
|
-
resetHookStateForTests();
|
|
28
|
-
resetStoreStateForTests();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test.afterEach(() => {
|
|
32
|
-
delete hookSlot()[HOOK_KEY];
|
|
33
|
-
resetHookStateForTests();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const labelsProvider = (id: string, category: string, labels: string[]): RowProvider => ({
|
|
37
|
-
id,
|
|
38
|
-
category,
|
|
39
|
-
rows: () => labels.map((label) => ({ label, action: () => {} })),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
interface FakeApi {
|
|
43
|
-
api: BridgeExtensionApi;
|
|
44
|
-
sessionHandlers: BridgeSessionHandler[];
|
|
45
|
-
shutdownHandlers: BridgeSessionHandler[];
|
|
46
|
-
commands: Array<{ name: string; description?: string; handler: BridgeCommandHandler }>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const createFakeApi = (): FakeApi => {
|
|
50
|
-
const sessionHandlers: BridgeSessionHandler[] = [];
|
|
51
|
-
const shutdownHandlers: BridgeSessionHandler[] = [];
|
|
52
|
-
const commands: Array<{ name: string; description?: string; handler: BridgeCommandHandler }> = [];
|
|
53
|
-
const api: BridgeExtensionApi = {
|
|
54
|
-
// The REQUIRED host package is present: `session_start` arms as before (S9/T12).
|
|
55
|
-
getCommands: () => [{ name: "gentle:customize" }],
|
|
56
|
-
on(event, handler) {
|
|
57
|
-
if (event === "session_start") {
|
|
58
|
-
sessionHandlers.push(handler);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
assert.equal(event, "session_shutdown", `not a session event: ${String(event)}`);
|
|
62
|
-
shutdownHandlers.push(handler);
|
|
63
|
-
},
|
|
64
|
-
registerCommand(name, command) {
|
|
65
|
-
commands.push({ name, description: command.description, handler: command.handler });
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
return { api, sessionHandlers, shutdownHandlers, commands };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
test("registers one session_start handler, one session_shutdown handler and one command", () => {
|
|
72
|
-
const fake = createFakeApi();
|
|
73
|
-
|
|
74
|
-
gentleShellBridge(fake.api);
|
|
75
|
-
|
|
76
|
-
assert.equal(fake.sessionHandlers.length, 1);
|
|
77
|
-
assert.equal(fake.shutdownHandlers.length, 1, "shutdown is how the INTEGRATIONS row is cleared");
|
|
78
|
-
assert.equal(fake.commands.length, 1);
|
|
79
|
-
assert.equal(fake.commands[0].name, STATUS_COMMAND);
|
|
80
|
-
assert.equal(typeof fake.commands[0].description, "string");
|
|
81
|
-
assert.ok((fake.commands[0].description ?? "").length > 0);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("a session_start without UI arms nothing", () => {
|
|
85
|
-
const host = createFakeHost();
|
|
86
|
-
registerRows(labelsProvider("p1", "Cards", ["Must not appear"]));
|
|
87
|
-
const fake = createFakeApi();
|
|
88
|
-
gentleShellBridge(fake.api);
|
|
89
|
-
|
|
90
|
-
const context: BridgeSessionContext = { hasUI: false };
|
|
91
|
-
fake.sessionHandlers[0]({}, context);
|
|
92
|
-
|
|
93
|
-
assert.equal(getCaptureDiagnostics().armedTargets, 0);
|
|
94
|
-
host.buildMenu();
|
|
95
|
-
assert.deepEqual(host.rendered().slice(4), []);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("two session_start events with UI arm once and inject once", () => {
|
|
99
|
-
const host = createFakeHost();
|
|
100
|
-
registerRows(labelsProvider("p1", "Cards", ["Once"]));
|
|
101
|
-
const fake = createFakeApi();
|
|
102
|
-
gentleShellBridge(fake.api);
|
|
103
|
-
const context: BridgeSessionContext = { hasUI: true, ui: host.ui };
|
|
104
|
-
|
|
105
|
-
fake.sessionHandlers[0]({}, context);
|
|
106
|
-
fake.sessionHandlers[0]({}, context);
|
|
107
|
-
|
|
108
|
-
assert.equal(getCaptureDiagnostics().armedTargets, 1, "one wrapper across two events");
|
|
109
|
-
host.buildMenu();
|
|
110
|
-
assert.deepEqual(
|
|
111
|
-
host.rendered().filter((label) => label === "Once"),
|
|
112
|
-
["Once"],
|
|
113
|
-
);
|
|
114
|
-
assert.equal(host.invalidateCount(), 1);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("loading the extension twice still arms the same ui once", () => {
|
|
118
|
-
const host = createFakeHost();
|
|
119
|
-
registerRows(labelsProvider("p1", "Cards", ["Once"]));
|
|
120
|
-
const first = createFakeApi();
|
|
121
|
-
const second = createFakeApi();
|
|
122
|
-
gentleShellBridge(first.api);
|
|
123
|
-
gentleShellBridge(second.api);
|
|
124
|
-
const context: BridgeSessionContext = { hasUI: true, ui: host.ui };
|
|
125
|
-
|
|
126
|
-
first.sessionHandlers[0]({}, context);
|
|
127
|
-
second.sessionHandlers[0]({}, context);
|
|
128
|
-
|
|
129
|
-
assert.equal(getCaptureDiagnostics().armedTargets, 1);
|
|
130
|
-
host.buildMenu();
|
|
131
|
-
assert.deepEqual(
|
|
132
|
-
host.rendered().filter((label) => label === "Once"),
|
|
133
|
-
["Once"],
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("the status command prints the formatter output", () => {
|
|
138
|
-
registerRows(labelsProvider("loon.banner-source", "Banner", ["Row"]));
|
|
139
|
-
const fake = createFakeApi();
|
|
140
|
-
gentleShellBridge(fake.api);
|
|
141
|
-
const printed: string[] = [];
|
|
142
|
-
const context: BridgeCommandContext = {
|
|
143
|
-
ui: { notify: (message: string) => printed.push(message) },
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
fake.commands[0].handler("", context);
|
|
147
|
-
|
|
148
|
-
assert.equal(printed.length, 1);
|
|
149
|
-
assert.match(printed[0], /^gentle-shell-bridge status$/m);
|
|
150
|
-
assert.match(printed[0], /^providers: 1$/m);
|
|
151
|
-
assert.match(printed[0], /loon\.banner-source \(Banner\)/);
|
|
152
|
-
assert.match(printed[0], /^duplicate settings: no build yet$/m);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("the status command does not throw when the command context has no ui", () => {
|
|
156
|
-
const fake = createFakeApi();
|
|
157
|
-
gentleShellBridge(fake.api);
|
|
158
|
-
|
|
159
|
-
assert.doesNotThrow(() => fake.commands[0].handler("", {}));
|
|
160
|
-
});
|
package/test/fake-events.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A fake `pi.events`: Pi's cross-extension bus (`docs/extensions.md:1965-1972`).
|
|
3
|
-
*
|
|
4
|
-
* The real bus (`dist/core/event-bus.js`) is an `EventEmitter` behind two methods: `on(channel,
|
|
5
|
-
* handler)` returns an unsubscribe function, and `emit(channel, data)` calls every handler. The
|
|
6
|
-
* real handler wrapper is `async`, so a handler's *synchronous* work has completed by the time
|
|
7
|
-
* `emit` returns — this fake delivers synchronously for the same reason, which is what lets a
|
|
8
|
-
* bridge that publishes later register its plugin's surfaces during the emit.
|
|
9
|
-
*
|
|
10
|
-
* It counts listeners so a test can prove a plugin that found the API at load leaves nothing
|
|
11
|
-
* behind, and records every emit so "exactly once per process" is observable.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type FakeEventHandler = (payload: unknown) => void;
|
|
15
|
-
|
|
16
|
-
export interface FakeEmit {
|
|
17
|
-
channel: string;
|
|
18
|
-
payload: unknown;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface FakeEvents {
|
|
22
|
-
on(channel: string, handler: FakeEventHandler): () => void;
|
|
23
|
-
emit(channel: string, payload?: unknown): void;
|
|
24
|
-
/** Every `emit` this bus saw, in order. */
|
|
25
|
-
emitted(): FakeEmit[];
|
|
26
|
-
/** Live listeners on one channel, or on every channel when omitted. */
|
|
27
|
-
listenerCount(channel?: string): number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function createFakeEvents(): FakeEvents {
|
|
31
|
-
const listeners = new Map<string, Set<FakeEventHandler>>();
|
|
32
|
-
const emits: FakeEmit[] = [];
|
|
33
|
-
|
|
34
|
-
const count = (channel: string): number => listeners.get(channel)?.size ?? 0;
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
on(channel, handler) {
|
|
38
|
-
const set = listeners.get(channel) ?? new Set<FakeEventHandler>();
|
|
39
|
-
set.add(handler);
|
|
40
|
-
listeners.set(channel, set);
|
|
41
|
-
return () => {
|
|
42
|
-
set.delete(handler);
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
emit(channel, payload) {
|
|
46
|
-
emits.push({ channel, payload });
|
|
47
|
-
// A copy, so a listener that unsubscribes while being notified cannot skip its neighbour.
|
|
48
|
-
for (const handler of [...(listeners.get(channel) ?? [])]) handler(payload);
|
|
49
|
-
},
|
|
50
|
-
emitted: () => [...emits],
|
|
51
|
-
listenerCount: (channel) => {
|
|
52
|
-
if (channel !== undefined) return count(channel);
|
|
53
|
-
let total = 0;
|
|
54
|
-
for (const set of listeners.values()) total += set.size;
|
|
55
|
-
return total;
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
package/test/fake-host.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A fake host for the capture backend, modelling `gentle-shell.ts:3550-3562` and
|
|
3
|
-
* `lib/visual-customize-view.ts:387`.
|
|
4
|
-
*
|
|
5
|
-
* The host builds its `rows` array, derives categories from it with a single synchronous
|
|
6
|
-
* `rows.map(...)` inside the factory — the one moment the array is reachable — and returns a
|
|
7
|
-
* plain adapter `{ render, handleInput, invalidate, dispose }` that hides the rows. Duck-typing
|
|
8
|
-
* `options.rows` on the adapter never matches, exactly like the real host.
|
|
9
|
-
*
|
|
10
|
-
* The adapter renders from the live array, so a test asserts what the user would see after an
|
|
11
|
-
* injection, not just what the bridge captured.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { CustomizeRow, InlineHelpers, RowLabel } from "../src/types.ts";
|
|
15
|
-
|
|
16
|
-
export type CustomFactory = (ui: unknown, theme: unknown, ...rest: unknown[]) => unknown;
|
|
17
|
-
|
|
18
|
-
/** The `custom` surface Pi hands extensions; own property or inherited, depending on shape. */
|
|
19
|
-
export interface FakeUiTarget {
|
|
20
|
-
custom?: (factory: CustomFactory, ...rest: unknown[]) => unknown;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FakeAdapter {
|
|
24
|
-
render: () => string[];
|
|
25
|
-
handleInput: (data: string) => unknown;
|
|
26
|
-
invalidate: () => void;
|
|
27
|
-
dispose: () => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type FakeRow = CustomizeRow;
|
|
31
|
-
|
|
32
|
-
export interface FakeHostOptions {
|
|
33
|
-
/** The host's own menu rows. Must hold at least the capture threshold's worth of rows. */
|
|
34
|
-
rows?: FakeRow[];
|
|
35
|
-
/** `own` puts `custom` on the instance; `prototype` leaves the instance inheriting it. */
|
|
36
|
-
customShape?: "own" | "prototype";
|
|
37
|
-
/** `promise` makes the factory return a promise, as the real chain allows. */
|
|
38
|
-
factoryResult?: "adapter" | "promise";
|
|
39
|
-
/** Throw inside the factory after the category map ran. */
|
|
40
|
-
throwInsideFactory?: boolean;
|
|
41
|
-
/** Map only a non-row array inside the factory, never the rows array. */
|
|
42
|
-
mapOnlyNonRows?: boolean;
|
|
43
|
-
/** Theme handed to the factory as its second argument. */
|
|
44
|
-
theme?: unknown;
|
|
45
|
-
/** Extra factory arguments after the theme (the host passes keybindings and `close`). */
|
|
46
|
-
extraFactoryArgs?: unknown[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface FakeHost {
|
|
50
|
-
readonly ui: FakeUiTarget;
|
|
51
|
-
/** The factory itself, so a nesting test can re-enter a wrapped `custom` call. */
|
|
52
|
-
readonly factory: CustomFactory;
|
|
53
|
-
/** What Pi does: `ui.custom(factory, options)`. */
|
|
54
|
-
buildMenu: () => unknown;
|
|
55
|
-
/** The live array the adapter renders from (rows the host built plus anything injected). */
|
|
56
|
-
liveRows: () => FakeRow[];
|
|
57
|
-
/** Categories the host derived from that array in the factory's single `rows.map(...)`. */
|
|
58
|
-
categories: () => string[];
|
|
59
|
-
/** Labels the user would see right now. */
|
|
60
|
-
rendered: () => string[];
|
|
61
|
-
invalidateCount: () => number;
|
|
62
|
-
disposeCount: () => number;
|
|
63
|
-
factoryCallCount: () => number;
|
|
64
|
-
/** Enter on the first visible row whose rendered label matches, including the view's repaint. */
|
|
65
|
-
press: (label: string) => Promise<void>;
|
|
66
|
-
/** Enter on the first visible row matching, so a test can target a choice value, not a label. */
|
|
67
|
-
pressRow: (match: (row: FakeRow) => boolean) => Promise<void>;
|
|
68
|
-
/**
|
|
69
|
-
* The `●`/`○` lines the real renderer draws: consecutive rows sharing `choice.setting` become one
|
|
70
|
-
* line (`lib/visual-customize-view.ts:583-595`), so this reads `current()` at render time.
|
|
71
|
-
*/
|
|
72
|
-
toggleLines: () => string[];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const inlineHelpers: InlineHelpers = {
|
|
76
|
-
input: async () => undefined,
|
|
77
|
-
confirm: async () => true,
|
|
78
|
-
capture: async () => undefined,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const labelText = (label: RowLabel | undefined): string => {
|
|
82
|
-
if (typeof label === "function") return label();
|
|
83
|
-
return label ?? "";
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const isVisible = (row: FakeRow): boolean => {
|
|
87
|
-
if (row.visible === undefined) return true;
|
|
88
|
-
try {
|
|
89
|
-
return row.visible();
|
|
90
|
-
} catch {
|
|
91
|
-
return false; // the renderer fails closed
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** Four rows: one more than the capture threshold, spread over three categories. */
|
|
96
|
-
const defaultRows = (): FakeRow[] => [
|
|
97
|
-
{ category: "Cards", label: "Card bash: builtin", action: () => {} },
|
|
98
|
-
{ category: "Cards", label: "Card output: builtin", action: () => {} },
|
|
99
|
-
{ category: "Banner", label: "Banner: builtin", action: () => {} },
|
|
100
|
-
{ category: "Layout", label: "Layout: builtin", action: () => {} },
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
export function createFakeHost(options: FakeHostOptions = {}): FakeHost {
|
|
104
|
-
const rows: FakeRow[] = options.rows ?? defaultRows();
|
|
105
|
-
const theme = "theme" in options ? options.theme : { name: "fake-theme" };
|
|
106
|
-
const extraFactoryArgs = options.extraFactoryArgs ?? [{ keybindings: "fake" }, () => {}];
|
|
107
|
-
const customShape = options.customShape ?? "own";
|
|
108
|
-
const factoryResult = options.factoryResult ?? "adapter";
|
|
109
|
-
|
|
110
|
-
let invalidateCount = 0;
|
|
111
|
-
let disposeCount = 0;
|
|
112
|
-
let factoryCallCount = 0;
|
|
113
|
-
let categories: string[] = [];
|
|
114
|
-
|
|
115
|
-
const render = (): string[] => rows.filter(isVisible).map((row) => labelText(row.label));
|
|
116
|
-
|
|
117
|
-
const runAction = async (row: FakeRow): Promise<void> => {
|
|
118
|
-
await row.action?.(inlineHelpers);
|
|
119
|
-
invalidateCount += 1; // the real view repaints after any action (visual-customize-view.ts:579)
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const adapter = (): FakeAdapter => ({
|
|
123
|
-
render,
|
|
124
|
-
handleInput: (data: string) => (data === "enter" ? press(render()[0] ?? "") : undefined),
|
|
125
|
-
invalidate: () => {
|
|
126
|
-
invalidateCount += 1;
|
|
127
|
-
},
|
|
128
|
-
dispose: () => {
|
|
129
|
-
disposeCount += 1;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const pressRow = async (match: (row: FakeRow) => boolean): Promise<void> => {
|
|
134
|
-
const row = rows.filter(isVisible).find(match);
|
|
135
|
-
if (row === undefined) throw new Error("fake host: no visible row matched the predicate");
|
|
136
|
-
await runAction(row);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const press = (label: string): Promise<void> =>
|
|
140
|
-
pressRow((candidate) => labelText(candidate.label) === label);
|
|
141
|
-
|
|
142
|
-
const toggleLines = (): string[] => {
|
|
143
|
-
const visible = rows.filter(isVisible);
|
|
144
|
-
const lines: string[] = [];
|
|
145
|
-
let index = 0;
|
|
146
|
-
while (index < visible.length) {
|
|
147
|
-
const setting = visible[index].choice?.setting;
|
|
148
|
-
if (setting === undefined) {
|
|
149
|
-
index += 1;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let end = index;
|
|
153
|
-
while (end + 1 < visible.length && visible[end + 1].choice?.setting === setting) end += 1;
|
|
154
|
-
const marks = visible
|
|
155
|
-
.slice(index, end + 1)
|
|
156
|
-
.map((row) => {
|
|
157
|
-
const mark = row.choice?.current() === true ? "●" : "○";
|
|
158
|
-
return `${mark} ${row.choice?.value ?? ""}`.trim();
|
|
159
|
-
});
|
|
160
|
-
lines.push(`${setting} ${marks.join(" ")}`);
|
|
161
|
-
index = end + 1;
|
|
162
|
-
}
|
|
163
|
-
return lines;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const factory: CustomFactory = (_ui, _theme, ..._rest) => {
|
|
167
|
-
factoryCallCount += 1;
|
|
168
|
-
// The one reachable moment (lib/visual-customize-view.ts:387): the view derives its
|
|
169
|
-
// categories from `options.rows` with a single synchronous `map` call inside the factory.
|
|
170
|
-
if (options.mapOnlyNonRows === true) {
|
|
171
|
-
[1, 2, 3].map((value) => value * 2); // a plain array of numbers: not a row array
|
|
172
|
-
} else {
|
|
173
|
-
categories = rows.map((row) => row.category ?? "Settings");
|
|
174
|
-
}
|
|
175
|
-
if (options.throwInsideFactory === true) throw new Error("factory exploded");
|
|
176
|
-
|
|
177
|
-
const built = adapter();
|
|
178
|
-
return factoryResult === "promise" ? Promise.resolve(built) : built;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const invoke = (factoryArg: CustomFactory): unknown => factoryArg(ui, theme, ...extraFactoryArgs);
|
|
182
|
-
|
|
183
|
-
const ownShape: FakeUiTarget = {
|
|
184
|
-
custom: (factoryArg: CustomFactory) => invoke(factoryArg),
|
|
185
|
-
};
|
|
186
|
-
const prototypeShape: FakeUiTarget = Object.create({
|
|
187
|
-
custom(this: unknown, factoryArg: CustomFactory) {
|
|
188
|
-
return invoke(factoryArg);
|
|
189
|
-
},
|
|
190
|
-
}) as FakeUiTarget;
|
|
191
|
-
|
|
192
|
-
const ui: FakeUiTarget = customShape === "prototype" ? prototypeShape : ownShape;
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
ui,
|
|
196
|
-
factory,
|
|
197
|
-
buildMenu: () => {
|
|
198
|
-
if (typeof ui.custom !== "function") throw new Error("fake host: ui.custom disappeared");
|
|
199
|
-
return ui.custom(factory);
|
|
200
|
-
},
|
|
201
|
-
liveRows: () => rows,
|
|
202
|
-
categories: () => [...categories],
|
|
203
|
-
rendered: render,
|
|
204
|
-
invalidateCount: () => invalidateCount,
|
|
205
|
-
disposeCount: () => disposeCount,
|
|
206
|
-
factoryCallCount: () => factoryCallCount,
|
|
207
|
-
press,
|
|
208
|
-
pressRow,
|
|
209
|
-
toggleLines,
|
|
210
|
-
};
|
|
211
|
-
}
|