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
package/src/cdp-relay.ts CHANGED
@@ -83,6 +83,7 @@ export async function startPlayWriterCDPRelayServer({
83
83
  } = {}): Promise<RelayServer> {
84
84
  const emitter = new EventEmitter()
85
85
  const store = relayState.createRelayStore()
86
+ const extensionDownloadBehavior = new Map<string, Protocol.Browser.SetDownloadBehaviorRequest>()
86
87
 
87
88
  const resolvedCdpLogger = cdpLogger || createCdpLogger()
88
89
  const logCdpJson = (entry: CdpLogEntry) => {
@@ -557,6 +558,92 @@ export async function startPlayWriterCDPRelayServer({
557
558
  }
558
559
  }
559
560
 
561
+ function getPageTargetSessionIds({ extensionId }: { extensionId: string }): string[] {
562
+ const extensionState = store.getState().extensions.get(extensionId)
563
+ if (!extensionState) {
564
+ return []
565
+ }
566
+ return Array.from(extensionState.connectedTargets.values())
567
+ .filter((target) => {
568
+ return target.targetInfo.type === 'page'
569
+ })
570
+ .map((target) => {
571
+ return target.sessionId
572
+ })
573
+ }
574
+
575
+ function maybeEmitBrowserDownloadCompatEvent({
576
+ method,
577
+ params,
578
+ extensionId,
579
+ }: {
580
+ method: string
581
+ params: unknown
582
+ extensionId: string
583
+ }): void {
584
+ const browserEventMethod =
585
+ method === 'Page.downloadWillBegin'
586
+ ? 'Browser.downloadWillBegin'
587
+ : method === 'Page.downloadProgress'
588
+ ? 'Browser.downloadProgress'
589
+ : null
590
+ if (!browserEventMethod) {
591
+ return
592
+ }
593
+ sendToPlaywright({
594
+ message: {
595
+ method: browserEventMethod,
596
+ params,
597
+ } as CDPEventBase,
598
+ source: 'server',
599
+ extensionId,
600
+ })
601
+ }
602
+
603
+ async function applyDownloadBehaviorToTargets({
604
+ extensionId,
605
+ behavior,
606
+ source,
607
+ targetSessionIds,
608
+ }: {
609
+ extensionId: string
610
+ behavior: Protocol.Browser.SetDownloadBehaviorRequest
611
+ source?: CDPCommand['source']
612
+ targetSessionIds?: string[]
613
+ }): Promise<void> {
614
+ const pageBehavior: Protocol.Page.SetDownloadBehaviorRequest['behavior'] =
615
+ behavior.behavior === 'allowAndName' ? 'allow' : behavior.behavior
616
+ const pageParams: Protocol.Page.SetDownloadBehaviorRequest = (() => {
617
+ if (pageBehavior === 'allow' && behavior.downloadPath) {
618
+ return { behavior: pageBehavior, downloadPath: behavior.downloadPath }
619
+ }
620
+ return { behavior: pageBehavior }
621
+ })()
622
+ const sessions = targetSessionIds || getPageTargetSessionIds({ extensionId })
623
+ if (sessions.length === 0) {
624
+ return
625
+ }
626
+ await Promise.all(
627
+ sessions.map(async (targetSessionId) => {
628
+ try {
629
+ await sendToExtension({
630
+ extensionId,
631
+ method: 'forwardCDPCommand',
632
+ params: {
633
+ sessionId: targetSessionId,
634
+ method: 'Page.setDownloadBehavior',
635
+ params: pageParams,
636
+ source,
637
+ },
638
+ })
639
+ } catch (error) {
640
+ const message = error instanceof Error ? error.message : String(error)
641
+ logger?.log(pc.yellow(`[Server] Failed to apply Page.setDownloadBehavior to ${targetSessionId}: ${message}`))
642
+ }
643
+ }),
644
+ )
645
+ }
646
+
560
647
  async function routeCdpCommand({
561
648
  extensionId,
562
649
  method,
@@ -585,6 +672,18 @@ export async function startPlayWriterCDPRelayServer({
585
672
  }
586
673
 
587
674
  case 'Browser.setDownloadBehavior': {
675
+ const downloadBehaviorParams = params as Protocol.Browser.SetDownloadBehaviorRequest | undefined
676
+ if (!downloadBehaviorParams?.behavior) {
677
+ throw new Error('behavior is required for Browser.setDownloadBehavior')
678
+ }
679
+ if (resolvedExtensionId) {
680
+ extensionDownloadBehavior.set(resolvedExtensionId, downloadBehaviorParams)
681
+ await applyDownloadBehaviorToTargets({
682
+ extensionId: resolvedExtensionId,
683
+ behavior: downloadBehaviorParams,
684
+ source,
685
+ })
686
+ }
588
687
  return {}
589
688
  }
590
689
 
@@ -1336,6 +1435,8 @@ export async function startPlayWriterCDPRelayServer({
1336
1435
  const cdpEvent: CDPEventBase = { method, sessionId, params }
1337
1436
  emitter.emit('cdp:event', { event: cdpEvent, sessionId })
1338
1437
 
1438
+ maybeEmitBrowserDownloadCompatEvent({ method, params, extensionId: connectionId })
1439
+
1339
1440
  if (method === 'Target.attachedToTarget') {
1340
1441
  const targetParams = params as Protocol.Target.AttachedToTargetEvent
1341
1442
  const incomingSessionId = sessionId
@@ -1396,6 +1497,15 @@ export async function startPlayWriterCDPRelayServer({
1396
1497
  }),
1397
1498
  )
1398
1499
 
1500
+ const cachedDownloadBehavior = extensionDownloadBehavior.get(connectionId)
1501
+ if (cachedDownloadBehavior && targetParams.targetInfo.type === 'page') {
1502
+ void applyDownloadBehaviorToTargets({
1503
+ extensionId: connectionId,
1504
+ behavior: cachedDownloadBehavior,
1505
+ targetSessionIds: [targetParams.sessionId],
1506
+ })
1507
+ }
1508
+
1399
1509
  // Only forward to Playwright if this is a new target to avoid duplicates
1400
1510
  if (!alreadyConnected) {
1401
1511
  sendToPlaywright({
package/src/executor.ts CHANGED
@@ -592,9 +592,10 @@ export class PlaywrightExecutor {
592
592
  const contexts = browser.contexts()
593
593
  const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
594
594
 
595
- // Action timeout (click, fill, hover, etc.) is short for fast agent failure.
596
- // Navigation timeout (goto, reload) is longer since page loads are slower.
597
- context.setDefaultTimeout(2000)
595
+ // Action timeout (click, fill, hover, etc.) is longer to tolerate slower
596
+ // SPA/Turbo navigations and post-click settling on real sites.
597
+ // Navigation timeout (goto, reload) remains separate.
598
+ context.setDefaultTimeout(60000)
598
599
  context.setDefaultNavigationTimeout(10000)
599
600
 
600
601
  context.on('page', (page) => {
@@ -674,9 +675,10 @@ export class PlaywrightExecutor {
674
675
  const contexts = browser.contexts()
675
676
  const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
676
677
 
677
- // Action timeout (click, fill, hover, etc.) is short for fast agent failure.
678
- // Navigation timeout (goto, reload) is longer since page loads are slower.
679
- context.setDefaultTimeout(2000)
678
+ // Action timeout (click, fill, hover, etc.) is longer to tolerate slower
679
+ // SPA/Turbo navigations and post-click settling on real sites.
680
+ // Navigation timeout (goto, reload) remains separate.
681
+ context.setDefaultTimeout(60000)
680
682
  context.setDefaultNavigationTimeout(10000)
681
683
 
682
684
  context.on('page', (page) => {
package/src/ffmpeg.ts CHANGED
@@ -14,8 +14,8 @@ import path from 'node:path'
14
14
  // Constants
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- /** Seconds of normal-speed buffer kept before and after each execution */
18
- export const INTERACTION_BUFFER_SECONDS = 1
17
+ /** Seconds of normal-speed buffer kept before and after each execution (0.5s each side = 1s total) */
18
+ export const INTERACTION_BUFFER_SECONDS = 0.5
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
21
  // Hardware encoder detection
@@ -635,13 +635,13 @@ export interface ExecutionTimestamp {
635
635
  export function computeIdleSections({
636
636
  executionTimestamps,
637
637
  totalDurationMs,
638
- speed = 5,
638
+ speed = 6,
639
639
  bufferSeconds = INTERACTION_BUFFER_SECONDS,
640
640
  }: {
641
641
  executionTimestamps: ExecutionTimestamp[]
642
642
  /** Total recording duration in milliseconds (from stopRecording result) */
643
643
  totalDurationMs: number
644
- /** Speed multiplier for idle sections (default 5) */
644
+ /** Speed multiplier for idle sections (default 6) */
645
645
  speed?: number
646
646
  /** Override the default buffer around each execution (seconds) */
647
647
  bufferSeconds?: number
@@ -711,7 +711,7 @@ export interface CreateDemoVideoOptions {
711
711
  durationMs: number
712
712
  /** Execution timestamps (from stopRecording result) */
713
713
  executionTimestamps: ExecutionTimestamp[]
714
- /** Speed multiplier for idle sections (default 5) */
714
+ /** Speed multiplier for idle sections (default 6) */
715
715
  speed?: number
716
716
  /** Output file path (defaults to recordingPath with `-demo` suffix) */
717
717
  outputFile?: string
@@ -722,8 +722,8 @@ export interface CreateDemoVideoOptions {
722
722
  * Create a demo video from a recording by speeding up idle sections
723
723
  * (gaps between execute() calls) while keeping interactions at normal speed.
724
724
  *
725
- * A 1-second buffer (INTERACTION_BUFFER_SECONDS) is preserved around each
726
- * interaction so viewers see context before and after each action.
725
+ * A 0.5-second buffer (INTERACTION_BUFFER_SECONDS) is preserved on each side of
726
+ * an interaction (1 second total) so viewers see context before and after each action.
727
727
  *
728
728
  * Requires `ffmpeg` and `ffprobe` installed on the system.
729
729
  *
@@ -736,7 +736,7 @@ export async function createDemoVideo(
736
736
  recordingPath,
737
737
  durationMs,
738
738
  executionTimestamps,
739
- speed = 5,
739
+ speed = 6,
740
740
  signal,
741
741
  } = options
742
742
 
@@ -247,7 +247,8 @@ function applyRuntimeVisualOptions(): void {
247
247
  }
248
248
 
249
249
  if (runtime.options.style === 'minimal') {
250
- const triangleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="${runtime.options.color}" d="m23.284 19.124l-6.866-6.895a.4.4 0 0 1-.118-.296a.43.43 0 0 1 .163-.282l4.439-3.077a1.48 1.48 0 0 0 .621-1.48a1.48 1.48 0 0 0-1.036-1.198L1.623.302a1.14 1.14 0 0 0-1.11.282A1.13 1.13 0 0 0 .29 1.649L5.928 20.44a1.48 1.48 0 0 0 1.183 1.035a1.48 1.48 0 0 0 1.48-.621l3.078-4.44a.37.37 0 0 1 .31-.118a.43.43 0 0 1 .296.104l6.91 6.91a1.48 1.48 0 0 0 2.087 0l2.086-2.086a1.48 1.48 0 0 0-.074-2.101"/></svg>`
250
+ // White fill with dark border stroke, like a standard macOS cursor
251
+ const triangleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="-1 -1 26 26"><path fill="white" stroke="${runtime.options.color}" stroke-width="1.5" stroke-linejoin="round" d="m23.284 19.124l-6.866-6.895a.4.4 0 0 1-.118-.296a.43.43 0 0 1 .163-.282l4.439-3.077a1.48 1.48 0 0 0 .621-1.48a1.48 1.48 0 0 0-1.036-1.198L1.623.302a1.14 1.14 0 0 0-1.11.282A1.13 1.13 0 0 0 .29 1.649L5.928 20.44a1.48 1.48 0 0 0 1.183 1.035a1.48 1.48 0 0 0 1.48-.621l3.078-4.44a.37.37 0 0 1 .31-.118a.43.43 0 0 1 .296.104l6.91 6.91a1.48 1.48 0 0 0 2.087 0l2.086-2.086a1.48 1.48 0 0 0-.074-2.101"/></svg>`
251
252
  const triangleDataUrl = `url("data:image/svg+xml,${encodeURIComponent(triangleSvg)}")`
252
253
  runtime.cursorElement.style.borderRadius = '0'
253
254
  runtime.cursorElement.style.border = 'none'
@@ -258,7 +259,7 @@ function applyRuntimeVisualOptions(): void {
258
259
  runtime.cursorElement.style.backgroundPosition = 'left top'
259
260
  runtime.cursorElement.style.backdropFilter = 'none'
260
261
  runtime.cursorElement.style.boxShadow = 'none'
261
- runtime.cursorElement.style.filter = 'drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3))'
262
+ runtime.cursorElement.style.filter = 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))'
262
263
  runtime.cursorElement.style.opacity = getBaseOpacity()
263
264
  return
264
265
  }
@@ -4,7 +4,12 @@
4
4
  */
5
5
 
6
6
  import type { BrowserContext, Page } from '@xmorse/playwright-core'
7
- import { applyGhostCursorMouseAction, disableGhostCursor, enableGhostCursor, type GhostCursorClientOptions } from './ghost-cursor.js'
7
+ import {
8
+ applyGhostCursorMouseAction,
9
+ disableGhostCursor,
10
+ enableGhostCursor,
11
+ type GhostCursorClientOptions,
12
+ } from './ghost-cursor.js'
8
13
 
9
14
  interface RecordingGhostCursorLogger {
10
15
  error: (...args: unknown[]) => void
@@ -53,6 +58,7 @@ export class RecordingGhostCursorController {
53
58
 
54
59
  try {
55
60
  await enableGhostCursor({ page })
61
+
56
62
  if (!this.previousMouseActionByPage.has(page)) {
57
63
  this.previousMouseActionByPage.set(page, page.onMouseAction)
58
64
  }
@@ -27,7 +27,7 @@ export type ExtensionStatus = {
27
27
  export async function getRelayServerVersion(port: number = RELAY_PORT): Promise<string | null> {
28
28
  try {
29
29
  const response = await fetch(`http://127.0.0.1:${port}/version`, {
30
- signal: AbortSignal.timeout(500),
30
+ signal: AbortSignal.timeout(2000),
31
31
  })
32
32
  if (!response.ok) {
33
33
  return null