playwriter 0.0.63 → 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 (223) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts +41 -3
  3. package/dist/aria-snapshot.d.ts.map +1 -1
  4. package/dist/aria-snapshot.js +134 -55
  5. package/dist/aria-snapshot.js.map +1 -1
  6. package/dist/aria-snapshot.test.js +5 -2
  7. package/dist/aria-snapshot.test.js.map +1 -1
  8. package/dist/aria-snapshot.unit.test.js +83 -41
  9. package/dist/aria-snapshot.unit.test.js.map +1 -1
  10. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  11. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  12. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  13. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  14. package/dist/bippy.js +1 -1
  15. package/dist/cdp-log.d.ts +1 -1
  16. package/dist/cdp-log.d.ts.map +1 -1
  17. package/dist/cdp-log.js +1 -1
  18. package/dist/cdp-log.js.map +1 -1
  19. package/dist/cdp-relay.d.ts.map +1 -1
  20. package/dist/cdp-relay.js +492 -298
  21. package/dist/cdp-relay.js.map +1 -1
  22. package/dist/cdp-session.d.ts.map +1 -1
  23. package/dist/cdp-session.js.map +1 -1
  24. package/dist/cdp-types.d.ts.map +1 -1
  25. package/dist/cdp-types.js +7 -7
  26. package/dist/cdp-types.js.map +1 -1
  27. package/dist/clean-html.d.ts.map +1 -1
  28. package/dist/clean-html.js +4 -5
  29. package/dist/clean-html.js.map +1 -1
  30. package/dist/cli.js +45 -27
  31. package/dist/cli.js.map +1 -1
  32. package/dist/create-logger.d.ts.map +1 -1
  33. package/dist/create-logger.js +3 -1
  34. package/dist/create-logger.js.map +1 -1
  35. package/dist/debugger-examples-types.d.ts.map +1 -1
  36. package/dist/debugger.d.ts.map +1 -1
  37. package/dist/debugger.js +1 -3
  38. package/dist/debugger.js.map +1 -1
  39. package/dist/diff-utils.d.ts.map +1 -1
  40. package/dist/diff-utils.js +1 -4
  41. package/dist/diff-utils.js.map +1 -1
  42. package/dist/editor-api.md +12 -2
  43. package/dist/editor-examples.d.ts +1 -1
  44. package/dist/editor-examples.d.ts.map +1 -1
  45. package/dist/editor-examples.js +1 -1
  46. package/dist/editor-examples.js.map +1 -1
  47. package/dist/editor.d.ts +1 -1
  48. package/dist/editor.d.ts.map +1 -1
  49. package/dist/editor.js +1 -1
  50. package/dist/editor.js.map +1 -1
  51. package/dist/executor.d.ts +26 -3
  52. package/dist/executor.d.ts.map +1 -1
  53. package/dist/executor.js +297 -64
  54. package/dist/executor.js.map +1 -1
  55. package/dist/executor.unit.test.js +38 -1
  56. package/dist/executor.unit.test.js.map +1 -1
  57. package/dist/extension-connection.test.js +139 -36
  58. package/dist/extension-connection.test.js.map +1 -1
  59. package/dist/ffmpeg.d.ts +148 -0
  60. package/dist/ffmpeg.d.ts.map +1 -0
  61. package/dist/ffmpeg.js +523 -0
  62. package/dist/ffmpeg.js.map +1 -0
  63. package/dist/ghost-browser.d.ts.map +1 -1
  64. package/dist/ghost-browser.js.map +1 -1
  65. package/dist/ghost-cursor-client.js +287 -0
  66. package/dist/ghost-cursor.d.ts +27 -0
  67. package/dist/ghost-cursor.d.ts.map +1 -0
  68. package/dist/ghost-cursor.js +63 -0
  69. package/dist/ghost-cursor.js.map +1 -0
  70. package/dist/htmlrewrite.d.ts.map +1 -1
  71. package/dist/htmlrewrite.js +17 -55
  72. package/dist/htmlrewrite.js.map +1 -1
  73. package/dist/htmlrewrite.test.js.map +1 -1
  74. package/dist/kill-port.d.ts.map +1 -1
  75. package/dist/kill-port.js +1 -3
  76. package/dist/kill-port.js.map +1 -1
  77. package/dist/locator-selector.test.d.ts +2 -0
  78. package/dist/locator-selector.test.d.ts.map +1 -0
  79. package/dist/locator-selector.test.js +96 -0
  80. package/dist/locator-selector.test.js.map +1 -0
  81. package/dist/mcp-client.js.map +1 -1
  82. package/dist/mcp.d.ts.map +1 -1
  83. package/dist/mcp.js +8 -3
  84. package/dist/mcp.js.map +1 -1
  85. package/dist/on-mouse-action.test.d.ts +2 -0
  86. package/dist/on-mouse-action.test.d.ts.map +1 -0
  87. package/dist/on-mouse-action.test.js +155 -0
  88. package/dist/on-mouse-action.test.js.map +1 -0
  89. package/dist/page-markdown.js +4 -4
  90. package/dist/page-markdown.js.map +1 -1
  91. package/dist/prompt.md +450 -377
  92. package/dist/protocol.d.ts +4 -0
  93. package/dist/protocol.d.ts.map +1 -1
  94. package/dist/readability.js +16 -2
  95. package/dist/recording-ghost-cursor.d.ts +41 -0
  96. package/dist/recording-ghost-cursor.d.ts.map +1 -0
  97. package/dist/recording-ghost-cursor.js +79 -0
  98. package/dist/recording-ghost-cursor.js.map +1 -0
  99. package/dist/recording-relay.d.ts.map +1 -1
  100. package/dist/recording-relay.js +8 -8
  101. package/dist/recording-relay.js.map +1 -1
  102. package/dist/relay-client.d.ts +17 -4
  103. package/dist/relay-client.d.ts.map +1 -1
  104. package/dist/relay-client.js +45 -11
  105. package/dist/relay-client.js.map +1 -1
  106. package/dist/relay-core.test.d.ts.map +1 -1
  107. package/dist/relay-core.test.js +515 -26
  108. package/dist/relay-core.test.js.map +1 -1
  109. package/dist/relay-navigation.test.d.ts.map +1 -1
  110. package/dist/relay-navigation.test.js +169 -31
  111. package/dist/relay-navigation.test.js.map +1 -1
  112. package/dist/relay-session.test.d.ts.map +1 -1
  113. package/dist/relay-session.test.js +113 -65
  114. package/dist/relay-session.test.js.map +1 -1
  115. package/dist/relay-state.d.ts +158 -0
  116. package/dist/relay-state.d.ts.map +1 -0
  117. package/dist/relay-state.js +306 -0
  118. package/dist/relay-state.js.map +1 -0
  119. package/dist/relay-state.test.d.ts +2 -0
  120. package/dist/relay-state.test.d.ts.map +1 -0
  121. package/dist/relay-state.test.js +472 -0
  122. package/dist/relay-state.test.js.map +1 -0
  123. package/dist/scoped-fs.d.ts.map +1 -1
  124. package/dist/scoped-fs.js.map +1 -1
  125. package/dist/screen-recording.d.ts +66 -4
  126. package/dist/screen-recording.d.ts.map +1 -1
  127. package/dist/screen-recording.js +150 -13
  128. package/dist/screen-recording.js.map +1 -1
  129. package/dist/screen-recording.test.d.ts +2 -0
  130. package/dist/screen-recording.test.d.ts.map +1 -0
  131. package/dist/screen-recording.test.js +102 -0
  132. package/dist/screen-recording.test.js.map +1 -0
  133. package/dist/selector-generator.js +1 -1
  134. package/dist/snapshot-tools.test.js +71 -28
  135. package/dist/snapshot-tools.test.js.map +1 -1
  136. package/dist/start-relay-server.d.ts +1 -1
  137. package/dist/start-relay-server.d.ts.map +1 -1
  138. package/dist/start-relay-server.js +1 -1
  139. package/dist/start-relay-server.js.map +1 -1
  140. package/dist/styles-api.md +8 -1
  141. package/dist/styles-examples.d.ts +1 -1
  142. package/dist/styles-examples.d.ts.map +1 -1
  143. package/dist/styles-examples.js +1 -1
  144. package/dist/styles-examples.js.map +1 -1
  145. package/dist/styles.d.ts.map +1 -1
  146. package/dist/styles.js +1 -3
  147. package/dist/styles.js.map +1 -1
  148. package/dist/test-declarations.d.ts.map +1 -1
  149. package/dist/test-utils.d.ts +1 -1
  150. package/dist/test-utils.d.ts.map +1 -1
  151. package/dist/test-utils.js +7 -5
  152. package/dist/test-utils.js.map +1 -1
  153. package/dist/utils.d.ts.map +1 -1
  154. package/dist/utils.js.map +1 -1
  155. package/dist/wait-for-page-load.d.ts.map +1 -1
  156. package/dist/wait-for-page-load.js +1 -1
  157. package/dist/wait-for-page-load.js.map +1 -1
  158. package/package.json +4 -3
  159. package/src/a11y-client.ts +5 -4
  160. package/src/aria-snapshot.test.ts +5 -2
  161. package/src/aria-snapshot.ts +306 -117
  162. package/src/aria-snapshot.unit.test.ts +199 -141
  163. package/src/aria-snapshots/github-interactive.txt +2 -0
  164. package/src/aria-snapshots/github-raw.txt +5 -1
  165. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  166. package/src/aria-snapshots/hackernews-raw.txt +265 -269
  167. package/src/assets/aria-labels-example.png +0 -0
  168. package/src/assets/aria-labels-github.png +0 -0
  169. package/src/assets/aria-labels-hacker-news.png +0 -0
  170. package/src/assets/aria-labels-old-reddit.png +0 -0
  171. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  172. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  173. package/src/cdp-log.ts +4 -1
  174. package/src/cdp-relay.ts +1059 -737
  175. package/src/cdp-session.ts +12 -3
  176. package/src/cdp-types.ts +51 -51
  177. package/src/clean-html.ts +4 -5
  178. package/src/cli.ts +82 -55
  179. package/src/create-logger.ts +5 -3
  180. package/src/debugger-examples-types.ts +4 -1
  181. package/src/debugger.ts +1 -5
  182. package/src/diff-utils.ts +2 -5
  183. package/src/editor-examples.ts +11 -1
  184. package/src/editor.ts +10 -2
  185. package/src/executor.ts +374 -73
  186. package/src/executor.unit.test.ts +48 -1
  187. package/src/extension-connection.test.ts +612 -488
  188. package/src/ffmpeg.ts +769 -0
  189. package/src/ghost-browser.ts +4 -6
  190. package/src/ghost-cursor-client.ts +369 -0
  191. package/src/ghost-cursor.ts +110 -0
  192. package/src/htmlrewrite.test.ts +6 -2
  193. package/src/htmlrewrite.ts +348 -386
  194. package/src/kill-port.ts +1 -3
  195. package/src/locator-selector.test.ts +115 -0
  196. package/src/mcp-client.ts +1 -1
  197. package/src/mcp.ts +21 -15
  198. package/src/on-mouse-action.test.ts +196 -0
  199. package/src/page-markdown.ts +7 -7
  200. package/src/protocol.ts +73 -57
  201. package/src/recording-ghost-cursor.ts +113 -0
  202. package/src/recording-relay.ts +20 -12
  203. package/src/relay-client.ts +85 -18
  204. package/src/relay-core.test.ts +1117 -578
  205. package/src/relay-navigation.test.ts +648 -483
  206. package/src/relay-session.test.ts +984 -929
  207. package/src/relay-state.test.ts +570 -0
  208. package/src/relay-state.ts +497 -0
  209. package/src/resource.md +21 -49
  210. package/src/scoped-fs.ts +9 -3
  211. package/src/screen-recording.test.ts +111 -0
  212. package/src/screen-recording.ts +256 -31
  213. package/src/skill.md +476 -396
  214. package/src/snapshot-tools.test.ts +580 -528
  215. package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
  216. package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
  217. package/src/start-relay-server.ts +14 -11
  218. package/src/styles-examples.ts +8 -1
  219. package/src/styles.ts +20 -21
  220. package/src/test-declarations.ts +6 -6
  221. package/src/test-utils.ts +104 -91
  222. package/src/utils.ts +2 -1
  223. package/src/wait-for-page-load.ts +6 -1
@@ -40,9 +40,7 @@ export type GhostBrowserCommandParams = {
40
40
  args: unknown[]
41
41
  }
42
42
 
43
- export type GhostBrowserCommandResult =
44
- | { success: true; result: unknown }
45
- | { success: false; error: string }
43
+ export type GhostBrowserCommandResult = { success: true; result: unknown } | { success: false; error: string }
46
44
 
47
45
  /**
48
46
  * Function signature for sending ghost-browser commands.
@@ -52,7 +50,7 @@ export type GhostBrowserCommandResult =
52
50
  export type SendGhostBrowserCommand = (
53
51
  namespace: GhostBrowserNamespace,
54
52
  method: string,
55
- args: unknown[]
53
+ args: unknown[],
56
54
  ) => Promise<unknown>
57
55
 
58
56
  // =============================================================================
@@ -66,7 +64,7 @@ export type SendGhostBrowserCommand = (
66
64
  function createGhostBrowserProxy(
67
65
  namespace: GhostBrowserNamespace,
68
66
  constants: Record<string, unknown>,
69
- sendCommand: SendGhostBrowserCommand
67
+ sendCommand: SendGhostBrowserCommand,
70
68
  ) {
71
69
  return new Proxy(constants, {
72
70
  get(target, prop: string) {
@@ -108,7 +106,7 @@ export function createGhostBrowserChrome(sendCommand: SendGhostBrowserCommand) {
108
106
  */
109
107
  export async function handleGhostBrowserCommand(
110
108
  params: GhostBrowserCommandParams,
111
- chromeApi: typeof chrome
109
+ chromeApi: typeof chrome,
112
110
  ): Promise<GhostBrowserCommandResult> {
113
111
  const { namespace, method, args } = params
114
112
 
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Browser-side ghost cursor renderer.
3
+ * Injected into the page to visualize automated mouse actions with smooth easing.
4
+ */
5
+
6
+ import { SCREENSTUDIO_POINTER_MACOS_TAHOE_DATA_URL } from './assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js'
7
+
8
+ type GhostCursorActionType = 'move' | 'down' | 'up' | 'wheel'
9
+ type GhostCursorButton = 'left' | 'right' | 'middle' | 'none'
10
+ type GhostCursorStyle = 'minimal' | 'dot' | 'screenstudio'
11
+
12
+ interface GhostCursorAction {
13
+ type: GhostCursorActionType
14
+ x: number
15
+ y: number
16
+ button: GhostCursorButton
17
+ }
18
+
19
+ export interface GhostCursorClientOptions {
20
+ style?: GhostCursorStyle
21
+ color?: string
22
+ size?: number
23
+ zIndex?: number
24
+ easing?: string
25
+ minDurationMs?: number
26
+ maxDurationMs?: number
27
+ speedPxPerMs?: number
28
+ }
29
+
30
+ interface GhostCursorRuntimeOptions {
31
+ style: GhostCursorStyle
32
+ color: string
33
+ size: number
34
+ zIndex: number
35
+ easing: string
36
+ minDurationMs: number
37
+ maxDurationMs: number
38
+ speedPxPerMs: number
39
+ }
40
+
41
+ interface GhostCursorRuntimeState {
42
+ cursorElement: ReturnType<typeof createCursorElement> | null
43
+ options: GhostCursorRuntimeOptions
44
+ x: number
45
+ y: number
46
+ scale: number
47
+ hasPosition: boolean
48
+ enabled: boolean
49
+ }
50
+
51
+ interface GhostCursorApi {
52
+ enable: (options?: GhostCursorClientOptions) => void
53
+ disable: () => void
54
+ applyMouseAction: (action: GhostCursorAction) => void
55
+ isEnabled: () => boolean
56
+ }
57
+
58
+ declare global {
59
+ var __playwriterGhostCursor: GhostCursorApi | undefined
60
+ }
61
+
62
+ const CURSOR_ID = '__playwriter_ghost_cursor__'
63
+ const SCREENSTUDIO_POINTER_ASPECT_RATIO = 618 / 958
64
+ const SCREENSTUDIO_HOTSPOT_X_RATIO = 0.14
65
+ const SCREENSTUDIO_HOTSPOT_Y_RATIO = 0.06
66
+ const MINIMAL_TRIANGLE_HOTSPOT_X_RATIO = 0.07
67
+ const MINIMAL_TRIANGLE_HOTSPOT_Y_RATIO = 0.06
68
+
69
+ const DEFAULT_OPTIONS: GhostCursorRuntimeOptions = {
70
+ style: 'minimal',
71
+ color: '#111827',
72
+ size: 22,
73
+ zIndex: 2147483647,
74
+ easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
75
+ minDurationMs: 24,
76
+ maxDurationMs: 320,
77
+ speedPxPerMs: 4,
78
+ }
79
+
80
+ const runtime: GhostCursorRuntimeState = {
81
+ cursorElement: null,
82
+ options: DEFAULT_OPTIONS,
83
+ x: 0,
84
+ y: 0,
85
+ scale: 1,
86
+ hasPosition: false,
87
+ enabled: false,
88
+ }
89
+
90
+ function clamp(options: { value: number; min: number; max: number }): number {
91
+ const { value, min, max } = options
92
+ return Math.min(max, Math.max(min, value))
93
+ }
94
+
95
+ function mergeOptions(options?: GhostCursorClientOptions): GhostCursorRuntimeOptions {
96
+ if (!options) {
97
+ return DEFAULT_OPTIONS
98
+ }
99
+
100
+ return {
101
+ style: options.style ?? DEFAULT_OPTIONS.style,
102
+ color: options.color ?? DEFAULT_OPTIONS.color,
103
+ size: options.size ?? DEFAULT_OPTIONS.size,
104
+ zIndex: options.zIndex ?? DEFAULT_OPTIONS.zIndex,
105
+ easing: options.easing ?? DEFAULT_OPTIONS.easing,
106
+ minDurationMs: options.minDurationMs ?? DEFAULT_OPTIONS.minDurationMs,
107
+ maxDurationMs: options.maxDurationMs ?? DEFAULT_OPTIONS.maxDurationMs,
108
+ speedPxPerMs: options.speedPxPerMs ?? DEFAULT_OPTIONS.speedPxPerMs,
109
+ }
110
+ }
111
+
112
+ function getCursorDimensions(): { width: number; height: number } {
113
+ if (runtime.options.style === 'screenstudio') {
114
+ const height = runtime.options.size
115
+ const width = Math.max(10, Math.round(height * SCREENSTUDIO_POINTER_ASPECT_RATIO))
116
+ return { width, height }
117
+ }
118
+
119
+ if (runtime.options.style === 'minimal') {
120
+ const size = Math.max(12, runtime.options.size)
121
+ return { width: size, height: size }
122
+ }
123
+
124
+ return { width: runtime.options.size, height: runtime.options.size }
125
+ }
126
+
127
+ function getHotspotOffsetPx(): { x: number; y: number } {
128
+ const dimensions = getCursorDimensions()
129
+
130
+ if (runtime.options.style === 'screenstudio') {
131
+ return {
132
+ x: Math.round(dimensions.width * SCREENSTUDIO_HOTSPOT_X_RATIO),
133
+ y: Math.round(dimensions.height * SCREENSTUDIO_HOTSPOT_Y_RATIO),
134
+ }
135
+ }
136
+
137
+ if (runtime.options.style === 'minimal') {
138
+ return {
139
+ x: Math.round(dimensions.width * MINIMAL_TRIANGLE_HOTSPOT_X_RATIO),
140
+ y: Math.round(dimensions.height * MINIMAL_TRIANGLE_HOTSPOT_Y_RATIO),
141
+ }
142
+ }
143
+
144
+ return {
145
+ x: Math.round(dimensions.width / 2),
146
+ y: Math.round(dimensions.height / 2),
147
+ }
148
+ }
149
+
150
+ function getBaseOpacity(): string {
151
+ if (runtime.options.style === 'screenstudio') {
152
+ return '0.95'
153
+ }
154
+
155
+ if (runtime.options.style === 'minimal') {
156
+ return '1'
157
+ }
158
+
159
+ return '0.72'
160
+ }
161
+
162
+ function applyTransform(): void {
163
+ if (!runtime.cursorElement) {
164
+ return
165
+ }
166
+
167
+ const hotspot = getHotspotOffsetPx()
168
+ runtime.cursorElement.style.transform = `translate3d(${runtime.x - hotspot.x}px, ${runtime.y - hotspot.y}px, 0) scale(${runtime.scale})`
169
+ }
170
+
171
+ function computeDurationMs(options: { targetX: number; targetY: number }): number {
172
+ if (!runtime.hasPosition) {
173
+ return 0
174
+ }
175
+
176
+ const dx = options.targetX - runtime.x
177
+ const dy = options.targetY - runtime.y
178
+ const distance = Math.hypot(dx, dy)
179
+ const rawDurationMs = distance / runtime.options.speedPxPerMs
180
+
181
+ return clamp({
182
+ value: rawDurationMs,
183
+ min: runtime.options.minDurationMs,
184
+ max: runtime.options.maxDurationMs,
185
+ })
186
+ }
187
+
188
+ function createCursorElement() {
189
+ const element = document.createElement('div')
190
+ element.id = CURSOR_ID
191
+ element.setAttribute('aria-hidden', 'true')
192
+ element.style.position = 'fixed'
193
+ element.style.left = '0'
194
+ element.style.top = '0'
195
+ element.style.pointerEvents = 'none'
196
+ element.style.zIndex = `${runtime.options.zIndex}`
197
+ element.style.opacity = getBaseOpacity()
198
+ element.style.transitionProperty = 'transform, opacity'
199
+ element.style.transitionTimingFunction = runtime.options.easing
200
+ element.style.transitionDuration = '0ms'
201
+ element.style.willChange = 'transform'
202
+
203
+ runtime.cursorElement = element
204
+ applyRuntimeVisualOptions()
205
+
206
+ return element
207
+ }
208
+
209
+ function ensureCursorElement() {
210
+ const existing = document.getElementById(CURSOR_ID)
211
+ if (existing) {
212
+ runtime.cursorElement = existing
213
+ return existing
214
+ }
215
+
216
+ const element = createCursorElement()
217
+ runtime.cursorElement = element
218
+ const root = document.documentElement || document.body
219
+ root.appendChild(element)
220
+ return element
221
+ }
222
+
223
+ function applyRuntimeVisualOptions(): void {
224
+ if (!runtime.cursorElement) {
225
+ return
226
+ }
227
+
228
+ const dimensions = getCursorDimensions()
229
+ runtime.cursorElement.style.width = `${dimensions.width}px`
230
+ runtime.cursorElement.style.height = `${dimensions.height}px`
231
+ runtime.cursorElement.style.zIndex = `${runtime.options.zIndex}`
232
+ runtime.cursorElement.style.transitionTimingFunction = runtime.options.easing
233
+
234
+ if (runtime.options.style === 'screenstudio') {
235
+ runtime.cursorElement.style.borderRadius = '0'
236
+ runtime.cursorElement.style.border = 'none'
237
+ runtime.cursorElement.style.backgroundColor = 'transparent'
238
+ runtime.cursorElement.style.backgroundImage = `url("${SCREENSTUDIO_POINTER_MACOS_TAHOE_DATA_URL}")`
239
+ runtime.cursorElement.style.backgroundRepeat = 'no-repeat'
240
+ runtime.cursorElement.style.backgroundPosition = 'left top'
241
+ runtime.cursorElement.style.backgroundSize = 'contain'
242
+ runtime.cursorElement.style.backdropFilter = 'none'
243
+ runtime.cursorElement.style.filter = 'none'
244
+ runtime.cursorElement.style.boxShadow = 'none'
245
+ runtime.cursorElement.style.opacity = getBaseOpacity()
246
+ return
247
+ }
248
+
249
+ if (runtime.options.style === 'minimal') {
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>`
252
+ const triangleDataUrl = `url("data:image/svg+xml,${encodeURIComponent(triangleSvg)}")`
253
+ runtime.cursorElement.style.borderRadius = '0'
254
+ runtime.cursorElement.style.border = 'none'
255
+ runtime.cursorElement.style.backgroundColor = 'transparent'
256
+ runtime.cursorElement.style.backgroundImage = triangleDataUrl
257
+ runtime.cursorElement.style.backgroundRepeat = 'no-repeat'
258
+ runtime.cursorElement.style.backgroundSize = 'contain'
259
+ runtime.cursorElement.style.backgroundPosition = 'left top'
260
+ runtime.cursorElement.style.backdropFilter = 'none'
261
+ runtime.cursorElement.style.boxShadow = 'none'
262
+ runtime.cursorElement.style.filter = 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))'
263
+ runtime.cursorElement.style.opacity = getBaseOpacity()
264
+ return
265
+ }
266
+
267
+ runtime.cursorElement.style.borderRadius = '999px'
268
+ runtime.cursorElement.style.border = 'none'
269
+ runtime.cursorElement.style.backgroundColor = runtime.options.color
270
+ runtime.cursorElement.style.backgroundImage = 'none'
271
+ runtime.cursorElement.style.backdropFilter = 'none'
272
+ runtime.cursorElement.style.filter = 'none'
273
+ runtime.cursorElement.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.18), inset 0 0 0 2px rgba(255, 255, 255, 0.55)'
274
+ runtime.cursorElement.style.opacity = getBaseOpacity()
275
+ }
276
+
277
+ function moveCursor(options: { x: number; y: number }): void {
278
+ if (!runtime.enabled) {
279
+ return
280
+ }
281
+
282
+ const element = ensureCursorElement()
283
+ const durationMs = computeDurationMs({ targetX: options.x, targetY: options.y })
284
+ element.style.transitionDuration = `${Math.round(durationMs)}ms`
285
+
286
+ runtime.x = options.x
287
+ runtime.y = options.y
288
+ runtime.hasPosition = true
289
+ applyTransform()
290
+ }
291
+
292
+ function setPressed(options: { pressed: boolean }): void {
293
+ if (!runtime.enabled) {
294
+ return
295
+ }
296
+
297
+ const element = ensureCursorElement()
298
+ runtime.scale = options.pressed
299
+ ? runtime.options.style === 'screenstudio'
300
+ ? 0.94
301
+ : runtime.options.style === 'minimal'
302
+ ? 0.93
303
+ : 0.82
304
+ : 1
305
+ element.style.opacity = options.pressed ? '1' : getBaseOpacity()
306
+ applyTransform()
307
+ }
308
+
309
+ function enable(options?: GhostCursorClientOptions): void {
310
+ runtime.options = mergeOptions(options)
311
+ runtime.enabled = true
312
+ ensureCursorElement()
313
+ applyRuntimeVisualOptions()
314
+
315
+ if (!runtime.hasPosition) {
316
+ runtime.x = Math.round(window.innerWidth / 2)
317
+ runtime.y = Math.round(window.innerHeight / 2)
318
+ runtime.scale = 1
319
+ runtime.hasPosition = true
320
+ }
321
+
322
+ applyTransform()
323
+ }
324
+
325
+ function disable(): void {
326
+ runtime.enabled = false
327
+ runtime.scale = 1
328
+ runtime.hasPosition = false
329
+
330
+ if (runtime.cursorElement) {
331
+ runtime.cursorElement.remove()
332
+ runtime.cursorElement = null
333
+ }
334
+ }
335
+
336
+ function applyMouseAction(action: GhostCursorAction): void {
337
+ if (!runtime.enabled) {
338
+ return
339
+ }
340
+
341
+ if (action.type === 'move' || action.type === 'wheel') {
342
+ moveCursor({ x: action.x, y: action.y })
343
+ return
344
+ }
345
+
346
+ if (action.type === 'down') {
347
+ moveCursor({ x: action.x, y: action.y })
348
+ setPressed({ pressed: true })
349
+ return
350
+ }
351
+
352
+ if (action.type === 'up') {
353
+ moveCursor({ x: action.x, y: action.y })
354
+ setPressed({ pressed: false })
355
+ }
356
+ }
357
+
358
+ const api: GhostCursorApi = {
359
+ enable,
360
+ disable,
361
+ applyMouseAction,
362
+ isEnabled: () => {
363
+ return runtime.enabled
364
+ },
365
+ }
366
+
367
+ globalThis.__playwriterGhostCursor = api
368
+
369
+ export {}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Node-side ghost cursor helpers.
3
+ * Injects the browser bundle and forwards mouse action events to the page overlay.
4
+ */
5
+
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import type { Page, MouseActionEvent } from '@xmorse/playwright-core'
10
+
11
+ export interface GhostCursorClientOptions {
12
+ style?: 'minimal' | 'dot' | 'screenstudio'
13
+ color?: string
14
+ size?: number
15
+ zIndex?: number
16
+ easing?: string
17
+ minDurationMs?: number
18
+ maxDurationMs?: number
19
+ speedPxPerMs?: number
20
+ }
21
+
22
+ interface GhostCursorBrowserApi {
23
+ enable: (options?: GhostCursorClientOptions) => void
24
+ disable: () => void
25
+ applyMouseAction: (event: MouseActionEvent) => void
26
+ }
27
+
28
+ let ghostCursorCode: string | null = null
29
+
30
+ function getGhostCursorCode(): string {
31
+ if (ghostCursorCode) {
32
+ return ghostCursorCode
33
+ }
34
+
35
+ const currentDir = path.dirname(fileURLToPath(import.meta.url))
36
+ const bundlePath = path.join(currentDir, '..', 'dist', 'ghost-cursor-client.js')
37
+ ghostCursorCode = fs.readFileSync(bundlePath, 'utf-8')
38
+ return ghostCursorCode
39
+ }
40
+
41
+ async function ensureGhostCursorInjected(options: { page: Page }): Promise<void> {
42
+ const { page } = options
43
+ const hasGhostCursor = await page.evaluate(() => {
44
+ return Boolean((globalThis as { __playwriterGhostCursor?: unknown }).__playwriterGhostCursor)
45
+ })
46
+
47
+ if (hasGhostCursor) {
48
+ return
49
+ }
50
+
51
+ const code = getGhostCursorCode()
52
+ await page.evaluate(code)
53
+ }
54
+
55
+ export async function enableGhostCursor(options: {
56
+ page: Page
57
+ cursorOptions?: GhostCursorClientOptions
58
+ }): Promise<void> {
59
+ const { page, cursorOptions } = options
60
+ await ensureGhostCursorInjected({ page })
61
+
62
+ await page.evaluate(
63
+ ({ optionsFromNode }) => {
64
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
65
+ api?.enable(optionsFromNode)
66
+ },
67
+ { optionsFromNode: cursorOptions },
68
+ )
69
+ }
70
+
71
+ export async function disableGhostCursor(options: { page: Page }): Promise<void> {
72
+ const { page } = options
73
+ await page.evaluate(() => {
74
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
75
+ api?.disable()
76
+ })
77
+ }
78
+
79
+ export async function applyGhostCursorMouseAction(options: {
80
+ page: Page
81
+ event: MouseActionEvent
82
+ }): Promise<void> {
83
+ const { page, event } = options
84
+
85
+ const applied = await page.evaluate(
86
+ ({ serializedEvent }) => {
87
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
88
+ if (!api) {
89
+ return false
90
+ }
91
+
92
+ api.applyMouseAction(serializedEvent)
93
+ return true
94
+ },
95
+ { serializedEvent: event },
96
+ )
97
+
98
+ if (applied) {
99
+ return
100
+ }
101
+
102
+ await ensureGhostCursorInjected({ page })
103
+ await page.evaluate(
104
+ ({ serializedEvent }) => {
105
+ const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
106
+ api?.applyMouseAction(serializedEvent)
107
+ },
108
+ { serializedEvent: event },
109
+ )
110
+ }
@@ -7977,8 +7977,12 @@ test('processes x.com.html with size savings', async () => {
7977
7977
 
7978
7978
  console.log(`\nšŸ“Š x.com.html processing stats:`)
7979
7979
  console.log(` Original: ${originalSize.toLocaleString()} chars (${originalTokens.toLocaleString()} tokens)`)
7980
- console.log(` Without styles: ${processedSize.toLocaleString()} chars (${processedTokens.toLocaleString()} tokens) - ${savingsPercent}% savings`)
7981
- console.log(` With styles: ${withStylesSize.toLocaleString()} chars (${withStylesTokens.toLocaleString()} tokens) - ${withStylesPercent}% savings`)
7980
+ console.log(
7981
+ ` Without styles: ${processedSize.toLocaleString()} chars (${processedTokens.toLocaleString()} tokens) - ${savingsPercent}% savings`,
7982
+ )
7983
+ console.log(
7984
+ ` With styles: ${withStylesSize.toLocaleString()} chars (${withStylesTokens.toLocaleString()} tokens) - ${withStylesPercent}% savings`,
7985
+ )
7982
7986
 
7983
7987
  await expect(result).toMatchFileSnapshot('./__snapshots__/x.com.processed.html')
7984
7988
  await expect(resultWithStyles).toMatchFileSnapshot('./__snapshots__/x.com.processed.withStyles.html')