mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: simplify dom tasks (#9089)
This commit is contained in:
parent
d7901ea9ff
commit
de4aa50d55
@ -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';
|
||||
|
||||
@ -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>> {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user