Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 7 additions & 96 deletions src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@
* limitations under the License.
*/

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 { ElementStateWithoutStable, InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
import * as injectedScriptSource from '../generated/injectedScriptSource';
import type { InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
import { CallMetadata } from './instrumentation';
import * as js from './javascript';
import * as mime from 'mime';
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'];

Expand Down Expand Up @@ -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';
113 changes: 52 additions & 61 deletions src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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>> {
Expand Down
2 changes: 2 additions & 0 deletions src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -325,6 +326,7 @@ export class InjectedScript {

let lastLog = '';
const progress: InjectedScriptProgress = {
injectedScript: this,
aborted: false,
log: (message: string) => {
lastLog = message;
Expand Down
1 change: 0 additions & 1 deletion tests/inspector/pause.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
Expand Down