@vestra-player/sdk 1.6.17 → 1.6.19
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/CHANGELOG.md +24 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/plugin/appearance.js +1 -1
- package/dist/plugin/bindings.js +1 -1
- package/dist/plugin/commands.js +3 -2
- package/dist/plugin/connection.d.ts +119 -0
- package/dist/plugin/connection.js +402 -0
- package/dist/plugin/drops.d.ts +3 -3
- package/dist/plugin/drops.js +1 -1
- package/dist/plugin/errors.d.ts +9 -0
- package/dist/plugin/errors.js +35 -0
- package/dist/plugin/filters.js +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/language.js +1 -1
- package/dist/plugin/latest.d.ts +12 -0
- package/dist/plugin/latest.js +52 -0
- package/dist/plugin/local.js +1 -2
- package/dist/plugin/markdown.d.ts +2 -2
- package/dist/plugin/markdown.js +1 -1
- package/dist/plugin/media.js +1 -1
- package/dist/plugin/osd.js +2 -2
- package/dist/plugin/packages.js +1 -1
- package/dist/plugin/passes.d.ts +111 -0
- package/dist/plugin/passes.js +341 -0
- package/dist/plugin/player.d.ts +2 -3
- package/dist/plugin/player.js +2 -7
- package/dist/plugin/resolvers.js +1 -1
- package/dist/plugin/runtime.d.ts +26 -185
- package/dist/plugin/runtime.js +35 -755
- package/dist/plugin/scope.d.ts +2 -1
- package/dist/plugin/scope.js +5 -2
- package/dist/plugin/services/dialogs.d.ts +3 -0
- package/dist/plugin/services/dialogs.js +62 -0
- package/dist/plugin/services.js +1 -1
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/shaders.js +2 -1
- package/dist/plugin/stream.js +1 -2
- package/dist/plugin/subtitles.js +2 -2
- package/dist/plugin/testing/commands.d.ts +65 -0
- package/dist/plugin/testing/commands.js +412 -0
- package/dist/plugin/testing/core-actions.d.ts +39 -0
- package/dist/plugin/testing/core-actions.js +371 -0
- package/dist/plugin/testing/declarations.d.ts +37 -0
- package/dist/plugin/testing/declarations.js +80 -0
- package/dist/plugin/testing/i18n.d.ts +28 -0
- package/dist/plugin/testing/i18n.js +107 -0
- package/dist/plugin/testing/index.d.ts +3140 -0
- package/dist/plugin/testing/index.js +2796 -0
- package/dist/plugin/testing/internals.d.ts +76 -0
- package/dist/plugin/testing/internals.js +60 -0
- package/dist/plugin/testing/jobs.d.ts +9 -0
- package/dist/plugin/testing/jobs.js +304 -0
- package/dist/plugin/testing/layout.d.ts +85 -0
- package/dist/plugin/testing/layout.js +622 -0
- package/dist/plugin/testing/media.d.ts +55 -0
- package/dist/plugin/testing/media.js +615 -0
- package/dist/plugin/testing/osd.d.ts +4 -0
- package/dist/plugin/testing/osd.js +57 -0
- package/dist/plugin/testing/processing.d.ts +56 -0
- package/dist/plugin/testing/processing.js +491 -0
- package/dist/plugin/testing/services.d.ts +50 -0
- package/dist/plugin/testing/services.js +466 -0
- package/dist/plugin/testing/transport.d.ts +29 -0
- package/dist/plugin/testing/transport.js +140 -0
- package/dist/plugin/testing.d.ts +1 -3140
- package/dist/plugin/testing.js +1 -6203
- package/dist/plugin/timeline.js +2 -1
- package/dist/plugin/view/base.d.ts +29 -0
- package/dist/plugin/view/base.js +88 -0
- package/dist/plugin/view/binder.d.ts +22 -0
- package/dist/plugin/view/binder.js +57 -0
- package/dist/plugin/view/image.d.ts +21 -0
- package/dist/plugin/view/image.js +60 -0
- package/dist/plugin/view/layout.d.ts +47 -0
- package/dist/plugin/view/layout.js +171 -0
- package/dist/plugin/view/panel.d.ts +3 -0
- package/dist/plugin/view/panel.js +178 -0
- package/dist/plugin/view/window.d.ts +3 -0
- package/dist/plugin/view/window.js +99 -0
- package/dist/plugin/view/writer.d.ts +58 -0
- package/dist/plugin/view/writer.js +634 -0
- package/dist/plugin/view.js +2 -1
- package/dist/plugin/window.js +1 -1
- package/llms.txt +1 -1
- package/package.json +6 -4
- package/dist/plugin/ui.d.ts +0 -74
- package/dist/plugin/ui.js +0 -1234
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -1,3140 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { HostError } from "../wire.ts";
|
|
3
|
-
import type { PackageManifest, PackageSource } from "./types.ts";
|
|
4
|
-
import type { Json, LayoutSlot, LocalServices, PlaybackStats, Transport, WireSystemInfo } from "../wire.ts";
|
|
5
|
-
import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS } from "../commands.ts";
|
|
6
|
-
import type { BuiltinCommand, BuiltinSubmenu } from "../commands.ts";
|
|
7
|
-
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.ts";
|
|
8
|
-
import type { CapabilityInfo } from "../capabilities.ts";
|
|
9
|
-
import { OSD_LIMITS } from "./osd.ts";
|
|
10
|
-
import { TIMELINE_LIMITS } from "./timeline.ts";
|
|
11
|
-
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.ts";
|
|
12
|
-
import { SUBTITLE_LIMITS } from "./subtitles.ts";
|
|
13
|
-
import { BINDING_INPUTS } from "./bindings.ts";
|
|
14
|
-
import { THEME_PALETTES } from "../palettes.ts";
|
|
15
|
-
import { IMAGE_LIMITS, UI_VALUE_LIMITS } from "./values.ts";
|
|
16
|
-
import type { CommandDeclaration } from "../commands.ts";
|
|
17
|
-
import type { SourceState } from "../source.ts";
|
|
18
|
-
import type { SubtitleState } from "../subtitles.ts";
|
|
19
|
-
import { VIDEO_PROCESSING_LIMITS } from "../processing.ts";
|
|
20
|
-
import type { RenderingMode, RenderingPreference, RenderingReason, RenderSettings, UpscalingDecodingCause, UpscalingPlugin, UpscalingReason, UpscalingVideoSize, ViewSettings } from "./types.ts";
|
|
21
|
-
import type { MediaJob } from "../media.ts";
|
|
22
|
-
import type { LanguagePreference, Locale, Plugin, StopReason, TemplateArgs, TextValue, ThemeId } from "./types.ts";
|
|
23
|
-
import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
|
|
24
|
-
/** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
|
|
25
|
-
export { HostError };
|
|
26
|
-
/** The player's command caps, which the test host enforces as well. */
|
|
27
|
-
export { COMMAND_LIMITS, MENU_IDS };
|
|
28
|
-
/** The icons and limits of bottom-bar buttons, which the test host checks as well. */
|
|
29
|
-
export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS };
|
|
30
|
-
/** The submenus of the player's built-in plugins, by key path. */
|
|
31
|
-
export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS };
|
|
32
|
-
export type { BuiltinCommand, BuiltinSubmenu };
|
|
33
|
-
/** The manifest capabilities: what each opens and how the store shows it. */
|
|
34
|
-
export { CAPABILITY_INFO };
|
|
35
|
-
export type { CapabilityInfo };
|
|
36
|
-
/** The longest message a plugin may send the player. */
|
|
37
|
-
export { MESSAGE_LIMIT };
|
|
38
|
-
/** The player's limits on the on-screen display. */
|
|
39
|
-
export { OSD_LIMITS };
|
|
40
|
-
/** The player's limits on a timeline layer. */
|
|
41
|
-
export { TIMELINE_LIMITS };
|
|
42
|
-
/** The player's limits on stored secrets. */
|
|
43
|
-
export { SECRET_LIMITS };
|
|
44
|
-
/** The player's limits on `plugin.storage.session`. */
|
|
45
|
-
export { SESSION_STORAGE_LIMITS };
|
|
46
|
-
/** The player's limits on loaded subtitle files, which the test host enforces as well. */
|
|
47
|
-
export { SUBTITLE_LIMITS };
|
|
48
|
-
/** The player's bounds on convolution, lut3d and shader-channel requests, which the test host enforces as well. */
|
|
49
|
-
export { VIDEO_PROCESSING_LIMITS };
|
|
50
|
-
/** The names bindings are made of. */
|
|
51
|
-
export { BINDING_INPUTS };
|
|
52
|
-
/** The colours of the player's palettes, by `Theme` token. */
|
|
53
|
-
export { THEME_PALETTES };
|
|
54
|
-
/** The player's property value limits, which the test host enforces as well. */
|
|
55
|
-
export { IMAGE_LIMITS, UI_VALUE_LIMITS };
|
|
56
|
-
/**
|
|
57
|
-
* The player's limits on one text value ({@link Text}, {@link TextList},
|
|
58
|
-
* with everything nested in it), which the test host enforces as well:
|
|
59
|
-
* nesting depth 8, 1024 texts, lists and arguments in all, 16 arguments per
|
|
60
|
-
* text, 256 items per list, 4096 bytes per source text or string argument.
|
|
61
|
-
*
|
|
62
|
-
* @since 1.5.11
|
|
63
|
-
*/
|
|
64
|
-
export { TEXT_LIMITS };
|
|
65
|
-
/**
|
|
66
|
-
* The separator of each {@link ListStyle} per interface language, as the
|
|
67
|
-
* player joins a {@link TextList}.
|
|
68
|
-
*
|
|
69
|
-
* @since 1.5.11
|
|
70
|
-
*/
|
|
71
|
-
export { LIST_SEPARATORS };
|
|
72
|
-
/**
|
|
73
|
-
* The player's interface languages, English (the source) first, as the
|
|
74
|
-
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
75
|
-
*
|
|
76
|
-
* @example Check that every language translates a menu item
|
|
77
|
-
* The test fails for each interface language in which the "Open…" item
|
|
78
|
-
* still shows English. It starts the plugin once per language except
|
|
79
|
-
* English (`LOCALES[0]`): the `locale` option loads the plugin's
|
|
80
|
-
* `locales/<locale>.po` and the host shows menu texts translated with it,
|
|
81
|
-
* which `display` reports.
|
|
82
|
-
* ```ts
|
|
83
|
-
* import type { Plugin } from "@vestra-player/sdk";
|
|
84
|
-
* declare function activate(plugin: Plugin): Promise<void>;
|
|
85
|
-
* // ---cut---
|
|
86
|
-
* import assert from "node:assert/strict";
|
|
87
|
-
* import { LOCALES, startPlugin } from "@vestra-player/sdk/testing";
|
|
88
|
-
*
|
|
89
|
-
* const directory = new URL("..", import.meta.url);
|
|
90
|
-
* for (const locale of LOCALES.slice(1)) {
|
|
91
|
-
* const { host, stop } = await startPlugin(activate, { directory, locale });
|
|
92
|
-
* try {
|
|
93
|
-
* assert.notEqual(host.command("open").display.title, "Open…", locale);
|
|
94
|
-
* } finally {
|
|
95
|
-
* await stop(); // also when the assertion fails
|
|
96
|
-
* }
|
|
97
|
-
* }
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
export { LOCALES };
|
|
101
|
-
type Handler = (params: Record<string, Json>) => unknown;
|
|
102
|
-
type Call = {
|
|
103
|
-
method: string;
|
|
104
|
-
params: Record<string, Json>;
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* A tool window as the test host keeps it: the record of a
|
|
108
|
-
* `plugin.windows.create(...)` window after its first `show()`. Get one with
|
|
109
|
-
* {@link TestHost.window}.
|
|
110
|
-
*
|
|
111
|
-
* @example Change a setting in a tool window from a test
|
|
112
|
-
* The test opens the plugin's Picture window, moves its brightness control
|
|
113
|
-
* and checks that the plugin wrote the value back. `emit` fires a Slint
|
|
114
|
-
* callback as if the user used the control; the plugin's `on` handler runs
|
|
115
|
-
* and sets properties, which reach the host as requests, so `settle()`
|
|
116
|
-
* waits for them before `props` is read.
|
|
117
|
-
* ```ts
|
|
118
|
-
* import assert from "node:assert/strict";
|
|
119
|
-
* declare const host: TestHost;
|
|
120
|
-
* // ---cut---
|
|
121
|
-
* await host.command("open-picture").run(); // the plugin calls pictureWindow.show()
|
|
122
|
-
* const picture = host.window("picture");
|
|
123
|
-
* assert.equal(picture.visible, true);
|
|
124
|
-
* picture.emit("adjust", "brightness", 40); // fire a Slint callback
|
|
125
|
-
* await host.settle();
|
|
126
|
-
* assert.equal(picture.props["brightness"], 40);
|
|
127
|
-
* ```
|
|
128
|
-
*
|
|
129
|
-
* @remarks
|
|
130
|
-
* A record, not a window: no Slint file is compiled and nothing is drawn.
|
|
131
|
-
* `props` holds what the plugin wrote, by Slint name; `emit` fires a
|
|
132
|
-
* callback the way the user's input would; `userClose` closes it the way
|
|
133
|
-
* the title-bar button would.
|
|
134
|
-
*
|
|
135
|
-
* @capability None.
|
|
136
|
-
* @since 1.0.0
|
|
137
|
-
* @category Testing
|
|
138
|
-
*/
|
|
139
|
-
export interface FakeWindow {
|
|
140
|
-
/** The `.slint` file the window was created from (package-relative). */
|
|
141
|
-
file: string;
|
|
142
|
-
/** Whether the window is shown (`show()`) or hidden (`hide()`). */
|
|
143
|
-
visible: boolean;
|
|
144
|
-
/**
|
|
145
|
-
* The properties the plugin wrote, by their Slint (kebab-case) names:
|
|
146
|
-
* `canMark` is stored as `can-mark`. Runtime images appear in their wire
|
|
147
|
-
* form; read the path with {@link TestHost.imagePath}.
|
|
148
|
-
*/
|
|
149
|
-
props: Record<string, Json>;
|
|
150
|
-
/** The Slint callback names (kebab-case) the plugin bound with `on()`. */
|
|
151
|
-
callbacks: string[];
|
|
152
|
-
/**
|
|
153
|
-
* Fire a Slint callback as if the user triggered it in the window. The
|
|
154
|
-
* name may be kebab-case or camelCase; `args` are the callback arguments.
|
|
155
|
-
* Follow it with `await host.settle()` before asserting.
|
|
156
|
-
*
|
|
157
|
-
* @capability None.
|
|
158
|
-
*
|
|
159
|
-
* @example Type into a window's search field
|
|
160
|
-
* The test types "cat" into the window's search box and checks the
|
|
161
|
-
* plugin's results. `emit` delivers the Slint callback `search-changed`
|
|
162
|
-
* with its argument to the plugin's `on` handler; `settle()` waits for the
|
|
163
|
-
* properties the handler wrote.
|
|
164
|
-
* ```ts
|
|
165
|
-
* import assert from "node:assert/strict";
|
|
166
|
-
* declare const host: TestHost;
|
|
167
|
-
* // ---cut---
|
|
168
|
-
* const search = host.window("search");
|
|
169
|
-
* search.emit("searchChanged", "cat"); // camelCase or kebab-case
|
|
170
|
-
* await host.settle();
|
|
171
|
-
* assert.ok((search.props["results"] as unknown[]).length > 0);
|
|
172
|
-
* ```
|
|
173
|
-
*
|
|
174
|
-
* @since 1.0.0
|
|
175
|
-
*/
|
|
176
|
-
emit(callback: string, ...args: Json[]): void;
|
|
177
|
-
/**
|
|
178
|
-
* The user closes the window with its title-bar button: the window closes
|
|
179
|
-
* and the plugin's `window.onClose` listeners run (unlike the plugin's own
|
|
180
|
-
* `close()`).
|
|
181
|
-
*
|
|
182
|
-
* @capability None.
|
|
183
|
-
*
|
|
184
|
-
* @example Close a window the way the user does
|
|
185
|
-
* The test closes the plugin's window with its title-bar button and checks
|
|
186
|
-
* that the plugin stopped its timer. Unlike the plugin's own `close()`,
|
|
187
|
-
* `userClose` runs the plugin's `onClose` listeners.
|
|
188
|
-
* ```ts
|
|
189
|
-
* import assert from "node:assert/strict";
|
|
190
|
-
* declare const host: TestHost;
|
|
191
|
-
* // ---cut---
|
|
192
|
-
* host.window("clock").userClose();
|
|
193
|
-
* await host.settle();
|
|
194
|
-
* assert.equal(host.window("clock").visible, false);
|
|
195
|
-
* ```
|
|
196
|
-
*
|
|
197
|
-
* @since 1.0.0
|
|
198
|
-
*/
|
|
199
|
-
userClose(): void;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* A file dialog as the player shows it ({@link TestHost.fileDialogs}).
|
|
203
|
-
*
|
|
204
|
-
* @since 1.5.11
|
|
205
|
-
*
|
|
206
|
-
* @category Testing
|
|
207
|
-
*/
|
|
208
|
-
export interface FileDialogText {
|
|
209
|
-
/** Which dialog. */
|
|
210
|
-
kind: "openFile" | "openFiles" | "openDirectory" | "saveFile";
|
|
211
|
-
/** Its title as shown, if the plugin gave one. */
|
|
212
|
-
title?: string;
|
|
213
|
-
/** The proposed file name as shown, if the plugin gave one. */
|
|
214
|
-
fileName?: string;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* A command the plugin registered with `plugin.commands.add`, as the test
|
|
218
|
-
* host keeps it. Get one with {@link TestHost.command} (the plugin's own) or
|
|
219
|
-
* {@link TestHost.coreAction} (the player's core actions).
|
|
220
|
-
*
|
|
221
|
-
* @example Run a checkable menu item from a test
|
|
222
|
-
* The test checks the Loop item's menu place, picks it and sees its check
|
|
223
|
-
* mark turn on. `host.command(id)` is the host's record of what the plugin
|
|
224
|
-
* declared and the state it last sent; `run()` calls the plugin's `run`
|
|
225
|
-
* like the menu, after which the SDK re-evaluates the command's state and
|
|
226
|
-
* sends it, and `run()` resolves once the host is idle again.
|
|
227
|
-
* ```ts
|
|
228
|
-
* import assert from "node:assert/strict";
|
|
229
|
-
* declare const host: TestHost;
|
|
230
|
-
* // ---cut---
|
|
231
|
-
* const toggle = host.command("toggle-loop");
|
|
232
|
-
* assert.deepEqual(toggle.declaration.path, ["Playback", "Loop"]);
|
|
233
|
-
* assert.equal(toggle.state.checked, false);
|
|
234
|
-
* await toggle.run();
|
|
235
|
-
* assert.equal(host.command("toggle-loop").state.checked, true);
|
|
236
|
-
* ```
|
|
237
|
-
*
|
|
238
|
-
* @remarks
|
|
239
|
-
* The host's record of one command: what the plugin declared, the state
|
|
240
|
-
* it last sent, and how the menu shows it in {@link TestHost.locale}.
|
|
241
|
-
* Read it again after `settle()` to see changes; `run` executes it as the
|
|
242
|
-
* menu would.
|
|
243
|
-
*
|
|
244
|
-
* @capability None.
|
|
245
|
-
* @since 1.0.0
|
|
246
|
-
* @category Testing
|
|
247
|
-
*/
|
|
248
|
-
export interface FakeCommand {
|
|
249
|
-
/**
|
|
250
|
-
* What the plugin declared: id, menu path, title, order, group, default
|
|
251
|
-
* bindings, focus and repeat. Wire-level shape; it may gain fields in minor
|
|
252
|
-
* versions.
|
|
253
|
-
*/
|
|
254
|
-
declaration: CommandDeclaration;
|
|
255
|
-
/**
|
|
256
|
-
* What the menu shows, as the player shows it in the test host's
|
|
257
|
-
* {@link TestHost.locale}: the title and each path segment translated with
|
|
258
|
-
* the plugin's catalog (the player's own menus in the player's words), the
|
|
259
|
-
* keys normalized (`@video` is "Video").
|
|
260
|
-
*
|
|
261
|
-
* @since 1.5.9
|
|
262
|
-
*/
|
|
263
|
-
readonly display: {
|
|
264
|
-
/** The menu text of the item. */
|
|
265
|
-
title: string;
|
|
266
|
-
/** Each segment of the path as shown; empty without a path. */
|
|
267
|
-
path: string[];
|
|
268
|
-
/**
|
|
269
|
-
* The disabled or busy reason as shown, `null` without one (a plugin
|
|
270
|
-
* command's; not enumerable, so comparing `display` with
|
|
271
|
-
* `{ title, path }` still works).
|
|
272
|
-
*
|
|
273
|
-
* @since 1.5.11
|
|
274
|
-
*/
|
|
275
|
-
reason?: string | null;
|
|
276
|
-
/**
|
|
277
|
-
* The category as shown (`CommandOptions.category`, translated with
|
|
278
|
-
* the plugin's catalog), `null` without one. Not enumerable, like
|
|
279
|
-
* `reason`.
|
|
280
|
-
*
|
|
281
|
-
* @since 1.6.4
|
|
282
|
-
*/
|
|
283
|
-
category?: string | null;
|
|
284
|
-
};
|
|
285
|
-
/**
|
|
286
|
-
* The conditions the plugin sent for `enabled` and `checked`
|
|
287
|
-
* (`plugin.when`, wire form), `undefined` without. Like the player, the
|
|
288
|
-
* host evaluates them in {@link FakeCommand.state} against `source`,
|
|
289
|
-
* `mainWindow.fullscreen`, `appearance` and `language`.
|
|
290
|
-
*
|
|
291
|
-
* @since 1.5.40
|
|
292
|
-
*/
|
|
293
|
-
when?: {
|
|
294
|
-
/** The condition of `enabled`. */
|
|
295
|
-
enabled?: Json;
|
|
296
|
-
/** The condition of `checked`. */
|
|
297
|
-
checked?: Json;
|
|
298
|
-
};
|
|
299
|
-
/**
|
|
300
|
-
* The command state as the player shows it: what the plugin last sent
|
|
301
|
-
* (after the update pass), with the fields given as conditions
|
|
302
|
-
* (`plugin.when`) evaluated now. With an `enabled` condition, `reason`
|
|
303
|
-
* is `null` while the command is enabled and not busy.
|
|
304
|
-
*/
|
|
305
|
-
state: {
|
|
306
|
-
/** `false` when the plugin disabled the command. */
|
|
307
|
-
enabled: boolean;
|
|
308
|
-
/** The check mark: `true`, `false`, or `null` for none. */
|
|
309
|
-
checked: boolean | null;
|
|
310
|
-
/** `true` while the plugin reports the command as busy. */
|
|
311
|
-
busy: boolean;
|
|
312
|
-
/** The reason the plugin sent (source text or a text value; see `display.reason`). */
|
|
313
|
-
reason: TextValue | null;
|
|
314
|
-
};
|
|
315
|
-
/**
|
|
316
|
-
* The state revision the player holds; increases with every state update
|
|
317
|
-
* the SDK sends.
|
|
318
|
-
*/
|
|
319
|
-
revision: number;
|
|
320
|
-
/**
|
|
321
|
-
* Execute the command the way the menu would, then wait until the host is
|
|
322
|
-
* idle. Rejects (with a {@link HostError}) when the command is disabled or
|
|
323
|
-
* busy, like the player, which does not call `run` then.
|
|
324
|
-
*
|
|
325
|
-
* @capability None.
|
|
326
|
-
* @throws `command` (rejects) A `HostError`: the command is disabled or
|
|
327
|
-
* busy (the message says which).
|
|
328
|
-
*
|
|
329
|
-
* @example Check that a disabled item refuses to run
|
|
330
|
-
* The test checks that Export cannot run while nothing is open. Like the
|
|
331
|
-
* player, the host does not call `run` for a disabled or busy command;
|
|
332
|
-
* the promise rejects with a `HostError`.
|
|
333
|
-
* ```ts
|
|
334
|
-
* import assert from "node:assert/strict";
|
|
335
|
-
* declare const host: TestHost;
|
|
336
|
-
* // ---cut---
|
|
337
|
-
* await assert.rejects(host.command("export").run());
|
|
338
|
-
* ```
|
|
339
|
-
*
|
|
340
|
-
* @since 1.0.0
|
|
341
|
-
*/
|
|
342
|
-
run(): Promise<void>;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* A command's bottom-bar button (`CommandOptions.button`) as the player shows
|
|
346
|
-
* it: an entry of {@link TestHost.barButtons}.
|
|
347
|
-
*
|
|
348
|
-
* @since 1.5.29
|
|
349
|
-
*
|
|
350
|
-
* @remarks
|
|
351
|
-
* A bottom-bar button as the player would show it, derived from the
|
|
352
|
-
* command's declaration and state each time it is read: clickable while
|
|
353
|
-
* the command is enabled and not busy, accent-coloured while checked, with
|
|
354
|
-
* the command's title (and reason) as the tooltip.
|
|
355
|
-
*
|
|
356
|
-
* @capability None.
|
|
357
|
-
*
|
|
358
|
-
* @example Check a bar button's tooltip while it is disabled
|
|
359
|
-
* The test checks that the Export button shows why it cannot be clicked.
|
|
360
|
-
* The tooltip is the command's title; while the command is disabled or
|
|
361
|
-
* busy and has a non-empty `reason`, the reason follows on a second line.
|
|
362
|
-
* ```ts
|
|
363
|
-
* import assert from "node:assert/strict";
|
|
364
|
-
* declare const host: TestHost;
|
|
365
|
-
* // ---cut---
|
|
366
|
-
* const [button] = host.barButtons;
|
|
367
|
-
* assert.equal(button?.enabled, false);
|
|
368
|
-
* assert.match(button?.tooltip ?? "", /\n/); // the command declares a reason
|
|
369
|
-
* ```
|
|
370
|
-
*
|
|
371
|
-
* @category Testing
|
|
372
|
-
*/
|
|
373
|
-
export interface FakeBarButton {
|
|
374
|
-
/** The command's local id. */
|
|
375
|
-
readonly command: string;
|
|
376
|
-
/** The icon name, or the image's path for a `plugin.ui.image` icon. */
|
|
377
|
-
readonly icon: string;
|
|
378
|
-
/** `true` when the icon is the plugin's image. */
|
|
379
|
-
readonly image: boolean;
|
|
380
|
-
/** The declared `order`. */
|
|
381
|
-
readonly order: number;
|
|
382
|
-
/** Clickable: the command is enabled and not busy. */
|
|
383
|
-
readonly enabled: boolean;
|
|
384
|
-
/** Shown with the on dot under its icon: the command is checked. */
|
|
385
|
-
readonly checked: boolean;
|
|
386
|
-
/**
|
|
387
|
-
* The tooltip without the key: the command's shown title (trailing "…"
|
|
388
|
-
* removed), then, while it cannot run, its reason on a second line.
|
|
389
|
-
*/
|
|
390
|
-
readonly tooltip: string;
|
|
391
|
-
/**
|
|
392
|
-
* Click the button, then wait until the host is idle: runs the command
|
|
393
|
-
* as its menu item would. Rejects (with a {@link HostError}) while the
|
|
394
|
-
* button is disabled, and once the command is gone.
|
|
395
|
-
*
|
|
396
|
-
* @capability None.
|
|
397
|
-
* @throws `command` (rejects) A `HostError`: the command is disabled or
|
|
398
|
-
* busy.
|
|
399
|
-
* @throws `missing` (rejects) A `HostError`: the command is gone.
|
|
400
|
-
*
|
|
401
|
-
* @example Click the button and wait for its effect
|
|
402
|
-
* The test clicks the plugin's button and checks the OSD. `click` runs the
|
|
403
|
-
* command like its menu item and resolves once the host is idle.
|
|
404
|
-
* ```ts
|
|
405
|
-
* import assert from "node:assert/strict";
|
|
406
|
-
* declare const host: TestHost;
|
|
407
|
-
* // ---cut---
|
|
408
|
-
* await host.barButtons[0]?.click();
|
|
409
|
-
* assert.ok(host.osd.length > 0);
|
|
410
|
-
* ```
|
|
411
|
-
*
|
|
412
|
-
* @since 1.5.29
|
|
413
|
-
*/
|
|
414
|
-
click(): Promise<void>;
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* A generated stream (`plugin.player.openStream`) as the test host sees it:
|
|
418
|
-
* the fake player end of the frame connection. Assertion surface; see
|
|
419
|
-
* {@link TestHost.producers}.
|
|
420
|
-
*
|
|
421
|
-
* @remarks
|
|
422
|
-
* The player's end of a generated stream's frame connection
|
|
423
|
-
* ({@link TestHost.producers}): how many frames arrived and whether the
|
|
424
|
-
* plugin released it or the player retired it. Tests assert on it; nothing
|
|
425
|
-
* is played.
|
|
426
|
-
*
|
|
427
|
-
* @capability None.
|
|
428
|
-
*
|
|
429
|
-
* @example Check that the stream delivers frames
|
|
430
|
-
* The test starts the plugin's generator and checks that video and audio
|
|
431
|
-
* arrive. The counters grow as the SDK pulls frames and sends them over
|
|
432
|
-
* the fake connection.
|
|
433
|
-
* ```ts
|
|
434
|
-
* import assert from "node:assert/strict";
|
|
435
|
-
* declare const host: TestHost;
|
|
436
|
-
* // ---cut---
|
|
437
|
-
* await host.command("start-generator").run();
|
|
438
|
-
* await host.settle(200); // let a few frames through
|
|
439
|
-
* const [producer] = host.producers.values();
|
|
440
|
-
* assert.ok((producer?.videoFrames ?? 0) > 0 && (producer?.receivedFrames ?? 0) > 0);
|
|
441
|
-
* ```
|
|
442
|
-
*
|
|
443
|
-
* @since 1.0.0
|
|
444
|
-
* @category Testing
|
|
445
|
-
*/
|
|
446
|
-
export interface FakeProducer {
|
|
447
|
-
/** The producer id the host assigned (`1`, `2`, … per host). */
|
|
448
|
-
id: number;
|
|
449
|
-
/** Audio sample frames received so far (one stereo f32 pair = 1 frame). */
|
|
450
|
-
receivedFrames: number;
|
|
451
|
-
/** Video frames received so far. */
|
|
452
|
-
videoFrames: number;
|
|
453
|
-
/** `true` once the plugin released the producer (stream stopped or disposed). */
|
|
454
|
-
released: boolean;
|
|
455
|
-
/**
|
|
456
|
-
* `true` once the player retired the producer, as the real player does at
|
|
457
|
-
* once: another source replaced it (`open`, another stream, a resolved
|
|
458
|
-
* file), the source was closed, or {@link TestHost.failStream} failed it.
|
|
459
|
-
* A retired producer's `producer.state` answers `unavailable`.
|
|
460
|
-
*/
|
|
461
|
-
retired: boolean;
|
|
462
|
-
/**
|
|
463
|
-
* `true` if the frame connection closed while this stream was still the
|
|
464
|
-
* player's source: the player reports that as a playback failure
|
|
465
|
-
* ("producer transport closed"). `stop()` closes the source first.
|
|
466
|
-
*/
|
|
467
|
-
droppedWhileSelected: boolean;
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* A Markdown document as {@link TestHost.markdown} reads it back.
|
|
471
|
-
*
|
|
472
|
-
* @since 1.1.0
|
|
473
|
-
* @category Testing
|
|
474
|
-
*/
|
|
475
|
-
export interface MarkdownWire {
|
|
476
|
-
/** The Markdown text. */
|
|
477
|
-
text: string;
|
|
478
|
-
/** The base given to `ui.markdown`, or `null`. */
|
|
479
|
-
base: string | null;
|
|
480
|
-
/** Remote image address → the file the SDK downloaded for it. */
|
|
481
|
-
images: Record<string, string>;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* One mounted panel of the plugin under test ({@link TestHost.mounted},
|
|
485
|
-
* {@link TestHost.panels}): a {@link FakeWindow} record with its slot,
|
|
486
|
-
* component and interaction locks.
|
|
487
|
-
*
|
|
488
|
-
* @remarks
|
|
489
|
-
* One per slot the plugin mounted (SDK 1.6.15), kept in
|
|
490
|
-
* {@link TestHost.panels}; disposing a panel removes only its own.
|
|
491
|
-
*
|
|
492
|
-
* @example Check the adjust panel's locks
|
|
493
|
-
* The plugin's adjust panel reports a drag; the test reads the locks it
|
|
494
|
-
* sent. `mounted(slot)` picks one of the plugin's panels by slot.
|
|
495
|
-
* ```ts
|
|
496
|
-
* import assert from "node:assert/strict";
|
|
497
|
-
* declare const host: TestHost;
|
|
498
|
-
* // ---cut---
|
|
499
|
-
* const panel: FakePanel = host.mounted("adjust");
|
|
500
|
-
* assert.deepEqual(panel.locks, { focused: false, dragging: true, popupOpen: false });
|
|
501
|
-
* ```n *
|
|
502
|
-
* @capability None.
|
|
503
|
-
* @since 1.6.15
|
|
504
|
-
* @category Testing
|
|
505
|
-
*/
|
|
506
|
-
export type FakePanel = Omit<FakeWindow, "callbacks" | "userClose"> & {
|
|
507
|
-
/** `"bottom"`, `"sidebar"`, `"adjust"` or `"backdrop"`. */
|
|
508
|
-
slot: string;
|
|
509
|
-
/** The exported component the SDK found in the file. */
|
|
510
|
-
component: string;
|
|
511
|
-
/** The last `panel.setInteraction(...)` locks, `null` before any. */
|
|
512
|
-
locks: Json;
|
|
513
|
-
};
|
|
514
|
-
/**
|
|
515
|
-
* An in-memory fake Vestra Player for plugin tests.
|
|
516
|
-
*
|
|
517
|
-
* The plugin under test talks to it over the real wire protocol (the host is
|
|
518
|
-
* the plugin's transport), so everything the SDK does runs unchanged. The
|
|
519
|
-
* host answers each request with simplified player behaviour, records what
|
|
520
|
-
* the plugin did, and lets the test act as the user or another plugin.
|
|
521
|
-
*
|
|
522
|
-
* Members fall into a few groups:
|
|
523
|
-
*
|
|
524
|
-
* - **Drive the plugin**: {@link TestHost.command}`(id).run()`,
|
|
525
|
-
* {@link TestHost.press}, {@link TestHost.window}`(id).emit(...)`,
|
|
526
|
-
* {@link TestHost.mounted}`().emit(...)`, {@link TestHost.drop},
|
|
527
|
-
* {@link TestHost.step}, {@link TestHost.dialogAnswers}.
|
|
528
|
-
* - **Simulate the player and other plugins**: {@link TestHost.setSource},
|
|
529
|
-
* {@link TestHost.openElsewhere}, {@link TestHost.setMainWindow},
|
|
530
|
-
* {@link TestHost.setTimelineLayer}, {@link TestHost.setAppearance},
|
|
531
|
-
* {@link TestHost.setPackages}, {@link TestHost.emit}.
|
|
532
|
-
* - **Preset data** the plugin reads: {@link TestHost.stats},
|
|
533
|
-
* {@link TestHost.app}, {@link TestHost.systemInfo},
|
|
534
|
-
* {@link TestHost.chapters}, {@link TestHost.audioDevices},
|
|
535
|
-
* {@link TestHost.secrets}, {@link TestHost.clipboard},
|
|
536
|
-
* {@link TestHost.tools}, {@link TestHost.rendering},
|
|
537
|
-
* {@link TestHost.upscaling}.
|
|
538
|
-
* - **Assert** on records: {@link TestHost.osd}, {@link TestHost.calls},
|
|
539
|
-
* {@link TestHost.sent}, {@link TestHost.conflicts},
|
|
540
|
-
* {@link TestHost.layout}, {@link TestHost.menus}, and many more.
|
|
541
|
-
* - **Inject failures**: {@link TestHost.handle} / {@link TestHost.reset}.
|
|
542
|
-
* - **Synchronise**: {@link TestHost.settle}.
|
|
543
|
-
*
|
|
544
|
-
* Usually created by {@link startPlugin}. Create one yourself only to preset
|
|
545
|
-
* data that the plugin reads during activation (`app`, `appearance`,
|
|
546
|
-
* `secrets`, ...), then pass it as `startPlugin(activate, { host })`.
|
|
547
|
-
*
|
|
548
|
-
* Fields that mirror player state at the wire level (`source`, `subtitles`,
|
|
549
|
-
* `processing`, `snapshots`, `jobs`, `producers`, `uploads`, `resolvers`,
|
|
550
|
-
* `calls`) use the protocol's shapes. They are fine for assertions but are
|
|
551
|
-
* not part of the plugin API: their shape may change in minor versions.
|
|
552
|
-
*
|
|
553
|
-
* @example Preset the player, then react to another plugin's file
|
|
554
|
-
* The test starts the plugin in a player with the dark theme, opens a file
|
|
555
|
-
* the way another plugin would, and checks the plugin's panel title. A host
|
|
556
|
-
* created first can be preset with data the plugin reads during
|
|
557
|
-
* activation; `startPlugin` then uses it. `openElsewhere` changes the
|
|
558
|
-
* source and broadcasts it, the plugin's listener updates its panel, and
|
|
559
|
-
* `settle()` waits for those requests before the assertion.
|
|
560
|
-
* ```ts
|
|
561
|
-
* import assert from "node:assert/strict";
|
|
562
|
-
* import test from "node:test";
|
|
563
|
-
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
564
|
-
* import activate from "../main.ts";
|
|
565
|
-
*
|
|
566
|
-
* test("shows the playing file in the panel", async () => {
|
|
567
|
-
* const host = new TestHost();
|
|
568
|
-
* host.appearance = "vestra"; // read during activation
|
|
569
|
-
* const { stop } = await startPlugin(activate, {
|
|
570
|
-
* directory: new URL("..", import.meta.url),
|
|
571
|
-
* host,
|
|
572
|
-
* });
|
|
573
|
-
* host.openElsewhere("/media/clip.mp4"); // another plugin opened a file
|
|
574
|
-
* await host.settle();
|
|
575
|
-
* assert.equal(host.mounted().props["title"], "clip.mp4");
|
|
576
|
-
* await stop();
|
|
577
|
-
* });
|
|
578
|
-
* ```
|
|
579
|
-
*
|
|
580
|
-
* @remarks
|
|
581
|
-
* An in-memory player that speaks the real wire protocol, so the SDK's own
|
|
582
|
-
* logic runs as it does in the player; only the player side is simulated
|
|
583
|
-
* (no decoding, no Slint, no windows). Make one per test, preset it if the
|
|
584
|
-
* plugin reads something during activation, start the plugin with
|
|
585
|
-
* {@link startPlugin}, drive it, `settle()`, and assert on what it
|
|
586
|
-
* recorded.
|
|
587
|
-
*
|
|
588
|
-
* @capability None.
|
|
589
|
-
* @see [Test a plugin](../../docs/how-to/testing.md)
|
|
590
|
-
* @since 1.0.0
|
|
591
|
-
* @category Testing
|
|
592
|
-
*/
|
|
593
|
-
export declare class TestHost implements Transport {
|
|
594
|
-
#private;
|
|
595
|
-
/**
|
|
596
|
-
* Every request the plugin sent, in order: `{ method, params }`.
|
|
597
|
-
* Wire-level; prefer {@link TestHost.sent} for one method.
|
|
598
|
-
*/
|
|
599
|
-
readonly calls: Call[];
|
|
600
|
-
/** The host events the plugin subscribed to (needs `events` etc.). */
|
|
601
|
-
readonly subscriptions: Set<string>;
|
|
602
|
-
/**
|
|
603
|
-
* The subscribed events the plugin really listens to: an `onChange` and
|
|
604
|
-
* the like, or a binding that reads the state. The SDK's own copies of
|
|
605
|
-
* the state (what keeps `plugin.player.state` current) are left out.
|
|
606
|
-
* Empty means the player may stop the plugin while nothing uses it
|
|
607
|
-
* (unless it holds something else: a window, a panel, a filter…).
|
|
608
|
-
*
|
|
609
|
-
* @since 1.5.38
|
|
610
|
-
*/
|
|
611
|
-
readonly listening: Set<string>;
|
|
612
|
-
/**
|
|
613
|
-
* Why the plugin listens to each event of {@link TestHost.listening}:
|
|
614
|
-
* every listener and every binding that reads its state, as the
|
|
615
|
-
* player's log says them (`a listener added at main.ts:12`,
|
|
616
|
-
* `command "Save" enabled is a function that reads plugin.player.state
|
|
617
|
-
* (main.ts:40) — use plugin.when`), in the order they came.
|
|
618
|
-
*
|
|
619
|
-
* @since 1.5.40
|
|
620
|
-
*/
|
|
621
|
-
readonly listeningWhy: Map<string, string[]>;
|
|
622
|
-
/**
|
|
623
|
-
* The player's source facts (phase, position, duration, paused, rate,
|
|
624
|
-
* audio tracks, loop, ...). Wire-level shape; change it with
|
|
625
|
-
* {@link TestHost.setSource}, which also broadcasts it.
|
|
626
|
-
*/
|
|
627
|
-
source: SourceState;
|
|
628
|
-
/** Tool windows by id (see {@link TestHost.window}). */
|
|
629
|
-
readonly windows: Map<string, FakeWindow>;
|
|
630
|
-
/**
|
|
631
|
-
* The plugin's mounted panels by slot, in mount order (SDK 1.6.15: one
|
|
632
|
-
* panel per slot). See {@link TestHost.mounted}.
|
|
633
|
-
*/
|
|
634
|
-
readonly panels: Map<string, FakePanel>;
|
|
635
|
-
/**
|
|
636
|
-
* The plugin's mounted panel (bottom bar, sidebar, adjust column or
|
|
637
|
-
* backdrop), the last mounted when it has several, or `null`. See
|
|
638
|
-
* {@link TestHost.mounted}.
|
|
639
|
-
*/
|
|
640
|
-
get panel(): FakePanel | null;
|
|
641
|
-
/** The plugin's commands by local id (see {@link TestHost.command}). */
|
|
642
|
-
readonly commands: Map<string, FakeCommand>;
|
|
643
|
-
/** The player's 14 core actions (`host/<id>`), kept apart from the plugin's `commands`. */
|
|
644
|
-
readonly coreCommands: Map<string, FakeCommand>;
|
|
645
|
-
/**
|
|
646
|
-
* Both panel positions as the player reports them (`null` while no panel
|
|
647
|
-
* is mounted there). You may change a slot directly and then call
|
|
648
|
-
* `host.emit("window.state", {})` to simulate a change made elsewhere.
|
|
649
|
-
*
|
|
650
|
-
* @example Hide the sidebar and check the player's layout
|
|
651
|
-
* With the plugin's panel in the sidebar, toggling the sidebar records
|
|
652
|
-
* that the user prefers it hidden. `toggle()` reads the slot from the
|
|
653
|
-
* player, asks for the opposite of its `preferred` flag and resolves once
|
|
654
|
-
* the player answered, so the host's `layout` already shows the new value.
|
|
655
|
-
* ```ts
|
|
656
|
-
* import assert from "node:assert/strict";
|
|
657
|
-
* declare const host: TestHost;
|
|
658
|
-
* // ---cut---
|
|
659
|
-
* assert.equal(host.layout.sidebar?.preferred, true);
|
|
660
|
-
* await plugin.panels.sidebar.toggle();
|
|
661
|
-
* assert.equal(host.layout.sidebar?.preferred, false);
|
|
662
|
-
* ```
|
|
663
|
-
*/
|
|
664
|
-
readonly layout: {
|
|
665
|
-
/**
|
|
666
|
-
* The bottom bar position: `preferred`, `effective`, `reason`,
|
|
667
|
-
* `detached`, `owner` and `panels` (its one panel).
|
|
668
|
-
*/
|
|
669
|
-
bottom: LayoutSlot | null;
|
|
670
|
-
/**
|
|
671
|
-
* The sidebar position: `preferred`, `effective`, `reason`, `detached`;
|
|
672
|
-
* `panels` are its tabs in order (`{ owner, plugin, title }`), `owner`
|
|
673
|
-
* the current tab's (the plugin under test is owner 1).
|
|
674
|
-
*/
|
|
675
|
-
sidebar: LayoutSlot | null;
|
|
676
|
-
/**
|
|
677
|
-
* The adjust column (SDK 1.6.11, player 0.7.6), like the sidebar but
|
|
678
|
-
* never `detached`. It starts hidden (`preferred: false`, `reason:
|
|
679
|
-
* "userHidden"`), as the player's does until the user opens it.
|
|
680
|
-
*/
|
|
681
|
-
adjust: LayoutSlot | null;
|
|
682
|
-
};
|
|
683
|
-
/**
|
|
684
|
-
* Every `osd.show` text as the player shows it (translated with the
|
|
685
|
-
* plugin's catalog and formatted with its args), newest last. The
|
|
686
|
-
* template and args the plugin sent are in `host.sent("osd.show")`.
|
|
687
|
-
*/
|
|
688
|
-
readonly osd: string[];
|
|
689
|
-
/**
|
|
690
|
-
* The action buttons of each `osd.show` message, in step with `osd`
|
|
691
|
-
* (index for index): each button's title as shown and its command; `[]`
|
|
692
|
-
* for a message without actions (SDK 1.6.9).
|
|
693
|
-
*/
|
|
694
|
-
readonly osdActions: {
|
|
695
|
-
/** The button title as shown, in the plugin's locale. */
|
|
696
|
-
title: string;
|
|
697
|
-
/** The local id of the plugin's command the button runs. */
|
|
698
|
-
command: string;
|
|
699
|
-
}[][];
|
|
700
|
-
/**
|
|
701
|
-
* Press button `index` of the newest OSD message, as the user does
|
|
702
|
-
* (player 0.7.6): its command runs as if picked from a menu, and settles.
|
|
703
|
-
* A disabled or busy command does nothing, as in the player.
|
|
704
|
-
*
|
|
705
|
-
* @param index - 0 or 1.
|
|
706
|
-
*
|
|
707
|
-
* @throws `Error` (rejects) The newest message has no button `index`.
|
|
708
|
-
*
|
|
709
|
-
* @example Press "Show in Folder" on the saved message
|
|
710
|
-
* The test checks the button the plugin put on its newest message, then
|
|
711
|
-
* presses it as the user would: the plugin's `reveal` command runs.
|
|
712
|
-
* ```ts
|
|
713
|
-
* import assert from "node:assert/strict";
|
|
714
|
-
* declare const host: TestHost;
|
|
715
|
-
* // ---cut---
|
|
716
|
-
* assert.deepEqual(host.osdActions.at(-1), [
|
|
717
|
-
* { title: "Show in Folder", command: "reveal" },
|
|
718
|
-
* ]);
|
|
719
|
-
* await host.pressOsdAction(0); // runs the plugin's `reveal` command
|
|
720
|
-
* ```
|
|
721
|
-
*
|
|
722
|
-
* @capability None.
|
|
723
|
-
* @since 1.6.9
|
|
724
|
-
*/
|
|
725
|
-
pressOsdAction(index: number): Promise<void>;
|
|
726
|
-
/** The text of this plugin's osd panel as shown, `null` when hidden. */
|
|
727
|
-
osdPanel: string | null;
|
|
728
|
-
/**
|
|
729
|
-
* The interface language the test host plays (`plugin.i18n.locale`), `"en"`
|
|
730
|
-
* by default. Set it before {@link startPlugin} (or pass its `locale`
|
|
731
|
-
* option): the plugin's catalog for it is read from the package's
|
|
732
|
-
* `locales/` then, and the menus ({@link FakeCommand.display}), OSD
|
|
733
|
-
* messages, dialog texts, settings page and panel titles show its
|
|
734
|
-
* translations, the way the player shows them.
|
|
735
|
-
*
|
|
736
|
-
* The player's own texts (core actions' messages and menus, its
|
|
737
|
-
* refusals) are English, or the player's Simplified Chinese for
|
|
738
|
-
* `"zh-Hans"`. While `locale` was never set they stay in Chinese, as
|
|
739
|
-
* players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
|
|
740
|
-
* passing; set `locale` to test what a user sees now.
|
|
741
|
-
*
|
|
742
|
-
* @example Check a menu path in Simplified Chinese
|
|
743
|
-
* The test starts the plugin with a Chinese interface and checks the
|
|
744
|
-
* menu path its Blur item shows. The `locale` option sets
|
|
745
|
-
* `host.locale` before activation, so the plugin's `locales/zh-Hans.po`
|
|
746
|
-
* is loaded; the built-in menu `@video` shows in the player's own words
|
|
747
|
-
* (视频), the item's title in the plugin's translation.
|
|
748
|
-
* ```ts
|
|
749
|
-
* import assert from "node:assert/strict";
|
|
750
|
-
* declare function activate(plugin: Plugin): Promise<void>;
|
|
751
|
-
* declare const directory: URL;
|
|
752
|
-
* // ---cut---
|
|
753
|
-
* const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
754
|
-
* assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
|
|
755
|
-
* await stop();
|
|
756
|
-
* ```
|
|
757
|
-
*
|
|
758
|
-
* @since 1.5.9
|
|
759
|
-
*/
|
|
760
|
-
get locale(): Locale;
|
|
761
|
-
set locale(locale: Locale);
|
|
762
|
-
/**
|
|
763
|
-
* Read the plugin's translations for {@link TestHost.locale} from a
|
|
764
|
-
* package directory, as the player would (`locales/<locale>.json` of a
|
|
765
|
-
* built package, else the source `locales/<locale>.po`); `startPlugin`
|
|
766
|
-
* does this. None for English; a missing file means none; a broken `.po`
|
|
767
|
-
* throws.
|
|
768
|
-
*
|
|
769
|
-
* @param directory - The package directory (holding `locales/`).
|
|
770
|
-
* @since 1.5.9
|
|
771
|
-
*
|
|
772
|
-
* @capability None.
|
|
773
|
-
*
|
|
774
|
-
* @example Load the translations after changing the language
|
|
775
|
-
* The test switches the host to German before starting the plugin and
|
|
776
|
-
* loads that catalog itself (`startPlugin` does the same). Texts are then
|
|
777
|
-
* shown in German.
|
|
778
|
-
* ```ts
|
|
779
|
-
* declare const host: TestHost;
|
|
780
|
-
* declare const directory: string;
|
|
781
|
-
* // ---cut---
|
|
782
|
-
* host.locale = "de";
|
|
783
|
-
* host.loadCatalog(directory); // locales/de.po (or de.json)
|
|
784
|
-
* ```
|
|
785
|
-
*/
|
|
786
|
-
loadCatalog(directory: string): void;
|
|
787
|
-
/**
|
|
788
|
-
* A text of the plugin as the player shows it: translated with the
|
|
789
|
-
* plugin's catalog ({@link TestHost.loadCatalog}), then formatted with
|
|
790
|
-
* `args` when given.
|
|
791
|
-
*
|
|
792
|
-
* @param text - English source text or template.
|
|
793
|
-
* @param args - Values for its placeholders.
|
|
794
|
-
* @returns The shown text.
|
|
795
|
-
*
|
|
796
|
-
* @example Compare with a translated message
|
|
797
|
-
* The test expects the text a Chinese user sees for a template the plugin
|
|
798
|
-
* shows. `translate` looks the English source up in the plugin's catalog
|
|
799
|
-
* for `host.locale` and fills in the arguments, the way the player does.
|
|
800
|
-
* ```ts
|
|
801
|
-
* import assert from "node:assert/strict";
|
|
802
|
-
* declare const host: TestHost;
|
|
803
|
-
* // ---cut---
|
|
804
|
-
* assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
|
|
805
|
-
* ```
|
|
806
|
-
*
|
|
807
|
-
* @since 1.5.9
|
|
808
|
-
*
|
|
809
|
-
* @capability None.
|
|
810
|
-
*/
|
|
811
|
-
translate(text: TextValue, args?: TemplateArgs): string;
|
|
812
|
-
/**
|
|
813
|
-
* The saved interface language (`plugin.appearance.language`), changed by
|
|
814
|
-
* `appearance.setLanguage` or {@link TestHost.setLanguage}.
|
|
815
|
-
*
|
|
816
|
-
* @since 1.5.9
|
|
817
|
-
*/
|
|
818
|
-
language: LanguagePreference;
|
|
819
|
-
/**
|
|
820
|
-
* Simulate another plugin (or the user) saving the interface language:
|
|
821
|
-
* pushes `appearance.changed`.
|
|
822
|
-
*
|
|
823
|
-
* @param language - `"system"` or a locale.
|
|
824
|
-
* @since 1.5.9
|
|
825
|
-
*
|
|
826
|
-
* @capability None.
|
|
827
|
-
*
|
|
828
|
-
* @example Simulate the user choosing French for the next start
|
|
829
|
-
* The plugin hears that the saved interface language changed. The host
|
|
830
|
-
* pushes `appearance.changed`, which a plugin with `events` sees in
|
|
831
|
-
* `appearance.language` once it has read it and subscribed; this run's
|
|
832
|
-
* language does not change.
|
|
833
|
-
* ```ts
|
|
834
|
-
* import assert from "node:assert/strict";
|
|
835
|
-
* declare const host: TestHost;
|
|
836
|
-
* // ---cut---
|
|
837
|
-
* void plugin.appearance.language; // read once: the SDK then follows it
|
|
838
|
-
* plugin.appearance.onChange(() => {}); // subscribes to appearance events
|
|
839
|
-
* host.setLanguage("fr");
|
|
840
|
-
* await host.settle();
|
|
841
|
-
* assert.equal(plugin.appearance.language, "fr");
|
|
842
|
-
* ```
|
|
843
|
-
*/
|
|
844
|
-
setLanguage(language: LanguagePreference): void;
|
|
845
|
-
/**
|
|
846
|
-
* Every `dialogs.confirm` as the player shows it: its texts translated
|
|
847
|
-
* (message and detail formatted with `args`), newest last. The options
|
|
848
|
-
* the plugin sent are in {@link TestHost.confirms}.
|
|
849
|
-
*
|
|
850
|
-
* @since 1.5.9
|
|
851
|
-
*/
|
|
852
|
-
readonly confirmTexts: {
|
|
853
|
-
/** The question. */
|
|
854
|
-
message: string;
|
|
855
|
-
/** The text under it, if any. */
|
|
856
|
-
detail?: string;
|
|
857
|
-
/** The confirm button. */
|
|
858
|
-
confirm: string;
|
|
859
|
-
/** The cancel button (the player's "Cancel" by default). */
|
|
860
|
-
cancel: string;
|
|
861
|
-
/** The checkbox, if any. */
|
|
862
|
-
checkbox?: string;
|
|
863
|
-
}[];
|
|
864
|
-
/**
|
|
865
|
-
* Every file dialog (`openFile`, `openFiles`, `openDirectory`,
|
|
866
|
-
* `saveFile`) as the player shows it: the title translated, a text file
|
|
867
|
-
* name translated, newest last. The options the plugin sent are in
|
|
868
|
-
* `host.sent("dialogs.open")`.
|
|
869
|
-
*
|
|
870
|
-
* @since 1.5.11
|
|
871
|
-
*/
|
|
872
|
-
readonly fileDialogs: FileDialogText[];
|
|
873
|
-
/** The sequence provider's flags, `null` without a provider. */
|
|
874
|
-
sequence: {
|
|
875
|
-
/** The provider's `canNext`, as last evaluated. */
|
|
876
|
-
canNext: boolean;
|
|
877
|
-
/** The provider's `canPrevious`, as last evaluated. */
|
|
878
|
-
canPrevious: boolean;
|
|
879
|
-
} | null;
|
|
880
|
-
/**
|
|
881
|
-
* What `player.stats()` returns. Fields added in later rounds may be left
|
|
882
|
-
* out; the SDK fills them with `null`.
|
|
883
|
-
*
|
|
884
|
-
* @example Pretend a 1080p H.264 video is playing
|
|
885
|
-
* The plugin's `player.stats()` now reports a 1920×1080 H.264 stream at
|
|
886
|
-
* 24 fps. The host answers `stats()` with this object as it stands, so a
|
|
887
|
-
* test sets it before the plugin asks; fields it leaves out come back as
|
|
888
|
-
* `null` from the SDK.
|
|
889
|
-
* ```ts
|
|
890
|
-
* declare const host: TestHost;
|
|
891
|
-
* // ---cut---
|
|
892
|
-
* host.stats = {
|
|
893
|
-
* ...host.stats, // keep the other fields
|
|
894
|
-
* video: { codec: "h264", width: 1920, height: 1080, fps: 24 },
|
|
895
|
-
* } as typeof host.stats;
|
|
896
|
-
* ```
|
|
897
|
-
*/
|
|
898
|
-
stats: PlaybackStats;
|
|
899
|
-
/** Every `subtitles.load`: its options and bytes, newest last. */
|
|
900
|
-
readonly subtitleLoads: {
|
|
901
|
-
/** The `format` option (`"srt"` by default). */
|
|
902
|
-
format: "srt" | "ass";
|
|
903
|
-
/** The `title` option, `null` when omitted. */
|
|
904
|
-
title: string | null;
|
|
905
|
-
/** The `select` option (`"primary"` by default). */
|
|
906
|
-
select: "primary" | "secondary" | "none";
|
|
907
|
-
/** The uploaded file bytes. */
|
|
908
|
-
bytes: Buffer;
|
|
909
|
-
}[];
|
|
910
|
-
/**
|
|
911
|
-
* The fake system clipboard. Preset `text` to test `readText`;
|
|
912
|
-
* `writeText` replaces it and `writeImage` appends the image path.
|
|
913
|
-
*/
|
|
914
|
-
readonly clipboard: {
|
|
915
|
-
/** The clipboard's text, `null` when it holds none. */
|
|
916
|
-
text: string | null;
|
|
917
|
-
/** Paths given to `clipboard.writeImage`, newest last. */
|
|
918
|
-
images: string[];
|
|
919
|
-
};
|
|
920
|
-
/** The effective `power.preventSleep` mode. */
|
|
921
|
-
power: "off" | "playing";
|
|
922
|
-
/**
|
|
923
|
-
* The fake `ffmpeg` / `ffprobe` behind `plugin.tools`. A tool without a
|
|
924
|
-
* handler is not found (`available()` is false, `run()` rejects with
|
|
925
|
-
* `unavailable`).
|
|
926
|
-
*
|
|
927
|
-
* @example Fake ffprobe and a failing ffmpeg
|
|
928
|
-
* The plugin's Probe command gets an empty stream list from ffprobe, and
|
|
929
|
-
* any ffmpeg run fails as it would on a broken file. `handle` makes the
|
|
930
|
-
* tool available and answers each `tools.run` with the handler's result;
|
|
931
|
-
* a thrown `HostError` makes the run reject with that code. `calls`
|
|
932
|
-
* records every run with its arguments.
|
|
933
|
-
* ```ts
|
|
934
|
-
* import assert from "node:assert/strict";
|
|
935
|
-
* declare const host: TestHost;
|
|
936
|
-
* // ---cut---
|
|
937
|
-
* host.tools.handle("ffprobe", (args) => {
|
|
938
|
-
* assert.ok(args.includes("-show_streams")); // what the plugin asked for
|
|
939
|
-
* return { stdout: JSON.stringify({ streams: [] }) }; // a file with no streams
|
|
940
|
-
* });
|
|
941
|
-
* // Every ffmpeg run rejects with code `failed`.
|
|
942
|
-
* host.tools.handle("ffmpeg", () => {
|
|
943
|
-
* throw new HostError("failed", "Invalid data found when processing input");
|
|
944
|
-
* });
|
|
945
|
-
* await host.command("probe").run();
|
|
946
|
-
* assert.equal(host.tools.calls[0]?.tool, "ffprobe");
|
|
947
|
-
* ```
|
|
948
|
-
*/
|
|
949
|
-
readonly tools: {
|
|
950
|
-
/** Every run: the tool name and its arguments, newest last. */
|
|
951
|
-
calls: {
|
|
952
|
-
/** `"ffmpeg"` or `"ffprobe"`. */
|
|
953
|
-
tool: string;
|
|
954
|
-
/** The arguments, as passed to `tools.run`. */
|
|
955
|
-
args: string[];
|
|
956
|
-
}[];
|
|
957
|
-
/** @internal The installed handlers by tool; use `handle()`. */
|
|
958
|
-
handlers: Map<string, (args: string[]) => {
|
|
959
|
-
stdout?: Uint8Array | string;
|
|
960
|
-
stderr?: string;
|
|
961
|
-
} | Promise<{
|
|
962
|
-
stdout?: Uint8Array | string;
|
|
963
|
-
stderr?: string;
|
|
964
|
-
}>>;
|
|
965
|
-
/**
|
|
966
|
-
* Make `tool` available and answer each run with `handler(args)`. Return
|
|
967
|
-
* `{ stdout?, stderr? }` (a string `stdout` is UTF-8 encoded), or throw a
|
|
968
|
-
* {@link HostError} to fail the run with that code. A later call
|
|
969
|
-
* replaces the handler.
|
|
970
|
-
*/
|
|
971
|
-
handle(tool: "ffmpeg" | "ffprobe", handler: (args: string[]) => {
|
|
972
|
-
stdout?: Uint8Array | string;
|
|
973
|
-
stderr?: string;
|
|
974
|
-
} | Promise<{
|
|
975
|
-
stdout?: Uint8Array | string;
|
|
976
|
-
stderr?: string;
|
|
977
|
-
}>): void;
|
|
978
|
-
};
|
|
979
|
-
/**
|
|
980
|
-
* Paths given to `system.reveal` / `system.openPath`, newest last; inject
|
|
981
|
-
* failures with `host.handle("system.openPath", …)`.
|
|
982
|
-
*/
|
|
983
|
-
readonly system: {
|
|
984
|
-
/** Paths given to `system.reveal`, newest last. */
|
|
985
|
-
revealed: string[];
|
|
986
|
-
/** Paths given to `system.openPath`, newest last. */
|
|
987
|
-
opened: string[];
|
|
988
|
-
/** `system.openUrl` addresses, newest last. */
|
|
989
|
-
urls: string[];
|
|
990
|
-
};
|
|
991
|
-
/** @internal Local processes, faked: see {@link TestHost.tools}. */
|
|
992
|
-
readonly local: LocalServices;
|
|
993
|
-
/** Every `panel.showMenu`, newest last. */
|
|
994
|
-
readonly menus: {
|
|
995
|
-
/** Command ids, `null` for a separator. */
|
|
996
|
-
items: (string | null)[];
|
|
997
|
-
/** `at.x`, in window coordinates. */
|
|
998
|
-
x: number;
|
|
999
|
-
/** `at.y`, in window coordinates. */
|
|
1000
|
-
y: number;
|
|
1001
|
-
}[];
|
|
1002
|
-
/**
|
|
1003
|
-
* Timeline layers by plugin id (this plugin's is `manifestId`) and the
|
|
1004
|
-
* merged list `plugin.timeline.marks` reads. A new source clears them.
|
|
1005
|
-
*/
|
|
1006
|
-
readonly timeline: {
|
|
1007
|
-
/** Each plugin's layer by plugin id; layers with no marks are absent. */
|
|
1008
|
-
layers: Map<string, {
|
|
1009
|
-
/** Seconds from the start. */
|
|
1010
|
-
time: number;
|
|
1011
|
-
/** `"chapter"`, `"bookmark"` or `"marker"`. */
|
|
1012
|
-
kind: string;
|
|
1013
|
-
/** The mark's title, `null` without one. */
|
|
1014
|
-
title: string | null;
|
|
1015
|
-
}[]>;
|
|
1016
|
-
/** Every layer merged, sorted by time, with the publishing plugin's id. */
|
|
1017
|
-
readonly marks: {
|
|
1018
|
-
/** The manifest id of the plugin that published the mark. */
|
|
1019
|
-
plugin: string;
|
|
1020
|
-
/** Seconds from the start. */
|
|
1021
|
-
time: number;
|
|
1022
|
-
/** `"chapter"`, `"bookmark"` or `"marker"`. */
|
|
1023
|
-
kind: string;
|
|
1024
|
-
/** The mark's title, `null` without one. */
|
|
1025
|
-
title: string | null;
|
|
1026
|
-
}[];
|
|
1027
|
-
/**
|
|
1028
|
-
* Each plugin's ranges by plugin id; layers without ranges are absent
|
|
1029
|
-
* (SDK 1.6.9).
|
|
1030
|
-
*/
|
|
1031
|
-
rangeLayers: Map<string, {
|
|
1032
|
-
/** Seconds from the start. */
|
|
1033
|
-
start: number;
|
|
1034
|
-
/** Seconds from the start, after `start`. */
|
|
1035
|
-
end: number;
|
|
1036
|
-
/** `"selection"` or `"region"`. */
|
|
1037
|
-
style: string;
|
|
1038
|
-
/** The range's title, `null` without one. */
|
|
1039
|
-
title: string | null;
|
|
1040
|
-
}[]>;
|
|
1041
|
-
/** Every layer's ranges merged, sorted by start, with the plugin's id. */
|
|
1042
|
-
readonly ranges: {
|
|
1043
|
-
/** The manifest id of the plugin that published the range. */
|
|
1044
|
-
plugin: string;
|
|
1045
|
-
/** Seconds from the start. */
|
|
1046
|
-
start: number;
|
|
1047
|
-
/** Seconds from the start, after `start`. */
|
|
1048
|
-
end: number;
|
|
1049
|
-
/** `"selection"` or `"region"`. */
|
|
1050
|
-
style: string;
|
|
1051
|
-
/** The range's title, `null` without one. */
|
|
1052
|
-
title: string | null;
|
|
1053
|
-
}[];
|
|
1054
|
-
};
|
|
1055
|
-
/**
|
|
1056
|
-
* Simulate another plugin publishing its timeline ranges (or clearing
|
|
1057
|
-
* them with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
1058
|
-
*
|
|
1059
|
-
* @param pluginId - The other plugin's manifest id.
|
|
1060
|
-
* @param ranges - All its ranges.
|
|
1061
|
-
*
|
|
1062
|
-
* @example Let another plugin publish a clip range
|
|
1063
|
-
* The plugin under test sees a range another plugin put on the seek bar:
|
|
1064
|
-
* `setTimelineRanges` stores that plugin's ranges and pushes
|
|
1065
|
-
* `timeline.changed`, and `settle()` lets it arrive.
|
|
1066
|
-
* ```ts
|
|
1067
|
-
* import assert from "node:assert/strict";
|
|
1068
|
-
* declare const host: TestHost;
|
|
1069
|
-
* // ---cut---
|
|
1070
|
-
* host.setTimelineRanges("community.clips", [
|
|
1071
|
-
* { start: 40, end: 52.5, style: "selection", title: "Clip" },
|
|
1072
|
-
* ]);
|
|
1073
|
-
* await host.settle();
|
|
1074
|
-
* assert.equal(plugin.timeline.ranges[0]?.plugin, "community.clips");
|
|
1075
|
-
* ```
|
|
1076
|
-
*
|
|
1077
|
-
* @capability None.
|
|
1078
|
-
* @since 1.6.9
|
|
1079
|
-
*/
|
|
1080
|
-
setTimelineRanges(pluginId: string, ranges: {
|
|
1081
|
-
start: number;
|
|
1082
|
-
end: number;
|
|
1083
|
-
style: string;
|
|
1084
|
-
title?: string | null;
|
|
1085
|
-
}[]): void;
|
|
1086
|
-
/**
|
|
1087
|
-
* Simulate another plugin publishing its timeline layer (or clearing it
|
|
1088
|
-
* with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
1089
|
-
*
|
|
1090
|
-
* @param pluginId - The other plugin's manifest id.
|
|
1091
|
-
* @param marks - Its whole layer.
|
|
1092
|
-
*
|
|
1093
|
-
* @example Let another plugin publish chapters
|
|
1094
|
-
* The plugin under test sees two chapter marks another plugin put on the
|
|
1095
|
-
* timeline. `setTimelineLayer` stores that plugin's whole layer and pushes
|
|
1096
|
-
* `timeline.changed`; once the plugin uses `plugin.timeline` (with
|
|
1097
|
-
* `events`) the SDK keeps `marks` current from that event, and `settle()`
|
|
1098
|
-
* lets it arrive.
|
|
1099
|
-
* ```ts
|
|
1100
|
-
* import assert from "node:assert/strict";
|
|
1101
|
-
* declare const host: TestHost;
|
|
1102
|
-
* // ---cut---
|
|
1103
|
-
* host.setTimelineLayer("community.chapters", [
|
|
1104
|
-
* { time: 0, kind: "chapter", title: "Intro" },
|
|
1105
|
-
* { time: 95, kind: "chapter", title: "Part 1" },
|
|
1106
|
-
* ]); // replaces that plugin's whole layer; [] would clear it
|
|
1107
|
-
* await host.settle();
|
|
1108
|
-
* assert.equal(plugin.timeline.marks.length, 2);
|
|
1109
|
-
* ```
|
|
1110
|
-
*
|
|
1111
|
-
* @capability None.
|
|
1112
|
-
* @since 1.0.0
|
|
1113
|
-
*/
|
|
1114
|
-
setTimelineLayer(pluginId: string, marks: {
|
|
1115
|
-
time: number;
|
|
1116
|
-
kind: string;
|
|
1117
|
-
title?: string | null;
|
|
1118
|
-
}[]): void;
|
|
1119
|
-
/**
|
|
1120
|
-
* Runtime images written to views (`plugin.ui.image`,
|
|
1121
|
-
* `plugin.ui.remoteImage`), whole values and (SDK 1.5.19) images nested
|
|
1122
|
-
* in rows, newest last. The SDK refuses images of released scopes before
|
|
1123
|
-
* anything is sent. The test host does not read the files, so it does not
|
|
1124
|
-
* check the player's per-view budget (`IMAGE_LIMITS.viewBytes`).
|
|
1125
|
-
*/
|
|
1126
|
-
readonly images: {
|
|
1127
|
-
/** The image file's path, as given to `ui.image`. */
|
|
1128
|
-
path: string;
|
|
1129
|
-
/** The SDK's reference number for that `ImageRef`. */
|
|
1130
|
-
ref: number;
|
|
1131
|
-
/** Its `maxSide` option, when given. */
|
|
1132
|
-
maxSide?: number;
|
|
1133
|
-
}[];
|
|
1134
|
-
/**
|
|
1135
|
-
* The file an image property shows, or `null` when the value is not a
|
|
1136
|
-
* runtime image. View properties hold images in their wire form; this
|
|
1137
|
-
* reads the path back.
|
|
1138
|
-
*
|
|
1139
|
-
* @example Check which file a window shows as its cover
|
|
1140
|
-
* The test checks that the Cover window's image comes from the plugin's
|
|
1141
|
-
* data directory. An image property holds the SDK's wire form of
|
|
1142
|
-
* `plugin.ui.image(path)`, not a path; `imagePath` reads the path back.
|
|
1143
|
-
* ```ts
|
|
1144
|
-
* import assert from "node:assert/strict";
|
|
1145
|
-
* import { join } from "node:path";
|
|
1146
|
-
* declare const host: TestHost;
|
|
1147
|
-
* // ---cut---
|
|
1148
|
-
* assert.equal(
|
|
1149
|
-
* host.imagePath(host.window("cover").props["cover"]),
|
|
1150
|
-
* join(plugin.dataDirectory, "cover.png"),
|
|
1151
|
-
* );
|
|
1152
|
-
* ```
|
|
1153
|
-
*
|
|
1154
|
-
* @capability None.
|
|
1155
|
-
* @since 1.0.0
|
|
1156
|
-
*/
|
|
1157
|
-
imagePath(value: Json | undefined): string | null;
|
|
1158
|
-
/** `drops.provide` registrations of this plugin, in order (`id` is the SDK's). */
|
|
1159
|
-
readonly dropProviders: {
|
|
1160
|
-
/** The SDK's provider id. */
|
|
1161
|
-
id: string;
|
|
1162
|
-
/** The extensions it takes (lower case, `"folder"` for directories). */
|
|
1163
|
-
extensions: string[];
|
|
1164
|
-
}[];
|
|
1165
|
-
/** The panel has an `onDrop` listener. */
|
|
1166
|
-
panelDrops: boolean;
|
|
1167
|
-
/**
|
|
1168
|
-
* Drop files like the player: on a panel with an `onDrop` listener the
|
|
1169
|
-
* whole list goes there; otherwise (and on the video area) each path goes
|
|
1170
|
-
* to the first provider of its extension (`folder` for existing
|
|
1171
|
-
* directories), one call per provider with all of its paths.
|
|
1172
|
-
* Files nobody takes are ignored. Resolves once the host is idle again.
|
|
1173
|
-
*
|
|
1174
|
-
* @param paths - The dropped paths, in the order the file manager gives.
|
|
1175
|
-
* @param options - `target`: `"video"` (default) or `"panel"`; `at`: the
|
|
1176
|
-
* drop point in panel coordinates (default `{ x: 0, y: 0 }`);
|
|
1177
|
-
* `modifiers`: keys held (default all `false`).
|
|
1178
|
-
*
|
|
1179
|
-
* @example Drop files on the video and on the panel
|
|
1180
|
-
* The first drop lands on the video area: each extension belongs to the
|
|
1181
|
-
* provider that registered it first, and each provider gets one call with
|
|
1182
|
-
* all of its paths. The second lands on the panel at a point, with Alt
|
|
1183
|
-
* held, and the panel's `onDrop` gets the whole list. `drop` resolves
|
|
1184
|
-
* once the host is idle.
|
|
1185
|
-
* ```ts
|
|
1186
|
-
* declare const host: TestHost;
|
|
1187
|
-
* // ---cut---
|
|
1188
|
-
* await host.drop(["/media/a.srt", "/media/b.mkv"]); // on the video area
|
|
1189
|
-
* // On the panel, 10 px from its left and 48 px from its top, Alt held.
|
|
1190
|
-
* await host.drop(["/media/list.m3u"], {
|
|
1191
|
-
* target: "panel",
|
|
1192
|
-
* at: { x: 10, y: 48 },
|
|
1193
|
-
* modifiers: { alt: true },
|
|
1194
|
-
* });
|
|
1195
|
-
* ```
|
|
1196
|
-
*
|
|
1197
|
-
* @capability None.
|
|
1198
|
-
* @throws Never: files nobody takes are ignored, as in the player.
|
|
1199
|
-
* @since 1.0.0
|
|
1200
|
-
*/
|
|
1201
|
-
drop(paths: string[], options?: {
|
|
1202
|
-
target?: "video" | "panel";
|
|
1203
|
-
at?: {
|
|
1204
|
-
x: number;
|
|
1205
|
-
y: number;
|
|
1206
|
-
};
|
|
1207
|
-
modifiers?: Partial<{
|
|
1208
|
-
alt: boolean;
|
|
1209
|
-
shift: boolean;
|
|
1210
|
-
cmd: boolean;
|
|
1211
|
-
}>;
|
|
1212
|
-
}): Promise<void>;
|
|
1213
|
-
/** The palette (`plugin.appearance.theme`); `setAppearance` changes it elsewhere. */
|
|
1214
|
-
appearance: ThemeId;
|
|
1215
|
-
/**
|
|
1216
|
-
* Simulate another plugin switching the palette: updates
|
|
1217
|
-
* {@link TestHost.appearance} and pushes `appearance.changed`.
|
|
1218
|
-
*
|
|
1219
|
-
* @capability None.
|
|
1220
|
-
*
|
|
1221
|
-
* @example Simulate another plugin switching to the light palette
|
|
1222
|
-
* The plugin hears the palette change. The host updates `appearance` and
|
|
1223
|
-
* pushes `appearance.changed` to a plugin that listens with `events`.
|
|
1224
|
-
* ```ts
|
|
1225
|
-
* import assert from "node:assert/strict";
|
|
1226
|
-
* declare const host: TestHost;
|
|
1227
|
-
* // ---cut---
|
|
1228
|
-
* const themes: ThemeId[] = [];
|
|
1229
|
-
* plugin.appearance.onChange((theme) => themes.push(theme)); // subscribes (`events`)
|
|
1230
|
-
* host.setAppearance("vestra-light");
|
|
1231
|
-
* await host.settle();
|
|
1232
|
-
* assert.deepEqual(themes, ["vestra-light"]);
|
|
1233
|
-
* ```
|
|
1234
|
-
*
|
|
1235
|
-
* @since 1.0.0
|
|
1236
|
-
*/
|
|
1237
|
-
setAppearance(theme: ThemeId): void;
|
|
1238
|
-
/**
|
|
1239
|
-
* Settings pages by id (`plugin.settings.page`), every `show` (page id or
|
|
1240
|
-
* null), and the page the options window shows.
|
|
1241
|
-
*/
|
|
1242
|
-
readonly settings: {
|
|
1243
|
-
/** Registered pages by page id. */
|
|
1244
|
-
pages: Map<string, {
|
|
1245
|
-
/** The page's settings category. */
|
|
1246
|
-
category: string;
|
|
1247
|
-
/** The page title. */
|
|
1248
|
-
title: string;
|
|
1249
|
-
/** The page order within its category. */
|
|
1250
|
-
order: number;
|
|
1251
|
-
/** The `.slint` file (package-relative). */
|
|
1252
|
-
file: string;
|
|
1253
|
-
/** The exported component the SDK found. */
|
|
1254
|
-
component: string;
|
|
1255
|
-
/** The exported global (the page's bridge) the SDK found. */
|
|
1256
|
-
global: string;
|
|
1257
|
-
/** The page's properties by Slint (kebab-case) name. */
|
|
1258
|
-
props: Record<string, Json>;
|
|
1259
|
-
/** Fire one of the page's callbacks. */
|
|
1260
|
-
emit(callback: string, ...args: Json[]): void;
|
|
1261
|
-
}>;
|
|
1262
|
-
/** Every `settings.show(pageId)`: the page id, `null` when omitted. */
|
|
1263
|
-
shows: (string | null)[];
|
|
1264
|
-
/** The page the options window shows, `null` before any `show`. */
|
|
1265
|
-
current: string | null;
|
|
1266
|
-
/** Whether the options window is open. */
|
|
1267
|
-
open: boolean;
|
|
1268
|
-
};
|
|
1269
|
-
/**
|
|
1270
|
-
* One settings page by id. Throws when it was never registered.
|
|
1271
|
-
*
|
|
1272
|
-
* @example Change a value on the plugin's settings page
|
|
1273
|
-
* The test sets the step on the General page and checks the page shows
|
|
1274
|
-
* it. `emit` fires the page's callback as the user's input would; the
|
|
1275
|
-
* plugin's handler stores the value and its `bind` function sends it back
|
|
1276
|
-
* as a property, which `settle()` waits for.
|
|
1277
|
-
* ```ts
|
|
1278
|
-
* import assert from "node:assert/strict";
|
|
1279
|
-
* declare const host: TestHost;
|
|
1280
|
-
* // ---cut---
|
|
1281
|
-
* const page = host.settingsPage("general");
|
|
1282
|
-
* page.emit("setStep", 10);
|
|
1283
|
-
* await host.settle();
|
|
1284
|
-
* assert.equal(page.props["step"], 10);
|
|
1285
|
-
* ```
|
|
1286
|
-
*
|
|
1287
|
-
* @capability None.
|
|
1288
|
-
* @since 1.0.0
|
|
1289
|
-
*/
|
|
1290
|
-
settingsPage(id: string): {
|
|
1291
|
-
/** The page's settings category. */
|
|
1292
|
-
category: string;
|
|
1293
|
-
/** The page title. */
|
|
1294
|
-
title: string;
|
|
1295
|
-
/** The page order within its category. */
|
|
1296
|
-
order: number;
|
|
1297
|
-
/** The `.slint` file (package-relative). */
|
|
1298
|
-
file: string;
|
|
1299
|
-
/** The exported component the SDK found. */
|
|
1300
|
-
component: string;
|
|
1301
|
-
/** The exported global (the page's bridge) the SDK found. */
|
|
1302
|
-
global: string;
|
|
1303
|
-
/** The page's properties by Slint (kebab-case) name. */
|
|
1304
|
-
props: Record<string, Json>;
|
|
1305
|
-
/** Fire one of the page's callbacks. */
|
|
1306
|
-
emit(callback: string, ...args: Json[]): void;
|
|
1307
|
-
};
|
|
1308
|
-
/** The backdrop: who mounted it and whether it shows (`panels.backdrop.state`). */
|
|
1309
|
-
backdrop: {
|
|
1310
|
-
/** The manifest id of the plugin that mounted it. */
|
|
1311
|
-
owner: string;
|
|
1312
|
-
/** Whether it shows now (no video frame). `true` when mounted. */
|
|
1313
|
-
effective: boolean;
|
|
1314
|
-
} | null;
|
|
1315
|
-
/**
|
|
1316
|
-
* Simulate the backdrop's `effective` changing (`false`: a video frame
|
|
1317
|
-
* appeared; `true`: there is none again); pushed to the plugin.
|
|
1318
|
-
*
|
|
1319
|
-
* @capability None.
|
|
1320
|
-
*
|
|
1321
|
-
* @example Make a video picture replace the backdrop
|
|
1322
|
-
* The plugin's backdrop hears that a video frame now covers it. `false`
|
|
1323
|
-
* means a picture appeared; the change is pushed like the player's
|
|
1324
|
-
* `window.state`.
|
|
1325
|
-
* ```ts
|
|
1326
|
-
* import assert from "node:assert/strict";
|
|
1327
|
-
* declare const host: TestHost;
|
|
1328
|
-
* // ---cut---
|
|
1329
|
-
* host.setBackdropEffective(false);
|
|
1330
|
-
* await host.settle();
|
|
1331
|
-
* assert.equal(plugin.panels.backdrop.state?.effective, false);
|
|
1332
|
-
* ```
|
|
1333
|
-
*
|
|
1334
|
-
* @since 1.0.0
|
|
1335
|
-
*/
|
|
1336
|
-
setBackdropEffective(effective: boolean): void;
|
|
1337
|
-
/**
|
|
1338
|
-
* Simulate another plugin mounting a panel, before or after the plugin
|
|
1339
|
-
* under test, as the player handles it: in the sidebar it is another tab
|
|
1340
|
-
* (after the existing ones; the first panel's tab is current); the bottom
|
|
1341
|
-
* bar and the backdrop take one panel, so the plugin's own mount there
|
|
1342
|
-
* then rejects with `conflict` (not an activation failure, as in the
|
|
1343
|
-
* player). Pushed as a `window.state` change.
|
|
1344
|
-
*
|
|
1345
|
-
* @param slot - The position.
|
|
1346
|
-
* @param plugin - The other plugin's manifest id.
|
|
1347
|
-
* @param options - `title`: its tab label (default: `plugin`).
|
|
1348
|
-
*
|
|
1349
|
-
* @example Put the playlist next to this plugin's sidebar panel
|
|
1350
|
-
* The sidebar ends up with two tabs: the playlist, mounted first, and the
|
|
1351
|
-
* plugin's own panel after it. `mountOther` before `startPlugin` mounts
|
|
1352
|
-
* another plugin's panel as the player would, so the plugin's mount
|
|
1353
|
-
* during activation becomes the second tab (in the bottom bar or the
|
|
1354
|
-
* backdrop it would be refused with `conflict` instead).
|
|
1355
|
-
* ```ts
|
|
1356
|
-
* import assert from "node:assert/strict";
|
|
1357
|
-
* declare function activate(plugin: Plugin): Promise<void>;
|
|
1358
|
-
* declare const directory: URL;
|
|
1359
|
-
* // ---cut---
|
|
1360
|
-
* const host = new TestHost();
|
|
1361
|
-
* host.mountOther("sidebar", "vestra.playlist", { title: "播放列表" });
|
|
1362
|
-
* await startPlugin(activate, { host, directory });
|
|
1363
|
-
* assert.equal(host.layout.sidebar?.panels?.length, 2);
|
|
1364
|
-
* ```
|
|
1365
|
-
*
|
|
1366
|
-
* @since 1.5.2
|
|
1367
|
-
*
|
|
1368
|
-
* @capability None.
|
|
1369
|
-
*/
|
|
1370
|
-
mountOther(slot: "bottom" | "sidebar" | "adjust" | "backdrop", plugin: string, options?: {
|
|
1371
|
-
title?: string;
|
|
1372
|
-
}): void;
|
|
1373
|
-
/**
|
|
1374
|
-
* Simulate another plugin's panel going away ({@link TestHost.mountOther}):
|
|
1375
|
-
* when its tab was current, the next tab (else the previous one) is.
|
|
1376
|
-
*
|
|
1377
|
-
* @since 1.5.2
|
|
1378
|
-
*
|
|
1379
|
-
* @capability None.
|
|
1380
|
-
*
|
|
1381
|
-
* @example Let the playlist's tab go away
|
|
1382
|
-
* The other plugin's sidebar tab disappears and the plugin's own tab
|
|
1383
|
-
* becomes current. When the removed tab was current, the next tab (else
|
|
1384
|
-
* the previous one) becomes current, as in the player.
|
|
1385
|
-
* ```ts
|
|
1386
|
-
* import assert from "node:assert/strict";
|
|
1387
|
-
* declare const host: TestHost;
|
|
1388
|
-
* // ---cut---
|
|
1389
|
-
* host.unmountOther("vestra.playlist");
|
|
1390
|
-
* await host.settle();
|
|
1391
|
-
* assert.equal(host.layout.sidebar?.panels?.length, 1);
|
|
1392
|
-
* ```
|
|
1393
|
-
*/
|
|
1394
|
-
unmountOther(plugin: string): void;
|
|
1395
|
-
/**
|
|
1396
|
-
* Simulate the user clicking a sidebar tab: `plugin`'s panel (the plugin
|
|
1397
|
-
* under test's `manifestId`, or one of {@link TestHost.mountOther})
|
|
1398
|
-
* becomes the current tab. Pushed as a `window.state` change.
|
|
1399
|
-
*
|
|
1400
|
-
* @since 1.5.2
|
|
1401
|
-
*
|
|
1402
|
-
* @capability None.
|
|
1403
|
-
*
|
|
1404
|
-
* @example Click another plugin's sidebar tab
|
|
1405
|
-
* The user switches the sidebar to the playlist; the plugin's panel is in
|
|
1406
|
-
* a background tab now. The change is pushed as `window.state`.
|
|
1407
|
-
* ```ts
|
|
1408
|
-
* import assert from "node:assert/strict";
|
|
1409
|
-
* declare const host: TestHost;
|
|
1410
|
-
* // ---cut---
|
|
1411
|
-
* host.selectTab("vestra.playlist");
|
|
1412
|
-
* await host.settle();
|
|
1413
|
-
* assert.notEqual(host.layout.sidebar?.owner, 1); // the plugin under test is owner 1
|
|
1414
|
-
* ```
|
|
1415
|
-
*/
|
|
1416
|
-
selectTab(plugin: string): void;
|
|
1417
|
-
/**
|
|
1418
|
-
* The display layer (`plugin.video.view()`): who owns it (this plugin is
|
|
1419
|
-
* `manifestId`) and the values the player holds.
|
|
1420
|
-
*/
|
|
1421
|
-
readonly videoView: {
|
|
1422
|
-
/** The owning plugin's manifest id, `null` while nobody owns it. */
|
|
1423
|
-
owner: string | null;
|
|
1424
|
-
/** The values the player holds. */
|
|
1425
|
-
values: ViewSettings;
|
|
1426
|
-
};
|
|
1427
|
-
/**
|
|
1428
|
-
* Simulate another plugin taking the display layer first (the plugin's
|
|
1429
|
-
* `video.view()` then fails with `conflict`), or, with `null`, releasing
|
|
1430
|
-
* it (values reset to the defaults).
|
|
1431
|
-
*
|
|
1432
|
-
* @capability None.
|
|
1433
|
-
*
|
|
1434
|
-
* @example Let another plugin own the display layer
|
|
1435
|
-
* The plugin's `video.view()` fails with `conflict` because another plugin
|
|
1436
|
-
* took the display layer first. `null` releases it again.
|
|
1437
|
-
* ```ts
|
|
1438
|
-
* import assert from "node:assert/strict";
|
|
1439
|
-
* declare const host: TestHost;
|
|
1440
|
-
* // ---cut---
|
|
1441
|
-
* host.setVideoViewOwner("someone.zoom");
|
|
1442
|
-
* const view = plugin.video.view();
|
|
1443
|
-
* view.set({ zoom: 2 });
|
|
1444
|
-
* await host.settle();
|
|
1445
|
-
* assert.equal(view.status.state, "failed");
|
|
1446
|
-
* ```
|
|
1447
|
-
*
|
|
1448
|
-
* @since 1.0.0
|
|
1449
|
-
*/
|
|
1450
|
-
setVideoViewOwner(pluginId: string | null): void;
|
|
1451
|
-
/**
|
|
1452
|
-
* The render options (`plugin.video.render()`): who owns them, the values
|
|
1453
|
-
* the player holds, and whether it renders on the GPU (`gpu: false` is
|
|
1454
|
-
* software rendering, where every `set` fails with `unsupported`).
|
|
1455
|
-
*/
|
|
1456
|
-
readonly videoRender: {
|
|
1457
|
-
/** The owning plugin's manifest id, `null` while nobody owns them. */
|
|
1458
|
-
owner: string | null;
|
|
1459
|
-
/** The values the player holds. */
|
|
1460
|
-
values: RenderSettings;
|
|
1461
|
-
/**
|
|
1462
|
-
* `true`: GPU rendering; `false`: software rendering (`unsupported`).
|
|
1463
|
-
* `plugin.app.system()` reports it as `rendering`.
|
|
1464
|
-
*/
|
|
1465
|
-
gpu: boolean;
|
|
1466
|
-
};
|
|
1467
|
-
/**
|
|
1468
|
-
* Simulate another plugin owning the render options (the plugin's
|
|
1469
|
-
* `video.render()` then fails with `conflict`), or, with `null`, releasing
|
|
1470
|
-
* them (values reset to the defaults).
|
|
1471
|
-
*
|
|
1472
|
-
* @capability None.
|
|
1473
|
-
*
|
|
1474
|
-
* @example Let another plugin own the render options
|
|
1475
|
-
* The plugin's render options report `conflict` while another plugin owns
|
|
1476
|
-
* them; `null` releases them and resets the values to the defaults.
|
|
1477
|
-
* ```ts
|
|
1478
|
-
* import assert from "node:assert/strict";
|
|
1479
|
-
* declare const host: TestHost;
|
|
1480
|
-
* // ---cut---
|
|
1481
|
-
* host.setVideoRenderOwner("someone.hdr");
|
|
1482
|
-
* const render = plugin.video.render();
|
|
1483
|
-
* render.set({ toneMapping: "bt.2390" });
|
|
1484
|
-
* await host.settle();
|
|
1485
|
-
* assert.equal(render.status.state, "failed");
|
|
1486
|
-
* ```
|
|
1487
|
-
*
|
|
1488
|
-
* @since 1.0.0
|
|
1489
|
-
*/
|
|
1490
|
-
setVideoRenderOwner(pluginId: string | null): void;
|
|
1491
|
-
/**
|
|
1492
|
-
* What the player pushes when mpv cannot compile a shader (code `failed`)
|
|
1493
|
-
* or switches to software rendering (`unsupported`, which also sets
|
|
1494
|
-
* `videoRender.gpu` to false and `rendering.reason` to `window-fallback`
|
|
1495
|
-
* with `message`). Pushed only when this plugin is the owner.
|
|
1496
|
-
* `previous-crash` (SDK 1.5.54) is a shortcut that only sets the handle's
|
|
1497
|
-
* status and reaches `onError`: the real player never pushes it, it
|
|
1498
|
-
* refuses the `set` request itself. To test that, set
|
|
1499
|
-
* {@link TestHost.previousCrash}.
|
|
1500
|
-
*
|
|
1501
|
-
* @example Make a shader fail to compile
|
|
1502
|
-
* The plugin's render options report `failed`, as when mpv rejects a
|
|
1503
|
-
* shader. The host pushes the failure only while this plugin owns the
|
|
1504
|
-
* render options, which its first accepted `set` made it; the SDK updates
|
|
1505
|
-
* the handle's `status` when the push arrives, so the test settles first.
|
|
1506
|
-
* ```ts
|
|
1507
|
-
* import assert from "node:assert/strict";
|
|
1508
|
-
* declare const host: TestHost;
|
|
1509
|
-
* // ---cut---
|
|
1510
|
-
* const render = plugin.video.render();
|
|
1511
|
-
* render.set({ toneMapping: "bt.2390" }); // makes the plugin the owner
|
|
1512
|
-
* await host.settle();
|
|
1513
|
-
* host.failVideoRender("failed", "shader compile failed: ERROR: 0:39: ...");
|
|
1514
|
-
* await host.settle();
|
|
1515
|
-
* assert.equal(render.status.state, "failed");
|
|
1516
|
-
* ```
|
|
1517
|
-
*
|
|
1518
|
-
* @capability None.
|
|
1519
|
-
* @since 1.0.0
|
|
1520
|
-
*/
|
|
1521
|
-
failVideoRender(code: "failed" | "unsupported" | "previous-crash", message: string): void;
|
|
1522
|
-
/**
|
|
1523
|
-
* The shader channel (`plugin.video.shaders(id)`, SDK 1.5.39): the lists
|
|
1524
|
-
* the player holds for this plugin's handles by id (the real paths, after
|
|
1525
|
-
* following links), and how many shaders other plugins hold, which count
|
|
1526
|
-
* against {@link VIDEO_PROCESSING_LIMITS}`.shaders` (`limit`). Files must
|
|
1527
|
-
* be under {@link TestHost.fileRoots}; under software rendering
|
|
1528
|
-
* (`videoRender.gpu = false`) a non-empty list fails with `unsupported`.
|
|
1529
|
-
*
|
|
1530
|
-
* @since 1.5.39
|
|
1531
|
-
*/
|
|
1532
|
-
readonly videoShaders: {
|
|
1533
|
-
/** This plugin's lists by handle id; a cleared handle has none. */
|
|
1534
|
-
handles: Map<string, string[]>;
|
|
1535
|
-
/** Shaders other plugins hold (0–16). */
|
|
1536
|
-
others: number;
|
|
1537
|
-
};
|
|
1538
|
-
/**
|
|
1539
|
-
* Where the plugin's own files may be: its package directory and its
|
|
1540
|
-
* data directory ({@link startPlugin} sets both when the list is still
|
|
1541
|
-
* empty). `lut3d` files and shader-channel files outside them fail with
|
|
1542
|
-
* `invalid`, as in the player.
|
|
1543
|
-
*
|
|
1544
|
-
* @since 1.5.39
|
|
1545
|
-
*/
|
|
1546
|
-
fileRoots: string[];
|
|
1547
|
-
/**
|
|
1548
|
-
* What the player pushes to a shader handle when mpv cannot compile a
|
|
1549
|
-
* shader (code `failed`; the player cannot tell whose shader failed, so
|
|
1550
|
-
* every handle with shaders hears it) or the player switches to software
|
|
1551
|
-
* rendering (`unsupported`; use {@link TestHost.failVideoRender}, which
|
|
1552
|
-
* also tells the handles). Pushed only to a handle that holds shaders.
|
|
1553
|
-
*
|
|
1554
|
-
* `code` (SDK 1.5.54, default `failed`) is the error code pushed.
|
|
1555
|
-
* `previous-crash` is a shortcut that only sets the handle's status and
|
|
1556
|
-
* reaches `onError`: the real player (0.6.3) never pushes it, it refuses
|
|
1557
|
-
* the `set` request itself after an abnormal end. To test that, set
|
|
1558
|
-
* {@link TestHost.previousCrash}.
|
|
1559
|
-
*
|
|
1560
|
-
* @capability None.
|
|
1561
|
-
*
|
|
1562
|
-
* @example Make the plugin's shader fail to compile
|
|
1563
|
-
* The plugin's shader handle reports `failed`, as when mpv rejects a
|
|
1564
|
-
* shader. The host pushes the failure only to a handle that holds
|
|
1565
|
-
* shaders, which its accepted `set` made it; the SDK updates the
|
|
1566
|
-
* handle's `status` when the push arrives, so the test settles first.
|
|
1567
|
-
* ```ts
|
|
1568
|
-
* import assert from "node:assert/strict";
|
|
1569
|
-
* import { join } from "node:path";
|
|
1570
|
-
* import { writeFileSync } from "node:fs";
|
|
1571
|
-
* declare const host: TestHost;
|
|
1572
|
-
* // ---cut---
|
|
1573
|
-
* const file = join(plugin.dataDirectory, "sharpen.glsl");
|
|
1574
|
-
* writeFileSync(file, "//!HOOK MAIN\n");
|
|
1575
|
-
* const shaders = plugin.video.shaders("sharpen");
|
|
1576
|
-
* shaders.set([file]); // the handle now holds a shader
|
|
1577
|
-
* await host.settle();
|
|
1578
|
-
* host.failVideoShaders("sharpen", "shader compile failed: ERROR: 0:3: ...");
|
|
1579
|
-
* await host.settle();
|
|
1580
|
-
* assert.equal(shaders.status.state, "failed");
|
|
1581
|
-
* ```
|
|
1582
|
-
*
|
|
1583
|
-
* @since 1.5.39
|
|
1584
|
-
*/
|
|
1585
|
-
failVideoShaders(id: string, message: string, code?: "failed" | "previous-crash"): void;
|
|
1586
|
-
/**
|
|
1587
|
-
* Play a run after an abnormal end with plugin shaders on (player 0.6.3):
|
|
1588
|
-
* while set, a non-empty `video.shaders` list and a render options
|
|
1589
|
-
* `set` with a non-empty `shaders` list are refused with
|
|
1590
|
-
* `previous-crash` and this message; clearing still works. `null`
|
|
1591
|
-
* (default): shaders apply.
|
|
1592
|
-
*
|
|
1593
|
-
* @example Check that the plugin stops retrying while shaders are off
|
|
1594
|
-
* The plugin's shader handle turns `failed` with `previous-crash`, as
|
|
1595
|
-
* in the player after a crash while plugin shaders were drawing. The
|
|
1596
|
-
* player refuses the list itself (and shows its own OSD line), so the
|
|
1597
|
-
* plugin should report it once and not set the list again.
|
|
1598
|
-
* ```ts
|
|
1599
|
-
* import assert from "node:assert/strict";
|
|
1600
|
-
* import { join } from "node:path";
|
|
1601
|
-
* import { writeFileSync } from "node:fs";
|
|
1602
|
-
* declare const host: TestHost;
|
|
1603
|
-
* // ---cut---
|
|
1604
|
-
* host.previousCrash = "plugin shaders are off for this start";
|
|
1605
|
-
* const file = join(plugin.dataDirectory, "sharpen.glsl");
|
|
1606
|
-
* writeFileSync(file, "//!HOOK MAIN\n");
|
|
1607
|
-
* const shaders = plugin.video.shaders("sharpen");
|
|
1608
|
-
* shaders.set([file]); // refused: the previous run ended abnormally
|
|
1609
|
-
* await host.settle();
|
|
1610
|
-
* assert.equal(shaders.status.state, "failed");
|
|
1611
|
-
* ```
|
|
1612
|
-
*
|
|
1613
|
-
* @since 1.5.54
|
|
1614
|
-
*/
|
|
1615
|
-
previousCrash: string | null;
|
|
1616
|
-
/**
|
|
1617
|
-
* The player's processing (filter) state. Wire-level; may change in minor
|
|
1618
|
-
* versions. `resources` holds each `video.filter` / `audio.filter` chain the
|
|
1619
|
-
* plugin applied, keyed by the SDK's resource id.
|
|
1620
|
-
*
|
|
1621
|
-
* @example Check the filter chain the plugin applied
|
|
1622
|
-
* After the plugin set its picture chain, the test reads the one chain
|
|
1623
|
-
* the host holds and compares its nodes. `resources` keeps each applied
|
|
1624
|
-
* `video.filter` / `audio.filter` chain by the SDK's resource id, after
|
|
1625
|
-
* the host validated it with the player's value ranges.
|
|
1626
|
-
* ```ts
|
|
1627
|
-
* import assert from "node:assert/strict";
|
|
1628
|
-
* declare const host: TestHost;
|
|
1629
|
-
* // ---cut---
|
|
1630
|
-
* const [chain] = [...host.processing.resources.values()];
|
|
1631
|
-
* assert.deepEqual(chain?.nodes, [{ kind: "hue", degrees: 30 }]);
|
|
1632
|
-
* ```
|
|
1633
|
-
*/
|
|
1634
|
-
readonly processing: {
|
|
1635
|
-
/** Increases with every accepted change. */
|
|
1636
|
-
configurationRevision: number;
|
|
1637
|
-
/** Set a stage here to simulate a blocked backend (`backend-uncertain`). */
|
|
1638
|
-
backendUncertain: Partial<Record<"video" | "audio", string>>;
|
|
1639
|
-
/** Applied chains by resource id; an empty chain removes its entry, as in the player. */
|
|
1640
|
-
resources: Map<string, {
|
|
1641
|
-
/** `"video"` or `"audio"`. */
|
|
1642
|
-
stage: string;
|
|
1643
|
-
/** The chain's revision. */
|
|
1644
|
-
revision: number;
|
|
1645
|
-
/** The applied nodes. */
|
|
1646
|
-
nodes: Json;
|
|
1647
|
-
/** The audio delay in milliseconds (audio chains only). */
|
|
1648
|
-
offsetMs?: number;
|
|
1649
|
-
}>;
|
|
1650
|
-
/** Outcomes of processing operations, for the SDK's recovery queries. */
|
|
1651
|
-
book: Map<string, {
|
|
1652
|
-
/** The operation's outcome. */
|
|
1653
|
-
status: "completed" | "failed";
|
|
1654
|
-
/** The result of a completed operation. */
|
|
1655
|
-
result?: Json;
|
|
1656
|
-
/** The refusal of a failed operation. */
|
|
1657
|
-
error?: {
|
|
1658
|
-
/** The error code. */
|
|
1659
|
-
code: string;
|
|
1660
|
-
/** The error message. */
|
|
1661
|
-
message: string;
|
|
1662
|
-
};
|
|
1663
|
-
}>;
|
|
1664
|
-
};
|
|
1665
|
-
/**
|
|
1666
|
-
* The player's subtitle state: tracks, selection, style layers, fonts and
|
|
1667
|
-
* loaded resources. Wire-level shape (may change in minor versions); add
|
|
1668
|
-
* tracks to `subtitles.tracks` (entries may carry `codec`, `text`,
|
|
1669
|
-
* `streamIndex`) to simulate a source's subtitles.
|
|
1670
|
-
*/
|
|
1671
|
-
subtitles: SubtitleState;
|
|
1672
|
-
/** Subtitle uploads in progress, by upload id. Wire-level. */
|
|
1673
|
-
readonly uploads: Map<number, {
|
|
1674
|
-
/** The chunks received so far. */
|
|
1675
|
-
bytes: Buffer[];
|
|
1676
|
-
/** The announced total size in bytes. */
|
|
1677
|
-
expected: number;
|
|
1678
|
-
/**
|
|
1679
|
-
* The source generation it was begun for; like the player, a new
|
|
1680
|
-
* source drops the uploads of the old one.
|
|
1681
|
-
*
|
|
1682
|
-
* @since 1.5.34
|
|
1683
|
-
*/
|
|
1684
|
-
generation?: number;
|
|
1685
|
-
}>;
|
|
1686
|
-
/** Snapshots taken with `media.capture`, by snapshot id. Wire-level. */
|
|
1687
|
-
readonly snapshots: Map<number, Json>;
|
|
1688
|
-
/** Every `jobs.start` request (`kind`, `destination`, `quality`, …), newest last. */
|
|
1689
|
-
readonly exports: Record<string, Json>[];
|
|
1690
|
-
/**
|
|
1691
|
-
* Export jobs by job id. Each state query advances a job one step
|
|
1692
|
-
* (`queued` → `running` → `completed`; `cancelling` → `cancelled`).
|
|
1693
|
-
* A running clip reports half its `progress`; a finished clip's
|
|
1694
|
-
* `outcome` has `start` and `end` (a stream copy starts 1 s before the
|
|
1695
|
-
* snapshot time, as if snapped to a keyframe, and its destination gets
|
|
1696
|
-
* the extension of its container: `.mkv` for `container: "mkv"`, else
|
|
1697
|
-
* `.mp4`). Wire-level.
|
|
1698
|
-
*/
|
|
1699
|
-
readonly jobs: Map<number, MediaJob>;
|
|
1700
|
-
/**
|
|
1701
|
-
* Set it to make every running export end `failed` with this error on
|
|
1702
|
-
* its next state query, as the player reports one (`unsupported` for a
|
|
1703
|
-
* stream copy it cannot make, `export-failed` otherwise); `null` (the
|
|
1704
|
-
* default) completes them.
|
|
1705
|
-
*
|
|
1706
|
-
* @since 1.6.10
|
|
1707
|
-
*/
|
|
1708
|
-
exportFailure: {
|
|
1709
|
-
/** The `outcome.error.code`. */
|
|
1710
|
-
code: string;
|
|
1711
|
-
/** The `outcome.error.message`. */
|
|
1712
|
-
message: string;
|
|
1713
|
-
} | null;
|
|
1714
|
-
/**
|
|
1715
|
-
* `openStream`'s transport: a local stream (a Unix socket on macOS and
|
|
1716
|
-
* Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the
|
|
1717
|
-
* plugin must send the token first, as the player on Windows answers.
|
|
1718
|
-
* Set it before the plugin calls `openStream`.
|
|
1719
|
-
*/
|
|
1720
|
-
producerTransport: "unix" | "tcp";
|
|
1721
|
-
/**
|
|
1722
|
-
* Generated streams (`player.openStream`) by producer id: the fake player
|
|
1723
|
-
* end of each frame connection. Wire-level beyond the
|
|
1724
|
-
* {@link FakeProducer} fields.
|
|
1725
|
-
*/
|
|
1726
|
-
readonly producers: Map<number, FakeProducer & {
|
|
1727
|
-
/** The listening server for the frame connection. */
|
|
1728
|
-
server: Server;
|
|
1729
|
-
/** The Unix socket path (unused with TCP). */
|
|
1730
|
-
path: string;
|
|
1731
|
-
/** TCP: the connection sent the right token (always true for Unix). */
|
|
1732
|
-
admitted: boolean;
|
|
1733
|
-
/** The current start/stop control epoch. */
|
|
1734
|
-
epoch: number;
|
|
1735
|
-
/** A control waiting for the plugin's acknowledgement, or `null`. */
|
|
1736
|
-
pending: Json | null;
|
|
1737
|
-
/** Whether the last control was acknowledged. */
|
|
1738
|
-
acknowledged: boolean;
|
|
1739
|
-
/** Whether the stream is producing (selected and started). */
|
|
1740
|
-
active: boolean;
|
|
1741
|
-
}>;
|
|
1742
|
-
/** Registered source resolvers (`resolvers.add`) by id: their declarations. */
|
|
1743
|
-
readonly resolvers: Map<string, Json>;
|
|
1744
|
-
/**
|
|
1745
|
-
* Resolvers other plugins hold, as `resolvers.add` declares them plus the
|
|
1746
|
-
* holder's id: a registration of this plugin that claims one of their
|
|
1747
|
-
* extensions (or scheme + host pairs) is refused with `conflict`, which
|
|
1748
|
-
* does not fail activation, like the player.
|
|
1749
|
-
*
|
|
1750
|
-
* @example Start with another plugin holding a file type
|
|
1751
|
-
* The plugin starts in a player where another plugin already resolves
|
|
1752
|
-
* `.ch8` files. Pushed before `startPlugin`, the entry makes the plugin's
|
|
1753
|
-
* own resolver for `.ch8` fail with `conflict` during activation, which
|
|
1754
|
-
* (as in the player) does not fail the activation.
|
|
1755
|
-
* ```ts
|
|
1756
|
-
* const host = new TestHost();
|
|
1757
|
-
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
|
|
1758
|
-
* ```
|
|
1759
|
-
*/
|
|
1760
|
-
readonly otherResolvers: {
|
|
1761
|
-
/** The manifest id of the plugin holding them (shown in the refusal). */
|
|
1762
|
-
owner: string;
|
|
1763
|
-
/** File extensions it resolves (a file resolver), lower case, no dot. */
|
|
1764
|
-
extensions?: string[];
|
|
1765
|
-
/** URL schemes it resolves (a web resolver), with `hosts`. */
|
|
1766
|
-
schemes?: string[];
|
|
1767
|
-
/** Exact host names it resolves (a web resolver), with `schemes`. */
|
|
1768
|
-
hosts?: string[];
|
|
1769
|
-
}[];
|
|
1770
|
-
/**
|
|
1771
|
-
* What `plugin.resolvers.fileTypes` reports: the extensions of this
|
|
1772
|
-
* plugin's file resolvers and of {@link TestHost.otherResolvers}, sorted,
|
|
1773
|
-
* each once.
|
|
1774
|
-
*
|
|
1775
|
-
* @capability None.
|
|
1776
|
-
*
|
|
1777
|
-
* @example Check the file types a resolver adds
|
|
1778
|
-
* The test checks the list the plugin will read as
|
|
1779
|
-
* `resolvers.fileTypes`: the extensions of its own file resolvers and of
|
|
1780
|
-
* `otherResolvers`, sorted, each once.
|
|
1781
|
-
* ```ts
|
|
1782
|
-
* import assert from "node:assert/strict";
|
|
1783
|
-
* declare const host: TestHost;
|
|
1784
|
-
* // ---cut---
|
|
1785
|
-
* assert.deepEqual(host.fileTypes(), ["ch8"]);
|
|
1786
|
-
* ```
|
|
1787
|
-
*
|
|
1788
|
-
* @since 1.5.8
|
|
1789
|
-
*/
|
|
1790
|
-
fileTypes(): string[];
|
|
1791
|
-
/**
|
|
1792
|
-
* Push `resolvers.fileTypes` when {@link TestHost.fileTypes} changed:
|
|
1793
|
-
* called after this plugin's resolvers come and go; call it yourself
|
|
1794
|
-
* after changing {@link TestHost.otherResolvers} (another plugin started
|
|
1795
|
-
* or stopped).
|
|
1796
|
-
*
|
|
1797
|
-
* @example Let another plugin's file type appear while running
|
|
1798
|
-
* Another plugin starts and adds `.ch8`; the plugin under test then sees
|
|
1799
|
-
* it in `resolvers.fileTypes`. Changing `otherResolvers` alone pushes
|
|
1800
|
-
* nothing: `updateFileTypes` sends the new list as the player does when
|
|
1801
|
-
* a plugin starts or stops, and `settle()` lets the SDK apply it.
|
|
1802
|
-
* ```ts
|
|
1803
|
-
* import assert from "node:assert/strict";
|
|
1804
|
-
* declare const host: TestHost;
|
|
1805
|
-
* // ---cut---
|
|
1806
|
-
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
|
|
1807
|
-
* host.updateFileTypes();
|
|
1808
|
-
* await host.settle();
|
|
1809
|
-
* assert.deepEqual(plugin.resolvers.fileTypes, ["ch8"]);
|
|
1810
|
-
* ```
|
|
1811
|
-
*
|
|
1812
|
-
* @capability None.
|
|
1813
|
-
* @since 1.5.8
|
|
1814
|
-
*/
|
|
1815
|
-
updateFileTypes(): void;
|
|
1816
|
-
/**
|
|
1817
|
-
* File opens waiting for this plugin's file resolver (`extensions`), by
|
|
1818
|
-
* request id: the path and how it was opened. Opening a matching path
|
|
1819
|
-
* (`player.open`, {@link TestHost.openElsewhere}) sends
|
|
1820
|
-
* `source.resolveRequest`; the answer selects the stream, fails the open
|
|
1821
|
-
* or, once the source changed, is `stale`. Any later source change
|
|
1822
|
-
* cancels it (`source.resolveCancel`). Web resolvers are not simulated:
|
|
1823
|
-
* emit `source.resolveRequest` yourself.
|
|
1824
|
-
*/
|
|
1825
|
-
readonly resolutions: Map<number, {
|
|
1826
|
-
/** The id of this plugin's resolver asked (`resolvers.add` `id`). */
|
|
1827
|
-
resolverId: string;
|
|
1828
|
-
/** The path being opened, as sent to `resolve`. */
|
|
1829
|
-
source: string;
|
|
1830
|
-
/** The open asked for a paused start (`player.open(path, { paused })`). */
|
|
1831
|
-
paused: boolean;
|
|
1832
|
-
}>;
|
|
1833
|
-
/**
|
|
1834
|
-
* Answers for `dialogs.open` (paths, `null` = cancelled) and
|
|
1835
|
-
* `dialogs.confirm` (`{ confirmed, checked }`), consumed in order. A
|
|
1836
|
-
* dialog with no queued answer, or with the wrong kind, fails the request
|
|
1837
|
-
* (as a lost reply).
|
|
1838
|
-
*
|
|
1839
|
-
* For `openFile` / `openDirectory` / `saveFile` queue a one-path array;
|
|
1840
|
-
* for `openFiles` any number of paths.
|
|
1841
|
-
*
|
|
1842
|
-
* @example Answer the plugin's dialogs in advance
|
|
1843
|
-
* The plugin's Open command gets a picked file, then a cancelled file
|
|
1844
|
-
* dialog, then a confirmed question. All answers share one queue, taken
|
|
1845
|
-
* from the front in the order the plugin opens dialogs of any kind: if
|
|
1846
|
-
* the next queued answer is missing or of the wrong kind, the dialog
|
|
1847
|
-
* fails like a lost reply, so queue them in the order the plugin asks.
|
|
1848
|
-
* ```ts
|
|
1849
|
-
* declare const host: TestHost;
|
|
1850
|
-
* // ---cut---
|
|
1851
|
-
* host.dialogAnswers.push(["/media/movie.mkv"]); // openFile picks it
|
|
1852
|
-
* host.dialogAnswers.push(null); // the next file dialog is cancelled
|
|
1853
|
-
* host.dialogAnswers.push({ confirmed: true, checked: false }); // confirm
|
|
1854
|
-
* await host.command("open").run();
|
|
1855
|
-
* ```
|
|
1856
|
-
*/
|
|
1857
|
-
readonly dialogAnswers: (string[] | null | {
|
|
1858
|
-
/** The user clicked the confirm button. */
|
|
1859
|
-
confirmed: boolean;
|
|
1860
|
-
/** The checkbox state. */
|
|
1861
|
-
checked: boolean;
|
|
1862
|
-
})[];
|
|
1863
|
-
/** Every `dialogs.confirm` request (its options), newest last. */
|
|
1864
|
-
readonly confirms: Record<string, Json>[];
|
|
1865
|
-
/**
|
|
1866
|
-
* The main window as the player would show it. Change it with
|
|
1867
|
-
* `setMainWindow` (the user resized or maximized it, entered fullscreen,
|
|
1868
|
-
* closed the picture-in-picture window); `onTop`, `cursorAutoHide` and
|
|
1869
|
-
* `pictureInPicture.active` record the plugin's `window.set…` calls.
|
|
1870
|
-
* `pictureInPicture.supported: false` stands for Windows and Linux.
|
|
1871
|
-
*/
|
|
1872
|
-
readonly mainWindow: {
|
|
1873
|
-
/** Whether the main window is fullscreen. */
|
|
1874
|
-
fullscreen: boolean;
|
|
1875
|
-
/**
|
|
1876
|
-
* Whether the main window is maximized (default `false`). Like the
|
|
1877
|
-
* player, `window.state` reports `false` while fullscreen and this
|
|
1878
|
-
* value again after leaving it.
|
|
1879
|
-
*
|
|
1880
|
-
* @since 1.5.52
|
|
1881
|
-
*/
|
|
1882
|
-
maximized: boolean;
|
|
1883
|
-
/** The video area in logical points (default 780×609). */
|
|
1884
|
-
videoArea: {
|
|
1885
|
-
/** Width in logical points. */
|
|
1886
|
-
width: number;
|
|
1887
|
-
/** Height in logical points. */
|
|
1888
|
-
height: number;
|
|
1889
|
-
};
|
|
1890
|
-
/** The effective `window.setAlwaysOnTop` value. */
|
|
1891
|
-
onTop: boolean;
|
|
1892
|
-
/** The effective `window.setCursorAutoHide` mode. */
|
|
1893
|
-
cursorAutoHide: "fullscreen" | "playing";
|
|
1894
|
-
/** Picture-in-picture: `supported` (preset) and `active`. */
|
|
1895
|
-
pictureInPicture: {
|
|
1896
|
-
/** Set to `false` to simulate Windows or Linux. */
|
|
1897
|
-
supported: boolean;
|
|
1898
|
-
/** Whether the picture-in-picture window shows the video. */
|
|
1899
|
-
active: boolean;
|
|
1900
|
-
};
|
|
1901
|
-
};
|
|
1902
|
-
/**
|
|
1903
|
-
* The player's secret store for this plugin: name → value. Preset entries
|
|
1904
|
-
* to simulate stored secrets; inject failures with
|
|
1905
|
-
* `host.handle("secrets.get", ...)`. Like the player, `secrets.set` of a
|
|
1906
|
-
* new name fails `limit` once 256 names are stored.
|
|
1907
|
-
*/
|
|
1908
|
-
readonly secrets: Map<string, string>;
|
|
1909
|
-
/**
|
|
1910
|
-
* `plugin.storage.session`: key → value, as the player keeps them. Preset
|
|
1911
|
-
* entries to simulate values an earlier run of the plugin stored in the
|
|
1912
|
-
* same player session, or pass `startPlugin(..., { sessionStorage })`.
|
|
1913
|
-
* Like the player, a key is 1–1024 bytes, a `set` that would make the
|
|
1914
|
-
* entries larger than 1 MiB (keys, values as compact JSON and
|
|
1915
|
-
* `SESSION_STORAGE_LIMITS.entryBytes` per key) fails `limit`, and an
|
|
1916
|
-
* object's properties are stored sorted, as the player hands them back.
|
|
1917
|
-
*
|
|
1918
|
-
* @since 1.5.25
|
|
1919
|
-
*/
|
|
1920
|
-
readonly sessionStorage: Map<string, Json>;
|
|
1921
|
-
/**
|
|
1922
|
-
* What `plugin.app.system()` reports. Its `rendering` follows
|
|
1923
|
-
* {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
|
|
1924
|
-
* `"software"`), so one switch covers the render options and the fact;
|
|
1925
|
-
* its `renderingReason` is {@link TestHost.rendering | rendering.reason}.
|
|
1926
|
-
*/
|
|
1927
|
-
systemInfo: Omit<WireSystemInfo, "rendering" | "renderingReason">;
|
|
1928
|
-
/**
|
|
1929
|
-
* What `plugin.app` reports. Set it before {@link startPlugin}: the plugin
|
|
1930
|
-
* receives it once at start-up.
|
|
1931
|
-
*/
|
|
1932
|
-
app: {
|
|
1933
|
-
/** The player version. */
|
|
1934
|
-
version: string;
|
|
1935
|
-
/** The short git hash of the build, `null` when unknown. */
|
|
1936
|
-
build: string | null;
|
|
1937
|
-
/** Component versions. */
|
|
1938
|
-
components: {
|
|
1939
|
-
/** libmpv's `mpv-version`. */
|
|
1940
|
-
mpv: string | null;
|
|
1941
|
-
/** `{ ffmpeg: <version> }`. */
|
|
1942
|
-
libav: Record<string, string>;
|
|
1943
|
-
/** The Slint version. */
|
|
1944
|
-
slint: string;
|
|
1945
|
-
};
|
|
1946
|
-
};
|
|
1947
|
-
/**
|
|
1948
|
-
* The player's rendering choice behind `plugin.rendering` (player
|
|
1949
|
-
* 0.5.13). The mode drawing now is {@link TestHost.videoRender |
|
|
1950
|
-
* videoRender.gpu}, as for `plugin.app.system()`; the rest is preset here
|
|
1951
|
-
* and changed by the plugin's calls, as the player would:
|
|
1952
|
-
*
|
|
1953
|
-
* - `setPreference` saves `preference` and never changes the mode. It
|
|
1954
|
-
* refuses `"gpu"` while `gpu.available` is false: with `unsupported`
|
|
1955
|
-
* when `gpu.reason.code` is `no-gpu-backend` or `headless`, otherwise
|
|
1956
|
-
* with `unavailable`, the reason's message in both cases.
|
|
1957
|
-
* - The next start uses `"software"` for the `"software"` choice, else
|
|
1958
|
-
* software with `gpu.reason` while the GPU is not available, else the
|
|
1959
|
-
* GPU (code `preference` for a `"gpu"` choice, `default` for
|
|
1960
|
-
* `"auto"`). `restartPending` compares that with `startedWith`.
|
|
1961
|
-
* - `restartPlayer` counts in `restarts` and leaves the plugin running
|
|
1962
|
-
* (call `stop()` yourself); a second one rejects with `busy`, as a
|
|
1963
|
-
* restart is then under way.
|
|
1964
|
-
*
|
|
1965
|
-
* Inject other refusals (`failed` for an unwritable settings file or a
|
|
1966
|
-
* new process that could not start) with `host.handle("rendering.setPreference", …)`
|
|
1967
|
-
* or `host.handle("rendering.restart", …)`. The defaults are a macOS
|
|
1968
|
-
* player on the GPU: `"auto"`, available, started on the GPU.
|
|
1969
|
-
*
|
|
1970
|
-
* @example Test a GPU switch on a system without a GPU renderer
|
|
1971
|
-
* The plugin's GPU menu item is greyed out with the player's reason when
|
|
1972
|
-
* the GPU cannot be chosen. The host is preset before the plugin starts,
|
|
1973
|
-
* because the plugin reads the state while activating; software
|
|
1974
|
-
* rendering is `videoRender.gpu = false`, and `gpu` says why the GPU
|
|
1975
|
-
* cannot be chosen.
|
|
1976
|
-
* ```ts
|
|
1977
|
-
* import assert from "node:assert/strict";
|
|
1978
|
-
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
1979
|
-
* import activate from "../main.ts";
|
|
1980
|
-
*
|
|
1981
|
-
* const host = new TestHost();
|
|
1982
|
-
* host.videoRender.gpu = false; // software draws now (Linux)
|
|
1983
|
-
* host.rendering.startedWith = "software";
|
|
1984
|
-
* host.rendering.reason = { code: "no-gpu-backend", message: "this player has no GPU renderer on Linux yet" };
|
|
1985
|
-
* host.rendering.gpu = { available: false, reason: host.rendering.reason };
|
|
1986
|
-
* const { stop } = await startPlugin(activate, {
|
|
1987
|
-
* directory: new URL("..", import.meta.url),
|
|
1988
|
-
* host,
|
|
1989
|
-
* });
|
|
1990
|
-
* const item = host.command("gpu-rendering");
|
|
1991
|
-
* assert.equal(item.state.enabled, false);
|
|
1992
|
-
* assert.equal(item.state.reason, "this player has no GPU renderer on Linux yet");
|
|
1993
|
-
* await stop();
|
|
1994
|
-
* ```
|
|
1995
|
-
*
|
|
1996
|
-
* @since 1.5.35
|
|
1997
|
-
*/
|
|
1998
|
-
readonly rendering: {
|
|
1999
|
-
/** Why the mode drawing now is what it is (`RenderingState.reason`, `SystemInfo.renderingReason`). */
|
|
2000
|
-
reason: RenderingReason;
|
|
2001
|
-
/** The saved choice: `"auto"`, `"gpu"` or `"software"`. */
|
|
2002
|
-
preference: RenderingPreference;
|
|
2003
|
-
/** The mode this run started with; `restartPending` compares the next start's mode with it. */
|
|
2004
|
-
startedWith: RenderingMode;
|
|
2005
|
-
/** Whether the plugin may choose `"gpu"`, and why not (`null` while available). */
|
|
2006
|
-
gpu: {
|
|
2007
|
-
/** `false`: `setPreference("gpu")` is refused. */
|
|
2008
|
-
available: boolean;
|
|
2009
|
-
/** Why the GPU cannot be chosen; `null` while it can. */
|
|
2010
|
-
reason: RenderingReason | null;
|
|
2011
|
-
};
|
|
2012
|
-
/** Accepted `restartPlayer` calls. */
|
|
2013
|
-
restarts: number;
|
|
2014
|
-
};
|
|
2015
|
-
/**
|
|
2016
|
-
* The GPU's video upscaler behind `plugin.video.upscaling` (player
|
|
2017
|
-
* 0.5.16), answered as the player would:
|
|
2018
|
-
*
|
|
2019
|
-
* - `state()` and `set(true)` set `checked` (the player's one GPU check; a
|
|
2020
|
-
* plugin that should not wake the GPU while it starts can assert it is
|
|
2021
|
-
* still `false`) and report `available` from `reason` (`null`: it
|
|
2022
|
-
* works), this plugin's `enabled`, and `requested` with `factor` while
|
|
2023
|
-
* enabled, available and `factor` is above 1.
|
|
2024
|
-
* - `set(true)` refuses with `unsupported` and the reason's message while
|
|
2025
|
-
* `reason` is set, unless its code is `decoding`, or `rendering` with
|
|
2026
|
-
* `renderingStarting` (GPU rendering still starting): those can pass
|
|
2027
|
-
* during the run, so the player keeps the request. Nothing changes when
|
|
2028
|
-
* it refuses.
|
|
2029
|
-
* - `set(false)` always succeeds and does not check the GPU: before any
|
|
2030
|
-
* check it reports `available: false` with the reason `unchecked`.
|
|
2031
|
-
*
|
|
2032
|
-
* Inject other answers with `host.handle("video.upscaling.set", …)`. The
|
|
2033
|
-
* defaults are a player where upscaling works and a 1080p video shown
|
|
2034
|
-
* full screen on a 4K display (`factor` 2), whatever `systemInfo` says.
|
|
2035
|
-
*
|
|
2036
|
-
* @example Test the menu item on a computer without an RTX GPU
|
|
2037
|
-
* The plugin's upscaling item is greyed out with the player's reason after
|
|
2038
|
-
* the user first picks it. Starting the plugin checks nothing (`checked`
|
|
2039
|
-
* stays false, so no GPU wakes up); the first pick reads the state, and
|
|
2040
|
-
* `set(true)` would reject with `unsupported`, as in the player.
|
|
2041
|
-
* ```ts
|
|
2042
|
-
* import assert from "node:assert/strict";
|
|
2043
|
-
* import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
2044
|
-
* import activate from "../main.ts";
|
|
2045
|
-
*
|
|
2046
|
-
* const host = new TestHost();
|
|
2047
|
-
* host.upscaling.reason = { code: "no-gpu", message: "no NVIDIA GPU" };
|
|
2048
|
-
* const { stop } = await startPlugin(activate, {
|
|
2049
|
-
* directory: new URL("..", import.meta.url),
|
|
2050
|
-
* host,
|
|
2051
|
-
* });
|
|
2052
|
-
* assert.equal(host.upscaling.checked, false); // starting woke no GPU
|
|
2053
|
-
* const item = host.command("upscaling");
|
|
2054
|
-
* await item.run(); // the first pick checks the GPU
|
|
2055
|
-
* assert.equal(item.state.enabled, false); // greyed out
|
|
2056
|
-
* assert.equal(item.state.reason, "no NVIDIA GPU"); // the player's message
|
|
2057
|
-
* await stop();
|
|
2058
|
-
* ```
|
|
2059
|
-
*
|
|
2060
|
-
* @since 1.5.37
|
|
2061
|
-
*/
|
|
2062
|
-
readonly upscaling: {
|
|
2063
|
-
/** Why upscaling does not work; `null` while it does. */
|
|
2064
|
-
reason: UpscalingReason | null;
|
|
2065
|
-
/** Whether this plugin has upscaling on (`set`); a test may preset it. */
|
|
2066
|
-
enabled: boolean;
|
|
2067
|
-
/** The factor the player would request; `null` or at most 1: the video needs no upscaling. */
|
|
2068
|
-
factor: number | null;
|
|
2069
|
-
/** Whether the plugin made the player check the GPU (`state`, `set(true)`). */
|
|
2070
|
-
checked: boolean;
|
|
2071
|
-
/** GPU rendering is still starting: a `rendering` reason then lets `set(true)` keep the request. */
|
|
2072
|
-
renderingStarting: boolean;
|
|
2073
|
-
/** The video's size the state reports (player 0.5.25); `null`: no video. */
|
|
2074
|
-
video: UpscalingVideoSize | null;
|
|
2075
|
-
/** The video area the state reports (player 0.5.25); `null`: none. */
|
|
2076
|
-
area: {
|
|
2077
|
-
/** Width in physical pixels. */
|
|
2078
|
-
width: number;
|
|
2079
|
-
/** Height in physical pixels. */
|
|
2080
|
-
height: number;
|
|
2081
|
-
} | null;
|
|
2082
|
-
/** The rendering GPU's name the state reports (player 0.5.25). */
|
|
2083
|
-
gpu: string | null;
|
|
2084
|
-
/** mpv's decoder the state reports (player 0.5.25); `null`: no video. */
|
|
2085
|
-
hwdecCurrent: string | null;
|
|
2086
|
-
};
|
|
2087
|
-
/**
|
|
2088
|
-
* Make the upscaler's reason `decoding` with `cause`, with the player's
|
|
2089
|
-
* message (player 0.5.25), and set `upscaling.hwdecCurrent` to match
|
|
2090
|
-
* (`d3d11va-copy`, `no`, `nvdec`). `plugins` are named for `copy-back`
|
|
2091
|
-
* only, as the player does. Returns the reason; call
|
|
2092
|
-
* {@link TestHost.upscalingChanged} to tell a listening plugin.
|
|
2093
|
-
*
|
|
2094
|
-
* @example Test the window's advice for each cause
|
|
2095
|
-
* The plugin's window explains why upscaling waits. The test fakes the
|
|
2096
|
-
* copy-back cause that Picture Adjustments' filters cause and checks
|
|
2097
|
-
* that the window names the plugin.
|
|
2098
|
-
* ```ts
|
|
2099
|
-
* import assert from "node:assert/strict";
|
|
2100
|
-
* declare const host: TestHost;
|
|
2101
|
-
* // ---cut---
|
|
2102
|
-
* host.upscalingDecoding("copy-back", [{ id: "vestra.video", name: "Picture Adjustments" }]);
|
|
2103
|
-
* host.upscalingChanged();
|
|
2104
|
-
* await host.settle();
|
|
2105
|
-
* assert.match(String(host.window("upscale").props.blocker), /Picture Adjustments/);
|
|
2106
|
-
* ```
|
|
2107
|
-
*
|
|
2108
|
-
* @capability None.
|
|
2109
|
-
* @since 1.5.43
|
|
2110
|
-
*/
|
|
2111
|
-
upscalingDecoding(cause: UpscalingDecodingCause, plugins?: readonly UpscalingPlugin[]): UpscalingReason;
|
|
2112
|
-
/**
|
|
2113
|
-
* Send `video.upscaling.changed` with the state {@link TestHost.upscaling}
|
|
2114
|
-
* describes now (without `enabled`), as the player does when it changes
|
|
2115
|
-
* (player 0.5.25); a plugin listening with
|
|
2116
|
-
* `plugin.video.upscaling.onChange` receives it. Nothing reaches a plugin
|
|
2117
|
-
* that does not listen.
|
|
2118
|
-
*
|
|
2119
|
-
* @capability None.
|
|
2120
|
-
*
|
|
2121
|
-
* @example Show the new factor in the plugin's window
|
|
2122
|
-
* The window resized: the test changes the area and the factor, sends
|
|
2123
|
-
* the change, and checks that the plugin's window shows the new factor.
|
|
2124
|
-
* ```ts
|
|
2125
|
-
* import assert from "node:assert/strict";
|
|
2126
|
-
* declare const host: TestHost;
|
|
2127
|
-
* // ---cut---
|
|
2128
|
-
* host.upscaling.area = { width: 2560, height: 1440 };
|
|
2129
|
-
* host.upscaling.factor = 1.33; // 1080p in a 1440p area
|
|
2130
|
-
* host.upscalingChanged(); // as the player after the resize settled
|
|
2131
|
-
* await host.settle();
|
|
2132
|
-
* assert.equal(host.window("upscale").props.factor, "×1.33");
|
|
2133
|
-
* ```
|
|
2134
|
-
*
|
|
2135
|
-
* @since 1.5.43
|
|
2136
|
-
*/
|
|
2137
|
-
upscalingChanged(): void;
|
|
2138
|
-
/** What `player.chapters()` lists while media is ready. */
|
|
2139
|
-
chapters: {
|
|
2140
|
-
/** The chapter title, `null` without one. */
|
|
2141
|
-
title: string | null;
|
|
2142
|
-
/** Start time in seconds. */
|
|
2143
|
-
start: number;
|
|
2144
|
-
}[];
|
|
2145
|
-
/**
|
|
2146
|
-
* @internal `startPlugin`'s `disconnect()`: refuse every request, the
|
|
2147
|
-
* ones in flight included, as the runner does once the player is gone.
|
|
2148
|
-
*/
|
|
2149
|
-
loseConnection(): void;
|
|
2150
|
-
/**
|
|
2151
|
-
* `plugin.dataDirectory`: a fresh empty temporary directory, removed with
|
|
2152
|
-
* the host on `stop()`. `plugin.storage.json` reads and writes real files
|
|
2153
|
-
* here. Pass `startPlugin(..., { dataDirectory })` to use another one.
|
|
2154
|
-
*/
|
|
2155
|
-
readonly dataDirectory: string;
|
|
2156
|
-
/**
|
|
2157
|
-
* The manifest id of the plugin under test (set by {@link startPlugin};
|
|
2158
|
-
* `"test.plugin"` by default).
|
|
2159
|
-
*/
|
|
2160
|
-
manifestId: string;
|
|
2161
|
-
/**
|
|
2162
|
-
* A fresh in-memory player: nothing open, no plugin, the player's core
|
|
2163
|
-
* actions registered. Preset what the plugin reads during activation, then
|
|
2164
|
-
* pass it to {@link startPlugin} as `host`.
|
|
2165
|
-
*/
|
|
2166
|
-
constructor();
|
|
2167
|
-
/** The `player.setStepSeek` value in effect (default 5). */
|
|
2168
|
-
stepSeek: number;
|
|
2169
|
-
/** The `player.setStepSeek` `keyframes` in effect (default `false`, SDK 1.5.19). */
|
|
2170
|
-
stepSeekKeyframes: boolean;
|
|
2171
|
-
/**
|
|
2172
|
-
* Test data: the keyframe times (seconds, ascending) of the current
|
|
2173
|
-
* source, for keyframe step seeks. `null` (the default): none modelled,
|
|
2174
|
-
* and step seeks are exact even with `stepSeekKeyframes`.
|
|
2175
|
-
*
|
|
2176
|
-
* @example Step forward to the next keyframe
|
|
2177
|
-
* With keyframe step seeks on, Forward from 0:12 lands on the keyframe at
|
|
2178
|
-
* 0:20, not at 0:17, and the OSD says so. `keyframes` gives the source's
|
|
2179
|
-
* keyframe times; `setStepSeek` turns on keyframe steps of 5 s, and the
|
|
2180
|
-
* core action `seek-forward` seeks to the first keyframe at or after
|
|
2181
|
-
* the target, as the player does.
|
|
2182
|
-
* ```ts
|
|
2183
|
-
* import assert from "node:assert/strict";
|
|
2184
|
-
* declare const host: TestHost;
|
|
2185
|
-
* // ---cut---
|
|
2186
|
-
* host.setSource({ phase: "ready", duration: 45, position: 12 });
|
|
2187
|
-
* host.keyframes = [0, 10, 20, 30, 40];
|
|
2188
|
-
* await plugin.player.setStepSeek({ seconds: 5, keyframes: true });
|
|
2189
|
-
* await plugin.host.run("seek-forward"); // target 17 s, next keyframe 20 s
|
|
2190
|
-
* assert.equal(host.source.position, 20);
|
|
2191
|
-
* assert.equal(host.osd.at(-1), "Forward 5 s: 00:20 / 00:45");
|
|
2192
|
-
* ```
|
|
2193
|
-
*/
|
|
2194
|
-
keyframes: number[] | null;
|
|
2195
|
-
/**
|
|
2196
|
-
* Every step seek the core ran (`seek-backward` / `seek-forward`, from a
|
|
2197
|
-
* key, the menu or `host.run`), newest last: the step, whether it went to
|
|
2198
|
-
* a keyframe, and where it landed.
|
|
2199
|
-
*/
|
|
2200
|
-
readonly coreSeeks: {
|
|
2201
|
-
/** `seek-backward` or `seek-forward`. */
|
|
2202
|
-
action: string;
|
|
2203
|
-
/** The step: `host.run`'s `seconds`, else `setStepSeek`'s. */
|
|
2204
|
-
seconds: number;
|
|
2205
|
-
/** It landed on a keyframe (`stepSeekKeyframes` and keyframes modelled). */
|
|
2206
|
-
keyframes: boolean;
|
|
2207
|
-
/** The new position. */
|
|
2208
|
-
landed: number;
|
|
2209
|
-
}[];
|
|
2210
|
-
/**
|
|
2211
|
-
* Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest
|
|
2212
|
-
* last, with the mode it asked for. The test host lands on the exact
|
|
2213
|
-
* target either way (the player lands a `keyframes` seek on the last
|
|
2214
|
-
* keyframe at or before it); the core's step seeks are in `coreSeeks`.
|
|
2215
|
-
*/
|
|
2216
|
-
readonly seeks: {
|
|
2217
|
-
/** The target. */
|
|
2218
|
-
seconds: number;
|
|
2219
|
-
/** `Scrub.seek(seconds, { keyframes: true })` (SDK 1.5.22). */
|
|
2220
|
-
keyframes: boolean;
|
|
2221
|
-
}[];
|
|
2222
|
-
/** Every `player.step`, newest last. */
|
|
2223
|
-
readonly steps: {
|
|
2224
|
-
/** `"frame"` or `"keyframe"`. */
|
|
2225
|
-
unit: string;
|
|
2226
|
-
/** `"backward"` or `"forward"`. */
|
|
2227
|
-
direction: string;
|
|
2228
|
-
}[];
|
|
2229
|
-
/** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
|
|
2230
|
-
readonly coreRuns: string[];
|
|
2231
|
-
/**
|
|
2232
|
-
* One of the 14 core actions (`play-pause`, `stop`, `seek-forward`, ...)
|
|
2233
|
-
* that the test host registers like the player, as `host/<id>`. Throws for
|
|
2234
|
-
* an unknown id. `run()` executes it like its menu item: a disabled
|
|
2235
|
-
* action rejects with `command`, as in the player (`plugin.host.run`
|
|
2236
|
-
* rejects with `unavailable` instead); otherwise it runs with the
|
|
2237
|
-
* player's checks and writes its on-screen text to {@link TestHost.osd}.
|
|
2238
|
-
*
|
|
2239
|
-
* @example Pause through the player's own action
|
|
2240
|
-
* The test pauses playback the way the Space key does and checks that the
|
|
2241
|
-
* host recorded it. Core actions belong to the player (`host/<id>`);
|
|
2242
|
-
* `run()` applies the player's checks, so a source must be ready first,
|
|
2243
|
-
* and every run is appended to `coreRuns`.
|
|
2244
|
-
* ```ts
|
|
2245
|
-
* import assert from "node:assert/strict";
|
|
2246
|
-
* declare const host: TestHost;
|
|
2247
|
-
* // ---cut---
|
|
2248
|
-
* host.setSource({ phase: "ready", duration: 100, position: 10 });
|
|
2249
|
-
* await host.coreAction("play-pause").run();
|
|
2250
|
-
* assert.equal(host.source.paused, true);
|
|
2251
|
-
* assert.deepEqual(host.coreRuns, ["play-pause"]);
|
|
2252
|
-
* ```
|
|
2253
|
-
*
|
|
2254
|
-
* @capability None.
|
|
2255
|
-
* @since 1.0.0
|
|
2256
|
-
*/
|
|
2257
|
-
coreAction(id: string): FakeCommand;
|
|
2258
|
-
/**
|
|
2259
|
-
* @internal The transport side the SDK calls: answer one request with the
|
|
2260
|
-
* override from {@link TestHost.handle} or the default behaviour.
|
|
2261
|
-
*/
|
|
2262
|
-
request<T>(method: string, params?: Json): Promise<T>;
|
|
2263
|
-
/** @internal The transport side the SDK calls: listen to a host event. */
|
|
2264
|
-
on(event: string, listener: (data: unknown) => void): () => void;
|
|
2265
|
-
/**
|
|
2266
|
-
* Replace the host's behaviour for one wire method until
|
|
2267
|
-
* {@link TestHost.reset}. The handler receives the request parameters and
|
|
2268
|
-
* returns the reply (or a promise of it).
|
|
2269
|
-
*
|
|
2270
|
-
* Throw a {@link HostError} to simulate a definite refusal (the plugin's
|
|
2271
|
-
* call rejects with that `code`); throw a plain `Error` to simulate a lost
|
|
2272
|
-
* reply (the SDK then runs its recovery path). Method names are protocol
|
|
2273
|
-
* names, e.g. `"clipboard.writeText"`, `"secrets.get"`,
|
|
2274
|
-
* `"system.openPath"`, `"audio.addTrack"`; see {@link TestHost.sent} to
|
|
2275
|
-
* find the one a call uses.
|
|
2276
|
-
*
|
|
2277
|
-
* @param method - The wire method name.
|
|
2278
|
-
* @param handler - Produces the reply from the request parameters.
|
|
2279
|
-
*
|
|
2280
|
-
* @example Make opening a folder fail
|
|
2281
|
-
* The plugin's Open Folder command meets a missing folder and must tell
|
|
2282
|
-
* the user. `handle` replaces the host's answer to one wire method; the
|
|
2283
|
-
* `HostError` it throws reaches the plugin as a rejection with code
|
|
2284
|
-
* `missing`, like the player's refusal. `reset` restores the default
|
|
2285
|
-
* behaviour for the rest of the test.
|
|
2286
|
-
* ```ts
|
|
2287
|
-
* import assert from "node:assert/strict";
|
|
2288
|
-
* declare const host: TestHost;
|
|
2289
|
-
* // ---cut---
|
|
2290
|
-
* host.handle("system.openPath", () => {
|
|
2291
|
-
* throw new HostError("missing", "no such file");
|
|
2292
|
-
* });
|
|
2293
|
-
* await host.command("open-folder").run(); // the plugin catches `missing`
|
|
2294
|
-
* assert.deepEqual(host.osd, ["Folder not found"]);
|
|
2295
|
-
* host.reset("system.openPath");
|
|
2296
|
-
* ```
|
|
2297
|
-
*
|
|
2298
|
-
* @capability None.
|
|
2299
|
-
* @since 1.0.0
|
|
2300
|
-
*/
|
|
2301
|
-
handle(method: string, handler: Handler): void;
|
|
2302
|
-
/**
|
|
2303
|
-
* Restore the default behaviour of a method replaced with {@link TestHost.handle}.
|
|
2304
|
-
*
|
|
2305
|
-
* @capability None.
|
|
2306
|
-
*
|
|
2307
|
-
* @example Restore the default answer after a simulated failure
|
|
2308
|
-
* The first write fails, the second succeeds. `reset` removes the handler
|
|
2309
|
-
* that `handle` installed, so the method is answered by the test host
|
|
2310
|
-
* again.
|
|
2311
|
-
* ```ts
|
|
2312
|
-
* import assert from "node:assert/strict";
|
|
2313
|
-
* declare const host: TestHost;
|
|
2314
|
-
* // ---cut---
|
|
2315
|
-
* host.handle("clipboard.writeText", () => {
|
|
2316
|
-
* throw new HostError("failed", "denied");
|
|
2317
|
-
* });
|
|
2318
|
-
* await assert.rejects(plugin.clipboard.writeText("x"));
|
|
2319
|
-
* host.reset("clipboard.writeText");
|
|
2320
|
-
* await plugin.clipboard.writeText("x"); // the default handler again
|
|
2321
|
-
* ```
|
|
2322
|
-
*
|
|
2323
|
-
* @since 1.0.0
|
|
2324
|
-
*/
|
|
2325
|
-
reset(method: string): void;
|
|
2326
|
-
/**
|
|
2327
|
-
* Deliver a host event to the plugin. Like the player, subscription-based
|
|
2328
|
-
* events (`source.state`, `playback.backend`, `window.state`,
|
|
2329
|
-
* `timeline.changed`, `appearance.changed`) are delivered only when the
|
|
2330
|
-
* plugin subscribed (it declared `events` and listens). Most tests use the
|
|
2331
|
-
* higher-level helpers (`setSource`, `setMainWindow`, ...) instead.
|
|
2332
|
-
*
|
|
2333
|
-
* @param event - The wire event name.
|
|
2334
|
-
* @param data - The event payload (wire-level shape).
|
|
2335
|
-
*
|
|
2336
|
-
* @capability None.
|
|
2337
|
-
*
|
|
2338
|
-
* @example Deliver a raw event the helpers do not cover
|
|
2339
|
-
* The test pushes a `window.state` event after changing `layout` by hand.
|
|
2340
|
-
* Like the player, the host delivers it only when the plugin subscribed;
|
|
2341
|
-
* most tests use `setSource`, `setMainWindow` and the other helpers
|
|
2342
|
-
* instead.
|
|
2343
|
-
* ```ts
|
|
2344
|
-
* declare const host: TestHost;
|
|
2345
|
-
* // ---cut---
|
|
2346
|
-
* host.emit("window.state", {}); // no facts: the SDK reads window.state again
|
|
2347
|
-
* await host.settle();
|
|
2348
|
-
* ```
|
|
2349
|
-
*
|
|
2350
|
-
* @since 1.0.0
|
|
2351
|
-
*/
|
|
2352
|
-
emit(event: string, data: unknown): void;
|
|
2353
|
-
/**
|
|
2354
|
-
* Change the source facts and broadcast them, as if the player's state
|
|
2355
|
-
* changed (the user paused, playback reached the end, a file finished
|
|
2356
|
-
* loading). Any phase other than `empty` also turns on the source
|
|
2357
|
-
* capabilities (pause, seek, rate, ...) unless the patch sets them.
|
|
2358
|
-
* `phase: "loading"` makes a mounted backdrop effective, like the player.
|
|
2359
|
-
*
|
|
2360
|
-
* @param patch - Wire-level source fields, e.g. `phase`, `position`,
|
|
2361
|
-
* `duration`, `paused`, `audioTracks`.
|
|
2362
|
-
*
|
|
2363
|
-
* @example Load a file, then let it play to the end
|
|
2364
|
-
* The plugin sees a one-minute source become ready, then end on its own.
|
|
2365
|
-
* `setSource` changes the host's source facts and broadcasts them like
|
|
2366
|
-
* the player's events; the SDK updates `player.state` when the event
|
|
2367
|
-
* arrives, so the test settles before reading it.
|
|
2368
|
-
* ```ts
|
|
2369
|
-
* import assert from "node:assert/strict";
|
|
2370
|
-
* declare const host: TestHost;
|
|
2371
|
-
* // ---cut---
|
|
2372
|
-
* host.setSource({ phase: "ready", duration: 60, position: 0 });
|
|
2373
|
-
* await host.settle();
|
|
2374
|
-
* assert.equal(plugin.player.state.phase, "ready");
|
|
2375
|
-
* host.setSource({ phase: "ended", terminalReason: "natural" });
|
|
2376
|
-
* await host.settle();
|
|
2377
|
-
* ```
|
|
2378
|
-
*
|
|
2379
|
-
* @capability None.
|
|
2380
|
-
* @since 1.0.0
|
|
2381
|
-
*/
|
|
2382
|
-
setSource(patch: Partial<SourceState>): void;
|
|
2383
|
-
/**
|
|
2384
|
-
* Simulate the user changing the main window (resizing, maximizing or
|
|
2385
|
-
* restoring it, entering or leaving fullscreen, closing the
|
|
2386
|
-
* picture-in-picture window) and broadcast it. `maximized` (SDK 1.5.52)
|
|
2387
|
-
* reads `false` while fullscreen, as player 0.6.3 reports it. Like the player, entering fullscreen collapses an attached, visible
|
|
2388
|
-
* sidebar (`effective: false`, `reason: "fullscreen"`) and leaving
|
|
2389
|
-
* fullscreen docks it again.
|
|
2390
|
-
*
|
|
2391
|
-
* @example Go fullscreen, report picture-in-picture support, maximize
|
|
2392
|
-
* The plugin sees the main window fullscreen at 1920×1080, then learns
|
|
2393
|
-
* that picture-in-picture is available, then sees the window leave
|
|
2394
|
-
* fullscreen maximized. `setMainWindow` changes the
|
|
2395
|
-
* host's window facts and broadcasts them as the player does when the
|
|
2396
|
-
* user resizes or toggles fullscreen; entering fullscreen also collapses
|
|
2397
|
-
* an attached sidebar.
|
|
2398
|
-
* ```ts
|
|
2399
|
-
* import assert from "node:assert/strict";
|
|
2400
|
-
* declare const host: TestHost;
|
|
2401
|
-
* // ---cut---
|
|
2402
|
-
* host.setMainWindow({ fullscreen: true, videoArea: { width: 1920, height: 1080 } });
|
|
2403
|
-
* await host.settle();
|
|
2404
|
-
* assert.equal(plugin.window.state.fullscreen, true);
|
|
2405
|
-
* host.setMainWindow({ pictureInPicture: { supported: true, active: false } });
|
|
2406
|
-
* // Leaving fullscreen into a maximized window: `maximized` reads true again.
|
|
2407
|
-
* host.setMainWindow({ fullscreen: false, maximized: true });
|
|
2408
|
-
* await host.settle();
|
|
2409
|
-
* assert.equal(plugin.window.state.maximized, true);
|
|
2410
|
-
* ```
|
|
2411
|
-
*
|
|
2412
|
-
* @capability None.
|
|
2413
|
-
* @since 1.0.0
|
|
2414
|
-
*/
|
|
2415
|
-
setMainWindow(patch: Partial<{
|
|
2416
|
-
fullscreen: boolean;
|
|
2417
|
-
maximized: boolean;
|
|
2418
|
-
videoArea: {
|
|
2419
|
-
width: number;
|
|
2420
|
-
height: number;
|
|
2421
|
-
};
|
|
2422
|
-
pictureInPicture: {
|
|
2423
|
-
supported: boolean;
|
|
2424
|
-
active: boolean;
|
|
2425
|
-
};
|
|
2426
|
-
}>): void;
|
|
2427
|
-
/**
|
|
2428
|
-
* The pointer reached the right edge in fullscreen (`true`) or left the
|
|
2429
|
-
* popped-out columns (`false`): the attached sidebar and the adjust column
|
|
2430
|
-
* (SDK 1.6.13), those switched on, pop out or collapse (reference §6.3). No
|
|
2431
|
-
* effect outside fullscreen, or on a column switched off or floating.
|
|
2432
|
-
*
|
|
2433
|
-
* @capability None.
|
|
2434
|
-
*
|
|
2435
|
-
* @example Pop the sidebar out in fullscreen
|
|
2436
|
-
* The pointer reaches the right edge in fullscreen and the attached
|
|
2437
|
-
* sidebar pops out over the video. Outside fullscreen, or with the
|
|
2438
|
-
* sidebar hidden or floating, nothing happens.
|
|
2439
|
-
* ```ts
|
|
2440
|
-
* declare const host: TestHost;
|
|
2441
|
-
* // ---cut---
|
|
2442
|
-
* host.setMainWindow({ fullscreen: true });
|
|
2443
|
-
* host.popSidebar(true);
|
|
2444
|
-
* await host.settle();
|
|
2445
|
-
* ```
|
|
2446
|
-
*
|
|
2447
|
-
* @since 1.0.0
|
|
2448
|
-
*/
|
|
2449
|
-
popSidebar(popped: boolean): void;
|
|
2450
|
-
/**
|
|
2451
|
-
* Simulate another plugin (or the user) opening a source: a new
|
|
2452
|
-
* `sourceId`, then the given phase. Calls the plugin made against the old
|
|
2453
|
-
* state now fail with `stale`.
|
|
2454
|
-
*
|
|
2455
|
-
* @param source - The path or URL.
|
|
2456
|
-
* @param phase - The phase to end in (default `"ready"`).
|
|
2457
|
-
*
|
|
2458
|
-
* @example Let the user open another file meanwhile
|
|
2459
|
-
* A state the plugin captured before stops being current once another
|
|
2460
|
-
* file opens. `openElsewhere` gives the source a new `sourceId`, so
|
|
2461
|
-
* `isCurrent` turns false. Promise-returning calls made with
|
|
2462
|
-
* `ifCurrent: before` then reject with `stale`; calls that return nothing
|
|
2463
|
-
* drop the write silently instead (`TimelineLayer.set` with `ifCurrent`).
|
|
2464
|
-
* ```ts
|
|
2465
|
-
* import assert from "node:assert/strict";
|
|
2466
|
-
* declare const host: TestHost;
|
|
2467
|
-
* // ---cut---
|
|
2468
|
-
* const before = plugin.player.state;
|
|
2469
|
-
* host.openElsewhere("/media/other.mp4");
|
|
2470
|
-
* await host.settle();
|
|
2471
|
-
* assert.equal(plugin.player.isCurrent(before), false);
|
|
2472
|
-
* ```
|
|
2473
|
-
*
|
|
2474
|
-
* @capability None.
|
|
2475
|
-
* @since 1.0.0
|
|
2476
|
-
*/
|
|
2477
|
-
openElsewhere(source: string, phase?: SourceState["phase"]): void;
|
|
2478
|
-
/**
|
|
2479
|
-
* Simulate a generated stream failing in the player (its audio device
|
|
2480
|
-
* lost, its frame connection broken), like the player: the source fails
|
|
2481
|
-
* with `message` (`phase: "error"`) and the producer is retired at once.
|
|
2482
|
-
* The stream's `onStop` listeners get a `stream` error with `message`.
|
|
2483
|
-
*
|
|
2484
|
-
* @param producerId - The producer, from {@link TestHost.producers}.
|
|
2485
|
-
* @param message - The player's failure text.
|
|
2486
|
-
*
|
|
2487
|
-
* @example Break the plugin's generated stream
|
|
2488
|
-
* The plugin's stream stops with an error, as when the player loses its
|
|
2489
|
-
* audio device. `failStream` puts the source into `error` with the
|
|
2490
|
-
* message and retires the producer at once; the stream's `onStop`
|
|
2491
|
-
* listeners get a `stream` error carrying the message.
|
|
2492
|
-
* ```ts
|
|
2493
|
-
* declare const host: TestHost;
|
|
2494
|
-
* // ---cut---
|
|
2495
|
-
* const [producer] = host.producers.values();
|
|
2496
|
-
* host.failStream(producer!.id, "producer transport closed");
|
|
2497
|
-
* ```
|
|
2498
|
-
*
|
|
2499
|
-
* @capability None.
|
|
2500
|
-
* @since 1.5.7
|
|
2501
|
-
*/
|
|
2502
|
-
failStream(producerId: number, message: string): void;
|
|
2503
|
-
/**
|
|
2504
|
-
* Press a binding like the host's table would: panel-focus bindings first
|
|
2505
|
-
* when `panel` is true, then global ones; within a scope the earliest
|
|
2506
|
-
* registration first (the player's own commands before the plugin's), as
|
|
2507
|
-
* the conflict rule orders them. A disabled command lets the key pass to
|
|
2508
|
-
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
2509
|
-
* shadowed, then the global scope. The first enabled one runs; `press`
|
|
2510
|
-
* resolves after it ran (`"ran"`). When only disabled commands hold the
|
|
2511
|
-
* key, the first of them decides, as in the player: with a `reason` it
|
|
2512
|
-
* shows the reason on the OSD and eats the key (`"reason"`, see
|
|
2513
|
-
* `host.osd`); without one the key goes on to the focused control
|
|
2514
|
-
* (`"passed"`, nothing runs). Unlike the player, which only logs a key
|
|
2515
|
-
* that reaches a busy command, `press` then rejects, so a test notices. Rejects when nothing is bound. Bindings use the
|
|
2516
|
-
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
2517
|
-
* overrides in `bindings.overlay` apply.
|
|
2518
|
-
*
|
|
2519
|
-
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
2520
|
-
*
|
|
2521
|
-
* @example Press a global key and a panel key
|
|
2522
|
-
* Ctrl+B runs whatever is bound globally; Delete is pressed while the
|
|
2523
|
-
* plugin's panel has focus, so panel bindings are tried first. `press`
|
|
2524
|
-
* follows the player's binding table and resolves with what happened:
|
|
2525
|
-
* `"ran"`, `"reason"` (a disabled command showed why) or `"passed"`.
|
|
2526
|
-
* ```ts
|
|
2527
|
-
* declare const host: TestHost;
|
|
2528
|
-
* // ---cut---
|
|
2529
|
-
* await host.press("Ctrl+B"); // a global binding
|
|
2530
|
-
* await host.press("Delete", { panel: true }); // while the panel has focus
|
|
2531
|
-
* ```
|
|
2532
|
-
*
|
|
2533
|
-
* @capability None.
|
|
2534
|
-
* @throws `command` (rejects) A `HostError`: the command that would run is
|
|
2535
|
-
* busy (the player only logs this).
|
|
2536
|
-
* @throws `Error` (rejects) Nothing is bound to the key.
|
|
2537
|
-
* @since 1.0.0
|
|
2538
|
-
*/
|
|
2539
|
-
press(binding: string, options?: {
|
|
2540
|
-
panel?: boolean;
|
|
2541
|
-
}): Promise<"ran" | "reason" | "passed">;
|
|
2542
|
-
/**
|
|
2543
|
-
* Bindings declared by more than one command, each as
|
|
2544
|
-
* `[binding, winner, ...ignored]` (command ids; core actions as
|
|
2545
|
-
* `host/<id>`). Empty when there are none.
|
|
2546
|
-
*
|
|
2547
|
-
* @example Check that no default keys collide
|
|
2548
|
-
* The test fails when two commands, the player's or the plugin's, declare
|
|
2549
|
-
* the same binding. Each entry is `[binding, winner, ...ignored]`, so an
|
|
2550
|
-
* empty list means every key does what its command says.
|
|
2551
|
-
* ```ts
|
|
2552
|
-
* import assert from "node:assert/strict";
|
|
2553
|
-
* declare const host: TestHost;
|
|
2554
|
-
* // ---cut---
|
|
2555
|
-
* assert.deepEqual(host.conflicts, []);
|
|
2556
|
-
* ```
|
|
2557
|
-
*/
|
|
2558
|
-
get conflicts(): string[][];
|
|
2559
|
-
/**
|
|
2560
|
-
* Change the keys of one of the plugin's commands, or of one of the
|
|
2561
|
-
* player's core commands by its `host/<id>` (SDK 1.6.16), as the user
|
|
2562
|
-
* does in Keyboard Shortcuts (SDK 1.6.6): `inputs` replaces its keys (`[]`
|
|
2563
|
-
* clears them, as Ignore Default Shortcuts does for every default),
|
|
2564
|
-
* `null` gives it its own defaults back. It writes
|
|
2565
|
-
* {@link TestHost.bindings | bindings.overlay}, and the plugin hears it
|
|
2566
|
-
* as from the player: `bindings.changed` and, when what
|
|
2567
|
-
* `plugin.commands.keys` reads changed, `commands.keys`.
|
|
2568
|
-
*
|
|
2569
|
-
* @example Check that a key hint follows a rebinding
|
|
2570
|
-
* The view showed W for Brightness −; after the user moves it to
|
|
2571
|
-
* Shift+W, it shows the new key. `rebind` changes the overlay and
|
|
2572
|
-
* pushes the new labels, and `settle` lets the binding run again. The
|
|
2573
|
-
* test host is a Mac unless `systemInfo.os` says otherwise, hence ⇧W.
|
|
2574
|
-
* ```ts
|
|
2575
|
-
* import assert from "node:assert/strict";
|
|
2576
|
-
* declare const host: TestHost;
|
|
2577
|
-
* // ---cut---
|
|
2578
|
-
* host.rebind("brightness.down", ["Shift+W"]);
|
|
2579
|
-
* await host.settle();
|
|
2580
|
-
* assert.equal(host.window("picture").props["brightness-down"], "⇧W");
|
|
2581
|
-
* ```
|
|
2582
|
-
*
|
|
2583
|
-
* @capability None.
|
|
2584
|
-
* @throws `Error` `command` is neither a command of the plugin nor a
|
|
2585
|
-
* core command.
|
|
2586
|
-
* @since 1.6.6
|
|
2587
|
-
*/
|
|
2588
|
-
rebind(command: string, inputs: readonly string[] | null): void;
|
|
2589
|
-
/**
|
|
2590
|
-
* The binding overlay `plugin.bindings.set` writes (`"<plugin>/<command>"`
|
|
2591
|
-
* to inputs; preset it to simulate saved overrides). `press` honours it.
|
|
2592
|
-
*/
|
|
2593
|
-
readonly bindings: {
|
|
2594
|
-
/** `"<plugin>/<command>"` → the user's inputs for it. */
|
|
2595
|
-
overlay: Map<string, string[]>;
|
|
2596
|
-
};
|
|
2597
|
-
/**
|
|
2598
|
-
* What `packages.list` reports; `setEnabled`, `uninstall` and `install`
|
|
2599
|
-
* update it. `builtin`, `source` and `dataDirectory` are optional here
|
|
2600
|
-
* (default `false`, `null`, `null`).
|
|
2601
|
-
*/
|
|
2602
|
-
packages: {
|
|
2603
|
-
/** The plugin's manifest id. */
|
|
2604
|
-
id: string;
|
|
2605
|
-
/** Its version, `null` when unknown. */
|
|
2606
|
-
version: string | null;
|
|
2607
|
-
/** Where it was loaded from. */
|
|
2608
|
-
origin: "installed" | "development";
|
|
2609
|
-
/** Its run state. */
|
|
2610
|
-
state: "starting" | "running" | "disabled" | "failed" | "stopped";
|
|
2611
|
-
/** Why it failed (`state: "failed"`). */
|
|
2612
|
-
error: string | null;
|
|
2613
|
-
/** Its declared capabilities. */
|
|
2614
|
-
capabilities: string[];
|
|
2615
|
-
/** The package directory. */
|
|
2616
|
-
directory: string;
|
|
2617
|
-
/** Shipped with the player: `uninstall` and `install` are `unsupported`. */
|
|
2618
|
-
builtin?: boolean;
|
|
2619
|
-
/** Where it was installed from. */
|
|
2620
|
-
source?: PackageSource | null;
|
|
2621
|
-
/** Its data directory. */
|
|
2622
|
-
dataDirectory?: string | null;
|
|
2623
|
-
}[];
|
|
2624
|
-
/** Every `packages.setEnabled` / `uninstall` / `install`, newest last. */
|
|
2625
|
-
readonly packageCalls: {
|
|
2626
|
-
/** Which call. */
|
|
2627
|
-
method: "setEnabled" | "reload" | "uninstall" | "install";
|
|
2628
|
-
/** The target plugin id. */
|
|
2629
|
-
id: string;
|
|
2630
|
-
/** `setEnabled`'s value, or `install`'s `enabled` option when given. */
|
|
2631
|
-
enabled?: boolean;
|
|
2632
|
-
/** `uninstall`'s `removeData` option. */
|
|
2633
|
-
removeData?: boolean;
|
|
2634
|
-
/** `install`'s source (`{ kind: "file" }` when not given). */
|
|
2635
|
-
source?: PackageSource;
|
|
2636
|
-
/** `install`'s `replace` option. */
|
|
2637
|
-
replace?: boolean;
|
|
2638
|
-
}[];
|
|
2639
|
-
/**
|
|
2640
|
-
* Packages `validate` and `validateArchive` accept: path (directory or
|
|
2641
|
-
* `.vxp` file) → its manifest. Other paths fail with `missing`. `install`
|
|
2642
|
-
* adds or upgrades the entry in {@link TestHost.packages} following the
|
|
2643
|
-
* player's source rules: the same source upgrades, another source needs
|
|
2644
|
-
* `replace` (`conflict` otherwise), a built-in id is `unsupported`. As
|
|
2645
|
-
* player 0.6.4 and later, the installed entry's `state` follows
|
|
2646
|
-
* {@link "@vestra-player/sdk"!InstallOptions.enabled}: a first install
|
|
2647
|
-
* runs, an update of a `disabled` entry stays `disabled` unless `enabled`
|
|
2648
|
-
* is `true`, and `enabled: false` installs it `disabled`. Set
|
|
2649
|
-
* {@link TestHost.installKeepsDisabled} to `false` for the older players'
|
|
2650
|
-
* rule (every install runs).
|
|
2651
|
-
*
|
|
2652
|
-
* @example Offer a package file the plugin can install
|
|
2653
|
-
* The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
|
|
2654
|
-
* as if the file existed. The host accepts only paths listed here (others
|
|
2655
|
-
* fail with `missing`) and returns this manifest from `validate`; an
|
|
2656
|
-
* install then follows the player's source rules.
|
|
2657
|
-
* ```ts
|
|
2658
|
-
* declare const host: TestHost;
|
|
2659
|
-
* // ---cut---
|
|
2660
|
-
* host.candidates.set("/downloads/someone.demo-1.0.0.vxp", {
|
|
2661
|
-
* id: "someone.demo", version: "1.0.0", apiVersion: "vestra/1",
|
|
2662
|
-
* entry: "main.js", capabilities: [],
|
|
2663
|
-
* });
|
|
2664
|
-
* ```
|
|
2665
|
-
*/
|
|
2666
|
-
readonly candidates: Map<string, PackageManifest>;
|
|
2667
|
-
/**
|
|
2668
|
-
* Which player's install rule `packages.install` plays. `true` (default,
|
|
2669
|
-
* SDK 1.5.55): player 0.6.4 and later, where an update or reinstall
|
|
2670
|
-
* keeps a `disabled` plugin disabled unless the plugin passes
|
|
2671
|
-
* {@link "@vestra-player/sdk"!InstallOptions.enabled}. `false`: players
|
|
2672
|
-
* up to 0.6.3, where every install runs the plugin and `enabled` is
|
|
2673
|
-
* ignored (it is still checked and recorded in
|
|
2674
|
-
* {@link TestHost.packageCalls}). A test written for the older rule that
|
|
2675
|
-
* updates a disabled plugin sets it to `false`; one that keeps testing
|
|
2676
|
-
* both runs once with each.
|
|
2677
|
-
*
|
|
2678
|
-
* @example Keep a test written for players up to 0.6.3
|
|
2679
|
-
* The plugin under test installs an update over a plugin the user
|
|
2680
|
-
* disabled and expects it to run afterwards, as players up to 0.6.3 do.
|
|
2681
|
-
* With `false` the host plays that rule; the default plays player 0.6.4,
|
|
2682
|
-
* where the update stays disabled.
|
|
2683
|
-
* ```ts
|
|
2684
|
-
* declare const host: TestHost;
|
|
2685
|
-
* // ---cut---
|
|
2686
|
-
* host.installKeepsDisabled = false; // every install runs, as before 0.6.4
|
|
2687
|
-
* ```
|
|
2688
|
-
*
|
|
2689
|
-
* @capability None.
|
|
2690
|
-
* @since 1.5.55
|
|
2691
|
-
*/
|
|
2692
|
-
installKeepsDisabled: boolean;
|
|
2693
|
-
/**
|
|
2694
|
-
* Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address
|
|
2695
|
-
* → a local PNG or JPEG file served for it, or `null` for a failing
|
|
2696
|
-
* download. Nothing is fetched from the network in tests; unknown
|
|
2697
|
-
* addresses fail.
|
|
2698
|
-
*/
|
|
2699
|
-
readonly remoteImages: Map<string, string | null>;
|
|
2700
|
-
/**
|
|
2701
|
-
* The Markdown document a value holds (its wire form read back), or
|
|
2702
|
-
* `null` when the value is not one. `images` maps each remote address to
|
|
2703
|
-
* the downloaded file. Pass a whole property value or a field of one: a
|
|
2704
|
-
* document nested in rows is read the same way.
|
|
2705
|
-
*
|
|
2706
|
-
* @example Read back a Markdown message in a chat window
|
|
2707
|
-
* The test checks the Markdown source of the first chat message. Rows
|
|
2708
|
-
* hold the document in its wire form, as `plugin.ui.markdown` produced
|
|
2709
|
-
* it; `markdown` reads it back, nested in a row or not.
|
|
2710
|
-
* ```ts
|
|
2711
|
-
* import assert from "node:assert/strict";
|
|
2712
|
-
* declare const host: TestHost;
|
|
2713
|
-
* // ---cut---
|
|
2714
|
-
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
2715
|
-
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
2716
|
-
* ```
|
|
2717
|
-
*
|
|
2718
|
-
* @capability None.
|
|
2719
|
-
* @since 1.1.0
|
|
2720
|
-
*/
|
|
2721
|
-
markdown(value: Json | undefined): MarkdownWire | null;
|
|
2722
|
-
/**
|
|
2723
|
-
* What `player.audioDevices()` lists. `setAudioDevice` updates
|
|
2724
|
-
* `host.source.audioOutput`.
|
|
2725
|
-
*/
|
|
2726
|
-
audioDevices: {
|
|
2727
|
-
/** The device id, e.g. `coreaudio/<UID>`. */
|
|
2728
|
-
id: string;
|
|
2729
|
-
/** The system's name for it. */
|
|
2730
|
-
name: string;
|
|
2731
|
-
}[];
|
|
2732
|
-
/**
|
|
2733
|
-
* Replace the plugin list and push `packages.changed` (another plugin
|
|
2734
|
-
* started, stopped, was installed or removed).
|
|
2735
|
-
*
|
|
2736
|
-
* @capability None.
|
|
2737
|
-
*
|
|
2738
|
-
* @example Simulate another plugin starting
|
|
2739
|
-
* The plugin's store window hears that a plugin is running now. The list
|
|
2740
|
-
* replaces `packages` and is pushed as `packages.changed`, which the
|
|
2741
|
-
* plugin's `packages.onChange` listeners receive.
|
|
2742
|
-
* ```ts
|
|
2743
|
-
* declare const host: TestHost;
|
|
2744
|
-
* // ---cut---
|
|
2745
|
-
* host.setPackages([...host.packages]); // edit a copy, then push it
|
|
2746
|
-
* await host.settle();
|
|
2747
|
-
* ```
|
|
2748
|
-
*
|
|
2749
|
-
* @since 1.0.0
|
|
2750
|
-
*/
|
|
2751
|
-
setPackages(list: TestHost["packages"]): void;
|
|
2752
|
-
/**
|
|
2753
|
-
* Simulate another plugin calling `player.next()` / `previous()`: the host
|
|
2754
|
-
* asks this plugin's sequence provider. Rejects with `unavailable` when
|
|
2755
|
-
* there is no provider or its flag is false.
|
|
2756
|
-
*
|
|
2757
|
-
* @capability None.
|
|
2758
|
-
* @throws `unavailable` (rejects) A `HostError`: there is no sequence
|
|
2759
|
-
* provider, or its flag is false.
|
|
2760
|
-
*
|
|
2761
|
-
* @example Ask the plugin's provider for the next item
|
|
2762
|
-
* Another plugin calls `player.next()`; the host asks the plugin under
|
|
2763
|
-
* test, which is the sequence provider, and the test checks what it
|
|
2764
|
-
* opened.
|
|
2765
|
-
* ```ts
|
|
2766
|
-
* import assert from "node:assert/strict";
|
|
2767
|
-
* declare const host: TestHost;
|
|
2768
|
-
* // ---cut---
|
|
2769
|
-
* await host.step("next");
|
|
2770
|
-
* await host.settle();
|
|
2771
|
-
* assert.equal(host.source.source, "/media/b.mp4");
|
|
2772
|
-
* ```
|
|
2773
|
-
*
|
|
2774
|
-
* @since 1.0.0
|
|
2775
|
-
*/
|
|
2776
|
-
step(direction: "next" | "previous"): Promise<unknown>;
|
|
2777
|
-
/**
|
|
2778
|
-
* The plugin's command with this local id. Throws when it is not
|
|
2779
|
-
* registered (never added, or disposed).
|
|
2780
|
-
*
|
|
2781
|
-
* @capability None.
|
|
2782
|
-
*
|
|
2783
|
-
* @example Read a command's state after a change
|
|
2784
|
-
* The test checks that Export becomes enabled once a file is ready. The
|
|
2785
|
-
* record is updated as the plugin sends new state; read it after
|
|
2786
|
-
* `settle()`.
|
|
2787
|
-
* ```ts
|
|
2788
|
-
* import assert from "node:assert/strict";
|
|
2789
|
-
* declare const host: TestHost;
|
|
2790
|
-
* // ---cut---
|
|
2791
|
-
* host.setSource({ phase: "ready", duration: 60 });
|
|
2792
|
-
* await host.settle();
|
|
2793
|
-
* assert.equal(host.command("export").state.enabled, true); // throws if not registered
|
|
2794
|
-
* ```
|
|
2795
|
-
*
|
|
2796
|
-
* @since 1.0.0
|
|
2797
|
-
*/
|
|
2798
|
-
command(id: string): FakeCommand;
|
|
2799
|
-
/**
|
|
2800
|
-
* The plugin's bottom-bar buttons (`CommandOptions.button`) as the player
|
|
2801
|
-
* shows them: at most one, the first declared (`BAR_BUTTON_LIMITS`; a
|
|
2802
|
-
* later one stays a menu item only). Empty once the command is disposed.
|
|
2803
|
-
* Whether one is in the strip or its » menu is the player's business
|
|
2804
|
-
* (the room and the other plugins' buttons).
|
|
2805
|
-
*
|
|
2806
|
-
* @example Click the plugin's bottom-bar button in a test
|
|
2807
|
-
* The test finds the plugin's Store button, clicks it and checks that the
|
|
2808
|
-
* Store window opened. `barButtons` lists the buttons of the plugin's
|
|
2809
|
-
* commands declared with `button`, as the bar shows them; `click()` runs
|
|
2810
|
-
* the command like its menu item and waits until the host is idle, and
|
|
2811
|
-
* rejects while the command is disabled or busy.
|
|
2812
|
-
* ```ts
|
|
2813
|
-
* import assert from "node:assert/strict";
|
|
2814
|
-
* declare const host: TestHost;
|
|
2815
|
-
* // ---cut---
|
|
2816
|
-
* const [button] = host.barButtons; // one button per plugin
|
|
2817
|
-
* assert.equal(button?.icon, "store");
|
|
2818
|
-
* await button?.click();
|
|
2819
|
-
* assert.equal(host.window("store").visible, true);
|
|
2820
|
-
* ```
|
|
2821
|
-
*
|
|
2822
|
-
* @since 1.5.29
|
|
2823
|
-
*/
|
|
2824
|
-
get barButtons(): readonly FakeBarButton[];
|
|
2825
|
-
/**
|
|
2826
|
-
* The plugin's tool window with this id (by default the file name without
|
|
2827
|
-
* extension: `ui/picture.slint` → `picture`). Throws when it was never
|
|
2828
|
-
* opened with `show()`.
|
|
2829
|
-
*
|
|
2830
|
-
* @example Press a window's Reset button from a test
|
|
2831
|
-
* The Picture window's Reset callback runs and the brightness returns to
|
|
2832
|
-
* 0. The window is found by its id (the `.slint` file name by default);
|
|
2833
|
-
* `emit` fires the callback the plugin bound, and `settle()` waits for
|
|
2834
|
-
* the property the handler wrote.
|
|
2835
|
-
* ```ts
|
|
2836
|
-
* import assert from "node:assert/strict";
|
|
2837
|
-
* declare const host: TestHost;
|
|
2838
|
-
* // ---cut---
|
|
2839
|
-
* host.window("picture").emit("reset");
|
|
2840
|
-
* await host.settle();
|
|
2841
|
-
* assert.equal(host.window("picture").props["brightness"], 0);
|
|
2842
|
-
* ```
|
|
2843
|
-
*
|
|
2844
|
-
* @capability None.
|
|
2845
|
-
* @since 1.0.0
|
|
2846
|
-
*/
|
|
2847
|
-
window(id: string): FakeWindow;
|
|
2848
|
-
/**
|
|
2849
|
-
* The plugin's mounted panel (bottom bar, sidebar, adjust column or
|
|
2850
|
-
* backdrop): the one in `slot`, or without it the only one (SDK 1.6.15: a
|
|
2851
|
-
* plugin mounts one panel per slot; with several, name the slot). Throws
|
|
2852
|
-
* when there is none. Its `props` use Slint (kebab-case) names; `emit`
|
|
2853
|
-
* fires a callback on the panel's global.
|
|
2854
|
-
*
|
|
2855
|
-
* @example Select a row in the plugin's sidebar panel
|
|
2856
|
-
* The test checks the panel sits in the sidebar and selects its third
|
|
2857
|
-
* row. `emit` fires a callback on the panel's global as a click would;
|
|
2858
|
-
* the plugin's handler runs, and `settle()` waits for what it sent.
|
|
2859
|
-
* ```ts
|
|
2860
|
-
* import assert from "node:assert/strict";
|
|
2861
|
-
* declare const host: TestHost;
|
|
2862
|
-
* // ---cut---
|
|
2863
|
-
* const panel = host.mounted();
|
|
2864
|
-
* assert.equal(panel.slot, "sidebar");
|
|
2865
|
-
* panel.emit("select", 2);
|
|
2866
|
-
* await host.settle();
|
|
2867
|
-
* ```
|
|
2868
|
-
*
|
|
2869
|
-
* @capability None.
|
|
2870
|
-
* @since 1.0.0
|
|
2871
|
-
*/
|
|
2872
|
-
mounted(slot?: "bottom" | "sidebar" | "adjust" | "backdrop"): FakePanel;
|
|
2873
|
-
/**
|
|
2874
|
-
* The parameters of every request of one wire method, oldest first.
|
|
2875
|
-
*
|
|
2876
|
-
* @example Check the request behind an OSD message
|
|
2877
|
-
* The test checks the last OSD request the plugin sent, before
|
|
2878
|
-
* translation. `sent` lists the parameters of every request of one wire
|
|
2879
|
-
* method, oldest first; `host.osd` holds the texts as shown instead.
|
|
2880
|
-
* ```ts
|
|
2881
|
-
* import assert from "node:assert/strict";
|
|
2882
|
-
* declare const host: TestHost;
|
|
2883
|
-
* // ---cut---
|
|
2884
|
-
* assert.deepEqual(host.sent("osd.show").at(-1), { text: "Saved" });
|
|
2885
|
-
* ```
|
|
2886
|
-
*
|
|
2887
|
-
* @capability None.
|
|
2888
|
-
* @since 1.0.0
|
|
2889
|
-
*/
|
|
2890
|
-
sent(method: string): Record<string, Json>[];
|
|
2891
|
-
/**
|
|
2892
|
-
* Wait until no request is in flight (and the SDK's follow-up passes ran),
|
|
2893
|
-
* and at least `ms` milliseconds have passed so that timers due by then
|
|
2894
|
-
* fired. Call it after every action that makes the plugin talk to the host,
|
|
2895
|
-
* before asserting.
|
|
2896
|
-
*
|
|
2897
|
-
* It only waits for host calls. Work the plugin does on its own (reading
|
|
2898
|
-
* or parsing a file, a child process, a network request) is not waited
|
|
2899
|
-
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
2900
|
-
* the result you assert on instead (second example).
|
|
2901
|
-
*
|
|
2902
|
-
* `settle()` does not wait for the SDK's once-a-second full update pass
|
|
2903
|
-
* (SDK 1.5.45). After `setSource` (or another player state change) only
|
|
2904
|
-
* the bindings that read a changed field of `plugin.player.state` run
|
|
2905
|
-
* again; state the plugin changed outside a handler without
|
|
2906
|
-
* `plugin.update()` shows only in the next full pass. Have the plugin
|
|
2907
|
-
* call `plugin.update()` after such a change, or wait `settle(1000)`.
|
|
2908
|
-
*
|
|
2909
|
-
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
2910
|
-
*
|
|
2911
|
-
* @example Wait past a debounced save
|
|
2912
|
-
* The plugin's store writes its file 300 ms after the last `save`, so
|
|
2913
|
-
* the test waits 400 ms before it checks what the plugin did next.
|
|
2914
|
-
* `settle(ms)` returns when no request is in flight and at least `ms`
|
|
2915
|
-
* have passed, so the store's timer has fired. The file write itself is
|
|
2916
|
-
* the plugin's own work, which `settle` does not wait for: a test that
|
|
2917
|
-
* reads the file calls `await store.flush()` instead.
|
|
2918
|
-
* ```ts
|
|
2919
|
-
* declare const host: TestHost;
|
|
2920
|
-
* // ---cut---
|
|
2921
|
-
* const store = plugin.storage.json("prefs", { defaults: { step: 5 } });
|
|
2922
|
-
* store.save({ step: 10 });
|
|
2923
|
-
* await host.settle(400); // past storage.json's 300 ms debounce
|
|
2924
|
-
* ```
|
|
2925
|
-
*
|
|
2926
|
-
* @example Poll for work the plugin does on its own
|
|
2927
|
-
* The plugin reads and parses a file before it shows a status; `settle`
|
|
2928
|
-
* does not wait for that (it only waits for host requests), so the test
|
|
2929
|
-
* polls the property it asserts on, 20 ms at a time, for up to 2 seconds.
|
|
2930
|
-
* ```ts
|
|
2931
|
-
* import assert from "node:assert/strict";
|
|
2932
|
-
* declare const host: TestHost;
|
|
2933
|
-
* declare const expected: string;
|
|
2934
|
-
* // ---cut---
|
|
2935
|
-
* const importWindow = host.window("import");
|
|
2936
|
-
* importWindow.emit("load-file"); // the plugin reads and parses the file itself
|
|
2937
|
-
* for (let i = 0; i < 100 && importWindow.props["status-text"] !== expected; i++)
|
|
2938
|
-
* await host.settle(20);
|
|
2939
|
-
* assert.equal(importWindow.props["status-text"], expected);
|
|
2940
|
-
* ```
|
|
2941
|
-
*
|
|
2942
|
-
* @capability None.
|
|
2943
|
-
* @throws Never.
|
|
2944
|
-
* @since 1.0.0
|
|
2945
|
-
*/
|
|
2946
|
-
settle(ms?: number): Promise<void>;
|
|
2947
|
-
/**
|
|
2948
|
-
* Close the fake stream servers and delete the host's temporary
|
|
2949
|
-
* directories. `stop()` from {@link startPlugin} calls it.
|
|
2950
|
-
*
|
|
2951
|
-
* @capability None.
|
|
2952
|
-
*
|
|
2953
|
-
* @example Clean up a host that was never started
|
|
2954
|
-
* A test that made a host but failed before `startPlugin` removes the
|
|
2955
|
-
* host's temporary directories itself. `stop()` from `startPlugin` calls
|
|
2956
|
-
* `close` for you otherwise.
|
|
2957
|
-
* ```ts
|
|
2958
|
-
* const host = new TestHost();
|
|
2959
|
-
* try {
|
|
2960
|
-
* // … preset the host; the test fails here …
|
|
2961
|
-
* } finally {
|
|
2962
|
-
* host.close(); // deletes the host's temporary data
|
|
2963
|
-
* }
|
|
2964
|
-
* ```
|
|
2965
|
-
*
|
|
2966
|
-
* @since 1.0.0
|
|
2967
|
-
*/
|
|
2968
|
-
close(): void;
|
|
2969
|
-
/**
|
|
2970
|
-
* Why the player would keep this plugin running while nothing uses it
|
|
2971
|
-
* (lazy activation, player 0.5.21), one line per reason: an event it
|
|
2972
|
-
* listens to that is not one the player wakes it for (with what listens
|
|
2973
|
-
* and where it was added), its panel in the bottom bar or the backdrop,
|
|
2974
|
-
* its open windows (these as the player's log says them), and, from
|
|
2975
|
-
* player 0.7.3, its sidebar tab while that is the current tab. The
|
|
2976
|
-
* player counts the current tab as busy work, not as something live: it
|
|
2977
|
-
* keeps the plugin running only while the tab is shown, and a sidebar
|
|
2978
|
-
* panel in a background tab is no reason at all (the plugin may go to
|
|
2979
|
-
* standby; showing the tab starts it again). Empty: the player may stop
|
|
2980
|
-
* it while idle and start it again on its next use. Filter chains,
|
|
2981
|
-
* streams, jobs and the like (which also keep a plugin running) are not
|
|
2982
|
-
* listed.
|
|
2983
|
-
*
|
|
2984
|
-
* @capability None.
|
|
2985
|
-
* @since 1.5.40
|
|
2986
|
-
*
|
|
2987
|
-
* @example Check that a plugin can be stopped while idle
|
|
2988
|
-
* The test fails if the plugin keeps running for no reason after it
|
|
2989
|
-
* starts, and names the reason (a command whose `enabled` reads
|
|
2990
|
-
* `plugin.player.state`, say: give it a `plugin.when` condition). The
|
|
2991
|
-
* player stops such a plugin while nothing uses it and starts it again
|
|
2992
|
-
* on its next use, so it costs no process meanwhile.
|
|
2993
|
-
* ```ts
|
|
2994
|
-
* import assert from "node:assert/strict";
|
|
2995
|
-
* declare const host: TestHost;
|
|
2996
|
-
* // ---cut---
|
|
2997
|
-
* assert.deepEqual(host.keepsRunning, []);
|
|
2998
|
-
* ```
|
|
2999
|
-
*/
|
|
3000
|
-
get keepsRunning(): string[];
|
|
3001
|
-
}
|
|
3002
|
-
/**
|
|
3003
|
-
* Activate a plugin against a test host, the way the player's runner does:
|
|
3004
|
-
* build the `plugin` object, call `activate(plugin)`, wait for every
|
|
3005
|
-
* registration started during activation (commands, panels, resolvers, ...),
|
|
3006
|
-
* then wait until the host is idle.
|
|
3007
|
-
*
|
|
3008
|
-
* The manifest id and capabilities come from `directory/manifest.json`
|
|
3009
|
-
* unless `capabilities` is given; then `id` (default `"test.plugin"`) and
|
|
3010
|
-
* `capabilities` are used and no manifest is read. Calls that need an
|
|
3011
|
-
* undeclared capability fail with `capability`, as in the player.
|
|
3012
|
-
*
|
|
3013
|
-
* Rejects when `manifest.json` cannot be read or parsed, when `activate`
|
|
3014
|
-
* throws, or when a registration made during activation fails (for example
|
|
3015
|
-
* `limit`, `conflict`, `invalid`). Stricter than the player's runner for
|
|
3016
|
-
* commands (SDK 1.5.49): there the SDK logs a command declaration the
|
|
3017
|
-
* player refuses during activation, skips it and activates the rest of the
|
|
3018
|
-
* plugin (`onError` hears it); `startPlugin` fails, so a test shows the
|
|
3019
|
-
* bad declaration.
|
|
3020
|
-
*
|
|
3021
|
-
* @param activate - The plugin's default-exported `activate` function.
|
|
3022
|
-
* @param options - Where the plugin lives and how to start it.
|
|
3023
|
-
* @returns `host`: the {@link TestHost}; `plugin`: the object passed to
|
|
3024
|
-
* `activate` (call the plugin API on it directly); `stop()`: dispose the
|
|
3025
|
-
* plugin like the player stopping it (scopes released, stores flushed),
|
|
3026
|
-
* wait until the host is idle and remove the host's temporary files;
|
|
3027
|
-
* `stop({ reason })` (SDK 1.5.54): `reason` is the `StopReason` the player
|
|
3028
|
-
* sends; `disconnect()` (SDK 1.5.54) stops it as when the player is lost
|
|
3029
|
-
* (`host-lost`, requests refused with `disabled`). Removing the
|
|
3030
|
-
* temporary files never fails a test.
|
|
3031
|
-
*
|
|
3032
|
-
* @example Test that a setting survives a restart
|
|
3033
|
-
* The test turns on the 10-step option, stops the plugin, starts it again
|
|
3034
|
-
* on the same data directory and checks the option is still on. Both runs
|
|
3035
|
-
* get a directory the test made (`dataDirectory`): `stop()` disposes the
|
|
3036
|
-
* plugin like the player, which writes its pending stores there, and only
|
|
3037
|
-
* removes the host's own temporary directory, so the second run reads what
|
|
3038
|
-
* the first one saved.
|
|
3039
|
-
* ```ts
|
|
3040
|
-
* import assert from "node:assert/strict";
|
|
3041
|
-
* import { mkdtempSync, rmSync } from "node:fs";
|
|
3042
|
-
* import { tmpdir } from "node:os";
|
|
3043
|
-
* import { join } from "node:path";
|
|
3044
|
-
* import test from "node:test";
|
|
3045
|
-
* import { startPlugin } from "@vestra-player/sdk/testing";
|
|
3046
|
-
* import activate from "../main.ts";
|
|
3047
|
-
*
|
|
3048
|
-
* test("remembers the step across restarts", async () => {
|
|
3049
|
-
* const directory = new URL("..", import.meta.url);
|
|
3050
|
-
* const data = mkdtempSync(join(tmpdir(), "data-"));
|
|
3051
|
-
* try {
|
|
3052
|
-
* const first = await startPlugin(activate, { directory, dataDirectory: data });
|
|
3053
|
-
* try {
|
|
3054
|
-
* await first.host.command("step-10").run();
|
|
3055
|
-
* } finally {
|
|
3056
|
-
* await first.stop(); // flushes the plugin's stores into `data`
|
|
3057
|
-
* }
|
|
3058
|
-
*
|
|
3059
|
-
* // A restart: a new host and a new plugin instance on the same data.
|
|
3060
|
-
* const second = await startPlugin(activate, { directory, dataDirectory: data });
|
|
3061
|
-
* try {
|
|
3062
|
-
* assert.equal(second.host.command("step-10").state.checked, true);
|
|
3063
|
-
* } finally {
|
|
3064
|
-
* await second.stop();
|
|
3065
|
-
* }
|
|
3066
|
-
* } finally {
|
|
3067
|
-
* rmSync(data, { recursive: true, force: true });
|
|
3068
|
-
* }
|
|
3069
|
-
* });
|
|
3070
|
-
* ```
|
|
3071
|
-
*
|
|
3072
|
-
* @remarks
|
|
3073
|
-
* The way into a plugin's tests: one call starts the plugin the way the
|
|
3074
|
-
* player's runner does and returns the host to drive, the `plugin` object
|
|
3075
|
-
* to call directly, and `stop` to end it like the player.
|
|
3076
|
-
*
|
|
3077
|
-
* @capability None.
|
|
3078
|
-
* @see [Test a plugin](../../docs/how-to/testing.md)
|
|
3079
|
-
* @since 1.0.0
|
|
3080
|
-
* @category Testing
|
|
3081
|
-
*/
|
|
3082
|
-
export declare function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
|
|
3083
|
-
/**
|
|
3084
|
-
* The plugin's package directory, as a path or a `file:` URL (e.g.
|
|
3085
|
-
* `new URL("..", import.meta.url)` from `tests/`). Relative `.slint`
|
|
3086
|
-
* files resolve against it. Default: the current working directory.
|
|
3087
|
-
*/
|
|
3088
|
-
directory?: string | URL;
|
|
3089
|
-
/** The manifest id when `capabilities` is given. Default `"test.plugin"`. */
|
|
3090
|
-
id?: string;
|
|
3091
|
-
/** Use these capabilities instead of reading `manifest.json`. */
|
|
3092
|
-
capabilities?: string[];
|
|
3093
|
-
/** A host to use (to preset data before activation). Default: a new one. */
|
|
3094
|
-
host?: TestHost;
|
|
3095
|
-
/** Use an existing directory as `plugin.dataDirectory` (to test a restart). */
|
|
3096
|
-
dataDirectory?: string;
|
|
3097
|
-
/**
|
|
3098
|
-
* Start with these `plugin.storage.session` values (to test a restart
|
|
3099
|
-
* within one player session): pass the previous run's
|
|
3100
|
-
* `host.sessionStorage`. They are copied into the new host's
|
|
3101
|
-
* {@link TestHost.sessionStorage}.
|
|
3102
|
-
*
|
|
3103
|
-
* @since 1.5.25
|
|
3104
|
-
*/
|
|
3105
|
-
sessionStorage?: ReadonlyMap<string, Json>;
|
|
3106
|
-
/**
|
|
3107
|
-
* The interface language to play ({@link TestHost.locale}): the plugin's
|
|
3108
|
-
* `locales/<locale>.po` (or `.json`) in `directory` is loaded and its
|
|
3109
|
-
* menus, messages and titles show translated. Default: the host's.
|
|
3110
|
-
*
|
|
3111
|
-
* @since 1.5.9
|
|
3112
|
-
*/
|
|
3113
|
-
locale?: Locale;
|
|
3114
|
-
}): Promise<{
|
|
3115
|
-
/** The test host the plugin talks to. */
|
|
3116
|
-
host: TestHost;
|
|
3117
|
-
/** The `plugin` object passed to `activate`. */
|
|
3118
|
-
plugin: Plugin;
|
|
3119
|
-
/**
|
|
3120
|
-
* Stop the plugin, wait for the host, and remove temporary files.
|
|
3121
|
-
* `stop({ reason })` (SDK 1.5.54): `reason` is the reason the player
|
|
3122
|
-
* gives (`plugin.stopReason`, the `onDispose` cleanups'
|
|
3123
|
-
* `info.reason`); `stop()`: none (`undefined`), as with players up to
|
|
3124
|
-
* 0.6.3. It still takes no declared parameter, so it can be passed as
|
|
3125
|
-
* a callback (`t.after(stop)`, `promise.then(stop)`).
|
|
3126
|
-
*/
|
|
3127
|
-
stop: {
|
|
3128
|
-
(): Promise<void>;
|
|
3129
|
-
(options: {
|
|
3130
|
-
reason?: StopReason;
|
|
3131
|
-
}): Promise<void>;
|
|
3132
|
-
};
|
|
3133
|
-
/**
|
|
3134
|
-
* Lose the player (SDK 1.5.54): as when it crashes or is killed, the
|
|
3135
|
-
* runner stops the plugin with reason `host-lost`; requests still
|
|
3136
|
-
* waiting for their answer and every later request are refused with
|
|
3137
|
-
* `disabled`. Then waits and removes the temporary files like `stop`.
|
|
3138
|
-
*/
|
|
3139
|
-
disconnect(): Promise<void>;
|
|
3140
|
-
}>;
|
|
1
|
+
export * from "./testing/index.ts";
|