playwriter 0.0.80 → 0.0.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a11y-client.js +18 -8
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +3 -1
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/bippy.js +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +84 -0
- package/dist/cdp-relay.js.map +1 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +8 -6
- package/dist/executor.js.map +1 -1
- package/dist/ffmpeg.d.ts +6 -6
- package/dist/ffmpeg.d.ts.map +1 -1
- package/dist/ffmpeg.js +6 -6
- package/dist/ffmpeg.js.map +1 -1
- package/dist/ghost-cursor-client.js +15 -9
- package/dist/prompt.md +71 -337
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts.map +1 -1
- package/dist/recording-ghost-cursor.js +1 -1
- package/dist/recording-ghost-cursor.js.map +1 -1
- package/dist/relay-client.js +1 -1
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +344 -16
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +115 -0
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/screen-recording.d.ts +24 -0
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +62 -0
- package/dist/screen-recording.js.map +1 -1
- package/dist/screen-recording.test.d.ts +2 -0
- package/dist/screen-recording.test.d.ts.map +1 -0
- package/dist/screen-recording.test.js +102 -0
- package/dist/screen-recording.test.js.map +1 -0
- package/dist/selector-generator.js +1 -1
- package/package.json +2 -2
- package/src/aria-snapshot.ts +3 -1
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +4 -0
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +267 -271
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +110 -0
- package/src/executor.ts +8 -6
- package/src/ffmpeg.ts +8 -8
- package/src/ghost-cursor-client.ts +3 -2
- package/src/recording-ghost-cursor.ts +7 -1
- package/src/relay-client.ts +1 -1
- package/src/relay-core.test.ts +378 -17
- package/src/relay-navigation.test.ts +132 -0
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +81 -0
- package/src/skill.md +71 -339
- package/src/snapshots/shadcn-ui-accessibility-full.md +182 -180
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +120 -118
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for fitToAspectRatio — verifies viewport shrink-to-fit
|
|
3
|
+
* for common screen sizes and aspect ratios.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect } from 'vitest'
|
|
6
|
+
import { fitToAspectRatio } from './screen-recording.js'
|
|
7
|
+
|
|
8
|
+
describe('fitToAspectRatio', () => {
|
|
9
|
+
test('common sizes → 16:9', () => {
|
|
10
|
+
const ratio = { width: 16, height: 9 }
|
|
11
|
+
|
|
12
|
+
// Already 16:9 — no change
|
|
13
|
+
expect(fitToAspectRatio({ width: 1920, height: 1080 }, ratio)).toMatchInlineSnapshot(`
|
|
14
|
+
{
|
|
15
|
+
"height": 1080,
|
|
16
|
+
"width": 1920,
|
|
17
|
+
}
|
|
18
|
+
`)
|
|
19
|
+
expect(fitToAspectRatio({ width: 1280, height: 720 }, ratio)).toMatchInlineSnapshot(`
|
|
20
|
+
{
|
|
21
|
+
"height": 720,
|
|
22
|
+
"width": 1280,
|
|
23
|
+
}
|
|
24
|
+
`)
|
|
25
|
+
|
|
26
|
+
// 16:10 (MacBook default) — too tall, shrink height
|
|
27
|
+
expect(fitToAspectRatio({ width: 1440, height: 900 }, ratio)).toMatchInlineSnapshot(`
|
|
28
|
+
{
|
|
29
|
+
"height": 810,
|
|
30
|
+
"width": 1440,
|
|
31
|
+
}
|
|
32
|
+
`)
|
|
33
|
+
expect(fitToAspectRatio({ width: 1680, height: 1050 }, ratio)).toMatchInlineSnapshot(`
|
|
34
|
+
{
|
|
35
|
+
"height": 945,
|
|
36
|
+
"width": 1680,
|
|
37
|
+
}
|
|
38
|
+
`)
|
|
39
|
+
|
|
40
|
+
// 4:3 — too tall, shrink height
|
|
41
|
+
expect(fitToAspectRatio({ width: 1024, height: 768 }, ratio)).toMatchInlineSnapshot(`
|
|
42
|
+
{
|
|
43
|
+
"height": 576,
|
|
44
|
+
"width": 1024,
|
|
45
|
+
}
|
|
46
|
+
`)
|
|
47
|
+
|
|
48
|
+
// Ultra-wide 21:9 — too wide, shrink width
|
|
49
|
+
expect(fitToAspectRatio({ width: 2560, height: 1080 }, ratio)).toMatchInlineSnapshot(`
|
|
50
|
+
{
|
|
51
|
+
"height": 1080,
|
|
52
|
+
"width": 1920,
|
|
53
|
+
}
|
|
54
|
+
`)
|
|
55
|
+
expect(fitToAspectRatio({ width: 3440, height: 1440 }, ratio)).toMatchInlineSnapshot(`
|
|
56
|
+
{
|
|
57
|
+
"height": 1440,
|
|
58
|
+
"width": 2560,
|
|
59
|
+
}
|
|
60
|
+
`)
|
|
61
|
+
|
|
62
|
+
// Square — too tall, shrink height
|
|
63
|
+
expect(fitToAspectRatio({ width: 1000, height: 1000 }, ratio)).toMatchInlineSnapshot(`
|
|
64
|
+
{
|
|
65
|
+
"height": 563,
|
|
66
|
+
"width": 1000,
|
|
67
|
+
}
|
|
68
|
+
`)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('custom aspect ratios', () => {
|
|
72
|
+
// 4:3
|
|
73
|
+
expect(fitToAspectRatio({ width: 1920, height: 1080 }, { width: 4, height: 3 })).toMatchInlineSnapshot(`
|
|
74
|
+
{
|
|
75
|
+
"height": 1080,
|
|
76
|
+
"width": 1440,
|
|
77
|
+
}
|
|
78
|
+
`)
|
|
79
|
+
|
|
80
|
+
// 1:1
|
|
81
|
+
expect(fitToAspectRatio({ width: 1920, height: 1080 }, { width: 1, height: 1 })).toMatchInlineSnapshot(`
|
|
82
|
+
{
|
|
83
|
+
"height": 1080,
|
|
84
|
+
"width": 1080,
|
|
85
|
+
}
|
|
86
|
+
`)
|
|
87
|
+
|
|
88
|
+
// 9:16 vertical
|
|
89
|
+
expect(fitToAspectRatio({ width: 1920, height: 1080 }, { width: 9, height: 16 })).toMatchInlineSnapshot(`
|
|
90
|
+
{
|
|
91
|
+
"height": 1080,
|
|
92
|
+
"width": 608,
|
|
93
|
+
}
|
|
94
|
+
`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('never increases dimensions', () => {
|
|
98
|
+
const ratio = { width: 16, height: 9 }
|
|
99
|
+
const sizes = [
|
|
100
|
+
{ width: 800, height: 600 },
|
|
101
|
+
{ width: 1440, height: 900 },
|
|
102
|
+
{ width: 2560, height: 1080 },
|
|
103
|
+
{ width: 1000, height: 1000 },
|
|
104
|
+
]
|
|
105
|
+
for (const size of sizes) {
|
|
106
|
+
const result = fitToAspectRatio(size, ratio)
|
|
107
|
+
expect(result.width).toBeLessThanOrEqual(size.width)
|
|
108
|
+
expect(result.height).toBeLessThanOrEqual(size.height)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
})
|
package/src/screen-recording.ts
CHANGED
|
@@ -42,6 +42,30 @@ export function getChromeRestartCommand(): string {
|
|
|
42
42
|
return `pkill chrome; sleep 1; google-chrome ${flags}`
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
const DEFAULT_ASPECT_RATIO = { width: 16, height: 9 }
|
|
46
|
+
|
|
47
|
+
/** Default max recording duration: 15 minutes in milliseconds */
|
|
48
|
+
const DEFAULT_MAX_DURATION_MS = 15 * 60 * 1000
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute the largest viewport that fits inside `current` at the target aspect ratio.
|
|
52
|
+
* Never increases width or height beyond current values — only shrinks the
|
|
53
|
+
* dimension that's "too large" relative to the target ratio.
|
|
54
|
+
*/
|
|
55
|
+
export function fitToAspectRatio(
|
|
56
|
+
current: { width: number; height: number },
|
|
57
|
+
ratio: { width: number; height: number } = DEFAULT_ASPECT_RATIO,
|
|
58
|
+
): { width: number; height: number } {
|
|
59
|
+
const targetRatio = ratio.width / ratio.height
|
|
60
|
+
const currentRatio = current.width / current.height
|
|
61
|
+
if (currentRatio > targetRatio) {
|
|
62
|
+
// Too wide — keep height, shrink width
|
|
63
|
+
return { width: Math.round(current.height * targetRatio), height: current.height }
|
|
64
|
+
}
|
|
65
|
+
// Too tall (or already exact) — keep width, shrink height
|
|
66
|
+
return { width: current.width, height: Math.round(current.width / targetRatio) }
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
/**
|
|
46
70
|
* Check if an error is related to missing activeTab permission for recording.
|
|
47
71
|
*/
|
|
@@ -70,6 +94,12 @@ export interface StartRecordingOptions {
|
|
|
70
94
|
outputPath: string
|
|
71
95
|
/** Relay server port (default: 19988) */
|
|
72
96
|
relayPort?: number
|
|
97
|
+
/** Aspect ratio to fit viewport to before recording (default: { width: 16, height: 9 }).
|
|
98
|
+
* Set to null to skip viewport resizing. */
|
|
99
|
+
aspectRatio?: { width: number; height: number } | null
|
|
100
|
+
/** Max recording duration in ms (default: 15 min = 900000). Auto-stops recording
|
|
101
|
+
* when exceeded to prevent accidentally filling disk. Set to 0 or Infinity to disable. */
|
|
102
|
+
maxDurationMs?: number
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
export interface StopRecordingOptions {
|
|
@@ -152,6 +182,11 @@ export function createRecordingApi(options: CreateRecordingApiOptions): {
|
|
|
152
182
|
} {
|
|
153
183
|
const { context, defaultPage, relayPort, ghostCursorController, onStart, onFinish, getExecutionTimestamps } = options
|
|
154
184
|
|
|
185
|
+
// Stores the original viewport before aspect-ratio resize so we can restore on stop/cancel
|
|
186
|
+
let preRecordingViewport: { width: number; height: number } | null = null
|
|
187
|
+
// Auto-stop timer to prevent unbounded recordings
|
|
188
|
+
let maxDurationTimer: ReturnType<typeof setTimeout> | null = null
|
|
189
|
+
|
|
155
190
|
const startWithDefaults = withRecordingDefaults<StartRecordingWithDefaultsOptions, RecordingState>({
|
|
156
191
|
relayPort,
|
|
157
192
|
defaultPage,
|
|
@@ -176,28 +211,74 @@ export function createRecordingApi(options: CreateRecordingApiOptions): {
|
|
|
176
211
|
|
|
177
212
|
const start = async (opts?: StartRecordingWithDefaultsOptions): Promise<RecordingState> => {
|
|
178
213
|
const targetPage = resolveRecordingTargetPage({ context, defaultPage, ghostCursorController, target: opts })
|
|
214
|
+
|
|
215
|
+
// Resize viewport to target aspect ratio (default 16:9) before recording.
|
|
216
|
+
// Only shrinks — never increases width or height beyond current values.
|
|
217
|
+
const aspectRatio = opts?.aspectRatio === undefined ? DEFAULT_ASPECT_RATIO : opts.aspectRatio
|
|
218
|
+
if (aspectRatio) {
|
|
219
|
+
const current = targetPage.viewportSize()
|
|
220
|
+
if (current) {
|
|
221
|
+
const fitted = fitToAspectRatio(current, aspectRatio)
|
|
222
|
+
if (fitted.width !== current.width || fitted.height !== current.height) {
|
|
223
|
+
preRecordingViewport = current
|
|
224
|
+
await targetPage.setViewportSize(fitted)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
179
229
|
const result = await startWithDefaults(opts)
|
|
180
230
|
onStart()
|
|
181
231
|
await ghostCursorController.enableForRecording({ page: targetPage })
|
|
232
|
+
|
|
233
|
+
// Schedule auto-stop to prevent unbounded recordings filling disk.
|
|
234
|
+
// Default 15 min. Set maxDurationMs to 0 or Infinity to disable.
|
|
235
|
+
const maxMs = opts?.maxDurationMs ?? DEFAULT_MAX_DURATION_MS
|
|
236
|
+
if (maxMs > 0 && maxMs < Infinity) {
|
|
237
|
+
maxDurationTimer = setTimeout(() => {
|
|
238
|
+
maxDurationTimer = null
|
|
239
|
+
stop(opts ? { page: opts.page, sessionId: opts.sessionId } : undefined).catch(() => {})
|
|
240
|
+
}, maxMs)
|
|
241
|
+
}
|
|
242
|
+
|
|
182
243
|
return result
|
|
183
244
|
}
|
|
184
245
|
|
|
246
|
+
const clearMaxDurationTimer = (): void => {
|
|
247
|
+
if (maxDurationTimer) {
|
|
248
|
+
clearTimeout(maxDurationTimer)
|
|
249
|
+
maxDurationTimer = null
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const restoreViewport = async (targetPage: Page): Promise<void> => {
|
|
254
|
+
if (!preRecordingViewport) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const saved = preRecordingViewport
|
|
258
|
+
preRecordingViewport = null
|
|
259
|
+
await targetPage.setViewportSize(saved)
|
|
260
|
+
}
|
|
261
|
+
|
|
185
262
|
const stop = async (
|
|
186
263
|
opts?: StopRecordingWithDefaultsOptions,
|
|
187
264
|
): Promise<{ path: string; duration: number; size: number; executionTimestamps: ExecutionTimestamp[] }> => {
|
|
265
|
+
clearMaxDurationTimer()
|
|
188
266
|
const targetPage = resolveRecordingTargetPage({ context, defaultPage, ghostCursorController, target: opts })
|
|
189
267
|
const result = await stopWithDefaults(opts)
|
|
190
268
|
const executionTimestamps = [...getExecutionTimestamps()]
|
|
191
269
|
onFinish()
|
|
192
270
|
await ghostCursorController.disableForRecording({ page: targetPage })
|
|
271
|
+
await restoreViewport(targetPage)
|
|
193
272
|
return { ...result, executionTimestamps }
|
|
194
273
|
}
|
|
195
274
|
|
|
196
275
|
const cancel = async (opts?: CancelRecordingWithDefaultsOptions): Promise<void> => {
|
|
276
|
+
clearMaxDurationTimer()
|
|
197
277
|
const targetPage = resolveRecordingTargetPage({ context, defaultPage, ghostCursorController, target: opts })
|
|
198
278
|
await cancelWithDefaults(opts)
|
|
199
279
|
onFinish()
|
|
200
280
|
await ghostCursorController.disableForRecording({ page: targetPage })
|
|
281
|
+
await restoreViewport(targetPage)
|
|
201
282
|
}
|
|
202
283
|
|
|
203
284
|
return {
|