chore: simplify dom tasks (#9089)

This commit is contained in:
Pavel Feldman 2021-09-22 17:17:49 -07:00 committed by GitHub
parent d7901ea9ff
commit de4aa50d55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 160 deletions

View file

@ -14,19 +14,19 @@
* limitations under the License. * 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 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 { Page } from './page';
import { Progress, ProgressController } from './progress';
import { SelectorInfo } from './selectors'; import { SelectorInfo } from './selectors';
import * as types from './types'; 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']; type SetInputFilesFiles = channels.ElementHandleSetInputFilesParams['files'];
@ -1003,93 +1003,4 @@ export function waitForSelectorTask(selector: SelectorInfo, state: 'attached' |
}, { parsed: selector.parsed, strict: selector.strict, state, root }); }, { parsed: selector.parsed, strict: selector.strict, state, root });
} }
export function dispatchEventTask(selector: SelectorInfo, type: string, eventInit: Object): SchedulableTask<undefined> {
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, strict, type, eventInit }) => {
return injected.pollRaf<undefined>((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<string | null> {
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<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)}`);
return element.innerHTML;
});
}, { parsed: selector.parsed, strict: selector.strict });
}
export function getAttributeTask(selector: SelectorInfo, name: string): SchedulableTask<string | null> {
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<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.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<boolean | 'error:notconnected' | FatalDOMError> {
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'; export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document';

View file

@ -30,7 +30,7 @@ import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../util
import { ManualPromise } from '../utils/async'; import { ManualPromise } from '../utils/async';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation'; import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
import { ElementStateWithoutStable } from './injected/injectedScript'; import InjectedScript, { ElementStateWithoutStable, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript';
import { isSessionClosedError } from './common/protocolError'; import { isSessionClosedError } from './common/protocolError';
type ContextData = { type ContextData = {
@ -68,6 +68,9 @@ export type NavigationEvent = {
error?: Error, error?: Error,
}; };
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;
export type DomTaskBody<T, R> = (progress: InjectedScriptProgress, element: Element, data: T) => R;
export class FrameManager { export class FrameManager {
private _page: Page; private _page: Page;
private _frames = new Map<string, Frame>(); private _frames = new Map<string, Frame>();
@ -736,15 +739,10 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit?: Object, options: types.QueryOnSelectorOptions = {}): Promise<void> { async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this); await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => {
const info = this._page.parseSelector(selector, options); progress.injectedScript.dispatchEvent(element, data.type, data.eventInit);
const task = dom.dispatchEventTask(info, type, eventInit || {}); }, { type, eventInit }, { mainWorld: true, ...options });
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));
await this._page._doSlowMo(); await this._page._doSlowMo();
} }
@ -1042,64 +1040,38 @@ export class Frame extends SdkObject {
} }
async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> { async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
const controller = new ProgressController(metadata, this); return this._scheduleRerunnableTask(metadata, selector, (progress, element) => element.textContent, undefined, options);
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));
} }
async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> { async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
const controller = new ProgressController(metadata, this); return this._scheduleRerunnableTask(metadata, selector, (progress, element) => {
const info = this._page.parseSelector(selector, options); if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml')
const task = dom.innerTextTask(info); return 'error:nothtmlelement';
return controller.run(async progress => { return (element as HTMLElement).innerText;
progress.log(` retrieving innerText from "${selector}"`); }, undefined, options);
const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
return result.innerText;
}, this._page._timeoutSettings.timeout(options));
} }
async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> { async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
const controller = new ProgressController(metadata, this); return this._scheduleRerunnableTask(metadata, selector, (progress, element) => element.innerHTML, undefined, options);
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));
} }
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> { async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
const controller = new ProgressController(metadata, this); return this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => element.getAttribute(data.name), { name }, options);
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));
} }
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise<string> { async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise<string> {
const controller = new ProgressController(metadata, this); return this._scheduleRerunnableTask(metadata, selector, (progress, element) => {
const info = this._page.parseSelector(selector, options); if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
const task = dom.inputValueTask(info); return 'error:hasnovalue';
return controller.run(async progress => { return (element as any).value;
progress.log(` retrieving value from "${selector}"`); }, undefined, options);
return dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
}, this._page._timeoutSettings.timeout(options));
} }
private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> { private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
const controller = new ProgressController(metadata, this); const result = await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => {
const info = this._page.parseSelector(selector, options); const injected = progress.injectedScript;
const task = dom.elementStateTask(info, state); return injected.checkElementState(element, data.state);
const result = await controller.run(async progress => { }, { state }, options);
progress.log(` checking "${state}" state of "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result)); return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result));
} }
@ -1245,14 +1217,33 @@ export class Frame extends SdkObject {
this._parentFrame = null; this._parentFrame = null;
} }
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> { private async _scheduleRerunnableTask<T, R>(metadata: CallMetadata, selector: string, body: DomTaskBody<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise<R> {
const data = this._contextData.get(world)!; const controller = new ProgressController(metadata, this);
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */); const info = this._page.parseSelector(selector, options);
if (this._detached) const callbackText = body.toString();
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.')); const data = this._contextData.get(options.mainWorld ? 'main' : info.world)!;
if (data.context)
rerunnableTask.rerun(data.context); return controller.run(async progress => {
return rerunnableTask.promise; 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<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> { private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {

View file

@ -27,6 +27,7 @@ import { generateSelector } from './selectorGenerator';
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol; type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
export type InjectedScriptProgress = { export type InjectedScriptProgress = {
injectedScript: InjectedScript,
aborted: boolean, aborted: boolean,
log: (message: string) => void, log: (message: string) => void,
logRepeating: (message: string) => void, logRepeating: (message: string) => void,
@ -325,6 +326,7 @@ export class InjectedScript {
let lastLog = ''; let lastLog = '';
const progress: InjectedScriptProgress = { const progress: InjectedScriptProgress = {
injectedScript: this,
aborted: false, aborted: false,
log: (message: string) => { log: (message: string) => {
lastLog = message; lastLog = message;

View file

@ -225,7 +225,6 @@ it.describe('pause', () => {
expect(await sanitizeLog(recorderPage)).toEqual([ expect(await sanitizeLog(recorderPage)).toEqual([
'page.pause- XXms', 'page.pause- XXms',
'page.isChecked(button)- XXms', 'page.isChecked(button)- XXms',
'checking \"checked\" state of \"button\"',
'selector resolved to <button onclick=\"console.log(1)\">Submit</button>', 'selector resolved to <button onclick=\"console.log(1)\">Submit</button>',
'error: Not a checkbox or radio button', 'error: Not a checkbox or radio button',
]); ]);