@vestra-player/sdk 1.5.28 → 1.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/bin/vestra-plugin.mjs +103 -0
- package/dist/capabilities.d.ts +13 -3
- package/dist/capabilities.js +13 -3
- package/dist/commands.d.ts +94 -8
- package/dist/commands.js +124 -7
- package/dist/index.d.ts +6 -1
- package/dist/palettes.d.ts +16 -2
- package/dist/palettes.js +16 -2
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/bindings.d.ts +27 -3
- package/dist/plugin/bindings.js +27 -3
- package/dist/plugin/commands.js +29 -2
- package/dist/plugin/osd.d.ts +13 -1
- package/dist/plugin/osd.js +13 -1
- package/dist/plugin/services.d.ts +46 -6
- package/dist/plugin/services.js +46 -6
- package/dist/plugin/testing.d.ts +403 -61
- package/dist/plugin/testing.js +408 -60
- package/dist/plugin/types.d.ts +860 -162
- package/dist/plugin/values.d.ts +27 -3
- package/dist/plugin/values.js +27 -3
- package/dist/processing.d.ts +16 -4
- package/dist/source-resolvers.d.ts +33 -8
- package/dist/subtitles.d.ts +19 -9
- package/dist/wire.d.ts +12 -3
- package/dist/wire.js +12 -3
- package/llms-full.txt +3503 -25
- package/llms.txt +26 -26
- package/package.json +1 -1
- package/slint/vestra/bar-buttons.slint +138 -0
- package/slint/vestra/icons.slint +4 -0
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Server } from "node:net";
|
|
|
2
2
|
import { HostError } from "../wire.ts";
|
|
3
3
|
import type { PackageManifest, PackageSource } from "./types.ts";
|
|
4
4
|
import type { Json, LayoutSlot, LocalServices, PlaybackStats, Transport, WireSystemInfo } from "../wire.ts";
|
|
5
|
-
import { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS } from "../commands.ts";
|
|
5
|
+
import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS } from "../commands.ts";
|
|
6
6
|
import type { BuiltinCommand, BuiltinSubmenu } from "../commands.ts";
|
|
7
7
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.ts";
|
|
8
8
|
import type { CapabilityInfo } from "../capabilities.ts";
|
|
@@ -22,6 +22,8 @@ import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
|
|
|
22
22
|
export { HostError };
|
|
23
23
|
/** The player's command caps, which the test host enforces as well. */
|
|
24
24
|
export { COMMAND_LIMITS, MENU_IDS };
|
|
25
|
+
/** The icons and limits of bottom-bar buttons, which the test host checks as well. */
|
|
26
|
+
export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS };
|
|
25
27
|
/** The submenus of the player's built-in plugins, by key path. */
|
|
26
28
|
export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS };
|
|
27
29
|
export type { BuiltinCommand, BuiltinSubmenu };
|
|
@@ -62,14 +64,27 @@ export { LIST_SEPARATORS };
|
|
|
62
64
|
* The player's interface languages, English (the source) first, as the
|
|
63
65
|
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
64
66
|
*
|
|
65
|
-
* @example
|
|
67
|
+
* @example Check that every language translates a menu item
|
|
68
|
+
* The test fails for each interface language in which the "Open…" item
|
|
69
|
+
* still shows English. It starts the plugin once per language except
|
|
70
|
+
* English (`LOCALES[0]`): the `locale` option loads the plugin's
|
|
71
|
+
* `locales/<locale>.po` and the host shows menu texts translated with it,
|
|
72
|
+
* which `display` reports.
|
|
66
73
|
* ```ts
|
|
67
|
-
* import {
|
|
74
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
75
|
+
* declare function activate(plugin: Plugin): Promise<void>;
|
|
76
|
+
* // ---cut---
|
|
77
|
+
* import assert from "node:assert/strict";
|
|
78
|
+
* import { LOCALES, startPlugin } from "@vestra-player/sdk/testing";
|
|
68
79
|
*
|
|
80
|
+
* const directory = new URL("..", import.meta.url);
|
|
69
81
|
* for (const locale of LOCALES.slice(1)) {
|
|
70
82
|
* const { host, stop } = await startPlugin(activate, { directory, locale });
|
|
71
|
-
*
|
|
72
|
-
*
|
|
83
|
+
* try {
|
|
84
|
+
* assert.notEqual(host.command("open").display.title, "Open…", locale);
|
|
85
|
+
* } finally {
|
|
86
|
+
* await stop(); // also when the assertion fails
|
|
87
|
+
* }
|
|
73
88
|
* }
|
|
74
89
|
* ```
|
|
75
90
|
*/
|
|
@@ -84,8 +99,16 @@ type Call = {
|
|
|
84
99
|
* `plugin.windows.create(...)` window after its first `show()`. Get one with
|
|
85
100
|
* {@link TestHost.window}.
|
|
86
101
|
*
|
|
87
|
-
* @example
|
|
102
|
+
* @example Change a setting in a tool window from a test
|
|
103
|
+
* The test opens the plugin's Picture window, moves its brightness control
|
|
104
|
+
* and checks that the plugin wrote the value back. `emit` fires a Slint
|
|
105
|
+
* callback as if the user used the control; the plugin's `on` handler runs
|
|
106
|
+
* and sets properties, which reach the host as requests, so `settle()`
|
|
107
|
+
* waits for them before `props` is read.
|
|
88
108
|
* ```ts
|
|
109
|
+
* import assert from "node:assert/strict";
|
|
110
|
+
* declare const host: TestHost;
|
|
111
|
+
* // ---cut---
|
|
89
112
|
* await host.command("open-picture").run(); // the plugin calls window.show()
|
|
90
113
|
* const picture = host.window("picture");
|
|
91
114
|
* assert.equal(picture.visible, true);
|
|
@@ -138,8 +161,16 @@ export interface FileDialogText {
|
|
|
138
161
|
* host keeps it. Get one with {@link TestHost.command} (the plugin's own) or
|
|
139
162
|
* {@link TestHost.coreAction} (the player's core actions).
|
|
140
163
|
*
|
|
141
|
-
* @example
|
|
164
|
+
* @example Run a checkable menu item from a test
|
|
165
|
+
* The test checks the Loop item's menu place, picks it and sees its check
|
|
166
|
+
* mark turn on. `host.command(id)` is the host's record of what the plugin
|
|
167
|
+
* declared and the state it last sent; `run()` calls the plugin's `run`
|
|
168
|
+
* like the menu, after which the SDK re-evaluates the command's state and
|
|
169
|
+
* sends it, and `run()` resolves once the host is idle again.
|
|
142
170
|
* ```ts
|
|
171
|
+
* import assert from "node:assert/strict";
|
|
172
|
+
* declare const host: TestHost;
|
|
173
|
+
* // ---cut---
|
|
143
174
|
* const toggle = host.command("toggle-loop");
|
|
144
175
|
* assert.deepEqual(toggle.declaration.path, ["Playback", "Loop"]);
|
|
145
176
|
* assert.equal(toggle.state.checked, false);
|
|
@@ -199,6 +230,37 @@ export interface FakeCommand {
|
|
|
199
230
|
*/
|
|
200
231
|
run(): Promise<void>;
|
|
201
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* A command's bottom-bar button (`CommandOptions.button`) as the player shows
|
|
235
|
+
* it: an entry of {@link TestHost.barButtons}.
|
|
236
|
+
*
|
|
237
|
+
* @since 1.5.29
|
|
238
|
+
*/
|
|
239
|
+
export interface FakeBarButton {
|
|
240
|
+
/** The command's local id. */
|
|
241
|
+
readonly command: string;
|
|
242
|
+
/** The icon name, or the image's path for a `plugin.ui.image` icon. */
|
|
243
|
+
readonly icon: string;
|
|
244
|
+
/** `true` when the icon is the plugin's image. */
|
|
245
|
+
readonly image: boolean;
|
|
246
|
+
/** The declared `order`. */
|
|
247
|
+
readonly order: number;
|
|
248
|
+
/** Clickable: the command is enabled and not busy. */
|
|
249
|
+
readonly enabled: boolean;
|
|
250
|
+
/** Drawn in the accent colour: the command is checked. */
|
|
251
|
+
readonly checked: boolean;
|
|
252
|
+
/**
|
|
253
|
+
* The tooltip without the key: the command's shown title (trailing "…"
|
|
254
|
+
* removed), then, while it cannot run, its reason on a second line.
|
|
255
|
+
*/
|
|
256
|
+
readonly tooltip: string;
|
|
257
|
+
/**
|
|
258
|
+
* Click the button, then wait until the host is idle: runs the command
|
|
259
|
+
* as its menu item would. Rejects (with a {@link HostError}) while the
|
|
260
|
+
* button is disabled, and once the command is gone.
|
|
261
|
+
*/
|
|
262
|
+
click(): Promise<void>;
|
|
263
|
+
}
|
|
202
264
|
/**
|
|
203
265
|
* A generated stream (`plugin.player.openStream`) as the test host sees it:
|
|
204
266
|
* the fake player end of the frame connection. Assertion surface; see
|
|
@@ -274,7 +336,13 @@ export interface MarkdownWire {
|
|
|
274
336
|
* `calls`) use the protocol's shapes. They are fine for assertions but are
|
|
275
337
|
* not part of the plugin API: their shape may change in minor versions.
|
|
276
338
|
*
|
|
277
|
-
* @example
|
|
339
|
+
* @example Preset the player, then react to another plugin's file
|
|
340
|
+
* The test starts the plugin in a player with the dark theme, opens a file
|
|
341
|
+
* the way another plugin would, and checks the plugin's panel title. A host
|
|
342
|
+
* created first can be preset with data the plugin reads during
|
|
343
|
+
* activation; `startPlugin` then uses it. `openElsewhere` changes the
|
|
344
|
+
* source and broadcasts it, the plugin's listener updates its panel, and
|
|
345
|
+
* `settle()` waits for those requests before the assertion.
|
|
278
346
|
* ```ts
|
|
279
347
|
* import assert from "node:assert/strict";
|
|
280
348
|
* import test from "node:test";
|
|
@@ -333,8 +401,15 @@ export declare class TestHost implements Transport {
|
|
|
333
401
|
* is mounted there). You may change a slot directly and then call
|
|
334
402
|
* `host.emit("window.state", {})` to simulate a change made elsewhere.
|
|
335
403
|
*
|
|
336
|
-
* @example
|
|
404
|
+
* @example Hide the sidebar and check the player's layout
|
|
405
|
+
* With the plugin's panel in the sidebar, toggling the sidebar records
|
|
406
|
+
* that the user prefers it hidden. `toggle()` reads the slot from the
|
|
407
|
+
* player, asks for the opposite of its `preferred` flag and resolves once
|
|
408
|
+
* the player answered, so the host's `layout` already shows the new value.
|
|
337
409
|
* ```ts
|
|
410
|
+
* import assert from "node:assert/strict";
|
|
411
|
+
* declare const host: TestHost;
|
|
412
|
+
* // ---cut---
|
|
338
413
|
* assert.equal(host.layout.sidebar?.preferred, true);
|
|
339
414
|
* await plugin.panels.sidebar.toggle();
|
|
340
415
|
* assert.equal(host.layout.sidebar?.preferred, false);
|
|
@@ -375,8 +450,17 @@ export declare class TestHost implements Transport {
|
|
|
375
450
|
* players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
|
|
376
451
|
* passing; set `locale` to test what a user sees now.
|
|
377
452
|
*
|
|
378
|
-
* @example
|
|
453
|
+
* @example Check a menu path in Simplified Chinese
|
|
454
|
+
* The test starts the plugin with a Chinese interface and checks the
|
|
455
|
+
* menu path its Blur item shows. The `locale` option sets
|
|
456
|
+
* `host.locale` before activation, so the plugin's `locales/zh-Hans.po`
|
|
457
|
+
* is loaded; the built-in menu `@video` shows in the player's own words
|
|
458
|
+
* (视频), the item's title in the plugin's translation.
|
|
379
459
|
* ```ts
|
|
460
|
+
* import assert from "node:assert/strict";
|
|
461
|
+
* declare function activate(plugin: Plugin): Promise<void>;
|
|
462
|
+
* declare const directory: URL;
|
|
463
|
+
* // ---cut---
|
|
380
464
|
* const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
381
465
|
* assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
|
|
382
466
|
* await stop();
|
|
@@ -406,8 +490,14 @@ export declare class TestHost implements Transport {
|
|
|
406
490
|
* @param args - Values for its placeholders.
|
|
407
491
|
* @returns The shown text.
|
|
408
492
|
*
|
|
409
|
-
* @example
|
|
493
|
+
* @example Compare with a translated message
|
|
494
|
+
* The test expects the text a Chinese user sees for a template the plugin
|
|
495
|
+
* shows. `translate` looks the English source up in the plugin's catalog
|
|
496
|
+
* for `host.locale` and fills in the arguments, the way the player does.
|
|
410
497
|
* ```ts
|
|
498
|
+
* import assert from "node:assert/strict";
|
|
499
|
+
* declare const host: TestHost;
|
|
500
|
+
* // ---cut---
|
|
411
501
|
* assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
|
|
412
502
|
* ```
|
|
413
503
|
*
|
|
@@ -468,10 +558,16 @@ export declare class TestHost implements Transport {
|
|
|
468
558
|
* What `player.stats()` returns. Fields added in later rounds may be left
|
|
469
559
|
* out; the SDK fills them with `null`.
|
|
470
560
|
*
|
|
471
|
-
* @example
|
|
561
|
+
* @example Pretend a 1080p H.264 video is playing
|
|
562
|
+
* The plugin's `player.stats()` now reports a 1920×1080 H.264 stream at
|
|
563
|
+
* 24 fps. The host answers `stats()` with this object as it stands, so a
|
|
564
|
+
* test sets it before the plugin asks; fields it leaves out come back as
|
|
565
|
+
* `null` from the SDK.
|
|
472
566
|
* ```ts
|
|
567
|
+
* declare const host: TestHost;
|
|
568
|
+
* // ---cut---
|
|
473
569
|
* host.stats = {
|
|
474
|
-
* ...host.stats,
|
|
570
|
+
* ...host.stats, // keep the other fields
|
|
475
571
|
* video: { codec: "h264", width: 1920, height: 1080, fps: 24 },
|
|
476
572
|
* } as typeof host.stats;
|
|
477
573
|
* ```
|
|
@@ -505,12 +601,21 @@ export declare class TestHost implements Transport {
|
|
|
505
601
|
* handler is not found (`available()` is false, `run()` rejects with
|
|
506
602
|
* `unavailable`).
|
|
507
603
|
*
|
|
508
|
-
* @example
|
|
604
|
+
* @example Fake ffprobe and a failing ffmpeg
|
|
605
|
+
* The plugin's Probe command gets an empty stream list from ffprobe, and
|
|
606
|
+
* any ffmpeg run fails as it would on a broken file. `handle` makes the
|
|
607
|
+
* tool available and answers each `tools.run` with the handler's result;
|
|
608
|
+
* a thrown `HostError` makes the run reject with that code. `calls`
|
|
609
|
+
* records every run with its arguments.
|
|
509
610
|
* ```ts
|
|
611
|
+
* import assert from "node:assert/strict";
|
|
612
|
+
* declare const host: TestHost;
|
|
613
|
+
* // ---cut---
|
|
510
614
|
* host.tools.handle("ffprobe", (args) => {
|
|
511
|
-
* assert.ok(args.includes("-show_streams"));
|
|
512
|
-
* return { stdout: JSON.stringify({ streams: [] }) };
|
|
615
|
+
* assert.ok(args.includes("-show_streams")); // what the plugin asked for
|
|
616
|
+
* return { stdout: JSON.stringify({ streams: [] }) }; // a file with no streams
|
|
513
617
|
* });
|
|
618
|
+
* // Every ffmpeg run rejects with code `failed`.
|
|
514
619
|
* host.tools.handle("ffmpeg", () => {
|
|
515
620
|
* throw new HostError("failed", "Invalid data found when processing input");
|
|
516
621
|
* });
|
|
@@ -604,12 +709,20 @@ export declare class TestHost implements Transport {
|
|
|
604
709
|
* @param pluginId - The other plugin's manifest id.
|
|
605
710
|
* @param marks - Its whole layer.
|
|
606
711
|
*
|
|
607
|
-
* @example
|
|
712
|
+
* @example Let another plugin publish chapters
|
|
713
|
+
* The plugin under test sees two chapter marks another plugin put on the
|
|
714
|
+
* timeline. `setTimelineLayer` stores that plugin's whole layer and pushes
|
|
715
|
+
* `timeline.changed`; once the plugin uses `plugin.timeline` (with
|
|
716
|
+
* `events`) the SDK keeps `marks` current from that event, and `settle()`
|
|
717
|
+
* lets it arrive.
|
|
608
718
|
* ```ts
|
|
719
|
+
* import assert from "node:assert/strict";
|
|
720
|
+
* declare const host: TestHost;
|
|
721
|
+
* // ---cut---
|
|
609
722
|
* host.setTimelineLayer("community.chapters", [
|
|
610
723
|
* { time: 0, kind: "chapter", title: "Intro" },
|
|
611
724
|
* { time: 95, kind: "chapter", title: "Part 1" },
|
|
612
|
-
* ]);
|
|
725
|
+
* ]); // replaces that plugin's whole layer; [] would clear it
|
|
613
726
|
* await host.settle();
|
|
614
727
|
* assert.equal(plugin.timeline.marks.length, 2);
|
|
615
728
|
* ```
|
|
@@ -639,8 +752,15 @@ export declare class TestHost implements Transport {
|
|
|
639
752
|
* runtime image. View properties hold images in their wire form; this
|
|
640
753
|
* reads the path back.
|
|
641
754
|
*
|
|
642
|
-
* @example
|
|
755
|
+
* @example Check which file a window shows as its cover
|
|
756
|
+
* The test checks that the Cover window's image comes from the plugin's
|
|
757
|
+
* data directory. An image property holds the SDK's wire form of
|
|
758
|
+
* `plugin.ui.image(path)`, not a path; `imagePath` reads the path back.
|
|
643
759
|
* ```ts
|
|
760
|
+
* import assert from "node:assert/strict";
|
|
761
|
+
* import { join } from "node:path";
|
|
762
|
+
* declare const host: TestHost;
|
|
763
|
+
* // ---cut---
|
|
644
764
|
* assert.equal(
|
|
645
765
|
* host.imagePath(host.window("cover").props["cover"]),
|
|
646
766
|
* join(plugin.dataDirectory, "cover.png"),
|
|
@@ -659,8 +779,9 @@ export declare class TestHost implements Transport {
|
|
|
659
779
|
panelDrops: boolean;
|
|
660
780
|
/**
|
|
661
781
|
* Drop files like the player: on a panel with an `onDrop` listener the
|
|
662
|
-
* whole list goes there; otherwise (and on the video area)
|
|
663
|
-
*
|
|
782
|
+
* whole list goes there; otherwise (and on the video area) each path goes
|
|
783
|
+
* to the first provider of its extension (`folder` for existing
|
|
784
|
+
* directories), one call per provider with all of its paths.
|
|
664
785
|
* Files nobody takes are ignored. Resolves once the host is idle again.
|
|
665
786
|
*
|
|
666
787
|
* @param paths - The dropped paths, in the order the file manager gives.
|
|
@@ -668,9 +789,17 @@ export declare class TestHost implements Transport {
|
|
|
668
789
|
* drop point in panel coordinates (default `{ x: 0, y: 0 }`);
|
|
669
790
|
* `modifiers`: keys held (default all `false`).
|
|
670
791
|
*
|
|
671
|
-
* @example
|
|
792
|
+
* @example Drop files on the video and on the panel
|
|
793
|
+
* The first drop lands on the video area: each extension belongs to the
|
|
794
|
+
* provider that registered it first, and each provider gets one call with
|
|
795
|
+
* all of its paths. The second lands on the panel at a point, with Alt
|
|
796
|
+
* held, and the panel's `onDrop` gets the whole list. `drop` resolves
|
|
797
|
+
* once the host is idle.
|
|
672
798
|
* ```ts
|
|
673
|
-
*
|
|
799
|
+
* declare const host: TestHost;
|
|
800
|
+
* // ---cut---
|
|
801
|
+
* await host.drop(["/media/a.srt", "/media/b.mkv"]); // on the video area
|
|
802
|
+
* // On the panel, 10 px from its left and 48 px from its top, Alt held.
|
|
674
803
|
* await host.drop(["/media/list.m3u"], {
|
|
675
804
|
* target: "panel",
|
|
676
805
|
* at: { x: 10, y: 48 },
|
|
@@ -731,8 +860,15 @@ export declare class TestHost implements Transport {
|
|
|
731
860
|
/**
|
|
732
861
|
* One settings page by id. Throws when it was never registered.
|
|
733
862
|
*
|
|
734
|
-
* @example
|
|
863
|
+
* @example Change a value on the plugin's settings page
|
|
864
|
+
* The test sets the step on the General page and checks the page shows
|
|
865
|
+
* it. `emit` fires the page's callback as the user's input would; the
|
|
866
|
+
* plugin's handler stores the value and its `bind` function sends it back
|
|
867
|
+
* as a property, which `settle()` waits for.
|
|
735
868
|
* ```ts
|
|
869
|
+
* import assert from "node:assert/strict";
|
|
870
|
+
* declare const host: TestHost;
|
|
871
|
+
* // ---cut---
|
|
736
872
|
* const page = host.settingsPage("general");
|
|
737
873
|
* page.emit("setStep", 10);
|
|
738
874
|
* await host.settle();
|
|
@@ -781,11 +917,20 @@ export declare class TestHost implements Transport {
|
|
|
781
917
|
* @param plugin - The other plugin's manifest id.
|
|
782
918
|
* @param options - `title`: its tab label (default: `plugin`).
|
|
783
919
|
*
|
|
784
|
-
* @example
|
|
920
|
+
* @example Put the playlist next to this plugin's sidebar panel
|
|
921
|
+
* The sidebar ends up with two tabs: the playlist, mounted first, and the
|
|
922
|
+
* plugin's own panel after it. `mountOther` before `startPlugin` mounts
|
|
923
|
+
* another plugin's panel as the player would, so the plugin's mount
|
|
924
|
+
* during activation becomes the second tab (in the bottom bar or the
|
|
925
|
+
* backdrop it would be refused with `conflict` instead).
|
|
785
926
|
* ```ts
|
|
927
|
+
* import assert from "node:assert/strict";
|
|
928
|
+
* declare function activate(plugin: Plugin): Promise<void>;
|
|
929
|
+
* declare const directory: URL;
|
|
930
|
+
* // ---cut---
|
|
786
931
|
* const host = new TestHost();
|
|
787
932
|
* host.mountOther("sidebar", "vestra.playlist", { title: "播放列表" });
|
|
788
|
-
*
|
|
933
|
+
* await startPlugin(activate, { host, directory });
|
|
789
934
|
* assert.equal(host.layout.sidebar?.panels?.length, 2);
|
|
790
935
|
* ```
|
|
791
936
|
*
|
|
@@ -849,8 +994,18 @@ export declare class TestHost implements Transport {
|
|
|
849
994
|
* or switches to software rendering (`unsupported`, which also sets
|
|
850
995
|
* `videoRender.gpu` to false). Pushed only when this plugin is the owner.
|
|
851
996
|
*
|
|
852
|
-
* @example
|
|
997
|
+
* @example Make a shader fail to compile
|
|
998
|
+
* The plugin's render options report `failed`, as when mpv rejects a
|
|
999
|
+
* shader. The host pushes the failure only while this plugin owns the
|
|
1000
|
+
* render options, which its first accepted `set` made it; the SDK updates
|
|
1001
|
+
* the handle's `status` when the push arrives, so the test settles first.
|
|
853
1002
|
* ```ts
|
|
1003
|
+
* import assert from "node:assert/strict";
|
|
1004
|
+
* declare const host: TestHost;
|
|
1005
|
+
* // ---cut---
|
|
1006
|
+
* const render = plugin.video.render();
|
|
1007
|
+
* render.set({ toneMapping: "bt.2390" }); // makes the plugin the owner
|
|
1008
|
+
* await host.settle();
|
|
854
1009
|
* host.failVideoRender("failed", "shader compile failed: ERROR: 0:39: ...");
|
|
855
1010
|
* await host.settle();
|
|
856
1011
|
* assert.equal(render.status.state, "failed");
|
|
@@ -862,8 +1017,15 @@ export declare class TestHost implements Transport {
|
|
|
862
1017
|
* versions. `resources` holds each `video.filter` / `audio.filter` chain the
|
|
863
1018
|
* plugin applied, keyed by the SDK's resource id.
|
|
864
1019
|
*
|
|
865
|
-
* @example
|
|
1020
|
+
* @example Check the filter chain the plugin applied
|
|
1021
|
+
* After the plugin set its picture chain, the test reads the one chain
|
|
1022
|
+
* the host holds and compares its nodes. `resources` keeps each applied
|
|
1023
|
+
* `video.filter` / `audio.filter` chain by the SDK's resource id, after
|
|
1024
|
+
* the host validated it with the player's value ranges.
|
|
866
1025
|
* ```ts
|
|
1026
|
+
* import assert from "node:assert/strict";
|
|
1027
|
+
* declare const host: TestHost;
|
|
1028
|
+
* // ---cut---
|
|
867
1029
|
* const [chain] = [...host.processing.resources.values()];
|
|
868
1030
|
* assert.deepEqual(chain?.nodes, [{ kind: "hue", degrees: 30 }]);
|
|
869
1031
|
* ```
|
|
@@ -959,10 +1121,14 @@ export declare class TestHost implements Transport {
|
|
|
959
1121
|
* extensions (or scheme + host pairs) is refused with `conflict`, which
|
|
960
1122
|
* does not fail activation, like the player.
|
|
961
1123
|
*
|
|
962
|
-
* @example
|
|
1124
|
+
* @example Start with another plugin holding a file type
|
|
1125
|
+
* The plugin starts in a player where another plugin already resolves
|
|
1126
|
+
* `.ch8` files. Pushed before `startPlugin`, the entry makes the plugin's
|
|
1127
|
+
* own resolver for `.ch8` fail with `conflict` during activation, which
|
|
1128
|
+
* (as in the player) does not fail the activation.
|
|
963
1129
|
* ```ts
|
|
964
1130
|
* const host = new TestHost();
|
|
965
|
-
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["
|
|
1131
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
|
|
966
1132
|
* ```
|
|
967
1133
|
*/
|
|
968
1134
|
readonly otherResolvers: {
|
|
@@ -987,12 +1153,19 @@ export declare class TestHost implements Transport {
|
|
|
987
1153
|
* after changing {@link TestHost.otherResolvers} (another plugin started
|
|
988
1154
|
* or stopped).
|
|
989
1155
|
*
|
|
990
|
-
* @example
|
|
1156
|
+
* @example Let another plugin's file type appear while running
|
|
1157
|
+
* Another plugin starts and adds `.ch8`; the plugin under test then sees
|
|
1158
|
+
* it in `resolvers.fileTypes`. Changing `otherResolvers` alone pushes
|
|
1159
|
+
* nothing: `updateFileTypes` sends the new list as the player does when
|
|
1160
|
+
* a plugin starts or stops, and `settle()` lets the SDK apply it.
|
|
991
1161
|
* ```ts
|
|
992
|
-
*
|
|
1162
|
+
* import assert from "node:assert/strict";
|
|
1163
|
+
* declare const host: TestHost;
|
|
1164
|
+
* // ---cut---
|
|
1165
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
|
|
993
1166
|
* host.updateFileTypes();
|
|
994
1167
|
* await host.settle();
|
|
995
|
-
* assert.deepEqual(plugin.resolvers.fileTypes, ["
|
|
1168
|
+
* assert.deepEqual(plugin.resolvers.fileTypes, ["ch8"]);
|
|
996
1169
|
* ```
|
|
997
1170
|
*/
|
|
998
1171
|
updateFileTypes(): void;
|
|
@@ -1022,8 +1195,15 @@ export declare class TestHost implements Transport {
|
|
|
1022
1195
|
* For `openFile` / `openDirectory` / `saveFile` queue a one-path array;
|
|
1023
1196
|
* for `openFiles` any number of paths.
|
|
1024
1197
|
*
|
|
1025
|
-
* @example
|
|
1198
|
+
* @example Answer the plugin's dialogs in advance
|
|
1199
|
+
* The plugin's Open command gets a picked file, then a cancelled file
|
|
1200
|
+
* dialog, then a confirmed question. All answers share one queue, taken
|
|
1201
|
+
* from the front in the order the plugin opens dialogs of any kind: if
|
|
1202
|
+
* the next queued answer is missing or of the wrong kind, the dialog
|
|
1203
|
+
* fails like a lost reply, so queue them in the order the plugin asks.
|
|
1026
1204
|
* ```ts
|
|
1205
|
+
* declare const host: TestHost;
|
|
1206
|
+
* // ---cut---
|
|
1027
1207
|
* host.dialogAnswers.push(["/media/movie.mkv"]); // openFile picks it
|
|
1028
1208
|
* host.dialogAnswers.push(null); // the next file dialog is cancelled
|
|
1029
1209
|
* host.dialogAnswers.push({ confirmed: true, checked: false }); // confirm
|
|
@@ -1135,12 +1315,20 @@ export declare class TestHost implements Transport {
|
|
|
1135
1315
|
* source, for keyframe step seeks. `null` (the default): none modelled,
|
|
1136
1316
|
* and step seeks are exact even with `stepSeekKeyframes`.
|
|
1137
1317
|
*
|
|
1138
|
-
* @example
|
|
1318
|
+
* @example Step forward to the next keyframe
|
|
1319
|
+
* With keyframe step seeks on, Forward from 0:12 lands on the keyframe at
|
|
1320
|
+
* 0:20, not at 0:17, and the OSD says so. `keyframes` gives the source's
|
|
1321
|
+
* keyframe times; `setStepSeek` turns on keyframe steps of 5 s, and the
|
|
1322
|
+
* core action `seek-forward` seeks to the first keyframe at or after
|
|
1323
|
+
* the target, as the player does.
|
|
1139
1324
|
* ```ts
|
|
1325
|
+
* import assert from "node:assert/strict";
|
|
1326
|
+
* declare const host: TestHost;
|
|
1327
|
+
* // ---cut---
|
|
1140
1328
|
* host.setSource({ phase: "ready", duration: 45, position: 12 });
|
|
1141
1329
|
* host.keyframes = [0, 10, 20, 30, 40];
|
|
1142
1330
|
* await plugin.player.setStepSeek({ seconds: 5, keyframes: true });
|
|
1143
|
-
* await plugin.host.run("seek-forward");
|
|
1331
|
+
* await plugin.host.run("seek-forward"); // target 17 s, next keyframe 20 s
|
|
1144
1332
|
* assert.equal(host.source.position, 20);
|
|
1145
1333
|
* assert.equal(host.osd.at(-1), "Forward 5 s: 00:20 / 00:45");
|
|
1146
1334
|
* ```
|
|
@@ -1188,8 +1376,15 @@ export declare class TestHost implements Transport {
|
|
|
1188
1376
|
* an unknown id. `run()` executes it with the player's checks and writes
|
|
1189
1377
|
* its on-screen text to {@link TestHost.osd}.
|
|
1190
1378
|
*
|
|
1191
|
-
* @example
|
|
1379
|
+
* @example Pause through the player's own action
|
|
1380
|
+
* The test pauses playback the way the Space key does and checks that the
|
|
1381
|
+
* host recorded it. Core actions belong to the player (`host/<id>`);
|
|
1382
|
+
* `run()` applies the player's checks, so a source must be ready first,
|
|
1383
|
+
* and every run is appended to `coreRuns`.
|
|
1192
1384
|
* ```ts
|
|
1385
|
+
* import assert from "node:assert/strict";
|
|
1386
|
+
* declare const host: TestHost;
|
|
1387
|
+
* // ---cut---
|
|
1193
1388
|
* host.setSource({ phase: "ready", duration: 100, position: 10 });
|
|
1194
1389
|
* await host.coreAction("play-pause").run();
|
|
1195
1390
|
* assert.equal(host.source.paused, true);
|
|
@@ -1219,12 +1414,20 @@ export declare class TestHost implements Transport {
|
|
|
1219
1414
|
* @param method - The wire method name.
|
|
1220
1415
|
* @param handler - Produces the reply from the request parameters.
|
|
1221
1416
|
*
|
|
1222
|
-
* @example
|
|
1417
|
+
* @example Make opening a folder fail
|
|
1418
|
+
* The plugin's Open Folder command meets a missing folder and must tell
|
|
1419
|
+
* the user. `handle` replaces the host's answer to one wire method; the
|
|
1420
|
+
* `HostError` it throws reaches the plugin as a rejection with code
|
|
1421
|
+
* `missing`, like the player's refusal. `reset` restores the default
|
|
1422
|
+
* behaviour for the rest of the test.
|
|
1223
1423
|
* ```ts
|
|
1424
|
+
* import assert from "node:assert/strict";
|
|
1425
|
+
* declare const host: TestHost;
|
|
1426
|
+
* // ---cut---
|
|
1224
1427
|
* host.handle("system.openPath", () => {
|
|
1225
1428
|
* throw new HostError("missing", "no such file");
|
|
1226
1429
|
* });
|
|
1227
|
-
* await host.command("open-folder").run();
|
|
1430
|
+
* await host.command("open-folder").run(); // the plugin catches `missing`
|
|
1228
1431
|
* assert.deepEqual(host.osd, ["Folder not found"]);
|
|
1229
1432
|
* host.reset("system.openPath");
|
|
1230
1433
|
* ```
|
|
@@ -1253,8 +1456,15 @@ export declare class TestHost implements Transport {
|
|
|
1253
1456
|
* @param patch - Wire-level source fields, e.g. `phase`, `position`,
|
|
1254
1457
|
* `duration`, `paused`, `audioTracks`.
|
|
1255
1458
|
*
|
|
1256
|
-
* @example
|
|
1459
|
+
* @example Load a file, then let it play to the end
|
|
1460
|
+
* The plugin sees a one-minute source become ready, then end on its own.
|
|
1461
|
+
* `setSource` changes the host's source facts and broadcasts them like
|
|
1462
|
+
* the player's events; the SDK updates `player.state` when the event
|
|
1463
|
+
* arrives, so the test settles before reading it.
|
|
1257
1464
|
* ```ts
|
|
1465
|
+
* import assert from "node:assert/strict";
|
|
1466
|
+
* declare const host: TestHost;
|
|
1467
|
+
* // ---cut---
|
|
1258
1468
|
* host.setSource({ phase: "ready", duration: 60, position: 0 });
|
|
1259
1469
|
* await host.settle();
|
|
1260
1470
|
* assert.equal(plugin.player.state.phase, "ready");
|
|
@@ -1270,8 +1480,16 @@ export declare class TestHost implements Transport {
|
|
|
1270
1480
|
* sidebar (`effective: false`, `reason: "fullscreen"`) and leaving
|
|
1271
1481
|
* fullscreen docks it again.
|
|
1272
1482
|
*
|
|
1273
|
-
* @example
|
|
1483
|
+
* @example Go fullscreen, then report picture-in-picture support
|
|
1484
|
+
* The plugin sees the main window fullscreen at 1920×1080, then learns
|
|
1485
|
+
* that picture-in-picture is available. `setMainWindow` changes the
|
|
1486
|
+
* host's window facts and broadcasts them as the player does when the
|
|
1487
|
+
* user resizes or toggles fullscreen; entering fullscreen also collapses
|
|
1488
|
+
* an attached sidebar.
|
|
1274
1489
|
* ```ts
|
|
1490
|
+
* import assert from "node:assert/strict";
|
|
1491
|
+
* declare const host: TestHost;
|
|
1492
|
+
* // ---cut---
|
|
1275
1493
|
* host.setMainWindow({ fullscreen: true, videoArea: { width: 1920, height: 1080 } });
|
|
1276
1494
|
* await host.settle();
|
|
1277
1495
|
* assert.equal(plugin.window.state.fullscreen, true);
|
|
@@ -1303,8 +1521,16 @@ export declare class TestHost implements Transport {
|
|
|
1303
1521
|
* @param source - The path or URL.
|
|
1304
1522
|
* @param phase - The phase to end in (default `"ready"`).
|
|
1305
1523
|
*
|
|
1306
|
-
* @example
|
|
1524
|
+
* @example Let the user open another file meanwhile
|
|
1525
|
+
* A state the plugin captured before stops being current once another
|
|
1526
|
+
* file opens. `openElsewhere` gives the source a new `sourceId`, so
|
|
1527
|
+
* `isCurrent` turns false. Promise-returning calls made with
|
|
1528
|
+
* `ifCurrent: before` then reject with `stale`; calls that return nothing
|
|
1529
|
+
* drop the write silently instead (`TimelineLayer.set` with `ifCurrent`).
|
|
1307
1530
|
* ```ts
|
|
1531
|
+
* import assert from "node:assert/strict";
|
|
1532
|
+
* declare const host: TestHost;
|
|
1533
|
+
* // ---cut---
|
|
1308
1534
|
* const before = plugin.player.state;
|
|
1309
1535
|
* host.openElsewhere("/media/other.mp4");
|
|
1310
1536
|
* await host.settle();
|
|
@@ -1321,10 +1547,16 @@ export declare class TestHost implements Transport {
|
|
|
1321
1547
|
* @param producerId - The producer, from {@link TestHost.producers}.
|
|
1322
1548
|
* @param message - The player's failure text.
|
|
1323
1549
|
*
|
|
1324
|
-
* @example
|
|
1550
|
+
* @example Break the plugin's generated stream
|
|
1551
|
+
* The plugin's stream stops with an error, as when the player loses its
|
|
1552
|
+
* audio device. `failStream` puts the source into `error` with the
|
|
1553
|
+
* message and retires the producer at once; the stream's `onStop`
|
|
1554
|
+
* listeners get a `stream` error carrying the message.
|
|
1325
1555
|
* ```ts
|
|
1556
|
+
* declare const host: TestHost;
|
|
1557
|
+
* // ---cut---
|
|
1326
1558
|
* const [producer] = host.producers.values();
|
|
1327
|
-
* host.failStream(producer
|
|
1559
|
+
* host.failStream(producer!.id, "producer transport closed");
|
|
1328
1560
|
* ```
|
|
1329
1561
|
*/
|
|
1330
1562
|
failStream(producerId: number, message: string): void;
|
|
@@ -1345,8 +1577,14 @@ export declare class TestHost implements Transport {
|
|
|
1345
1577
|
*
|
|
1346
1578
|
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
1347
1579
|
*
|
|
1348
|
-
* @example
|
|
1580
|
+
* @example Press a global key and a panel key
|
|
1581
|
+
* Ctrl+B runs whatever is bound globally; Delete is pressed while the
|
|
1582
|
+
* plugin's panel has focus, so panel bindings are tried first. `press`
|
|
1583
|
+
* follows the player's binding table and resolves with what happened:
|
|
1584
|
+
* `"ran"`, `"reason"` (a disabled command showed why) or `"passed"`.
|
|
1349
1585
|
* ```ts
|
|
1586
|
+
* declare const host: TestHost;
|
|
1587
|
+
* // ---cut---
|
|
1350
1588
|
* await host.press("Ctrl+B"); // a global binding
|
|
1351
1589
|
* await host.press("Delete", { panel: true }); // while the panel has focus
|
|
1352
1590
|
* ```
|
|
@@ -1359,8 +1597,14 @@ export declare class TestHost implements Transport {
|
|
|
1359
1597
|
* `[binding, winner, ...ignored]` (command ids; core actions as
|
|
1360
1598
|
* `host/<id>`). Empty when there are none.
|
|
1361
1599
|
*
|
|
1362
|
-
* @example
|
|
1600
|
+
* @example Check that no default keys collide
|
|
1601
|
+
* The test fails when two commands, the player's or the plugin's, declare
|
|
1602
|
+
* the same binding. Each entry is `[binding, winner, ...ignored]`, so an
|
|
1603
|
+
* empty list means every key does what its command says.
|
|
1363
1604
|
* ```ts
|
|
1605
|
+
* import assert from "node:assert/strict";
|
|
1606
|
+
* declare const host: TestHost;
|
|
1607
|
+
* // ---cut---
|
|
1364
1608
|
* assert.deepEqual(host.conflicts, []);
|
|
1365
1609
|
* ```
|
|
1366
1610
|
*/
|
|
@@ -1422,8 +1666,14 @@ export declare class TestHost implements Transport {
|
|
|
1422
1666
|
* player's source rules: the same source upgrades, another source needs
|
|
1423
1667
|
* `replace` (`conflict` otherwise), a built-in id is `unsupported`.
|
|
1424
1668
|
*
|
|
1425
|
-
* @example
|
|
1669
|
+
* @example Offer a package file the plugin can install
|
|
1670
|
+
* The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
|
|
1671
|
+
* as if the file existed. The host accepts only paths listed here (others
|
|
1672
|
+
* fail with `missing`) and returns this manifest from `validate`; an
|
|
1673
|
+
* install then follows the player's source rules.
|
|
1426
1674
|
* ```ts
|
|
1675
|
+
* declare const host: TestHost;
|
|
1676
|
+
* // ---cut---
|
|
1427
1677
|
* host.candidates.set("/downloads/someone.demo-1.0.0.vxp", {
|
|
1428
1678
|
* id: "someone.demo", version: "1.0.0", apiVersion: "vestra/1",
|
|
1429
1679
|
* entry: "main.js", capabilities: [],
|
|
@@ -1444,8 +1694,14 @@ export declare class TestHost implements Transport {
|
|
|
1444
1694
|
* the downloaded file. Pass a whole property value or a field of one: a
|
|
1445
1695
|
* document nested in rows is read the same way.
|
|
1446
1696
|
*
|
|
1447
|
-
* @example
|
|
1697
|
+
* @example Read back a Markdown message in a chat window
|
|
1698
|
+
* The test checks the Markdown source of the first chat message. Rows
|
|
1699
|
+
* hold the document in its wire form, as `plugin.ui.markdown` produced
|
|
1700
|
+
* it; `markdown` reads it back, nested in a row or not.
|
|
1448
1701
|
* ```ts
|
|
1702
|
+
* import assert from "node:assert/strict";
|
|
1703
|
+
* declare const host: TestHost;
|
|
1704
|
+
* // ---cut---
|
|
1449
1705
|
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
1450
1706
|
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
1451
1707
|
* ```
|
|
@@ -1477,13 +1733,46 @@ export declare class TestHost implements Transport {
|
|
|
1477
1733
|
* registered (never added, or disposed).
|
|
1478
1734
|
*/
|
|
1479
1735
|
command(id: string): FakeCommand;
|
|
1736
|
+
/**
|
|
1737
|
+
* The plugin's bottom-bar buttons (`CommandOptions.button`) as the player
|
|
1738
|
+
* shows them: at most one, the first declared (`BAR_BUTTON_LIMITS`; a
|
|
1739
|
+
* later one stays a menu item only). Empty once the command is disposed.
|
|
1740
|
+
* Whether the bar places them is the bar's business: the list is what
|
|
1741
|
+
* the player hands to `BarButtons`.
|
|
1742
|
+
*
|
|
1743
|
+
* @example Click the plugin's bottom-bar button in a test
|
|
1744
|
+
* The test finds the plugin's Store button, clicks it and checks that the
|
|
1745
|
+
* Store window opened. `barButtons` lists the buttons of the plugin's
|
|
1746
|
+
* commands declared with `button`, as the bar shows them; `click()` runs
|
|
1747
|
+
* the command like its menu item and waits until the host is idle, and
|
|
1748
|
+
* rejects while the command is disabled or busy.
|
|
1749
|
+
* ```ts
|
|
1750
|
+
* import assert from "node:assert/strict";
|
|
1751
|
+
* declare const host: TestHost;
|
|
1752
|
+
* // ---cut---
|
|
1753
|
+
* const [button] = host.barButtons; // one button per plugin
|
|
1754
|
+
* assert.equal(button?.icon, "store");
|
|
1755
|
+
* await button?.click();
|
|
1756
|
+
* assert.equal(host.window("store").visible, true);
|
|
1757
|
+
* ```
|
|
1758
|
+
*
|
|
1759
|
+
* @since 1.5.29
|
|
1760
|
+
*/
|
|
1761
|
+
get barButtons(): readonly FakeBarButton[];
|
|
1480
1762
|
/**
|
|
1481
1763
|
* The plugin's tool window with this id (by default the file name without
|
|
1482
1764
|
* extension: `ui/picture.slint` → `picture`). Throws when it was never
|
|
1483
1765
|
* opened with `show()`.
|
|
1484
1766
|
*
|
|
1485
|
-
* @example
|
|
1767
|
+
* @example Press a window's Reset button from a test
|
|
1768
|
+
* The Picture window's Reset callback runs and the brightness returns to
|
|
1769
|
+
* 0. The window is found by its id (the `.slint` file name by default);
|
|
1770
|
+
* `emit` fires the callback the plugin bound, and `settle()` waits for
|
|
1771
|
+
* the property the handler wrote.
|
|
1486
1772
|
* ```ts
|
|
1773
|
+
* import assert from "node:assert/strict";
|
|
1774
|
+
* declare const host: TestHost;
|
|
1775
|
+
* // ---cut---
|
|
1487
1776
|
* host.window("picture").emit("reset");
|
|
1488
1777
|
* await host.settle();
|
|
1489
1778
|
* assert.equal(host.window("picture").props["brightness"], 0);
|
|
@@ -1495,8 +1784,14 @@ export declare class TestHost implements Transport {
|
|
|
1495
1784
|
* none is mounted. Its `props` use Slint (kebab-case) names; `emit` fires a
|
|
1496
1785
|
* callback on the panel's global.
|
|
1497
1786
|
*
|
|
1498
|
-
* @example
|
|
1787
|
+
* @example Select a row in the plugin's sidebar panel
|
|
1788
|
+
* The test checks the panel sits in the sidebar and selects its third
|
|
1789
|
+
* row. `emit` fires a callback on the panel's global as a click would;
|
|
1790
|
+
* the plugin's handler runs, and `settle()` waits for what it sent.
|
|
1499
1791
|
* ```ts
|
|
1792
|
+
* import assert from "node:assert/strict";
|
|
1793
|
+
* declare const host: TestHost;
|
|
1794
|
+
* // ---cut---
|
|
1500
1795
|
* const panel = host.mounted();
|
|
1501
1796
|
* assert.equal(panel.slot, "sidebar");
|
|
1502
1797
|
* panel.emit("select", 2);
|
|
@@ -1514,8 +1809,14 @@ export declare class TestHost implements Transport {
|
|
|
1514
1809
|
/**
|
|
1515
1810
|
* The parameters of every request of one wire method, oldest first.
|
|
1516
1811
|
*
|
|
1517
|
-
* @example
|
|
1812
|
+
* @example Check the request behind an OSD message
|
|
1813
|
+
* The test checks the last OSD request the plugin sent, before
|
|
1814
|
+
* translation. `sent` lists the parameters of every request of one wire
|
|
1815
|
+
* method, oldest first; `host.osd` holds the texts as shown instead.
|
|
1518
1816
|
* ```ts
|
|
1817
|
+
* import assert from "node:assert/strict";
|
|
1818
|
+
* declare const host: TestHost;
|
|
1819
|
+
* // ---cut---
|
|
1519
1820
|
* assert.deepEqual(host.sent("osd.show").at(-1), { text: "Saved" });
|
|
1520
1821
|
* ```
|
|
1521
1822
|
*/
|
|
@@ -1533,14 +1834,31 @@ export declare class TestHost implements Transport {
|
|
|
1533
1834
|
*
|
|
1534
1835
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
1535
1836
|
*
|
|
1536
|
-
* @example
|
|
1837
|
+
* @example Wait past a debounced save
|
|
1838
|
+
* The plugin's store writes its file 300 ms after the last `save`, so
|
|
1839
|
+
* the test waits 400 ms before it checks what the plugin did next.
|
|
1840
|
+
* `settle(ms)` returns when no request is in flight and at least `ms`
|
|
1841
|
+
* have passed, so the store's timer has fired. The file write itself is
|
|
1842
|
+
* the plugin's own work, which `settle` does not wait for: a test that
|
|
1843
|
+
* reads the file calls `await store.flush()` instead.
|
|
1537
1844
|
* ```ts
|
|
1845
|
+
* declare const host: TestHost;
|
|
1846
|
+
* // ---cut---
|
|
1847
|
+
* const store = plugin.storage.json("prefs", { defaults: { step: 5 } });
|
|
1538
1848
|
* store.save({ step: 10 });
|
|
1539
1849
|
* await host.settle(400); // past storage.json's 300 ms debounce
|
|
1540
1850
|
* ```
|
|
1541
1851
|
*
|
|
1542
|
-
* @example
|
|
1852
|
+
* @example Poll for work the plugin does on its own
|
|
1853
|
+
* The plugin reads and parses a file before it shows a status; `settle`
|
|
1854
|
+
* does not wait for that (it only waits for host requests), so the test
|
|
1855
|
+
* polls the property it asserts on, 20 ms at a time, for up to 2 seconds.
|
|
1543
1856
|
* ```ts
|
|
1857
|
+
* import assert from "node:assert/strict";
|
|
1858
|
+
* declare const host: TestHost;
|
|
1859
|
+
* declare const expected: string;
|
|
1860
|
+
* // ---cut---
|
|
1861
|
+
* const window = host.window("import");
|
|
1544
1862
|
* window.emit("load-file"); // the plugin reads and parses the file itself
|
|
1545
1863
|
* for (let i = 0; i < 100 && window.props["status-text"] !== expected; i++)
|
|
1546
1864
|
* await host.settle(20);
|
|
@@ -1577,19 +1895,43 @@ export declare class TestHost implements Transport {
|
|
|
1577
1895
|
* plugin like the player stopping it (scopes released, stores flushed),
|
|
1578
1896
|
* wait until the host is idle and remove the host's temporary files.
|
|
1579
1897
|
*
|
|
1580
|
-
* @example
|
|
1898
|
+
* @example Test that a setting survives a restart
|
|
1899
|
+
* The test turns on the 10-step option, stops the plugin, starts it again
|
|
1900
|
+
* on the same data directory and checks the option is still on. Both runs
|
|
1901
|
+
* get a directory the test made (`dataDirectory`): `stop()` disposes the
|
|
1902
|
+
* plugin like the player, which writes its pending stores there, and only
|
|
1903
|
+
* removes the host's own temporary directory, so the second run reads what
|
|
1904
|
+
* the first one saved.
|
|
1581
1905
|
* ```ts
|
|
1906
|
+
* import assert from "node:assert/strict";
|
|
1907
|
+
* import { mkdtempSync, rmSync } from "node:fs";
|
|
1908
|
+
* import { tmpdir } from "node:os";
|
|
1909
|
+
* import { join } from "node:path";
|
|
1910
|
+
* import test from "node:test";
|
|
1911
|
+
* import { startPlugin } from "@vestra-player/sdk/testing";
|
|
1912
|
+
* import activate from "../main.ts";
|
|
1913
|
+
*
|
|
1582
1914
|
* test("remembers the step across restarts", async () => {
|
|
1583
1915
|
* const directory = new URL("..", import.meta.url);
|
|
1584
|
-
* const first = await startPlugin(activate, { directory });
|
|
1585
|
-
* await first.host.command("step-10").run();
|
|
1586
1916
|
* const data = mkdtempSync(join(tmpdir(), "data-"));
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1917
|
+
* try {
|
|
1918
|
+
* const first = await startPlugin(activate, { directory, dataDirectory: data });
|
|
1919
|
+
* try {
|
|
1920
|
+
* await first.host.command("step-10").run();
|
|
1921
|
+
* } finally {
|
|
1922
|
+
* await first.stop(); // flushes the plugin's stores into `data`
|
|
1923
|
+
* }
|
|
1589
1924
|
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1925
|
+
* // A restart: a new host and a new plugin instance on the same data.
|
|
1926
|
+
* const second = await startPlugin(activate, { directory, dataDirectory: data });
|
|
1927
|
+
* try {
|
|
1928
|
+
* assert.equal(second.host.command("step-10").state.checked, true);
|
|
1929
|
+
* } finally {
|
|
1930
|
+
* await second.stop();
|
|
1931
|
+
* }
|
|
1932
|
+
* } finally {
|
|
1933
|
+
* rmSync(data, { recursive: true, force: true });
|
|
1934
|
+
* }
|
|
1593
1935
|
* });
|
|
1594
1936
|
* ```
|
|
1595
1937
|
*/
|