fix: revert ghost cursor init-script persistence path
This reverts the addGhostCursorInitScript approach after it proved unreliable in real usage. What changed:\n- remove addGhostCursorInitScript/removeGhostCursorInitScript and related CDP init-script wiring\n- restore direct per-page ghost cursor injection fallback in applyGhostCursorMouseAction\n- remove recording controller state/cleanup tied to init-script identifiers\n- remove skill.md claim that ghost cursor persists automatically across MPA navigations\n- bump playwriter version to 0.0.85 and document the revert in changelog
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Revert ghost cursor init-script persistence**: Removed the `Page.addScriptToEvaluateOnNewDocument` cursor injection path (`addGhostCursorInitScript`) and restored direct per-page injection behavior used before, since the init-script approach did not work reliably in practice.
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Docs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "playwriter",
|
||||
"description": "",
|
||||
"version": "0.0.84",
|
||||
"version": "0.0.85",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
/**
|
||||
* Node-side ghost cursor helpers.
|
||||
* Injects the browser bundle and forwards mouse action events to the page overlay.
|
||||
*
|
||||
* Two injection strategies:
|
||||
* 1. One-shot: page.evaluate() — used by enableGhostCursor() for single pages
|
||||
* 2. Persistent: Page.addScriptToEvaluateOnNewDocument — used by
|
||||
* addGhostCursorInitScript() so the cursor survives MPA navigations.
|
||||
* Chrome re-runs the script on every new document automatically.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Page, MouseActionEvent } from '@xmorse/playwright-core'
|
||||
import { getCDPSessionForPage, type ICDPSession } from './cdp-session.js'
|
||||
|
||||
export interface GhostCursorClientOptions {
|
||||
style?: 'minimal' | 'dot' | 'screenstudio'
|
||||
@@ -30,7 +23,6 @@ interface GhostCursorBrowserApi {
|
||||
enable: (options?: GhostCursorClientOptions) => void
|
||||
disable: () => void
|
||||
applyMouseAction: (event: MouseActionEvent) => void
|
||||
isEnabled: () => boolean
|
||||
}
|
||||
|
||||
let ghostCursorCode: string | null = null
|
||||
@@ -84,84 +76,34 @@ export async function disableGhostCursor(options: { page: Page }): Promise<void>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the ghost cursor bundle as a persistent init script via CDP
|
||||
* Page.addScriptToEvaluateOnNewDocument. Chrome re-runs this on every new
|
||||
* document (navigation), so the cursor survives MPA page loads.
|
||||
*
|
||||
* The script auto-enables when the DOM is ready (DOMContentLoaded or
|
||||
* requestAnimationFrame fallback) so the cursor is visible immediately
|
||||
* on each new page.
|
||||
*
|
||||
* Returns the CDP identifier needed to remove it later.
|
||||
*/
|
||||
export async function addGhostCursorInitScript(options: {
|
||||
page: Page
|
||||
cursorOptions?: GhostCursorClientOptions
|
||||
}): Promise<{ cdp: ICDPSession; identifier: string }> {
|
||||
const { page, cursorOptions } = options
|
||||
const cdp = await getCDPSessionForPage({ page })
|
||||
const code = getGhostCursorCode()
|
||||
|
||||
// Wrap the bundle: inject, then auto-enable once DOM is ready.
|
||||
// DOMContentLoaded may have already fired if the script runs late,
|
||||
// so check document.readyState first.
|
||||
const optionsJson = JSON.stringify(cursorOptions ?? {})
|
||||
const wrappedCode = `
|
||||
${code}
|
||||
;(function() {
|
||||
function autoEnable() {
|
||||
var api = globalThis.__playwriterGhostCursor;
|
||||
if (api) {
|
||||
var opts = ${optionsJson};
|
||||
api.enable(Object.keys(opts).length ? opts : undefined);
|
||||
}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', autoEnable, { once: true });
|
||||
} else {
|
||||
requestAnimationFrame(autoEnable);
|
||||
}
|
||||
})();
|
||||
`
|
||||
|
||||
const result = await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: wrappedCode,
|
||||
})
|
||||
|
||||
return { cdp, identifier: result.identifier }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a previously registered ghost cursor init script.
|
||||
*/
|
||||
export async function removeGhostCursorInitScript(options: {
|
||||
cdp: ICDPSession
|
||||
identifier: string
|
||||
}): Promise<void> {
|
||||
const { cdp, identifier } = options
|
||||
await cdp.send('Page.removeScriptToEvaluateOnNewDocument', { identifier })
|
||||
}
|
||||
|
||||
export async function applyGhostCursorMouseAction(options: {
|
||||
page: Page
|
||||
event: MouseActionEvent
|
||||
}): Promise<void> {
|
||||
const { page, event } = options
|
||||
|
||||
await page.evaluate(
|
||||
const applied = await page.evaluate(
|
||||
({ serializedEvent }) => {
|
||||
const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
|
||||
if (!api) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// Ensure cursor is enabled (may be a freshly injected init script
|
||||
// where DOMContentLoaded hasn't fired yet)
|
||||
if (!api.isEnabled()) {
|
||||
api.enable()
|
||||
}
|
||||
api.applyMouseAction(serializedEvent)
|
||||
return true
|
||||
},
|
||||
{ serializedEvent: event },
|
||||
)
|
||||
|
||||
if (applied) {
|
||||
return
|
||||
}
|
||||
|
||||
await ensureGhostCursorInjected({ page })
|
||||
await page.evaluate(
|
||||
({ serializedEvent }) => {
|
||||
const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
|
||||
api?.applyMouseAction(serializedEvent)
|
||||
},
|
||||
{ serializedEvent: event },
|
||||
)
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
/**
|
||||
* Encapsulates ghost cursor lifecycle for recording sessions.
|
||||
* Keeps onMouseAction chaining/restoration isolated from executor logic.
|
||||
*
|
||||
* Uses Page.addScriptToEvaluateOnNewDocument so the cursor persists across
|
||||
* MPA navigations — Chrome re-runs the init script on every new document.
|
||||
*/
|
||||
|
||||
import type { BrowserContext, Page } from '@xmorse/playwright-core'
|
||||
import type { ICDPSession } from './cdp-session.js'
|
||||
import {
|
||||
addGhostCursorInitScript,
|
||||
applyGhostCursorMouseAction,
|
||||
disableGhostCursor,
|
||||
enableGhostCursor,
|
||||
removeGhostCursorInitScript,
|
||||
type GhostCursorClientOptions,
|
||||
} from './ghost-cursor.js'
|
||||
|
||||
@@ -26,15 +20,9 @@ interface RecordingTargetOptions {
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
interface InitScriptHandle {
|
||||
cdp: ICDPSession
|
||||
identifier: string
|
||||
}
|
||||
|
||||
export class RecordingGhostCursorController {
|
||||
private readonly previousMouseActionByPage = new WeakMap<Page, Page['onMouseAction']>()
|
||||
private readonly cursorApplyQueueByPage = new WeakMap<Page, Promise<void>>()
|
||||
private readonly initScriptByPage = new WeakMap<Page, InitScriptHandle>()
|
||||
private readonly logger: RecordingGhostCursorLogger
|
||||
|
||||
constructor(options: { logger: RecordingGhostCursorLogger }) {
|
||||
@@ -69,12 +57,6 @@ export class RecordingGhostCursorController {
|
||||
const { page } = options
|
||||
|
||||
try {
|
||||
// Register persistent init script so cursor survives MPA navigations
|
||||
const handle = await addGhostCursorInitScript({ page })
|
||||
this.initScriptByPage.set(page, handle)
|
||||
|
||||
// Also enable on the current page immediately (init script only runs
|
||||
// on future navigations, not the current document)
|
||||
await enableGhostCursor({ page })
|
||||
|
||||
if (!this.previousMouseActionByPage.has(page)) {
|
||||
@@ -112,17 +94,6 @@ export class RecordingGhostCursorController {
|
||||
this.previousMouseActionByPage.delete(page)
|
||||
this.cursorApplyQueueByPage.delete(page)
|
||||
|
||||
// Remove the persistent init script so future navigations don't inject cursor
|
||||
const handle = this.initScriptByPage.get(page)
|
||||
if (handle) {
|
||||
try {
|
||||
await removeGhostCursorInitScript(handle)
|
||||
} catch (error) {
|
||||
this.logger.error('[playwriter] Failed to remove ghost cursor init script', error)
|
||||
}
|
||||
this.initScriptByPage.delete(page)
|
||||
}
|
||||
|
||||
try {
|
||||
await disableGhostCursor({ page })
|
||||
} catch (error) {
|
||||
|
||||
@@ -938,8 +938,6 @@ Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkbo
|
||||
|
||||
While recording is active, Playwriter automatically overlays a smooth ghost cursor that follows automated mouse actions (`page.mouse.*`, `locator.click()`, hover flows) using `page.onMouseAction` from the Playwright fork.
|
||||
|
||||
**Ghost cursor survives MPA navigation**: when recording starts, the cursor bundle is registered via `Page.addScriptToEvaluateOnNewDocument`, so Chrome re-injects and re-enables it on every new document. This means the cursor is visible across full-page navigations (GitHub, Hacker News, etc.) without any extra work.
|
||||
|
||||
For demos where cursor movement should be visible and human-like, drive the page with interaction methods (`locator.click()`, `page.click()`, `page.mouse.move()`, `press`, typing). Avoid skipping interactions with direct state jumps (for example, `goto(itemUrl)` instead of clicking the link) when your goal is to show realistic pointer motion in the recording.
|
||||
|
||||
**Note**: Recording requires the user to have clicked the Playwriter extension icon on the tab. This grants `activeTab` permission needed for `chrome.tabCapture`. Recording works on tabs where the icon was clicked - if you need to record a new tab, ask the user to click the icon on it first.
|
||||
|
||||
Reference in New Issue
Block a user