@vestra-player/sdk 1.5.31 → 1.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/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/palettes.d.ts +2 -0
- package/dist/palettes.js +2 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -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 +4 -0
- package/dist/plugin/services.js +4 -0
- package/dist/plugin/subtitles.d.ts +59 -0
- package/dist/plugin/subtitles.js +70 -4
- package/dist/plugin/testing.d.ts +538 -12
- package/dist/plugin/testing.js +458 -23
- package/dist/plugin/types.d.ts +1524 -64
- package/dist/plugin/values.d.ts +5 -0
- package/dist/plugin/values.js +5 -0
- package/dist/processing.d.ts +6 -0
- package/dist/source-resolvers.d.ts +3 -0
- package/dist/subtitles.d.ts +3 -0
- package/dist/wire.d.ts +16 -1
- package/dist/wire.js +10 -0
- package/llms-full.txt +1489 -55
- package/llms.txt +22 -22
- package/package.json +1 -1
package/dist/plugin/testing.js
CHANGED
|
@@ -77,6 +77,7 @@ import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS
|
|
|
77
77
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
|
|
78
78
|
import { OSD_LIMITS } from "./osd.js";
|
|
79
79
|
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
|
|
80
|
+
import { SUBTITLE_LIMITS } from "./subtitles.js";
|
|
80
81
|
import { BINDING_INPUTS } from "./bindings.js";
|
|
81
82
|
import { THEME_PALETTES } from "../palettes.js";
|
|
82
83
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
@@ -104,6 +105,8 @@ export { OSD_LIMITS };
|
|
|
104
105
|
export { SECRET_LIMITS };
|
|
105
106
|
/** The player's limits on `plugin.storage.session`. */
|
|
106
107
|
export { SESSION_STORAGE_LIMITS };
|
|
108
|
+
/** The player's limits on loaded subtitle files, which the test host enforces as well. */
|
|
109
|
+
export { SUBTITLE_LIMITS };
|
|
107
110
|
/** The names bindings are made of. */
|
|
108
111
|
export { BINDING_INPUTS };
|
|
109
112
|
/** The colours of the player's palettes, by `Theme` token. */
|
|
@@ -307,6 +310,19 @@ const MAX_SIDEBAR_TABS = 8;
|
|
|
307
310
|
* await stop();
|
|
308
311
|
* });
|
|
309
312
|
* ```
|
|
313
|
+
*
|
|
314
|
+
* @remarks
|
|
315
|
+
* An in-memory player that speaks the real wire protocol, so the SDK's own
|
|
316
|
+
* logic runs as it does in the player; only the player side is simulated
|
|
317
|
+
* (no decoding, no Slint, no windows). Make one per test, preset it if the
|
|
318
|
+
* plugin reads something during activation, start the plugin with
|
|
319
|
+
* {@link startPlugin}, drive it, `settle()`, and assert on what it
|
|
320
|
+
* recorded.
|
|
321
|
+
*
|
|
322
|
+
* @capability None.
|
|
323
|
+
* @see [Test a plugin](../../docs/how-to/testing.md)
|
|
324
|
+
* @since 1.0.0
|
|
325
|
+
* @category Testing
|
|
310
326
|
*/
|
|
311
327
|
export class TestHost {
|
|
312
328
|
/**
|
|
@@ -416,6 +432,20 @@ export class TestHost {
|
|
|
416
432
|
*
|
|
417
433
|
* @param directory - The package directory (holding `locales/`).
|
|
418
434
|
* @since 1.5.9
|
|
435
|
+
*
|
|
436
|
+
* @capability None.
|
|
437
|
+
*
|
|
438
|
+
* @example Load the translations after changing the language
|
|
439
|
+
* The test switches the host to German before starting the plugin and
|
|
440
|
+
* loads that catalog itself (`startPlugin` does the same). Texts are then
|
|
441
|
+
* shown in German.
|
|
442
|
+
* ```ts
|
|
443
|
+
* declare const host: TestHost;
|
|
444
|
+
* declare const directory: string;
|
|
445
|
+
* // ---cut---
|
|
446
|
+
* host.locale = "de";
|
|
447
|
+
* host.loadCatalog(directory); // locales/de.po (or de.json)
|
|
448
|
+
* ```
|
|
419
449
|
*/
|
|
420
450
|
loadCatalog(directory) {
|
|
421
451
|
this.#catalog = loadPackageCatalog(directory, this.locale);
|
|
@@ -441,6 +471,8 @@ export class TestHost {
|
|
|
441
471
|
* ```
|
|
442
472
|
*
|
|
443
473
|
* @since 1.5.9
|
|
474
|
+
*
|
|
475
|
+
* @capability None.
|
|
444
476
|
*/
|
|
445
477
|
translate(text, args) {
|
|
446
478
|
if (typeof text !== "string")
|
|
@@ -520,6 +552,24 @@ export class TestHost {
|
|
|
520
552
|
*
|
|
521
553
|
* @param language - `"system"` or a locale.
|
|
522
554
|
* @since 1.5.9
|
|
555
|
+
*
|
|
556
|
+
* @capability None.
|
|
557
|
+
*
|
|
558
|
+
* @example Simulate the user choosing French for the next start
|
|
559
|
+
* The plugin hears that the saved interface language changed. The host
|
|
560
|
+
* pushes `appearance.changed`, which a plugin with `events` sees in
|
|
561
|
+
* `appearance.language` once it has read it and subscribed; this run's
|
|
562
|
+
* language does not change.
|
|
563
|
+
* ```ts
|
|
564
|
+
* import assert from "node:assert/strict";
|
|
565
|
+
* declare const host: TestHost;
|
|
566
|
+
* // ---cut---
|
|
567
|
+
* void plugin.appearance.language; // read once: the SDK then follows it
|
|
568
|
+
* plugin.appearance.onChange(() => {}); // subscribes to appearance events
|
|
569
|
+
* host.setLanguage("fr");
|
|
570
|
+
* await host.settle();
|
|
571
|
+
* assert.equal(plugin.appearance.language, "fr");
|
|
572
|
+
* ```
|
|
523
573
|
*/
|
|
524
574
|
setLanguage(language) {
|
|
525
575
|
this.language = language;
|
|
@@ -713,6 +763,9 @@ export class TestHost {
|
|
|
713
763
|
* await host.settle();
|
|
714
764
|
* assert.equal(plugin.timeline.marks.length, 2);
|
|
715
765
|
* ```
|
|
766
|
+
*
|
|
767
|
+
* @capability None.
|
|
768
|
+
* @since 1.0.0
|
|
716
769
|
*/
|
|
717
770
|
setTimelineLayer(pluginId, marks) {
|
|
718
771
|
if (marks.length)
|
|
@@ -755,6 +808,9 @@ export class TestHost {
|
|
|
755
808
|
* join(plugin.dataDirectory, "cover.png"),
|
|
756
809
|
* );
|
|
757
810
|
* ```
|
|
811
|
+
*
|
|
812
|
+
* @capability None.
|
|
813
|
+
* @since 1.0.0
|
|
758
814
|
*/
|
|
759
815
|
imagePath(value) {
|
|
760
816
|
const image = value;
|
|
@@ -865,6 +921,10 @@ export class TestHost {
|
|
|
865
921
|
* modifiers: { alt: true },
|
|
866
922
|
* });
|
|
867
923
|
* ```
|
|
924
|
+
*
|
|
925
|
+
* @capability None.
|
|
926
|
+
* @throws Never: files nobody takes are ignored, as in the player.
|
|
927
|
+
* @since 1.0.0
|
|
868
928
|
*/
|
|
869
929
|
drop(paths, options = {}) {
|
|
870
930
|
const modifiers = {
|
|
@@ -899,6 +959,24 @@ export class TestHost {
|
|
|
899
959
|
/**
|
|
900
960
|
* Simulate another plugin switching the palette: updates
|
|
901
961
|
* {@link TestHost.appearance} and pushes `appearance.changed`.
|
|
962
|
+
*
|
|
963
|
+
* @capability None.
|
|
964
|
+
*
|
|
965
|
+
* @example Simulate another plugin switching to the light palette
|
|
966
|
+
* The plugin hears the palette change. The host updates `appearance` and
|
|
967
|
+
* pushes `appearance.changed` to a plugin that listens with `events`.
|
|
968
|
+
* ```ts
|
|
969
|
+
* import assert from "node:assert/strict";
|
|
970
|
+
* declare const host: TestHost;
|
|
971
|
+
* // ---cut---
|
|
972
|
+
* const themes: ThemeId[] = [];
|
|
973
|
+
* plugin.appearance.onChange((theme) => themes.push(theme)); // subscribes (`events`)
|
|
974
|
+
* host.setAppearance("vestra-light");
|
|
975
|
+
* await host.settle();
|
|
976
|
+
* assert.deepEqual(themes, ["vestra-light"]);
|
|
977
|
+
* ```
|
|
978
|
+
*
|
|
979
|
+
* @since 1.0.0
|
|
902
980
|
*/
|
|
903
981
|
setAppearance(theme) {
|
|
904
982
|
this.appearance = theme;
|
|
@@ -935,6 +1013,9 @@ export class TestHost {
|
|
|
935
1013
|
* await host.settle();
|
|
936
1014
|
* assert.equal(page.props["step"], 10);
|
|
937
1015
|
* ```
|
|
1016
|
+
*
|
|
1017
|
+
* @capability None.
|
|
1018
|
+
* @since 1.0.0
|
|
938
1019
|
*/
|
|
939
1020
|
settingsPage(id) {
|
|
940
1021
|
const page = this.settings.pages.get(id);
|
|
@@ -947,6 +1028,23 @@ export class TestHost {
|
|
|
947
1028
|
/**
|
|
948
1029
|
* Simulate the backdrop's `effective` changing (`false`: a video frame
|
|
949
1030
|
* appeared; `true`: there is none again); pushed to the plugin.
|
|
1031
|
+
*
|
|
1032
|
+
* @capability None.
|
|
1033
|
+
*
|
|
1034
|
+
* @example Make a video picture replace the backdrop
|
|
1035
|
+
* The plugin's backdrop hears that a video frame now covers it. `false`
|
|
1036
|
+
* means a picture appeared; the change is pushed like the player's
|
|
1037
|
+
* `window.state`.
|
|
1038
|
+
* ```ts
|
|
1039
|
+
* import assert from "node:assert/strict";
|
|
1040
|
+
* declare const host: TestHost;
|
|
1041
|
+
* // ---cut---
|
|
1042
|
+
* host.setBackdropEffective(false);
|
|
1043
|
+
* await host.settle();
|
|
1044
|
+
* assert.equal(plugin.panels.backdrop.state?.effective, false);
|
|
1045
|
+
* ```
|
|
1046
|
+
*
|
|
1047
|
+
* @since 1.0.0
|
|
950
1048
|
*/
|
|
951
1049
|
setBackdropEffective(effective) {
|
|
952
1050
|
if (this.backdrop)
|
|
@@ -986,6 +1084,8 @@ export class TestHost {
|
|
|
986
1084
|
* ```
|
|
987
1085
|
*
|
|
988
1086
|
* @since 1.5.2
|
|
1087
|
+
*
|
|
1088
|
+
* @capability None.
|
|
989
1089
|
*/
|
|
990
1090
|
mountOther(slot, plugin, options = {}) {
|
|
991
1091
|
if (this.#others.has(plugin))
|
|
@@ -1009,6 +1109,21 @@ export class TestHost {
|
|
|
1009
1109
|
* when its tab was current, the next tab (else the previous one) is.
|
|
1010
1110
|
*
|
|
1011
1111
|
* @since 1.5.2
|
|
1112
|
+
*
|
|
1113
|
+
* @capability None.
|
|
1114
|
+
*
|
|
1115
|
+
* @example Let the playlist's tab go away
|
|
1116
|
+
* The other plugin's sidebar tab disappears and the plugin's own tab
|
|
1117
|
+
* becomes current. When the removed tab was current, the next tab (else
|
|
1118
|
+
* the previous one) becomes current, as in the player.
|
|
1119
|
+
* ```ts
|
|
1120
|
+
* import assert from "node:assert/strict";
|
|
1121
|
+
* declare const host: TestHost;
|
|
1122
|
+
* // ---cut---
|
|
1123
|
+
* host.unmountOther("vestra.playlist");
|
|
1124
|
+
* await host.settle();
|
|
1125
|
+
* assert.equal(host.layout.sidebar?.panels?.length, 1);
|
|
1126
|
+
* ```
|
|
1012
1127
|
*/
|
|
1013
1128
|
unmountOther(plugin) {
|
|
1014
1129
|
const other = this.#others.get(plugin);
|
|
@@ -1027,6 +1142,20 @@ export class TestHost {
|
|
|
1027
1142
|
* becomes the current tab. Pushed as a `window.state` change.
|
|
1028
1143
|
*
|
|
1029
1144
|
* @since 1.5.2
|
|
1145
|
+
*
|
|
1146
|
+
* @capability None.
|
|
1147
|
+
*
|
|
1148
|
+
* @example Click another plugin's sidebar tab
|
|
1149
|
+
* The user switches the sidebar to the playlist; the plugin's panel is in
|
|
1150
|
+
* a background tab now. The change is pushed as `window.state`.
|
|
1151
|
+
* ```ts
|
|
1152
|
+
* import assert from "node:assert/strict";
|
|
1153
|
+
* declare const host: TestHost;
|
|
1154
|
+
* // ---cut---
|
|
1155
|
+
* host.selectTab("vestra.playlist");
|
|
1156
|
+
* await host.settle();
|
|
1157
|
+
* assert.notEqual(host.layout.sidebar?.owner, 1); // the plugin under test is owner 1
|
|
1158
|
+
* ```
|
|
1030
1159
|
*/
|
|
1031
1160
|
selectTab(plugin) {
|
|
1032
1161
|
const slot = this.layout.sidebar;
|
|
@@ -1077,6 +1206,24 @@ export class TestHost {
|
|
|
1077
1206
|
* Simulate another plugin taking the display layer first (the plugin's
|
|
1078
1207
|
* `video.view()` then fails with `conflict`), or, with `null`, releasing
|
|
1079
1208
|
* it (values reset to the defaults).
|
|
1209
|
+
*
|
|
1210
|
+
* @capability None.
|
|
1211
|
+
*
|
|
1212
|
+
* @example Let another plugin own the display layer
|
|
1213
|
+
* The plugin's `video.view()` fails with `conflict` because another plugin
|
|
1214
|
+
* took the display layer first. `null` releases it again.
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* import assert from "node:assert/strict";
|
|
1217
|
+
* declare const host: TestHost;
|
|
1218
|
+
* // ---cut---
|
|
1219
|
+
* host.setVideoViewOwner("someone.zoom");
|
|
1220
|
+
* const view = plugin.video.view();
|
|
1221
|
+
* view.set({ zoom: 2 });
|
|
1222
|
+
* await host.settle();
|
|
1223
|
+
* assert.equal(view.status.state, "failed");
|
|
1224
|
+
* ```
|
|
1225
|
+
*
|
|
1226
|
+
* @since 1.0.0
|
|
1080
1227
|
*/
|
|
1081
1228
|
setVideoViewOwner(pluginId) {
|
|
1082
1229
|
this.videoView.owner = pluginId;
|
|
@@ -1093,6 +1240,24 @@ export class TestHost {
|
|
|
1093
1240
|
* Simulate another plugin owning the render options (the plugin's
|
|
1094
1241
|
* `video.render()` then fails with `conflict`), or, with `null`, releasing
|
|
1095
1242
|
* them (values reset to the defaults).
|
|
1243
|
+
*
|
|
1244
|
+
* @capability None.
|
|
1245
|
+
*
|
|
1246
|
+
* @example Let another plugin own the render options
|
|
1247
|
+
* The plugin's render options report `conflict` while another plugin owns
|
|
1248
|
+
* them; `null` releases them and resets the values to the defaults.
|
|
1249
|
+
* ```ts
|
|
1250
|
+
* import assert from "node:assert/strict";
|
|
1251
|
+
* declare const host: TestHost;
|
|
1252
|
+
* // ---cut---
|
|
1253
|
+
* host.setVideoRenderOwner("someone.hdr");
|
|
1254
|
+
* const render = plugin.video.render();
|
|
1255
|
+
* render.set({ toneMapping: "bt.2390" });
|
|
1256
|
+
* await host.settle();
|
|
1257
|
+
* assert.equal(render.status.state, "failed");
|
|
1258
|
+
* ```
|
|
1259
|
+
*
|
|
1260
|
+
* @since 1.0.0
|
|
1096
1261
|
*/
|
|
1097
1262
|
setVideoRenderOwner(pluginId) {
|
|
1098
1263
|
this.videoRender.owner = pluginId;
|
|
@@ -1120,6 +1285,9 @@ export class TestHost {
|
|
|
1120
1285
|
* await host.settle();
|
|
1121
1286
|
* assert.equal(render.status.state, "failed");
|
|
1122
1287
|
* ```
|
|
1288
|
+
*
|
|
1289
|
+
* @capability None.
|
|
1290
|
+
* @since 1.0.0
|
|
1123
1291
|
*/
|
|
1124
1292
|
failVideoRender(code, message) {
|
|
1125
1293
|
if (code === "unsupported")
|
|
@@ -1213,6 +1381,21 @@ export class TestHost {
|
|
|
1213
1381
|
* What `plugin.resolvers.fileTypes` reports: the extensions of this
|
|
1214
1382
|
* plugin's file resolvers and of {@link TestHost.otherResolvers}, sorted,
|
|
1215
1383
|
* each once.
|
|
1384
|
+
*
|
|
1385
|
+
* @capability None.
|
|
1386
|
+
*
|
|
1387
|
+
* @example Check the file types a resolver adds
|
|
1388
|
+
* The test checks the list the plugin will read as
|
|
1389
|
+
* `resolvers.fileTypes`: the extensions of its own file resolvers and of
|
|
1390
|
+
* `otherResolvers`, sorted, each once.
|
|
1391
|
+
* ```ts
|
|
1392
|
+
* import assert from "node:assert/strict";
|
|
1393
|
+
* declare const host: TestHost;
|
|
1394
|
+
* // ---cut---
|
|
1395
|
+
* assert.deepEqual(host.fileTypes(), ["ch8"]);
|
|
1396
|
+
* ```
|
|
1397
|
+
*
|
|
1398
|
+
* @since 1.5.8
|
|
1216
1399
|
*/
|
|
1217
1400
|
fileTypes() {
|
|
1218
1401
|
const types = new Set();
|
|
@@ -1245,6 +1428,9 @@ export class TestHost {
|
|
|
1245
1428
|
* await host.settle();
|
|
1246
1429
|
* assert.deepEqual(plugin.resolvers.fileTypes, ["ch8"]);
|
|
1247
1430
|
* ```
|
|
1431
|
+
*
|
|
1432
|
+
* @capability None.
|
|
1433
|
+
* @since 1.5.8
|
|
1248
1434
|
*/
|
|
1249
1435
|
updateFileTypes() {
|
|
1250
1436
|
const types = this.fileTypes();
|
|
@@ -1338,7 +1524,11 @@ export class TestHost {
|
|
|
1338
1524
|
* @since 1.5.25
|
|
1339
1525
|
*/
|
|
1340
1526
|
sessionStorage = new Map();
|
|
1341
|
-
/**
|
|
1527
|
+
/**
|
|
1528
|
+
* What `plugin.app.system()` reports. Its `rendering` follows
|
|
1529
|
+
* {@link TestHost.videoRender | videoRender.gpu} (`"gpu"` or
|
|
1530
|
+
* `"software"`), so one switch covers the render options and the fact.
|
|
1531
|
+
*/
|
|
1342
1532
|
systemInfo = {
|
|
1343
1533
|
os: "macos",
|
|
1344
1534
|
name: "macOS",
|
|
@@ -1393,6 +1583,11 @@ export class TestHost {
|
|
|
1393
1583
|
* `"test.plugin"` by default).
|
|
1394
1584
|
*/
|
|
1395
1585
|
manifestId = "test.plugin";
|
|
1586
|
+
/**
|
|
1587
|
+
* A fresh in-memory player: nothing open, no plugin, the player's core
|
|
1588
|
+
* actions registered. Preset what the plugin reads during activation, then
|
|
1589
|
+
* pass it to {@link startPlugin} as `host`.
|
|
1590
|
+
*/
|
|
1396
1591
|
constructor() {
|
|
1397
1592
|
this.subtitles = {
|
|
1398
1593
|
presentation: {
|
|
@@ -1481,8 +1676,10 @@ export class TestHost {
|
|
|
1481
1676
|
/**
|
|
1482
1677
|
* One of the 14 core actions (`play-pause`, `stop`, `seek-forward`, ...)
|
|
1483
1678
|
* that the test host registers like the player, as `host/<id>`. Throws for
|
|
1484
|
-
* an unknown id. `run()` executes it
|
|
1485
|
-
*
|
|
1679
|
+
* an unknown id. `run()` executes it like its menu item: a disabled
|
|
1680
|
+
* action rejects with `command`, as in the player (`plugin.host.run`
|
|
1681
|
+
* rejects with `unavailable` instead); otherwise it runs with the
|
|
1682
|
+
* player's checks and writes its on-screen text to {@link TestHost.osd}.
|
|
1486
1683
|
*
|
|
1487
1684
|
* @example Pause through the player's own action
|
|
1488
1685
|
* The test pauses playback the way the Space key does and checks that the
|
|
@@ -1498,6 +1695,9 @@ export class TestHost {
|
|
|
1498
1695
|
* assert.equal(host.source.paused, true);
|
|
1499
1696
|
* assert.deepEqual(host.coreRuns, ["play-pause"]);
|
|
1500
1697
|
* ```
|
|
1698
|
+
*
|
|
1699
|
+
* @capability None.
|
|
1700
|
+
* @since 1.0.0
|
|
1501
1701
|
*/
|
|
1502
1702
|
coreAction(id) {
|
|
1503
1703
|
const command = this.coreCommands.get(`host/${id}`);
|
|
@@ -1703,6 +1903,10 @@ export class TestHost {
|
|
|
1703
1903
|
set state(_value) { },
|
|
1704
1904
|
revision: 1,
|
|
1705
1905
|
async run() {
|
|
1906
|
+
// Like the player's menu: a disabled or busy command does not run.
|
|
1907
|
+
const state = host.#coreState(id);
|
|
1908
|
+
if (state.busy || !state.enabled)
|
|
1909
|
+
throw new HostError("command", `host/${id} is ${state.busy ? "busy" : "disabled"}`);
|
|
1706
1910
|
await host.request("host.run", { action: id });
|
|
1707
1911
|
await host.settle();
|
|
1708
1912
|
},
|
|
@@ -1879,11 +2083,36 @@ export class TestHost {
|
|
|
1879
2083
|
* assert.deepEqual(host.osd, ["Folder not found"]);
|
|
1880
2084
|
* host.reset("system.openPath");
|
|
1881
2085
|
* ```
|
|
2086
|
+
*
|
|
2087
|
+
* @capability None.
|
|
2088
|
+
* @since 1.0.0
|
|
1882
2089
|
*/
|
|
1883
2090
|
handle(method, handler) {
|
|
1884
2091
|
this.#overrides.set(method, handler);
|
|
1885
2092
|
}
|
|
1886
|
-
/**
|
|
2093
|
+
/**
|
|
2094
|
+
* Restore the default behaviour of a method replaced with {@link TestHost.handle}.
|
|
2095
|
+
*
|
|
2096
|
+
* @capability None.
|
|
2097
|
+
*
|
|
2098
|
+
* @example Restore the default answer after a simulated failure
|
|
2099
|
+
* The first write fails, the second succeeds. `reset` removes the handler
|
|
2100
|
+
* that `handle` installed, so the method is answered by the test host
|
|
2101
|
+
* again.
|
|
2102
|
+
* ```ts
|
|
2103
|
+
* import assert from "node:assert/strict";
|
|
2104
|
+
* declare const host: TestHost;
|
|
2105
|
+
* // ---cut---
|
|
2106
|
+
* host.handle("clipboard.writeText", () => {
|
|
2107
|
+
* throw new HostError("failed", "denied");
|
|
2108
|
+
* });
|
|
2109
|
+
* await assert.rejects(plugin.clipboard.writeText("x"));
|
|
2110
|
+
* host.reset("clipboard.writeText");
|
|
2111
|
+
* await plugin.clipboard.writeText("x"); // the default handler again
|
|
2112
|
+
* ```
|
|
2113
|
+
*
|
|
2114
|
+
* @since 1.0.0
|
|
2115
|
+
*/
|
|
1887
2116
|
reset(method) {
|
|
1888
2117
|
this.#overrides.delete(method);
|
|
1889
2118
|
}
|
|
@@ -1896,6 +2125,22 @@ export class TestHost {
|
|
|
1896
2125
|
*
|
|
1897
2126
|
* @param event - The wire event name.
|
|
1898
2127
|
* @param data - The event payload (wire-level shape).
|
|
2128
|
+
*
|
|
2129
|
+
* @capability None.
|
|
2130
|
+
*
|
|
2131
|
+
* @example Deliver a raw event the helpers do not cover
|
|
2132
|
+
* The test pushes a `window.state` event after changing `layout` by hand.
|
|
2133
|
+
* Like the player, the host delivers it only when the plugin subscribed;
|
|
2134
|
+
* most tests use `setSource`, `setMainWindow` and the other helpers
|
|
2135
|
+
* instead.
|
|
2136
|
+
* ```ts
|
|
2137
|
+
* declare const host: TestHost;
|
|
2138
|
+
* // ---cut---
|
|
2139
|
+
* host.emit("window.state", {}); // no facts: the SDK reads window.state again
|
|
2140
|
+
* await host.settle();
|
|
2141
|
+
* ```
|
|
2142
|
+
*
|
|
2143
|
+
* @since 1.0.0
|
|
1899
2144
|
*/
|
|
1900
2145
|
emit(event, data) {
|
|
1901
2146
|
if ([
|
|
@@ -1935,6 +2180,9 @@ export class TestHost {
|
|
|
1935
2180
|
* host.setSource({ phase: "ended", terminalReason: "natural" });
|
|
1936
2181
|
* await host.settle();
|
|
1937
2182
|
* ```
|
|
2183
|
+
*
|
|
2184
|
+
* @capability None.
|
|
2185
|
+
* @since 1.0.0
|
|
1938
2186
|
*/
|
|
1939
2187
|
setSource(patch) {
|
|
1940
2188
|
this.source = {
|
|
@@ -1975,6 +2223,9 @@ export class TestHost {
|
|
|
1975
2223
|
* assert.equal(plugin.window.state.fullscreen, true);
|
|
1976
2224
|
* host.setMainWindow({ pictureInPicture: { supported: true, active: false } });
|
|
1977
2225
|
* ```
|
|
2226
|
+
*
|
|
2227
|
+
* @capability None.
|
|
2228
|
+
* @since 1.0.0
|
|
1978
2229
|
*/
|
|
1979
2230
|
setMainWindow(patch) {
|
|
1980
2231
|
const entering = patch.fullscreen !== undefined &&
|
|
@@ -1993,6 +2244,22 @@ export class TestHost {
|
|
|
1993
2244
|
* The pointer reached the right edge in fullscreen (`true`) or left the
|
|
1994
2245
|
* popped-out sidebar (`false`): the attached sidebar pops out or collapses
|
|
1995
2246
|
* (reference §6.3). No effect outside fullscreen, when hidden or floating.
|
|
2247
|
+
*
|
|
2248
|
+
* @capability None.
|
|
2249
|
+
*
|
|
2250
|
+
* @example Pop the sidebar out in fullscreen
|
|
2251
|
+
* The pointer reaches the right edge in fullscreen and the attached
|
|
2252
|
+
* sidebar pops out over the video. Outside fullscreen, or with the
|
|
2253
|
+
* sidebar hidden or floating, nothing happens.
|
|
2254
|
+
* ```ts
|
|
2255
|
+
* declare const host: TestHost;
|
|
2256
|
+
* // ---cut---
|
|
2257
|
+
* host.setMainWindow({ fullscreen: true });
|
|
2258
|
+
* host.popSidebar(true);
|
|
2259
|
+
* await host.settle();
|
|
2260
|
+
* ```
|
|
2261
|
+
*
|
|
2262
|
+
* @since 1.0.0
|
|
1996
2263
|
*/
|
|
1997
2264
|
popSidebar(popped) {
|
|
1998
2265
|
const sidebar = this.layout.sidebar;
|
|
@@ -2036,6 +2303,9 @@ export class TestHost {
|
|
|
2036
2303
|
* await host.settle();
|
|
2037
2304
|
* assert.equal(plugin.player.isCurrent(before), false);
|
|
2038
2305
|
* ```
|
|
2306
|
+
*
|
|
2307
|
+
* @capability None.
|
|
2308
|
+
* @since 1.0.0
|
|
2039
2309
|
*/
|
|
2040
2310
|
openElsewhere(source, phase = "ready") {
|
|
2041
2311
|
this.#intent({ kind: "open", source, startPaused: false });
|
|
@@ -2063,6 +2333,9 @@ export class TestHost {
|
|
|
2063
2333
|
* const [producer] = host.producers.values();
|
|
2064
2334
|
* host.failStream(producer!.id, "producer transport closed");
|
|
2065
2335
|
* ```
|
|
2336
|
+
*
|
|
2337
|
+
* @capability None.
|
|
2338
|
+
* @since 1.5.7
|
|
2066
2339
|
*/
|
|
2067
2340
|
failStream(producerId, message) {
|
|
2068
2341
|
const producer = this.producers.get(producerId);
|
|
@@ -2081,11 +2354,12 @@ export class TestHost {
|
|
|
2081
2354
|
* the conflict rule orders them. A disabled command lets the key pass to
|
|
2082
2355
|
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
2083
2356
|
* 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).
|
|
2357
|
+
* resolves after it ran (`"ran"`). When only disabled commands hold the
|
|
2358
|
+
* key, the first of them decides, as in the player: with a `reason` it
|
|
2359
|
+
* shows the reason on the OSD and eats the key (`"reason"`, see
|
|
2360
|
+
* `host.osd`); without one the key goes on to the focused control
|
|
2361
|
+
* (`"passed"`, nothing runs). Unlike the player, which only logs a key
|
|
2362
|
+
* that reaches a busy command, `press` then rejects, so a test notices. Rejects when nothing is bound. Bindings use the
|
|
2089
2363
|
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
2090
2364
|
* overrides in `bindings.overlay` apply.
|
|
2091
2365
|
*
|
|
@@ -2102,6 +2376,12 @@ export class TestHost {
|
|
|
2102
2376
|
* await host.press("Ctrl+B"); // a global binding
|
|
2103
2377
|
* await host.press("Delete", { panel: true }); // while the panel has focus
|
|
2104
2378
|
* ```
|
|
2379
|
+
*
|
|
2380
|
+
* @capability None.
|
|
2381
|
+
* @throws `command` (rejects) A `HostError`: the command that would run is
|
|
2382
|
+
* busy (the player only logs this).
|
|
2383
|
+
* @throws `Error` (rejects) Nothing is bound to the key.
|
|
2384
|
+
* @since 1.0.0
|
|
2105
2385
|
*/
|
|
2106
2386
|
async press(binding, options = {}) {
|
|
2107
2387
|
const claims = this.#claims();
|
|
@@ -2117,14 +2397,15 @@ export class TestHost {
|
|
|
2117
2397
|
await command(enabled).run();
|
|
2118
2398
|
return "ran";
|
|
2119
2399
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2400
|
+
// Like the player: only the first disabled command counts; without a
|
|
2401
|
+
// reason the key goes on to the focused control.
|
|
2402
|
+
const first = ids[0];
|
|
2403
|
+
const reason = this.commands.has(first)
|
|
2404
|
+
? command(first).display.reason
|
|
2405
|
+
: command(first).state.reason;
|
|
2406
|
+
if (typeof reason === "string" && reason !== "") {
|
|
2407
|
+
this.osd.push(reason);
|
|
2408
|
+
return "reason";
|
|
2128
2409
|
}
|
|
2129
2410
|
return "passed";
|
|
2130
2411
|
}
|
|
@@ -2232,6 +2513,9 @@ export class TestHost {
|
|
|
2232
2513
|
* const rows = host.window("chat").props["messages"] as { body: Json }[];
|
|
2233
2514
|
* assert.equal(host.markdown(rows[0]!.body)?.text, "**Hello**");
|
|
2234
2515
|
* ```
|
|
2516
|
+
*
|
|
2517
|
+
* @capability None.
|
|
2518
|
+
* @since 1.1.0
|
|
2235
2519
|
*/
|
|
2236
2520
|
markdown(value) {
|
|
2237
2521
|
const doc = value
|
|
@@ -2248,6 +2532,21 @@ export class TestHost {
|
|
|
2248
2532
|
/**
|
|
2249
2533
|
* Replace the plugin list and push `packages.changed` (another plugin
|
|
2250
2534
|
* started, stopped, was installed or removed).
|
|
2535
|
+
*
|
|
2536
|
+
* @capability None.
|
|
2537
|
+
*
|
|
2538
|
+
* @example Simulate another plugin starting
|
|
2539
|
+
* The plugin's store window hears that a plugin is running now. The list
|
|
2540
|
+
* replaces `packages` and is pushed as `packages.changed`, which the
|
|
2541
|
+
* plugin's `packages.onChange` listeners receive.
|
|
2542
|
+
* ```ts
|
|
2543
|
+
* declare const host: TestHost;
|
|
2544
|
+
* // ---cut---
|
|
2545
|
+
* host.setPackages([...host.packages]); // edit a copy, then push it
|
|
2546
|
+
* await host.settle();
|
|
2547
|
+
* ```
|
|
2548
|
+
*
|
|
2549
|
+
* @since 1.0.0
|
|
2251
2550
|
*/
|
|
2252
2551
|
setPackages(list) {
|
|
2253
2552
|
this.packages = list;
|
|
@@ -2297,6 +2596,25 @@ export class TestHost {
|
|
|
2297
2596
|
* Simulate another plugin calling `player.next()` / `previous()`: the host
|
|
2298
2597
|
* asks this plugin's sequence provider. Rejects with `unavailable` when
|
|
2299
2598
|
* there is no provider or its flag is false.
|
|
2599
|
+
*
|
|
2600
|
+
* @capability None.
|
|
2601
|
+
* @throws `unavailable` (rejects) A `HostError`: there is no sequence
|
|
2602
|
+
* provider, or its flag is false.
|
|
2603
|
+
*
|
|
2604
|
+
* @example Ask the plugin's provider for the next item
|
|
2605
|
+
* Another plugin calls `player.next()`; the host asks the plugin under
|
|
2606
|
+
* test, which is the sequence provider, and the test checks what it
|
|
2607
|
+
* opened.
|
|
2608
|
+
* ```ts
|
|
2609
|
+
* import assert from "node:assert/strict";
|
|
2610
|
+
* declare const host: TestHost;
|
|
2611
|
+
* // ---cut---
|
|
2612
|
+
* await host.step("next");
|
|
2613
|
+
* await host.settle();
|
|
2614
|
+
* assert.equal(host.source.source, "/media/b.mp4");
|
|
2615
|
+
* ```
|
|
2616
|
+
*
|
|
2617
|
+
* @since 1.0.0
|
|
2300
2618
|
*/
|
|
2301
2619
|
step(direction) {
|
|
2302
2620
|
return this.request("sequence.step", { direction });
|
|
@@ -2304,6 +2622,23 @@ export class TestHost {
|
|
|
2304
2622
|
/**
|
|
2305
2623
|
* The plugin's command with this local id. Throws when it is not
|
|
2306
2624
|
* registered (never added, or disposed).
|
|
2625
|
+
*
|
|
2626
|
+
* @capability None.
|
|
2627
|
+
*
|
|
2628
|
+
* @example Read a command's state after a change
|
|
2629
|
+
* The test checks that Export becomes enabled once a file is ready. The
|
|
2630
|
+
* record is updated as the plugin sends new state; read it after
|
|
2631
|
+
* `settle()`.
|
|
2632
|
+
* ```ts
|
|
2633
|
+
* import assert from "node:assert/strict";
|
|
2634
|
+
* declare const host: TestHost;
|
|
2635
|
+
* // ---cut---
|
|
2636
|
+
* host.setSource({ phase: "ready", duration: 60 });
|
|
2637
|
+
* await host.settle();
|
|
2638
|
+
* assert.equal(host.command("export").state.enabled, true); // throws if not registered
|
|
2639
|
+
* ```
|
|
2640
|
+
*
|
|
2641
|
+
* @since 1.0.0
|
|
2307
2642
|
*/
|
|
2308
2643
|
command(id) {
|
|
2309
2644
|
const command = this.commands.get(id);
|
|
@@ -2384,6 +2719,9 @@ export class TestHost {
|
|
|
2384
2719
|
* await host.settle();
|
|
2385
2720
|
* assert.equal(host.window("picture").props["brightness"], 0);
|
|
2386
2721
|
* ```
|
|
2722
|
+
*
|
|
2723
|
+
* @capability None.
|
|
2724
|
+
* @since 1.0.0
|
|
2387
2725
|
*/
|
|
2388
2726
|
window(id) {
|
|
2389
2727
|
const window = this.windows.get(id);
|
|
@@ -2409,6 +2747,9 @@ export class TestHost {
|
|
|
2409
2747
|
* panel.emit("select", 2);
|
|
2410
2748
|
* await host.settle();
|
|
2411
2749
|
* ```
|
|
2750
|
+
*
|
|
2751
|
+
* @capability None.
|
|
2752
|
+
* @since 1.0.0
|
|
2412
2753
|
*/
|
|
2413
2754
|
mounted() {
|
|
2414
2755
|
if (!this.panel)
|
|
@@ -2428,6 +2769,9 @@ export class TestHost {
|
|
|
2428
2769
|
* // ---cut---
|
|
2429
2770
|
* assert.deepEqual(host.sent("osd.show").at(-1), { text: "Saved" });
|
|
2430
2771
|
* ```
|
|
2772
|
+
*
|
|
2773
|
+
* @capability None.
|
|
2774
|
+
* @since 1.0.0
|
|
2431
2775
|
*/
|
|
2432
2776
|
sent(method) {
|
|
2433
2777
|
return this.calls
|
|
@@ -2477,6 +2821,10 @@ export class TestHost {
|
|
|
2477
2821
|
* await host.settle(20);
|
|
2478
2822
|
* assert.equal(window.props["status-text"], expected);
|
|
2479
2823
|
* ```
|
|
2824
|
+
*
|
|
2825
|
+
* @capability None.
|
|
2826
|
+
* @throws Never.
|
|
2827
|
+
* @since 1.0.0
|
|
2480
2828
|
*/
|
|
2481
2829
|
async settle(ms = 0) {
|
|
2482
2830
|
const deadline = Date.now() + ms;
|
|
@@ -2489,6 +2837,23 @@ export class TestHost {
|
|
|
2489
2837
|
/**
|
|
2490
2838
|
* Close the fake stream servers and delete the host's temporary
|
|
2491
2839
|
* directories. `stop()` from {@link startPlugin} calls it.
|
|
2840
|
+
*
|
|
2841
|
+
* @capability None.
|
|
2842
|
+
*
|
|
2843
|
+
* @example Clean up a host that was never started
|
|
2844
|
+
* A test that made a host but failed before `startPlugin` removes the
|
|
2845
|
+
* host's temporary directories itself. `stop()` from `startPlugin` calls
|
|
2846
|
+
* `close` for you otherwise.
|
|
2847
|
+
* ```ts
|
|
2848
|
+
* const host = new TestHost();
|
|
2849
|
+
* try {
|
|
2850
|
+
* // … preset the host; the test fails here …
|
|
2851
|
+
* } finally {
|
|
2852
|
+
* host.close(); // deletes the host's temporary data
|
|
2853
|
+
* }
|
|
2854
|
+
* ```
|
|
2855
|
+
*
|
|
2856
|
+
* @since 1.0.0
|
|
2492
2857
|
*/
|
|
2493
2858
|
close() {
|
|
2494
2859
|
for (const producer of this.producers.values())
|
|
@@ -2777,12 +3142,49 @@ export class TestHost {
|
|
|
2777
3142
|
!["ready", "ended"].includes(this.source.phase))
|
|
2778
3143
|
throw new HostError("unavailable", "audio tracks need ready media");
|
|
2779
3144
|
}
|
|
2780
|
-
|
|
3145
|
+
/** `stale` unless `expected` is the current subtitle stamp. */
|
|
3146
|
+
#subtitleExpected(expected) {
|
|
2781
3147
|
const stamp = this.subtitles.selection.stamp;
|
|
2782
3148
|
const want = expected;
|
|
2783
3149
|
if (want.generation !== stamp.generation ||
|
|
2784
3150
|
want.revision !== stamp.revision)
|
|
2785
3151
|
throw new HostError("stale", "subtitle selection changed");
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* The resources this plugin loaded into the current source (resource id →
|
|
3155
|
+
* bytes), removed ones included: like the player, a removed track frees
|
|
3156
|
+
* its place among the live tracks but not among the loads.
|
|
3157
|
+
*/
|
|
3158
|
+
#subtitleResources = { generation: -1, bytes: new Map() };
|
|
3159
|
+
#loadedSubtitles() {
|
|
3160
|
+
const generation = this.source.stamp.generation;
|
|
3161
|
+
if (this.#subtitleResources.generation !== generation)
|
|
3162
|
+
this.#subtitleResources = { generation, bytes: new Map() };
|
|
3163
|
+
return this.#subtitleResources.bytes;
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* The player's `SUBTITLE_LIMITS` for a new resource (`busy`, as the
|
|
3167
|
+
* player reports them). External tracks in `subtitles.tracks` that this
|
|
3168
|
+
* plugin did not load count as other plugins'. `totalBytes` is not
|
|
3169
|
+
* modelled: one plugin's 4 files of at most 2 MiB stay below it, and the
|
|
3170
|
+
* bytes of other plugins' tracks are unknown.
|
|
3171
|
+
*/
|
|
3172
|
+
#subtitleRoom(resourceId) {
|
|
3173
|
+
const loaded = this.#loadedSubtitles();
|
|
3174
|
+
if (loaded.has(resourceId))
|
|
3175
|
+
return;
|
|
3176
|
+
const external = this.subtitles.tracks.filter((t) => t.target.kind === "external");
|
|
3177
|
+
const own = external.filter((t) => t.target.kind === "external" && loaded.has(t.target.resourceId));
|
|
3178
|
+
const others = external.length - own.length;
|
|
3179
|
+
if (loaded.size >= SUBTITLE_LIMITS.loadsPerPlugin ||
|
|
3180
|
+
loaded.size + others >= SUBTITLE_LIMITS.loads ||
|
|
3181
|
+
own.length >= SUBTITLE_LIMITS.tracksPerPlugin ||
|
|
3182
|
+
external.length >= SUBTITLE_LIMITS.tracks)
|
|
3183
|
+
throw new HostError("busy", "subtitle resource or counter limit exceeded");
|
|
3184
|
+
}
|
|
3185
|
+
#subtitleChange(expected) {
|
|
3186
|
+
this.#subtitleExpected(expected);
|
|
3187
|
+
const stamp = this.subtitles.selection.stamp;
|
|
2786
3188
|
this.subtitles.selection.stamp = {
|
|
2787
3189
|
generation: this.source.stamp.generation,
|
|
2788
3190
|
revision: stamp.revision + 1,
|
|
@@ -3101,7 +3503,10 @@ export class TestHost {
|
|
|
3101
3503
|
},
|
|
3102
3504
|
"storage.session.delete": (p) => void this.sessionStorage.delete(sessionKey(p.key)),
|
|
3103
3505
|
"storage.session.keys": () => [...this.sessionStorage.keys()].sort(byCodePoint),
|
|
3104
|
-
"app.system": () =>
|
|
3506
|
+
"app.system": () => ({
|
|
3507
|
+
...structuredClone(this.systemInfo),
|
|
3508
|
+
rendering: this.videoRender.gpu ? "gpu" : "software",
|
|
3509
|
+
}),
|
|
3105
3510
|
"clipboard.readText": () => this.clipboard.text,
|
|
3106
3511
|
"clipboard.writeText": (p) => {
|
|
3107
3512
|
if (typeof p.text !== "string")
|
|
@@ -3785,9 +4190,24 @@ export class TestHost {
|
|
|
3785
4190
|
"subtitles.upload.begin": (p) => {
|
|
3786
4191
|
if (p.generation !== this.source.stamp.generation)
|
|
3787
4192
|
throw new HostError("stale", "source generation changed");
|
|
4193
|
+
const expectedBytes = Number(p.expectedBytes);
|
|
4194
|
+
if (!Number.isSafeInteger(expectedBytes) ||
|
|
4195
|
+
expectedBytes < 1 ||
|
|
4196
|
+
expectedBytes > SUBTITLE_LIMITS.fileBytes)
|
|
4197
|
+
throw new HostError("invalid", `a subtitle file has 1..${SUBTITLE_LIMITS.fileBytes} bytes`);
|
|
4198
|
+
// Like the player: a new source drops the old one's uploads. A commit
|
|
4199
|
+
// the player refused leaves its upload for `subtitles.upload.discard`,
|
|
4200
|
+
// which the SDK sends after any failed load.
|
|
4201
|
+
for (const [id, upload] of this.uploads)
|
|
4202
|
+
if (upload.generation !== undefined &&
|
|
4203
|
+
upload.generation !== this.source.stamp.generation)
|
|
4204
|
+
this.uploads.delete(id);
|
|
4205
|
+
if (this.uploads.size >= SUBTITLE_LIMITS.uploadsPerPlugin)
|
|
4206
|
+
throw new HostError("busy", "upload memory, owner or active-token limit reached");
|
|
3788
4207
|
this.uploads.set(Number(p.uploadId), {
|
|
3789
4208
|
bytes: [],
|
|
3790
|
-
expected:
|
|
4209
|
+
expected: expectedBytes,
|
|
4210
|
+
generation: this.source.stamp.generation,
|
|
3791
4211
|
});
|
|
3792
4212
|
return {
|
|
3793
4213
|
generation: p.generation,
|
|
@@ -3805,9 +4225,8 @@ export class TestHost {
|
|
|
3805
4225
|
},
|
|
3806
4226
|
"subtitles.upload.discard": (p) => void this.uploads.delete(Number(p.uploadId)),
|
|
3807
4227
|
"subtitles.commit": (p) => {
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
throw new HostError("missing", "upload");
|
|
4228
|
+
// The player's order: the request's own fields, then `stale`, then
|
|
4229
|
+
// the upload and its content, then the limits.
|
|
3811
4230
|
const format = p.format === "ass" ? "ass" : "srt";
|
|
3812
4231
|
const select = p.select === "none"
|
|
3813
4232
|
? "none"
|
|
@@ -3821,12 +4240,18 @@ export class TestHost {
|
|
|
3821
4240
|
: null;
|
|
3822
4241
|
if (title !== null && ([...title].length < 1 || [...title].length > 200))
|
|
3823
4242
|
throw new HostError("invalid", "subtitle title needs 1..200 characters");
|
|
4243
|
+
this.#subtitleExpected(p.expected);
|
|
4244
|
+
const upload = this.uploads.get(Number(p.uploadId));
|
|
4245
|
+
if (!upload)
|
|
4246
|
+
throw new HostError("missing", "upload");
|
|
3824
4247
|
const bytes = Buffer.concat(upload.bytes);
|
|
3825
4248
|
if (format === "ass" &&
|
|
3826
4249
|
!(/\[Script Info\]/i.test(bytes.toString()) &&
|
|
3827
4250
|
/\[Events\]/i.test(bytes.toString())))
|
|
3828
4251
|
throw new HostError("invalid", "ASS requires [Script Info] and [Events] sections");
|
|
4252
|
+
this.#subtitleRoom(String(p.resourceId));
|
|
3829
4253
|
this.#subtitleChange(p.expected);
|
|
4254
|
+
this.#loadedSubtitles().set(String(p.resourceId), bytes.length);
|
|
3830
4255
|
const target = {
|
|
3831
4256
|
kind: "external",
|
|
3832
4257
|
owner: 1,
|
|
@@ -4138,6 +4563,16 @@ export class TestHost {
|
|
|
4138
4563
|
* }
|
|
4139
4564
|
* });
|
|
4140
4565
|
* ```
|
|
4566
|
+
*
|
|
4567
|
+
* @remarks
|
|
4568
|
+
* The way into a plugin's tests: one call starts the plugin the way the
|
|
4569
|
+
* player's runner does and returns the host to drive, the `plugin` object
|
|
4570
|
+
* to call directly, and `stop` to end it like the player.
|
|
4571
|
+
*
|
|
4572
|
+
* @capability None.
|
|
4573
|
+
* @see [Test a plugin](../../docs/how-to/testing.md)
|
|
4574
|
+
* @since 1.0.0
|
|
4575
|
+
* @category Testing
|
|
4141
4576
|
*/
|
|
4142
4577
|
export async function startPlugin(activate, options = {}) {
|
|
4143
4578
|
const host = options.host ?? new TestHost();
|