@democraft/playwright 0.1.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Democraft contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @democraft/playwright
2
+
3
+ Playwright runtime for executing compiled Democraft IR against a real Chromium browser and writing a reusable recording manifest.
4
+
5
+ This package consumes generated IR. It does not add a second public authoring API.
@@ -0,0 +1,271 @@
1
+ import { CaptureArtifactMetadata, DemoIR, RecordedDemoManifest, TargetSnapshot, RecordedStep } from '@democraft/schema';
2
+
3
+ type RuntimeEnvironment = {
4
+ viewport?: {
5
+ width: number;
6
+ height: number;
7
+ };
8
+ /**
9
+ * Device pixel ratio for screenshot capture. Higher values produce sharper
10
+ * screenshots (more device pixels per CSS pixel). Defaults to 2, which
11
+ * captures at 2× resolution — e.g. a 1920×1080 viewport yields 3840×2160
12
+ * PNGs. This gives the Remotion renderer enough pixel density for camera
13
+ * zoom without pixelation.
14
+ */
15
+ deviceScaleFactor?: number;
16
+ locale?: string;
17
+ timezone?: string;
18
+ storageState?: string;
19
+ /**
20
+ * How the capture runtime decides a page is "ready" before taking each
21
+ * screenshot. When omitted, the runtime uses sensible defaults
22
+ * (`DEFAULT_SETTLE_STRATEGY`) so demos are captured after the page settles
23
+ * without any author configuration. Pass `false` to disable settling and
24
+ * fall back to a fixed hold before each screenshot.
25
+ *
26
+ * See {@link SettleStrategy} for the tunables (idle window, timeout, signal).
27
+ */
28
+ settle?: SettleStrategy | false;
29
+ };
30
+ /**
31
+ * Strategy for detecting that a page has settled before a screenshot is taken.
32
+ *
33
+ * Modern web apps keep loading content after the DOM is ready — data fetches,
34
+ * hydration, animations, lazy images. A screenshot taken the instant an action
35
+ * returns often captures a half-rendered view, so downstream captions/callouts
36
+ * narrate something the screen isn't showing yet.
37
+ *
38
+ * Rather than wait a fixed duration (too short for slow pages, dead air for
39
+ * fast ones), the settle gate waits for the page to **stop changing**. Two
40
+ * signals detect that, chosen because they measure what the viewer actually
41
+ * sees (not network activity, which `networkidle` relies on and is flaky with
42
+ * polling/analytics/SSE — officially discouraged by Playwright):
43
+ *
44
+ * - `"dom"` — a `MutationObserver` counts DOM mutations; idle when none occur
45
+ * for `idleWindowMs`.
46
+ * - `"visual"` — periodic low-resolution screenshots are compared; idle when
47
+ * two consecutive samples are identical within `idleWindowMs`. Catches
48
+ * pure-visual motion (CSS animations, fade-ins) that doesn't touch the DOM.
49
+ * - `"both"` (default) — idle only when both signals agree. Most robust.
50
+ *
51
+ * Settling is best-effort: if the page never quiets down, the gate gives up at
52
+ * `timeoutMs` and captures anyway (never throws), so a pathological page can't
53
+ * stall the whole capture.
54
+ */
55
+ type SettleStrategy = {
56
+ /**
57
+ * How long the page must stay quiet (no DOM mutations and/or no visual
58
+ * changes, per `signal`) before it's considered settled. Shorter is more
59
+ * responsive; longer is more conservative. Default 350ms.
60
+ */
61
+ idleWindowMs?: number;
62
+ /**
63
+ * Maximum time to wait for the page to settle before capturing anyway
64
+ * (best-effort). Should be long enough for real loads but bounded so a
65
+ * misbehaving page (infinite animation, endless polling UI) can't stall the
66
+ * capture. Default 4000ms.
67
+ */
68
+ timeoutMs?: number;
69
+ /**
70
+ * Which signal(s) to use to detect "quiet".
71
+ * - `"dom"` — DOM mutations only (structural rendering).
72
+ * - `"visual"` — screenshot diffing only (pure-visual motion).
73
+ * - `"network"` — in-flight fetch/XHR only (pending data requests).
74
+ * - `"both"` (default) — all three; settled only when every signal agrees.
75
+ * Most robust, since each catches a different kind of in-flight work
76
+ * (e.g. the network signal reveals a pending data fetch even when the DOM
77
+ * is momentarily quiet).
78
+ */
79
+ signal?: "dom" | "visual" | "network" | "both";
80
+ };
81
+ /**
82
+ * Default settle strategy: both signals, 350ms idle window, 4s timeout. Tuned
83
+ * for typical SaaS apps (App Router / SPA with hydration + data fetch).
84
+ */
85
+ declare const DEFAULT_SETTLE_STRATEGY: Required<SettleStrategy>;
86
+ type RunDemoOptions = {
87
+ outputDir?: string;
88
+ /** Managed capture root used only when outputDir is omitted. */
89
+ captureRootDir?: string;
90
+ headless?: boolean;
91
+ environment?: RuntimeEnvironment;
92
+ timeoutMs?: number;
93
+ signal?: AbortSignal;
94
+ /** Called after the capture directory and initial metadata exist. */
95
+ onArtifactCreated?: (artifact: {
96
+ captureRunId: string;
97
+ outputDir: string;
98
+ manifestPath: string;
99
+ metadataPath: string;
100
+ }) => void | Promise<void>;
101
+ };
102
+ type BrowserLike = {
103
+ close(): Promise<void>;
104
+ newContext(options?: Record<string, unknown>): Promise<BrowserContextLike>;
105
+ };
106
+ type BrowserContextLike = {
107
+ newPage(): Promise<PageLike>;
108
+ close(): Promise<void>;
109
+ tracing?: {
110
+ start(options?: Record<string, unknown>): Promise<void>;
111
+ stop(options?: Record<string, unknown>): Promise<void>;
112
+ };
113
+ };
114
+ type PageLike = {
115
+ goto(url: string): Promise<unknown>;
116
+ url(): string;
117
+ getByRole(role: string, options?: {
118
+ name?: string;
119
+ }): LocatorLike;
120
+ getByLabel(text: string): LocatorLike;
121
+ getByTestId(id: string): LocatorLike;
122
+ getByText(text: string): LocatorLike;
123
+ video?(): {
124
+ path(): Promise<string>;
125
+ } | null;
126
+ screenshot?(options?: Record<string, unknown>): Promise<Buffer>;
127
+ waitForTimeout?(durationMs: number): Promise<void>;
128
+ /**
129
+ * Optional navigation gate. SPAs (Next.js App Router, React Router) resolve
130
+ * client-side navigation AFTER `<a>`/`<Link>` click resolves, so a click
131
+ * that changes the route returns before the new view is mounted. When
132
+ * present, the runtime races this against a short timeout after a click so
133
+ * the next step sees the navigated page — without stalling on clicks that
134
+ * don't navigate (dialogs, toggles).
135
+ */
136
+ waitForLoadState?(state?: string): Promise<void>;
137
+ waitForURL?(url: string | RegExp, options?: {
138
+ timeout?: number;
139
+ }): Promise<void>;
140
+ /**
141
+ * Evaluate a function in the page context. Used by the settle gate to
142
+ * install/read a MutationObserver for DOM-idle detection. Absent in
143
+ * lightweight mocks (settle falls back to the visual signal only).
144
+ */
145
+ evaluate?<T>(fn: () => T | Promise<T>): Promise<T>;
146
+ };
147
+ type LocatorLike = {
148
+ click(): Promise<void>;
149
+ fill(value: string): Promise<void>;
150
+ selectOption(value: string): Promise<unknown>;
151
+ boundingBox(options?: {
152
+ timeout?: number;
153
+ }): Promise<{
154
+ x: number;
155
+ y: number;
156
+ width: number;
157
+ height: number;
158
+ } | null>;
159
+ isVisible(options?: {
160
+ timeout?: number;
161
+ }): Promise<boolean>;
162
+ textContent(): Promise<string | null>;
163
+ /**
164
+ * Optional auto-polling visibility gate. Unlike `isVisible` (which reports
165
+ * the *current* state), `waitFor({ state: "visible" })` blocks until the
166
+ * element is visible or the timeout elapses — the right primitive for SPA
167
+ * views that mount a frame after navigation. Absent in lightweight mocks.
168
+ */
169
+ waitFor?(state: {
170
+ state: "visible";
171
+ timeout?: number;
172
+ }): Promise<unknown>;
173
+ };
174
+ type PlaywrightBindings = {
175
+ chromium: {
176
+ launch(options?: {
177
+ headless?: boolean;
178
+ }): Promise<BrowserLike>;
179
+ };
180
+ };
181
+
182
+ declare const CAPTURE_ENVIRONMENT_HASH_PREFIX = "capture-env-v1:sha256:";
183
+ type CaptureRuntimeIdentity = {
184
+ node: string;
185
+ platform: string;
186
+ arch: string;
187
+ engine: "chromium";
188
+ };
189
+ declare function resolveCaptureEnvironment(options?: Pick<RunDemoOptions, "environment" | "headless" | "timeoutMs">, runtime?: CaptureRuntimeIdentity): Promise<{
190
+ environment: CaptureArtifactMetadata["environment"];
191
+ captureEnvironmentHash: string;
192
+ }>;
193
+
194
+ declare function runDemo(ir: DemoIR, options?: RunDemoOptions): Promise<RecordedDemoManifest>;
195
+ declare function runDemoWithBindings(ir: DemoIR, bindings: PlaywrightBindings, options?: RunDemoOptions): Promise<RecordedDemoManifest>;
196
+
197
+ declare function resolveTarget(ir: DemoIR, page: PageLike, targetId: string, timeoutMs?: number): Promise<{
198
+ locator?: LocatorLike;
199
+ snapshot: TargetSnapshot;
200
+ }>;
201
+
202
+ declare function canonicalScreenshotFilename(sceneId: string, stepId: string): string;
203
+ declare function screenshotRelativePath(sceneId: string, stepId: string): string;
204
+ declare function resolveRecordedScreenshotPath(captureDirectory: string, step: Pick<RecordedStep, "sceneId" | "stepId" | "screenshotPath">): string | undefined;
205
+
206
+ type CaptureArtifact = {
207
+ captureRunId: string;
208
+ demoId: string;
209
+ directory: string;
210
+ metadataPath: string;
211
+ manifestPath: string;
212
+ screenshotsPath: string;
213
+ tracePath: string;
214
+ metadata: CaptureArtifactMetadata;
215
+ managed: boolean;
216
+ demoDirectory?: string;
217
+ releaseLock?: () => Promise<void>;
218
+ };
219
+ type CaptureEnvironmentMetadata = CaptureArtifactMetadata["environment"];
220
+ type CreateCaptureArtifactOptions = {
221
+ rootDirectory: string;
222
+ outputDirectory?: string;
223
+ demoId: string;
224
+ definitionHash?: string;
225
+ captureHash?: string;
226
+ captureEnvironmentHash?: string;
227
+ environment: CaptureEnvironmentMetadata;
228
+ /** Internal/test tuning; production defaults use a 30s renewable lease. */
229
+ lockOptions?: CaptureLeaseOptions;
230
+ };
231
+ type ArtifactDependencies = {
232
+ now?: () => Date;
233
+ randomId?: () => string;
234
+ };
235
+ declare function createCaptureArtifact(options: CreateCaptureArtifactOptions, dependencies?: ArtifactDependencies): Promise<CaptureArtifact>;
236
+ declare function startCaptureArtifact(artifact: CaptureArtifact, now?: Date): Promise<void>;
237
+ declare function completeCaptureArtifact(artifact: CaptureArtifact, options?: {
238
+ recordingPath?: string;
239
+ traceAvailable?: boolean;
240
+ now?: Date;
241
+ }): Promise<void>;
242
+ declare function failCaptureArtifact(artifact: CaptureArtifact, error: unknown, now?: Date): Promise<void>;
243
+ declare function cancelCaptureArtifact(artifact: CaptureArtifact, now?: Date): Promise<void>;
244
+ declare function writeCaptureManifestAtomic(artifact: CaptureArtifact, json: string): Promise<void>;
245
+ declare function resolveLatestCompletedCapture(rootDirectory: string, demoId: string): Promise<{
246
+ captureDir: string;
247
+ manifestPath: string;
248
+ captureRunId?: string;
249
+ legacy: boolean;
250
+ } | undefined>;
251
+ declare function isReusableCaptureDirectory(directory: string, demoId: string): Promise<boolean>;
252
+ declare function captureSlug(demoId: string): string;
253
+ declare function captureNamespace(demoId: string): string;
254
+ declare class CaptureAbortError extends Error {
255
+ constructor();
256
+ }
257
+ type CaptureLeaseOptions = {
258
+ leaseMs?: number;
259
+ retryMs?: number;
260
+ maxAttempts?: number;
261
+ malformedStaleMs?: number;
262
+ insideReleaseGuard?: () => void | Promise<void>;
263
+ insideRecoveryGuard?: () => void | Promise<void>;
264
+ };
265
+ type CaptureLeaseRelease = (() => Promise<void>) & {
266
+ refresh: () => Promise<boolean>;
267
+ };
268
+ declare function acquireCaptureLeaseLock(lockPath: string, options?: CaptureLeaseOptions): Promise<CaptureLeaseRelease>;
269
+ declare function redactCaptureErrorMessage(error: unknown, directory?: string): string;
270
+
271
+ export { type BrowserContextLike, type BrowserLike, CAPTURE_ENVIRONMENT_HASH_PREFIX, CaptureAbortError, type CaptureArtifact, type CaptureLeaseOptions, type CaptureLeaseRelease, type CaptureRuntimeIdentity, type CreateCaptureArtifactOptions, DEFAULT_SETTLE_STRATEGY, type LocatorLike, type PageLike, type PlaywrightBindings, type RunDemoOptions, type RuntimeEnvironment, type SettleStrategy, acquireCaptureLeaseLock, cancelCaptureArtifact, canonicalScreenshotFilename, captureNamespace, captureSlug, completeCaptureArtifact, createCaptureArtifact, failCaptureArtifact, isReusableCaptureDirectory, redactCaptureErrorMessage, resolveCaptureEnvironment, resolveLatestCompletedCapture, resolveRecordedScreenshotPath, resolveTarget, runDemo, runDemoWithBindings, screenshotRelativePath, startCaptureArtifact, writeCaptureManifestAtomic };