@vestra-player/sdk 1.5.29 → 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 +12 -0
- package/dist/capabilities.d.ts +13 -3
- package/dist/capabilities.js +13 -3
- package/dist/commands.d.ts +49 -9
- package/dist/commands.js +49 -9
- package/dist/index.d.ts +6 -1
- package/dist/palettes.d.ts +16 -2
- package/dist/palettes.js +16 -2
- package/dist/plugin/bindings.d.ts +27 -3
- package/dist/plugin/bindings.js +27 -3
- 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 +353 -62
- package/dist/plugin/testing.js +343 -61
- package/dist/plugin/types.d.ts +781 -164
- 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 +3312 -20
- package/llms.txt +20 -20
- package/package.json +1 -1
package/dist/plugin/testing.js
CHANGED
|
@@ -32,7 +32,14 @@
|
|
|
32
32
|
* Use it from `node:test` (what `vestra-plugin test` runs). Each test should
|
|
33
33
|
* create its own host; `stop()` removes the host's temporary directories.
|
|
34
34
|
*
|
|
35
|
-
* @example
|
|
35
|
+
* @example Test that a menu item shows a message
|
|
36
|
+
* A complete test file: it activates the plugin against an in-memory
|
|
37
|
+
* player, picks its Hello menu item and checks what the OSD showed.
|
|
38
|
+
* `startPlugin` reads the plugin's `manifest.json` from `directory`, calls
|
|
39
|
+
* `activate` and waits for its registrations; `run()` executes the command
|
|
40
|
+
* as the menu would and waits until the host is idle. `stop()` disposes
|
|
41
|
+
* the plugin and removes the host's temporary files, even when an
|
|
42
|
+
* assertion fails.
|
|
36
43
|
* ```ts
|
|
37
44
|
* // tests/main.test.ts
|
|
38
45
|
* import assert from "node:assert/strict";
|
|
@@ -123,14 +130,27 @@ export { LIST_SEPARATORS };
|
|
|
123
130
|
* The player's interface languages, English (the source) first, as the
|
|
124
131
|
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
125
132
|
*
|
|
126
|
-
* @example
|
|
133
|
+
* @example Check that every language translates a menu item
|
|
134
|
+
* The test fails for each interface language in which the "Open…" item
|
|
135
|
+
* still shows English. It starts the plugin once per language except
|
|
136
|
+
* English (`LOCALES[0]`): the `locale` option loads the plugin's
|
|
137
|
+
* `locales/<locale>.po` and the host shows menu texts translated with it,
|
|
138
|
+
* which `display` reports.
|
|
127
139
|
* ```ts
|
|
128
|
-
* import {
|
|
140
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
141
|
+
* declare function activate(plugin: Plugin): Promise<void>;
|
|
142
|
+
* // ---cut---
|
|
143
|
+
* import assert from "node:assert/strict";
|
|
144
|
+
* import { LOCALES, startPlugin } from "@vestra-player/sdk/testing";
|
|
129
145
|
*
|
|
146
|
+
* const directory = new URL("..", import.meta.url);
|
|
130
147
|
* for (const locale of LOCALES.slice(1)) {
|
|
131
148
|
* const { host, stop } = await startPlugin(activate, { directory, locale });
|
|
132
|
-
*
|
|
133
|
-
*
|
|
149
|
+
* try {
|
|
150
|
+
* assert.notEqual(host.command("open").display.title, "Open…", locale);
|
|
151
|
+
* } finally {
|
|
152
|
+
* await stop(); // also when the assertion fails
|
|
153
|
+
* }
|
|
134
154
|
* }
|
|
135
155
|
* ```
|
|
136
156
|
*/
|
|
@@ -261,7 +281,13 @@ const MAX_SIDEBAR_TABS = 8;
|
|
|
261
281
|
* `calls`) use the protocol's shapes. They are fine for assertions but are
|
|
262
282
|
* not part of the plugin API: their shape may change in minor versions.
|
|
263
283
|
*
|
|
264
|
-
* @example
|
|
284
|
+
* @example Preset the player, then react to another plugin's file
|
|
285
|
+
* The test starts the plugin in a player with the dark theme, opens a file
|
|
286
|
+
* the way another plugin would, and checks the plugin's panel title. A host
|
|
287
|
+
* created first can be preset with data the plugin reads during
|
|
288
|
+
* activation; `startPlugin` then uses it. `openElsewhere` changes the
|
|
289
|
+
* source and broadcasts it, the plugin's listener updates its panel, and
|
|
290
|
+
* `settle()` waits for those requests before the assertion.
|
|
265
291
|
* ```ts
|
|
266
292
|
* import assert from "node:assert/strict";
|
|
267
293
|
* import test from "node:test";
|
|
@@ -312,8 +338,15 @@ export class TestHost {
|
|
|
312
338
|
* is mounted there). You may change a slot directly and then call
|
|
313
339
|
* `host.emit("window.state", {})` to simulate a change made elsewhere.
|
|
314
340
|
*
|
|
315
|
-
* @example
|
|
341
|
+
* @example Hide the sidebar and check the player's layout
|
|
342
|
+
* With the plugin's panel in the sidebar, toggling the sidebar records
|
|
343
|
+
* that the user prefers it hidden. `toggle()` reads the slot from the
|
|
344
|
+
* player, asks for the opposite of its `preferred` flag and resolves once
|
|
345
|
+
* the player answered, so the host's `layout` already shows the new value.
|
|
316
346
|
* ```ts
|
|
347
|
+
* import assert from "node:assert/strict";
|
|
348
|
+
* declare const host: TestHost;
|
|
349
|
+
* // ---cut---
|
|
317
350
|
* assert.equal(host.layout.sidebar?.preferred, true);
|
|
318
351
|
* await plugin.panels.sidebar.toggle();
|
|
319
352
|
* assert.equal(host.layout.sidebar?.preferred, false);
|
|
@@ -346,8 +379,17 @@ export class TestHost {
|
|
|
346
379
|
* players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
|
|
347
380
|
* passing; set `locale` to test what a user sees now.
|
|
348
381
|
*
|
|
349
|
-
* @example
|
|
382
|
+
* @example Check a menu path in Simplified Chinese
|
|
383
|
+
* The test starts the plugin with a Chinese interface and checks the
|
|
384
|
+
* menu path its Blur item shows. The `locale` option sets
|
|
385
|
+
* `host.locale` before activation, so the plugin's `locales/zh-Hans.po`
|
|
386
|
+
* is loaded; the built-in menu `@video` shows in the player's own words
|
|
387
|
+
* (视频), the item's title in the plugin's translation.
|
|
350
388
|
* ```ts
|
|
389
|
+
* import assert from "node:assert/strict";
|
|
390
|
+
* declare function activate(plugin: Plugin): Promise<void>;
|
|
391
|
+
* declare const directory: URL;
|
|
392
|
+
* // ---cut---
|
|
351
393
|
* const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
352
394
|
* assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
|
|
353
395
|
* await stop();
|
|
@@ -387,8 +429,14 @@ export class TestHost {
|
|
|
387
429
|
* @param args - Values for its placeholders.
|
|
388
430
|
* @returns The shown text.
|
|
389
431
|
*
|
|
390
|
-
* @example
|
|
432
|
+
* @example Compare with a translated message
|
|
433
|
+
* The test expects the text a Chinese user sees for a template the plugin
|
|
434
|
+
* shows. `translate` looks the English source up in the plugin's catalog
|
|
435
|
+
* for `host.locale` and fills in the arguments, the way the player does.
|
|
391
436
|
* ```ts
|
|
437
|
+
* import assert from "node:assert/strict";
|
|
438
|
+
* declare const host: TestHost;
|
|
439
|
+
* // ---cut---
|
|
392
440
|
* assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
|
|
393
441
|
* ```
|
|
394
442
|
*
|
|
@@ -500,10 +548,16 @@ export class TestHost {
|
|
|
500
548
|
* What `player.stats()` returns. Fields added in later rounds may be left
|
|
501
549
|
* out; the SDK fills them with `null`.
|
|
502
550
|
*
|
|
503
|
-
* @example
|
|
551
|
+
* @example Pretend a 1080p H.264 video is playing
|
|
552
|
+
* The plugin's `player.stats()` now reports a 1920×1080 H.264 stream at
|
|
553
|
+
* 24 fps. The host answers `stats()` with this object as it stands, so a
|
|
554
|
+
* test sets it before the plugin asks; fields it leaves out come back as
|
|
555
|
+
* `null` from the SDK.
|
|
504
556
|
* ```ts
|
|
557
|
+
* declare const host: TestHost;
|
|
558
|
+
* // ---cut---
|
|
505
559
|
* host.stats = {
|
|
506
|
-
* ...host.stats,
|
|
560
|
+
* ...host.stats, // keep the other fields
|
|
507
561
|
* video: { codec: "h264", width: 1920, height: 1080, fps: 24 },
|
|
508
562
|
* } as typeof host.stats;
|
|
509
563
|
* ```
|
|
@@ -536,12 +590,21 @@ export class TestHost {
|
|
|
536
590
|
* handler is not found (`available()` is false, `run()` rejects with
|
|
537
591
|
* `unavailable`).
|
|
538
592
|
*
|
|
539
|
-
* @example
|
|
593
|
+
* @example Fake ffprobe and a failing ffmpeg
|
|
594
|
+
* The plugin's Probe command gets an empty stream list from ffprobe, and
|
|
595
|
+
* any ffmpeg run fails as it would on a broken file. `handle` makes the
|
|
596
|
+
* tool available and answers each `tools.run` with the handler's result;
|
|
597
|
+
* a thrown `HostError` makes the run reject with that code. `calls`
|
|
598
|
+
* records every run with its arguments.
|
|
540
599
|
* ```ts
|
|
600
|
+
* import assert from "node:assert/strict";
|
|
601
|
+
* declare const host: TestHost;
|
|
602
|
+
* // ---cut---
|
|
541
603
|
* host.tools.handle("ffprobe", (args) => {
|
|
542
|
-
* assert.ok(args.includes("-show_streams"));
|
|
543
|
-
* return { stdout: JSON.stringify({ streams: [] }) };
|
|
604
|
+
* assert.ok(args.includes("-show_streams")); // what the plugin asked for
|
|
605
|
+
* return { stdout: JSON.stringify({ streams: [] }) }; // a file with no streams
|
|
544
606
|
* });
|
|
607
|
+
* // Every ffmpeg run rejects with code `failed`.
|
|
545
608
|
* host.tools.handle("ffmpeg", () => {
|
|
546
609
|
* throw new HostError("failed", "Invalid data found when processing input");
|
|
547
610
|
* });
|
|
@@ -633,12 +696,20 @@ export class TestHost {
|
|
|
633
696
|
* @param pluginId - The other plugin's manifest id.
|
|
634
697
|
* @param marks - Its whole layer.
|
|
635
698
|
*
|
|
636
|
-
* @example
|
|
699
|
+
* @example Let another plugin publish chapters
|
|
700
|
+
* The plugin under test sees two chapter marks another plugin put on the
|
|
701
|
+
* timeline. `setTimelineLayer` stores that plugin's whole layer and pushes
|
|
702
|
+
* `timeline.changed`; once the plugin uses `plugin.timeline` (with
|
|
703
|
+
* `events`) the SDK keeps `marks` current from that event, and `settle()`
|
|
704
|
+
* lets it arrive.
|
|
637
705
|
* ```ts
|
|
706
|
+
* import assert from "node:assert/strict";
|
|
707
|
+
* declare const host: TestHost;
|
|
708
|
+
* // ---cut---
|
|
638
709
|
* host.setTimelineLayer("community.chapters", [
|
|
639
710
|
* { time: 0, kind: "chapter", title: "Intro" },
|
|
640
711
|
* { time: 95, kind: "chapter", title: "Part 1" },
|
|
641
|
-
* ]);
|
|
712
|
+
* ]); // replaces that plugin's whole layer; [] would clear it
|
|
642
713
|
* await host.settle();
|
|
643
714
|
* assert.equal(plugin.timeline.marks.length, 2);
|
|
644
715
|
* ```
|
|
@@ -670,8 +741,15 @@ export class TestHost {
|
|
|
670
741
|
* runtime image. View properties hold images in their wire form; this
|
|
671
742
|
* reads the path back.
|
|
672
743
|
*
|
|
673
|
-
* @example
|
|
744
|
+
* @example Check which file a window shows as its cover
|
|
745
|
+
* The test checks that the Cover window's image comes from the plugin's
|
|
746
|
+
* data directory. An image property holds the SDK's wire form of
|
|
747
|
+
* `plugin.ui.image(path)`, not a path; `imagePath` reads the path back.
|
|
674
748
|
* ```ts
|
|
749
|
+
* import assert from "node:assert/strict";
|
|
750
|
+
* import { join } from "node:path";
|
|
751
|
+
* declare const host: TestHost;
|
|
752
|
+
* // ---cut---
|
|
675
753
|
* assert.equal(
|
|
676
754
|
* host.imagePath(host.window("cover").props["cover"]),
|
|
677
755
|
* join(plugin.dataDirectory, "cover.png"),
|
|
@@ -760,8 +838,9 @@ export class TestHost {
|
|
|
760
838
|
panelDrops = false;
|
|
761
839
|
/**
|
|
762
840
|
* Drop files like the player: on a panel with an `onDrop` listener the
|
|
763
|
-
* whole list goes there; otherwise (and on the video area)
|
|
764
|
-
*
|
|
841
|
+
* whole list goes there; otherwise (and on the video area) each path goes
|
|
842
|
+
* to the first provider of its extension (`folder` for existing
|
|
843
|
+
* directories), one call per provider with all of its paths.
|
|
765
844
|
* Files nobody takes are ignored. Resolves once the host is idle again.
|
|
766
845
|
*
|
|
767
846
|
* @param paths - The dropped paths, in the order the file manager gives.
|
|
@@ -769,9 +848,17 @@ export class TestHost {
|
|
|
769
848
|
* drop point in panel coordinates (default `{ x: 0, y: 0 }`);
|
|
770
849
|
* `modifiers`: keys held (default all `false`).
|
|
771
850
|
*
|
|
772
|
-
* @example
|
|
851
|
+
* @example Drop files on the video and on the panel
|
|
852
|
+
* The first drop lands on the video area: each extension belongs to the
|
|
853
|
+
* provider that registered it first, and each provider gets one call with
|
|
854
|
+
* all of its paths. The second lands on the panel at a point, with Alt
|
|
855
|
+
* held, and the panel's `onDrop` gets the whole list. `drop` resolves
|
|
856
|
+
* once the host is idle.
|
|
773
857
|
* ```ts
|
|
774
|
-
*
|
|
858
|
+
* declare const host: TestHost;
|
|
859
|
+
* // ---cut---
|
|
860
|
+
* await host.drop(["/media/a.srt", "/media/b.mkv"]); // on the video area
|
|
861
|
+
* // On the panel, 10 px from its left and 48 px from its top, Alt held.
|
|
775
862
|
* await host.drop(["/media/list.m3u"], {
|
|
776
863
|
* target: "panel",
|
|
777
864
|
* at: { x: 10, y: 48 },
|
|
@@ -834,8 +921,15 @@ export class TestHost {
|
|
|
834
921
|
/**
|
|
835
922
|
* One settings page by id. Throws when it was never registered.
|
|
836
923
|
*
|
|
837
|
-
* @example
|
|
924
|
+
* @example Change a value on the plugin's settings page
|
|
925
|
+
* The test sets the step on the General page and checks the page shows
|
|
926
|
+
* it. `emit` fires the page's callback as the user's input would; the
|
|
927
|
+
* plugin's handler stores the value and its `bind` function sends it back
|
|
928
|
+
* as a property, which `settle()` waits for.
|
|
838
929
|
* ```ts
|
|
930
|
+
* import assert from "node:assert/strict";
|
|
931
|
+
* declare const host: TestHost;
|
|
932
|
+
* // ---cut---
|
|
839
933
|
* const page = host.settingsPage("general");
|
|
840
934
|
* page.emit("setStep", 10);
|
|
841
935
|
* await host.settle();
|
|
@@ -874,11 +968,20 @@ export class TestHost {
|
|
|
874
968
|
* @param plugin - The other plugin's manifest id.
|
|
875
969
|
* @param options - `title`: its tab label (default: `plugin`).
|
|
876
970
|
*
|
|
877
|
-
* @example
|
|
971
|
+
* @example Put the playlist next to this plugin's sidebar panel
|
|
972
|
+
* The sidebar ends up with two tabs: the playlist, mounted first, and the
|
|
973
|
+
* plugin's own panel after it. `mountOther` before `startPlugin` mounts
|
|
974
|
+
* another plugin's panel as the player would, so the plugin's mount
|
|
975
|
+
* during activation becomes the second tab (in the bottom bar or the
|
|
976
|
+
* backdrop it would be refused with `conflict` instead).
|
|
878
977
|
* ```ts
|
|
978
|
+
* import assert from "node:assert/strict";
|
|
979
|
+
* declare function activate(plugin: Plugin): Promise<void>;
|
|
980
|
+
* declare const directory: URL;
|
|
981
|
+
* // ---cut---
|
|
879
982
|
* const host = new TestHost();
|
|
880
983
|
* host.mountOther("sidebar", "vestra.playlist", { title: "播放列表" });
|
|
881
|
-
*
|
|
984
|
+
* await startPlugin(activate, { host, directory });
|
|
882
985
|
* assert.equal(host.layout.sidebar?.panels?.length, 2);
|
|
883
986
|
* ```
|
|
884
987
|
*
|
|
@@ -1001,8 +1104,18 @@ export class TestHost {
|
|
|
1001
1104
|
* or switches to software rendering (`unsupported`, which also sets
|
|
1002
1105
|
* `videoRender.gpu` to false). Pushed only when this plugin is the owner.
|
|
1003
1106
|
*
|
|
1004
|
-
* @example
|
|
1107
|
+
* @example Make a shader fail to compile
|
|
1108
|
+
* The plugin's render options report `failed`, as when mpv rejects a
|
|
1109
|
+
* shader. The host pushes the failure only while this plugin owns the
|
|
1110
|
+
* render options, which its first accepted `set` made it; the SDK updates
|
|
1111
|
+
* the handle's `status` when the push arrives, so the test settles first.
|
|
1005
1112
|
* ```ts
|
|
1113
|
+
* import assert from "node:assert/strict";
|
|
1114
|
+
* declare const host: TestHost;
|
|
1115
|
+
* // ---cut---
|
|
1116
|
+
* const render = plugin.video.render();
|
|
1117
|
+
* render.set({ toneMapping: "bt.2390" }); // makes the plugin the owner
|
|
1118
|
+
* await host.settle();
|
|
1006
1119
|
* host.failVideoRender("failed", "shader compile failed: ERROR: 0:39: ...");
|
|
1007
1120
|
* await host.settle();
|
|
1008
1121
|
* assert.equal(render.status.state, "failed");
|
|
@@ -1022,8 +1135,15 @@ export class TestHost {
|
|
|
1022
1135
|
* versions. `resources` holds each `video.filter` / `audio.filter` chain the
|
|
1023
1136
|
* plugin applied, keyed by the SDK's resource id.
|
|
1024
1137
|
*
|
|
1025
|
-
* @example
|
|
1138
|
+
* @example Check the filter chain the plugin applied
|
|
1139
|
+
* After the plugin set its picture chain, the test reads the one chain
|
|
1140
|
+
* the host holds and compares its nodes. `resources` keeps each applied
|
|
1141
|
+
* `video.filter` / `audio.filter` chain by the SDK's resource id, after
|
|
1142
|
+
* the host validated it with the player's value ranges.
|
|
1026
1143
|
* ```ts
|
|
1144
|
+
* import assert from "node:assert/strict";
|
|
1145
|
+
* declare const host: TestHost;
|
|
1146
|
+
* // ---cut---
|
|
1027
1147
|
* const [chain] = [...host.processing.resources.values()];
|
|
1028
1148
|
* assert.deepEqual(chain?.nodes, [{ kind: "hue", degrees: 30 }]);
|
|
1029
1149
|
* ```
|
|
@@ -1078,10 +1198,14 @@ export class TestHost {
|
|
|
1078
1198
|
* extensions (or scheme + host pairs) is refused with `conflict`, which
|
|
1079
1199
|
* does not fail activation, like the player.
|
|
1080
1200
|
*
|
|
1081
|
-
* @example
|
|
1201
|
+
* @example Start with another plugin holding a file type
|
|
1202
|
+
* The plugin starts in a player where another plugin already resolves
|
|
1203
|
+
* `.ch8` files. Pushed before `startPlugin`, the entry makes the plugin's
|
|
1204
|
+
* own resolver for `.ch8` fail with `conflict` during activation, which
|
|
1205
|
+
* (as in the player) does not fail the activation.
|
|
1082
1206
|
* ```ts
|
|
1083
1207
|
* const host = new TestHost();
|
|
1084
|
-
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["
|
|
1208
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
|
|
1085
1209
|
* ```
|
|
1086
1210
|
*/
|
|
1087
1211
|
otherResolvers = [];
|
|
@@ -1107,12 +1231,19 @@ export class TestHost {
|
|
|
1107
1231
|
* after changing {@link TestHost.otherResolvers} (another plugin started
|
|
1108
1232
|
* or stopped).
|
|
1109
1233
|
*
|
|
1110
|
-
* @example
|
|
1234
|
+
* @example Let another plugin's file type appear while running
|
|
1235
|
+
* Another plugin starts and adds `.ch8`; the plugin under test then sees
|
|
1236
|
+
* it in `resolvers.fileTypes`. Changing `otherResolvers` alone pushes
|
|
1237
|
+
* nothing: `updateFileTypes` sends the new list as the player does when
|
|
1238
|
+
* a plugin starts or stops, and `settle()` lets the SDK apply it.
|
|
1111
1239
|
* ```ts
|
|
1112
|
-
*
|
|
1240
|
+
* import assert from "node:assert/strict";
|
|
1241
|
+
* declare const host: TestHost;
|
|
1242
|
+
* // ---cut---
|
|
1243
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
|
|
1113
1244
|
* host.updateFileTypes();
|
|
1114
1245
|
* await host.settle();
|
|
1115
|
-
* assert.deepEqual(plugin.resolvers.fileTypes, ["
|
|
1246
|
+
* assert.deepEqual(plugin.resolvers.fileTypes, ["ch8"]);
|
|
1116
1247
|
* ```
|
|
1117
1248
|
*/
|
|
1118
1249
|
updateFileTypes() {
|
|
@@ -1141,8 +1272,15 @@ export class TestHost {
|
|
|
1141
1272
|
* For `openFile` / `openDirectory` / `saveFile` queue a one-path array;
|
|
1142
1273
|
* for `openFiles` any number of paths.
|
|
1143
1274
|
*
|
|
1144
|
-
* @example
|
|
1275
|
+
* @example Answer the plugin's dialogs in advance
|
|
1276
|
+
* The plugin's Open command gets a picked file, then a cancelled file
|
|
1277
|
+
* dialog, then a confirmed question. All answers share one queue, taken
|
|
1278
|
+
* from the front in the order the plugin opens dialogs of any kind: if
|
|
1279
|
+
* the next queued answer is missing or of the wrong kind, the dialog
|
|
1280
|
+
* fails like a lost reply, so queue them in the order the plugin asks.
|
|
1145
1281
|
* ```ts
|
|
1282
|
+
* declare const host: TestHost;
|
|
1283
|
+
* // ---cut---
|
|
1146
1284
|
* host.dialogAnswers.push(["/media/movie.mkv"]); // openFile picks it
|
|
1147
1285
|
* host.dialogAnswers.push(null); // the next file dialog is cancelled
|
|
1148
1286
|
* host.dialogAnswers.push({ confirmed: true, checked: false }); // confirm
|
|
@@ -1304,12 +1442,20 @@ export class TestHost {
|
|
|
1304
1442
|
* source, for keyframe step seeks. `null` (the default): none modelled,
|
|
1305
1443
|
* and step seeks are exact even with `stepSeekKeyframes`.
|
|
1306
1444
|
*
|
|
1307
|
-
* @example
|
|
1445
|
+
* @example Step forward to the next keyframe
|
|
1446
|
+
* With keyframe step seeks on, Forward from 0:12 lands on the keyframe at
|
|
1447
|
+
* 0:20, not at 0:17, and the OSD says so. `keyframes` gives the source's
|
|
1448
|
+
* keyframe times; `setStepSeek` turns on keyframe steps of 5 s, and the
|
|
1449
|
+
* core action `seek-forward` seeks to the first keyframe at or after
|
|
1450
|
+
* the target, as the player does.
|
|
1308
1451
|
* ```ts
|
|
1452
|
+
* import assert from "node:assert/strict";
|
|
1453
|
+
* declare const host: TestHost;
|
|
1454
|
+
* // ---cut---
|
|
1309
1455
|
* host.setSource({ phase: "ready", duration: 45, position: 12 });
|
|
1310
1456
|
* host.keyframes = [0, 10, 20, 30, 40];
|
|
1311
1457
|
* await plugin.player.setStepSeek({ seconds: 5, keyframes: true });
|
|
1312
|
-
* await plugin.host.run("seek-forward");
|
|
1458
|
+
* await plugin.host.run("seek-forward"); // target 17 s, next keyframe 20 s
|
|
1313
1459
|
* assert.equal(host.source.position, 20);
|
|
1314
1460
|
* assert.equal(host.osd.at(-1), "Forward 5 s: 00:20 / 00:45");
|
|
1315
1461
|
* ```
|
|
@@ -1338,8 +1484,15 @@ export class TestHost {
|
|
|
1338
1484
|
* an unknown id. `run()` executes it with the player's checks and writes
|
|
1339
1485
|
* its on-screen text to {@link TestHost.osd}.
|
|
1340
1486
|
*
|
|
1341
|
-
* @example
|
|
1487
|
+
* @example Pause through the player's own action
|
|
1488
|
+
* The test pauses playback the way the Space key does and checks that the
|
|
1489
|
+
* host recorded it. Core actions belong to the player (`host/<id>`);
|
|
1490
|
+
* `run()` applies the player's checks, so a source must be ready first,
|
|
1491
|
+
* and every run is appended to `coreRuns`.
|
|
1342
1492
|
* ```ts
|
|
1493
|
+
* import assert from "node:assert/strict";
|
|
1494
|
+
* declare const host: TestHost;
|
|
1495
|
+
* // ---cut---
|
|
1343
1496
|
* host.setSource({ phase: "ready", duration: 100, position: 10 });
|
|
1344
1497
|
* await host.coreAction("play-pause").run();
|
|
1345
1498
|
* assert.equal(host.source.paused, true);
|
|
@@ -1709,12 +1862,20 @@ export class TestHost {
|
|
|
1709
1862
|
* @param method - The wire method name.
|
|
1710
1863
|
* @param handler - Produces the reply from the request parameters.
|
|
1711
1864
|
*
|
|
1712
|
-
* @example
|
|
1865
|
+
* @example Make opening a folder fail
|
|
1866
|
+
* The plugin's Open Folder command meets a missing folder and must tell
|
|
1867
|
+
* the user. `handle` replaces the host's answer to one wire method; the
|
|
1868
|
+
* `HostError` it throws reaches the plugin as a rejection with code
|
|
1869
|
+
* `missing`, like the player's refusal. `reset` restores the default
|
|
1870
|
+
* behaviour for the rest of the test.
|
|
1713
1871
|
* ```ts
|
|
1872
|
+
* import assert from "node:assert/strict";
|
|
1873
|
+
* declare const host: TestHost;
|
|
1874
|
+
* // ---cut---
|
|
1714
1875
|
* host.handle("system.openPath", () => {
|
|
1715
1876
|
* throw new HostError("missing", "no such file");
|
|
1716
1877
|
* });
|
|
1717
|
-
* await host.command("open-folder").run();
|
|
1878
|
+
* await host.command("open-folder").run(); // the plugin catches `missing`
|
|
1718
1879
|
* assert.deepEqual(host.osd, ["Folder not found"]);
|
|
1719
1880
|
* host.reset("system.openPath");
|
|
1720
1881
|
* ```
|
|
@@ -1759,8 +1920,15 @@ export class TestHost {
|
|
|
1759
1920
|
* @param patch - Wire-level source fields, e.g. `phase`, `position`,
|
|
1760
1921
|
* `duration`, `paused`, `audioTracks`.
|
|
1761
1922
|
*
|
|
1762
|
-
* @example
|
|
1923
|
+
* @example Load a file, then let it play to the end
|
|
1924
|
+
* The plugin sees a one-minute source become ready, then end on its own.
|
|
1925
|
+
* `setSource` changes the host's source facts and broadcasts them like
|
|
1926
|
+
* the player's events; the SDK updates `player.state` when the event
|
|
1927
|
+
* arrives, so the test settles before reading it.
|
|
1763
1928
|
* ```ts
|
|
1929
|
+
* import assert from "node:assert/strict";
|
|
1930
|
+
* declare const host: TestHost;
|
|
1931
|
+
* // ---cut---
|
|
1764
1932
|
* host.setSource({ phase: "ready", duration: 60, position: 0 });
|
|
1765
1933
|
* await host.settle();
|
|
1766
1934
|
* assert.equal(plugin.player.state.phase, "ready");
|
|
@@ -1792,8 +1960,16 @@ export class TestHost {
|
|
|
1792
1960
|
* sidebar (`effective: false`, `reason: "fullscreen"`) and leaving
|
|
1793
1961
|
* fullscreen docks it again.
|
|
1794
1962
|
*
|
|
1795
|
-
* @example
|
|
1963
|
+
* @example Go fullscreen, then report picture-in-picture support
|
|
1964
|
+
* The plugin sees the main window fullscreen at 1920×1080, then learns
|
|
1965
|
+
* that picture-in-picture is available. `setMainWindow` changes the
|
|
1966
|
+
* host's window facts and broadcasts them as the player does when the
|
|
1967
|
+
* user resizes or toggles fullscreen; entering fullscreen also collapses
|
|
1968
|
+
* an attached sidebar.
|
|
1796
1969
|
* ```ts
|
|
1970
|
+
* import assert from "node:assert/strict";
|
|
1971
|
+
* declare const host: TestHost;
|
|
1972
|
+
* // ---cut---
|
|
1797
1973
|
* host.setMainWindow({ fullscreen: true, videoArea: { width: 1920, height: 1080 } });
|
|
1798
1974
|
* await host.settle();
|
|
1799
1975
|
* assert.equal(plugin.window.state.fullscreen, true);
|
|
@@ -1845,8 +2021,16 @@ export class TestHost {
|
|
|
1845
2021
|
* @param source - The path or URL.
|
|
1846
2022
|
* @param phase - The phase to end in (default `"ready"`).
|
|
1847
2023
|
*
|
|
1848
|
-
* @example
|
|
2024
|
+
* @example Let the user open another file meanwhile
|
|
2025
|
+
* A state the plugin captured before stops being current once another
|
|
2026
|
+
* file opens. `openElsewhere` gives the source a new `sourceId`, so
|
|
2027
|
+
* `isCurrent` turns false. Promise-returning calls made with
|
|
2028
|
+
* `ifCurrent: before` then reject with `stale`; calls that return nothing
|
|
2029
|
+
* drop the write silently instead (`TimelineLayer.set` with `ifCurrent`).
|
|
1849
2030
|
* ```ts
|
|
2031
|
+
* import assert from "node:assert/strict";
|
|
2032
|
+
* declare const host: TestHost;
|
|
2033
|
+
* // ---cut---
|
|
1850
2034
|
* const before = plugin.player.state;
|
|
1851
2035
|
* host.openElsewhere("/media/other.mp4");
|
|
1852
2036
|
* await host.settle();
|
|
@@ -1868,10 +2052,16 @@ export class TestHost {
|
|
|
1868
2052
|
* @param producerId - The producer, from {@link TestHost.producers}.
|
|
1869
2053
|
* @param message - The player's failure text.
|
|
1870
2054
|
*
|
|
1871
|
-
* @example
|
|
2055
|
+
* @example Break the plugin's generated stream
|
|
2056
|
+
* The plugin's stream stops with an error, as when the player loses its
|
|
2057
|
+
* audio device. `failStream` puts the source into `error` with the
|
|
2058
|
+
* message and retires the producer at once; the stream's `onStop`
|
|
2059
|
+
* listeners get a `stream` error carrying the message.
|
|
1872
2060
|
* ```ts
|
|
2061
|
+
* declare const host: TestHost;
|
|
2062
|
+
* // ---cut---
|
|
1873
2063
|
* const [producer] = host.producers.values();
|
|
1874
|
-
* host.failStream(producer
|
|
2064
|
+
* host.failStream(producer!.id, "producer transport closed");
|
|
1875
2065
|
* ```
|
|
1876
2066
|
*/
|
|
1877
2067
|
failStream(producerId, message) {
|
|
@@ -1901,8 +2091,14 @@ export class TestHost {
|
|
|
1901
2091
|
*
|
|
1902
2092
|
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
1903
2093
|
*
|
|
1904
|
-
* @example
|
|
2094
|
+
* @example Press a global key and a panel key
|
|
2095
|
+
* Ctrl+B runs whatever is bound globally; Delete is pressed while the
|
|
2096
|
+
* plugin's panel has focus, so panel bindings are tried first. `press`
|
|
2097
|
+
* follows the player's binding table and resolves with what happened:
|
|
2098
|
+
* `"ran"`, `"reason"` (a disabled command showed why) or `"passed"`.
|
|
1905
2099
|
* ```ts
|
|
2100
|
+
* declare const host: TestHost;
|
|
2101
|
+
* // ---cut---
|
|
1906
2102
|
* await host.press("Ctrl+B"); // a global binding
|
|
1907
2103
|
* await host.press("Delete", { panel: true }); // while the panel has focus
|
|
1908
2104
|
* ```
|
|
@@ -1937,8 +2133,14 @@ export class TestHost {
|
|
|
1937
2133
|
* `[binding, winner, ...ignored]` (command ids; core actions as
|
|
1938
2134
|
* `host/<id>`). Empty when there are none.
|
|
1939
2135
|
*
|
|
1940
|
-
* @example
|
|
2136
|
+
* @example Check that no default keys collide
|
|
2137
|
+
* The test fails when two commands, the player's or the plugin's, declare
|
|
2138
|
+
* the same binding. Each entry is `[binding, winner, ...ignored]`, so an
|
|
2139
|
+
* empty list means every key does what its command says.
|
|
1941
2140
|
* ```ts
|
|
2141
|
+
* import assert from "node:assert/strict";
|
|
2142
|
+
* declare const host: TestHost;
|
|
2143
|
+
* // ---cut---
|
|
1942
2144
|
* assert.deepEqual(host.conflicts, []);
|
|
1943
2145
|
* ```
|
|
1944
2146
|
*/
|
|
@@ -1991,8 +2193,14 @@ export class TestHost {
|
|
|
1991
2193
|
* player's source rules: the same source upgrades, another source needs
|
|
1992
2194
|
* `replace` (`conflict` otherwise), a built-in id is `unsupported`.
|
|
1993
2195
|
*
|
|
1994
|
-
* @example
|
|
2196
|
+
* @example Offer a package file the plugin can install
|
|
2197
|
+
* The plugin can validate and install `/downloads/someone.demo-1.0.0.vxp`
|
|
2198
|
+
* as if the file existed. The host accepts only paths listed here (others
|
|
2199
|
+
* fail with `missing`) and returns this manifest from `validate`; an
|
|
2200
|
+
* install then follows the player's source rules.
|
|
1995
2201
|
* ```ts
|
|
2202
|
+
* declare const host: TestHost;
|
|
2203
|
+
* // ---cut---
|
|
1996
2204
|
* host.candidates.set("/downloads/someone.demo-1.0.0.vxp", {
|
|
1997
2205
|
* id: "someone.demo", version: "1.0.0", apiVersion: "vestra/1",
|
|
1998
2206
|
* entry: "main.js", capabilities: [],
|
|
@@ -2013,8 +2221,14 @@ export class TestHost {
|
|
|
2013
2221
|
* the downloaded file. Pass a whole property value or a field of one: a
|
|
2014
2222
|
* document nested in rows is read the same way.
|
|
2015
2223
|
*
|
|
2016
|
-
* @example
|
|
2224
|
+
* @example Read back a Markdown message in a chat window
|
|
2225
|
+
* The test checks the Markdown source of the first chat message. Rows
|
|
2226
|
+
* hold the document in its wire form, as `plugin.ui.markdown` produced
|
|
2227
|
+
* it; `markdown` reads it back, nested in a row or not.
|
|
2017
2228
|
* ```ts
|
|
2229
|
+
* import assert from "node:assert/strict";
|
|
2230
|
+
* declare const host: TestHost;
|
|
2231
|
+
* // ---cut---
|
|
2018
2232
|
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
2019
2233
|
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
2020
2234
|
* ```
|
|
@@ -2104,9 +2318,17 @@ export class TestHost {
|
|
|
2104
2318
|
* Whether the bar places them is the bar's business: the list is what
|
|
2105
2319
|
* the player hands to `BarButtons`.
|
|
2106
2320
|
*
|
|
2107
|
-
* @example
|
|
2321
|
+
* @example Click the plugin's bottom-bar button in a test
|
|
2322
|
+
* The test finds the plugin's Store button, clicks it and checks that the
|
|
2323
|
+
* Store window opened. `barButtons` lists the buttons of the plugin's
|
|
2324
|
+
* commands declared with `button`, as the bar shows them; `click()` runs
|
|
2325
|
+
* the command like its menu item and waits until the host is idle, and
|
|
2326
|
+
* rejects while the command is disabled or busy.
|
|
2108
2327
|
* ```ts
|
|
2109
|
-
*
|
|
2328
|
+
* import assert from "node:assert/strict";
|
|
2329
|
+
* declare const host: TestHost;
|
|
2330
|
+
* // ---cut---
|
|
2331
|
+
* const [button] = host.barButtons; // one button per plugin
|
|
2110
2332
|
* assert.equal(button?.icon, "store");
|
|
2111
2333
|
* await button?.click();
|
|
2112
2334
|
* assert.equal(host.window("store").visible, true);
|
|
@@ -2149,8 +2371,15 @@ export class TestHost {
|
|
|
2149
2371
|
* extension: `ui/picture.slint` → `picture`). Throws when it was never
|
|
2150
2372
|
* opened with `show()`.
|
|
2151
2373
|
*
|
|
2152
|
-
* @example
|
|
2374
|
+
* @example Press a window's Reset button from a test
|
|
2375
|
+
* The Picture window's Reset callback runs and the brightness returns to
|
|
2376
|
+
* 0. The window is found by its id (the `.slint` file name by default);
|
|
2377
|
+
* `emit` fires the callback the plugin bound, and `settle()` waits for
|
|
2378
|
+
* the property the handler wrote.
|
|
2153
2379
|
* ```ts
|
|
2380
|
+
* import assert from "node:assert/strict";
|
|
2381
|
+
* declare const host: TestHost;
|
|
2382
|
+
* // ---cut---
|
|
2154
2383
|
* host.window("picture").emit("reset");
|
|
2155
2384
|
* await host.settle();
|
|
2156
2385
|
* assert.equal(host.window("picture").props["brightness"], 0);
|
|
@@ -2167,8 +2396,14 @@ export class TestHost {
|
|
|
2167
2396
|
* none is mounted. Its `props` use Slint (kebab-case) names; `emit` fires a
|
|
2168
2397
|
* callback on the panel's global.
|
|
2169
2398
|
*
|
|
2170
|
-
* @example
|
|
2399
|
+
* @example Select a row in the plugin's sidebar panel
|
|
2400
|
+
* The test checks the panel sits in the sidebar and selects its third
|
|
2401
|
+
* row. `emit` fires a callback on the panel's global as a click would;
|
|
2402
|
+
* the plugin's handler runs, and `settle()` waits for what it sent.
|
|
2171
2403
|
* ```ts
|
|
2404
|
+
* import assert from "node:assert/strict";
|
|
2405
|
+
* declare const host: TestHost;
|
|
2406
|
+
* // ---cut---
|
|
2172
2407
|
* const panel = host.mounted();
|
|
2173
2408
|
* assert.equal(panel.slot, "sidebar");
|
|
2174
2409
|
* panel.emit("select", 2);
|
|
@@ -2183,8 +2418,14 @@ export class TestHost {
|
|
|
2183
2418
|
/**
|
|
2184
2419
|
* The parameters of every request of one wire method, oldest first.
|
|
2185
2420
|
*
|
|
2186
|
-
* @example
|
|
2421
|
+
* @example Check the request behind an OSD message
|
|
2422
|
+
* The test checks the last OSD request the plugin sent, before
|
|
2423
|
+
* translation. `sent` lists the parameters of every request of one wire
|
|
2424
|
+
* method, oldest first; `host.osd` holds the texts as shown instead.
|
|
2187
2425
|
* ```ts
|
|
2426
|
+
* import assert from "node:assert/strict";
|
|
2427
|
+
* declare const host: TestHost;
|
|
2428
|
+
* // ---cut---
|
|
2188
2429
|
* assert.deepEqual(host.sent("osd.show").at(-1), { text: "Saved" });
|
|
2189
2430
|
* ```
|
|
2190
2431
|
*/
|
|
@@ -2206,14 +2447,31 @@ export class TestHost {
|
|
|
2206
2447
|
*
|
|
2207
2448
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
2208
2449
|
*
|
|
2209
|
-
* @example
|
|
2450
|
+
* @example Wait past a debounced save
|
|
2451
|
+
* The plugin's store writes its file 300 ms after the last `save`, so
|
|
2452
|
+
* the test waits 400 ms before it checks what the plugin did next.
|
|
2453
|
+
* `settle(ms)` returns when no request is in flight and at least `ms`
|
|
2454
|
+
* have passed, so the store's timer has fired. The file write itself is
|
|
2455
|
+
* the plugin's own work, which `settle` does not wait for: a test that
|
|
2456
|
+
* reads the file calls `await store.flush()` instead.
|
|
2210
2457
|
* ```ts
|
|
2458
|
+
* declare const host: TestHost;
|
|
2459
|
+
* // ---cut---
|
|
2460
|
+
* const store = plugin.storage.json("prefs", { defaults: { step: 5 } });
|
|
2211
2461
|
* store.save({ step: 10 });
|
|
2212
2462
|
* await host.settle(400); // past storage.json's 300 ms debounce
|
|
2213
2463
|
* ```
|
|
2214
2464
|
*
|
|
2215
|
-
* @example
|
|
2465
|
+
* @example Poll for work the plugin does on its own
|
|
2466
|
+
* The plugin reads and parses a file before it shows a status; `settle`
|
|
2467
|
+
* does not wait for that (it only waits for host requests), so the test
|
|
2468
|
+
* polls the property it asserts on, 20 ms at a time, for up to 2 seconds.
|
|
2216
2469
|
* ```ts
|
|
2470
|
+
* import assert from "node:assert/strict";
|
|
2471
|
+
* declare const host: TestHost;
|
|
2472
|
+
* declare const expected: string;
|
|
2473
|
+
* // ---cut---
|
|
2474
|
+
* const window = host.window("import");
|
|
2217
2475
|
* window.emit("load-file"); // the plugin reads and parses the file itself
|
|
2218
2476
|
* for (let i = 0; i < 100 && window.props["status-text"] !== expected; i++)
|
|
2219
2477
|
* await host.settle(20);
|
|
@@ -3841,19 +4099,43 @@ export class TestHost {
|
|
|
3841
4099
|
* plugin like the player stopping it (scopes released, stores flushed),
|
|
3842
4100
|
* wait until the host is idle and remove the host's temporary files.
|
|
3843
4101
|
*
|
|
3844
|
-
* @example
|
|
4102
|
+
* @example Test that a setting survives a restart
|
|
4103
|
+
* The test turns on the 10-step option, stops the plugin, starts it again
|
|
4104
|
+
* on the same data directory and checks the option is still on. Both runs
|
|
4105
|
+
* get a directory the test made (`dataDirectory`): `stop()` disposes the
|
|
4106
|
+
* plugin like the player, which writes its pending stores there, and only
|
|
4107
|
+
* removes the host's own temporary directory, so the second run reads what
|
|
4108
|
+
* the first one saved.
|
|
3845
4109
|
* ```ts
|
|
4110
|
+
* import assert from "node:assert/strict";
|
|
4111
|
+
* import { mkdtempSync, rmSync } from "node:fs";
|
|
4112
|
+
* import { tmpdir } from "node:os";
|
|
4113
|
+
* import { join } from "node:path";
|
|
4114
|
+
* import test from "node:test";
|
|
4115
|
+
* import { startPlugin } from "@vestra-player/sdk/testing";
|
|
4116
|
+
* import activate from "../main.ts";
|
|
4117
|
+
*
|
|
3846
4118
|
* test("remembers the step across restarts", async () => {
|
|
3847
4119
|
* const directory = new URL("..", import.meta.url);
|
|
3848
|
-
* const first = await startPlugin(activate, { directory });
|
|
3849
|
-
* await first.host.command("step-10").run();
|
|
3850
4120
|
* const data = mkdtempSync(join(tmpdir(), "data-"));
|
|
3851
|
-
*
|
|
3852
|
-
*
|
|
4121
|
+
* try {
|
|
4122
|
+
* const first = await startPlugin(activate, { directory, dataDirectory: data });
|
|
4123
|
+
* try {
|
|
4124
|
+
* await first.host.command("step-10").run();
|
|
4125
|
+
* } finally {
|
|
4126
|
+
* await first.stop(); // flushes the plugin's stores into `data`
|
|
4127
|
+
* }
|
|
3853
4128
|
*
|
|
3854
|
-
*
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
4129
|
+
* // A restart: a new host and a new plugin instance on the same data.
|
|
4130
|
+
* const second = await startPlugin(activate, { directory, dataDirectory: data });
|
|
4131
|
+
* try {
|
|
4132
|
+
* assert.equal(second.host.command("step-10").state.checked, true);
|
|
4133
|
+
* } finally {
|
|
4134
|
+
* await second.stop();
|
|
4135
|
+
* }
|
|
4136
|
+
* } finally {
|
|
4137
|
+
* rmSync(data, { recursive: true, force: true });
|
|
4138
|
+
* }
|
|
3857
4139
|
* });
|
|
3858
4140
|
* ```
|
|
3859
4141
|
*/
|