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
package/README.md
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
# playwright-director
|
|
2
|
+
|
|
3
|
+
**Turn your Playwright end-to-end tests into directed video guides — automatically.**
|
|
4
|
+
|
|
5
|
+
Write your tests once, get polished how-to videos with voice narration, animated cursor, step overlays, background music, and ffmpeg post-processing. Publish them as a static gallery site, and embed any tutorial back into your app as contextual help with a single `data-tutorial` attribute. No screen-recording software, no video editors, no docs that lag behind the product.
|
|
6
|
+
|
|
7
|
+
An AI agent can also write or adapt the test for a specific communication goal — a product demo, a feature explanation, an onboarding walkthrough — and the direction is yours to apply: what to emphasize, how fast to move, where to pause. Every demonstration stays reproducible, because it originates from an automated test.
|
|
8
|
+
|
|
9
|
+
🌐 **[Project site](https://youniwemi.github.io/playwright-director/)** · 🎬 **[Live demo gallery](https://youniwemi.github.io/playwright-director/gallery/)** — every gallery video is recorded by this repo's own CI from its e2e tests on each push to `main`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Why playwright-director?](#why-playwright-director)
|
|
16
|
+
- [The name](#the-name)
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [Multiple user profiles](#multiple-user-profiles)
|
|
21
|
+
- [Variants — record the same tutorial for mobile](#variants--record-the-same-tutorial-for-mobile)
|
|
22
|
+
- [TTS Configuration](#tts-configuration)
|
|
23
|
+
- [Environment Variables](#environment-variables)
|
|
24
|
+
- [Styling](#styling)
|
|
25
|
+
- [Output Files](#output-files)
|
|
26
|
+
- [Reviewing & Correcting Narration](#reviewing--correcting-narration)
|
|
27
|
+
- [Claude Code Integration](#claude-code-integration)
|
|
28
|
+
- [Tutorial Gallery Site](#tutorial-gallery-site)
|
|
29
|
+
- [In-app help widget](#in-app-help-widget)
|
|
30
|
+
- [Deploying to GitHub Pages from CI](#deploying-to-github-pages-from-ci)
|
|
31
|
+
- [How It Works](#how-it-works)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Why playwright-director?
|
|
37
|
+
|
|
38
|
+
Most software teams maintain **tests** and **documentation** separately. Tests verify features work; docs explain how to use them. When a feature changes, the docs lag behind — or never get updated.
|
|
39
|
+
|
|
40
|
+
`playwright-director` bridges this gap: your Playwright tests **are** your tutorial source. Run them normally for CI; flip a flag and they produce broadcast-ready **directed video guides**.
|
|
41
|
+
|
|
42
|
+
### Key Features
|
|
43
|
+
|
|
44
|
+
- **Dual-mode tests** — Same test file runs as a fast E2E test (`playwright test`) or a narrated video tutorial (`TUTORIAL_MODE=true`)
|
|
45
|
+
- **Voice narration** — Built-in TTS with macOS `say`, Microsoft Edge TTS, or any custom command via `TUTORIAL_TTS_CMD`
|
|
46
|
+
- **Animated cursor** — Smooth, eased mouse movements with click animations that follow your test actions
|
|
47
|
+
- **Step overlays** — On-screen banners showing current step, progress bar, and descriptions
|
|
48
|
+
- **Context screens** — Goal / clarification / attention cards between steps to explain what's happening
|
|
49
|
+
- **Background music** — Looping audio with fade-out on completion
|
|
50
|
+
- **Email previews** — Simulated email popups for verification flow demos
|
|
51
|
+
- **Multiple user profiles** — Two signed-in personas as browser-like tabs in one video, with an optional side-by-side moment
|
|
52
|
+
- **ffmpeg post-processing** — Automatic video + audio merge with timeline-accurate voice placement
|
|
53
|
+
- **Screenshot capture** — WebP screenshots at each step, poster image from step 1
|
|
54
|
+
- **Transcript generation** — Markdown transcripts auto-generated from timeline data
|
|
55
|
+
- **Multi-language** — Full RTL support (Arabic), per-language video filenames, i18n-ready
|
|
56
|
+
- **Playwright Reporter** — Auto-merges audio into video as each tutorial test completes
|
|
57
|
+
- **Gallery site** — `build-site` turns your recordings into a static video gallery with step-by-step guides, ready for any static host
|
|
58
|
+
- **In-app help widget** — the published gallery serves a `widget.js`: one `data-tutorial` attribute opens any tutorial as an overlay inside your own app
|
|
59
|
+
- **Zero runtime overhead** — All tutorial logic is no-op when `TUTORIAL_MODE` is not set
|
|
60
|
+
|
|
61
|
+
## The name
|
|
62
|
+
|
|
63
|
+
This package went through more names than it went through versions. It started as `pw-tutorial-video` — literal, honest, and about as charming as a filename. It worked until the tool outgrew it: this package no longer just records videos, it stages them — deciding what to emphasize, how fast to move, where to pause, what deserves the viewer's attention.
|
|
64
|
+
|
|
65
|
+
Then came the search, and the search was humbling. `tutorialize` — the most natural verb in the space — was taken. `testory` (test + story) was brandable but told you nothing. `test2video` described the mechanics, not the craft. `playwright-tutorial` was accurate and instantly forgettable. Every candidate either over-explained or under-sold.
|
|
66
|
+
|
|
67
|
+
The way out was to flip the question: stop naming the output, name the role. Playwright is the actor — it performs every click, scroll, and keystroke on stage. This package is everything around the performance: the staging, the pacing, the narration, the final cut. That job already has a name — **director**. `playwright-director` was free, and it was the only candidate that described the job instead of the file format.
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install --save-dev playwright-director
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Peer Dependencies
|
|
76
|
+
|
|
77
|
+
| Package | Required | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `@playwright/test` | Yes | >= 1.40.0 |
|
|
80
|
+
| `sharp` | Optional | For optimized WebP screenshots (falls back to raw PNG) |
|
|
81
|
+
| `ffmpeg` | Runtime | Required on PATH for audio/video merge |
|
|
82
|
+
| `ffprobe` | Runtime | Required on PATH for audio duration detection |
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
### 1. Create a tutorial-enabled test
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { test, expect } from '@playwright/test';
|
|
90
|
+
import { Tutorial } from 'playwright-director';
|
|
91
|
+
|
|
92
|
+
test('Create your first invoice', { tag: ['@tutorial'] }, async ({ page }, testInfo) => {
|
|
93
|
+
const tutorial = new Tutorial(page, {
|
|
94
|
+
title: 'Create Your First Invoice',
|
|
95
|
+
lang: 'en',
|
|
96
|
+
audioBaseUrl: 'http://localhost:5173', // your dev server port
|
|
97
|
+
backgroundMusic: '', // or a URL to a .mp3 file
|
|
98
|
+
// Required for the reporter to match and merge the video:
|
|
99
|
+
testTitle: testInfo.title,
|
|
100
|
+
testFile: testInfo.file,
|
|
101
|
+
projectName: testInfo.project.name,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Add a context screen (goal explanation)
|
|
105
|
+
tutorial.context('invoice.intro', {
|
|
106
|
+
text: 'Learn how to create and send your first invoice',
|
|
107
|
+
style: 'goal',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Add steps — actions are queued, not executed yet
|
|
111
|
+
tutorial.step('invoice.client', async () => {
|
|
112
|
+
await tutorial.click(page.getByLabel('Client'));
|
|
113
|
+
await tutorial.selectOption(page.getByLabel('Client'), 'Acme Corp');
|
|
114
|
+
}, {
|
|
115
|
+
do: 'Select your client',
|
|
116
|
+
explain: 'Choose from your existing client list or create a new one',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
tutorial.step('invoice.amount', async () => {
|
|
120
|
+
await tutorial.fill(page.getByLabel('Amount'), '1500');
|
|
121
|
+
}, {
|
|
122
|
+
do: 'Enter the invoice amount',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
tutorial.step('invoice.send', async () => {
|
|
126
|
+
await tutorial.click(page.getByRole('button', { name: 'Send' }));
|
|
127
|
+
await expect(page.getByText('Invoice sent')).toBeVisible();
|
|
128
|
+
}, {
|
|
129
|
+
do: 'Send the invoice',
|
|
130
|
+
explain: 'Your client will receive the invoice by email',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Execute all steps and finalize the video
|
|
134
|
+
await tutorial.complete('Invoice created successfully!');
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 2. Run as a normal test
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npx playwright test --grep "@tutorial"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 3. Run in tutorial mode (generates video)
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
TUTORIAL_MODE=true npx playwright test --grep "@tutorial"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Videos are saved to `tutorials/videos/`, timelines to `tutorials/output/`.
|
|
151
|
+
|
|
152
|
+
> **Important**: Never override `--reporter` on the CLI for tutorial runs — it disables the merge step. Use the config file instead.
|
|
153
|
+
|
|
154
|
+
### 4. Add generated paths to `.gitignore`
|
|
155
|
+
|
|
156
|
+
Tutorial runs create files in your project. Add these to `.gitignore`:
|
|
157
|
+
|
|
158
|
+
```gitignore
|
|
159
|
+
# playwright-director generated files
|
|
160
|
+
tutorials/
|
|
161
|
+
static/audio/tutorial-voice/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 5. Converting an existing test
|
|
165
|
+
|
|
166
|
+
The most common use case is converting an existing Playwright test:
|
|
167
|
+
|
|
168
|
+
| Before (plain test) | After (tutorial) |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `await page.click(...)` | `await tutorial.click(...)` |
|
|
171
|
+
| `await page.fill(...)` | `await tutorial.fill(...)` or `tutorial.typeSlowly(...)` |
|
|
172
|
+
| `await page.selectOption(...)` | `await tutorial.selectOption(...)` |
|
|
173
|
+
| — | `tutorial.context(...)` between sections |
|
|
174
|
+
| — | `tutorial.step(title, action, { do, explain })` wrapping groups |
|
|
175
|
+
| — | `await tutorial.complete(message)` at the end |
|
|
176
|
+
|
|
177
|
+
Keep the `expect()` assertions — they still run in both modes, ensuring your tutorial stays in sync with the real UI.
|
|
178
|
+
|
|
179
|
+
## API Reference
|
|
180
|
+
|
|
181
|
+
### `Tutorial` class
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { Tutorial } from 'playwright-director';
|
|
185
|
+
|
|
186
|
+
const tutorial = new Tutorial(page, options);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `TutorialOptions`
|
|
190
|
+
|
|
191
|
+
| Option | Type | Default | Description |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| `title` | `string` | *required* | Tutorial title shown in overlay |
|
|
194
|
+
| `translate` | `(key: string) => string` | `k => k` | Translation function — pass your i18n `t()` |
|
|
195
|
+
| `audioBaseUrl` | `string` | `'http://localhost:5173'` | Base URL for serving audio files |
|
|
196
|
+
| `lang` | `string` | `'en'` | Language for UI text and TTS (e.g. `'en'`, `'fr'`, `'ar'`) |
|
|
197
|
+
| `testName` | `string` | auto | Output filename slug |
|
|
198
|
+
| `testFile` | `string` | `''` | Test file path for metadata |
|
|
199
|
+
| `projectName` | `string` | `''` | Playwright project name |
|
|
200
|
+
| `enableVoice` | `boolean` | `true` | Enable TTS voice narration |
|
|
201
|
+
| `voiceName` | `string` | auto | TTS voice name |
|
|
202
|
+
| `voiceRate` | `number` | `1.0` | Speech rate multiplier |
|
|
203
|
+
| `backgroundMusic` | `string` | `''` | Music file URL |
|
|
204
|
+
| `musicVolume` | `number` | `0.15` | Background music volume (0-1) |
|
|
205
|
+
| `voiceVolume` | `number` | `2.5` | Voice volume multiplier |
|
|
206
|
+
| `stepDelay` | `number` | `500` | Delay before the action for non-voiced steps (ms). Voiced steps overlap narration and action instead: the action starts at the estimated end of the `do` sentence inside the clip (25% of the clip for single-phase steps), and the step lasts `max(narration, offset + action)` |
|
|
207
|
+
| `mouseSteps` | `number` | `25` | Cursor animation smoothness |
|
|
208
|
+
| `customStyles` | `string` | built-in | Custom CSS for overlays |
|
|
209
|
+
| `scenes` | `Record<string, SceneOptions>` | — | Named scenes for multi-profile tutorials — see [Multiple user profiles](#multiple-user-profiles) |
|
|
210
|
+
| `focus` | `string \| string[]` | first scene | Scene(s) active when the stage mounts |
|
|
211
|
+
| `sceneTransition` | `{ duration?: number }` | `{ duration: 600 }` | Pause on a scene switch (ms) |
|
|
212
|
+
|
|
213
|
+
#### Methods
|
|
214
|
+
|
|
215
|
+
| Method | Description |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `context(key, options?)` | Add a context screen (goal/clarification/attention) |
|
|
218
|
+
| `step(key, action, options?)` | Add a tutorial step with an action callback |
|
|
219
|
+
| `complete(message?)` | Execute all queued steps and finalize |
|
|
220
|
+
| `click(locator)` | Click with cursor animation and highlight |
|
|
221
|
+
| `fill(locator, value)` | Fill input with highlight |
|
|
222
|
+
| `typeSlowly(locator, value, delay?)` | Type character by character (visual effect) |
|
|
223
|
+
| `selectOption(locator, value)` | Select dropdown option with highlight |
|
|
224
|
+
| `highlight(locator, duration?)` | Highlight an element |
|
|
225
|
+
| `moveMouseToElement(locator)` | Animate cursor to element |
|
|
226
|
+
| `showEmailPreview(options)` | Show simulated email popup |
|
|
227
|
+
| `switchPage(page)` | Switch recording to another tab/window |
|
|
228
|
+
| `clearFields()` | Clear form fields on next page load |
|
|
229
|
+
| `stage()` | Mount the multi-scene stage (tab bar + one iframe per scene) |
|
|
230
|
+
| `scene(name)` | Get a scene as a Playwright `FrameLocator` |
|
|
231
|
+
| `goto(name, url)` | Navigate a scene (relative to its `baseUrl`, or absolute) |
|
|
232
|
+
| `focus(name \| names[])` | Bring scene(s) on stage — one fills it, two share it |
|
|
233
|
+
|
|
234
|
+
### `StepOptions`
|
|
235
|
+
|
|
236
|
+
| Option | Type | Description |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| `do` | `string` | Short action text shown in overlay |
|
|
239
|
+
| `explain` | `string` | Explanation played during/after action |
|
|
240
|
+
| `voiceText` | `string` | Custom TTS text (overrides do/explain) |
|
|
241
|
+
| `skipVoice` | `boolean` | Skip voice for this step |
|
|
242
|
+
| `description` | `string` | Description shown below step title |
|
|
243
|
+
| `delay` | `number` | Custom delay after this step (ms) |
|
|
244
|
+
| `scene` | `string \| string[]` | Scene(s) this step plays on — the stage switches before the action runs |
|
|
245
|
+
|
|
246
|
+
### `ContextOptions`
|
|
247
|
+
|
|
248
|
+
| Option | Type | Description |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `text` | `string` | Description shown below title |
|
|
251
|
+
| `style` | `'goal' \| 'clarification' \| 'attention'` | Visual style |
|
|
252
|
+
| `voiceText` | `string` | Custom TTS text |
|
|
253
|
+
|
|
254
|
+
## Multiple user profiles
|
|
255
|
+
|
|
256
|
+
Some stories need two people: an accountant issues an invoice, a client pays it.
|
|
257
|
+
Declare each one as a **scene** and the stage becomes a browser-like tab bar,
|
|
258
|
+
with one `<iframe>` per scene.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const tutorial = new Tutorial(page, {
|
|
262
|
+
title: 'Invoice, end to end',
|
|
263
|
+
testTitle: 'invoice issued then paid',
|
|
264
|
+
audioBaseUrl: 'http://localhost:5173',
|
|
265
|
+
scenes: {
|
|
266
|
+
accountant: { label: 'Sara — Accountant', baseUrl: 'http://localhost:5173' },
|
|
267
|
+
client: { label: 'ACME — Client', baseUrl: 'http://localhost:5174' },
|
|
268
|
+
},
|
|
269
|
+
focus: 'accountant',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const accountant = tutorial.scene('accountant'); // a Playwright FrameLocator
|
|
273
|
+
const client = tutorial.scene('client');
|
|
274
|
+
|
|
275
|
+
await tutorial.stage();
|
|
276
|
+
await tutorial.goto('accountant', '/invoices/new');
|
|
277
|
+
await tutorial.goto('client', '/login');
|
|
278
|
+
|
|
279
|
+
tutorial.step('The accountant issues the invoice',
|
|
280
|
+
() => tutorial.click(accountant.getByRole('button', { name: 'Issue' })),
|
|
281
|
+
{ scene: 'accountant' });
|
|
282
|
+
|
|
283
|
+
tutorial.step('The client pays it',
|
|
284
|
+
() => tutorial.click(client.getByRole('button', { name: 'Pay now' })),
|
|
285
|
+
{ scene: 'client' }); // the stage switches tabs on its own
|
|
286
|
+
|
|
287
|
+
tutorial.step('Both sides, at once',
|
|
288
|
+
() => expect(accountant.getByText('Paid')).toBeVisible(),
|
|
289
|
+
{ scene: ['accountant', 'client'] }); // side by side, just for this step
|
|
290
|
+
|
|
291
|
+
await tutorial.complete();
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### How it behaves
|
|
295
|
+
|
|
296
|
+
- **Sessions are independent** because each scene is a separate **origin**. Two
|
|
297
|
+
iframes on the same origin share cookies and `localStorage`, so the second
|
|
298
|
+
login overwrites the first. For two users of the *same* app, serve it under a
|
|
299
|
+
second hostname (`app.localhost` / `app2.localhost`) to get a second origin.
|
|
300
|
+
- **Inactive scenes stay mounted**, hidden but never unloaded — a profile logged
|
|
301
|
+
in behind another tab is still logged in when you come back to it.
|
|
302
|
+
- **A hidden scene is not interactive.** Pass `scene` on every step that touches
|
|
303
|
+
one; the stage switches before the action runs. Acting on an off-stage scene
|
|
304
|
+
will simply time out.
|
|
305
|
+
- **`scene: [a, b]` puts two scenes side by side**, each taking half the stage.
|
|
306
|
+
Treat it as an exception for the moment cause and effect must share one frame:
|
|
307
|
+
at 1280px wide, each pane only gets ~640px. In an array, the first scene is
|
|
308
|
+
the one acting.
|
|
309
|
+
- **Tabs are always all visible**, so the viewer knows who else is in the story
|
|
310
|
+
and who is speaking now.
|
|
311
|
+
- Each timeline step records its `scene`, so transcripts say who was on screen.
|
|
312
|
+
|
|
313
|
+
### Requirements
|
|
314
|
+
|
|
315
|
+
- Target pages must allow framing. Sites sending `X-Frame-Options: SAMEORIGIN`
|
|
316
|
+
or `DENY` (Google, Bing, many SaaS apps) **cannot** be used as scenes. For
|
|
317
|
+
your own app, relax `frame-ancestors` in tutorial mode only.
|
|
318
|
+
- `stage()` navigates the parent page to `audioBaseUrl` before injecting the
|
|
319
|
+
stage, so narration audio loads same-origin. Point `audioBaseUrl` at the app
|
|
320
|
+
serving `static/audio/tutorial-voice/`.
|
|
321
|
+
|
|
322
|
+
## Variants — record the same tutorial for mobile
|
|
323
|
+
|
|
324
|
+
Set `TUTORIAL_VARIANT=mobile` (or pass `variant: 'mobile'`) to record a second,
|
|
325
|
+
phone-sized version of a tutorial without touching the spec:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { Tutorial, mobileStage } from 'playwright-director';
|
|
329
|
+
|
|
330
|
+
// Widens the viewport (and video) to N phones side by side.
|
|
331
|
+
// Inert unless TUTORIAL_VARIANT=mobile — the same spec records both versions.
|
|
332
|
+
test.use(mobileStage(2)); // default device 'Pixel 7'; or mobileStage(2, 'iPhone 14'), or an explicit {width, height}
|
|
333
|
+
|
|
334
|
+
const tutorial = new Tutorial(page, { /* options unchanged */ });
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
TUTORIAL_MODE=true npx playwright test # → tutorials/videos/<name>.webm
|
|
339
|
+
TUTORIAL_MODE=true TUTORIAL_VARIANT=mobile npx playwright test # → tutorials/videos/<name>-mobile.webm
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
What the `mobile` variant does automatically:
|
|
343
|
+
|
|
344
|
+
- **Suffixes every output** with `-mobile` (video, timeline, transcript,
|
|
345
|
+
screenshots, poster) so the desktop version is never overwritten.
|
|
346
|
+
- **Pins the split** on multi-scene tutorials: every phone stays visible at
|
|
347
|
+
equal width for the whole video, the tab bar never shows, per-scene labels
|
|
348
|
+
take over (the inactive one is dimmed), and `focus()` ratios are ignored —
|
|
349
|
+
on phone-width panes an asymmetric split has no room to work.
|
|
350
|
+
- **Compacts the overlay**: smaller card, smaller type, icon and step badge
|
|
351
|
+
hidden. It is all CSS variables scoped under `html[data-tutorial-variant='mobile']`
|
|
352
|
+
remapping to `--tutorial-*-mobile` values, so tuning it is a plain `:root`
|
|
353
|
+
override from your own styles (see [Styling](#styling)):
|
|
354
|
+
|
|
355
|
+
```css
|
|
356
|
+
:root {
|
|
357
|
+
--tutorial-overlay-width-mobile: 220px;
|
|
358
|
+
--tutorial-icon-display-mobile: inline-flex; /* bring the icon back */
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Any other variant name (`TUTORIAL_VARIANT=tablet`) only suffixes the outputs
|
|
363
|
+
and stamps `data-tutorial-variant` — no preset. The reporter matches each run
|
|
364
|
+
to the timeline of the same variant, so both runs can share
|
|
365
|
+
`TUTORIAL_OUTPUT_DIR`, and the gallery site gains a Desktop/Mobile filter when
|
|
366
|
+
variants are present.
|
|
367
|
+
|
|
368
|
+
Note: `mobileStage()` must be passed to `test.use()` at the top of the spec —
|
|
369
|
+
the video size is frozen when the browser context is created.
|
|
370
|
+
|
|
371
|
+
### Sharp mobile video (oversampling)
|
|
372
|
+
|
|
373
|
+
Playwright records video at *window* pixels and never upscales, so a
|
|
374
|
+
phone-sized viewport would yield a blurry ~400px-wide video. In tutorial mode,
|
|
375
|
+
`mobileStage()` therefore records **oversampled 2× by default**: it launches
|
|
376
|
+
Chromium with `--force-device-scale-factor=2`, aligns `deviceScaleFactor`, and
|
|
377
|
+
doubles the video size — the page layout (CSS viewport) is unchanged, a Pixel 7
|
|
378
|
+
video comes out at 824×1678.
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
test.use(mobileStage(2, 'Pixel 7', { scale: 3 })); // even sharper
|
|
382
|
+
test.use(mobileStage(2, 'Pixel 7', { scale: 1 })); // old behavior, native CSS pixels
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Caveat: `test.use()` replaces the config's `launchOptions` wholesale. If your
|
|
386
|
+
playwright.config passes Chromium args for tutorial runs (e.g.
|
|
387
|
+
`--autoplay-policy=no-user-gesture-required`), repeat them via `launchArgs`:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
test.use(mobileStage(2, 'Pixel 7', {
|
|
391
|
+
launchArgs: ['--autoplay-policy=no-user-gesture-required'],
|
|
392
|
+
}));
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Playwright Reporter
|
|
396
|
+
|
|
397
|
+
Auto-merge audio into video after each tutorial test:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// playwright.config.ts
|
|
401
|
+
export default defineConfig({
|
|
402
|
+
reporter: [
|
|
403
|
+
['playwright-director/reporter', {
|
|
404
|
+
mappingFile: 'path/to/tutorial-mapping.txt', // optional
|
|
405
|
+
tutorialsJson: 'path/to/tutorials.json', // optional
|
|
406
|
+
}],
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Utilities
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { slugify } from 'playwright-director/slugify';
|
|
415
|
+
import { createTTSProvider } from 'playwright-director';
|
|
416
|
+
import { buildMergeCommand } from 'playwright-director';
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## TTS Configuration
|
|
420
|
+
|
|
421
|
+
### macOS (default)
|
|
422
|
+
|
|
423
|
+
Uses the built-in `say` command. Voices per language:
|
|
424
|
+
- French: Thomas
|
|
425
|
+
- English: Samantha
|
|
426
|
+
- Arabic: Maged
|
|
427
|
+
|
|
428
|
+
### Edge TTS
|
|
429
|
+
|
|
430
|
+
Free Microsoft neural voices. Install: `pip install edge-tts`
|
|
431
|
+
|
|
432
|
+
### Custom TTS
|
|
433
|
+
|
|
434
|
+
Set `TUTORIAL_TTS_CMD` with placeholders:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
# Custom voice engine
|
|
438
|
+
TUTORIAL_TTS_CMD='my-tts --voice premium -l {lang} {text} -o {output}'
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Pre-rendering the voice cache
|
|
442
|
+
|
|
443
|
+
TTS synthesis is the slow part of a tutorial run, and clips are cached by
|
|
444
|
+
`md5(lang:text)` — the hash covers the **text only**, so a voice or
|
|
445
|
+
`TUTORIAL_TTS_CMD` change never invalidates old clips. `regen-voices` generates
|
|
446
|
+
the clips outside of any test run:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
npx playwright-director regen-voices # synthesize the missing clips
|
|
450
|
+
npx playwright-director regen-voices --workers=4 # parallel TTS (default 2)
|
|
451
|
+
npx playwright-director regen-voices --force # after a voice change: redo everything
|
|
452
|
+
npx playwright-director regen-voices --lang=fr # one language only
|
|
453
|
+
rm -rf static/audio/tutorial-voice && npx playwright-director regen-voices # full rebuild
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Narration texts are collected from the artifacts that record them verbatim:
|
|
457
|
+
the transcripts (`tutorials/transcripts/*.md`) and the timelines
|
|
458
|
+
(`tutorials/output/*_timeline.json`). The project `.env` is loaded for the
|
|
459
|
+
`TUTORIAL_*` settings. Videos are not remixed — the next `TUTORIAL_MODE=true`
|
|
460
|
+
run picks the clips up from cache.
|
|
461
|
+
|
|
462
|
+
## Environment Variables
|
|
463
|
+
|
|
464
|
+
| Variable | Default | Description |
|
|
465
|
+
|---|---|---|
|
|
466
|
+
| `TUTORIAL_MODE` | `false` | Enable tutorial video generation |
|
|
467
|
+
| `TUTORIAL_VARIANT` | none | Recording variant — suffixes outputs; `mobile` also compacts the overlay and pins the split |
|
|
468
|
+
| `TUTORIAL_VOICE` | `true` | Enable/disable voice narration |
|
|
469
|
+
| `TUTORIAL_VOICE_NAME` | auto | TTS voice name override |
|
|
470
|
+
| `TUTORIAL_TTS_CMD` | `say` (macOS) | Custom TTS command |
|
|
471
|
+
| `TUTORIAL_OUTPUT_DIR` | `tutorials/output` | Timeline output directory |
|
|
472
|
+
| `TUTORIAL_MUSIC` | none | Background music file URL |
|
|
473
|
+
| `TUTORIAL_MUSIC_VOLUME` | `0.15` | Background music volume |
|
|
474
|
+
| `TUTORIAL_VOICE_VOLUME` | `2.5` | Voice narration volume |
|
|
475
|
+
| `TUTORIAL_MAPPING_FILE` | none | Tutorial-to-video mapping file |
|
|
476
|
+
| `TUTORIAL_TUTORIALS_JSON` | none | Tutorials metadata JSON |
|
|
477
|
+
|
|
478
|
+
## Styling
|
|
479
|
+
|
|
480
|
+
Import the default styles or provide your own:
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
// Use default styles (automatic)
|
|
484
|
+
const tutorial = new Tutorial(page, { title: 'My Tutorial' });
|
|
485
|
+
|
|
486
|
+
// Use custom styles
|
|
487
|
+
const tutorial = new Tutorial(page, {
|
|
488
|
+
title: 'My Tutorial',
|
|
489
|
+
customStyles: '.tutorial-overlay { background: navy; }'
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
CSS variables for theming:
|
|
494
|
+
|
|
495
|
+
```css
|
|
496
|
+
:root {
|
|
497
|
+
--tutorial-primary: #3b82f6;
|
|
498
|
+
--tutorial-bg-start: rgba(30, 41, 59, 0.95);
|
|
499
|
+
--tutorial-border: rgba(148, 163, 184, 0.3);
|
|
500
|
+
--tutorial-text: #f8fafc;
|
|
501
|
+
--tutorial-z-index: 10000;
|
|
502
|
+
--tutorial-animation-duration: 0.3s;
|
|
503
|
+
|
|
504
|
+
/* Multi-scene stage */
|
|
505
|
+
--tutorial-stage-bg: #1e293b;
|
|
506
|
+
--tutorial-scene-bg: #ffffff;
|
|
507
|
+
--tutorial-tab-bg: #334155;
|
|
508
|
+
--tutorial-tab-text: #94a3b8;
|
|
509
|
+
--tutorial-tab-bg-active: #f8fafc;
|
|
510
|
+
--tutorial-tab-text-active: #0f172a;
|
|
511
|
+
--tutorial-tab-dot: #64748b;
|
|
512
|
+
--tutorial-tab-dot-active: #22c55e;
|
|
513
|
+
--tutorial-tab-padding: 10px 20px;
|
|
514
|
+
--tutorial-tab-radius: 10px 10px 0 0;
|
|
515
|
+
--tutorial-tab-size: 15px;
|
|
516
|
+
--tutorial-tab-transition: 250ms;
|
|
517
|
+
--tutorial-stage-gap: 2px;
|
|
518
|
+
--tutorial-tabbar-padding: 8px 8px 0;
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Scene panes deliberately expose no size variables: they share the stage evenly
|
|
523
|
+
via flexbox, and animating that sizing makes Playwright treat the frame as never
|
|
524
|
+
stable, which times out every click inside it.
|
|
525
|
+
|
|
526
|
+
## Output Files
|
|
527
|
+
|
|
528
|
+
After running in tutorial mode:
|
|
529
|
+
|
|
530
|
+
```
|
|
531
|
+
tutorials/
|
|
532
|
+
├── output/
|
|
533
|
+
│ └── {test-name}_timeline.json # Step timing + ffmpeg command
|
|
534
|
+
├── transcripts/
|
|
535
|
+
│ └── {test-name}.md # Auto-generated transcript
|
|
536
|
+
└── videos/
|
|
537
|
+
├── {test-name}.webm # Merged video with audio
|
|
538
|
+
├── {test-name}-poster.webp # Poster image (step 1)
|
|
539
|
+
└── {test-name}-step-{n}.webp # Step screenshots
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Reviewing & Correcting Narration
|
|
543
|
+
|
|
544
|
+
Every tutorial run auto-generates a markdown transcript in
|
|
545
|
+
`tutorials/transcripts/{test-name}.md`. To rework the narration, edit the
|
|
546
|
+
transcript and write the corrections back into your test source:
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
# 1. (Optional) regenerate transcripts from the timeline JSON files
|
|
550
|
+
npx tutorial-transcript
|
|
551
|
+
|
|
552
|
+
# 2. Edit tutorials/transcripts/{test-name}.md — fix the narration texts
|
|
553
|
+
|
|
554
|
+
# 3. Apply: rewrites the corrected texts in your test file
|
|
555
|
+
npx tutorial-transcript apply
|
|
556
|
+
|
|
557
|
+
# 4. Re-run: only the changed TTS clips are regenerated (content-hash caching)
|
|
558
|
+
TUTORIAL_MODE=true npx playwright test
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`apply` pairs each transcript entry with its timeline step (by the `**key:**`
|
|
562
|
+
lines, in order — a key used twice corrects each occurrence in turn; the
|
|
563
|
+
`**[Complete]**` entry is the completion message), then locates the original
|
|
564
|
+
text in the test file (`testFile` from the timeline) as a quoted string
|
|
565
|
+
literal and replaces it. A narration composed of two fields
|
|
566
|
+
(`do` + `explain`, or title + `description`/`text`) is handled by splitting
|
|
567
|
+
old and new text at the first sentence boundary and replacing both halves.
|
|
568
|
+
|
|
569
|
+
What `apply` **won't** touch: texts that come from an i18n catalog (the
|
|
570
|
+
literal isn't in the test file) and step keys rendered verbatim as titles —
|
|
571
|
+
those are reported with their key so you can fix the translation instead.
|
|
572
|
+
The timeline JSON is updated on success, so re-running `apply` is a no-op.
|
|
573
|
+
|
|
574
|
+
`npx tutorial-transcript apply [file.md ...]` limits the run to specific
|
|
575
|
+
transcripts; without arguments it processes every `.md` in the transcript
|
|
576
|
+
directory (`TUTORIAL_TRANSCRIPT_DIR`, default `tutorials/transcripts`).
|
|
577
|
+
|
|
578
|
+
## Claude Code Integration
|
|
579
|
+
|
|
580
|
+
This package ships **two assets for Claude Code** that teach AI agents how to convert your Playwright tests into professional tutorials:
|
|
581
|
+
|
|
582
|
+
### What you get
|
|
583
|
+
|
|
584
|
+
| Asset | Installed to | Purpose |
|
|
585
|
+
|---|---|---|
|
|
586
|
+
| `/tutorialize` skill | `.claude/skills/tutorialize/` | Slash command that loads tutorial design methodology — persona analysis, storytelling arc, choreography rules, and the full `playwright-director` API. Invoke with `/tutorialize` in Claude Code. |
|
|
587
|
+
| `tutorial-crafter` agent | `.claude/agents/tutorial-crafter.md` | Specialized agent (Sonnet) that reads your test, designs the tutorial arc, and writes the tutorial code. Dispatched automatically or manually. |
|
|
588
|
+
|
|
589
|
+
### Skill reference files
|
|
590
|
+
|
|
591
|
+
The skill bundles two reference documents that Claude reads before tutorializing:
|
|
592
|
+
|
|
593
|
+
| File | Content |
|
|
594
|
+
|---|---|
|
|
595
|
+
| `SKILL.md` | 4-phase process: understand the viewer → design the arc → implement with `playwright-director` → verify |
|
|
596
|
+
| `references/storytelling.md` | Who is watching (role, expertise, emotional state), narration voice rules, pacing decisions, when to use context screens vs steps, multi-profile scene heuristics |
|
|
597
|
+
| `references/api.md` | Complete `Tutorial` class API with timing model, critical rules (e.g., navigate before any tutorial call, never override `--reporter`), and a pre-commit checklist |
|
|
598
|
+
|
|
599
|
+
### Setup
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
npx playwright-director init
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
This interactively copies the skill and agent into your `.claude/` directory. Example session:
|
|
606
|
+
|
|
607
|
+
```
|
|
608
|
+
playwright-director 0.2.0 — Claude Code Setup
|
|
609
|
+
|
|
610
|
+
Install /tutorialize skill into .claude/skills/? [Y/n] y
|
|
611
|
+
+ Skill copied to .claude/skills/tutorialize/
|
|
612
|
+
Install tutorial-crafter agent into .claude/agents/? [Y/n] y
|
|
613
|
+
+ Agent copied to .claude/agents/tutorial-crafter.md
|
|
614
|
+
|
|
615
|
+
Done! You can now use /tutorialize in Claude Code.
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Usage in Claude Code
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
# Ask Claude to convert a test into a tutorial
|
|
622
|
+
> Tutorialize tests/free/01_company.init.ts
|
|
623
|
+
|
|
624
|
+
# Or invoke the skill directly
|
|
625
|
+
> /tutorialize tests/premium/10_expense-scan.test.ts
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Claude will:
|
|
629
|
+
1. Read the test and identify the viewer persona
|
|
630
|
+
2. Design a storytelling arc (goal → steps → completion)
|
|
631
|
+
3. Write the tutorial code with `tutorial.context()`, `tutorial.step()`, voice narration text, and `tutorial.complete()`
|
|
632
|
+
4. Verify the test still passes in both normal and tutorial mode
|
|
633
|
+
|
|
634
|
+
### Keeping them up to date
|
|
635
|
+
|
|
636
|
+
The skill and agent are **copies**, so upgrading the package does not auto-update them.
|
|
637
|
+
`init` stamps the version it installed in `.claude/.playwright-director.json`, and:
|
|
638
|
+
|
|
639
|
+
- after an upgrade, a post-install message names what went stale and tells you to
|
|
640
|
+
re-run `init` — it only speaks when there is something to say, and never writes
|
|
641
|
+
to `.claude/` on its own;
|
|
642
|
+
- re-running `init` shows the transition (`0.1.0 → 0.2.0`) and skips anything
|
|
643
|
+
already current;
|
|
644
|
+
- `npx playwright-director init --yes` answers yes to everything, for scripted
|
|
645
|
+
updates.
|
|
646
|
+
|
|
647
|
+
If you customize the copied skill, keep your additions in a separate file next to
|
|
648
|
+
it — `init` overwrites, it does not merge.
|
|
649
|
+
|
|
650
|
+
## Tutorial Gallery Site
|
|
651
|
+
|
|
652
|
+
Generate a static video gallery website from your tutorials — one command, zero config.
|
|
653
|
+
|
|
654
|
+
### Quick start
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
npx playwright-director build-site
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
On the first run, a `tutorial-site.config.js` file is created with sensible defaults. Edit it to customize branding, then re-run `build-site` — the config is reused automatically.
|
|
661
|
+
|
|
662
|
+
The command scans your `tutorials/` directory for videos, screenshots, and timeline metadata, then builds a static site ready to deploy (e.g., to Cloudflare Pages, Netlify, or any static host).
|
|
663
|
+
|
|
664
|
+
### What gets generated
|
|
665
|
+
|
|
666
|
+
```
|
|
667
|
+
tutorial-site-dist/ # Static site output (configurable)
|
|
668
|
+
├── index.html # Gallery home — videos grouped by category
|
|
669
|
+
├── {video-slug}/index.html # Dedicated page per video: player + step-by-step guide
|
|
670
|
+
├── widget.js # Embeddable in-app help widget (see below)
|
|
671
|
+
├── embed/ # Widget data
|
|
672
|
+
│ ├── {video-slug}.json # One payload per tutorial: video, description, steps
|
|
673
|
+
│ └── index.json # All available slugs (discovery/debugging)
|
|
674
|
+
└── videos/ # Copied from tutorials/videos/
|
|
675
|
+
├── *.webm # Video files
|
|
676
|
+
└── *-step-*.webp # Step screenshots (carousel on cards, guide on video pages)
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Video pages & the step guide
|
|
680
|
+
|
|
681
|
+
Each video page shows the player, the tutorial's intro narration as a
|
|
682
|
+
description, and a **step-by-step guide** built from the timeline: every step's
|
|
683
|
+
narration text paired with its screenshot. Context narrations recorded
|
|
684
|
+
mid-flow appear as callouts between steps, and steps recorded without voice
|
|
685
|
+
still show their text.
|
|
686
|
+
|
|
687
|
+

|
|
688
|
+
|
|
689
|
+
Two settings control the guide (`tutorials` block of the config):
|
|
690
|
+
|
|
691
|
+
| Setting | Values | Effect |
|
|
692
|
+
|---|---|---|
|
|
693
|
+
| `showStrip` | `true` (default) / `false` | Horizontal thumbnail strip under the video info. Clicking a thumbnail opens the screenshot in a lightbox with the step's number, title and text as a caption. |
|
|
694
|
+
| `stepsLayout` | `'text'` (default) | Text-only cards — clicking a step reveals its screenshot inline. |
|
|
695
|
+
| | `'cards'` | Numbered cards with a small screenshot; click opens the lightbox. |
|
|
696
|
+
| | `'full'` | Narration text + full-width screenshot under each step. |
|
|
697
|
+
| | `'none'` | No step guide (strip only, if enabled). |
|
|
698
|
+
|
|
699
|
+
| `stepsLayout: 'cards'` | Strip thumbnail clicked → lightbox with caption |
|
|
700
|
+
|---|---|
|
|
701
|
+
|  |  |
|
|
702
|
+
|
|
703
|
+
<details>
|
|
704
|
+
<summary><code>stepsLayout: 'full'</code> — full-width screenshot per step</summary>
|
|
705
|
+
|
|
706
|
+

|
|
707
|
+
|
|
708
|
+
</details>
|
|
709
|
+
|
|
710
|
+
### In-app help widget
|
|
711
|
+
|
|
712
|
+
The published gallery doubles as a documentation backend for your app. Every
|
|
713
|
+
build ships a `widget.js` at the site root plus one JSON payload per tutorial
|
|
714
|
+
under `embed/`. Load the script from your deployed gallery and mark any
|
|
715
|
+
element with `data-tutorial="<video-slug>"`:
|
|
716
|
+
|
|
717
|
+
```html
|
|
718
|
+
<script src="https://tutorials.myapp.com/widget.js" defer></script>
|
|
719
|
+
|
|
720
|
+
<button data-tutorial="create-account">📘 How does this work?</button>
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Clicking the element opens an overlay right inside your app — the tutorial
|
|
724
|
+
video, its intro narration, and the full step-by-step guide (click a step to
|
|
725
|
+
reveal its screenshot), with a link to the full tutorial page on the gallery:
|
|
726
|
+
|
|
727
|
+

|
|
728
|
+
|
|
729
|
+
Notes:
|
|
730
|
+
|
|
731
|
+
- **Zero dependencies, framework-agnostic** — clicks are handled by event
|
|
732
|
+
delegation, so buttons added later (React/Vue re-renders) work without
|
|
733
|
+
re-initialization. Styles live in a shadow root and can't leak either way;
|
|
734
|
+
the widget inherits your app's font.
|
|
735
|
+
- **Slugs** are the video ids — the file names in the gallery's `videos/`
|
|
736
|
+
directory (a tutorial's `name`, plus `-<variant>` for variant recordings).
|
|
737
|
+
`GET <site>/embed/index.json` lists them all.
|
|
738
|
+
- **Programmatic API**: `window.PlaywrightDirector.open('create-account')` and
|
|
739
|
+
`window.PlaywrightDirector.close()` — e.g. to launch a tutorial from an onboarding
|
|
740
|
+
checklist instead of a button.
|
|
741
|
+
- **Theming**: the gallery's `primaryColor` is the default accent; override it
|
|
742
|
+
from the host page with `.playwright-director-widget { --playwright-director-accent: #16a34a; }`.
|
|
743
|
+
- **Cross-origin**: the widget fetches `embed/<slug>.json` from the gallery
|
|
744
|
+
host, so the gallery must send `Access-Control-Allow-Origin` for your app's
|
|
745
|
+
origin. GitHub Pages sends `*` out of the box; on Netlify or Cloudflare
|
|
746
|
+
Pages add a headers rule for `/embed/*` (e.g. a `_headers` file with
|
|
747
|
+
`Access-Control-Allow-Origin: *`). Video and screenshots are plain media
|
|
748
|
+
elements and need no CORS.
|
|
749
|
+
- **RTL** tutorials (`lang: 'ar'`, …) render the whole panel right-to-left;
|
|
750
|
+
individual texts use `dir="auto"` either way.
|
|
751
|
+
|
|
752
|
+
### Configuration
|
|
753
|
+
|
|
754
|
+
```js
|
|
755
|
+
// tutorial-site.config.js
|
|
756
|
+
export default {
|
|
757
|
+
// Branding
|
|
758
|
+
title: "My App Tutorials", // Site title (header + page titles)
|
|
759
|
+
logo: "./assets/logo.svg", // Path to logo image (optional)
|
|
760
|
+
primaryColor: "#6366f1", // Primary color (CSS)
|
|
761
|
+
font: "system-ui, sans-serif", // Font family (CSS)
|
|
762
|
+
|
|
763
|
+
// Paths
|
|
764
|
+
input: "tutorials/", // Where videos + timelines live
|
|
765
|
+
output: "tutorial-site-dist/", // Where the static site is built
|
|
766
|
+
|
|
767
|
+
// Site
|
|
768
|
+
baseUrl: "https://tutorials.myapp.com", // For SEO (optional)
|
|
769
|
+
lang: "fr", // Site language
|
|
770
|
+
|
|
771
|
+
// Content overrides (optional)
|
|
772
|
+
tutorials: {
|
|
773
|
+
categories: {
|
|
774
|
+
"getting-started": { icon: "⭐", label: "Getting Started" },
|
|
775
|
+
"advanced": { icon: "🚀", label: "Advanced" },
|
|
776
|
+
},
|
|
777
|
+
ui: {
|
|
778
|
+
heroTitle: "Learn My App",
|
|
779
|
+
heroSubtitle: "Step-by-step video tutorials",
|
|
780
|
+
},
|
|
781
|
+
stepsLayout: "text", // Step guide on video pages: 'text' | 'cards' | 'full' | 'none'
|
|
782
|
+
showStrip: true, // Thumbnail strip above the guide (click = lightbox + caption)
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### How videos are discovered
|
|
788
|
+
|
|
789
|
+
The scanner looks for `.webm` files in `<input>/videos/`. For each video:
|
|
790
|
+
|
|
791
|
+
- If a matching `_timeline.json` exists in `<input>/output/`, its metadata is used (title from narration steps, duration, category from `@feature:*` tag)
|
|
792
|
+
- Otherwise, a human-readable title is derived from the filename and duration is left blank
|
|
793
|
+
|
|
794
|
+
Step screenshots (`{video}-step-{n}.{png,webp}`) are auto-detected and displayed as a carousel on each video card.
|
|
795
|
+
|
|
796
|
+
### Previewing locally
|
|
797
|
+
|
|
798
|
+
The generated site uses relative paths, but browsers block `<video>` elements on `file://`. Use any static server:
|
|
799
|
+
|
|
800
|
+
```bash
|
|
801
|
+
npx serve tutorial-site-dist
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### CLI options
|
|
805
|
+
|
|
806
|
+
```bash
|
|
807
|
+
playwright-director build-site # Uses ./tutorial-site.config.js
|
|
808
|
+
playwright-director build-site --config=path/to.js # Custom config path
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Deploying to GitHub Pages from CI
|
|
812
|
+
|
|
813
|
+
The gallery is a plain static site, so it deploys anywhere — this repository deploys its own on every push to `main` ([`.github/workflows/deploy-site.yml`](.github/workflows/deploy-site.yml)): a marketing landing page at the site root and the generated gallery under `/gallery/`.
|
|
814
|
+
|
|
815
|
+

|
|
816
|
+
|
|
817
|
+
The workflow, in short:
|
|
818
|
+
|
|
819
|
+
```yaml
|
|
820
|
+
on:
|
|
821
|
+
push:
|
|
822
|
+
branches: [main]
|
|
823
|
+
|
|
824
|
+
steps:
|
|
825
|
+
- uses: actions/checkout@v4
|
|
826
|
+
- uses: actions/setup-node@v4
|
|
827
|
+
with: { node-version: 22, cache: npm }
|
|
828
|
+
- run: npm ci
|
|
829
|
+
- run: sudo apt-get update && sudo apt-get install -y ffmpeg
|
|
830
|
+
- run: pipx install edge-tts # real narration on Linux
|
|
831
|
+
- run: npx playwright install --with-deps chromium
|
|
832
|
+
- run: npm run test:e2e:video # TUTORIAL_MODE=true playwright test
|
|
833
|
+
- run: npx playwright-director build-site -y
|
|
834
|
+
- run: | # landing page + gallery
|
|
835
|
+
mkdir -p _site/gallery
|
|
836
|
+
cp -r landing/. _site/
|
|
837
|
+
cp -r tutorial-site-dist/. _site/gallery/
|
|
838
|
+
- uses: peaceiris/actions-gh-pages@v4
|
|
839
|
+
with:
|
|
840
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
841
|
+
publish_dir: ./_site
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
Things to know when adapting it:
|
|
845
|
+
|
|
846
|
+
- **TTS on Linux** — there is no macOS `say` on CI runners. With [`edge-tts`](https://pypi.org/project/edge-tts/) on the PATH the voice system falls back to it automatically, so the published videos have real narration. Alternatively set `TUTORIAL_TTS_CMD` to any command you prefer.
|
|
847
|
+
- **Sub-path hosting** — GitHub Pages serves project sites under `/<repo>/`. Set `baseUrl` in `tutorial-site.config.js` to the full public URL **including the path** (this repo uses `https://youniwemi.github.io/playwright-director/gallery/`); the site build derives the Astro `site` + `base` from it so all links and assets resolve.
|
|
848
|
+
- **Pages source** — the workflow publishes to the `gh-pages` branch; configure Pages (Settings → Pages) to serve from that branch.
|
|
849
|
+
- Skip the landing-page assembly step and publish `tutorial-site-dist/` directly if you only want the gallery at the site root.
|
|
850
|
+
|
|
851
|
+
## How It Works
|
|
852
|
+
|
|
853
|
+
1. **Test registration** — `tutorial.step()` / `tutorial.context()` queue actions and start preloading TTS audio in background
|
|
854
|
+
2. **Execution** — `tutorial.complete()` waits for all TTS preloads, then executes steps sequentially with overlays, cursor animation, and voice playback
|
|
855
|
+
3. **Timeline** — Each step records its timestamp and audio file reference
|
|
856
|
+
4. **Post-processing** — The Playwright reporter reads the timeline JSON, waits for Playwright to finalize the video file, then runs ffmpeg to merge the silent screen recording with the voice clips and background music
|
|
857
|
+
|
|
858
|
+
## License
|
|
859
|
+
|
|
860
|
+
MIT
|