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.
Files changed (58) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts.map +1 -1
  3. package/dist/aria-snapshot.js +3 -1
  4. package/dist/aria-snapshot.js.map +1 -1
  5. package/dist/bippy.js +1 -1
  6. package/dist/cdp-relay.d.ts.map +1 -1
  7. package/dist/cdp-relay.js +84 -0
  8. package/dist/cdp-relay.js.map +1 -1
  9. package/dist/executor.d.ts.map +1 -1
  10. package/dist/executor.js +8 -6
  11. package/dist/executor.js.map +1 -1
  12. package/dist/ffmpeg.d.ts +6 -6
  13. package/dist/ffmpeg.d.ts.map +1 -1
  14. package/dist/ffmpeg.js +6 -6
  15. package/dist/ffmpeg.js.map +1 -1
  16. package/dist/ghost-cursor-client.js +15 -9
  17. package/dist/prompt.md +71 -337
  18. package/dist/readability.js +16 -2
  19. package/dist/recording-ghost-cursor.d.ts.map +1 -1
  20. package/dist/recording-ghost-cursor.js +1 -1
  21. package/dist/recording-ghost-cursor.js.map +1 -1
  22. package/dist/relay-client.js +1 -1
  23. package/dist/relay-client.js.map +1 -1
  24. package/dist/relay-core.test.d.ts.map +1 -1
  25. package/dist/relay-core.test.js +344 -16
  26. package/dist/relay-core.test.js.map +1 -1
  27. package/dist/relay-navigation.test.d.ts.map +1 -1
  28. package/dist/relay-navigation.test.js +115 -0
  29. package/dist/relay-navigation.test.js.map +1 -1
  30. package/dist/screen-recording.d.ts +24 -0
  31. package/dist/screen-recording.d.ts.map +1 -1
  32. package/dist/screen-recording.js +62 -0
  33. package/dist/screen-recording.js.map +1 -1
  34. package/dist/screen-recording.test.d.ts +2 -0
  35. package/dist/screen-recording.test.d.ts.map +1 -0
  36. package/dist/screen-recording.test.js +102 -0
  37. package/dist/screen-recording.test.js.map +1 -0
  38. package/dist/selector-generator.js +1 -1
  39. package/package.json +2 -2
  40. package/src/aria-snapshot.ts +3 -1
  41. package/src/aria-snapshots/github-interactive.txt +2 -0
  42. package/src/aria-snapshots/github-raw.txt +4 -0
  43. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  44. package/src/aria-snapshots/hackernews-raw.txt +267 -271
  45. package/src/assets/aria-labels-hacker-news.png +0 -0
  46. package/src/cdp-relay.ts +110 -0
  47. package/src/executor.ts +8 -6
  48. package/src/ffmpeg.ts +8 -8
  49. package/src/ghost-cursor-client.ts +3 -2
  50. package/src/recording-ghost-cursor.ts +7 -1
  51. package/src/relay-client.ts +1 -1
  52. package/src/relay-core.test.ts +378 -17
  53. package/src/relay-navigation.test.ts +132 -0
  54. package/src/screen-recording.test.ts +111 -0
  55. package/src/screen-recording.ts +81 -0
  56. package/src/skill.md +71 -339
  57. package/src/snapshots/shadcn-ui-accessibility-full.md +182 -180
  58. 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
+ })
@@ -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 {