diff --git a/playwriter/CHANGELOG.md b/playwriter/CHANGELOG.md index edf38a0..758b74c 100644 --- a/playwriter/CHANGELOG.md +++ b/playwriter/CHANGELOG.md @@ -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 diff --git a/playwriter/package.json b/playwriter/package.json index 54ea52d..9224d48 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -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", diff --git a/playwriter/src/ghost-cursor.ts b/playwriter/src/ghost-cursor.ts index ca02582..f065d5f 100644 --- a/playwriter/src/ghost-cursor.ts +++ b/playwriter/src/ghost-cursor.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 }) } -/** - * 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 { - const { cdp, identifier } = options - await cdp.send('Page.removeScriptToEvaluateOnNewDocument', { identifier }) -} - export async function applyGhostCursorMouseAction(options: { page: Page event: MouseActionEvent }): Promise { 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 }, ) diff --git a/playwriter/src/recording-ghost-cursor.ts b/playwriter/src/recording-ghost-cursor.ts index 162ae97..7aa83a2 100644 --- a/playwriter/src/recording-ghost-cursor.ts +++ b/playwriter/src/recording-ghost-cursor.ts @@ -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() private readonly cursorApplyQueueByPage = new WeakMap>() - private readonly initScriptByPage = new WeakMap() 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) { diff --git a/playwriter/src/skill.md b/playwriter/src/skill.md index 0f37bfa..086b8bc 100644 --- a/playwriter/src/skill.md +++ b/playwriter/src/skill.md @@ -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.