@plaintake/scenario 1.6.0 → 1.7.0
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/dist/index.js +116 -5
- package/dist/schema/event.d.ts +3 -0
- package/dist/schema/render-plan.d.ts +99 -0
- package/dist/schema/results.d.ts +6 -0
- package/dist/types.d.ts +166 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,16 @@ var DemoEventTypeSchema = z.enum([
|
|
|
42
42
|
* Appended once, when the wait ends, so `tEndNs` is a real reading rather than a
|
|
43
43
|
* placeholder: the `assertion.pass` shape, not the `step.start`/`step.finish` pair.
|
|
44
44
|
*/
|
|
45
|
-
"human.wait"
|
|
45
|
+
"human.wait",
|
|
46
|
+
/*
|
|
47
|
+
* An actor hand-off: `demo.turn(actor, ...)` parking the outgoing actor's `BrowserContext`
|
|
48
|
+
* and activating the incoming one. A member of its own rather than folded into
|
|
49
|
+
* `step.start`/`step.finish`, because a hand-off is not a step — it has no `target` and
|
|
50
|
+
* narrates no action of its own. Verb steps taken during a turn keep using
|
|
51
|
+
* `step.start`/`step.finish` exactly as before; the `actor` field below records which
|
|
52
|
+
* actor performed the step.
|
|
53
|
+
*/
|
|
54
|
+
"turn"
|
|
46
55
|
]);
|
|
47
56
|
var TargetSchema = z.object({
|
|
48
57
|
role: z.string().optional(),
|
|
@@ -60,7 +69,15 @@ var DemoEventSchema = z.object({
|
|
|
60
69
|
title: z.string().optional(),
|
|
61
70
|
subtitle: z.string().optional(),
|
|
62
71
|
target: TargetSchema.optional(),
|
|
63
|
-
payload: z.record(z.string(), z.unknown()).optional()
|
|
72
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
73
|
+
/**
|
|
74
|
+
* Which actor performed this event, for a `turn` hand-off or a verb step taken during one.
|
|
75
|
+
* Absent means the scenario's default actor — a single-actor event log never sets this, so
|
|
76
|
+
* it stays byte-identical to every log recorded before actors existed. No `.default()`
|
|
77
|
+
* here: the default actor's id is an application-layer notion the recorder resolves, not a
|
|
78
|
+
* value this schema should invent and materialise on parse.
|
|
79
|
+
*/
|
|
80
|
+
actor: z.string().min(1).optional()
|
|
64
81
|
});
|
|
65
82
|
|
|
66
83
|
// ../schema/src/json-schema.ts
|
|
@@ -349,6 +366,25 @@ var CameraSchema = z5.object({
|
|
|
349
366
|
path: ["shots"]
|
|
350
367
|
}
|
|
351
368
|
);
|
|
369
|
+
var ActorSchema = z5.object({
|
|
370
|
+
id: z5.string().min(1),
|
|
371
|
+
label: z5.string().min(1)
|
|
372
|
+
});
|
|
373
|
+
var SegmentSchema = z5.object({
|
|
374
|
+
id: z5.string().min(1),
|
|
375
|
+
actorId: z5.string().min(1),
|
|
376
|
+
startMs: z5.number().int().nonnegative(),
|
|
377
|
+
endMs: z5.number().int().positive()
|
|
378
|
+
});
|
|
379
|
+
var TransitionCardSchema = z5.object({
|
|
380
|
+
fromActorId: z5.string().min(1),
|
|
381
|
+
toActorId: z5.string().min(1),
|
|
382
|
+
durationMs: z5.number().int().positive().max(15e3),
|
|
383
|
+
lines: z5.array(z5.string().min(1)).min(1).max(2),
|
|
384
|
+
backgroundColor: HEX_COLOUR,
|
|
385
|
+
textColor: HEX_COLOUR,
|
|
386
|
+
assPath: z5.string().regex(/^captions\/turn-[0-9]+\.ass$/)
|
|
387
|
+
});
|
|
352
388
|
var HighlightRectSchema = z5.object({
|
|
353
389
|
id: z5.string().min(1),
|
|
354
390
|
x: z5.number().int().min(0),
|
|
@@ -625,8 +661,54 @@ var RenderPlanSchema = z5.object({
|
|
|
625
661
|
cursor: CursorSchema.optional(),
|
|
626
662
|
camera: CameraSchema.optional(),
|
|
627
663
|
highlight: HighlightSchema.optional(),
|
|
628
|
-
speech: SpeechSchema.optional()
|
|
629
|
-
|
|
664
|
+
speech: SpeechSchema.optional(),
|
|
665
|
+
/**
|
|
666
|
+
* The multi-actor demo's cast, its capture timeline cut into per-actor windows, the
|
|
667
|
+
* cards drawn at each hand-off, and the badge track naming the active actor throughout —
|
|
668
|
+
* four fields that carry one capability and so, like every other optional block above,
|
|
669
|
+
* are present or absent together (enforced by the first refinement below). Absent means
|
|
670
|
+
* a single-actor demo, which is every plan before this and the default after it.
|
|
671
|
+
*/
|
|
672
|
+
actors: z5.array(ActorSchema).min(2).optional(),
|
|
673
|
+
segments: z5.array(SegmentSchema).min(2).optional(),
|
|
674
|
+
transitionCards: z5.array(TransitionCardSchema).min(1).optional(),
|
|
675
|
+
badgesAssPath: z5.literal("captions/badges.ass").optional()
|
|
676
|
+
}).refine(
|
|
677
|
+
(plan) => {
|
|
678
|
+
const capabilityFields = [plan.actors, plan.segments, plan.transitionCards, plan.badgesAssPath];
|
|
679
|
+
return capabilityFields.every((field) => field !== void 0) || capabilityFields.every((field) => field === void 0);
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
// The same "carries its capability" discipline every optional block above already
|
|
683
|
+
// follows: a plan naming actors with nowhere to draw their badges (or vice versa)
|
|
684
|
+
// cannot render, so it is refused at the parse rather than discovered mid-encode.
|
|
685
|
+
message: "actors, segments, transitionCards and badgesAssPath must be present together or absent together",
|
|
686
|
+
path: ["actors"]
|
|
687
|
+
}
|
|
688
|
+
).refine(
|
|
689
|
+
(plan) => {
|
|
690
|
+
if (plan.actors === void 0 || plan.segments === void 0 || plan.transitionCards === void 0 || plan.badgesAssPath === void 0) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
const { actors, segments, transitionCards } = plan;
|
|
694
|
+
const namesDeclaredActor = (actorId) => actors.some((actor) => actor.id === actorId);
|
|
695
|
+
return transitionCards.length === segments.length - 1 && segments[0]?.startMs === 0 && segments.every(
|
|
696
|
+
(segment, index) => segment.endMs > segment.startMs && (index === 0 || segments[index - 1]?.endMs === segment.startMs) && namesDeclaredActor(segment.actorId)
|
|
697
|
+
) && transitionCards.every(
|
|
698
|
+
(card) => namesDeclaredActor(card.fromActorId) && namesDeclaredActor(card.toActorId)
|
|
699
|
+
);
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
// Mirrors `ChaptersSchema`'s tiling refine: segments must start at 0 and tile the
|
|
703
|
+
// source contiguously, the same reason a gap or overlap in chapter marks is refused
|
|
704
|
+
// rather than tolerated. `transitionCards.length === segments.length - 1` is the
|
|
705
|
+
// count a hand-off between each pair of adjacent segments implies, and every
|
|
706
|
+
// `segment.actorId`/`transitionCard.fromActorId`/`transitionCard.toActorId` must name
|
|
707
|
+
// a declared actor or there is nobody for the recorder to have handed the context to.
|
|
708
|
+
message: "segments must tile raw/session.webm contiguously from 0, transitionCards.length must equal segments.length - 1, and every segment.actorId, transitionCard.fromActorId and transitionCard.toActorId must name a declared actor",
|
|
709
|
+
path: ["segments"]
|
|
710
|
+
}
|
|
711
|
+
);
|
|
630
712
|
|
|
631
713
|
// ../schema/src/results.ts
|
|
632
714
|
import { z as z6 } from "zod";
|
|
@@ -760,7 +842,7 @@ var VerificationReportSchema = z6.object({
|
|
|
760
842
|
bundleDir: DISPLAY_PATH,
|
|
761
843
|
artifactCount: z6.number().int().nonnegative()
|
|
762
844
|
});
|
|
763
|
-
var DifferenceCategorySchema = z6.enum(["step", "assertion", "target", "timing", "caption"]);
|
|
845
|
+
var DifferenceCategorySchema = z6.enum(["step", "assertion", "target", "timing", "caption", "actor"]);
|
|
764
846
|
var DiffCommandResultSchema = z6.object({
|
|
765
847
|
...envelope("diff"),
|
|
766
848
|
bundleA: DISPLAY_PATH,
|
|
@@ -846,6 +928,35 @@ var InspectResultSchema = z6.object({
|
|
|
846
928
|
suppliedCount: z6.number().int().nonnegative(),
|
|
847
929
|
voice: z6.string().optional()
|
|
848
930
|
}).optional(),
|
|
931
|
+
/**
|
|
932
|
+
* The multi-actor demo's cast, or absent for a single-actor bundle — every bundle
|
|
933
|
+
* recorded before actors existed, and still the default for most scenarios after, so
|
|
934
|
+
* this stays absent rather than a zero-cast object, the same distinction `narration`
|
|
935
|
+
* above draws between "silent" and "nothing to report".
|
|
936
|
+
*
|
|
937
|
+
* Sourced from the frozen plan's `actors`/`segments` fields, never the scenario file:
|
|
938
|
+
* `inspectCommand` (`apps/cli/src/commands.ts`) reads only the render plan and manifest,
|
|
939
|
+
* so a bundle whose scenario has since been edited or deleted still inspects correctly.
|
|
940
|
+
* Not sourced from `plan.transitionCards`, even though `transitionCards.length` and
|
|
941
|
+
* `segments.length - 1` are provably equal by the render plan's own refinement: `segments`
|
|
942
|
+
* is the more primal fact — the capture's own cut points — so `turnCount` is derived from
|
|
943
|
+
* the field a reader of this JSON would already trust as the timeline, not from a second
|
|
944
|
+
* field that only agrees with it by a schema-level invariant this result does not surface.
|
|
945
|
+
*/
|
|
946
|
+
actors: z6.object({
|
|
947
|
+
/** `plan.actors.length`. Reported alongside `labels` rather than left for a caller to
|
|
948
|
+
* compute, matching `narration.clipCount` sitting beside `narration` above for the
|
|
949
|
+
* same reason: the count is the fact most callers want first, before the list. */
|
|
950
|
+
count: z6.number().int().min(2),
|
|
951
|
+
/** In `plan.actors` order — the cast list a badge or transition card would draw, not
|
|
952
|
+
* deduplicated or sorted. */
|
|
953
|
+
labels: z6.array(z6.string().min(1)).min(2),
|
|
954
|
+
/** Hand-offs, not participants: one fewer than `plan.segments.length`, since a
|
|
955
|
+
* segment is a window one actor already holds and a turn is the cut between two of
|
|
956
|
+
* them — the same count `plan.transitionCards.length` carries, arrived at
|
|
957
|
+
* independently from `segments` for the reason above. */
|
|
958
|
+
turnCount: z6.number().int().nonnegative()
|
|
959
|
+
}).refine((value) => value.count === value.labels.length, "actors.count must equal actors.labels.length").optional(),
|
|
849
960
|
/** The rendered MP4s, from the manifest, so hashes are not recomputed. */
|
|
850
961
|
outputs: z6.array(ArtifactRefSchema),
|
|
851
962
|
assertions: z6.array(AssertionSchema),
|
package/dist/schema/event.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const DemoEventTypeSchema: z.ZodEnum<{
|
|
|
9
9
|
"assertion.fail": "assertion.fail";
|
|
10
10
|
"privacy.mask": "privacy.mask";
|
|
11
11
|
"human.wait": "human.wait";
|
|
12
|
+
turn: "turn";
|
|
12
13
|
}>;
|
|
13
14
|
export declare const TargetSchema: z.ZodObject<{
|
|
14
15
|
role: z.ZodOptional<z.ZodString>;
|
|
@@ -36,6 +37,7 @@ export declare const DemoEventSchema: z.ZodObject<{
|
|
|
36
37
|
"assertion.fail": "assertion.fail";
|
|
37
38
|
"privacy.mask": "privacy.mask";
|
|
38
39
|
"human.wait": "human.wait";
|
|
40
|
+
turn: "turn";
|
|
39
41
|
}>;
|
|
40
42
|
stableId: z.ZodOptional<z.ZodString>;
|
|
41
43
|
title: z.ZodOptional<z.ZodString>;
|
|
@@ -52,6 +54,7 @@ export declare const DemoEventSchema: z.ZodObject<{
|
|
|
52
54
|
}, z.core.$strip>>;
|
|
53
55
|
}, z.core.$strip>>;
|
|
54
56
|
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
57
|
+
actor: z.ZodOptional<z.ZodString>;
|
|
55
58
|
}, z.core.$strip>;
|
|
56
59
|
export type DemoEvent = z.infer<typeof DemoEventSchema>;
|
|
57
60
|
export type DemoEventType = z.infer<typeof DemoEventTypeSchema>;
|
|
@@ -190,6 +190,77 @@ export declare const CameraSchema: z.ZodObject<{
|
|
|
190
190
|
}, z.core.$strip>;
|
|
191
191
|
export type Camera = z.infer<typeof CameraSchema>;
|
|
192
192
|
export type CameraShot = z.infer<typeof CameraShotSchema>;
|
|
193
|
+
/**
|
|
194
|
+
* One participant in a multi-actor demo, each with its own Playwright `BrowserContext` —
|
|
195
|
+
* `AGENTS.md` §1 constraint 3 guarantees at most one is ever active at a single instant.
|
|
196
|
+
* `id` is what `SegmentSchema.actorId` and `TransitionCardSchema.fromActorId`/`toActorId`
|
|
197
|
+
* reference; `label` is the human-readable name a badge or a transition card draws.
|
|
198
|
+
*/
|
|
199
|
+
export declare const ActorSchema: z.ZodObject<{
|
|
200
|
+
id: z.ZodString;
|
|
201
|
+
label: z.ZodString;
|
|
202
|
+
}, z.core.$strip>;
|
|
203
|
+
export type Actor = z.infer<typeof ActorSchema>;
|
|
204
|
+
/**
|
|
205
|
+
* One contiguous window of `raw/session.webm` during which a single actor held the (one,
|
|
206
|
+
* at most) active `BrowserContext`. `startMs`/`endMs` are source-relative — the same space
|
|
207
|
+
* `source.durationMs` and the `trim=duration=...` FFmpeg argument already operate in — not
|
|
208
|
+
* a file path: the concatenation of raw capture segments into the one `raw/session.webm`
|
|
209
|
+
* this plan's `source` names has already happened by the time this schema is populated, so
|
|
210
|
+
* there is deliberately no `rawFile`/path field here, unlike what an earlier design draft
|
|
211
|
+
* suggested. A segment describes how that single file is internally structured, never a
|
|
212
|
+
* second file to read.
|
|
213
|
+
*/
|
|
214
|
+
export declare const SegmentSchema: z.ZodObject<{
|
|
215
|
+
id: z.ZodString;
|
|
216
|
+
actorId: z.ZodString;
|
|
217
|
+
startMs: z.ZodNumber;
|
|
218
|
+
endMs: z.ZodNumber;
|
|
219
|
+
}, z.core.$strip>;
|
|
220
|
+
export type Segment = z.infer<typeof SegmentSchema>;
|
|
221
|
+
/**
|
|
222
|
+
* A full-frame card drawn between two actors' segments on a hand-off — structurally
|
|
223
|
+
* `Intro`/`Outro`'s sibling, down to the same `HEX_COLOUR` injection guard on its colours,
|
|
224
|
+
* plus which actor is handing off to which.
|
|
225
|
+
*
|
|
226
|
+
* Unlike the single well-known `captions/intro.ass`/`captions/outro.ass`, a demo can have
|
|
227
|
+
* many hand-offs, so `assPath` cannot be one literal. It is a regex-constrained family
|
|
228
|
+
* instead — `SpeechClipSchema.path`'s discipline applied to its own N-member family:
|
|
229
|
+
* constrained enough that no filtergraph metacharacter or path traversal is representable.
|
|
230
|
+
*/
|
|
231
|
+
export declare const TransitionCardSchema: z.ZodObject<{
|
|
232
|
+
fromActorId: z.ZodString;
|
|
233
|
+
toActorId: z.ZodString;
|
|
234
|
+
durationMs: z.ZodNumber;
|
|
235
|
+
lines: z.ZodArray<z.ZodString>;
|
|
236
|
+
backgroundColor: z.ZodString;
|
|
237
|
+
textColor: z.ZodString;
|
|
238
|
+
assPath: z.ZodString;
|
|
239
|
+
}, z.core.$strip>;
|
|
240
|
+
export type TransitionCard = z.infer<typeof TransitionCardSchema>;
|
|
241
|
+
/**
|
|
242
|
+
* How long a `demo.turn(actor, { card })` transition card is on screen when its own
|
|
243
|
+
* `durationMs` was omitted.
|
|
244
|
+
*
|
|
245
|
+
* Lives here, beside `TransitionCardSchema`, rather than in either package that resolves it:
|
|
246
|
+
* `packages/recorder-playwright/src/pipeline.ts`'s `transitionCardDurationMs` reads it at
|
|
247
|
+
* derivation time, before `transitionCards` exists, to offset later segments; the renderer
|
|
248
|
+
* that eventually draws a frozen `RenderPlanSchema.transitionCards` entry with no
|
|
249
|
+
* `durationMs` of its own needs the same number. `AGENTS.md` §2 forbids `renderer-ffmpeg`
|
|
250
|
+
* from importing `recorder-playwright` (the reverse edge already exists, so importing back
|
|
251
|
+
* would be the cycle `no-circular` refuses), so a bare constant defined in either package
|
|
252
|
+
* would be unreachable from the other — both already depend on `@plaintake/schema`, which is
|
|
253
|
+
* why it is defined here instead.
|
|
254
|
+
*
|
|
255
|
+
* Matches `DEFAULT_INTRO_MS` and `FREE_TIER_OUTRO.durationMs`
|
|
256
|
+
* (`packages/license/src/branding.ts`), both already 3000ms for the same shape of decision —
|
|
257
|
+
* a full-frame card with a line or two to read, defaulted absent an author- or
|
|
258
|
+
* configuration-supplied length. Not imported from `@plaintake/license`: that package
|
|
259
|
+
* resolves *licensed* branding, and a transition card is never licence-gated (the camera is
|
|
260
|
+
* the one capability still Pro-gated; see `AGENTS.md`), so a shared import would be the wrong
|
|
261
|
+
* fix for a coincidence, not a rule the two packages share.
|
|
262
|
+
*/
|
|
263
|
+
export declare const TRANSITION_CARD_DEFAULT_MS = 3000;
|
|
193
264
|
/**
|
|
194
265
|
* One dimmed-frame-with-a-hole window: a step's measured target rect, frozen exactly as
|
|
195
266
|
* `CursorPointSchema` freezes a target's centre, and for the same reason. `renderer-ffmpeg`'s
|
|
@@ -339,6 +410,14 @@ export type HighlightInput = Omit<Highlight, 'assPath'>;
|
|
|
339
410
|
export type OutroInput = Omit<Outro, 'assPath'>;
|
|
340
411
|
/** The opening card as the application layer supplies it, exactly as `OutroInput` works. */
|
|
341
412
|
export type IntroInput = Omit<Intro, 'assPath'>;
|
|
413
|
+
/**
|
|
414
|
+
* The transition card decision as the application layer supplies it — `pipeline.ts`, in
|
|
415
|
+
* practice, which resolves a hand-off's concrete `durationMs` via `transitionCardDurationMs`
|
|
416
|
+
* before this is ever built, exactly as `intro`/`outro` durations are resolved before
|
|
417
|
+
* `buildRenderPlan` is called. `assPath` is filled in by `buildRenderPlan`, indexed by
|
|
418
|
+
* position in the array against the schema's own `captions/turn-[0-9]+\.ass` family.
|
|
419
|
+
*/
|
|
420
|
+
export type TransitionCardInput = Omit<TransitionCard, 'assPath'>;
|
|
342
421
|
/**
|
|
343
422
|
* The one value a plan's `schema` field ever holds.
|
|
344
423
|
*
|
|
@@ -543,6 +622,26 @@ export declare const RenderPlanSchema: z.ZodObject<{
|
|
|
543
622
|
voice: z.ZodString;
|
|
544
623
|
}, z.core.$strip>>;
|
|
545
624
|
}, z.core.$strip>>;
|
|
625
|
+
actors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
626
|
+
id: z.ZodString;
|
|
627
|
+
label: z.ZodString;
|
|
628
|
+
}, z.core.$strip>>>;
|
|
629
|
+
segments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
630
|
+
id: z.ZodString;
|
|
631
|
+
actorId: z.ZodString;
|
|
632
|
+
startMs: z.ZodNumber;
|
|
633
|
+
endMs: z.ZodNumber;
|
|
634
|
+
}, z.core.$strip>>>;
|
|
635
|
+
transitionCards: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
636
|
+
fromActorId: z.ZodString;
|
|
637
|
+
toActorId: z.ZodString;
|
|
638
|
+
durationMs: z.ZodNumber;
|
|
639
|
+
lines: z.ZodArray<z.ZodString>;
|
|
640
|
+
backgroundColor: z.ZodString;
|
|
641
|
+
textColor: z.ZodString;
|
|
642
|
+
assPath: z.ZodString;
|
|
643
|
+
}, z.core.$strip>>>;
|
|
644
|
+
badgesAssPath: z.ZodOptional<z.ZodLiteral<"captions/badges.ass">>;
|
|
546
645
|
}, z.core.$strip>;
|
|
547
646
|
export type RenderPlan = z.infer<typeof RenderPlanSchema>;
|
|
548
647
|
export type Cue = z.infer<typeof CueSchema>;
|
package/dist/schema/results.d.ts
CHANGED
|
@@ -228,6 +228,7 @@ export declare const DiffCommandResultSchema: z.ZodObject<{
|
|
|
228
228
|
category: z.ZodEnum<{
|
|
229
229
|
assertion: "assertion";
|
|
230
230
|
target: "target";
|
|
231
|
+
actor: "actor";
|
|
231
232
|
step: "step";
|
|
232
233
|
timing: "timing";
|
|
233
234
|
caption: "caption";
|
|
@@ -326,6 +327,11 @@ export declare const InspectResultSchema: z.ZodObject<{
|
|
|
326
327
|
suppliedCount: z.ZodNumber;
|
|
327
328
|
voice: z.ZodOptional<z.ZodString>;
|
|
328
329
|
}, z.core.$strip>>;
|
|
330
|
+
actors: z.ZodOptional<z.ZodObject<{
|
|
331
|
+
count: z.ZodNumber;
|
|
332
|
+
labels: z.ZodArray<z.ZodString>;
|
|
333
|
+
turnCount: z.ZodNumber;
|
|
334
|
+
}, z.core.$strip>>;
|
|
329
335
|
outputs: z.ZodArray<z.ZodObject<{
|
|
330
336
|
path: z.ZodString;
|
|
331
337
|
sha256: z.ZodString;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ScenarioMeta } from './schema/index.js';
|
|
2
|
-
import type { Locator, Page } from 'playwright-core';
|
|
2
|
+
import type { BrowserContextOptions, Locator, Page } from 'playwright-core';
|
|
3
3
|
/**
|
|
4
4
|
* What kind of interaction `run()` performs, declared rather than observed. Like
|
|
5
5
|
* `target`, this is metadata the recorder records and never performs — it tells the
|
|
6
|
-
* cursor track what to draw at the target (`click` a ripple, `type`/`point` a
|
|
7
|
-
*
|
|
6
|
+
* cursor track what to draw at the target (`click` a ripple, `type`/`point`/`scroll` a
|
|
7
|
+
* settle — none of the three pulse, only `click` does). Absent means `point`: the cursor
|
|
8
|
+
* still moves to the target, nothing pulses.
|
|
8
9
|
*/
|
|
9
|
-
export type DemoAction = 'click' | 'type' | 'point';
|
|
10
|
+
export type DemoAction = 'click' | 'type' | 'point' | 'scroll';
|
|
10
11
|
export type DemoStep = {
|
|
11
12
|
id: string;
|
|
12
13
|
title: string;
|
|
@@ -124,6 +125,124 @@ export type DemoHandoff = PreflightHandoff & {
|
|
|
124
125
|
*/
|
|
125
126
|
mask?: string;
|
|
126
127
|
};
|
|
128
|
+
/**
|
|
129
|
+
* Presentation fields shared by every `Actor` verb below — the same subset of `DemoStep`
|
|
130
|
+
* that is still the caller's to supply once `target` and `action` no longer are: a verb's
|
|
131
|
+
* argument list already gives the target (when it has one), and the verb's own name already
|
|
132
|
+
* gives the action. Only how the step presents is left to ask for.
|
|
133
|
+
*
|
|
134
|
+
* `title` is optional here where `DemoStep.title` is required, on purpose: a verb call that
|
|
135
|
+
* narrates nothing — a background click with no caption — is meant to stay a one-liner,
|
|
136
|
+
* not force every single interaction through the same captioning `demo.step()` requires.
|
|
137
|
+
*/
|
|
138
|
+
export type ActorStepMeta = {
|
|
139
|
+
/**
|
|
140
|
+
* Stable id for the step this call records, the same role `DemoStep.id` plays and for the
|
|
141
|
+
* same reasons: it becomes the `stableId` on this step's `step.start`/`step.finish` events,
|
|
142
|
+
* the key a target-miss or an abort diagnostic names, and — via `clipFileName(id)` — the
|
|
143
|
+
* exact filename an author drops into `narrationDir` for a `--speech file` WAV. Absent
|
|
144
|
+
* means the runtime synthesizes one; supply your own when you need that identity to stay
|
|
145
|
+
* stable across runs — a hand-chosen id does not shift when an earlier call in the same
|
|
146
|
+
* turn is added, removed or reordered the way a synthesized one might.
|
|
147
|
+
*/
|
|
148
|
+
id?: string;
|
|
149
|
+
title?: string;
|
|
150
|
+
subtitle?: string;
|
|
151
|
+
/** See `DemoStep.holdMs`. */
|
|
152
|
+
holdMs?: number;
|
|
153
|
+
/** See `DemoStep.highlight`, including the "requires a measurable target" rule. */
|
|
154
|
+
highlight?: boolean | {
|
|
155
|
+
label?: string;
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* A named participant in a multi-actor demo: its own Playwright `BrowserContext`, its own
|
|
160
|
+
* page, and a handle of verbs that each perform a Playwright action *and* record the
|
|
161
|
+
* render-track annotation that action needs — the pairing `demo.step()` otherwise leaves an
|
|
162
|
+
* author to assemble by hand (a `target`, a declared `action`, a `run`). `admin.click(button)`
|
|
163
|
+
* does both in one call; there is no separate bookkeeping step the way there is today.
|
|
164
|
+
*
|
|
165
|
+
* `page` is the same escape hatch `DemoRunArgs.page` already is: anything the named verbs
|
|
166
|
+
* below don't model — a Playwright call with no render-track equivalent — is still reachable
|
|
167
|
+
* through it, at the same cost reaching past `demo.step()` into raw Playwright already has
|
|
168
|
+
* today: nothing about the call is recorded automatically.
|
|
169
|
+
*
|
|
170
|
+
* Every verb that takes a `Locator` records that locator's measured rect on `step.start`, the
|
|
171
|
+
* same rect `DemoStep.target` records; a verb with nothing to point at (`goto`, and `scroll`'s
|
|
172
|
+
* delta form) records none, exactly as a `DemoStep` with no `target` does.
|
|
173
|
+
*
|
|
174
|
+
* Same name, different thing from `@plaintake/schema`'s `Actor`: that one is the frozen
|
|
175
|
+
* `{ id, label }` render-plan record a *finished* run leaves behind; this one is the live,
|
|
176
|
+
* verb-bearing handle a scenario's `run()` calls while a session is still recording.
|
|
177
|
+
*/
|
|
178
|
+
export type Actor = {
|
|
179
|
+
/** Raw Playwright escape hatch. See this type's own doc comment. */
|
|
180
|
+
page: Page;
|
|
181
|
+
goto(url: string, meta?: ActorStepMeta): Promise<void>;
|
|
182
|
+
click(target: Locator, meta?: ActorStepMeta): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Fills `target` with `text`. Masking a field's *value* once it exists is `mask`'s job,
|
|
185
|
+
* not this verb's — `type` only ever records that typing happened, never what was typed.
|
|
186
|
+
*/
|
|
187
|
+
type(target: Locator, text: string, meta?: ActorStepMeta): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* Presses `key` while `target` holds focus — Playwright's `locator.press`, not the
|
|
190
|
+
* page-global `page.keyboard.press`. A demo step points at something; a key press with
|
|
191
|
+
* nothing to point at is exactly what `actor.page.keyboard.press(key)` remains for, at
|
|
192
|
+
* the same recorded-nothing cost as any other reach past a named verb.
|
|
193
|
+
*/
|
|
194
|
+
press(target: Locator, key: string, meta?: ActorStepMeta): Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* Scrolls the page or a specific element into view — two different Playwright calls,
|
|
197
|
+
* `locator.scrollIntoViewIfNeeded()` and `page.mouse.wheel(deltaX, deltaY)`, behind one
|
|
198
|
+
* verb, chosen by which shape `where` is. Named `where` rather than `target` because the
|
|
199
|
+
* delta form is not a target — nothing measurable sits behind a pixel offset — where every
|
|
200
|
+
* other verb's `target` names a `Locator` and nothing else. A `Locator` scrolls that element
|
|
201
|
+
* into view and records its rect exactly as `click`/`hover` do; a `{ deltaX?, deltaY? }`
|
|
202
|
+
* delta scrolls the viewport by that many pixels (either defaulting to 0) and records no
|
|
203
|
+
* target, exactly as `goto` does.
|
|
204
|
+
*/
|
|
205
|
+
scroll(where: Locator | {
|
|
206
|
+
deltaX?: number;
|
|
207
|
+
deltaY?: number;
|
|
208
|
+
}, meta?: ActorStepMeta): Promise<void>;
|
|
209
|
+
hover(target: Locator, meta?: ActorStepMeta): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Mirrors `locator.selectOption`'s common case: one value or several. Anything its fuller
|
|
212
|
+
* overload set covers that a plain value or value list cannot is `step`'s or `page`'s to
|
|
213
|
+
* reach instead.
|
|
214
|
+
*/
|
|
215
|
+
select(target: Locator, value: string | string[], meta?: ActorStepMeta): Promise<void>;
|
|
216
|
+
/**
|
|
217
|
+
* Reuses `DemoAssertion` verbatim. The only thing this adds over `DemoContext.assert` is
|
|
218
|
+
* which actor the recorded event is attributed to.
|
|
219
|
+
*/
|
|
220
|
+
assert(assertion: DemoAssertion): Promise<void>;
|
|
221
|
+
/**
|
|
222
|
+
* Escape hatch for an interaction none of the named verbs above model, bound to this actor
|
|
223
|
+
* exactly as `DemoContext.step` is bound to the default one.
|
|
224
|
+
*/
|
|
225
|
+
step(step: DemoStep): Promise<void>;
|
|
226
|
+
/**
|
|
227
|
+
* Reuses `DemoMask` verbatim. Actor-scoped: applies to this actor's own `BrowserContext`
|
|
228
|
+
* only. Each actor's context is its own page, its own DOM and its own selectors, so a mask
|
|
229
|
+
* registered here has nothing to do in a context it was never registered against.
|
|
230
|
+
*/
|
|
231
|
+
mask(mask: DemoMask): Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Another `Page` on this actor's own `BrowserContext` — the multi-tab counterpart to
|
|
234
|
+
* `demo.actor`'s multi-context one, still governed by the same one-active-page-at-a-time
|
|
235
|
+
* rule within this actor's own turn.
|
|
236
|
+
*/
|
|
237
|
+
newTab(): Promise<Page>;
|
|
238
|
+
/**
|
|
239
|
+
* Hands the real browser window to a person during this actor's turn. Reuses `DemoHandoff`
|
|
240
|
+
* verbatim; the only difference from `DemoContext.handoff` is that a handoff can originate
|
|
241
|
+
* from any actor's turn, not only the default one a scenario would fall back to if none
|
|
242
|
+
* were ever named — a person can be handed the browser whoever currently holds it.
|
|
243
|
+
*/
|
|
244
|
+
handoff(request: DemoHandoff): Promise<void>;
|
|
245
|
+
};
|
|
127
246
|
/**
|
|
128
247
|
* What a scenario may do before recording starts.
|
|
129
248
|
*
|
|
@@ -153,6 +272,49 @@ export interface DemoContext extends PreflightContext {
|
|
|
153
272
|
assert(assertion: DemoAssertion): Promise<void>;
|
|
154
273
|
mask(mask: DemoMask): Promise<void>;
|
|
155
274
|
pause(ms: number): Promise<void>;
|
|
275
|
+
/**
|
|
276
|
+
* Returns a handle for a named actor, standing up its `BrowserContext` and `Page` the first
|
|
277
|
+
* time `id` is seen. Async, not a synchronous handle: `browser.newContext()` and
|
|
278
|
+
* `context.newPage()` are themselves async Playwright calls, and there is no way to hand
|
|
279
|
+
* back an `Actor` whose `page` is already a real, working page without awaiting them first
|
|
280
|
+
* — so authors write `const admin = await demo.actor('admin')`. See `turn` for what makes
|
|
281
|
+
* an actor's context the active one.
|
|
282
|
+
*
|
|
283
|
+
* The first `id` a scenario ever calls this with reuses the implicit default context and
|
|
284
|
+
* page every scenario already runs on, rather than opening a second one nobody asked for,
|
|
285
|
+
* so in practice that call resolves immediately — there is no new `BrowserContext` to wait
|
|
286
|
+
* on. The signature stays `Promise<Actor>` for every actor regardless, rather than
|
|
287
|
+
* special-casing the first one: a single-actor scenario written before actors existed keeps
|
|
288
|
+
* working unchanged, and turning it into a two-actor one only costs the second
|
|
289
|
+
* `demo.actor()` call, not a rewrite of the first actor's steps.
|
|
290
|
+
*/
|
|
291
|
+
actor(id: string, opts?: {
|
|
292
|
+
label?: string;
|
|
293
|
+
contextOptions?: BrowserContextOptions;
|
|
294
|
+
}): Promise<Actor>;
|
|
295
|
+
/**
|
|
296
|
+
* Marks a hand-off from whichever actor currently holds the browser to `actor`, parking the
|
|
297
|
+
* outgoing `BrowserContext` and activating the incoming one. Async because, like `chapter`
|
|
298
|
+
* and `pause`, it is itself an event on the render timeline, not just a bookkeeping call.
|
|
299
|
+
*
|
|
300
|
+
* `opts.card`, when given, plays a full-frame transition card between the two actors'
|
|
301
|
+
* segments — the `lines` it names, an optional spoken `narration`, held for `durationMs`.
|
|
302
|
+
*
|
|
303
|
+
* Omitting `opts.card` is allowed, but still draws a card: `RenderPlanSchema.transitionCards`
|
|
304
|
+
* requires exactly one entry per hand-off with non-empty `lines`, so a card-less turn's card
|
|
305
|
+
* is synthesised at render time as `Now: <label>`, naming the incoming actor by the `label`
|
|
306
|
+
* it was given to `demo.actor()`, held for the same default duration an explicit card with no
|
|
307
|
+
* `durationMs` of its own gets. The persistent corner badge names the active actor throughout
|
|
308
|
+
* either way; `opts.card` only ever changes whether that hand-off also gets a full-frame beat
|
|
309
|
+
* of its own, and what it says.
|
|
310
|
+
*/
|
|
311
|
+
turn(actor: Actor, opts?: {
|
|
312
|
+
card?: {
|
|
313
|
+
lines: string[];
|
|
314
|
+
narration?: string;
|
|
315
|
+
durationMs?: number;
|
|
316
|
+
};
|
|
317
|
+
}): Promise<void>;
|
|
156
318
|
}
|
|
157
319
|
export type DemoRunArgs = {
|
|
158
320
|
page: Page;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plaintake/scenario",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Authoring SDK for PlainTake demo scenarios: defineDemo and the scenario DSL types. Install for editor autocomplete; the PlainTake binary ships a runtime fallback.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|