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.
*/
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<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';

View File

@ -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<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 {
private _page: Page;
private _frames = new Map<string, Frame>();
@ -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<void> {
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<void> {
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<string | null> {
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<string> {
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<string> {
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<string | null> {
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<string> {
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<boolean> {
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<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
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<T, R>(metadata: CallMetadata, selector: string, body: DomTaskBody<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise<R> {
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<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;
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;

View File

@ -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 <button onclick=\"console.log(1)\">Submit</button>',
'error: Not a checkbox or radio button',
]);