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
30 changes: 0 additions & 30 deletions src/server/common/domErrors.ts

This file was deleted.

91 changes: 32 additions & 59 deletions src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
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, LogEntry } from './injected/injectedScript';
Expand Down Expand Up @@ -98,6 +97,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
return new pwExport(
${this.frame._page._delegate.rafCountForStablePosition()},
${!!process.env.PWTEST_USE_TIMEOUT_FOR_RAF},
"${this.frame._page._browserContext._browser.options.name}",
[${custom.join(',\n')}]
);
})();
Expand Down Expand Up @@ -182,21 +182,21 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async getAttribute(name: string): Promise<string | null> {
return throwFatalDOMError(throwRetargetableDOMError(await this.evaluateInUtility(([injeced, node, name]) => {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node, name]) => {
if (node.nodeType !== Node.ELEMENT_NODE)
return 'error:notelement';
throw injected.createStacklessError('Node is not an element');
const element = node as unknown as Element;
return { value: element.getAttribute(name) };
}, name))).value;
}, name)).value;
}

async inputValue(): Promise<string> {
return throwFatalDOMError(throwRetargetableDOMError(await this.evaluateInUtility(([injeced, node]) => {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
if (node.nodeType !== Node.ELEMENT_NODE || (node.nodeName !== 'INPUT' && node.nodeName !== 'TEXTAREA' && node.nodeName !== 'SELECT'))
return 'error:hasnovalue';
throw injected.createStacklessError('Node is not an <input>, <textarea> or <select> element');
const element = node as unknown as (HTMLInputElement | HTMLTextAreaElement);
return { value: element.value };
}, undefined))).value;
}, undefined)).value;
}

async textContent(): Promise<string | null> {
Expand All @@ -206,23 +206,23 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async innerText(): Promise<string> {
return throwFatalDOMError(throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
if (node.nodeType !== Node.ELEMENT_NODE)
return 'error:notelement';
throw injected.createStacklessError('Node is not an element');
if ((node as unknown as Element).namespaceURI !== 'http://www.w3.org/1999/xhtml')
return 'error:nothtmlelement';
throw injected.createStacklessError('Node is not an HTMLElement');
const element = node as unknown as HTMLElement;
return { value: element.innerText };
}, undefined))).value;
}, undefined)).value;
}

async innerHTML(): Promise<string> {
return throwFatalDOMError(throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
if (node.nodeType !== Node.ELEMENT_NODE)
return 'error:notelement';
throw injected.createStacklessError('Node is not an element');
const element = node as unknown as Element;
return { value: element.innerHTML };
}, undefined))).value;
}, undefined)).value;
}

async dispatchEvent(type: string, eventInit: Object = {}) {
Expand Down Expand Up @@ -505,7 +505,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (poll === 'error:notconnected')
return poll;
const pollHandler = new InjectedScriptPollHandler(progress, poll);
const result = throwFatalDOMError(await pollHandler.finish());
const result = await pollHandler.finish();
await this._page._doSlowMo();
return result;
});
Expand All @@ -530,7 +530,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (poll === 'error:notconnected')
return poll;
const pollHandler = new InjectedScriptPollHandler(progress, poll);
const filled = throwFatalDOMError(await pollHandler.finish());
const filled = await pollHandler.finish();
progress.throwIfAborted(); // Avoid action that has side-effects.
if (filled === 'error:notconnected')
return filled;
Expand All @@ -556,7 +556,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return injected.waitForElementStatesAndPerformAction(node, ['visible'], force, injected.selectText.bind(injected));
}, options.force);
const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll));
const result = throwFatalDOMError(await pollHandler.finish());
const result = await pollHandler.finish();
assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options));
}
Expand All @@ -574,20 +574,20 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (!payload.mimeType)
payload.mimeType = mime.getType(payload.name) || 'application/octet-stream';
}
const retargeted = await this.evaluateHandleInUtility(([injected, node, multiple]): FatalDOMError | 'error:notconnected' | Element => {
const retargeted = await this.evaluateHandleInUtility(([injected, node, multiple]): 'error:notconnected' | Element => {
const element = injected.retarget(node, 'follow-label');
if (!element)
return 'error:notconnected';
if (element.tagName !== 'INPUT')
return 'error:notinput';
throw injected.createStacklessError('Node is not an HTMLInputElement');
if (multiple && !(element as HTMLInputElement).multiple)
return 'error:notmultiplefileinput';
throw injected.createStacklessError('Non-multiple file input can only accept single file');
return element;
}, files.length > 1);
if (retargeted === 'error:notconnected')
return retargeted;
if (!retargeted._objectId)
return throwFatalDOMError(retargeted.rawValue() as FatalDOMError | 'error:notconnected');
return retargeted.rawValue() as 'error:notconnected';
await progress.beforeInputAction(this);
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects.
Expand All @@ -608,8 +608,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {

async _focus(progress: Progress, resetSelectionIfNotFocused?: boolean): Promise<'error:notconnected' | 'done'> {
progress.throwIfAborted(); // Avoid action that has side-effects.
const result = await this.evaluateInUtility(([injected, node, resetSelectionIfNotFocused]) => injected.focusNode(node, resetSelectionIfNotFocused), resetSelectionIfNotFocused);
return throwFatalDOMError(result);
return await this.evaluateInUtility(([injected, node, resetSelectionIfNotFocused]) => injected.focusNode(node, resetSelectionIfNotFocused), resetSelectionIfNotFocused);
}

async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<void> {
Expand Down Expand Up @@ -673,7 +672,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _setChecked(progress: Progress, state: boolean, options: { position?: types.Point } & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
const isChecked = async () => {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
return throwRetargetableDOMError(throwFatalDOMError(result));
return throwRetargetableDOMError(result);
};
if (await isChecked() === state)
return 'done';
Expand Down Expand Up @@ -726,32 +725,32 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'visible'), {});
if (result === 'error:notconnected')
return false;
return throwFatalDOMError(result);
return result;
}

async isHidden(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'hidden'), {});
return throwRetargetableDOMError(throwFatalDOMError(result));
return throwRetargetableDOMError(result);
}

async isEnabled(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'enabled'), {});
return throwRetargetableDOMError(throwFatalDOMError(result));
return throwRetargetableDOMError(result);
}

async isDisabled(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'disabled'), {});
return throwRetargetableDOMError(throwFatalDOMError(result));
return throwRetargetableDOMError(result);
}

async isEditable(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'editable'), {});
return throwRetargetableDOMError(throwFatalDOMError(result));
return throwRetargetableDOMError(result);
}

async isChecked(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
return throwRetargetableDOMError(throwFatalDOMError(result));
return throwRetargetableDOMError(result);
}

async waitForElementState(metadata: CallMetadata, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise<void> {
Expand All @@ -762,7 +761,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return injected.waitForElementStatesAndPerformAction(node, [state], false, () => 'done' as const);
}, state);
const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll));
assertDone(throwRetargetableDOMError(throwFatalDOMError(await pollHandler.finish())));
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
}, this._page._timeoutSettings.timeout(options));
}

Expand Down Expand Up @@ -814,7 +813,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
progress.log(' element is visible, enabled and stable');
else
progress.log(' element is visible and stable');
return throwFatalDOMError(result);
return result;
}

async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> {
Expand Down Expand Up @@ -901,33 +900,7 @@ export class InjectedScriptPollHandler<T> {
}
}

export function throwFatalDOMError<T>(result: T | FatalDOMError): T {
if (result === 'error:notelement')
throw new Error('Node is not an element');
if (result === 'error:nothtmlelement')
throw new Error('Not an HTMLElement');
if (result === 'error:notfillableelement')
throw new Error('Element is not an <input>, <textarea> or [contenteditable] element');
if (result === 'error:notfillableinputtype')
throw new Error('Input of this type cannot be filled');
if (result === 'error:notfillablenumberinput')
throw new Error('Cannot type text into input[type=number]');
if (result === 'error:notvaliddate')
throw new Error(`Malformed value`);
if (result === 'error:notinput')
throw new Error('Node is not an HTMLInputElement');
if (result === 'error:hasnovalue')
throw new Error('Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement');
if (result === 'error:notselect')
throw new Error('Element is not a <select> element.');
if (result === 'error:notcheckbox')
throw new Error('Not a checkbox or radio button');
if (result === 'error:notmultiplefileinput')
throw new Error('Non-multiple file input can only accept single file');
return result;
}

export function throwRetargetableDOMError<T>(result: T | RetargetableDOMError): T {
export function throwRetargetableDOMError<T>(result: T | 'error:notconnected'): T {
if (result === 'error:notconnected')
throw new Error('Element is not attached to the DOM');
return result;
Expand Down
2 changes: 1 addition & 1 deletion src/server/firefox/ffExecutionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function checkException(exceptionDetails?: Protocol.Runtime.ExceptionDetails) {
if (exceptionDetails.value)
throw new js.JavaScriptErrorInEvaluate(JSON.stringify(exceptionDetails.value));
else
throw new js.JavaScriptErrorInEvaluate(exceptionDetails.text + '\n' + exceptionDetails.stack);
throw new js.JavaScriptErrorInEvaluate(exceptionDetails.text + (exceptionDetails.stack ? '\n' + exceptionDetails.stack : ''));
}

function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) {
Expand Down
9 changes: 4 additions & 5 deletions src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ export class Frame extends SdkObject {
async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
return this._scheduleRerunnableTask(metadata, selector, (progress, element) => {
if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml')
return 'error:nothtmlelement';
throw progress.injectedScript.createStacklessError('Node is not an HTMLElement');
return (element as HTMLElement).innerText;
}, undefined, options);
}
Expand All @@ -1063,7 +1063,7 @@ export class Frame extends SdkObject {
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise<string> {
return this._scheduleRerunnableTask(metadata, selector, (progress, element) => {
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
return 'error:hasnovalue';
throw progress.injectedScript.createStacklessError('Node is not an <input>, <textarea> or <select> element');
return (element as any).value;
}, undefined, options);
}
Expand All @@ -1073,7 +1073,7 @@ export class Frame extends SdkObject {
const injected = progress.injectedScript;
return injected.elementState(element, data.state);
}, { state }, options);
return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result));
return dom.throwRetargetableDOMError(result);
}

async isVisible(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}): Promise<boolean> {
Expand Down Expand Up @@ -1272,8 +1272,7 @@ export class Frame extends SdkObject {
rerunnableTask.terminate(new Error('Frame got detached.'));
if (data.context)
rerunnableTask.rerun(data.context);
const result = await rerunnableTask.promise;
return dom.throwFatalDOMError(result);
return await rerunnableTask.promise;
}, this._page._timeoutSettings.timeout(options));
}

Expand Down
Loading