@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.
@@ -64,14 +64,27 @@ export { LIST_SEPARATORS };
64
64
  * The player's interface languages, English (the source) first, as the
65
65
  * player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
66
66
  *
67
- * @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.
68
73
  * ```ts
69
- * import { LOCALES } from "@vestra-player/sdk/testing";
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";
70
79
  *
80
+ * const directory = new URL("..", import.meta.url);
71
81
  * for (const locale of LOCALES.slice(1)) {
72
82
  * const { host, stop } = await startPlugin(activate, { directory, locale });
73
- * assert.notEqual(host.command("open").display.title, "Open…");
74
- * await stop();
83
+ * try {
84
+ * assert.notEqual(host.command("open").display.title, "Open…", locale);
85
+ * } finally {
86
+ * await stop(); // also when the assertion fails
87
+ * }
75
88
  * }
76
89
  * ```
77
90
  */
@@ -86,8 +99,16 @@ type Call = {
86
99
  * `plugin.windows.create(...)` window after its first `show()`. Get one with
87
100
  * {@link TestHost.window}.
88
101
  *
89
- * @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.
90
108
  * ```ts
109
+ * import assert from "node:assert/strict";
110
+ * declare const host: TestHost;
111
+ * // ---cut---
91
112
  * await host.command("open-picture").run(); // the plugin calls window.show()
92
113
  * const picture = host.window("picture");
93
114
  * assert.equal(picture.visible, true);
@@ -140,8 +161,16 @@ export interface FileDialogText {
140
161
  * host keeps it. Get one with {@link TestHost.command} (the plugin's own) or
141
162
  * {@link TestHost.coreAction} (the player's core actions).
142
163
  *
143
- * @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.
144
170
  * ```ts
171
+ * import assert from "node:assert/strict";
172
+ * declare const host: TestHost;
173
+ * // ---cut---
145
174
  * const toggle = host.command("toggle-loop");
146
175
  * assert.deepEqual(toggle.declaration.path, ["Playback", "Loop"]);
147
176
  * assert.equal(toggle.state.checked, false);
@@ -307,7 +336,13 @@ export interface MarkdownWire {
307
336
  * `calls`) use the protocol's shapes. They are fine for assertions but are
308
337
  * not part of the plugin API: their shape may change in minor versions.
309
338
  *
310
- * @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.
311
346
  * ```ts
312
347
  * import assert from "node:assert/strict";
313
348
  * import test from "node:test";
@@ -366,8 +401,15 @@ export declare class TestHost implements Transport {
366
401
  * is mounted there). You may change a slot directly and then call
367
402
  * `host.emit("window.state", {})` to simulate a change made elsewhere.
368
403
  *
369
- * @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.
370
409
  * ```ts
410
+ * import assert from "node:assert/strict";
411
+ * declare const host: TestHost;
412
+ * // ---cut---
371
413
  * assert.equal(host.layout.sidebar?.preferred, true);
372
414
  * await plugin.panels.sidebar.toggle();
373
415
  * assert.equal(host.layout.sidebar?.preferred, false);
@@ -408,8 +450,17 @@ export declare class TestHost implements Transport {
408
450
  * players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
409
451
  * passing; set `locale` to test what a user sees now.
410
452
  *
411
- * @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.
412
459
  * ```ts
460
+ * import assert from "node:assert/strict";
461
+ * declare function activate(plugin: Plugin): Promise<void>;
462
+ * declare const directory: URL;
463
+ * // ---cut---
413
464
  * const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
414
465
  * assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
415
466
  * await stop();
@@ -439,8 +490,14 @@ export declare class TestHost implements Transport {
439
490
  * @param args - Values for its placeholders.
440
491
  * @returns The shown text.
441
492
  *
442
- * @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.
443
497
  * ```ts
498
+ * import assert from "node:assert/strict";
499
+ * declare const host: TestHost;
500
+ * // ---cut---
444
501
  * assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
445
502
  * ```
446
503
  *
@@ -501,10 +558,16 @@ export declare class TestHost implements Transport {
501
558
  * What `player.stats()` returns. Fields added in later rounds may be left
502
559
  * out; the SDK fills them with `null`.
503
560
  *
504
- * @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.
505
566
  * ```ts
567
+ * declare const host: TestHost;
568
+ * // ---cut---
506
569
  * host.stats = {
507
- * ...host.stats,
570
+ * ...host.stats, // keep the other fields
508
571
  * video: { codec: "h264", width: 1920, height: 1080, fps: 24 },
509
572
  * } as typeof host.stats;
510
573
  * ```
@@ -538,12 +601,21 @@ export declare class TestHost implements Transport {
538
601
  * handler is not found (`available()` is false, `run()` rejects with
539
602
  * `unavailable`).
540
603
  *
541
- * @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.
542
610
  * ```ts
611
+ * import assert from "node:assert/strict";
612
+ * declare const host: TestHost;
613
+ * // ---cut---
543
614
  * host.tools.handle("ffprobe", (args) => {
544
- * assert.ok(args.includes("-show_streams"));
545
- * 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
546
617
  * });
618
+ * // Every ffmpeg run rejects with code `failed`.
547
619
  * host.tools.handle("ffmpeg", () => {
548
620
  * throw new HostError("failed", "Invalid data found when processing input");
549
621
  * });
@@ -637,12 +709,20 @@ export declare class TestHost implements Transport {
637
709
  * @param pluginId - The other plugin's manifest id.
638
710
  * @param marks - Its whole layer.
639
711
  *
640
- * @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.
641
718
  * ```ts
719
+ * import assert from "node:assert/strict";
720
+ * declare const host: TestHost;
721
+ * // ---cut---
642
722
  * host.setTimelineLayer("community.chapters", [
643
723
  * { time: 0, kind: "chapter", title: "Intro" },
644
724
  * { time: 95, kind: "chapter", title: "Part 1" },
645
- * ]);
725
+ * ]); // replaces that plugin's whole layer; [] would clear it
646
726
  * await host.settle();
647
727
  * assert.equal(plugin.timeline.marks.length, 2);
648
728
  * ```
@@ -672,8 +752,15 @@ export declare class TestHost implements Transport {
672
752
  * runtime image. View properties hold images in their wire form; this
673
753
  * reads the path back.
674
754
  *
675
- * @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.
676
759
  * ```ts
760
+ * import assert from "node:assert/strict";
761
+ * import { join } from "node:path";
762
+ * declare const host: TestHost;
763
+ * // ---cut---
677
764
  * assert.equal(
678
765
  * host.imagePath(host.window("cover").props["cover"]),
679
766
  * join(plugin.dataDirectory, "cover.png"),
@@ -692,8 +779,9 @@ export declare class TestHost implements Transport {
692
779
  panelDrops: boolean;
693
780
  /**
694
781
  * Drop files like the player: on a panel with an `onDrop` listener the
695
- * whole list goes there; otherwise (and on the video area) grouped by
696
- * extension to the first provider (`folder` for existing directories).
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.
697
785
  * Files nobody takes are ignored. Resolves once the host is idle again.
698
786
  *
699
787
  * @param paths - The dropped paths, in the order the file manager gives.
@@ -701,9 +789,17 @@ export declare class TestHost implements Transport {
701
789
  * drop point in panel coordinates (default `{ x: 0, y: 0 }`);
702
790
  * `modifiers`: keys held (default all `false`).
703
791
  *
704
- * @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.
705
798
  * ```ts
706
- * await host.drop(["/media/a.srt", "/media/b.mkv"]);
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.
707
803
  * await host.drop(["/media/list.m3u"], {
708
804
  * target: "panel",
709
805
  * at: { x: 10, y: 48 },
@@ -764,8 +860,15 @@ export declare class TestHost implements Transport {
764
860
  /**
765
861
  * One settings page by id. Throws when it was never registered.
766
862
  *
767
- * @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.
768
868
  * ```ts
869
+ * import assert from "node:assert/strict";
870
+ * declare const host: TestHost;
871
+ * // ---cut---
769
872
  * const page = host.settingsPage("general");
770
873
  * page.emit("setStep", 10);
771
874
  * await host.settle();
@@ -814,11 +917,20 @@ export declare class TestHost implements Transport {
814
917
  * @param plugin - The other plugin's manifest id.
815
918
  * @param options - `title`: its tab label (default: `plugin`).
816
919
  *
817
- * @example The playlist next to this plugin's sidebar panel
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).
818
926
  * ```ts
927
+ * import assert from "node:assert/strict";
928
+ * declare function activate(plugin: Plugin): Promise<void>;
929
+ * declare const directory: URL;
930
+ * // ---cut---
819
931
  * const host = new TestHost();
820
932
  * host.mountOther("sidebar", "vestra.playlist", { title: "播放列表" });
821
- * const { plugin } = await startPlugin(activate, { host, directory });
933
+ * await startPlugin(activate, { host, directory });
822
934
  * assert.equal(host.layout.sidebar?.panels?.length, 2);
823
935
  * ```
824
936
  *
@@ -882,8 +994,18 @@ export declare class TestHost implements Transport {
882
994
  * or switches to software rendering (`unsupported`, which also sets
883
995
  * `videoRender.gpu` to false). Pushed only when this plugin is the owner.
884
996
  *
885
- * @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.
886
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();
887
1009
  * host.failVideoRender("failed", "shader compile failed: ERROR: 0:39: ...");
888
1010
  * await host.settle();
889
1011
  * assert.equal(render.status.state, "failed");
@@ -895,8 +1017,15 @@ export declare class TestHost implements Transport {
895
1017
  * versions. `resources` holds each `video.filter` / `audio.filter` chain the
896
1018
  * plugin applied, keyed by the SDK's resource id.
897
1019
  *
898
- * @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.
899
1025
  * ```ts
1026
+ * import assert from "node:assert/strict";
1027
+ * declare const host: TestHost;
1028
+ * // ---cut---
900
1029
  * const [chain] = [...host.processing.resources.values()];
901
1030
  * assert.deepEqual(chain?.nodes, [{ kind: "hue", degrees: 30 }]);
902
1031
  * ```
@@ -992,10 +1121,14 @@ export declare class TestHost implements Transport {
992
1121
  * extensions (or scheme + host pairs) is refused with `conflict`, which
993
1122
  * does not fail activation, like the player.
994
1123
  *
995
- * @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.
996
1129
  * ```ts
997
1130
  * const host = new TestHost();
998
- * host.otherResolvers.push({ owner: "example.emulator", extensions: ["nes"] });
1131
+ * host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
999
1132
  * ```
1000
1133
  */
1001
1134
  readonly otherResolvers: {
@@ -1020,12 +1153,19 @@ export declare class TestHost implements Transport {
1020
1153
  * after changing {@link TestHost.otherResolvers} (another plugin started
1021
1154
  * or stopped).
1022
1155
  *
1023
- * @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.
1024
1161
  * ```ts
1025
- * host.otherResolvers.push({ owner: "example.emulator", extensions: ["nes"] });
1162
+ * import assert from "node:assert/strict";
1163
+ * declare const host: TestHost;
1164
+ * // ---cut---
1165
+ * host.otherResolvers.push({ owner: "example.emulator", extensions: ["ch8"] });
1026
1166
  * host.updateFileTypes();
1027
1167
  * await host.settle();
1028
- * assert.deepEqual(plugin.resolvers.fileTypes, ["nes"]);
1168
+ * assert.deepEqual(plugin.resolvers.fileTypes, ["ch8"]);
1029
1169
  * ```
1030
1170
  */
1031
1171
  updateFileTypes(): void;
@@ -1055,8 +1195,15 @@ export declare class TestHost implements Transport {
1055
1195
  * For `openFile` / `openDirectory` / `saveFile` queue a one-path array;
1056
1196
  * for `openFiles` any number of paths.
1057
1197
  *
1058
- * @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.
1059
1204
  * ```ts
1205
+ * declare const host: TestHost;
1206
+ * // ---cut---
1060
1207
  * host.dialogAnswers.push(["/media/movie.mkv"]); // openFile picks it
1061
1208
  * host.dialogAnswers.push(null); // the next file dialog is cancelled
1062
1209
  * host.dialogAnswers.push({ confirmed: true, checked: false }); // confirm
@@ -1168,12 +1315,20 @@ export declare class TestHost implements Transport {
1168
1315
  * source, for keyframe step seeks. `null` (the default): none modelled,
1169
1316
  * and step seeks are exact even with `stepSeekKeyframes`.
1170
1317
  *
1171
- * @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.
1172
1324
  * ```ts
1325
+ * import assert from "node:assert/strict";
1326
+ * declare const host: TestHost;
1327
+ * // ---cut---
1173
1328
  * host.setSource({ phase: "ready", duration: 45, position: 12 });
1174
1329
  * host.keyframes = [0, 10, 20, 30, 40];
1175
1330
  * await plugin.player.setStepSeek({ seconds: 5, keyframes: true });
1176
- * await plugin.host.run("seek-forward");
1331
+ * await plugin.host.run("seek-forward"); // target 17 s, next keyframe 20 s
1177
1332
  * assert.equal(host.source.position, 20);
1178
1333
  * assert.equal(host.osd.at(-1), "Forward 5 s: 00:20 / 00:45");
1179
1334
  * ```
@@ -1221,8 +1376,15 @@ export declare class TestHost implements Transport {
1221
1376
  * an unknown id. `run()` executes it with the player's checks and writes
1222
1377
  * its on-screen text to {@link TestHost.osd}.
1223
1378
  *
1224
- * @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`.
1225
1384
  * ```ts
1385
+ * import assert from "node:assert/strict";
1386
+ * declare const host: TestHost;
1387
+ * // ---cut---
1226
1388
  * host.setSource({ phase: "ready", duration: 100, position: 10 });
1227
1389
  * await host.coreAction("play-pause").run();
1228
1390
  * assert.equal(host.source.paused, true);
@@ -1252,12 +1414,20 @@ export declare class TestHost implements Transport {
1252
1414
  * @param method - The wire method name.
1253
1415
  * @param handler - Produces the reply from the request parameters.
1254
1416
  *
1255
- * @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.
1256
1423
  * ```ts
1424
+ * import assert from "node:assert/strict";
1425
+ * declare const host: TestHost;
1426
+ * // ---cut---
1257
1427
  * host.handle("system.openPath", () => {
1258
1428
  * throw new HostError("missing", "no such file");
1259
1429
  * });
1260
- * await host.command("open-folder").run();
1430
+ * await host.command("open-folder").run(); // the plugin catches `missing`
1261
1431
  * assert.deepEqual(host.osd, ["Folder not found"]);
1262
1432
  * host.reset("system.openPath");
1263
1433
  * ```
@@ -1286,8 +1456,15 @@ export declare class TestHost implements Transport {
1286
1456
  * @param patch - Wire-level source fields, e.g. `phase`, `position`,
1287
1457
  * `duration`, `paused`, `audioTracks`.
1288
1458
  *
1289
- * @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.
1290
1464
  * ```ts
1465
+ * import assert from "node:assert/strict";
1466
+ * declare const host: TestHost;
1467
+ * // ---cut---
1291
1468
  * host.setSource({ phase: "ready", duration: 60, position: 0 });
1292
1469
  * await host.settle();
1293
1470
  * assert.equal(plugin.player.state.phase, "ready");
@@ -1303,8 +1480,16 @@ export declare class TestHost implements Transport {
1303
1480
  * sidebar (`effective: false`, `reason: "fullscreen"`) and leaving
1304
1481
  * fullscreen docks it again.
1305
1482
  *
1306
- * @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.
1307
1489
  * ```ts
1490
+ * import assert from "node:assert/strict";
1491
+ * declare const host: TestHost;
1492
+ * // ---cut---
1308
1493
  * host.setMainWindow({ fullscreen: true, videoArea: { width: 1920, height: 1080 } });
1309
1494
  * await host.settle();
1310
1495
  * assert.equal(plugin.window.state.fullscreen, true);
@@ -1336,8 +1521,16 @@ export declare class TestHost implements Transport {
1336
1521
  * @param source - The path or URL.
1337
1522
  * @param phase - The phase to end in (default `"ready"`).
1338
1523
  *
1339
- * @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`).
1340
1530
  * ```ts
1531
+ * import assert from "node:assert/strict";
1532
+ * declare const host: TestHost;
1533
+ * // ---cut---
1341
1534
  * const before = plugin.player.state;
1342
1535
  * host.openElsewhere("/media/other.mp4");
1343
1536
  * await host.settle();
@@ -1354,10 +1547,16 @@ export declare class TestHost implements Transport {
1354
1547
  * @param producerId - The producer, from {@link TestHost.producers}.
1355
1548
  * @param message - The player's failure text.
1356
1549
  *
1357
- * @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.
1358
1555
  * ```ts
1556
+ * declare const host: TestHost;
1557
+ * // ---cut---
1359
1558
  * const [producer] = host.producers.values();
1360
- * host.failStream(producer.id, "producer transport closed");
1559
+ * host.failStream(producer!.id, "producer transport closed");
1361
1560
  * ```
1362
1561
  */
1363
1562
  failStream(producerId: number, message: string): void;
@@ -1378,8 +1577,14 @@ export declare class TestHost implements Transport {
1378
1577
  *
1379
1578
  * @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
1380
1579
  *
1381
- * @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"`.
1382
1585
  * ```ts
1586
+ * declare const host: TestHost;
1587
+ * // ---cut---
1383
1588
  * await host.press("Ctrl+B"); // a global binding
1384
1589
  * await host.press("Delete", { panel: true }); // while the panel has focus
1385
1590
  * ```
@@ -1392,8 +1597,14 @@ export declare class TestHost implements Transport {
1392
1597
  * `[binding, winner, ...ignored]` (command ids; core actions as
1393
1598
  * `host/<id>`). Empty when there are none.
1394
1599
  *
1395
- * @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.
1396
1604
  * ```ts
1605
+ * import assert from "node:assert/strict";
1606
+ * declare const host: TestHost;
1607
+ * // ---cut---
1397
1608
  * assert.deepEqual(host.conflicts, []);
1398
1609
  * ```
1399
1610
  */
@@ -1455,8 +1666,14 @@ export declare class TestHost implements Transport {
1455
1666
  * player's source rules: the same source upgrades, another source needs
1456
1667
  * `replace` (`conflict` otherwise), a built-in id is `unsupported`.
1457
1668
  *
1458
- * @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.
1459
1674
  * ```ts
1675
+ * declare const host: TestHost;
1676
+ * // ---cut---
1460
1677
  * host.candidates.set("/downloads/someone.demo-1.0.0.vxp", {
1461
1678
  * id: "someone.demo", version: "1.0.0", apiVersion: "vestra/1",
1462
1679
  * entry: "main.js", capabilities: [],
@@ -1477,8 +1694,14 @@ export declare class TestHost implements Transport {
1477
1694
  * the downloaded file. Pass a whole property value or a field of one: a
1478
1695
  * document nested in rows is read the same way.
1479
1696
  *
1480
- * @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.
1481
1701
  * ```ts
1702
+ * import assert from "node:assert/strict";
1703
+ * declare const host: TestHost;
1704
+ * // ---cut---
1482
1705
  * const rows = host.window("chat").props["messages"] as { body: Json }[];
1483
1706
  * assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
1484
1707
  * ```
@@ -1517,9 +1740,17 @@ export declare class TestHost implements Transport {
1517
1740
  * Whether the bar places them is the bar's business: the list is what
1518
1741
  * the player hands to `BarButtons`.
1519
1742
  *
1520
- * @example
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.
1521
1749
  * ```ts
1522
- * const [button] = host.barButtons;
1750
+ * import assert from "node:assert/strict";
1751
+ * declare const host: TestHost;
1752
+ * // ---cut---
1753
+ * const [button] = host.barButtons; // one button per plugin
1523
1754
  * assert.equal(button?.icon, "store");
1524
1755
  * await button?.click();
1525
1756
  * assert.equal(host.window("store").visible, true);
@@ -1533,8 +1764,15 @@ export declare class TestHost implements Transport {
1533
1764
  * extension: `ui/picture.slint` → `picture`). Throws when it was never
1534
1765
  * opened with `show()`.
1535
1766
  *
1536
- * @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.
1537
1772
  * ```ts
1773
+ * import assert from "node:assert/strict";
1774
+ * declare const host: TestHost;
1775
+ * // ---cut---
1538
1776
  * host.window("picture").emit("reset");
1539
1777
  * await host.settle();
1540
1778
  * assert.equal(host.window("picture").props["brightness"], 0);
@@ -1546,8 +1784,14 @@ export declare class TestHost implements Transport {
1546
1784
  * none is mounted. Its `props` use Slint (kebab-case) names; `emit` fires a
1547
1785
  * callback on the panel's global.
1548
1786
  *
1549
- * @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.
1550
1791
  * ```ts
1792
+ * import assert from "node:assert/strict";
1793
+ * declare const host: TestHost;
1794
+ * // ---cut---
1551
1795
  * const panel = host.mounted();
1552
1796
  * assert.equal(panel.slot, "sidebar");
1553
1797
  * panel.emit("select", 2);
@@ -1565,8 +1809,14 @@ export declare class TestHost implements Transport {
1565
1809
  /**
1566
1810
  * The parameters of every request of one wire method, oldest first.
1567
1811
  *
1568
- * @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.
1569
1816
  * ```ts
1817
+ * import assert from "node:assert/strict";
1818
+ * declare const host: TestHost;
1819
+ * // ---cut---
1570
1820
  * assert.deepEqual(host.sent("osd.show").at(-1), { text: "Saved" });
1571
1821
  * ```
1572
1822
  */
@@ -1584,14 +1834,31 @@ export declare class TestHost implements Transport {
1584
1834
  *
1585
1835
  * @param ms - Extra time to wait, for debounces and timers (default 0).
1586
1836
  *
1587
- * @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.
1588
1844
  * ```ts
1845
+ * declare const host: TestHost;
1846
+ * // ---cut---
1847
+ * const store = plugin.storage.json("prefs", { defaults: { step: 5 } });
1589
1848
  * store.save({ step: 10 });
1590
1849
  * await host.settle(400); // past storage.json's 300 ms debounce
1591
1850
  * ```
1592
1851
  *
1593
- * @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.
1594
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");
1595
1862
  * window.emit("load-file"); // the plugin reads and parses the file itself
1596
1863
  * for (let i = 0; i < 100 && window.props["status-text"] !== expected; i++)
1597
1864
  * await host.settle(20);
@@ -1628,19 +1895,43 @@ export declare class TestHost implements Transport {
1628
1895
  * plugin like the player stopping it (scopes released, stores flushed),
1629
1896
  * wait until the host is idle and remove the host's temporary files.
1630
1897
  *
1631
- * @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.
1632
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
+ *
1633
1914
  * test("remembers the step across restarts", async () => {
1634
1915
  * const directory = new URL("..", import.meta.url);
1635
- * const first = await startPlugin(activate, { directory });
1636
- * await first.host.command("step-10").run();
1637
1916
  * const data = mkdtempSync(join(tmpdir(), "data-"));
1638
- * cpSync(first.host.dataDirectory, data, { recursive: true });
1639
- * await first.stop();
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
+ * }
1640
1924
  *
1641
- * const second = await startPlugin(activate, { directory, dataDirectory: data });
1642
- * assert.equal(second.host.command("step-10").state.checked, true);
1643
- * await second.stop();
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
+ * }
1644
1935
  * });
1645
1936
  * ```
1646
1937
  */