diff --git a/playwright/packages/playwright-core/src/client/channelOwner.ts b/playwright/packages/playwright-core/src/client/channelOwner.ts index a5d753507..7f1100830 100644 --- a/playwright/packages/playwright-core/src/client/channelOwner.ts +++ b/playwright/packages/playwright-core/src/client/channelOwner.ts @@ -174,8 +174,10 @@ export abstract class ChannelOwner('crxZone'); + const stackTrace = captureLibraryStackTrace(); - let apiName: string | undefined = stackTrace.apiName; + let apiName: string | undefined = crxZone?.apiName ?? stackTrace.apiName; const frames: channels.StackFrame[] = stackTrace.frames; if (isInternal === undefined) diff --git a/playwright/packages/playwright-core/src/utils/zones.ts b/playwright/packages/playwright-core/src/utils/zones.ts index 15487d359..a805bd497 100644 --- a/playwright/packages/playwright-core/src/utils/zones.ts +++ b/playwright/packages/playwright-core/src/utils/zones.ts @@ -16,7 +16,7 @@ import { AsyncLocalStorage } from 'async_hooks'; -export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone'; +export type ZoneType = 'crxZone' | 'apiZone' | 'expectZone' | 'stepZone'; class ZoneManager { private readonly _asyncLocalStorage = new AsyncLocalStorage(); diff --git a/src/client/api.ts b/src/client/api.ts deleted file mode 100644 index 03e4af117..000000000 --- a/src/client/api.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Copyright (c) Rui Figueira. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// some types are commented out because they are not used in the extension -import { - Accessibility, - Browser, - BrowserContext, - BrowserType, - Clock, - ConsoleMessage, - Coverage, - Dialog, - Download, - // Electron, - // ElectronApplication, - Locator, - FrameLocator, - ElementHandle, - FileChooser, - TimeoutError, - Frame, - Keyboard, - Mouse, - Touchscreen, - JSHandle, - Route, - WebSocket, - WebSocketRoute, - // APIRequest, - // APIRequestContext, - // APIResponse, - Page, - Selectors, - Tracing, - Video, - Worker, - CDPSession, - Playwright, - WebError, -} from 'playwright-core/lib/client/api'; - -import { zones } from 'playwright-core/lib/utils'; - -type ApiTypeMap = { - 'accessibility': Accessibility, - // 'android': Android, - // 'androidDevice': AndroidDevice, - // 'androidWebView': AndroidWebView, - // 'androidInput': AndroidInput, - // 'androidSocket': AndroidSocket, - 'browser': Browser, - 'browserContext': BrowserContext, - 'browserType': BrowserType, - 'clock': Clock, - 'consoleMessage': ConsoleMessage, - 'coverage': Coverage, - 'dialog': Dialog, - 'download': Download, - // 'electron': Electron, - // 'electronApplication': ElectronApplication, - 'locator': Locator, - 'frameLocator': FrameLocator, - 'elementHandle': ElementHandle, - 'fileChooser': FileChooser, - 'timeoutError': TimeoutError, - 'frame': Frame, - 'keyboard': Keyboard, - 'mouse': Mouse, - 'touchscreen': Touchscreen, - 'jSHandle': JSHandle, - 'route': Route, - 'webSocket': WebSocket, - 'webSocketRoute': WebSocketRoute, - // 'request': APIRequest, - // 'requestContext': APIRequestContext, - // 'response': APIResponse, - 'page': Page, - 'selectors': Selectors, - 'tracing': Tracing, - 'video': Video, - 'worker': Worker, - 'session': CDPSession, - 'playwright': Playwright, - 'webError': WebError, -}; - -type KeysOfAsyncMethods = { - [K in keyof T]: T[K] extends (...args: any[]) => Promise ? (K extends `_${string}` ? never : K) : never; -}[Extract]; - -const apis: { [K in keyof ApiTypeMap]: [ApiTypeMap[K], ...Array>] } = { - accessibility: [Accessibility.prototype, 'snapshot'], - // android: [Android.prototype], - // androidDevice: [AndroidDevice.prototype], - // androidWebView: [AndroidWebView.prototype], - // androidInput: [AndroidInput.prototype], - // androidSocket: [AndroidSocket.prototype], - browser: [Browser.prototype, 'newContext', 'newPage', 'newBrowserCDPSession', 'startTracing', 'stopTracing', 'close'], - browserContext: [BrowserContext.prototype, 'newPage', 'cookies', 'addCookies', 'clearCookies', 'grantPermissions', 'clearPermissions', 'setGeolocation', 'setExtraHTTPHeaders', 'setOffline', 'setHTTPCredentials', 'addInitScript', 'exposeBinding', 'exposeFunction', 'route', 'routeWebSocket', 'routeFromHAR', 'unrouteAll', 'unroute', 'waitForEvent', 'storageState', 'newCDPSession', 'close'], - browserType: [BrowserType.prototype, 'launch', 'launchServer', 'launchPersistentContext', 'connect', 'connectOverCDP', 'removeAllListeners'], - clock: [Clock.prototype, 'install', 'fastForward', 'pauseAt', 'resume', 'runFor', 'setFixedTime', 'setSystemTime'], - consoleMessage: [ConsoleMessage.prototype], - coverage: [Coverage.prototype, 'startCSSCoverage', 'stopCSSCoverage', 'startJSCoverage', 'stopJSCoverage'], - dialog: [Dialog.prototype, 'accept', 'dismiss'], - download: [Download.prototype, 'path', 'failure', 'delete', 'saveAs'], - // electron: [Electron.prototype], - // electronApplication: [ElectronApplication.prototype], - locator: [Locator.prototype, 'setInputFiles', 'inputValue', 'click', 'hover', 'check', 'uncheck', 'selectOption', 'fill', 'press', 'focus', 'type', 'press', 'scrollIntoViewIfNeeded', 'boundingBox', 'screenshot', 'textContent', 'innerText', 'innerHTML', 'getAttribute', 'hover', 'click', 'dblclick', 'selectOption', 'fill', 'type', 'press', 'check', 'uncheck', 'scrollIntoViewIfNeeded', 'boundingBox', 'screenshot', 'textContent', 'innerText', 'innerHTML', 'getAttribute'], - frameLocator: [FrameLocator.prototype], - elementHandle: [ElementHandle.prototype, 'ownerFrame', 'contentFrame', 'getAttribute', 'inputValue', 'textContent', 'innerText', 'innerHTML', 'isChecked', 'isDisabled', 'isEditable', 'isEnabled', 'isHidden', 'isVisible', 'dispatchEvent', 'scrollIntoViewIfNeeded', 'hover', 'click', 'dblclick', 'tap', 'selectOption', 'fill', 'selectText', 'setInputFiles', 'focus', 'type', 'press', 'check', 'uncheck', 'setChecked', 'boundingBox', 'screenshot', '$', '$$', '$eval', '$$eval', 'waitForElementState', 'waitForSelector'], - fileChooser: [FileChooser.prototype, 'setFiles'], - timeoutError: [TimeoutError.prototype], - frame: [Frame.prototype, 'goto', 'waitForNavigation', 'waitForLoadState', 'waitForURL', 'frameElement', 'evaluateHandle', 'evaluate', '$', 'waitForSelector', 'dispatchEvent', '$eval', '$$', 'content', 'setContent', 'addScriptTag', 'addStyleTag', 'click', 'dblclick', 'dragAndDrop', 'tap', 'fill', 'focus', 'textContent', 'innerText', 'innerHTML', 'getAttribute', 'inputValue', 'isChecked', 'isDisabled', 'isEditable', 'isEnabled', 'isHidden', 'isVisible', 'hover', 'selectOption', 'setInputFiles', 'type', 'press', 'check', 'uncheck', 'setChecked', 'waitForTimeout', 'waitForFunction', 'title'], - keyboard: [Keyboard.prototype, 'down', 'up', 'insertText', 'type', 'press'], - mouse: [Mouse.prototype, 'click', 'dblclick', 'down', 'up', 'move', 'wheel'], - touchscreen: [Touchscreen.prototype, 'tap'], - jSHandle: [JSHandle.prototype, 'evaluate', 'evaluateHandle', 'getProperty', 'jsonValue', 'getProperties', 'dispose'], - route: [Route.prototype, 'fallback', 'abort', 'fetch', 'fulfill', 'continue'], - webSocket: [WebSocket.prototype, 'waitForEvent'], - webSocketRoute: [WebSocketRoute.prototype, 'close'], - // request: [APIRequest.prototype], - // requestContext: [APIRequestContext.prototype], - // response: [APIResponse.prototype], - page: [Page.prototype, 'opener', '$', '$$', 'waitForSelector', 'dispatchEvent', 'evaluateHandle', '$eval', '$$eval', 'addScriptTag', 'addStyleTag', 'exposeFunction', 'exposeBinding', 'setExtraHTTPHeaders', 'content', 'setContent', 'goto', 'reload', 'addLocatorHandler', 'removeLocatorHandler', 'waitForLoadState', 'waitForNavigation', 'waitForURL', 'waitForRequest', 'waitForResponse', 'waitForEvent', 'goBack', 'goForward', 'requestGC', 'emulateMedia', 'setViewportSize', 'evaluate', 'addInitScript', 'route', 'routeFromHAR', 'routeWebSocket', 'unrouteAll', 'unroute', 'screenshot', 'title', 'bringToFront', 'close', 'click', 'dragAndDrop', 'dblclick', 'tap', 'fill', 'focus', 'textContent', 'innerText', 'innerHTML', 'getAttribute', 'inputValue', 'isChecked', 'isDisabled', 'isEditable', 'isEnabled', 'isHidden', 'isVisible', 'hover', 'selectOption', 'setInputFiles', 'type', 'press', 'check', 'uncheck', 'setChecked', 'waitForTimeout', 'waitForFunction', 'pause', 'pdf'], - selectors: [Selectors.prototype, 'register'], - tracing: [Tracing.prototype, 'group', 'groupEnd', 'removeAllListeners', 'start', 'startChunk', 'stop', 'stopChunk'], - video: [Video.prototype, 'delete', 'path', 'saveAs'], - worker: [Worker.prototype, 'evaluate', 'evaluateHandle'], - session: [CDPSession.prototype, 'send', 'detach'], - playwright: [Playwright.prototype], - webError: [WebError.prototype], -}; - -for (const [typeName, [proto, ...props]] of Object.entries(apis)) { - for (const key of props) { - const originalFn = (proto as any)[key!]; - if (!originalFn || typeof originalFn !== 'function') - throw new Error(`Method ${key} not found in ${typeName}`); - - (proto as any)[key!] = async function(...args: any[]) { - const apiName = zones.zoneData<{ apiName: string }>('crxZone'); - if (apiName) - return await originalFn.apply(this, args); - return await zones.run('crxZone', { apiName: `${typeName}.${key}` }, async () => await originalFn.apply(this, args)); - }; - } -} \ No newline at end of file diff --git a/src/client/crxZone.ts b/src/client/crxZone.ts new file mode 100644 index 000000000..5c6dd214a --- /dev/null +++ b/src/client/crxZone.ts @@ -0,0 +1,421 @@ +/** + * Copyright (c) Rui Figueira. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// some types are commented out because they are not used in the extension +import { + Accessibility, + Browser, + BrowserContext, + BrowserType, + Clock, + ConsoleMessage, + Coverage, + Dialog, + Download, + // Electron, + // ElectronApplication, + Locator, + FrameLocator, + ElementHandle, + FileChooser, + TimeoutError, + Frame, + Keyboard, + Mouse, + Touchscreen, + JSHandle, + Route, + WebSocket, + WebSocketRoute, + // APIRequest, + // APIRequestContext, + // APIResponse, + Page, + Selectors, + Tracing, + Video, + Worker, + CDPSession, + Playwright, + WebError, +} from 'playwright-core/lib/client/api'; + +import { + Crx, + CrxApplication, + CrxRecorder, +} from './crx'; + +import { zones } from 'playwright-core/lib/utils'; + +type ApiTypeMap = { + 'accessibility': Accessibility, + // 'android': Android, + // 'androidDevice': AndroidDevice, + // 'androidWebView': AndroidWebView, + // 'androidInput': AndroidInput, + // 'androidSocket': AndroidSocket, + 'browser': Browser, + 'browserContext': BrowserContext, + 'browserType': BrowserType, + 'clock': Clock, + 'consoleMessage': ConsoleMessage, + 'coverage': Coverage, + 'dialog': Dialog, + 'download': Download, + // 'electron': Electron, + // 'electronApplication': ElectronApplication, + 'locator': Locator, + 'frameLocator': FrameLocator, + 'elementHandle': ElementHandle, + 'fileChooser': FileChooser, + 'timeoutError': TimeoutError, + 'frame': Frame, + 'keyboard': Keyboard, + 'mouse': Mouse, + 'touchscreen': Touchscreen, + 'jSHandle': JSHandle, + 'route': Route, + 'webSocket': WebSocket, + 'webSocketRoute': WebSocketRoute, + // 'request': APIRequest, + // 'requestContext': APIRequestContext, + // 'response': APIResponse, + 'page': Page, + 'selectors': Selectors, + 'tracing': Tracing, + 'video': Video, + 'worker': Worker, + 'session': CDPSession, + 'playwright': Playwright, + 'webError': WebError, + + // from crx + 'crx': Crx, + 'crxApplication': CrxApplication, + 'crxRecorder': CrxRecorder +}; + +type KeysOfAsyncMethods = { + [K in keyof T]: T[K] extends (...args: any[]) => Promise ? (K extends `_${string}` | 'removeAllListeners' ? never : K) : never; +}[Extract]; + +const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsyncMethods]: boolean }] } = { + accessibility: [Accessibility.prototype, { snapshot: true }], + // android: [Android.prototype], + // androidDevice: [AndroidDevice.prototype], + // androidWebView: [AndroidWebView.prototype], + // androidInput: [AndroidInput.prototype], + // androidSocket: [AndroidSocket.prototype], + browser: [Browser.prototype, { newContext: true, newPage: true, newBrowserCDPSession: true, startTracing: true, stopTracing: true, close: true }], + browserContext: [BrowserContext.prototype, { + newPage: true, + cookies: true, + addCookies: true, + clearCookies: true, + grantPermissions: true, + clearPermissions: true, + setGeolocation: true, + setExtraHTTPHeaders: true, + setOffline: true, + setHTTPCredentials: true, + addInitScript: true, + exposeBinding: true, + exposeFunction: true, + route: true, + routeWebSocket: true, + routeFromHAR: true, + unrouteAll: true, + unroute: true, + waitForEvent: true, + storageState: true, + newCDPSession: true, + close: true + }], + browserType: [BrowserType.prototype, { launch: true, launchServer: true, launchPersistentContext: true, connect: true, connectOverCDP: true }], + clock: [Clock.prototype, { install: true, fastForward: true, pauseAt: true, resume: true, runFor: true, setFixedTime: true, setSystemTime: true }], + consoleMessage: [ConsoleMessage.prototype, {}], + coverage: [Coverage.prototype, { startCSSCoverage: true, stopCSSCoverage: true, startJSCoverage: true, stopJSCoverage: true }], + dialog: [Dialog.prototype, { accept: true, dismiss: true }], + download: [Download.prototype, { cancel: true, createReadStream: true, path: true, failure: true, delete: true, saveAs: true }], + // electron: [Electron.prototype, {}], + // electronApplication: [ElectronApplication.prototype, {}], + locator: [Locator.prototype, { + boundingBox: true, + check: true, + click: true, + dblclick: true, + dispatchEvent: true, + dragTo: true, + evaluate: true, + evaluateAll: true, + evaluateHandle: true, + fill: true, + clear: true, + highlight: true, + elementHandle: true, + elementHandles: true, + focus: true, + blur: true, + count: true, + getAttribute: true, + hover: true, + innerHTML: true, + innerText: true, + inputValue: true, + isChecked: true, + isDisabled: true, + isEditable: true, + isEnabled: true, + isHidden: true, + isVisible: true, + press: true, + screenshot: true, + ariaSnapshot: true, + scrollIntoViewIfNeeded: true, + selectOption: true, + selectText: true, + setChecked: true, + setInputFiles: true, + tap: true, + textContent: true, + type: true, + pressSequentially: true, + uncheck: true, + all: true, + allInnerTexts: true, + allTextContents: true, + waitFor: true, + }], + frameLocator: [FrameLocator.prototype, {}], + elementHandle: [ElementHandle.prototype, { + // from JSHandle + evaluate: true, + evaluateHandle: true, + getProperty: true, + getProperties: true, + jsonValue: true, + dispose: true, + // from ElementHandle + ownerFrame: true, + contentFrame: true, + getAttribute: true, + inputValue: true, + textContent: true, + innerText: true, + innerHTML: true, + isChecked: true, + isDisabled: true, + isEditable: true, + isEnabled: true, + isHidden: true, + isVisible: true, + dispatchEvent: true, + scrollIntoViewIfNeeded: true, + hover: true, + click: true, + dblclick: true, + tap: true, + selectOption: true, + fill: true, + selectText: true, + setInputFiles: true, + focus: true, + type: true, + press: true, + check: true, + uncheck: true, + setChecked: true, + boundingBox: true, + screenshot: true, + $: true, + $$: true, + $eval: true, + $$eval: true, + waitForElementState: true, + waitForSelector: true, + }], + fileChooser: [FileChooser.prototype, { setFiles: true }], + timeoutError: [TimeoutError.prototype, {}], + frame: [Frame.prototype, { + goto: true, + waitForNavigation: true, + waitForLoadState: true, + waitForURL: true, + frameElement: true, + evaluateHandle: true, + evaluate: true, + $: true, + $$: true, + waitForSelector: true, + dispatchEvent: true, + $eval: true, + $$eval: true, + content: true, + setContent: true, + addScriptTag: true, + addStyleTag: true, + click: true, + dblclick: true, + dragAndDrop: true, + tap: true, + fill: true, + focus: true, + textContent: true, + innerText: true, + innerHTML: true, + getAttribute: true, + inputValue: true, + isChecked: true, + isDisabled: true, + isEditable: true, + isEnabled: true, + isHidden: true, + isVisible: true, + hover: true, + selectOption: true, + setInputFiles: true, + type: true, + press: true, + check: true, + uncheck: true, + setChecked: true, + waitForTimeout: true, + waitForFunction: true, + title: true + }], + keyboard: [Keyboard.prototype, { down: true, up: true, insertText: true, type: true, press: true }], + mouse: [Mouse.prototype, { click: true, dblclick: true, down: true, up: true, move: true, wheel: true }], + touchscreen: [Touchscreen.prototype, { tap: true }], + jSHandle: [JSHandle.prototype, { evaluate: true, evaluateHandle: true, getProperty: true, jsonValue: true, getProperties: true, dispose: true }], + route: [Route.prototype, { fallback: true, abort: true, fetch: true, fulfill: true, continue: true }], + webSocket: [WebSocket.prototype, { waitForEvent: true }], + webSocketRoute: [WebSocketRoute.prototype, { close: true }], + // request: [APIRequest.prototype, {}], + // requestContext: [APIRequestContext.prototype, {}], + // response: [APIResponse.prototype, {}], + page: [Page.prototype, { + opener: true, + waitForSelector: true, + dispatchEvent: true, + evaluateHandle: true, + $: true, + $$: true, + $eval: true, + $$eval: true, + addScriptTag: true, + addStyleTag: true, + exposeFunction: true, + exposeBinding: true, + setExtraHTTPHeaders: true, + content: true, + setContent: true, + goto: true, + reload: true, + addLocatorHandler: true, + removeLocatorHandler: true, + waitForLoadState: true, + waitForNavigation: true, + waitForURL: true, + waitForRequest: true, + waitForResponse: true, + waitForEvent: true, + goBack: true, + goForward: true, + requestGC: true, + emulateMedia: true, + setViewportSize: true, + evaluate: true, + addInitScript: true, + route: true, + routeFromHAR: true, + routeWebSocket: true, + unrouteAll: true, + unroute: true, + screenshot: true, + title: true, + bringToFront: true, + close: true, + click: true, + dragAndDrop: true, + dblclick: true, + tap: true, + fill: true, + focus: true, + textContent: true, + innerText: true, + innerHTML: true, + getAttribute: true, + inputValue: true, + isChecked: true, + isDisabled: true, + isEditable: true, + isEnabled: true, + isHidden: true, + isVisible: true, + hover: true, + selectOption: true, + setInputFiles: true, + type: true, + press: true, + check: true, + uncheck: true, + setChecked: true, + waitForTimeout: true, + waitForFunction: true, + pause: true, + pdf: true, + }], + selectors: [Selectors.prototype, { register: true }], + tracing: [Tracing.prototype, { group: true, groupEnd: true, start: true, startChunk: true, stop: true, stopChunk: true }], + video: [Video.prototype, { delete: true, path: true, saveAs: true }], + worker: [Worker.prototype, { evaluate: true, evaluateHandle: true }], + session: [CDPSession.prototype, { send: true, detach: true }], + playwright: [Playwright.prototype, { devices: false }], + webError: [WebError.prototype, {}], + + // from crx + crx: [Crx.prototype, { start: true, get: true }], + crxApplication: [CrxApplication.prototype, { attach: true, attachAll: true, close: true, detach: true, detachAll: true, newPage: true }], + crxRecorder: [CrxRecorder.prototype, { hide: true, list: true, load: true, run: true, setMode: true, show: true }], +}; + +const kCrxZoneWrapped = Symbol('crxZone'); + +export function wrapClientApis() { + for (const [typeName, [proto, props]] of Object.entries(apis)) { + for (const [key, needsWrap] of Object.entries(props)) { + if (!needsWrap) + continue; + const originalFn = (proto as any)[key!]; + + if (!originalFn || typeof originalFn !== 'function') + throw new Error(`Method ${key} not found in ${typeName}`); + + if (originalFn[kCrxZoneWrapped] === true) + continue; + + const wrapFn = async function(this: any, ...args: any[]) { + const apiName = zones.zoneData<{ apiName: string }>('crxZone'); + if (apiName) + return await originalFn.apply(this, args); + return await zones.run('crxZone', { apiName: `${typeName}.${key}` }, async () => await originalFn.apply(this, args)); + }; + wrapFn[kCrxZoneWrapped] = true; + (proto as any)[key!] = wrapFn; + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 87395fbec..020b9e8c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,8 @@ import { CrxPlaywright } from './server/crxPlaywright'; import { CrxPlaywrightDispatcher } from './server/dispatchers/crxPlaywrightDispatcher'; import { PageBinding } from 'playwright-core/lib/server/page'; +import { wrapClientApis } from './client/crxZone'; + export { debug as _debug } from 'debug'; export { setUnderTest as _setUnderTest, isUnderTest as _isUnderTest } from 'playwright-core/lib/utils'; @@ -56,4 +58,4 @@ clientConnection.toImpl = (x: any) => x ? dispatcherConnection._dispatchers.get( export const { _crx: crx, selectors } = playwrightAPI; export default playwrightAPI; -import './client/api'; +wrapClientApis(); \ No newline at end of file