@navipartner/demo-video 0.7.0 → 0.7.2
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/README.md +13 -9
- package/bin/demo-video.mjs +98 -17
- package/bin/init.d.mts +2 -0
- package/bin/init.mjs +17 -6
- package/dist/{adapter-CDOXdUZI.d.ts → adapter-DxZmb-QU.d.ts} +47 -8
- package/dist/{chunk-D7UYGNSF.js → chunk-XMOGUZJU.js} +211 -50
- package/dist/index.d.ts +119 -20
- package/dist/index.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/index.js +753 -217
- package/package.json +1 -1
- package/skills/np-demo-video/SKILL.md +242 -230
- package/skills/np-demo-video/look.md +182 -82
- package/skills/np-demo-video/reference.md +89 -30
package/dist/runner/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CARD_IN_MS,
|
|
3
3
|
CARD_OUT_MS,
|
|
4
|
+
COVERED_MAX_MS,
|
|
5
|
+
COVERED_SETTLED_MS,
|
|
6
|
+
CURSOR_MAX_MS,
|
|
7
|
+
CURSOR_MIN_MS,
|
|
4
8
|
DEFAULT_VIEWPORT,
|
|
9
|
+
ESTABLISH_MS,
|
|
5
10
|
LINGER_MS,
|
|
6
11
|
MIN_SCALE,
|
|
7
12
|
PARK,
|
|
@@ -11,15 +16,20 @@ import {
|
|
|
11
16
|
POST_KEY_MS,
|
|
12
17
|
PRESS_MS,
|
|
13
18
|
PRE_CLICK_MS,
|
|
19
|
+
REACTION_MS,
|
|
20
|
+
SETTLED_MS,
|
|
14
21
|
SETTLE_MS,
|
|
15
22
|
STAGE_HEIGHT,
|
|
16
23
|
STAGE_PATH,
|
|
17
24
|
STAGE_WIDTH,
|
|
25
|
+
STILLS_PATH,
|
|
18
26
|
StoryboardError,
|
|
27
|
+
TYPE_CHAR_MS,
|
|
19
28
|
ZOOM_MS,
|
|
20
29
|
cardSeconds,
|
|
21
30
|
coverage,
|
|
22
31
|
fittingScale,
|
|
32
|
+
isBehindCard,
|
|
23
33
|
isCoreStep,
|
|
24
34
|
lintStoryboard,
|
|
25
35
|
loadStoryboard,
|
|
@@ -27,10 +37,11 @@ import {
|
|
|
27
37
|
shows,
|
|
28
38
|
stageHtml,
|
|
29
39
|
stageLayout,
|
|
40
|
+
titleSeconds,
|
|
30
41
|
unionRect,
|
|
31
42
|
visibleRect,
|
|
32
43
|
wideShot
|
|
33
|
-
} from "../chunk-
|
|
44
|
+
} from "../chunk-XMOGUZJU.js";
|
|
34
45
|
|
|
35
46
|
// src/runner/config.ts
|
|
36
47
|
import path from "path";
|
|
@@ -67,8 +78,8 @@ var createDemoConfig = (adapter, demoDir, runnerDir, mode = process.env.DEMO_MOD
|
|
|
67
78
|
});
|
|
68
79
|
|
|
69
80
|
// src/runner/register.ts
|
|
70
|
-
import
|
|
71
|
-
import
|
|
81
|
+
import fs4 from "fs";
|
|
82
|
+
import path5 from "path";
|
|
72
83
|
|
|
73
84
|
// src/brand.ts
|
|
74
85
|
import fs from "fs";
|
|
@@ -249,16 +260,36 @@ var missingNeeds = (brand, needs = {}) => {
|
|
|
249
260
|
];
|
|
250
261
|
};
|
|
251
262
|
|
|
263
|
+
// src/director.ts
|
|
264
|
+
import fs2 from "fs";
|
|
265
|
+
import path3 from "path";
|
|
266
|
+
|
|
252
267
|
// src/changes.ts
|
|
268
|
+
var MAX_RESTYLES = 3;
|
|
253
269
|
var watchChanges = (target) => target.evaluate((el) => {
|
|
254
|
-
const
|
|
270
|
+
const w = window;
|
|
271
|
+
w.__demoChanges?.observer.disconnect();
|
|
272
|
+
const changed = /* @__PURE__ */ new Map();
|
|
273
|
+
const watch = { changed, lastAt: Date.now() };
|
|
255
274
|
const observer = new MutationObserver((records) => {
|
|
256
275
|
for (const record of records) {
|
|
276
|
+
if (record.type !== "attributes") {
|
|
277
|
+
watch.lastAt = Date.now();
|
|
278
|
+
}
|
|
257
279
|
const nodes = record.type === "childList" ? Array.from(record.addedNodes) : [record.target];
|
|
258
280
|
for (const node of nodes) {
|
|
259
281
|
const changedEl = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
260
282
|
if (changedEl && !el.contains(changedEl) && !changedEl.contains(el)) {
|
|
261
|
-
changed.
|
|
283
|
+
const seen = changed.get(changedEl) ?? {
|
|
284
|
+
content: false,
|
|
285
|
+
restyles: 0
|
|
286
|
+
};
|
|
287
|
+
if (record.type === "attributes") {
|
|
288
|
+
seen.restyles++;
|
|
289
|
+
} else {
|
|
290
|
+
seen.content = true;
|
|
291
|
+
}
|
|
292
|
+
changed.set(changedEl, seen);
|
|
262
293
|
}
|
|
263
294
|
}
|
|
264
295
|
}
|
|
@@ -266,14 +297,21 @@ var watchChanges = (target) => target.evaluate((el) => {
|
|
|
266
297
|
observer.observe(document.body, {
|
|
267
298
|
subtree: true,
|
|
268
299
|
childList: true,
|
|
269
|
-
characterData: true
|
|
300
|
+
characterData: true,
|
|
301
|
+
attributes: true,
|
|
302
|
+
attributeFilter: ["style", "class", "src"]
|
|
270
303
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
304
|
+
watch.observer = observer;
|
|
305
|
+
w.__demoChanges = watch;
|
|
306
|
+
});
|
|
307
|
+
var watching = (app) => app.evaluate(
|
|
308
|
+
() => Boolean(window.__demoChanges)
|
|
309
|
+
);
|
|
310
|
+
var quietMs = (app) => app.evaluate(() => {
|
|
311
|
+
const watch = window.__demoChanges;
|
|
312
|
+
return watch ? Date.now() - watch.lastAt : Number.POSITIVE_INFINITY;
|
|
275
313
|
});
|
|
276
|
-
var biggestChange = (app) => app.evaluate(() => {
|
|
314
|
+
var biggestChange = (app) => app.evaluate((maxRestyles) => {
|
|
277
315
|
const w = window;
|
|
278
316
|
const watch = w.__demoChanges;
|
|
279
317
|
delete w.__demoChanges;
|
|
@@ -282,9 +320,10 @@ var biggestChange = (app) => app.evaluate(() => {
|
|
|
282
320
|
}
|
|
283
321
|
watch.observer.disconnect();
|
|
284
322
|
let best = null;
|
|
323
|
+
let bestEl = null;
|
|
285
324
|
let label = "";
|
|
286
|
-
for (const el of watch.changed) {
|
|
287
|
-
if (!el.isConnected) {
|
|
325
|
+
for (const [el, seen] of watch.changed) {
|
|
326
|
+
if (!el.isConnected || !seen.content && seen.restyles > maxRestyles) {
|
|
288
327
|
continue;
|
|
289
328
|
}
|
|
290
329
|
const r = el.getBoundingClientRect();
|
|
@@ -293,16 +332,36 @@ var biggestChange = (app) => app.evaluate(() => {
|
|
|
293
332
|
}
|
|
294
333
|
if (!best || r.width * r.height > best.width * best.height) {
|
|
295
334
|
best = { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
335
|
+
bestEl = el;
|
|
296
336
|
label = el.getAttribute("data-testid") ?? el.tagName.toLowerCase() + (el.textContent ? ` "${el.textContent.trim().slice(0, 30)}"` : "");
|
|
297
337
|
}
|
|
298
338
|
}
|
|
299
|
-
|
|
300
|
-
|
|
339
|
+
if (!best || !bestEl) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
let fontPx = Number.POSITIVE_INFINITY;
|
|
343
|
+
const texts = [
|
|
344
|
+
bestEl,
|
|
345
|
+
...Array.from(bestEl.querySelectorAll("*")).slice(0, 200)
|
|
346
|
+
];
|
|
347
|
+
for (const node of texts) {
|
|
348
|
+
const hasText = Array.from(node.childNodes).some(
|
|
349
|
+
(c) => c.nodeType === Node.TEXT_NODE && c.textContent?.trim()
|
|
350
|
+
);
|
|
351
|
+
if (hasText) {
|
|
352
|
+
fontPx = Math.min(fontPx, parseFloat(getComputedStyle(node).fontSize));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { rect: best, label, fontPx };
|
|
356
|
+
}, MAX_RESTYLES);
|
|
301
357
|
|
|
302
358
|
// src/director.ts
|
|
303
359
|
var TARGET_TIMEOUT_MS = 15e3;
|
|
304
360
|
var SHOWN = 0.5;
|
|
305
361
|
var FRAME_MAX_SCALE = 1.8;
|
|
362
|
+
var PUSH_WORTHY = 1.4;
|
|
363
|
+
var READABLE_PX = 16;
|
|
364
|
+
var STILL_MAX_PX = 1600;
|
|
306
365
|
var testIdSelector = (selector) => /^[\w-]+$/.test(selector) ? `[data-testid="${selector}"]` : selector;
|
|
307
366
|
var normalise = (text) => text.replace(/\s+/g, " ").trim();
|
|
308
367
|
var sameShot = (a, b) => Math.abs(a.k - b.k) < 1e-3 && Math.abs(a.tx - b.tx) < 1 && Math.abs(a.ty - b.ty) < 1;
|
|
@@ -327,6 +386,20 @@ var Director = class {
|
|
|
327
386
|
* what the click did.
|
|
328
387
|
*/
|
|
329
388
|
unseen;
|
|
389
|
+
/** Behind a card: no cursor, no camera moves, nothing for the viewer. */
|
|
390
|
+
covered = false;
|
|
391
|
+
/**
|
|
392
|
+
* What clicks in the current wide shot used: their controls and what they
|
|
393
|
+
* changed. When the wide shot ends, all of it must not have fit a push-in.
|
|
394
|
+
*/
|
|
395
|
+
wideUse = {
|
|
396
|
+
rects: [],
|
|
397
|
+
names: [],
|
|
398
|
+
textPx: Number.POSITIVE_INFINITY
|
|
399
|
+
};
|
|
400
|
+
/** When the whole window came into view (virtual ms), for ESTABLISH_MS. */
|
|
401
|
+
wideSince = 0;
|
|
402
|
+
cdp;
|
|
330
403
|
get fast() {
|
|
331
404
|
return this.options.fast;
|
|
332
405
|
}
|
|
@@ -410,21 +483,39 @@ var Director = class {
|
|
|
410
483
|
}
|
|
411
484
|
// --- camera -----------------------------------------------------------------
|
|
412
485
|
async zoomTo(t, scale) {
|
|
486
|
+
this.closeWide();
|
|
413
487
|
const rect = await this.appRect(t);
|
|
414
488
|
const next = shotFor(this.layout, rect, scale);
|
|
415
489
|
this.requireSeen("zoom", next);
|
|
416
490
|
if (sameShot(next, this.shot)) {
|
|
417
491
|
return;
|
|
418
492
|
}
|
|
493
|
+
this.wasWide = this.isWide();
|
|
419
494
|
this.shot = next;
|
|
420
495
|
this.shotLabel = `the shot on "${t.target}" at ${scale}`;
|
|
496
|
+
await this.moveCamera(next);
|
|
497
|
+
}
|
|
498
|
+
// Films a camera move. A push-in from the wide shot waits until the whole
|
|
499
|
+
// window has been seen for ESTABLISH_MS: a page never opens zoomed.
|
|
500
|
+
async moveCamera(shot) {
|
|
421
501
|
if (this.fast) {
|
|
422
502
|
return;
|
|
423
503
|
}
|
|
424
|
-
|
|
425
|
-
|
|
504
|
+
const fromWide = this.wasWide;
|
|
505
|
+
if (fromWide && !this.covered) {
|
|
506
|
+
await this.wait(ESTABLISH_MS - (this.clockMs() - this.wideSince));
|
|
507
|
+
}
|
|
508
|
+
const instant = this.covered;
|
|
509
|
+
await this.page.evaluate(([s, i]) => window.__demo.setShot(s, i), [
|
|
510
|
+
shot,
|
|
511
|
+
instant
|
|
512
|
+
]);
|
|
513
|
+
if (!instant) {
|
|
514
|
+
await this.wait(ZOOM_MS + SETTLE_MS);
|
|
515
|
+
}
|
|
426
516
|
}
|
|
427
517
|
async frame(targets, options = {}) {
|
|
518
|
+
this.closeWide();
|
|
428
519
|
const names = targets.map((t) => `"${t.target}"`).join(" and ");
|
|
429
520
|
for (const t of targets) {
|
|
430
521
|
await this.appRect(t);
|
|
@@ -448,7 +539,7 @@ var Director = class {
|
|
|
448
539
|
);
|
|
449
540
|
if (scale < MIN_SCALE) {
|
|
450
541
|
throw new Error(
|
|
451
|
-
`${names} only fit in the wide shot (at ${scale.toFixed(2)})
|
|
542
|
+
`${names} only fit in the wide shot (at ${scale.toFixed(2)}): stay wide, or frame things that sit together`
|
|
452
543
|
);
|
|
453
544
|
}
|
|
454
545
|
const next = shotFor(this.layout, all, scale);
|
|
@@ -459,13 +550,10 @@ var Director = class {
|
|
|
459
550
|
if (sameShot(next, this.shot)) {
|
|
460
551
|
return;
|
|
461
552
|
}
|
|
553
|
+
this.wasWide = this.isWide();
|
|
462
554
|
this.shot = next;
|
|
463
555
|
this.shotLabel = `the shot framing ${names}`;
|
|
464
|
-
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
await this.page.evaluate((s) => window.__demo.setShot(s), next);
|
|
468
|
-
await this.wait(ZOOM_MS + SETTLE_MS);
|
|
556
|
+
await this.moveCamera(next);
|
|
469
557
|
}
|
|
470
558
|
async zoomOut(options = {}) {
|
|
471
559
|
this.requireSeen("zoomOut");
|
|
@@ -488,11 +576,19 @@ var Director = class {
|
|
|
488
576
|
await this.page.evaluate((s) => window.__demo.setShot(s), wide);
|
|
489
577
|
}
|
|
490
578
|
await this.wait(Math.max(moving ? ZOOM_MS + SETTLE_MS : 0, parkMs));
|
|
579
|
+
if (moving) {
|
|
580
|
+
this.wideSince = this.clockMs();
|
|
581
|
+
}
|
|
491
582
|
}
|
|
583
|
+
// Whether the camera shows the whole window now.
|
|
584
|
+
isWide() {
|
|
585
|
+
return this.shot.k <= this.layout.fit * 1.01;
|
|
586
|
+
}
|
|
587
|
+
wasWide = false;
|
|
492
588
|
// --- cursor + input -----------------------------------------------------------
|
|
493
589
|
/** Rests the cursor in empty space, off the content. */
|
|
494
590
|
async park() {
|
|
495
|
-
if (this.fast) {
|
|
591
|
+
if (this.fast || this.covered) {
|
|
496
592
|
return;
|
|
497
593
|
}
|
|
498
594
|
const ms = await this.page.evaluate(
|
|
@@ -510,28 +606,41 @@ var Director = class {
|
|
|
510
606
|
const margin = 8;
|
|
511
607
|
if (cx < v.x + margin || cx > v.x + v.width - margin || cy < v.y + margin || cy > v.y + v.height - margin) {
|
|
512
608
|
throw new Error(
|
|
513
|
-
`"${t.target}" is outside ${this.shotLabel}
|
|
609
|
+
`"${t.target}" is outside ${this.shotLabel}: zoomOut first, or frame an area that holds everything this shot uses.`
|
|
514
610
|
);
|
|
515
611
|
}
|
|
516
|
-
return this.toStagePoint(rect);
|
|
612
|
+
return { point: this.toStagePoint(rect), rect };
|
|
517
613
|
}
|
|
518
614
|
async click(t, options = {}) {
|
|
615
|
+
await this.interact(t, { zoom: options.zoom, verb: "clicking" });
|
|
616
|
+
}
|
|
617
|
+
// One interaction with a control, as a person makes it: the cursor glides
|
|
618
|
+
// there and rests, presses (a real click, or only the look of one when an
|
|
619
|
+
// adapter action does the work), `then` runs (typing, the action), and the
|
|
620
|
+
// app's reaction plays out. What it changes is watched from before the
|
|
621
|
+
// press to the end, so typing's live preview, a toast after a save and a
|
|
622
|
+
// restyle all count as the result. Behind a card none of it is filmed.
|
|
623
|
+
async interact(t, options) {
|
|
519
624
|
if (options.zoom !== void 0) {
|
|
520
625
|
await this.zoomTo(t, options.zoom);
|
|
521
626
|
} else {
|
|
522
627
|
this.requireSeen("other");
|
|
523
628
|
}
|
|
524
|
-
const point = await this.pointAt(t);
|
|
525
|
-
const
|
|
526
|
-
|
|
629
|
+
const { point, rect } = await this.pointAt(t);
|
|
630
|
+
const filmed = !this.fast && !this.covered;
|
|
631
|
+
const zoomed = !this.isWide();
|
|
632
|
+
const inner = await watching(this.app);
|
|
633
|
+
const fontPx = inner ? Number.NaN : await this.locator(t).evaluate(
|
|
634
|
+
(el) => parseFloat(getComputedStyle(el).fontSize),
|
|
635
|
+
void 0,
|
|
636
|
+
{
|
|
637
|
+
timeout: 1e3
|
|
638
|
+
}
|
|
639
|
+
).catch(() => Number.NaN);
|
|
640
|
+
if (!inner) {
|
|
527
641
|
await watchChanges(this.locator(t));
|
|
528
642
|
}
|
|
529
|
-
if (
|
|
530
|
-
await this.page.mouse.click(point.x, point.y);
|
|
531
|
-
if (zoomed) {
|
|
532
|
-
await this.page.waitForTimeout(POST_CLICK_MS);
|
|
533
|
-
}
|
|
534
|
-
} else {
|
|
643
|
+
if (filmed) {
|
|
535
644
|
const ms = await this.page.evaluate(
|
|
536
645
|
([x, y]) => window.__demo.cursorTo(x, y),
|
|
537
646
|
[point.x, point.y]
|
|
@@ -539,20 +648,87 @@ var Director = class {
|
|
|
539
648
|
await this.wait(ms);
|
|
540
649
|
await this.page.mouse.move(point.x, point.y);
|
|
541
650
|
await this.wait(PRE_CLICK_MS);
|
|
651
|
+
}
|
|
652
|
+
if (options.press === "look") {
|
|
653
|
+
await this.wait(PRESS_MS);
|
|
654
|
+
} else if (filmed) {
|
|
542
655
|
await this.page.mouse.down();
|
|
543
656
|
await this.wait(PRESS_MS);
|
|
544
657
|
await this.page.mouse.up();
|
|
658
|
+
} else {
|
|
659
|
+
await this.page.mouse.click(point.x, point.y);
|
|
660
|
+
}
|
|
661
|
+
await options.then?.();
|
|
662
|
+
if (this.covered) {
|
|
663
|
+
await this.settle(COVERED_SETTLED_MS, COVERED_MAX_MS);
|
|
664
|
+
if (!inner) {
|
|
665
|
+
await biggestChange(this.app);
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (filmed) {
|
|
545
670
|
await this.wait(POST_CLICK_MS);
|
|
671
|
+
} else {
|
|
672
|
+
await this.page.waitForTimeout(POST_CLICK_MS);
|
|
673
|
+
}
|
|
674
|
+
await this.settle(SETTLED_MS, REACTION_MS);
|
|
675
|
+
if (inner) {
|
|
676
|
+
return;
|
|
546
677
|
}
|
|
547
678
|
if (zoomed) {
|
|
548
|
-
await this.assertChangeInShot(t);
|
|
679
|
+
await this.assertChangeInShot(t, options.verb);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const biggest = await biggestChange(this.app);
|
|
683
|
+
this.wideUse.rects.push(rect, ...biggest ? [biggest.rect] : []);
|
|
684
|
+
this.wideUse.names.push(t.target);
|
|
685
|
+
const smallest = Math.min(
|
|
686
|
+
Number.isFinite(fontPx) ? fontPx : Number.POSITIVE_INFINITY,
|
|
687
|
+
biggest?.fontPx ?? Number.POSITIVE_INFINITY
|
|
688
|
+
);
|
|
689
|
+
if (Number.isFinite(smallest)) {
|
|
690
|
+
this.wideUse.textPx = Math.min(
|
|
691
|
+
this.wideUse.textPx,
|
|
692
|
+
smallest * this.layout.fit
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Until nothing in the app has changed for `quiet` ms, at most `max` ms:
|
|
697
|
+
// filmed on the virtual clock, in real time in check.
|
|
698
|
+
async settle(quiet, max) {
|
|
699
|
+
for (let waited = 0; waited < max; waited += POLL_MS) {
|
|
700
|
+
if (await quietMs(this.app) >= quiet) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (this.fast) {
|
|
704
|
+
await this.page.waitForTimeout(POLL_MS);
|
|
705
|
+
} else {
|
|
706
|
+
await this.wait(POLL_MS);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// The wide shot ends (a push-in, a card, the end). Its clicks must have
|
|
711
|
+
// needed it: if their controls and results all fit a push-in, the wide
|
|
712
|
+
// shot showed them too small to read on a phone.
|
|
713
|
+
closeWide() {
|
|
714
|
+
const { rects, names, textPx } = this.wideUse;
|
|
715
|
+
this.wideUse = { rects: [], names: [], textPx: Number.POSITIVE_INFINITY };
|
|
716
|
+
if (names.length === 0 || textPx >= READABLE_PX) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const scale = fittingScale(this.layout, unionRect(rects));
|
|
720
|
+
if (scale >= PUSH_WORTHY) {
|
|
721
|
+
const used = [...new Set(names)].map((n) => `"${n}"`).join(", ");
|
|
722
|
+
throw new Error(
|
|
723
|
+
`${used} ${names.length > 1 ? "are" : "is"} used in the wide shot, where its text is ${Math.round(textPx)} px, too small to read, and the controls with what they change fit a push-in at ${Math.min(scale, FRAME_MAX_SCALE).toFixed(1)}: frame them (the engine pushes in after a wide beat), or do it behindCard if it's setup`
|
|
724
|
+
);
|
|
549
725
|
}
|
|
550
726
|
}
|
|
551
727
|
// A zoomed shot is about what the click does. If the biggest thing the
|
|
552
728
|
// click changed lands outside the shot (an item added to a basket
|
|
553
729
|
// off-screen), the zoom is on the wrong area: the viewer would see a
|
|
554
730
|
// button pressed and nothing happen.
|
|
555
|
-
async assertChangeInShot(t) {
|
|
731
|
+
async assertChangeInShot(t, verb) {
|
|
556
732
|
const biggest = await biggestChange(this.app);
|
|
557
733
|
if (!biggest) {
|
|
558
734
|
return;
|
|
@@ -560,7 +736,7 @@ var Director = class {
|
|
|
560
736
|
if (coverage(biggest.rect, visibleRect(this.layout, this.shot)) < SHOWN) {
|
|
561
737
|
this.unseen = {
|
|
562
738
|
rect: biggest.rect,
|
|
563
|
-
message:
|
|
739
|
+
message: `${verb} "${t.target}" changes ${biggest.label}, which is outside ${this.shotLabel}, and the next step doesn't show it: zoomOut right after, or frame the control with what it changes.`
|
|
564
740
|
};
|
|
565
741
|
}
|
|
566
742
|
}
|
|
@@ -579,17 +755,34 @@ var Director = class {
|
|
|
579
755
|
}
|
|
580
756
|
throw new Error(unseen.message);
|
|
581
757
|
}
|
|
582
|
-
async type(t, text, perCharMs =
|
|
583
|
-
await this.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
758
|
+
async type(t, text, perCharMs = TYPE_CHAR_MS, options = {}) {
|
|
759
|
+
await this.interact(t, {
|
|
760
|
+
verb: "typing in",
|
|
761
|
+
then: async () => {
|
|
762
|
+
if (options.replace) {
|
|
763
|
+
await this.page.keyboard.press("ControlOrMeta+A");
|
|
764
|
+
} else {
|
|
765
|
+
await this.app.evaluate(() => {
|
|
766
|
+
const el = document.activeElement;
|
|
767
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
768
|
+
try {
|
|
769
|
+
el.setSelectionRange(el.value.length, el.value.length);
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (this.fast || this.covered) {
|
|
776
|
+
await this.page.keyboard.type(text);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
await this.wait(POST_KEY_MS);
|
|
780
|
+
for (const char of text) {
|
|
781
|
+
await this.page.keyboard.type(char);
|
|
782
|
+
await this.wait(perCharMs);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
});
|
|
593
786
|
}
|
|
594
787
|
async press(key) {
|
|
595
788
|
this.requireSeen("other");
|
|
@@ -603,14 +796,24 @@ var Director = class {
|
|
|
603
796
|
/** After the last step: a zoomed click's change must have been shown. */
|
|
604
797
|
end() {
|
|
605
798
|
this.requireSeen("other");
|
|
799
|
+
this.closeWide();
|
|
606
800
|
}
|
|
607
|
-
async during(work) {
|
|
608
|
-
this.requireSeen("other");
|
|
801
|
+
async during(work, options = {}) {
|
|
609
802
|
const recorder = this.options.recorder;
|
|
610
|
-
|
|
611
|
-
|
|
803
|
+
const run = () => this.fast || !recorder ? work() : recorder.during(work());
|
|
804
|
+
if (options.target) {
|
|
805
|
+
let result2;
|
|
806
|
+
await this.interact(options.target, {
|
|
807
|
+
verb: "using",
|
|
808
|
+
press: "look",
|
|
809
|
+
then: async () => {
|
|
810
|
+
result2 = await run();
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
return result2;
|
|
612
814
|
}
|
|
613
|
-
|
|
815
|
+
this.requireSeen("other");
|
|
816
|
+
const result = await run();
|
|
614
817
|
await this.wait(POST_ACTION_MS);
|
|
615
818
|
return result;
|
|
616
819
|
}
|
|
@@ -651,24 +854,85 @@ var Director = class {
|
|
|
651
854
|
// --- overlays --------------------------------------------------------------------
|
|
652
855
|
async card(spec, options = {}) {
|
|
653
856
|
this.requireSeen("other");
|
|
857
|
+
this.closeWide();
|
|
654
858
|
this.shot = wideShot(this.layout);
|
|
655
859
|
this.shotLabel = "the wide shot";
|
|
656
860
|
if (this.fast) {
|
|
861
|
+
await this.behind(options.behind);
|
|
657
862
|
return;
|
|
658
863
|
}
|
|
659
|
-
|
|
660
|
-
|
|
864
|
+
if (!options.opening) {
|
|
865
|
+
await this.showCard(spec);
|
|
866
|
+
await this.wait(CARD_IN_MS);
|
|
867
|
+
}
|
|
661
868
|
await this.page.evaluate(() => {
|
|
662
869
|
window.__demo.resetCamera();
|
|
663
870
|
window.__demo.parkNow();
|
|
664
871
|
});
|
|
665
872
|
await this.page.mouse.move(PARK.x, PARK.y);
|
|
666
|
-
|
|
873
|
+
const started = this.clockMs();
|
|
874
|
+
await this.behind(options.behind);
|
|
875
|
+
await this.page.mouse.move(PARK.x, PARK.y);
|
|
876
|
+
const readMs = (options.opening ? titleSeconds(spec, options.kind) : cardSeconds(spec)) * 1e3;
|
|
877
|
+
await this.wait(readMs - (this.clockMs() - started));
|
|
667
878
|
if (!options.keepUp) {
|
|
668
879
|
await this.hideCard();
|
|
669
880
|
await this.wait(CARD_OUT_MS);
|
|
881
|
+
this.wideSince = this.clockMs();
|
|
670
882
|
}
|
|
671
883
|
}
|
|
884
|
+
async behind(work) {
|
|
885
|
+
if (!work) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
this.covered = true;
|
|
889
|
+
try {
|
|
890
|
+
await work();
|
|
891
|
+
} finally {
|
|
892
|
+
this.covered = false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async capture(t, file) {
|
|
896
|
+
const locator = this.locator(t);
|
|
897
|
+
if (!await locator.isVisible().catch(() => false)) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
const r = await this.readRect(locator);
|
|
901
|
+
const { width, height } = this.layout.viewport;
|
|
902
|
+
const x0 = Math.max(0, r.x);
|
|
903
|
+
const y0 = Math.max(0, r.y);
|
|
904
|
+
const x1 = Math.min(width, r.x + r.width);
|
|
905
|
+
const y1 = Math.min(height, r.y + r.height);
|
|
906
|
+
if (x1 - x0 < 1 || y1 - y0 < 1) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
const shot = this.fast ? wideShot(this.layout) : this.shot;
|
|
910
|
+
const k = shot.k;
|
|
911
|
+
const left = Math.ceil(this.layout.left + shot.tx + k * x0);
|
|
912
|
+
const top = Math.ceil(this.layout.top + shot.ty + k * y0);
|
|
913
|
+
const right = Math.floor(this.layout.left + shot.tx + k * x1);
|
|
914
|
+
const bottom = Math.floor(this.layout.top + shot.ty + k * y1);
|
|
915
|
+
if (right - left < 1 || bottom - top < 1) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
this.cdp ??= await this.page.context().newCDPSession(this.page);
|
|
919
|
+
const { data } = await this.cdp.send("Page.captureScreenshot", {
|
|
920
|
+
format: "png",
|
|
921
|
+
clip: {
|
|
922
|
+
x: left,
|
|
923
|
+
y: top,
|
|
924
|
+
width: right - left,
|
|
925
|
+
height: bottom - top,
|
|
926
|
+
scale: Math.min(
|
|
927
|
+
2 / k,
|
|
928
|
+
STILL_MAX_PX / Math.max(right - left, bottom - top)
|
|
929
|
+
)
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
fs2.mkdirSync(path3.dirname(file), { recursive: true });
|
|
933
|
+
fs2.writeFileSync(file, Buffer.from(data, "base64"));
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
672
936
|
/** `instant` puts the card up with no dissolve (the video's first frame). */
|
|
673
937
|
async showCard(spec, instant = false) {
|
|
674
938
|
if (!this.fast) {
|
|
@@ -698,10 +962,367 @@ var Director = class {
|
|
|
698
962
|
}
|
|
699
963
|
};
|
|
700
964
|
|
|
965
|
+
// src/run.ts
|
|
966
|
+
var CAMERA_MOVE_MS = ZOOM_MS + SETTLE_MS;
|
|
967
|
+
var CARD_DISSOLVE_MS = CARD_IN_MS + CARD_OUT_MS;
|
|
968
|
+
var IDLE_MARGIN_MS = 1e4;
|
|
969
|
+
var INTERACTIONS = /* @__PURE__ */ new Set(["click", "type", "press"]);
|
|
970
|
+
var CAMERA_MOVES = /* @__PURE__ */ new Set(["zoom", "frame", "zoomOut"]);
|
|
971
|
+
var describeTarget = (t) => typeof t === "string" ? t : typeof t === "object" && t !== null && "target" in t ? `${String(t.target)}${"nth" in t && typeof t.nth === "number" ? `[${t.nth}]` : ""}` : "";
|
|
972
|
+
var describeStep = (step) => {
|
|
973
|
+
if ("targets" in step && Array.isArray(step.targets)) {
|
|
974
|
+
return `${step.do} ${step.targets.map(describeTarget).join(" + ")}`;
|
|
975
|
+
}
|
|
976
|
+
const target = "target" in step ? describeTarget(step) : "";
|
|
977
|
+
return `${step.do}${target ? ` ${target}` : ""}`;
|
|
978
|
+
};
|
|
979
|
+
var StepError = class extends Error {
|
|
980
|
+
constructor(index, step, reason, visible) {
|
|
981
|
+
super(`Step ${index + 1} (${describeStep(step)}) failed: ${reason}`);
|
|
982
|
+
this.index = index;
|
|
983
|
+
this.step = step;
|
|
984
|
+
this.reason = reason;
|
|
985
|
+
this.visible = visible;
|
|
986
|
+
}
|
|
987
|
+
index;
|
|
988
|
+
step;
|
|
989
|
+
reason;
|
|
990
|
+
visible;
|
|
991
|
+
};
|
|
992
|
+
var reasonOf = (cause) => {
|
|
993
|
+
if (cause && typeof cause === "object" && "issues" in cause && Array.isArray(cause.issues)) {
|
|
994
|
+
return cause.issues.map((i) => `${i.path.join(".") || "(params)"}: ${i.message}`).join("; ");
|
|
995
|
+
}
|
|
996
|
+
const text = cause instanceof Error ? cause.message : String(cause);
|
|
997
|
+
return text.split("\n")[0].trim();
|
|
998
|
+
};
|
|
999
|
+
var runStep = async (d, step, adapter) => {
|
|
1000
|
+
if (isCoreStep(step)) {
|
|
1001
|
+
switch (step.do) {
|
|
1002
|
+
case "click":
|
|
1003
|
+
return d.click(step, { zoom: step.zoom });
|
|
1004
|
+
case "type":
|
|
1005
|
+
return d.type(step, step.text, step.perCharMs, {
|
|
1006
|
+
replace: step.replace
|
|
1007
|
+
});
|
|
1008
|
+
case "press":
|
|
1009
|
+
return d.press(step.key);
|
|
1010
|
+
case "zoom":
|
|
1011
|
+
return d.zoomTo(step, step.scale);
|
|
1012
|
+
case "frame":
|
|
1013
|
+
return d.frame(
|
|
1014
|
+
step.targets.map((t) => typeof t === "string" ? { target: t } : t),
|
|
1015
|
+
{ maxScale: step.maxScale }
|
|
1016
|
+
);
|
|
1017
|
+
case "zoomOut":
|
|
1018
|
+
return d.zoomOut({ park: step.park });
|
|
1019
|
+
case "park":
|
|
1020
|
+
return d.park();
|
|
1021
|
+
case "waitFor":
|
|
1022
|
+
return d.waitFor(step, {
|
|
1023
|
+
hidden: step.hidden,
|
|
1024
|
+
timeoutMs: step.timeout * 1e3
|
|
1025
|
+
});
|
|
1026
|
+
case "expectText":
|
|
1027
|
+
return d.expectText(step, step.text, {
|
|
1028
|
+
contains: step.contains,
|
|
1029
|
+
timeoutMs: step.timeout * 1e3
|
|
1030
|
+
});
|
|
1031
|
+
case "hold":
|
|
1032
|
+
return d.hold(step.seconds * 1e3);
|
|
1033
|
+
case "card":
|
|
1034
|
+
throw new Error("a card is run by runSteps, not runStep");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const action = adapter.actions?.[step.do];
|
|
1038
|
+
if (!action) {
|
|
1039
|
+
throw new Error(`unknown step "${step.do}"`);
|
|
1040
|
+
}
|
|
1041
|
+
const params = Object.fromEntries(
|
|
1042
|
+
Object.entries(step).filter(
|
|
1043
|
+
([key]) => key !== "do" && key !== "behindCard"
|
|
1044
|
+
)
|
|
1045
|
+
);
|
|
1046
|
+
const parsed = action.params.parse(params);
|
|
1047
|
+
const target = action.target?.(parsed);
|
|
1048
|
+
await d.during(
|
|
1049
|
+
() => action.run(d, parsed),
|
|
1050
|
+
target === void 0 ? void 0 : { target: toTarget(target) }
|
|
1051
|
+
);
|
|
1052
|
+
};
|
|
1053
|
+
var needsLinger = (previous) => previous !== void 0 && previous.do !== "hold" && previous.do !== "card" && !isBehindCard(previous);
|
|
1054
|
+
var TITLE = -1;
|
|
1055
|
+
var planCards = (steps, hasTitle) => {
|
|
1056
|
+
const plans = /* @__PURE__ */ new Map();
|
|
1057
|
+
const starts = [
|
|
1058
|
+
...hasTitle ? [TITLE] : [],
|
|
1059
|
+
...steps.flatMap((s, i) => s.do === "card" ? [i] : [])
|
|
1060
|
+
];
|
|
1061
|
+
for (const start of starts) {
|
|
1062
|
+
const behind = [];
|
|
1063
|
+
let j = start + 1;
|
|
1064
|
+
while (j < steps.length && isBehindCard(steps[j])) {
|
|
1065
|
+
behind.push(j++);
|
|
1066
|
+
}
|
|
1067
|
+
plans.set(start, {
|
|
1068
|
+
behind,
|
|
1069
|
+
opens: j < steps.length && steps[j].do !== "card"
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
[...plans.entries()].filter(([at, plan]) => at !== TITLE && plan.opens).forEach(([, plan], n) => {
|
|
1073
|
+
plan.chapter = n + 1;
|
|
1074
|
+
});
|
|
1075
|
+
return plans;
|
|
1076
|
+
};
|
|
1077
|
+
var toTarget = (t) => typeof t === "string" ? { target: t } : t;
|
|
1078
|
+
var cardSpecFor = (card, plan, still) => ({
|
|
1079
|
+
eyebrow: card.eyebrow,
|
|
1080
|
+
heading: card.heading,
|
|
1081
|
+
sub: card.sub,
|
|
1082
|
+
logo: card.logo,
|
|
1083
|
+
seconds: card.seconds,
|
|
1084
|
+
style: card.style ?? (plan?.chapter !== void 0 && plan.chapter % 2 === 0 ? "brand" : "pale"),
|
|
1085
|
+
...still ? { still } : {}
|
|
1086
|
+
});
|
|
1087
|
+
var stillCards = (steps, title) => [
|
|
1088
|
+
...title?.show !== void 0 ? [TITLE] : [],
|
|
1089
|
+
...steps.flatMap(
|
|
1090
|
+
(s, i) => s.do === "card" && s.show !== void 0 ? [i] : []
|
|
1091
|
+
)
|
|
1092
|
+
];
|
|
1093
|
+
var WAITS = /* @__PURE__ */ new Set(["waitFor", "expectText"]);
|
|
1094
|
+
var runSteps = async (d, steps, adapter, options = {}) => {
|
|
1095
|
+
const idleLimitMs = adapter.idleTimeoutSeconds ? adapter.idleTimeoutSeconds * 1e3 - IDLE_MARGIN_MS : Number.POSITIVE_INFINITY;
|
|
1096
|
+
let quietMs2 = options.initialQuietMs ?? 0;
|
|
1097
|
+
const tooQuiet = () => new Error(
|
|
1098
|
+
`the app would sit untouched for ${Math.round(quietMs2 / 1e3)} s; its idle timeout is ${adapter.idleTimeoutSeconds} s. Add an interaction or shorten the holds.`
|
|
1099
|
+
);
|
|
1100
|
+
const plans = planCards(steps, Boolean(options.title));
|
|
1101
|
+
const covered = new Set([...plans.values()].flatMap((p) => p.behind));
|
|
1102
|
+
let pending;
|
|
1103
|
+
const hero = options.title?.show && options.stillFile ? { target: toTarget(options.title.show), seen: false } : void 0;
|
|
1104
|
+
const fail = async (index, cause) => {
|
|
1105
|
+
if (cause instanceof StepError) {
|
|
1106
|
+
throw cause;
|
|
1107
|
+
}
|
|
1108
|
+
const visible = await d.visibleTestIds().catch(() => []);
|
|
1109
|
+
throw new StepError(index, steps[index], reasonOf(cause), visible);
|
|
1110
|
+
};
|
|
1111
|
+
const settlePending = async () => {
|
|
1112
|
+
const was = pending;
|
|
1113
|
+
pending = void 0;
|
|
1114
|
+
if (was && !was.seen) {
|
|
1115
|
+
await fail(
|
|
1116
|
+
was.card,
|
|
1117
|
+
new Error(
|
|
1118
|
+
`"${was.target.target}" (show) never appears in the chapter after this card, so there is no still to put on it`
|
|
1119
|
+
)
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
const runCard = async (at) => {
|
|
1124
|
+
const plan = plans.get(at);
|
|
1125
|
+
const card = at === TITLE ? options.title : steps[at];
|
|
1126
|
+
const show = card.show ? toTarget(card.show) : void 0;
|
|
1127
|
+
if (show && options.stillFile && at !== TITLE) {
|
|
1128
|
+
if (plan?.opens) {
|
|
1129
|
+
pending = { card: at, target: show, seen: false };
|
|
1130
|
+
} else if (!await d.capture(show, options.stillFile(at))) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`"${show.target}" (show) isn't on screen when this card comes up`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const behind = plan?.behind ?? [];
|
|
1137
|
+
const last = behind.at(-1) ?? at;
|
|
1138
|
+
const next = steps[last + 1] ?? (last === steps.length - 1 && options.keepLastCardUp ? { do: "card" } : void 0);
|
|
1139
|
+
await d.card(
|
|
1140
|
+
cardSpecFor(
|
|
1141
|
+
card,
|
|
1142
|
+
plan,
|
|
1143
|
+
show && options.stillUrl ? options.stillUrl(at) : void 0
|
|
1144
|
+
),
|
|
1145
|
+
{
|
|
1146
|
+
keepUp: next?.do === "card",
|
|
1147
|
+
opening: at === TITLE,
|
|
1148
|
+
kind: options.kind,
|
|
1149
|
+
behind: behind.length ? async () => {
|
|
1150
|
+
for (const index of behind) {
|
|
1151
|
+
await runOne(index);
|
|
1152
|
+
}
|
|
1153
|
+
} : void 0
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
};
|
|
1157
|
+
const runOne = async (index) => {
|
|
1158
|
+
const step = steps[index];
|
|
1159
|
+
if (step.do === "card") {
|
|
1160
|
+
await settlePending();
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
const linger = step.do === "card" && needsLinger(steps[index - 1]);
|
|
1164
|
+
if (step.do === "hold" || step.do === "card" || CAMERA_MOVES.has(step.do) && !covered.has(index)) {
|
|
1165
|
+
quietMs2 += step.do === "hold" ? Number(step.seconds) * 1e3 : step.do === "card" ? cardSeconds(step) * 1e3 + CARD_DISSOLVE_MS + (linger ? LINGER_MS : 0) : CAMERA_MOVE_MS;
|
|
1166
|
+
if (quietMs2 > idleLimitMs) {
|
|
1167
|
+
throw tooQuiet();
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (linger) {
|
|
1171
|
+
await d.hold(LINGER_MS);
|
|
1172
|
+
}
|
|
1173
|
+
const started = d.clockMs();
|
|
1174
|
+
if (step.do === "card") {
|
|
1175
|
+
await runCard(index);
|
|
1176
|
+
} else {
|
|
1177
|
+
await runStep(d, step, adapter);
|
|
1178
|
+
}
|
|
1179
|
+
if (WAITS.has(step.do)) {
|
|
1180
|
+
quietMs2 += d.clockMs() - started;
|
|
1181
|
+
if (quietMs2 > idleLimitMs) {
|
|
1182
|
+
throw tooQuiet();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (INTERACTIONS.has(step.do) || adapter.actions?.[step.do]) {
|
|
1186
|
+
quietMs2 = 0;
|
|
1187
|
+
}
|
|
1188
|
+
} catch (cause) {
|
|
1189
|
+
await fail(index, cause);
|
|
1190
|
+
}
|
|
1191
|
+
if (pending && step.do !== "card" && options.stillFile) {
|
|
1192
|
+
const seen = await d.capture(
|
|
1193
|
+
pending.target,
|
|
1194
|
+
options.stillFile(pending.card)
|
|
1195
|
+
);
|
|
1196
|
+
pending.seen ||= seen;
|
|
1197
|
+
}
|
|
1198
|
+
if (hero && step.do !== "card" && options.stillFile) {
|
|
1199
|
+
hero.seen ||= await d.capture(hero.target, options.stillFile(TITLE));
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
if (options.title) {
|
|
1203
|
+
quietMs2 += titleSeconds(options.title, options.kind) * 1e3 + CARD_OUT_MS;
|
|
1204
|
+
await runCard(TITLE);
|
|
1205
|
+
}
|
|
1206
|
+
for (const index of steps.keys()) {
|
|
1207
|
+
if (!covered.has(index)) {
|
|
1208
|
+
await runOne(index);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
await settlePending();
|
|
1212
|
+
if (hero && !hero.seen) {
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
`title.show: "${hero.target.target}" never appears in the story, so the open card has no snapshot`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
// src/estimate.ts
|
|
1220
|
+
var CURSOR_MS = (CURSOR_MIN_MS + CURSOR_MAX_MS) / 2;
|
|
1221
|
+
var CLICK_MS = CURSOR_MS + PRE_CLICK_MS + PRESS_MS + POST_CLICK_MS;
|
|
1222
|
+
var CAMERA_MS = ZOOM_MS + SETTLE_MS;
|
|
1223
|
+
var stepMs = (step) => {
|
|
1224
|
+
const s = step;
|
|
1225
|
+
switch (step.do) {
|
|
1226
|
+
case "click":
|
|
1227
|
+
return CLICK_MS + (s.zoom !== void 0 ? CAMERA_MS : 0);
|
|
1228
|
+
case "type":
|
|
1229
|
+
return CLICK_MS + String(s.text).length * Number(s.perCharMs ?? TYPE_CHAR_MS) + POST_KEY_MS;
|
|
1230
|
+
case "press":
|
|
1231
|
+
return POST_KEY_MS;
|
|
1232
|
+
case "zoom":
|
|
1233
|
+
case "frame":
|
|
1234
|
+
case "zoomOut":
|
|
1235
|
+
return CAMERA_MS;
|
|
1236
|
+
case "park":
|
|
1237
|
+
return CURSOR_MS;
|
|
1238
|
+
case "hold":
|
|
1239
|
+
return Number(s.seconds) * 1e3;
|
|
1240
|
+
case "waitFor":
|
|
1241
|
+
case "expectText":
|
|
1242
|
+
return 0;
|
|
1243
|
+
default:
|
|
1244
|
+
return POST_ACTION_MS + CURSOR_MS;
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
var estimateSeconds = (story) => Math.round(estimateMs(story) / 1e3);
|
|
1248
|
+
var estimateMs = (story) => {
|
|
1249
|
+
const steps = story.steps;
|
|
1250
|
+
const plans = planCards(steps, Boolean(story.title));
|
|
1251
|
+
const covered = new Set([...plans.values()].flatMap((p) => p.behind));
|
|
1252
|
+
const after = (at) => steps[(plans.get(at)?.behind.at(-1) ?? at) + 1];
|
|
1253
|
+
let ms = 0;
|
|
1254
|
+
let wideAt = 0;
|
|
1255
|
+
if (story.title) {
|
|
1256
|
+
ms += titleSeconds(story.title, story.kind) * 1e3;
|
|
1257
|
+
ms += after(TITLE)?.do === "card" ? 0 : CARD_OUT_MS;
|
|
1258
|
+
wideAt = ms;
|
|
1259
|
+
}
|
|
1260
|
+
steps.forEach((step, i) => {
|
|
1261
|
+
if (covered.has(i)) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (step.do !== "card") {
|
|
1265
|
+
const pushes = step.do === "zoom" || step.do === "frame" || step.do === "click" && step.zoom !== void 0;
|
|
1266
|
+
if (pushes && wideAt !== null) {
|
|
1267
|
+
ms += Math.max(0, ESTABLISH_MS - (ms - wideAt));
|
|
1268
|
+
wideAt = null;
|
|
1269
|
+
}
|
|
1270
|
+
ms += stepMs(step);
|
|
1271
|
+
if (step.do === "zoomOut") {
|
|
1272
|
+
wideAt = ms;
|
|
1273
|
+
}
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const previous = steps[i - 1];
|
|
1277
|
+
const linger = previous !== void 0 && previous.do !== "hold" && previous.do !== "card" && !isBehindCard(previous);
|
|
1278
|
+
ms += (linger ? LINGER_MS : 0) + CARD_IN_MS;
|
|
1279
|
+
ms += cardSeconds(step) * 1e3;
|
|
1280
|
+
ms += after(i)?.do === "card" || !after(i) && story.closing ? 0 : CARD_OUT_MS;
|
|
1281
|
+
wideAt = ms;
|
|
1282
|
+
});
|
|
1283
|
+
if (story.closing) {
|
|
1284
|
+
const last = steps.at(-1)?.do;
|
|
1285
|
+
ms += last !== "card" && last !== "hold" ? LINGER_MS : 0;
|
|
1286
|
+
ms += CARD_IN_MS + titleSeconds(story.closing, story.kind) * 1e3;
|
|
1287
|
+
}
|
|
1288
|
+
return ms;
|
|
1289
|
+
};
|
|
1290
|
+
var COVERED_STEP_MS = {
|
|
1291
|
+
click: 300,
|
|
1292
|
+
type: 350,
|
|
1293
|
+
press: 100,
|
|
1294
|
+
waitFor: 200
|
|
1295
|
+
};
|
|
1296
|
+
var COVERED_ACTION_MS = 500;
|
|
1297
|
+
var slowCards = (story) => {
|
|
1298
|
+
const steps = story.steps;
|
|
1299
|
+
const plans = planCards(steps, Boolean(story.title));
|
|
1300
|
+
const slow = [];
|
|
1301
|
+
for (const [at, plan] of plans) {
|
|
1302
|
+
const card = at === TITLE ? story.title : steps[at];
|
|
1303
|
+
if (!card || plan.behind.length === 0) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
const setupMs = plan.behind.reduce(
|
|
1307
|
+
(sum, i) => sum + (COVERED_STEP_MS[steps[i].do] ?? COVERED_ACTION_MS),
|
|
1308
|
+
0
|
|
1309
|
+
);
|
|
1310
|
+
const readSeconds = at === TITLE ? titleSeconds(card, story.kind) : cardSeconds(card);
|
|
1311
|
+
if (setupMs / 1e3 > readSeconds + 0.3) {
|
|
1312
|
+
slow.push({
|
|
1313
|
+
where: at === TITLE ? "title" : `steps.${at}`,
|
|
1314
|
+
readSeconds,
|
|
1315
|
+
setupSeconds: Math.round(setupMs / 100) / 10
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return slow;
|
|
1320
|
+
};
|
|
1321
|
+
|
|
701
1322
|
// src/frames.ts
|
|
702
1323
|
import { spawn } from "child_process";
|
|
703
|
-
import
|
|
704
|
-
import
|
|
1324
|
+
import fs3 from "fs";
|
|
1325
|
+
import path4 from "path";
|
|
705
1326
|
import ffmpegPath from "ffmpeg-static";
|
|
706
1327
|
var FPS = 60;
|
|
707
1328
|
var DRAFT_FPS = 30;
|
|
@@ -712,8 +1333,8 @@ var FrameRecorder = class {
|
|
|
712
1333
|
this.workDir = workDir;
|
|
713
1334
|
this.stem = stem;
|
|
714
1335
|
this.fps = fps;
|
|
715
|
-
this.rawDir =
|
|
716
|
-
this.videoPath =
|
|
1336
|
+
this.rawDir = path4.join(workDir, "raw");
|
|
1337
|
+
this.videoPath = path4.join(this.rawDir, "stage.mkv");
|
|
717
1338
|
}
|
|
718
1339
|
page;
|
|
719
1340
|
workDir;
|
|
@@ -748,13 +1369,13 @@ var FrameRecorder = class {
|
|
|
748
1369
|
}
|
|
749
1370
|
/** Starts writing frames. Time advanced before this is not filmed. */
|
|
750
1371
|
start() {
|
|
751
|
-
if (!ffmpegPath || !
|
|
1372
|
+
if (!ffmpegPath || !fs3.existsSync(ffmpegPath)) {
|
|
752
1373
|
throw new Error(
|
|
753
1374
|
"ffmpeg-static has no binary; with pnpm >= 10, allow its install script (onlyBuiltDependencies) and reinstall."
|
|
754
1375
|
);
|
|
755
1376
|
}
|
|
756
|
-
|
|
757
|
-
|
|
1377
|
+
fs3.rmSync(this.rawDir, { recursive: true, force: true });
|
|
1378
|
+
fs3.mkdirSync(this.rawDir, { recursive: true });
|
|
758
1379
|
const ffmpeg = spawn(
|
|
759
1380
|
ffmpegPath,
|
|
760
1381
|
[
|
|
@@ -963,138 +1584,14 @@ var FrameRecorder = class {
|
|
|
963
1584
|
endAt: null,
|
|
964
1585
|
storyboard: this.stem
|
|
965
1586
|
};
|
|
966
|
-
|
|
967
|
-
|
|
1587
|
+
fs3.writeFileSync(
|
|
1588
|
+
path4.join(this.workDir, "manifest.json"),
|
|
968
1589
|
JSON.stringify(manifest, null, 2)
|
|
969
1590
|
);
|
|
970
1591
|
return manifest;
|
|
971
1592
|
}
|
|
972
1593
|
};
|
|
973
1594
|
|
|
974
|
-
// src/run.ts
|
|
975
|
-
var CAMERA_MOVE_MS = ZOOM_MS + SETTLE_MS;
|
|
976
|
-
var CARD_DISSOLVE_MS = CARD_IN_MS + CARD_OUT_MS;
|
|
977
|
-
var IDLE_MARGIN_MS = 1e4;
|
|
978
|
-
var INTERACTIONS = /* @__PURE__ */ new Set(["click", "type", "press"]);
|
|
979
|
-
var CAMERA_MOVES = /* @__PURE__ */ new Set(["zoom", "frame", "zoomOut"]);
|
|
980
|
-
var describeTarget = (t) => typeof t === "string" ? t : typeof t === "object" && t !== null && "target" in t ? `${String(t.target)}${"nth" in t && typeof t.nth === "number" ? `[${t.nth}]` : ""}` : "";
|
|
981
|
-
var describeStep = (step) => {
|
|
982
|
-
if ("targets" in step && Array.isArray(step.targets)) {
|
|
983
|
-
return `${step.do} ${step.targets.map(describeTarget).join(" + ")}`;
|
|
984
|
-
}
|
|
985
|
-
const target = "target" in step ? describeTarget(step) : "";
|
|
986
|
-
return `${step.do}${target ? ` ${target}` : ""}`;
|
|
987
|
-
};
|
|
988
|
-
var StepError = class extends Error {
|
|
989
|
-
constructor(index, step, reason, visible) {
|
|
990
|
-
super(`Step ${index + 1} (${describeStep(step)}) failed: ${reason}`);
|
|
991
|
-
this.index = index;
|
|
992
|
-
this.step = step;
|
|
993
|
-
this.reason = reason;
|
|
994
|
-
this.visible = visible;
|
|
995
|
-
}
|
|
996
|
-
index;
|
|
997
|
-
step;
|
|
998
|
-
reason;
|
|
999
|
-
visible;
|
|
1000
|
-
};
|
|
1001
|
-
var reasonOf = (cause) => {
|
|
1002
|
-
if (cause && typeof cause === "object" && "issues" in cause && Array.isArray(cause.issues)) {
|
|
1003
|
-
return cause.issues.map((i) => `${i.path.join(".") || "(params)"}: ${i.message}`).join("; ");
|
|
1004
|
-
}
|
|
1005
|
-
const text = cause instanceof Error ? cause.message : String(cause);
|
|
1006
|
-
return text.split("\n")[0].trim();
|
|
1007
|
-
};
|
|
1008
|
-
var runStep = async (d, step, adapter, next) => {
|
|
1009
|
-
if (isCoreStep(step)) {
|
|
1010
|
-
switch (step.do) {
|
|
1011
|
-
case "click":
|
|
1012
|
-
return d.click(step, { zoom: step.zoom });
|
|
1013
|
-
case "type":
|
|
1014
|
-
return d.type(step, step.text, step.perCharMs);
|
|
1015
|
-
case "press":
|
|
1016
|
-
return d.press(step.key);
|
|
1017
|
-
case "zoom":
|
|
1018
|
-
return d.zoomTo(step, step.scale);
|
|
1019
|
-
case "frame":
|
|
1020
|
-
return d.frame(
|
|
1021
|
-
step.targets.map((t) => typeof t === "string" ? { target: t } : t),
|
|
1022
|
-
{ maxScale: step.maxScale }
|
|
1023
|
-
);
|
|
1024
|
-
case "zoomOut":
|
|
1025
|
-
return d.zoomOut({ park: step.park });
|
|
1026
|
-
case "park":
|
|
1027
|
-
return d.park();
|
|
1028
|
-
case "waitFor":
|
|
1029
|
-
return d.waitFor(step, {
|
|
1030
|
-
hidden: step.hidden,
|
|
1031
|
-
timeoutMs: step.timeout * 1e3
|
|
1032
|
-
});
|
|
1033
|
-
case "expectText":
|
|
1034
|
-
return d.expectText(step, step.text, {
|
|
1035
|
-
contains: step.contains,
|
|
1036
|
-
timeoutMs: step.timeout * 1e3
|
|
1037
|
-
});
|
|
1038
|
-
case "hold":
|
|
1039
|
-
return d.hold(step.seconds * 1e3);
|
|
1040
|
-
case "card":
|
|
1041
|
-
return d.card(step, { keepUp: next?.do === "card" });
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
const action = adapter.actions?.[step.do];
|
|
1045
|
-
if (!action) {
|
|
1046
|
-
throw new Error(`unknown step "${step.do}"`);
|
|
1047
|
-
}
|
|
1048
|
-
const params = Object.fromEntries(
|
|
1049
|
-
Object.entries(step).filter(([key]) => key !== "do")
|
|
1050
|
-
);
|
|
1051
|
-
const parsed = action.params.parse(params);
|
|
1052
|
-
await d.during(() => action.run(d, parsed));
|
|
1053
|
-
};
|
|
1054
|
-
var needsLinger = (previous) => previous !== void 0 && previous.do !== "hold" && previous.do !== "card";
|
|
1055
|
-
var WAITS = /* @__PURE__ */ new Set(["waitFor", "expectText"]);
|
|
1056
|
-
var runSteps = async (d, steps, adapter, options = {}) => {
|
|
1057
|
-
const idleLimitMs = adapter.idleTimeoutSeconds ? adapter.idleTimeoutSeconds * 1e3 - IDLE_MARGIN_MS : Number.POSITIVE_INFINITY;
|
|
1058
|
-
let quietMs = options.initialQuietMs ?? 0;
|
|
1059
|
-
const tooQuiet = () => new Error(
|
|
1060
|
-
`the app would sit untouched for ${Math.round(quietMs / 1e3)} s; its idle timeout is ${adapter.idleTimeoutSeconds} s. Add an interaction or shorten the holds.`
|
|
1061
|
-
);
|
|
1062
|
-
for (const [index, step] of steps.entries()) {
|
|
1063
|
-
try {
|
|
1064
|
-
const linger = step.do === "card" && needsLinger(steps[index - 1]);
|
|
1065
|
-
if (step.do === "hold" || step.do === "card" || CAMERA_MOVES.has(step.do)) {
|
|
1066
|
-
quietMs += step.do === "hold" ? Number(step.seconds) * 1e3 : step.do === "card" ? cardSeconds(step) * 1e3 + CARD_DISSOLVE_MS + (linger ? LINGER_MS : 0) : CAMERA_MOVE_MS;
|
|
1067
|
-
if (quietMs > idleLimitMs) {
|
|
1068
|
-
throw tooQuiet();
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
if (linger) {
|
|
1072
|
-
await d.hold(LINGER_MS);
|
|
1073
|
-
}
|
|
1074
|
-
const isLast = index === steps.length - 1;
|
|
1075
|
-
const started = d.clockMs();
|
|
1076
|
-
await runStep(
|
|
1077
|
-
d,
|
|
1078
|
-
step,
|
|
1079
|
-
adapter,
|
|
1080
|
-
steps[index + 1] ?? (isLast && options.keepLastCardUp ? { do: "card" } : void 0)
|
|
1081
|
-
);
|
|
1082
|
-
if (WAITS.has(step.do)) {
|
|
1083
|
-
quietMs += d.clockMs() - started;
|
|
1084
|
-
if (quietMs > idleLimitMs) {
|
|
1085
|
-
throw tooQuiet();
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
if (INTERACTIONS.has(step.do) || adapter.actions?.[step.do]) {
|
|
1089
|
-
quietMs = 0;
|
|
1090
|
-
}
|
|
1091
|
-
} catch (cause) {
|
|
1092
|
-
const visible = await d.visibleTestIds().catch(() => []);
|
|
1093
|
-
throw new StepError(index, step, reasonOf(cause), visible);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
1595
|
// src/runner/env.ts
|
|
1099
1596
|
var readRunEnv = (env = process.env) => {
|
|
1100
1597
|
const storyboardPath = env.DEMO_STORYBOARD;
|
|
@@ -1111,7 +1608,8 @@ var readRunEnv = (env = process.env) => {
|
|
|
1111
1608
|
workDir: env.DEMO_WORK || `${outDir}/.work`,
|
|
1112
1609
|
brand: env.DEMO_BRAND || void 0,
|
|
1113
1610
|
brandsDir: env.DEMO_BRANDS || void 0,
|
|
1114
|
-
draft: env.DEMO_DRAFT === "1"
|
|
1611
|
+
draft: env.DEMO_DRAFT === "1",
|
|
1612
|
+
stillsDir: env.DEMO_STILLS || void 0
|
|
1115
1613
|
};
|
|
1116
1614
|
};
|
|
1117
1615
|
|
|
@@ -1156,20 +1654,20 @@ var bootFailure = (adapter, frameUrl, headers, cause) => {
|
|
|
1156
1654
|
return `The app didn't get ready: ${reason}. Check that the adapter's setup mocks what the app calls at start, and that "${adapter.ready}" is shown once it booted.`;
|
|
1157
1655
|
};
|
|
1158
1656
|
var writeJson = (file, value) => {
|
|
1159
|
-
|
|
1160
|
-
|
|
1657
|
+
fs4.mkdirSync(path5.dirname(file), { recursive: true });
|
|
1658
|
+
fs4.writeFileSync(file, JSON.stringify(value, null, 2));
|
|
1161
1659
|
};
|
|
1162
1660
|
var registerDemoTests = (test, adapter, demoDir) => {
|
|
1163
1661
|
if (process.env.DEMO_MODE === "needs") {
|
|
1164
1662
|
test("demo", async () => {
|
|
1165
|
-
writeJson(
|
|
1663
|
+
writeJson(path5.join(process.env.DEMO_WORK ?? demoDir, "needs.json"), {
|
|
1166
1664
|
app: adapter.app,
|
|
1167
1665
|
viewport: adapter.viewport ?? DEFAULT_VIEWPORT,
|
|
1168
1666
|
video: VIDEO_NEEDS,
|
|
1169
1667
|
appNeeds: describeNeeds(adapter.brandNeeds),
|
|
1170
1668
|
example: JSON.parse(
|
|
1171
|
-
|
|
1172
|
-
|
|
1669
|
+
fs4.readFileSync(
|
|
1670
|
+
path5.join(sharedBrandsDir(), "navipartner.json"),
|
|
1173
1671
|
"utf8"
|
|
1174
1672
|
)
|
|
1175
1673
|
)
|
|
@@ -1180,10 +1678,16 @@ var registerDemoTests = (test, adapter, demoDir) => {
|
|
|
1180
1678
|
test("demo", async ({ page }) => {
|
|
1181
1679
|
const env = readRunEnv();
|
|
1182
1680
|
if (env.mode === "check") {
|
|
1183
|
-
const reportPath =
|
|
1681
|
+
const reportPath = path5.join(env.workDir, "check.json");
|
|
1184
1682
|
try {
|
|
1185
|
-
const
|
|
1186
|
-
writeJson(reportPath, {
|
|
1683
|
+
const story = await perform(page, env);
|
|
1684
|
+
writeJson(reportPath, {
|
|
1685
|
+
ok: true,
|
|
1686
|
+
steps: story.steps.length,
|
|
1687
|
+
kind: story.kind ?? "showcase",
|
|
1688
|
+
seconds: estimateSeconds(story),
|
|
1689
|
+
slowCards: slowCards(story)
|
|
1690
|
+
});
|
|
1187
1691
|
} catch (error) {
|
|
1188
1692
|
writeJson(reportPath, failureReport(error));
|
|
1189
1693
|
throw error;
|
|
@@ -1198,7 +1702,7 @@ var registerDemoTests = (test, adapter, demoDir) => {
|
|
|
1198
1702
|
if (issues.length > 0) {
|
|
1199
1703
|
throw new StoryboardError(
|
|
1200
1704
|
[
|
|
1201
|
-
`${
|
|
1705
|
+
`${path5.basename(env.storyboardPath)} won't film well:`,
|
|
1202
1706
|
...issues.map((i) => ` ${i}`)
|
|
1203
1707
|
].join("\n")
|
|
1204
1708
|
);
|
|
@@ -1235,6 +1739,31 @@ var registerDemoTests = (test, adapter, demoDir) => {
|
|
|
1235
1739
|
})
|
|
1236
1740
|
);
|
|
1237
1741
|
const fast = env.mode === "check";
|
|
1742
|
+
const stillsDir = env.stillsDir ?? path5.join(env.workDir, "stills");
|
|
1743
|
+
const stillName = (index) => index === TITLE ? "card-title.png" : `card-${index}.png`;
|
|
1744
|
+
const stillFile = (index) => path5.join(stillsDir, stillName(index));
|
|
1745
|
+
const stillUrl = (index) => `${STILLS_PATH}/${stillName(index)}`;
|
|
1746
|
+
const stills = stillCards(storyboard.steps, storyboard.title);
|
|
1747
|
+
if (!fast && stills.length > 0) {
|
|
1748
|
+
const missing2 = stills.filter((i) => !fs4.existsSync(stillFile(i)));
|
|
1749
|
+
if (missing2.length > 0) {
|
|
1750
|
+
throw new Error(
|
|
1751
|
+
`No still for the card at ${missing2.map((i) => i === TITLE ? "title" : `steps.${i}`).join(", ")}: render the story with the demo-video CLI, which checks it first and captures them`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
await page.route(
|
|
1755
|
+
`**${STILLS_PATH}/*`,
|
|
1756
|
+
(route) => route.fulfill({
|
|
1757
|
+
contentType: "image/png",
|
|
1758
|
+
body: fs4.readFileSync(
|
|
1759
|
+
path5.join(
|
|
1760
|
+
stillsDir,
|
|
1761
|
+
path5.basename(new URL(route.request().url()).pathname)
|
|
1762
|
+
)
|
|
1763
|
+
)
|
|
1764
|
+
})
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1238
1767
|
if (!fast) {
|
|
1239
1768
|
await FrameRecorder.prepare(page);
|
|
1240
1769
|
}
|
|
@@ -1262,39 +1791,44 @@ var registerDemoTests = (test, adapter, demoDir) => {
|
|
|
1262
1791
|
cause: error
|
|
1263
1792
|
});
|
|
1264
1793
|
}
|
|
1794
|
+
const title = storyboard.title;
|
|
1265
1795
|
if (fast) {
|
|
1266
|
-
await runSteps(boot, storyboard.steps, adapter
|
|
1796
|
+
await runSteps(boot, storyboard.steps, adapter, {
|
|
1797
|
+
title,
|
|
1798
|
+
kind: storyboard.kind,
|
|
1799
|
+
...stills.length > 0 ? { stillFile } : {}
|
|
1800
|
+
});
|
|
1267
1801
|
boot.end();
|
|
1268
|
-
return storyboard
|
|
1802
|
+
return storyboard;
|
|
1269
1803
|
}
|
|
1270
1804
|
await page.evaluate(() => document.fonts.ready.then(() => void 0));
|
|
1271
1805
|
await frame.evaluate(() => document.fonts.ready.then(() => void 0));
|
|
1806
|
+
await page.evaluate(
|
|
1807
|
+
(urls) => window.__demo.preload(urls),
|
|
1808
|
+
stills.map(stillUrl)
|
|
1809
|
+
);
|
|
1272
1810
|
await page.waitForTimeout(300);
|
|
1273
1811
|
const recorder = new FrameRecorder(
|
|
1274
1812
|
page,
|
|
1275
1813
|
env.workDir,
|
|
1276
|
-
`${
|
|
1814
|
+
`${path5.basename(env.storyboardPath, ".json")}-${brand.name}${env.draft ? "-draft" : ""}`,
|
|
1277
1815
|
env.draft ? DRAFT_FPS : FPS
|
|
1278
1816
|
);
|
|
1279
1817
|
await recorder.freeze();
|
|
1280
1818
|
const d = new Director(page, frame, { fast: false, viewport, recorder });
|
|
1281
|
-
const title = storyboard.title;
|
|
1282
1819
|
if (title) {
|
|
1283
|
-
await d.showCard(
|
|
1820
|
+
await d.showCard(
|
|
1821
|
+
cardSpecFor(title, void 0, title.show ? stillUrl(TITLE) : void 0),
|
|
1822
|
+
true
|
|
1823
|
+
);
|
|
1284
1824
|
}
|
|
1285
1825
|
recorder.start();
|
|
1286
|
-
if (title) {
|
|
1287
|
-
await recorder.advance(cardSeconds(title) * 1e3);
|
|
1288
|
-
if (storyboard.steps[0]?.do !== "card") {
|
|
1289
|
-
await d.hideCard();
|
|
1290
|
-
await recorder.advance(CARD_OUT_MS);
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
1826
|
const closing = storyboard.closing;
|
|
1294
1827
|
await runSteps(d, storyboard.steps, adapter, {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
keepLastCardUp: Boolean(closing)
|
|
1828
|
+
title,
|
|
1829
|
+
kind: storyboard.kind,
|
|
1830
|
+
keepLastCardUp: Boolean(closing),
|
|
1831
|
+
stillUrl
|
|
1298
1832
|
});
|
|
1299
1833
|
d.end();
|
|
1300
1834
|
if (closing) {
|
|
@@ -1303,11 +1837,13 @@ var registerDemoTests = (test, adapter, demoDir) => {
|
|
|
1303
1837
|
await recorder.advance(LINGER_MS);
|
|
1304
1838
|
}
|
|
1305
1839
|
await d.showCard(closing);
|
|
1306
|
-
await recorder.advance(
|
|
1840
|
+
await recorder.advance(
|
|
1841
|
+
CARD_IN_MS + titleSeconds(closing, storyboard.kind) * 1e3
|
|
1842
|
+
);
|
|
1307
1843
|
}
|
|
1308
1844
|
await recorder.advance(300);
|
|
1309
1845
|
await recorder.stop();
|
|
1310
|
-
return storyboard
|
|
1846
|
+
return storyboard;
|
|
1311
1847
|
};
|
|
1312
1848
|
};
|
|
1313
1849
|
export {
|