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
|
# 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
|
## 0.0.84
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "playwriter",
|
"name": "playwriter",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "0.0.84",
|
"version": "0.0.85",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Node-side ghost cursor helpers.
|
* Node-side ghost cursor helpers.
|
||||||
* Injects the browser bundle and forwards mouse action events to the page overlay.
|
* 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 fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import type { Page, MouseActionEvent } from '@xmorse/playwright-core'
|
import type { Page, MouseActionEvent } from '@xmorse/playwright-core'
|
||||||
import { getCDPSessionForPage, type ICDPSession } from './cdp-session.js'
|
|
||||||
|
|
||||||
export interface GhostCursorClientOptions {
|
export interface GhostCursorClientOptions {
|
||||||
style?: 'minimal' | 'dot' | 'screenstudio'
|
style?: 'minimal' | 'dot' | 'screenstudio'
|
||||||
@@ -30,7 +23,6 @@ interface GhostCursorBrowserApi {
|
|||||||
enable: (options?: GhostCursorClientOptions) => void
|
enable: (options?: GhostCursorClientOptions) => void
|
||||||
disable: () => void
|
disable: () => void
|
||||||
applyMouseAction: (event: MouseActionEvent) => void
|
applyMouseAction: (event: MouseActionEvent) => void
|
||||||
isEnabled: () => boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ghostCursorCode: string | null = null
|
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: {
|
export async function applyGhostCursorMouseAction(options: {
|
||||||
page: Page
|
page: Page
|
||||||
event: MouseActionEvent
|
event: MouseActionEvent
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { page, event } = options
|
const { page, event } = options
|
||||||
|
|
||||||
await page.evaluate(
|
const applied = await page.evaluate(
|
||||||
({ serializedEvent }) => {
|
({ serializedEvent }) => {
|
||||||
const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
|
const api = (globalThis as { __playwriterGhostCursor?: GhostCursorBrowserApi }).__playwriterGhostCursor
|
||||||
if (!api) {
|
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)
|
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 },
|
{ serializedEvent: event },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Encapsulates ghost cursor lifecycle for recording sessions.
|
* Encapsulates ghost cursor lifecycle for recording sessions.
|
||||||
* Keeps onMouseAction chaining/restoration isolated from executor logic.
|
* 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 { BrowserContext, Page } from '@xmorse/playwright-core'
|
||||||
import type { ICDPSession } from './cdp-session.js'
|
|
||||||
import {
|
import {
|
||||||
addGhostCursorInitScript,
|
|
||||||
applyGhostCursorMouseAction,
|
applyGhostCursorMouseAction,
|
||||||
disableGhostCursor,
|
disableGhostCursor,
|
||||||
enableGhostCursor,
|
enableGhostCursor,
|
||||||
removeGhostCursorInitScript,
|
|
||||||
type GhostCursorClientOptions,
|
type GhostCursorClientOptions,
|
||||||
} from './ghost-cursor.js'
|
} from './ghost-cursor.js'
|
||||||
|
|
||||||
@@ -26,15 +20,9 @@ interface RecordingTargetOptions {
|
|||||||
sessionId?: string
|
sessionId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitScriptHandle {
|
|
||||||
cdp: ICDPSession
|
|
||||||
identifier: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RecordingGhostCursorController {
|
export class RecordingGhostCursorController {
|
||||||
private readonly previousMouseActionByPage = new WeakMap<Page, Page['onMouseAction']>()
|
private readonly previousMouseActionByPage = new WeakMap<Page, Page['onMouseAction']>()
|
||||||
private readonly cursorApplyQueueByPage = new WeakMap<Page, Promise<void>>()
|
private readonly cursorApplyQueueByPage = new WeakMap<Page, Promise<void>>()
|
||||||
private readonly initScriptByPage = new WeakMap<Page, InitScriptHandle>()
|
|
||||||
private readonly logger: RecordingGhostCursorLogger
|
private readonly logger: RecordingGhostCursorLogger
|
||||||
|
|
||||||
constructor(options: { logger: RecordingGhostCursorLogger }) {
|
constructor(options: { logger: RecordingGhostCursorLogger }) {
|
||||||
@@ -69,12 +57,6 @@ export class RecordingGhostCursorController {
|
|||||||
const { page } = options
|
const { page } = options
|
||||||
|
|
||||||
try {
|
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 })
|
await enableGhostCursor({ page })
|
||||||
|
|
||||||
if (!this.previousMouseActionByPage.has(page)) {
|
if (!this.previousMouseActionByPage.has(page)) {
|
||||||
@@ -112,17 +94,6 @@ export class RecordingGhostCursorController {
|
|||||||
this.previousMouseActionByPage.delete(page)
|
this.previousMouseActionByPage.delete(page)
|
||||||
this.cursorApplyQueueByPage.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 {
|
try {
|
||||||
await disableGhostCursor({ page })
|
await disableGhostCursor({ page })
|
||||||
} catch (error) {
|
} 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.
|
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.
|
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.
|
**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