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.
- package/README.md +860 -0
- package/dist/Tutorial.d.ts +108 -0
- package/dist/Tutorial.d.ts.map +1 -0
- package/dist/Tutorial.js +767 -0
- package/dist/Tutorial.js.map +1 -0
- package/dist/bin/export-transcript.d.ts +3 -0
- package/dist/bin/export-transcript.d.ts.map +1 -0
- package/dist/bin/export-transcript.js +111 -0
- package/dist/bin/export-transcript.js.map +1 -0
- package/dist/cursor.d.ts +24 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +96 -0
- package/dist/cursor.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +183 -0
- package/dist/init.js.map +1 -0
- package/dist/merge.d.ts +26 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +65 -0
- package/dist/merge.js.map +1 -0
- package/dist/music.d.ts +29 -0
- package/dist/music.d.ts.map +1 -0
- package/dist/music.js +107 -0
- package/dist/music.js.map +1 -0
- package/dist/overlay-html.d.ts +44 -0
- package/dist/overlay-html.d.ts.map +1 -0
- package/dist/overlay-html.js +150 -0
- package/dist/overlay-html.js.map +1 -0
- package/dist/overlay.d.ts +39 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +134 -0
- package/dist/overlay.js.map +1 -0
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +38 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/reporter.d.ts +22 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +166 -0
- package/dist/reporter.js.map +1 -0
- package/dist/site/build-site.d.ts +2 -0
- package/dist/site/build-site.d.ts.map +1 -0
- package/dist/site/build-site.js +82 -0
- package/dist/site/build-site.js.map +1 -0
- package/dist/site/embed.d.ts +48 -0
- package/dist/site/embed.d.ts.map +1 -0
- package/dist/site/embed.js +45 -0
- package/dist/site/embed.js.map +1 -0
- package/dist/site/generate-manifest.d.ts +15 -0
- package/dist/site/generate-manifest.d.ts.map +1 -0
- package/dist/site/generate-manifest.js +106 -0
- package/dist/site/generate-manifest.js.map +1 -0
- package/dist/site/scaffold.d.ts +3 -0
- package/dist/site/scaffold.d.ts.map +1 -0
- package/dist/site/scaffold.js +70 -0
- package/dist/site/scaffold.js.map +1 -0
- package/dist/site/scan-tutorials.d.ts +12 -0
- package/dist/site/scan-tutorials.d.ts.map +1 -0
- package/dist/site/scan-tutorials.js +54 -0
- package/dist/site/scan-tutorials.js.map +1 -0
- package/dist/site/types.d.ts +75 -0
- package/dist/site/types.d.ts.map +1 -0
- package/dist/site/types.js +2 -0
- package/dist/site/types.js.map +1 -0
- package/dist/skill-stamp.d.ts +21 -0
- package/dist/skill-stamp.d.ts.map +1 -0
- package/dist/skill-stamp.js +39 -0
- package/dist/skill-stamp.js.map +1 -0
- package/dist/slugify.d.ts +19 -0
- package/dist/slugify.d.ts.map +1 -0
- package/dist/slugify.js +29 -0
- package/dist/slugify.js.map +1 -0
- package/dist/stage-presets.d.ts +51 -0
- package/dist/stage-presets.d.ts.map +1 -0
- package/dist/stage-presets.js +45 -0
- package/dist/stage-presets.js.map +1 -0
- package/dist/styles.css +637 -0
- package/dist/timeline.d.ts +87 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +108 -0
- package/dist/timeline.js.map +1 -0
- package/dist/transcript.d.ts +87 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +291 -0
- package/dist/transcript.js.map +1 -0
- package/dist/tts-provider.d.ts +69 -0
- package/dist/tts-provider.d.ts.map +1 -0
- package/dist/tts-provider.js +182 -0
- package/dist/tts-provider.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/voice-prerender.d.ts +41 -0
- package/dist/voice-prerender.d.ts.map +1 -0
- package/dist/voice-prerender.js +122 -0
- package/dist/voice-prerender.js.map +1 -0
- package/dist/voice.d.ts +76 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +244 -0
- package/dist/voice.js.map +1 -0
- package/package.json +87 -0
- package/skills/tutorialize/SKILL.md +54 -0
- package/skills/tutorialize/agent.md +58 -0
- package/skills/tutorialize/references/api.md +489 -0
- package/skills/tutorialize/references/storytelling.md +193 -0
- package/src/styles.css +637 -0
- package/templates/site/public/.gitkeep +0 -0
- package/templates/site/public/widget.js +273 -0
- package/templates/site/src/components/TutorialsHome.astro +141 -0
- package/templates/site/src/components/VideoCard.astro +192 -0
- package/templates/site/src/components/VideoModal.astro +170 -0
- package/templates/site/src/data/.gitkeep +0 -0
- package/templates/site/src/layouts/Base.astro +56 -0
- package/templates/site/src/pages/[slug].astro +745 -0
- package/templates/site/src/pages/index.astro +5 -0
- 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.
|