@vestra-player/sdk 1.5.30 → 1.5.33
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 +26 -0
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.js +4 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +14 -0
- package/dist/i18n.d.ts +9 -0
- package/dist/i18n.js +9 -0
- package/dist/manifest.js +1 -1
- package/dist/palettes.d.ts +2 -0
- package/dist/palettes.js +2 -0
- package/dist/plugin/bindings.d.ts +2 -0
- package/dist/plugin/bindings.js +2 -0
- package/dist/plugin/osd.d.ts +2 -0
- package/dist/plugin/osd.js +2 -0
- package/dist/plugin/services.d.ts +6 -1
- package/dist/plugin/services.js +6 -1
- package/dist/plugin/testing.d.ts +518 -9
- package/dist/plugin/testing.js +385 -17
- package/dist/plugin/types.d.ts +2921 -101
- package/dist/plugin/values.d.ts +7 -2
- package/dist/plugin/values.js +7 -2
- package/dist/processing.d.ts +6 -0
- package/dist/processing.js +1 -1
- package/dist/source-resolvers.d.ts +3 -0
- package/dist/subtitles.d.ts +3 -0
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +10 -0
- package/llms-full.txt +2526 -83
- package/llms.txt +23 -23
- package/package.json +1 -1
package/dist/plugin/testing.js
CHANGED
|
@@ -307,6 +307,19 @@ const MAX_SIDEBAR_TABS = 8;
|
|
|
307
307
|
* await stop();
|
|
308
308
|
* });
|
|
309
309
|
* ```
|
|
310
|
+
*
|
|
311
|
+
* @remarks
|
|
312
|
+
* An in-memory player that speaks the real wire protocol, so the SDK's own
|
|
313
|
+
* logic runs as it does in the player; only the player side is simulated
|
|
314
|
+
* (no decoding, no Slint, no windows). Make one per test, preset it if the
|
|
315
|
+
* plugin reads something during activation, start the plugin with
|
|
316
|
+
* {@link startPlugin}, drive it, `settle()`, and assert on what it
|
|
317
|
+
* recorded.
|
|
318
|
+
*
|
|
319
|
+
* @capability None.
|
|
320
|
+
* @see [Test a plugin](../../docs/how-to/testing.md)
|
|
321
|
+
* @since 1.0.0
|
|
322
|
+
* @category Testing
|
|
310
323
|
*/
|
|
311
324
|
export class TestHost {
|
|
312
325
|
/**
|
|
@@ -416,6 +429,20 @@ export class TestHost {
|
|
|
416
429
|
*
|
|
417
430
|
* @param directory - The package directory (holding `locales/`).
|
|
418
431
|
* @since 1.5.9
|
|
432
|
+
*
|
|
433
|
+
* @capability None.
|
|
434
|
+
*
|
|
435
|
+
* @example Load the translations after changing the language
|
|
436
|
+
* The test switches the host to German before starting the plugin and
|
|
437
|
+
* loads that catalog itself (`startPlugin` does the same). Texts are then
|
|
438
|
+
* shown in German.
|
|
439
|
+
* ```ts
|
|
440
|
+
* declare const host: TestHost;
|
|
441
|
+
* declare const directory: string;
|
|
442
|
+
* // ---cut---
|
|
443
|
+
* host.locale = "de";
|
|
444
|
+
* host.loadCatalog(directory); // locales/de.po (or de.json)
|
|
445
|
+
* ```
|
|
419
446
|
*/
|
|
420
447
|
loadCatalog(directory) {
|
|
421
448
|
this.#catalog = loadPackageCatalog(directory, this.locale);
|
|
@@ -441,6 +468,8 @@ export class TestHost {
|
|
|
441
468
|
* ```
|
|
442
469
|
*
|
|
443
470
|
* @since 1.5.9
|
|
471
|
+
*
|
|
472
|
+
* @capability None.
|
|
444
473
|
*/
|
|
445
474
|
translate(text, args) {
|
|
446
475
|
if (typeof text !== "string")
|
|
@@ -520,6 +549,24 @@ export class TestHost {
|
|
|
520
549
|
*
|
|
521
550
|
* @param language - `"system"` or a locale.
|
|
522
551
|
* @since 1.5.9
|
|
552
|
+
*
|
|
553
|
+
* @capability None.
|
|
554
|
+
*
|
|
555
|
+
* @example Simulate the user choosing French for the next start
|
|
556
|
+
* The plugin hears that the saved interface language changed. The host
|
|
557
|
+
* pushes `appearance.changed`, which a plugin with `events` sees in
|
|
558
|
+
* `appearance.language` once it has read it and subscribed; this run's
|
|
559
|
+
* language does not change.
|
|
560
|
+
* ```ts
|
|
561
|
+
* import assert from "node:assert/strict";
|
|
562
|
+
* declare const host: TestHost;
|
|
563
|
+
* // ---cut---
|
|
564
|
+
* void plugin.appearance.language; // read once: the SDK then follows it
|
|
565
|
+
* plugin.appearance.onChange(() => {}); // subscribes to appearance events
|
|
566
|
+
* host.setLanguage("fr");
|
|
567
|
+
* await host.settle();
|
|
568
|
+
* assert.equal(plugin.appearance.language, "fr");
|
|
569
|
+
* ```
|
|
523
570
|
*/
|
|
524
571
|
setLanguage(language) {
|
|
525
572
|
this.language = language;
|
|
@@ -713,6 +760,9 @@ export class TestHost {
|
|
|
713
760
|
* await host.settle();
|
|
714
761
|
* assert.equal(plugin.timeline.marks.length, 2);
|
|
715
762
|
* ```
|
|
763
|
+
*
|
|
764
|
+
* @capability None.
|
|
765
|
+
* @since 1.0.0
|
|
716
766
|
*/
|
|
717
767
|
setTimelineLayer(pluginId, marks) {
|
|
718
768
|
if (marks.length)
|
|
@@ -755,6 +805,9 @@ export class TestHost {
|
|
|
755
805
|
* join(plugin.dataDirectory, "cover.png"),
|
|
756
806
|
* );
|
|
757
807
|
* ```
|
|
808
|
+
*
|
|
809
|
+
* @capability None.
|
|
810
|
+
* @since 1.0.0
|
|
758
811
|
*/
|
|
759
812
|
imagePath(value) {
|
|
760
813
|
const image = value;
|
|
@@ -865,6 +918,10 @@ export class TestHost {
|
|
|
865
918
|
* modifiers: { alt: true },
|
|
866
919
|
* });
|
|
867
920
|
* ```
|
|
921
|
+
*
|
|
922
|
+
* @capability None.
|
|
923
|
+
* @throws Never: files nobody takes are ignored, as in the player.
|
|
924
|
+
* @since 1.0.0
|
|
868
925
|
*/
|
|
869
926
|
drop(paths, options = {}) {
|
|
870
927
|
const modifiers = {
|
|
@@ -899,6 +956,24 @@ export class TestHost {
|
|
|
899
956
|
/**
|
|
900
957
|
* Simulate another plugin switching the palette: updates
|
|
901
958
|
* {@link TestHost.appearance} and pushes `appearance.changed`.
|
|
959
|
+
*
|
|
960
|
+
* @capability None.
|
|
961
|
+
*
|
|
962
|
+
* @example Simulate another plugin switching to the light palette
|
|
963
|
+
* The plugin hears the palette change. The host updates `appearance` and
|
|
964
|
+
* pushes `appearance.changed` to a plugin that listens with `events`.
|
|
965
|
+
* ```ts
|
|
966
|
+
* import assert from "node:assert/strict";
|
|
967
|
+
* declare const host: TestHost;
|
|
968
|
+
* // ---cut---
|
|
969
|
+
* const themes: ThemeId[] = [];
|
|
970
|
+
* plugin.appearance.onChange((theme) => themes.push(theme)); // subscribes (`events`)
|
|
971
|
+
* host.setAppearance("vestra-light");
|
|
972
|
+
* await host.settle();
|
|
973
|
+
* assert.deepEqual(themes, ["vestra-light"]);
|
|
974
|
+
* ```
|
|
975
|
+
*
|
|
976
|
+
* @since 1.0.0
|
|
902
977
|
*/
|
|
903
978
|
setAppearance(theme) {
|
|
904
979
|
this.appearance = theme;
|
|
@@ -935,6 +1010,9 @@ export class TestHost {
|
|
|
935
1010
|
* await host.settle();
|
|
936
1011
|
* assert.equal(page.props["step"], 10);
|
|
937
1012
|
* ```
|
|
1013
|
+
*
|
|
1014
|
+
* @capability None.
|
|
1015
|
+
* @since 1.0.0
|
|
938
1016
|
*/
|
|
939
1017
|
settingsPage(id) {
|
|
940
1018
|
const page = this.settings.pages.get(id);
|
|
@@ -947,6 +1025,23 @@ export class TestHost {
|
|
|
947
1025
|
/**
|
|
948
1026
|
* Simulate the backdrop's `effective` changing (`false`: a video frame
|
|
949
1027
|
* appeared; `true`: there is none again); pushed to the plugin.
|
|
1028
|
+
*
|
|
1029
|
+
* @capability None.
|
|
1030
|
+
*
|
|
1031
|
+
* @example Make a video picture replace the backdrop
|
|
1032
|
+
* The plugin's backdrop hears that a video frame now covers it. `false`
|
|
1033
|
+
* means a picture appeared; the change is pushed like the player's
|
|
1034
|
+
* `window.state`.
|
|
1035
|
+
* ```ts
|
|
1036
|
+
* import assert from "node:assert/strict";
|
|
1037
|
+
* declare const host: TestHost;
|
|
1038
|
+
* // ---cut---
|
|
1039
|
+
* host.setBackdropEffective(false);
|
|
1040
|
+
* await host.settle();
|
|
1041
|
+
* assert.equal(plugin.panels.backdrop.state?.effective, false);
|
|
1042
|
+
* ```
|
|
1043
|
+
*
|
|
1044
|
+
* @since 1.0.0
|
|
950
1045
|
*/
|
|
951
1046
|
setBackdropEffective(effective) {
|
|
952
1047
|
if (this.backdrop)
|
|
@@ -986,6 +1081,8 @@ export class TestHost {
|
|
|
986
1081
|
* ```
|
|
987
1082
|
*
|
|
988
1083
|
* @since 1.5.2
|
|
1084
|
+
*
|
|
1085
|
+
* @capability None.
|
|
989
1086
|
*/
|
|
990
1087
|
mountOther(slot, plugin, options = {}) {
|
|
991
1088
|
if (this.#others.has(plugin))
|
|
@@ -1009,6 +1106,21 @@ export class TestHost {
|
|
|
1009
1106
|
* when its tab was current, the next tab (else the previous one) is.
|
|
1010
1107
|
*
|
|
1011
1108
|
* @since 1.5.2
|
|
1109
|
+
*
|
|
1110
|
+
* @capability None.
|
|
1111
|
+
*
|
|
1112
|
+
* @example Let the playlist's tab go away
|
|
1113
|
+
* The other plugin's sidebar tab disappears and the plugin's own tab
|
|
1114
|
+
* becomes current. When the removed tab was current, the next tab (else
|
|
1115
|
+
* the previous one) becomes current, as in the player.
|
|
1116
|
+
* ```ts
|
|
1117
|
+
* import assert from "node:assert/strict";
|
|
1118
|
+
* declare const host: TestHost;
|
|
1119
|
+
* // ---cut---
|
|
1120
|
+
* host.unmountOther("vestra.playlist");
|
|
1121
|
+
* await host.settle();
|
|
1122
|
+
* assert.equal(host.layout.sidebar?.panels?.length, 1);
|
|
1123
|
+
* ```
|
|
1012
1124
|
*/
|
|
1013
1125
|
unmountOther(plugin) {
|
|
1014
1126
|
const other = this.#others.get(plugin);
|
|
@@ -1027,6 +1139,20 @@ export class TestHost {
|
|
|
1027
1139
|
* becomes the current tab. Pushed as a `window.state` change.
|
|
1028
1140
|
*
|
|
1029
1141
|
* @since 1.5.2
|
|
1142
|
+
*
|
|
1143
|
+
* @capability None.
|
|
1144
|
+
*
|
|
1145
|
+
* @example Click another plugin's sidebar tab
|
|
1146
|
+
* The user switches the sidebar to the playlist; the plugin's panel is in
|
|
1147
|
+
* a background tab now. The change is pushed as `window.state`.
|
|
1148
|
+
* ```ts
|
|
1149
|
+
* import assert from "node:assert/strict";
|
|
1150
|
+
* declare const host: TestHost;
|
|
1151
|
+
* // ---cut---
|
|
1152
|
+
* host.selectTab("vestra.playlist");
|
|
1153
|
+
* await host.settle();
|
|
1154
|
+
* assert.notEqual(host.layout.sidebar?.owner, 1); // the plugin under test is owner 1
|
|
1155
|
+
* ```
|
|
1030
1156
|
*/
|
|
1031
1157
|
selectTab(plugin) {
|
|
1032
1158
|
const slot = this.layout.sidebar;
|
|
@@ -1077,6 +1203,24 @@ export class TestHost {
|
|
|
1077
1203
|
* Simulate another plugin taking the display layer first (the plugin's
|
|
1078
1204
|
* `video.view()` then fails with `conflict`), or, with `null`, releasing
|
|
1079
1205
|
* it (values reset to the defaults).
|
|
1206
|
+
*
|
|
1207
|
+
* @capability None.
|
|
1208
|
+
*
|
|
1209
|
+
* @example Let another plugin own the display layer
|
|
1210
|
+
* The plugin's `video.view()` fails with `conflict` because another plugin
|
|
1211
|
+
* took the display layer first. `null` releases it again.
|
|
1212
|
+
* ```ts
|
|
1213
|
+
* import assert from "node:assert/strict";
|
|
1214
|
+
* declare const host: TestHost;
|
|
1215
|
+
* // ---cut---
|
|
1216
|
+
* host.setVideoViewOwner("someone.zoom");
|
|
1217
|
+
* const view = plugin.video.view();
|
|
1218
|
+
* view.set({ zoom: 2 });
|
|
1219
|
+
* await host.settle();
|
|
1220
|
+
* assert.equal(view.status.state, "failed");
|
|
1221
|
+
* ```
|
|
1222
|
+
*
|
|
1223
|
+
* @since 1.0.0
|
|
1080
1224
|
*/
|
|
1081
1225
|
setVideoViewOwner(pluginId) {
|
|
1082
1226
|
this.videoView.owner = pluginId;
|
|
@@ -1093,6 +1237,24 @@ export class TestHost {
|
|
|
1093
1237
|
* Simulate another plugin owning the render options (the plugin's
|
|
1094
1238
|
* `video.render()` then fails with `conflict`), or, with `null`, releasing
|
|
1095
1239
|
* them (values reset to the defaults).
|
|
1240
|
+
*
|
|
1241
|
+
* @capability None.
|
|
1242
|
+
*
|
|
1243
|
+
* @example Let another plugin own the render options
|
|
1244
|
+
* The plugin's render options report `conflict` while another plugin owns
|
|
1245
|
+
* them; `null` releases them and resets the values to the defaults.
|
|
1246
|
+
* ```ts
|
|
1247
|
+
* import assert from "node:assert/strict";
|
|
1248
|
+
* declare const host: TestHost;
|
|
1249
|
+
* // ---cut---
|
|
1250
|
+
* host.setVideoRenderOwner("someone.hdr");
|
|
1251
|
+
* const render = plugin.video.render();
|
|
1252
|
+
* render.set({ toneMapping: "bt.2390" });
|
|
1253
|
+
* await host.settle();
|
|
1254
|
+
* assert.equal(render.status.state, "failed");
|
|
1255
|
+
* ```
|
|
1256
|
+
*
|
|
1257
|
+
* @since 1.0.0
|
|
1096
1258
|
*/
|
|
1097
1259
|
setVideoRenderOwner(pluginId) {
|
|
1098
1260
|
this.videoRender.owner = pluginId;
|
|
@@ -1120,6 +1282,9 @@ export class TestHost {
|
|
|
1120
1282
|
* await host.settle();
|
|
1121
1283
|
* assert.equal(render.status.state, "failed");
|
|
1122
1284
|
* ```
|
|
1285
|
+
*
|
|
1286
|
+
* @capability None.
|
|
1287
|
+
* @since 1.0.0
|
|
1123
1288
|
*/
|
|
1124
1289
|
failVideoRender(code, message) {
|
|
1125
1290
|
if (code === "unsupported")
|
|
@@ -1213,6 +1378,21 @@ export class TestHost {
|
|
|
1213
1378
|
* What `plugin.resolvers.fileTypes` reports: the extensions of this
|
|
1214
1379
|
* plugin's file resolvers and of {@link TestHost.otherResolvers}, sorted,
|
|
1215
1380
|
* each once.
|
|
1381
|
+
*
|
|
1382
|
+
* @capability None.
|
|
1383
|
+
*
|
|
1384
|
+
* @example Check the file types a resolver adds
|
|
1385
|
+
* The test checks the list the plugin will read as
|
|
1386
|
+
* `resolvers.fileTypes`: the extensions of its own file resolvers and of
|
|
1387
|
+
* `otherResolvers`, sorted, each once.
|
|
1388
|
+
* ```ts
|
|
1389
|
+
* import assert from "node:assert/strict";
|
|
1390
|
+
* declare const host: TestHost;
|
|
1391
|
+
* // ---cut---
|
|
1392
|
+
* assert.deepEqual(host.fileTypes(), ["ch8"]);
|
|
1393
|
+
* ```
|
|
1394
|
+
*
|
|
1395
|
+
* @since 1.5.8
|
|
1216
1396
|
*/
|
|
1217
1397
|
fileTypes() {
|
|
1218
1398
|
const types = new Set();
|
|
@@ -1245,6 +1425,9 @@ export class TestHost {
|
|
|
1245
1425
|
* await host.settle();
|
|
1246
1426
|
* assert.deepEqual(plugin.resolvers.fileTypes, ["ch8"]);
|
|
1247
1427
|
* ```
|
|
1428
|
+
*
|
|
1429
|
+
* @capability None.
|
|
1430
|
+
* @since 1.5.8
|
|
1248
1431
|
*/
|
|
1249
1432
|
updateFileTypes() {
|
|
1250
1433
|
const types = this.fileTypes();
|
|
@@ -1393,6 +1576,11 @@ export class TestHost {
|
|
|
1393
1576
|
* `"test.plugin"` by default).
|
|
1394
1577
|
*/
|
|
1395
1578
|
manifestId = "test.plugin";
|
|
1579
|
+
/**
|
|
1580
|
+
* A fresh in-memory player: nothing open, no plugin, the player's core
|
|
1581
|
+
* actions registered. Preset what the plugin reads during activation, then
|
|
1582
|
+
* pass it to {@link startPlugin} as `host`.
|
|
1583
|
+
*/
|
|
1396
1584
|
constructor() {
|
|
1397
1585
|
this.subtitles = {
|
|
1398
1586
|
presentation: {
|
|
@@ -1481,8 +1669,10 @@ export class TestHost {
|
|
|
1481
1669
|
/**
|
|
1482
1670
|
* One of the 14 core actions (`play-pause`, `stop`, `seek-forward`, ...)
|
|
1483
1671
|
* that the test host registers like the player, as `host/<id>`. Throws for
|
|
1484
|
-
* an unknown id. `run()` executes it
|
|
1485
|
-
*
|
|
1672
|
+
* an unknown id. `run()` executes it like its menu item: a disabled
|
|
1673
|
+
* action rejects with `command`, as in the player (`plugin.host.run`
|
|
1674
|
+
* rejects with `unavailable` instead); otherwise it runs with the
|
|
1675
|
+
* player's checks and writes its on-screen text to {@link TestHost.osd}.
|
|
1486
1676
|
*
|
|
1487
1677
|
* @example Pause through the player's own action
|
|
1488
1678
|
* The test pauses playback the way the Space key does and checks that the
|
|
@@ -1498,6 +1688,9 @@ export class TestHost {
|
|
|
1498
1688
|
* assert.equal(host.source.paused, true);
|
|
1499
1689
|
* assert.deepEqual(host.coreRuns, ["play-pause"]);
|
|
1500
1690
|
* ```
|
|
1691
|
+
*
|
|
1692
|
+
* @capability None.
|
|
1693
|
+
* @since 1.0.0
|
|
1501
1694
|
*/
|
|
1502
1695
|
coreAction(id) {
|
|
1503
1696
|
const command = this.coreCommands.get(`host/${id}`);
|
|
@@ -1703,6 +1896,10 @@ export class TestHost {
|
|
|
1703
1896
|
set state(_value) { },
|
|
1704
1897
|
revision: 1,
|
|
1705
1898
|
async run() {
|
|
1899
|
+
// Like the player's menu: a disabled or busy command does not run.
|
|
1900
|
+
const state = host.#coreState(id);
|
|
1901
|
+
if (state.busy || !state.enabled)
|
|
1902
|
+
throw new HostError("command", `host/${id} is ${state.busy ? "busy" : "disabled"}`);
|
|
1706
1903
|
await host.request("host.run", { action: id });
|
|
1707
1904
|
await host.settle();
|
|
1708
1905
|
},
|
|
@@ -1879,11 +2076,36 @@ export class TestHost {
|
|
|
1879
2076
|
* assert.deepEqual(host.osd, ["Folder not found"]);
|
|
1880
2077
|
* host.reset("system.openPath");
|
|
1881
2078
|
* ```
|
|
2079
|
+
*
|
|
2080
|
+
* @capability None.
|
|
2081
|
+
* @since 1.0.0
|
|
1882
2082
|
*/
|
|
1883
2083
|
handle(method, handler) {
|
|
1884
2084
|
this.#overrides.set(method, handler);
|
|
1885
2085
|
}
|
|
1886
|
-
/**
|
|
2086
|
+
/**
|
|
2087
|
+
* Restore the default behaviour of a method replaced with {@link TestHost.handle}.
|
|
2088
|
+
*
|
|
2089
|
+
* @capability None.
|
|
2090
|
+
*
|
|
2091
|
+
* @example Restore the default answer after a simulated failure
|
|
2092
|
+
* The first write fails, the second succeeds. `reset` removes the handler
|
|
2093
|
+
* that `handle` installed, so the method is answered by the test host
|
|
2094
|
+
* again.
|
|
2095
|
+
* ```ts
|
|
2096
|
+
* import assert from "node:assert/strict";
|
|
2097
|
+
* declare const host: TestHost;
|
|
2098
|
+
* // ---cut---
|
|
2099
|
+
* host.handle("clipboard.writeText", () => {
|
|
2100
|
+
* throw new HostError("failed", "denied");
|
|
2101
|
+
* });
|
|
2102
|
+
* await assert.rejects(plugin.clipboard.writeText("x"));
|
|
2103
|
+
* host.reset("clipboard.writeText");
|
|
2104
|
+
* await plugin.clipboard.writeText("x"); // the default handler again
|
|
2105
|
+
* ```
|
|
2106
|
+
*
|
|
2107
|
+
* @since 1.0.0
|
|
2108
|
+
*/
|
|
1887
2109
|
reset(method) {
|
|
1888
2110
|
this.#overrides.delete(method);
|
|
1889
2111
|
}
|
|
@@ -1896,6 +2118,22 @@ export class TestHost {
|
|
|
1896
2118
|
*
|
|
1897
2119
|
* @param event - The wire event name.
|
|
1898
2120
|
* @param data - The event payload (wire-level shape).
|
|
2121
|
+
*
|
|
2122
|
+
* @capability None.
|
|
2123
|
+
*
|
|
2124
|
+
* @example Deliver a raw event the helpers do not cover
|
|
2125
|
+
* The test pushes a `window.state` event after changing `layout` by hand.
|
|
2126
|
+
* Like the player, the host delivers it only when the plugin subscribed;
|
|
2127
|
+
* most tests use `setSource`, `setMainWindow` and the other helpers
|
|
2128
|
+
* instead.
|
|
2129
|
+
* ```ts
|
|
2130
|
+
* declare const host: TestHost;
|
|
2131
|
+
* // ---cut---
|
|
2132
|
+
* host.emit("window.state", {}); // no facts: the SDK reads window.state again
|
|
2133
|
+
* await host.settle();
|
|
2134
|
+
* ```
|
|
2135
|
+
*
|
|
2136
|
+
* @since 1.0.0
|
|
1899
2137
|
*/
|
|
1900
2138
|
emit(event, data) {
|
|
1901
2139
|
if ([
|
|
@@ -1935,6 +2173,9 @@ export class TestHost {
|
|
|
1935
2173
|
* host.setSource({ phase: "ended", terminalReason: "natural" });
|
|
1936
2174
|
* await host.settle();
|
|
1937
2175
|
* ```
|
|
2176
|
+
*
|
|
2177
|
+
* @capability None.
|
|
2178
|
+
* @since 1.0.0
|
|
1938
2179
|
*/
|
|
1939
2180
|
setSource(patch) {
|
|
1940
2181
|
this.source = {
|
|
@@ -1975,6 +2216,9 @@ export class TestHost {
|
|
|
1975
2216
|
* assert.equal(plugin.window.state.fullscreen, true);
|
|
1976
2217
|
* host.setMainWindow({ pictureInPicture: { supported: true, active: false } });
|
|
1977
2218
|
* ```
|
|
2219
|
+
*
|
|
2220
|
+
* @capability None.
|
|
2221
|
+
* @since 1.0.0
|
|
1978
2222
|
*/
|
|
1979
2223
|
setMainWindow(patch) {
|
|
1980
2224
|
const entering = patch.fullscreen !== undefined &&
|
|
@@ -1993,6 +2237,22 @@ export class TestHost {
|
|
|
1993
2237
|
* The pointer reached the right edge in fullscreen (`true`) or left the
|
|
1994
2238
|
* popped-out sidebar (`false`): the attached sidebar pops out or collapses
|
|
1995
2239
|
* (reference §6.3). No effect outside fullscreen, when hidden or floating.
|
|
2240
|
+
*
|
|
2241
|
+
* @capability None.
|
|
2242
|
+
*
|
|
2243
|
+
* @example Pop the sidebar out in fullscreen
|
|
2244
|
+
* The pointer reaches the right edge in fullscreen and the attached
|
|
2245
|
+
* sidebar pops out over the video. Outside fullscreen, or with the
|
|
2246
|
+
* sidebar hidden or floating, nothing happens.
|
|
2247
|
+
* ```ts
|
|
2248
|
+
* declare const host: TestHost;
|
|
2249
|
+
* // ---cut---
|
|
2250
|
+
* host.setMainWindow({ fullscreen: true });
|
|
2251
|
+
* host.popSidebar(true);
|
|
2252
|
+
* await host.settle();
|
|
2253
|
+
* ```
|
|
2254
|
+
*
|
|
2255
|
+
* @since 1.0.0
|
|
1996
2256
|
*/
|
|
1997
2257
|
popSidebar(popped) {
|
|
1998
2258
|
const sidebar = this.layout.sidebar;
|
|
@@ -2036,6 +2296,9 @@ export class TestHost {
|
|
|
2036
2296
|
* await host.settle();
|
|
2037
2297
|
* assert.equal(plugin.player.isCurrent(before), false);
|
|
2038
2298
|
* ```
|
|
2299
|
+
*
|
|
2300
|
+
* @capability None.
|
|
2301
|
+
* @since 1.0.0
|
|
2039
2302
|
*/
|
|
2040
2303
|
openElsewhere(source, phase = "ready") {
|
|
2041
2304
|
this.#intent({ kind: "open", source, startPaused: false });
|
|
@@ -2063,6 +2326,9 @@ export class TestHost {
|
|
|
2063
2326
|
* const [producer] = host.producers.values();
|
|
2064
2327
|
* host.failStream(producer!.id, "producer transport closed");
|
|
2065
2328
|
* ```
|
|
2329
|
+
*
|
|
2330
|
+
* @capability None.
|
|
2331
|
+
* @since 1.5.7
|
|
2066
2332
|
*/
|
|
2067
2333
|
failStream(producerId, message) {
|
|
2068
2334
|
const producer = this.producers.get(producerId);
|
|
@@ -2081,11 +2347,12 @@ export class TestHost {
|
|
|
2081
2347
|
* the conflict rule orders them. A disabled command lets the key pass to
|
|
2082
2348
|
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
2083
2349
|
* shadowed, then the global scope. The first enabled one runs; `press`
|
|
2084
|
-
* resolves after it ran (`"ran"`)
|
|
2085
|
-
*
|
|
2086
|
-
* reason on the OSD and eats the key (`"reason"`, see
|
|
2087
|
-
* without
|
|
2088
|
-
* nothing runs).
|
|
2350
|
+
* resolves after it ran (`"ran"`). When only disabled commands hold the
|
|
2351
|
+
* key, the first of them decides, as in the player: with a `reason` it
|
|
2352
|
+
* shows the reason on the OSD and eats the key (`"reason"`, see
|
|
2353
|
+
* `host.osd`); without one the key goes on to the focused control
|
|
2354
|
+
* (`"passed"`, nothing runs). Unlike the player, which only logs a key
|
|
2355
|
+
* that reaches a busy command, `press` then rejects, so a test notices. Rejects when nothing is bound. Bindings use the
|
|
2089
2356
|
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
2090
2357
|
* overrides in `bindings.overlay` apply.
|
|
2091
2358
|
*
|
|
@@ -2102,6 +2369,12 @@ export class TestHost {
|
|
|
2102
2369
|
* await host.press("Ctrl+B"); // a global binding
|
|
2103
2370
|
* await host.press("Delete", { panel: true }); // while the panel has focus
|
|
2104
2371
|
* ```
|
|
2372
|
+
*
|
|
2373
|
+
* @capability None.
|
|
2374
|
+
* @throws `command` (rejects) A `HostError`: the command that would run is
|
|
2375
|
+
* busy (the player only logs this).
|
|
2376
|
+
* @throws `Error` (rejects) Nothing is bound to the key.
|
|
2377
|
+
* @since 1.0.0
|
|
2105
2378
|
*/
|
|
2106
2379
|
async press(binding, options = {}) {
|
|
2107
2380
|
const claims = this.#claims();
|
|
@@ -2117,14 +2390,15 @@ export class TestHost {
|
|
|
2117
2390
|
await command(enabled).run();
|
|
2118
2391
|
return "ran";
|
|
2119
2392
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2393
|
+
// Like the player: only the first disabled command counts; without a
|
|
2394
|
+
// reason the key goes on to the focused control.
|
|
2395
|
+
const first = ids[0];
|
|
2396
|
+
const reason = this.commands.has(first)
|
|
2397
|
+
? command(first).display.reason
|
|
2398
|
+
: command(first).state.reason;
|
|
2399
|
+
if (typeof reason === "string" && reason !== "") {
|
|
2400
|
+
this.osd.push(reason);
|
|
2401
|
+
return "reason";
|
|
2128
2402
|
}
|
|
2129
2403
|
return "passed";
|
|
2130
2404
|
}
|
|
@@ -2232,6 +2506,9 @@ export class TestHost {
|
|
|
2232
2506
|
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
2233
2507
|
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
2234
2508
|
* ```
|
|
2509
|
+
*
|
|
2510
|
+
* @capability None.
|
|
2511
|
+
* @since 1.1.0
|
|
2235
2512
|
*/
|
|
2236
2513
|
markdown(value) {
|
|
2237
2514
|
const doc = value
|
|
@@ -2248,6 +2525,21 @@ export class TestHost {
|
|
|
2248
2525
|
/**
|
|
2249
2526
|
* Replace the plugin list and push `packages.changed` (another plugin
|
|
2250
2527
|
* started, stopped, was installed or removed).
|
|
2528
|
+
*
|
|
2529
|
+
* @capability None.
|
|
2530
|
+
*
|
|
2531
|
+
* @example Simulate another plugin starting
|
|
2532
|
+
* The plugin's store window hears that a plugin is running now. The list
|
|
2533
|
+
* replaces `packages` and is pushed as `packages.changed`, which the
|
|
2534
|
+
* plugin's `packages.onChange` listeners receive.
|
|
2535
|
+
* ```ts
|
|
2536
|
+
* declare const host: TestHost;
|
|
2537
|
+
* // ---cut---
|
|
2538
|
+
* host.setPackages([...host.packages]); // edit a copy, then push it
|
|
2539
|
+
* await host.settle();
|
|
2540
|
+
* ```
|
|
2541
|
+
*
|
|
2542
|
+
* @since 1.0.0
|
|
2251
2543
|
*/
|
|
2252
2544
|
setPackages(list) {
|
|
2253
2545
|
this.packages = list;
|
|
@@ -2297,6 +2589,25 @@ export class TestHost {
|
|
|
2297
2589
|
* Simulate another plugin calling `player.next()` / `previous()`: the host
|
|
2298
2590
|
* asks this plugin's sequence provider. Rejects with `unavailable` when
|
|
2299
2591
|
* there is no provider or its flag is false.
|
|
2592
|
+
*
|
|
2593
|
+
* @capability None.
|
|
2594
|
+
* @throws `unavailable` (rejects) A `HostError`: there is no sequence
|
|
2595
|
+
* provider, or its flag is false.
|
|
2596
|
+
*
|
|
2597
|
+
* @example Ask the plugin's provider for the next item
|
|
2598
|
+
* Another plugin calls `player.next()`; the host asks the plugin under
|
|
2599
|
+
* test, which is the sequence provider, and the test checks what it
|
|
2600
|
+
* opened.
|
|
2601
|
+
* ```ts
|
|
2602
|
+
* import assert from "node:assert/strict";
|
|
2603
|
+
* declare const host: TestHost;
|
|
2604
|
+
* // ---cut---
|
|
2605
|
+
* await host.step("next");
|
|
2606
|
+
* await host.settle();
|
|
2607
|
+
* assert.equal(host.source.source, "/media/b.mp4");
|
|
2608
|
+
* ```
|
|
2609
|
+
*
|
|
2610
|
+
* @since 1.0.0
|
|
2300
2611
|
*/
|
|
2301
2612
|
step(direction) {
|
|
2302
2613
|
return this.request("sequence.step", { direction });
|
|
@@ -2304,6 +2615,23 @@ export class TestHost {
|
|
|
2304
2615
|
/**
|
|
2305
2616
|
* The plugin's command with this local id. Throws when it is not
|
|
2306
2617
|
* registered (never added, or disposed).
|
|
2618
|
+
*
|
|
2619
|
+
* @capability None.
|
|
2620
|
+
*
|
|
2621
|
+
* @example Read a command's state after a change
|
|
2622
|
+
* The test checks that Export becomes enabled once a file is ready. The
|
|
2623
|
+
* record is updated as the plugin sends new state; read it after
|
|
2624
|
+
* `settle()`.
|
|
2625
|
+
* ```ts
|
|
2626
|
+
* import assert from "node:assert/strict";
|
|
2627
|
+
* declare const host: TestHost;
|
|
2628
|
+
* // ---cut---
|
|
2629
|
+
* host.setSource({ phase: "ready", duration: 60 });
|
|
2630
|
+
* await host.settle();
|
|
2631
|
+
* assert.equal(host.command("export").state.enabled, true); // throws if not registered
|
|
2632
|
+
* ```
|
|
2633
|
+
*
|
|
2634
|
+
* @since 1.0.0
|
|
2307
2635
|
*/
|
|
2308
2636
|
command(id) {
|
|
2309
2637
|
const command = this.commands.get(id);
|
|
@@ -2384,6 +2712,9 @@ export class TestHost {
|
|
|
2384
2712
|
* await host.settle();
|
|
2385
2713
|
* assert.equal(host.window("picture").props["brightness"], 0);
|
|
2386
2714
|
* ```
|
|
2715
|
+
*
|
|
2716
|
+
* @capability None.
|
|
2717
|
+
* @since 1.0.0
|
|
2387
2718
|
*/
|
|
2388
2719
|
window(id) {
|
|
2389
2720
|
const window = this.windows.get(id);
|
|
@@ -2409,6 +2740,9 @@ export class TestHost {
|
|
|
2409
2740
|
* panel.emit("select", 2);
|
|
2410
2741
|
* await host.settle();
|
|
2411
2742
|
* ```
|
|
2743
|
+
*
|
|
2744
|
+
* @capability None.
|
|
2745
|
+
* @since 1.0.0
|
|
2412
2746
|
*/
|
|
2413
2747
|
mounted() {
|
|
2414
2748
|
if (!this.panel)
|
|
@@ -2428,6 +2762,9 @@ export class TestHost {
|
|
|
2428
2762
|
* // ---cut---
|
|
2429
2763
|
* assert.deepEqual(host.sent("osd.show").at(-1), { text: "Saved" });
|
|
2430
2764
|
* ```
|
|
2765
|
+
*
|
|
2766
|
+
* @capability None.
|
|
2767
|
+
* @since 1.0.0
|
|
2431
2768
|
*/
|
|
2432
2769
|
sent(method) {
|
|
2433
2770
|
return this.calls
|
|
@@ -2477,6 +2814,10 @@ export class TestHost {
|
|
|
2477
2814
|
* await host.settle(20);
|
|
2478
2815
|
* assert.equal(window.props["status-text"], expected);
|
|
2479
2816
|
* ```
|
|
2817
|
+
*
|
|
2818
|
+
* @capability None.
|
|
2819
|
+
* @throws Never.
|
|
2820
|
+
* @since 1.0.0
|
|
2480
2821
|
*/
|
|
2481
2822
|
async settle(ms = 0) {
|
|
2482
2823
|
const deadline = Date.now() + ms;
|
|
@@ -2489,6 +2830,23 @@ export class TestHost {
|
|
|
2489
2830
|
/**
|
|
2490
2831
|
* Close the fake stream servers and delete the host's temporary
|
|
2491
2832
|
* directories. `stop()` from {@link startPlugin} calls it.
|
|
2833
|
+
*
|
|
2834
|
+
* @capability None.
|
|
2835
|
+
*
|
|
2836
|
+
* @example Clean up a host that was never started
|
|
2837
|
+
* A test that made a host but failed before `startPlugin` removes the
|
|
2838
|
+
* host's temporary directories itself. `stop()` from `startPlugin` calls
|
|
2839
|
+
* `close` for you otherwise.
|
|
2840
|
+
* ```ts
|
|
2841
|
+
* const host = new TestHost();
|
|
2842
|
+
* try {
|
|
2843
|
+
* // … preset the host; the test fails here …
|
|
2844
|
+
* } finally {
|
|
2845
|
+
* host.close(); // deletes the host's temporary data
|
|
2846
|
+
* }
|
|
2847
|
+
* ```
|
|
2848
|
+
*
|
|
2849
|
+
* @since 1.0.0
|
|
2492
2850
|
*/
|
|
2493
2851
|
close() {
|
|
2494
2852
|
for (const producer of this.producers.values())
|
|
@@ -3080,7 +3438,7 @@ export class TestHost {
|
|
|
3080
3438
|
this.secrets.set(name, String(p.value));
|
|
3081
3439
|
},
|
|
3082
3440
|
"secrets.delete": (p) => void this.secrets.delete(String(p.name)),
|
|
3083
|
-
// As the player (host/src/session_storage.rs).
|
|
3441
|
+
// As the player (src/host/src/session_storage.rs).
|
|
3084
3442
|
"storage.session.get": (p) => {
|
|
3085
3443
|
const key = sessionKey(p.key);
|
|
3086
3444
|
return this.sessionStorage.has(key)
|
|
@@ -4138,6 +4496,16 @@ export class TestHost {
|
|
|
4138
4496
|
* }
|
|
4139
4497
|
* });
|
|
4140
4498
|
* ```
|
|
4499
|
+
*
|
|
4500
|
+
* @remarks
|
|
4501
|
+
* The way into a plugin's tests: one call starts the plugin the way the
|
|
4502
|
+
* player's runner does and returns the host to drive, the `plugin` object
|
|
4503
|
+
* to call directly, and `stop` to end it like the player.
|
|
4504
|
+
*
|
|
4505
|
+
* @capability None.
|
|
4506
|
+
* @see [Test a plugin](../../docs/how-to/testing.md)
|
|
4507
|
+
* @since 1.0.0
|
|
4508
|
+
* @category Testing
|
|
4141
4509
|
*/
|
|
4142
4510
|
export async function startPlugin(activate, options = {}) {
|
|
4143
4511
|
const host = options.host ?? new TestHost();
|