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.
- package/dist/a11y-client.js +18 -8
- package/dist/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +134 -55
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +5 -2
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/aria-snapshot.unit.test.js +83 -41
- package/dist/aria-snapshot.unit.test.js.map +1 -1
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +1 -1
- package/dist/cdp-log.d.ts +1 -1
- package/dist/cdp-log.d.ts.map +1 -1
- package/dist/cdp-log.js +1 -1
- package/dist/cdp-log.js.map +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +492 -298
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js.map +1 -1
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js +7 -7
- package/dist/cdp-types.js.map +1 -1
- package/dist/clean-html.d.ts.map +1 -1
- package/dist/clean-html.js +4 -5
- package/dist/clean-html.js.map +1 -1
- package/dist/cli.js +45 -27
- package/dist/cli.js.map +1 -1
- package/dist/create-logger.d.ts.map +1 -1
- package/dist/create-logger.js +3 -1
- package/dist/create-logger.js.map +1 -1
- package/dist/debugger-examples-types.d.ts.map +1 -1
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +1 -3
- package/dist/debugger.js.map +1 -1
- package/dist/diff-utils.d.ts.map +1 -1
- package/dist/diff-utils.js +1 -4
- package/dist/diff-utils.js.map +1 -1
- package/dist/editor-api.md +12 -2
- package/dist/editor-examples.d.ts +1 -1
- package/dist/editor-examples.d.ts.map +1 -1
- package/dist/editor-examples.js +1 -1
- package/dist/editor-examples.js.map +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/executor.d.ts +26 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +297 -64
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +38 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension-connection.test.js +139 -36
- package/dist/extension-connection.test.js.map +1 -1
- package/dist/ffmpeg.d.ts +148 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +523 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/ghost-browser.d.ts.map +1 -1
- package/dist/ghost-browser.js.map +1 -1
- package/dist/ghost-cursor-client.js +287 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +63 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts.map +1 -1
- package/dist/htmlrewrite.js +17 -55
- package/dist/htmlrewrite.js.map +1 -1
- package/dist/htmlrewrite.test.js.map +1 -1
- package/dist/kill-port.d.ts.map +1 -1
- package/dist/kill-port.js +1 -3
- package/dist/kill-port.js.map +1 -1
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +96 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -3
- package/dist/mcp.js.map +1 -1
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +155 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/page-markdown.js +4 -4
- package/dist/page-markdown.js.map +1 -1
- package/dist/prompt.md +450 -377
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts +41 -0
- package/dist/recording-ghost-cursor.d.ts.map +1 -0
- package/dist/recording-ghost-cursor.js +79 -0
- package/dist/recording-ghost-cursor.js.map +1 -0
- package/dist/recording-relay.d.ts.map +1 -1
- package/dist/recording-relay.js +8 -8
- package/dist/recording-relay.js.map +1 -1
- package/dist/relay-client.d.ts +17 -4
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +45 -11
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +515 -26
- 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 +169 -31
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/relay-session.test.d.ts.map +1 -1
- package/dist/relay-session.test.js +113 -65
- package/dist/relay-session.test.js.map +1 -1
- package/dist/relay-state.d.ts +158 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +306 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +472 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js.map +1 -1
- package/dist/screen-recording.d.ts +66 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +150 -13
- 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/dist/snapshot-tools.test.js +71 -28
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/start-relay-server.d.ts +1 -1
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +1 -1
- package/dist/start-relay-server.js.map +1 -1
- package/dist/styles-api.md +8 -1
- package/dist/styles-examples.d.ts +1 -1
- package/dist/styles-examples.d.ts.map +1 -1
- package/dist/styles-examples.js +1 -1
- package/dist/styles-examples.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +1 -3
- package/dist/styles.js.map +1 -1
- package/dist/test-declarations.d.ts.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +7 -5
- package/dist/test-utils.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts.map +1 -1
- package/dist/wait-for-page-load.js +1 -1
- package/dist/wait-for-page-load.js.map +1 -1
- package/package.json +4 -3
- package/src/a11y-client.ts +5 -4
- package/src/aria-snapshot.test.ts +5 -2
- package/src/aria-snapshot.ts +306 -117
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +5 -1
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +265 -269
- package/src/assets/aria-labels-example.png +0 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/cdp-log.ts +4 -1
- package/src/cdp-relay.ts +1059 -737
- package/src/cdp-session.ts +12 -3
- package/src/cdp-types.ts +51 -51
- package/src/clean-html.ts +4 -5
- package/src/cli.ts +82 -55
- package/src/create-logger.ts +5 -3
- package/src/debugger-examples-types.ts +4 -1
- package/src/debugger.ts +1 -5
- package/src/diff-utils.ts +2 -5
- package/src/editor-examples.ts +11 -1
- package/src/editor.ts +10 -2
- package/src/executor.ts +374 -73
- package/src/executor.unit.test.ts +48 -1
- package/src/extension-connection.test.ts +612 -488
- package/src/ffmpeg.ts +769 -0
- package/src/ghost-browser.ts +4 -6
- package/src/ghost-cursor-client.ts +369 -0
- package/src/ghost-cursor.ts +110 -0
- package/src/htmlrewrite.test.ts +6 -2
- package/src/htmlrewrite.ts +348 -386
- package/src/kill-port.ts +1 -3
- package/src/locator-selector.test.ts +115 -0
- package/src/mcp-client.ts +1 -1
- package/src/mcp.ts +21 -15
- package/src/on-mouse-action.test.ts +196 -0
- package/src/page-markdown.ts +7 -7
- package/src/protocol.ts +73 -57
- package/src/recording-ghost-cursor.ts +113 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +85 -18
- package/src/relay-core.test.ts +1117 -578
- package/src/relay-navigation.test.ts +648 -483
- package/src/relay-session.test.ts +984 -929
- package/src/relay-state.test.ts +570 -0
- package/src/relay-state.ts +497 -0
- package/src/resource.md +21 -49
- package/src/scoped-fs.ts +9 -3
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +256 -31
- package/src/skill.md +476 -396
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
- package/src/start-relay-server.ts +14 -11
- package/src/styles-examples.ts +8 -1
- package/src/styles.ts +20 -21
- package/src/test-declarations.ts +6 -6
- package/src/test-utils.ts +104 -91
- package/src/utils.ts +2 -1
- package/src/wait-for-page-load.ts +6 -1
package/src/ghost-browser.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/htmlrewrite.test.ts
CHANGED
|
@@ -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(
|
|
7981
|
-
|
|
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')
|