playwright-director 0.2.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.
Files changed (122) hide show
  1. package/README.md +860 -0
  2. package/dist/Tutorial.d.ts +108 -0
  3. package/dist/Tutorial.d.ts.map +1 -0
  4. package/dist/Tutorial.js +767 -0
  5. package/dist/Tutorial.js.map +1 -0
  6. package/dist/bin/export-transcript.d.ts +3 -0
  7. package/dist/bin/export-transcript.d.ts.map +1 -0
  8. package/dist/bin/export-transcript.js +111 -0
  9. package/dist/bin/export-transcript.js.map +1 -0
  10. package/dist/cursor.d.ts +24 -0
  11. package/dist/cursor.d.ts.map +1 -0
  12. package/dist/cursor.js +96 -0
  13. package/dist/cursor.js.map +1 -0
  14. package/dist/index.d.ts +16 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +12 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/init.d.ts +3 -0
  19. package/dist/init.d.ts.map +1 -0
  20. package/dist/init.js +183 -0
  21. package/dist/init.js.map +1 -0
  22. package/dist/merge.d.ts +26 -0
  23. package/dist/merge.d.ts.map +1 -0
  24. package/dist/merge.js +65 -0
  25. package/dist/merge.js.map +1 -0
  26. package/dist/music.d.ts +29 -0
  27. package/dist/music.d.ts.map +1 -0
  28. package/dist/music.js +107 -0
  29. package/dist/music.js.map +1 -0
  30. package/dist/overlay-html.d.ts +44 -0
  31. package/dist/overlay-html.d.ts.map +1 -0
  32. package/dist/overlay-html.js +150 -0
  33. package/dist/overlay-html.js.map +1 -0
  34. package/dist/overlay.d.ts +39 -0
  35. package/dist/overlay.d.ts.map +1 -0
  36. package/dist/overlay.js +134 -0
  37. package/dist/overlay.js.map +1 -0
  38. package/dist/postinstall.d.ts +3 -0
  39. package/dist/postinstall.d.ts.map +1 -0
  40. package/dist/postinstall.js +38 -0
  41. package/dist/postinstall.js.map +1 -0
  42. package/dist/reporter.d.ts +22 -0
  43. package/dist/reporter.d.ts.map +1 -0
  44. package/dist/reporter.js +166 -0
  45. package/dist/reporter.js.map +1 -0
  46. package/dist/site/build-site.d.ts +2 -0
  47. package/dist/site/build-site.d.ts.map +1 -0
  48. package/dist/site/build-site.js +82 -0
  49. package/dist/site/build-site.js.map +1 -0
  50. package/dist/site/embed.d.ts +48 -0
  51. package/dist/site/embed.d.ts.map +1 -0
  52. package/dist/site/embed.js +45 -0
  53. package/dist/site/embed.js.map +1 -0
  54. package/dist/site/generate-manifest.d.ts +15 -0
  55. package/dist/site/generate-manifest.d.ts.map +1 -0
  56. package/dist/site/generate-manifest.js +106 -0
  57. package/dist/site/generate-manifest.js.map +1 -0
  58. package/dist/site/scaffold.d.ts +3 -0
  59. package/dist/site/scaffold.d.ts.map +1 -0
  60. package/dist/site/scaffold.js +70 -0
  61. package/dist/site/scaffold.js.map +1 -0
  62. package/dist/site/scan-tutorials.d.ts +12 -0
  63. package/dist/site/scan-tutorials.d.ts.map +1 -0
  64. package/dist/site/scan-tutorials.js +54 -0
  65. package/dist/site/scan-tutorials.js.map +1 -0
  66. package/dist/site/types.d.ts +75 -0
  67. package/dist/site/types.d.ts.map +1 -0
  68. package/dist/site/types.js +2 -0
  69. package/dist/site/types.js.map +1 -0
  70. package/dist/skill-stamp.d.ts +21 -0
  71. package/dist/skill-stamp.d.ts.map +1 -0
  72. package/dist/skill-stamp.js +39 -0
  73. package/dist/skill-stamp.js.map +1 -0
  74. package/dist/slugify.d.ts +19 -0
  75. package/dist/slugify.d.ts.map +1 -0
  76. package/dist/slugify.js +29 -0
  77. package/dist/slugify.js.map +1 -0
  78. package/dist/stage-presets.d.ts +51 -0
  79. package/dist/stage-presets.d.ts.map +1 -0
  80. package/dist/stage-presets.js +45 -0
  81. package/dist/stage-presets.js.map +1 -0
  82. package/dist/styles.css +637 -0
  83. package/dist/timeline.d.ts +87 -0
  84. package/dist/timeline.d.ts.map +1 -0
  85. package/dist/timeline.js +108 -0
  86. package/dist/timeline.js.map +1 -0
  87. package/dist/transcript.d.ts +87 -0
  88. package/dist/transcript.d.ts.map +1 -0
  89. package/dist/transcript.js +291 -0
  90. package/dist/transcript.js.map +1 -0
  91. package/dist/tts-provider.d.ts +69 -0
  92. package/dist/tts-provider.d.ts.map +1 -0
  93. package/dist/tts-provider.js +182 -0
  94. package/dist/tts-provider.js.map +1 -0
  95. package/dist/types.d.ts +109 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +2 -0
  98. package/dist/types.js.map +1 -0
  99. package/dist/voice-prerender.d.ts +41 -0
  100. package/dist/voice-prerender.d.ts.map +1 -0
  101. package/dist/voice-prerender.js +122 -0
  102. package/dist/voice-prerender.js.map +1 -0
  103. package/dist/voice.d.ts +76 -0
  104. package/dist/voice.d.ts.map +1 -0
  105. package/dist/voice.js +244 -0
  106. package/dist/voice.js.map +1 -0
  107. package/package.json +87 -0
  108. package/skills/tutorialize/SKILL.md +54 -0
  109. package/skills/tutorialize/agent.md +58 -0
  110. package/skills/tutorialize/references/api.md +489 -0
  111. package/skills/tutorialize/references/storytelling.md +193 -0
  112. package/src/styles.css +637 -0
  113. package/templates/site/public/.gitkeep +0 -0
  114. package/templates/site/public/widget.js +273 -0
  115. package/templates/site/src/components/TutorialsHome.astro +141 -0
  116. package/templates/site/src/components/VideoCard.astro +192 -0
  117. package/templates/site/src/components/VideoModal.astro +170 -0
  118. package/templates/site/src/data/.gitkeep +0 -0
  119. package/templates/site/src/layouts/Base.astro +56 -0
  120. package/templates/site/src/pages/[slug].astro +745 -0
  121. package/templates/site/src/pages/index.astro +5 -0
  122. package/templates/site/src/styles/tutorials.css +303 -0
@@ -0,0 +1,489 @@
1
+ # API Reference — playwright-director
2
+
3
+ Technical reference for the `Tutorial` class, timing model, and implementation rules.
4
+
5
+ ## 1. Setup
6
+
7
+ ```typescript
8
+ import { Tutorial } from 'playwright-director';
9
+
10
+ const tutorial = new Tutorial(page, {
11
+ title: 'My Tutorial',
12
+ lang: 'en',
13
+ audioBaseUrl: 'http://localhost:5173',
14
+ testTitle: testInfo.title,
15
+ testFile: testInfo.file,
16
+ projectName: testInfo.project.name,
17
+ backgroundMusic: '',
18
+ });
19
+ ```
20
+
21
+ Or use a fixture that wraps this (the consuming project typically provides a `tutorial` fixture in its test setup).
22
+
23
+ ### Constructor options
24
+
25
+ | Option | Type | Default | Description |
26
+ |---|---|---|---|
27
+ | `title` | `string` | *required* | Overlay title |
28
+ | `lang` | `string` | `'en'` | Language for TTS and UI |
29
+ | `translate` | `(key: string) => string` | identity | i18n function — pass your `t()` |
30
+ | `audioBaseUrl` | `string` | `'http://localhost:5173'` | Base URL for audio files |
31
+ | `testTitle` | `string` | auto | Raw test title (for reporter matching) |
32
+ | `testFile` | `string` | `''` | Test file path (metadata) |
33
+ | `projectName` | `string` | `''` | Playwright project name |
34
+ | `enableVoice` | `boolean` | `true` | TTS enabled |
35
+ | `voiceName` | `string` | auto | TTS voice override |
36
+ | `voiceRate` | `number` | `1.0` | Speech rate multiplier |
37
+ | `backgroundMusic` | `string` | `''` | Music file URL |
38
+ | `musicVolume` | `number` | `0.15` | Music volume (0–1) |
39
+ | `voiceVolume` | `number` | `2.5` | Voice volume multiplier |
40
+ | `stepDelay` | `number` | `500` | Delay between steps (ms) |
41
+ | `mouseSteps` | `number` | `25` | Cursor animation smoothness |
42
+ | `customStyles` | `string` | built-in | CSS for overlay |
43
+ | `overlayPosition` | `'TL' \| 'TR' \| 'BL' \| 'BR'` | `'TR'` | Overlay corner position |
44
+ | `variant` | `string` | `env TUTORIAL_VARIANT` | Suffixes `testName` (`<testName>-<variant>`) so a second recording never overwrites the first. `'mobile'` also compacts the overlay and pins the multi-scene split (see below) |
45
+
46
+ ## 2. Core Methods
47
+
48
+ ### `tutorial.context(key, options?)`
49
+
50
+ Add a context screen — an overlay card that explains something before the next steps.
51
+
52
+ ```typescript
53
+ tutorial.context('Setting Up Your Company', {
54
+ text: 'This will configure your invoicing identity',
55
+ style: 'goal', // 'goal' | 'clarification' | 'attention'
56
+ voiceText: '...', // TTS override (optional)
57
+ });
58
+ ```
59
+
60
+ **Queued — no `await`.** Executed in order when `complete()` runs.
61
+
62
+ | Style | Icon | Use |
63
+ |---|---|---|
64
+ | `goal` | 🎯 | Tutorial opening — the ONE objective |
65
+ | `clarification` | 💡 | Framing before a complex section |
66
+ | `attention` | ⚠️ | Important warning |
67
+
68
+ ### `tutorial.step(key, action, options?)`
69
+
70
+ Add a step — an action wrapped in narration and visual effects.
71
+
72
+ ```typescript
73
+ // Simple step — action during title narration
74
+ tutorial.step('Save the document', async () => {
75
+ await tutorial.click(page.locator('button[type="submit"]'));
76
+ });
77
+
78
+ // Two-phase — "do" narration, then action during "explain"
79
+ tutorial.step('Company Name', async () => {
80
+ await tutorial.typeSlowly('input[name="name"]', 'ACME Corp');
81
+ }, {
82
+ do: 'Enter your company name',
83
+ explain: 'This will appear on all your invoices',
84
+ });
85
+
86
+ // With voiceText override (for acronym pronunciation)
87
+ tutorial.step('Tax Identifier', async () => {
88
+ await tutorial.typeSlowly('input[name="ice"]', '001234567000089');
89
+ }, {
90
+ do: 'Enter the ICE number',
91
+ explain: 'ICE identifies your company for tax purposes',
92
+ voiceText: "Enter the I.C.E. number. I.C.E. identifies your company for tax purposes",
93
+ });
94
+ ```
95
+
96
+ **Queued — no `await`.** Step options:
97
+
98
+ | Option | Type | Description |
99
+ |---|---|---|
100
+ | `do` | `string` | Short action narration (≤ 8 words) |
101
+ | `explain` | `string` | WHY narration (plays during action) |
102
+ | `voiceText` | `string` | TTS override (on-screen text unchanged) |
103
+ | `skipVoice` | `boolean` | Skip voice for this step |
104
+ | `description` | `string` | Description below step title |
105
+ | `delay` | `number` | Custom post-step delay (ms) |
106
+ | `overlayPosition` | `'TL' \| 'TR' \| 'BL' \| 'BR'` | Override overlay position for this step |
107
+
108
+ ### `await tutorial.complete(message?)`
109
+
110
+ Execute all queued contexts and steps, then show the completion screen.
111
+
112
+ ```typescript
113
+ await tutorial.complete('Client added! You can now invoice them.');
114
+ ```
115
+
116
+ **This is the only `await`.** It runs everything in order.
117
+
118
+ ## 3. Interaction Methods
119
+
120
+ Use these inside step actions instead of raw Playwright calls — they add cursor animation and visual highlights.
121
+
122
+ | Method | Replaces | Effect |
123
+ |---|---|---|
124
+ | `tutorial.click(locator)` | `page.click(...)` | Cursor animation → highlight → click |
125
+ | `tutorial.fill(locator, value)` | `page.fill(...)` | Highlight → fill |
126
+ | `tutorial.typeSlowly(locator, value, delay?)` | `page.fill(...)` | Highlight → character-by-character typing |
127
+ | `tutorial.selectOption(locator, value)` | `page.selectOption(...)` | Highlight → select |
128
+ | `tutorial.highlight(locator, duration?)` | — | Pulsing highlight around element |
129
+ | `tutorial.unhighlight(locator)` | — | Remove highlight |
130
+ | `tutorial.moveMouseToElement(locator)` | — | Animate cursor to element |
131
+ | `tutorial.showEmailPreview(options)` | — | Simulated email popup |
132
+ | `tutorial.switchPage(page)` | — | Switch recording to another tab |
133
+ | `tutorial.clearFields()` | — | Clear form fields on next load |
134
+
135
+ `locator` can be a Playwright `Locator` or a CSS selector string.
136
+
137
+ ## 4. Timing Model
138
+
139
+ ### Single-phase step (no `do`/`explain`)
140
+
141
+ ```
142
+ |------ Voice plays title (0–2000ms) ------|
143
+ |-- Action (25%–100%) --|
144
+ ```
145
+
146
+ Action starts at 25% of voice duration.
147
+
148
+ ### Two-phase step (`do` + `explain`)
149
+
150
+ ```
151
+ |-- "do" voice --|-- "explain" voice --|
152
+ |-- Action happens --|
153
+ ```
154
+
155
+ "Do" voice plays first. Action starts when "explain" begins.
156
+
157
+ The narration is one merged audio clip; the do/explain boundary inside it is
158
+ *estimated* by character share (`duration × (len(do) + 2) / len(full)`), so the
159
+ action may start slightly before or after the audible end of the "do". A
160
+ `voiceText` override is split at its first `'. '` (same rule as
161
+ `tutorial-transcript apply`); a voiceText with no sentence boundary behaves as
162
+ single-phase (action at 25%). Wall clock is always clamped to
163
+ `max(clip duration, offset + action)`, so the next clip never overlaps in the
164
+ ffmpeg mix.
165
+
166
+ ### Between steps
167
+
168
+ Voiced steps overlap narration and action (no `stepDelay` pause). Steps without
169
+ voice (`skipVoice`, voice disabled) pause `stepDelay` ms (default 500ms) before
170
+ the action. Every step ends with a trailing pause — override per-step with
171
+ `{ delay: 1000 }` (default 300ms).
172
+
173
+ Unvoiced steps/contexts are still recorded in the timeline JSON (empty
174
+ `audioFile`, `durationMs: 0`) so the generated site's step guide can show their
175
+ text; the merge, transcript, and voice-prerender pipelines ignore them.
176
+
177
+ ## 5. Critical Rules
178
+
179
+ ### 5.1 No blank-screen opening
180
+
181
+ Navigate to the first screen **BEFORE** any `tutorial.context()`. TTS preloads and voice plays before step actions run — if navigation is inside step 1, viewers see `about:blank` for 5–10 seconds.
182
+
183
+ ```typescript
184
+ // WRONG
185
+ test('my-flow', async ({ page, tutorial }) => {
186
+ tutorial.context('Goal', { text: '...', style: 'goal' });
187
+ tutorial.step('Open page', async () => {
188
+ await page.goto('/page/'); // blank until here
189
+ });
190
+ await tutorial.complete('Done');
191
+ });
192
+
193
+ // RIGHT
194
+ test('my-flow', async ({ page, tutorial }) => {
195
+ await page.goto('/page/');
196
+ await expect(page.getByRole('heading', { name: 'Expected' })).toBeVisible();
197
+
198
+ tutorial.context('Goal', { text: '...', style: 'goal' });
199
+ tutorial.step('The page', async () => {
200
+ // no-op — screen is already visible
201
+ }, { do: 'Here is the page', explain: 'This is where...' });
202
+ await tutorial.complete('Done');
203
+ });
204
+ ```
205
+
206
+ ### 5.2 Navigation transitions
207
+
208
+ When a step triggers a page change (form submit → redirect), either:
209
+ 1. `waitForURL(...)` at the end of that step, or
210
+ 2. Describe the new screen in the *following* step
211
+
212
+ Un-awaited client-side `goto()` calls (e.g., onboarding auto-advance) **must** be awaited:
213
+ ```typescript
214
+ await page.waitForURL(url => url.pathname.startsWith('/next/'), { timeout: 5000 }).catch(() => {});
215
+ ```
216
+
217
+ Without this, the next step's voice is killed by "Execution context was destroyed."
218
+
219
+ ### 5.3 Dual-mode (test + tutorial)
220
+
221
+ The same file runs as both:
222
+ - `playwright test` → fast E2E test (tutorial calls are no-op)
223
+ - `TUTORIAL_MODE=true playwright test` → narrated video
224
+
225
+ **Never create separate tutorial files.** One file, two modes.
226
+
227
+ ### 5.4 Tag requirement
228
+
229
+ ```typescript
230
+ test('My Tutorial', { tag: ['@tutorial'] }, async ({ page, tutorial }) => { ... });
231
+ ```
232
+
233
+ Tutorial-mode runners filter by `--grep "@tutorial"`.
234
+
235
+ ### 5.5 Queue vs execute
236
+
237
+ - `tutorial.context()` → **queues** (no await)
238
+ - `tutorial.step()` → **queues** (no await)
239
+ - `await tutorial.complete()` → **executes everything** (the only await)
240
+
241
+ ## 6. Acronym Pronunciation (voiceText)
242
+
243
+ TTS engines mispronounce acronyms. `voiceText` overrides what TTS says without changing on-screen text.
244
+
245
+ Only add when TTS actually mispronounces — not preemptively.
246
+
247
+ | Language | Strategy | Example |
248
+ |---|---|---|
249
+ | French | Phonetic spelling | "cé-i-ène" for CIN, "caisse nationale de sécurité sociale" for CNSS |
250
+ | English | Dotted abbreviations | "C.I.N.", "C.N.S.S." |
251
+ | Arabic | Usually fine (full terms) | Only if a Latin acronym appears in Arabic text |
252
+
253
+ When using i18n, put `voiceText` in the translation file alongside `do`/`explain`:
254
+ ```json
255
+ {
256
+ "employee_identity": {
257
+ "do": "Entrez le nom et la CIN",
258
+ "explain": "La CIN est le numéro de la carte d'identité nationale",
259
+ "voiceText": "Entrez le nom et le numéro de carte d'identité nationale."
260
+ }
261
+ }
262
+ ```
263
+
264
+ ## 7. TTS Configuration
265
+
266
+ | Provider | Setup | Best for |
267
+ |---|---|---|
268
+ | macOS `say` | Default, no config | Local dev |
269
+ | Edge TTS | `pip install edge-tts` | Free neural voices |
270
+ | Custom | `TUTORIAL_TTS_CMD='cmd {lang} {text} {output}'` | Premium voices |
271
+
272
+ Environment variables:
273
+ - `TUTORIAL_MODE` — master switch (`'true'` to generate video)
274
+ - `TUTORIAL_VOICE` — `'false'` disables TTS
275
+ - `TUTORIAL_TTS_CMD` — custom TTS command with `{lang}`, `{text}`, `{output}` placeholders
276
+ - `TUTORIAL_VOICE_NAME` — voice name override
277
+ - `TUTORIAL_OUTPUT_DIR` — timeline output dir (default: `tutorials/output`)
278
+
279
+ ## 8. Playwright Reporter
280
+
281
+ Auto-merges audio into video after each tutorial test:
282
+
283
+ ```typescript
284
+ // playwright.config.ts
285
+ export default defineConfig({
286
+ reporter: [
287
+ ['playwright-director/reporter', {
288
+ mappingFile: 'path/to/tutorial-mapping.txt',
289
+ tutorialsJson: 'path/to/tutorials.json',
290
+ }],
291
+ ],
292
+ });
293
+ ```
294
+
295
+ Never override `--reporter` on the CLI — it disables the merge step.
296
+
297
+ ## 9. Output
298
+
299
+ ```
300
+ tutorials/
301
+ ├── output/
302
+ │ └── {name}_timeline.json # Timing + ffmpeg command
303
+ ├── transcripts/
304
+ │ └── {name}.md # Auto-generated transcript (editable — see below)
305
+ └── videos/
306
+ ├── {name}.webm # Merged video + audio
307
+ ├── {name}-poster.webp # Poster image (step 1)
308
+ └── {name}-step-{n}.webp # Per-step screenshots
309
+ ```
310
+
311
+ `npx playwright-director build-site` turns this directory into a static gallery
312
+ site. Besides the pages, the build emits `widget.js` + `embed/<slug>.json`
313
+ payloads so any app can embed a tutorial in-app: load
314
+ `<script src="<site>/widget.js" defer>` and add a `data-tutorial="<slug>"`
315
+ attribute to any element (slug = video id, i.e. the tutorial `name` plus
316
+ `-<variant>` if any). `window.PlaywrightDirector.open(slug)` is the programmatic
317
+ equivalent.
318
+
319
+ ### Reviewing & correcting narration
320
+
321
+ The transcript is the review surface: edit the narration texts in
322
+ `tutorials/transcripts/{name}.md`, then run `npx tutorial-transcript apply` —
323
+ it locates each original text in the test source (via the timeline JSON) and
324
+ rewrites the string literals in place. Entries are paired by the `**key:**`
325
+ lines in order (a key used twice corrects each occurrence in turn;
326
+ `**[Complete]**` is the completion message). Two-part narrations
327
+ (`do` + `explain`, title + description) are split at the first sentence
328
+ boundary and both halves replaced. Texts sourced from an i18n catalog, or a
329
+ first sentence that is the verbatim step key, are never rewritten — they are
330
+ reported with their key for a manual fix in the translations. Re-run with
331
+ `TUTORIAL_MODE=true` afterwards: only changed TTS clips are regenerated
332
+ (content-hash caching). `npx tutorial-transcript` (no subcommand) just
333
+ regenerates the transcripts from the timeline JSON.
334
+
335
+ ## 10. Multiple user profiles (scenes)
336
+
337
+ When the story needs two people — one acts, the other reacts — declare each as a
338
+ **scene**. The stage becomes a browser-like tab bar with one `<iframe>` per scene.
339
+
340
+ ```typescript
341
+ const tutorial = new Tutorial(page, {
342
+ title: t('tutorial.invoice.title'),
343
+ scenes: {
344
+ accountant: { label: 'Sara — Accountant', baseUrl: 'http://localhost:5173' },
345
+ client: { label: 'ACME — Client', baseUrl: 'http://localhost:5174' },
346
+ },
347
+ focus: 'accountant',
348
+ });
349
+
350
+ const accountant = tutorial.scene('accountant'); // a Playwright FrameLocator
351
+ const client = tutorial.scene('client');
352
+
353
+ await tutorial.stage(); // mount tab bar + iframes
354
+ await tutorial.goto('accountant', '/invoices/new');
355
+
356
+ tutorial.step('issue_invoice', () => tutorial.click(accountant.getByRole('button')),
357
+ { scene: 'accountant' });
358
+
359
+ tutorial.step('client_pays', () => tutorial.click(client.getByRole('button')),
360
+ { scene: 'client' }); // tab switches automatically
361
+ ```
362
+
363
+ ### Scene methods
364
+
365
+ | Method | Effect |
366
+ |---|---|
367
+ | `tutorial.stage()` | Mount the stage — call once, before any `goto` |
368
+ | `tutorial.scene(name)` | The scene as a `FrameLocator` (full locator API) |
369
+ | `tutorial.goto(name, url)` | Navigate a scene; relative to its `baseUrl`, or absolute |
370
+ | `tutorial.focus(name \| names[], options?)` | Bring scene(s) on stage with optional `{ ratio: [30, 70] }` |
371
+
372
+ ### Rules
373
+
374
+ **10.1 Scenes must be different origins.** Same-origin iframes share cookies and
375
+ `localStorage`, so the second login overwrites the first. Two users of the same
376
+ app need a second hostname (`app.localhost` / `app2.localhost`).
377
+
378
+ **10.2 Tag every step with its scene.** A hidden scene is not interactive —
379
+ acting on an off-stage scene times out. `{ scene }` switches the stage first.
380
+
381
+ **10.3 Split layout with ratios.** `{ scene: ['a', 'b'] }` splits the stage
382
+ for one step. By default panes share equally; pass `ratio` to `focus()` for
383
+ asymmetric splits:
384
+
385
+ ```typescript
386
+ // 30/70 — focus on the right pane
387
+ await tutorial.focus(['accountant', 'client'], { ratio: [30, 70] });
388
+
389
+ // 50/50 — equal split
390
+ await tutorial.focus(['accountant', 'client'], { ratio: [50, 50] });
391
+
392
+ // 70/30 — focus on the left pane
393
+ await tutorial.focus(['accountant', 'client'], { ratio: [70, 30] });
394
+
395
+ // Back to single tab
396
+ await tutorial.focus('client');
397
+ ```
398
+
399
+ In split mode the shared tab bar hides; each pane gets its own label header
400
+ above its iframe, and a visible separator divides the two sides. In single
401
+ mode the regular tab bar shows all tabs (so the viewer knows who else is in
402
+ the story). In the array, the first scene is the one acting.
403
+
404
+ **10.4 Alternation is free.** Changing `scene` between steps switches tabs — you
405
+ never write the switch. Narration should acknowledge it ("meanwhile, the client…"),
406
+ otherwise the cut feels abrupt.
407
+
408
+ **10.5 The target app must allow framing.** `X-Frame-Options` or a strict
409
+ `frame-ancestors` blocks the scene and leaves an empty pane. Relax it in tutorial
410
+ mode only.
411
+
412
+ ## 11. Overlay Position
413
+
414
+ The overlay defaults to **top-right** (`TR`). Set `overlayPosition` globally or per-step to move it.
415
+
416
+ ```typescript
417
+ // Global — all steps in bottom-right
418
+ const tutorial = new Tutorial(page, {
419
+ title: 'My Tutorial',
420
+ overlayPosition: 'BR',
421
+ });
422
+
423
+ // Per-step override — this step only
424
+ tutorial.step('Look here', async () => { ... }, {
425
+ overlayPosition: 'TR',
426
+ });
427
+ ```
428
+
429
+ | Position | Placement |
430
+ |---|---|
431
+ | `TL` | Top-left |
432
+ | `TR` | Top-right (default) |
433
+ | `BL` | Bottom-left |
434
+ | `BR` | Bottom-right |
435
+
436
+ **RTL mirroring**: when `lang: 'ar'`, positions mirror automatically — `TL`↔`TR`, `BL`↔`BR`. No manual override needed.
437
+
438
+ **When to move the overlay**: place it where it won't cover the action. If the step interacts with a top-left form, move the overlay to `BR`. If the action is bottom-right, keep `TL`.
439
+
440
+ ## 12. Variants (mobile recording)
441
+
442
+ Record a second, phone-sized version of the same tutorial without touching the spec: run with `TUTORIAL_VARIANT=mobile` (or pass `variant: 'mobile'`).
443
+
444
+ ```typescript
445
+ import { Tutorial, mobileStage } from 'playwright-director';
446
+
447
+ // Top of the spec: widens viewport + video to N phones side by side.
448
+ // Inert unless TUTORIAL_VARIANT=mobile.
449
+ test.use(mobileStage(2)); // device name ('Pixel 7' default) or explicit {width, height}
450
+ ```
451
+
452
+ ```bash
453
+ TUTORIAL_MODE=true npx playwright test # <name>.webm
454
+ TUTORIAL_MODE=true TUTORIAL_VARIANT=mobile npx playwright test # <name>-mobile.webm
455
+ ```
456
+
457
+ The `mobile` variant automatically:
458
+
459
+ - suffixes every output (`-mobile`) — video, timeline, transcript, screenshots;
460
+ - **pins the split** on multi-scene tutorials: all scenes always visible at equal width, tab bar hidden, per-scene labels shown (inactive dimmed), `focus()` ratios ignored;
461
+ - **compacts the overlay** (smaller card and type, icon + step badge hidden). All of it is CSS variables scoped on `html[data-tutorial-variant='mobile']` remapping `--tutorial-*` to `--tutorial-*-mobile` values — tune from the consuming project with a plain `:root { --tutorial-overlay-width-mobile: 220px; }` override, or bring the icon back with `--tutorial-icon-display-mobile: inline-flex`.
462
+
463
+ Any other variant name only suffixes outputs and stamps `data-tutorial-variant` (no preset). The reporter matches timelines by `testTitle` **and** variant (newest mtime wins), so both runs may share the same output dir.
464
+
465
+ In tutorial mode `mobileStage()` records **oversampled 2× by default** (Playwright never upscales video, so 1× phone video is blurry): it forces `--force-device-scale-factor=2`, aligns `deviceScaleFactor` and doubles `video.size` — layout unchanged. Tune with `mobileStage(2, 'Pixel 7', { scale })`. Caveat: `test.use()` replaces the config's `launchOptions`; repeat any tutorial-mode Chromium args via `{ launchArgs: [...] }`.
466
+
467
+ ## 13. Checklist
468
+
469
+ Before submitting a tutorialized test:
470
+
471
+ - [ ] First screen rendered BEFORE `tutorial.context()` — no blank opening
472
+ - [ ] Every navigation inside a step either `waitForURL` or described in the next step
473
+ - [ ] Opens with a `goal` context
474
+ - [ ] No step has more than 2 sentences of narration
475
+ - [ ] Related fields grouped into single steps
476
+ - [ ] "Do" phrases ≤ 8 words
477
+ - [ ] "Explain" gives WHY, not WHAT
478
+ - [ ] Acronyms have `voiceText` where TTS mispronounces them
479
+ - [ ] Encouraging, specific completion message
480
+ - [ ] Test passes without `TUTORIAL_MODE` (plain E2E)
481
+ - [ ] Test passes with `TUTORIAL_MODE=true` (video generation)
482
+ - [ ] Video watched — does it feel human?
483
+
484
+ Multi-scene tutorials, additionally:
485
+
486
+ - [ ] Every step touching a scene carries `{ scene }`
487
+ - [ ] Scenes are on distinct origins (or aliased hostnames)
488
+ - [ ] Side-by-side used only where simultaneity carries meaning
489
+ - [ ] Narration acknowledges each tab switch
@@ -0,0 +1,193 @@
1
+ # Storytelling — The Human Side of Tutorials
2
+
3
+ Great tutorials feel human, not robotic. Narration LEADS, action FOLLOWS. One concept at a time. Explain WHY before showing HOW. Let the viewer's brain catch up.
4
+
5
+ ## 1. Persona — Know Your Viewer
6
+
7
+ Before writing a single step, identify who is watching. This shapes vocabulary, pacing, what to explain, what to skip.
8
+
9
+ ### Persona dimensions
10
+
11
+ | Dimension | Why it matters | Signals |
12
+ |---|---|---|
13
+ | **Role** | Which features they care about | Test path: `tests/free/` (owner), `tests/accountant/` (pro), `tests/portal/` (client) |
14
+ | **Domain expertise** | Whether to explain business concepts or just UI | An accountant knows what TVA is; a freelancer may not |
15
+ | **Technical comfort** | Pacing — fast for power users, slower for first-timers | Settings pages → power user; onboarding → newcomer |
16
+ | **Emotional state** | First-time setup is anxious; daily use is impatient | Onboarding tutorials reassure; feature tutorials are efficient |
17
+ | **Language & culture** | Tone, formality, examples, RTL layout | Moroccan French is more formal than Canadian French |
18
+
19
+ ### How to determine the persona
20
+
21
+ 1. **Test path** tells user type (`free/`, `premium/`, `accountant/`, `portal/`)
22
+ 2. **Feature complexity** tells expertise level (payroll = HR manager, invoicing = any business user)
23
+ 3. **Flow type** tells emotional state (onboarding = anxious newcomer, daily feature = impatient regular)
24
+ 4. **When in doubt, ask.** Don't guess — narration tone depends on it.
25
+
26
+ ### Persona → narration style
27
+
28
+ | Persona | Vocabulary | Pace | Explain |
29
+ |---|---|---|---|
30
+ | First-time owner (onboarding) | Simple, no jargon | Slow, reassuring | Why each field matters |
31
+ | Accountant switching tools | Professional, precise | Brisk, efficient | Where things are, what's different |
32
+ | Client in portal | Friendly, non-technical | Moderate | How to pay, where to find docs |
33
+ | HR manager (payroll) | Domain-appropriate | Moderate | Calculations, legal requirements |
34
+
35
+ ## 2. Goal — What Is the Viewer Trying to Accomplish?
36
+
37
+ State the goal as a **user outcome**, not a feature description.
38
+
39
+ | Bad | Good |
40
+ |---|---|
41
+ | "This tutorial covers the company settings page" | "Set up your company so your invoices show the right name, address, and tax IDs" |
42
+ | "Learn how to use the payroll module" | "Generate your first pay slip in under 5 minutes" |
43
+ | "Client creation walkthrough" | "Add a client so you can start invoicing them" |
44
+
45
+ The goal determines:
46
+ - What the opening `context({ style: 'goal' })` says
47
+ - Which steps are worth narrating and which are just mechanical (group or skip)
48
+ - What the completion message celebrates
49
+
50
+ ## 3. Prior Knowledge — What to Explain, What to Skip
51
+
52
+ | If the viewer is… | Explain | Skip |
53
+ |---|---|---|
54
+ | First-time user (onboarding) | Why each field matters, what happens after save | Basic navigation |
55
+ | Accountant switching tools | Where things are in this UI, what's different | What an ICE number is |
56
+ | Client viewing the portal | How to pay, where to find documents | Accounting terminology |
57
+ | Power user exploring settings | What each setting controls | How to click a button |
58
+
59
+ **Rule:** if the persona would say "obviously", don't explain it. If they'd say "wait, why?", explain it.
60
+
61
+ ## 4. Emotional Arc
62
+
63
+ Every good tutorial follows this arc:
64
+
65
+ ```
66
+ Reassurance → Confidence → Accomplishment
67
+ "Here's what "See, that "You did it!
68
+ we'll do" was easy" Here's what
69
+ you can do next"
70
+ ```
71
+
72
+ ### Opening (goal context)
73
+ - Set expectations — what will we accomplish?
74
+ - Reduce anxiety — "this only takes a minute"
75
+ - Be specific — "by the end, your invoices will show your company info"
76
+
77
+ ### Middle (clarification contexts, sparingly)
78
+ - Before complex sections: frame why they matter
79
+ - After a save: "great, that's locked in — now let's…"
80
+ - Before a warning: "one thing to know before we continue…"
81
+
82
+ ### Ending (complete)
83
+ - Celebrate the outcome, not the steps
84
+ - Tell them what they can do next
85
+ - Be encouraging, not generic
86
+
87
+ | Bad completion | Good completion |
88
+ |---|---|
89
+ | "Setup complete." | "Your company is ready! You can now create invoices." |
90
+ | "Tutorial finished." | "Pay slip generated! Your employee can view it in their portal." |
91
+ | "Done." | "Client added — you can invoice them right away." |
92
+
93
+ ## 5. Narration Voice
94
+
95
+ ### Write like you're sitting next to the viewer
96
+
97
+ | Rule | Bad (robotic) | Good (human) |
98
+ |---|---|---|
99
+ | ≤ 8 words for "do" | "Click on the save button to save your changes" | "Save your changes" |
100
+ | "Explain" gives the WHY | "This field is for the company name" | "This appears on all your invoices" |
101
+ | Second person | "The user enters their company name" | "Enter your company name" |
102
+ | No robot words | "Step 1: Fill in the name field" | "Start with your company name" |
103
+ | Natural speech | "Navigate to the clients section" | "Head over to your clients" |
104
+ | No tech-speak to non-tech viewers | "Click the CTA in the modal" | "Hit the button to confirm" |
105
+
106
+ ### Conversation, not instruction manual
107
+
108
+ Think of "do" as **what you'd say out loud** while pointing at the screen. Think of "explain" as **the follow-up when they ask "why?"**.
109
+
110
+ ```
111
+ "do": "Enter your company name"
112
+ "explain": "This goes on every invoice you send"
113
+ ↑ that's what you'd say if they paused and looked at you
114
+ ```
115
+
116
+ ## 6. Grouping Decisions
117
+
118
+ ### Group by concept, not by field
119
+
120
+ If the viewer would think of multiple fields as "one thing", they're one step.
121
+
122
+ | Separate concept | Same concept (group) |
123
+ |---|---|
124
+ | Company name + tax ID | Street + city + postal code ("address") |
125
+ | Select client + set date | Email + phone ("contact details") |
126
+ | Enter salary + pick benefits | First name + last name + CIN ("identity") |
127
+
128
+ ### When NOT to group
129
+
130
+ - Fields on different screens
131
+ - Fields that need individual explanation
132
+ - A field whose value depends on the previous one (show cause → effect)
133
+
134
+ ## 7. Context Placement
135
+
136
+ | Context style | When | Frequency |
137
+ |---|---|---|
138
+ | `goal` 🎯 | Tutorial opening — the ONE objective | Exactly once, always first |
139
+ | `clarification` 💡 | Before a complex section needing framing | 0–2 per tutorial |
140
+ | `attention` ⚠️ | Irreversible action, legal requirement, gotcha | Only when genuinely important |
141
+
142
+ **Overusing context cards breaks immersion.** If you have more than 3 total in a tutorial, cut the weakest ones.
143
+
144
+ ## 8. Two-Profile Stories
145
+
146
+ Some workflows only make sense from two sides: an accountant issues an invoice,
147
+ a client pays it. Multi-scene tutorials put both profiles in one video as
148
+ browser-like tabs. The mechanics are in `api.md` §10 — what follows is the
149
+ storytelling.
150
+
151
+ ### Is it really a two-profile story?
152
+
153
+ Ask what the viewer must *believe* at the end. If it is "I can do this", one
154
+ profile is enough — showing the other side is decoration that doubles the
155
+ length. Use two profiles only when the lesson is about the **handoff**: what the
156
+ other person receives, sees, or has to do next.
157
+
158
+ A single-profile tutorial with one sentence of narration ("your client now gets
159
+ an email") often teaches more than a second tab nobody asked for.
160
+
161
+ ### Narrate the switch, always
162
+
163
+ The tab moves on its own when a step changes scene, but a silent cut reads as a
164
+ glitch. Hand the viewer over explicitly:
165
+
166
+ > "That's Sara done. Now let's see what lands on ACME's side."
167
+
168
+ Name the person, not the mechanism. Never say "switching to the client tab" —
169
+ the viewer sees the tab; they need to know *why* they are being moved.
170
+
171
+ ### Side by side earns its place once
172
+
173
+ `{ scene: [a, b] }` splits the stage so app text shrinks. Use ratios to
174
+ control emphasis — `[30, 70]` keeps the acting pane readable while the other
175
+ stays visible for context. Each pane gets its own label header and a clear
176
+ separator divides them.
177
+
178
+ Spend the split on the moment where cause and effect must be seen together —
179
+ the payment landing while the accountant watches. Two side-by-side moments in
180
+ a tutorial is usually one too many.
181
+
182
+ Everywhere else, tabs give each profile the full screen, which is what makes
183
+ dense app UI readable at video resolution.
184
+
185
+ ### Give each scene a person, not a role
186
+
187
+ Labels are on screen the whole tutorial, so they carry the cast:
188
+
189
+ - Good: `Sara — Accountant`, `ACME — Client`
190
+ - Weak: `App`, `Portal`, `Tab 2`
191
+
192
+ A named person makes the handoff feel like a story rather than a demo of two
193
+ browser windows.