diff --git a/src/server/dom.ts b/src/server/dom.ts index c215979906..72be1187a5 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import * as channels from '../protocol/channels'; -import * as frames from './frames'; -import type { ElementStateWithoutStable, InjectedScript, InjectedScriptPoll } from './injected/injectedScript'; -import * as injectedScriptSource from '../generated/injectedScriptSource'; -import * as js from './javascript'; import * as mime from 'mime'; +import * as injectedScriptSource from '../generated/injectedScriptSource'; +import * as channels from '../protocol/channels'; +import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; +import { isSessionClosedError } from './common/protocolError'; +import * as frames from './frames'; +import type { InjectedScript, InjectedScriptPoll } from './injected/injectedScript'; +import { CallMetadata } from './instrumentation'; +import * as js from './javascript'; import { Page } from './page'; +import { Progress, ProgressController } from './progress'; import { SelectorInfo } from './selectors'; import * as types from './types'; -import { Progress, ProgressController } from './progress'; -import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; -import { CallMetadata } from './instrumentation'; -import { isSessionClosedError } from './common/protocolError'; type SetInputFilesFiles = channels.ElementHandleSetInputFilesParams['files']; @@ -1003,93 +1003,4 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' | }, { parsed: selector.parsed, strict: selector.strict, state, root }); } -export function dispatchEventTask(selector: SelectorInfo, type: string, eventInit: Object): SchedulableTask { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, type, eventInit }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - injected.dispatchEvent(element, type, eventInit); - }); - }, { parsed: selector.parsed, strict: selector.strict, type, eventInit }); -} - -export function textContentTask(selector: SelectorInfo): SchedulableTask { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - progress.log(` retrieving textContent`); - return element.textContent; - }); - }, { parsed: selector.parsed, strict: selector.strict }); -} - -export function innerTextTask(selector: SelectorInfo): SchedulableTask<'error:nothtmlelement' | { innerText: string }> { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml') - return 'error:nothtmlelement'; - return { innerText: (element as HTMLElement).innerText }; - }); - }, { parsed: selector.parsed, strict: selector.strict }); -} - -export function innerHTMLTask(selector: SelectorInfo): SchedulableTask { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - return element.innerHTML; - }); - }, { parsed: selector.parsed, strict: selector.strict }); -} - -export function getAttributeTask(selector: SelectorInfo, name: string): SchedulableTask { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, name }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - return element.getAttribute(name); - }); - }, { parsed: selector.parsed, strict: selector.strict, name }); -} - -export function inputValueTask(selector: SelectorInfo): SchedulableTask { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT') - return 'error:hasnovalue'; - return (element as any).value; - }); - }, { parsed: selector.parsed, strict: selector.strict, }); -} - -export function elementStateTask(selector: SelectorInfo, state: ElementStateWithoutStable): SchedulableTask { - return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, state }) => { - return injected.pollRaf((progress, continuePolling) => { - const element = injected.querySelector(parsed, document, strict); - if (!element) - return continuePolling; - progress.log(` selector resolved to ${injected.previewNode(element)}`); - return injected.checkElementState(element, state); - }); - }, { parsed: selector.parsed, strict: selector.strict, state }); -} - export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document'; diff --git a/src/server/frames.ts b/src/server/frames.ts index ab1b1f9629..c9431aa25a 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -30,7 +30,7 @@ import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../util import { ManualPromise } from '../utils/async'; import { debugLogger } from '../utils/debugLogger'; import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation'; -import { ElementStateWithoutStable } from './injected/injectedScript'; +import InjectedScript, { ElementStateWithoutStable, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript'; import { isSessionClosedError } from './common/protocolError'; type ContextData = { @@ -68,6 +68,9 @@ export type NavigationEvent = { error?: Error, }; +export type SchedulableTask = (injectedScript: js.JSHandle) => Promise>>; +export type DomTaskBody = (progress: InjectedScriptProgress, element: Element, data: T) => R; + export class FrameManager { private _page: Page; private _frames = new Map(); @@ -736,15 +739,10 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } - async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.QueryOnSelectorOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.dispatchEventTask(info, type, eventInit || {}); - await controller.run(async progress => { - progress.log(`Dispatching "${type}" event on selector "${selector}"...`); - // Note: we always dispatch events in the main world. - await this._scheduleRerunnableTask(progress, 'main', task); - }, this._page._timeoutSettings.timeout(options)); + async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions = {}): Promise { + await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => { + progress.injectedScript.dispatchEvent(element, data.type, data.eventInit); + }, { type, eventInit }, { mainWorld: true, ...options }); await this._page._doSlowMo(); } @@ -1042,64 +1040,38 @@ export class Frame extends SdkObject { } async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.textContentTask(info); - return controller.run(async progress => { - progress.log(` waiting for selector "${selector}"\u2026`); - return this._scheduleRerunnableTask(progress, info.world, task); - }, this._page._timeoutSettings.timeout(options)); + return this._scheduleRerunnableTask(metadata, selector, (progress, element) => element.textContent, undefined, options); } async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.innerTextTask(info); - return controller.run(async progress => { - progress.log(` retrieving innerText from "${selector}"`); - const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task)); - return result.innerText; - }, this._page._timeoutSettings.timeout(options)); + return this._scheduleRerunnableTask(metadata, selector, (progress, element) => { + if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml') + return 'error:nothtmlelement'; + return (element as HTMLElement).innerText; + }, undefined, options); } async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.innerHTMLTask(info); - return controller.run(async progress => { - progress.log(` retrieving innerHTML from "${selector}"`); - return this._scheduleRerunnableTask(progress, info.world, task); - }, this._page._timeoutSettings.timeout(options)); + return this._scheduleRerunnableTask(metadata, selector, (progress, element) => element.innerHTML, undefined, options); } async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.getAttributeTask(info, name); - return controller.run(async progress => { - progress.log(` retrieving attribute "${name}" from "${selector}"`); - return this._scheduleRerunnableTask(progress, info.world, task); - }, this._page._timeoutSettings.timeout(options)); + return this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => element.getAttribute(data.name), { name }, options); } async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.inputValueTask(info); - return controller.run(async progress => { - progress.log(` retrieving value from "${selector}"`); - return dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task)); - }, this._page._timeoutSettings.timeout(options)); + return this._scheduleRerunnableTask(metadata, selector, (progress, element) => { + if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT') + return 'error:hasnovalue'; + return (element as any).value; + }, undefined, options); } private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise { - const controller = new ProgressController(metadata, this); - const info = this._page.parseSelector(selector, options); - const task = dom.elementStateTask(info, state); - const result = await controller.run(async progress => { - progress.log(` checking "${state}" state of "${selector}"`); - return this._scheduleRerunnableTask(progress, info.world, task); - }, this._page._timeoutSettings.timeout(options)); + const result = await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => { + const injected = progress.injectedScript; + return injected.checkElementState(element, data.state); + }, { state }, options); return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result)); } @@ -1245,14 +1217,33 @@ export class Frame extends SdkObject { this._parentFrame = null; } - private _scheduleRerunnableTask(progress: Progress, world: types.World, task: dom.SchedulableTask): Promise { - const data = this._contextData.get(world)!; - const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */); - if (this._detached) - rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.')); - if (data.context) - rerunnableTask.rerun(data.context); - return rerunnableTask.promise; + private async _scheduleRerunnableTask(metadata: CallMetadata, selector: string, body: DomTaskBody, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise { + const controller = new ProgressController(metadata, this); + const info = this._page.parseSelector(selector, options); + const callbackText = body.toString(); + const data = this._contextData.get(options.mainWorld ? 'main' : info.world)!; + + return controller.run(async progress => { + const rerunnableTask = new RerunnableTask(data, progress, injectedScript => { + return injectedScript.evaluateHandle((injected, { info, taskData, callbackText }) => { + const callback = window.eval(callbackText); + return injected.pollRaf((progress, continuePolling) => { + const element = injected.querySelector(info.parsed, document, info.strict); + if (!element) + return continuePolling; + progress.log(` selector resolved to ${injected.previewNode(element)}`); + return callback(progress, element, taskData); + }); + }, { info, taskData, callbackText }); + }, true); + + if (this._detached) + rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.')); + if (data.context) + rerunnableTask.rerun(data.context); + const result = await rerunnableTask.promise; + return dom.throwFatalDOMError(result); + }, this._page._timeoutSettings.timeout(options)); } private _scheduleRerunnableHandleTask(progress: Progress, world: types.World, task: dom.SchedulableTask): Promise> { diff --git a/src/server/injected/injectedScript.ts b/src/server/injected/injectedScript.ts index 9d3923587a..3c57570893 100644 --- a/src/server/injected/injectedScript.ts +++ b/src/server/injected/injectedScript.ts @@ -27,6 +27,7 @@ import { generateSelector } from './selectorGenerator'; type Predicate = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol; export type InjectedScriptProgress = { + injectedScript: InjectedScript, aborted: boolean, log: (message: string) => void, logRepeating: (message: string) => void, @@ -325,6 +326,7 @@ export class InjectedScript { let lastLog = ''; const progress: InjectedScriptProgress = { + injectedScript: this, aborted: false, log: (message: string) => { lastLog = message; diff --git a/tests/inspector/pause.spec.ts b/tests/inspector/pause.spec.ts index ad72ca834a..d1a096845c 100644 --- a/tests/inspector/pause.spec.ts +++ b/tests/inspector/pause.spec.ts @@ -225,7 +225,6 @@ it.describe('pause', () => { expect(await sanitizeLog(recorderPage)).toEqual([ 'page.pause- XXms', 'page.isChecked(button)- XXms', - 'checking \"checked\" state of \"button\"', 'selector resolved to ', 'error: Not a checkbox or radio button', ]);