@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/docs-tree.json.gz +0 -0
  3. package/dist/plugin/appearance.js +1 -1
  4. package/dist/plugin/bindings.js +1 -1
  5. package/dist/plugin/commands.js +3 -2
  6. package/dist/plugin/connection.d.ts +119 -0
  7. package/dist/plugin/connection.js +402 -0
  8. package/dist/plugin/drops.d.ts +3 -3
  9. package/dist/plugin/drops.js +1 -1
  10. package/dist/plugin/errors.d.ts +9 -0
  11. package/dist/plugin/errors.js +35 -0
  12. package/dist/plugin/filters.js +1 -1
  13. package/dist/plugin/index.d.ts +1 -1
  14. package/dist/plugin/index.js +3 -1
  15. package/dist/plugin/language.js +1 -1
  16. package/dist/plugin/latest.d.ts +12 -0
  17. package/dist/plugin/latest.js +52 -0
  18. package/dist/plugin/local.js +1 -2
  19. package/dist/plugin/markdown.d.ts +2 -2
  20. package/dist/plugin/markdown.js +1 -1
  21. package/dist/plugin/media.js +1 -1
  22. package/dist/plugin/osd.js +2 -2
  23. package/dist/plugin/packages.js +1 -1
  24. package/dist/plugin/passes.d.ts +111 -0
  25. package/dist/plugin/passes.js +341 -0
  26. package/dist/plugin/player.d.ts +2 -3
  27. package/dist/plugin/player.js +2 -7
  28. package/dist/plugin/resolvers.js +1 -1
  29. package/dist/plugin/runtime.d.ts +26 -185
  30. package/dist/plugin/runtime.js +35 -755
  31. package/dist/plugin/scope.d.ts +2 -1
  32. package/dist/plugin/scope.js +5 -2
  33. package/dist/plugin/services/dialogs.d.ts +3 -0
  34. package/dist/plugin/services/dialogs.js +62 -0
  35. package/dist/plugin/services.js +1 -1
  36. package/dist/plugin/settings.js +2 -2
  37. package/dist/plugin/shaders.js +2 -1
  38. package/dist/plugin/stream.js +1 -2
  39. package/dist/plugin/subtitles.js +2 -2
  40. package/dist/plugin/testing/commands.d.ts +65 -0
  41. package/dist/plugin/testing/commands.js +412 -0
  42. package/dist/plugin/testing/core-actions.d.ts +39 -0
  43. package/dist/plugin/testing/core-actions.js +371 -0
  44. package/dist/plugin/testing/declarations.d.ts +37 -0
  45. package/dist/plugin/testing/declarations.js +80 -0
  46. package/dist/plugin/testing/i18n.d.ts +28 -0
  47. package/dist/plugin/testing/i18n.js +107 -0
  48. package/dist/plugin/testing/index.d.ts +3140 -0
  49. package/dist/plugin/testing/index.js +2796 -0
  50. package/dist/plugin/testing/internals.d.ts +76 -0
  51. package/dist/plugin/testing/internals.js +60 -0
  52. package/dist/plugin/testing/jobs.d.ts +9 -0
  53. package/dist/plugin/testing/jobs.js +304 -0
  54. package/dist/plugin/testing/layout.d.ts +85 -0
  55. package/dist/plugin/testing/layout.js +622 -0
  56. package/dist/plugin/testing/media.d.ts +55 -0
  57. package/dist/plugin/testing/media.js +615 -0
  58. package/dist/plugin/testing/osd.d.ts +4 -0
  59. package/dist/plugin/testing/osd.js +57 -0
  60. package/dist/plugin/testing/processing.d.ts +56 -0
  61. package/dist/plugin/testing/processing.js +491 -0
  62. package/dist/plugin/testing/services.d.ts +50 -0
  63. package/dist/plugin/testing/services.js +466 -0
  64. package/dist/plugin/testing/transport.d.ts +29 -0
  65. package/dist/plugin/testing/transport.js +140 -0
  66. package/dist/plugin/testing.d.ts +1 -3140
  67. package/dist/plugin/testing.js +1 -6203
  68. package/dist/plugin/timeline.js +2 -1
  69. package/dist/plugin/view/base.d.ts +29 -0
  70. package/dist/plugin/view/base.js +88 -0
  71. package/dist/plugin/view/binder.d.ts +22 -0
  72. package/dist/plugin/view/binder.js +57 -0
  73. package/dist/plugin/view/image.d.ts +21 -0
  74. package/dist/plugin/view/image.js +60 -0
  75. package/dist/plugin/view/layout.d.ts +47 -0
  76. package/dist/plugin/view/layout.js +171 -0
  77. package/dist/plugin/view/panel.d.ts +3 -0
  78. package/dist/plugin/view/panel.js +178 -0
  79. package/dist/plugin/view/window.d.ts +3 -0
  80. package/dist/plugin/view/window.js +99 -0
  81. package/dist/plugin/view/writer.d.ts +58 -0
  82. package/dist/plugin/view/writer.js +634 -0
  83. package/dist/plugin/view.js +2 -1
  84. package/dist/plugin/window.js +1 -1
  85. package/llms.txt +1 -1
  86. package/package.json +6 -4
  87. package/dist/plugin/ui.d.ts +0 -74
  88. package/dist/plugin/ui.js +0 -1234
@@ -1,3140 +1 @@
1
- import type { Server } from "node:net";
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";