chore: remove FatalDOMError (#9119)

We can now throw stackless errors instead.
Also fixed stackless errors on Firefox.
This commit is contained in:
Dmitry Gozman 2021-09-24 20:51:09 -07:00 committed by GitHub
parent d22dd4a4e7
commit ee25fefb62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 72 additions and 122 deletions

View File

@ -1,30 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type FatalDOMError =
'error:notelement' |
'error:nothtmlelement' |
'error:notfillableelement' |
'error:notfillableinputtype' |
'error:notfillablenumberinput' |
'error:notvaliddate' |
'error:notinput' |
'error:hasnovalue' |
'error:notselect' |
'error:notcheckbox' |
'error:notmultiplefileinput';
export type RetargetableDOMError = 'error:notconnected';

View File

@ -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';
@ -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')}]
);
})();
@ -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> {
@ -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 = {}) {
@ -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;
});
@ -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;
@ -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));
}
@ -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.
@ -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> {
@ -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';
@ -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> {
@ -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));
}
@ -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'> {
@ -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;

View File

@ -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) {

View File

@ -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);
}
@ -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);
}
@ -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> {
@ -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));
}

View File

@ -19,7 +19,6 @@ import { XPathEngine } from './xpathSelectorEngine';
import { ReactEngine } from './reactSelectorEngine';
import { VueEngine } from './vueSelectorEngine';
import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/selectorParser';
import { FatalDOMError } from '../common/domErrors';
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
import { CSSComplexSelectorList } from '../common/cssParser';
import { generateSelector } from './selectorGenerator';
@ -68,8 +67,9 @@ export class InjectedScript {
_evaluator: SelectorEvaluatorImpl;
private _stableRafCount: number;
private _replaceRafWithTimeout: boolean;
private _browserName: string;
constructor(stableRafCount: number, replaceRafWithTimeout: boolean, customEngines: { name: string, engine: SelectorEngine}[]) {
constructor(stableRafCount: number, replaceRafWithTimeout: boolean, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) {
this._evaluator = new SelectorEvaluatorImpl(new Map());
this._engines = new Map();
@ -96,6 +96,7 @@ export class InjectedScript {
this._stableRafCount = stableRafCount;
this._replaceRafWithTimeout = replaceRafWithTimeout;
this._browserName = browserName;
}
eval(expression: string): any {
@ -396,7 +397,7 @@ export class InjectedScript {
}
waitForElementStatesAndPerformAction<T>(node: Node, states: ElementState[], force: boolean | undefined,
callback: (node: Node, progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol): InjectedScriptPoll<T | 'error:notconnected' | FatalDOMError> {
callback: (node: Node, progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol): InjectedScriptPoll<T | 'error:notconnected'> {
let lastRect: { x: number, y: number, width: number, height: number } | undefined;
let counter = 0;
let samePositionCounter = 0;
@ -461,7 +462,7 @@ export class InjectedScript {
return this.pollRaf(predicate);
}
elementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' | 'error:notcheckbox' {
elementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' {
const element = this.retarget(node, ['stable', 'visible', 'hidden'].includes(state) ? 'no-follow-label' : 'follow-label');
if (!element || !element.isConnected) {
if (state === 'hidden')
@ -488,21 +489,21 @@ export class InjectedScript {
if (['checkbox', 'radio'].includes(element.getAttribute('role') || ''))
return element.getAttribute('aria-checked') === 'true';
if (element.nodeName !== 'INPUT')
return 'error:notcheckbox';
throw this.createStacklessError('Not a checkbox or radio button');
if (!['radio', 'checkbox'].includes((element as HTMLInputElement).type.toLowerCase()))
return 'error:notcheckbox';
throw this.createStacklessError('Not a checkbox or radio button');
return (element as HTMLInputElement).checked;
}
throw this.createStacklessError(`Unexpected element state "${state}"`);
}
selectOptions(optionsToSelect: (Node | { value?: string, label?: string, index?: number })[],
node: Node, progress: InjectedScriptProgress, continuePolling: symbol): string[] | 'error:notconnected' | FatalDOMError | symbol {
node: Node, progress: InjectedScriptProgress, continuePolling: symbol): string[] | 'error:notconnected' | symbol {
const element = this.retarget(node, 'follow-label');
if (!element)
return 'error:notconnected';
if (element.nodeName.toLowerCase() !== 'select')
return 'error:notselect';
throw this.createStacklessError('Element is not a <select> element');
const select = element as HTMLSelectElement;
const options = [...select.options];
const selectedOptions = [];
@ -543,7 +544,7 @@ export class InjectedScript {
return selectedOptions.map(option => option.value);
}
fill(value: string, node: Node, progress: InjectedScriptProgress): FatalDOMError | 'error:notconnected' | 'needsinput' | 'done' {
fill(value: string, node: Node, progress: InjectedScriptProgress): 'error:notconnected' | 'needsinput' | 'done' {
const element = this.retarget(node, 'follow-label');
if (!element)
return 'error:notconnected';
@ -554,19 +555,19 @@ export class InjectedScript {
const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
if (!kTextInputTypes.has(type) && !kDateTypes.has(type)) {
progress.log(` input of type "${type}" cannot be filled`);
return 'error:notfillableinputtype';
throw this.createStacklessError(`Input of type "${type}" cannot be filled`);
}
if (type === 'number') {
value = value.trim();
if (isNaN(Number(value)))
return 'error:notfillablenumberinput';
throw this.createStacklessError('Cannot type text into input[type=number]');
}
if (kDateTypes.has(type)) {
value = value.trim();
input.focus();
input.value = value;
if (input.value !== value)
return 'error:notvaliddate';
throw this.createStacklessError('Malformed value');
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return 'done'; // We have already changed the value, no need to input it.
@ -574,7 +575,7 @@ export class InjectedScript {
} else if (element.nodeName.toLowerCase() === 'textarea') {
// Nothing to check here.
} else if (!(element as HTMLElement).isContentEditable) {
return 'error:notfillableelement';
throw this.createStacklessError('Element is not an <input>, <textarea> or [contenteditable] element');
}
this.selectText(element);
return 'needsinput'; // Still need to input the value.
@ -608,11 +609,11 @@ export class InjectedScript {
return 'done';
}
focusNode(node: Node, resetSelectionIfNotFocused?: boolean): FatalDOMError | 'error:notconnected' | 'done' {
focusNode(node: Node, resetSelectionIfNotFocused?: boolean): 'error:notconnected' | 'done' {
if (!node.isConnected)
return 'error:notconnected';
if (node.nodeType !== Node.ELEMENT_NODE)
return 'error:notelement';
throw this.createStacklessError('Node is not an element');
const wasFocused = (node.getRootNode() as (Document | ShadowRoot)).activeElement === node && node.ownerDocument && node.ownerDocument.hasFocus();
(node as HTMLElement | SVGElement).focus();
@ -762,7 +763,14 @@ export class InjectedScript {
}
createStacklessError(message: string): Error {
if (this._browserName === 'firefox') {
const error = new Error('Error: ' + message);
// Firefox cannot delete the stack, so assign to an empty string.
error.stack = '';
return error;
}
const error = new Error(message);
// Chromium/WebKit should delete the stack instead.
delete error.stack;
return error;
}

View File

@ -227,7 +227,7 @@ it.describe('pause', () => {
'page.isChecked(button)- XXms',
'waiting for selector "button"',
'selector resolved to <button onclick=\"console.log(1)\">Submit</button>',
'error: Not a checkbox or radio button',
'error: Error: Not a checkbox or radio button',
]);
const error = await scriptPromise;
expect(error.message).toContain('Not a checkbox or radio button');

View File

@ -53,9 +53,9 @@ it('inputValue should work', async ({ page, server }) => {
const handle = await page.$('#input');
expect(await handle.inputValue()).toBe('input value');
expect(await page.inputValue('#inner').catch(e => e.message)).toContain('Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement');
expect(await page.inputValue('#inner').catch(e => e.message)).toContain('Node is not an <input>, <textarea> or <select> element');
const handle2 = await page.$('#inner');
expect(await handle2.inputValue().catch(e => e.message)).toContain('Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement');
expect(await handle2.inputValue().catch(e => e.message)).toContain('Node is not an <input>, <textarea> or <select> element');
});
it('innerHTML should work', async ({ page, server }) => {
@ -75,10 +75,10 @@ it('innerText should work', async ({ page, server }) => {
it('innerText should throw', async ({ page, server }) => {
await page.setContent(`<svg>text</svg>`);
const error1 = await page.innerText('svg').catch(e => e);
expect(error1.message).toContain('Not an HTMLElement');
expect(error1.message).toContain('Node is not an HTMLElement');
const handle = await page.$('svg');
const error2 = await handle.innerText().catch(e => e);
expect(error2.message).toContain('Not an HTMLElement');
expect(error2.message).toContain('Node is not an HTMLElement');
});
it('textContent should work', async ({ page, server }) => {

View File

@ -53,9 +53,9 @@ it('inputValue should work', async ({ page, server }) => {
const locator = page.locator('#input');
expect(await locator.inputValue()).toBe('input value');
expect(await page.inputValue('#inner').catch(e => e.message)).toContain('Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement');
expect(await page.inputValue('#inner').catch(e => e.message)).toContain('Node is not an <input>, <textarea> or <select> element');
const locator2 = page.locator('#inner');
expect(await locator2.inputValue().catch(e => e.message)).toContain('Node is not an HTMLInputElement or HTMLTextAreaElement or HTMLSelectElement');
expect(await locator2.inputValue().catch(e => e.message)).toContain('Node is not an <input>, <textarea> or <select> element');
});
it('innerHTML should work', async ({ page, server }) => {
@ -75,10 +75,10 @@ it('innerText should work', async ({ page, server }) => {
it('innerText should throw', async ({ page, server }) => {
await page.setContent(`<svg>text</svg>`);
const error1 = await page.innerText('svg').catch(e => e);
expect(error1.message).toContain('Not an HTMLElement');
expect(error1.message).toContain('Node is not an HTMLElement');
const locator = page.locator('svg');
const error2 = await locator.innerText().catch(e => e);
expect(error2.message).toContain('Not an HTMLElement');
expect(error2.message).toContain('Node is not an HTMLElement');
});
it('innerText should produce log', async ({ page, server }) => {

View File

@ -186,11 +186,11 @@ it('should fill elements with existing value and selection', async ({page, serve
expect(await page.$eval('div[contenteditable]', div => div.textContent)).toBe('replace with this');
});
it('should throw when element is not an <input>, <textarea> or [contenteditable]', async ({page, server}) => {
it('should throw nice error without injected script stack when element is not an <input>', async ({page, server}) => {
let error = null;
await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill('body', '').catch(e => error = e);
expect(error.message).toContain('Element is not an <input>');
expect(error.message).toContain('page.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element\n=========================== logs');
});
it('should throw if passed a non-string value', async ({page, server}) => {

View File

@ -139,7 +139,7 @@ it('should throw when element is not a <select>', async ({page, server}) => {
let error = null;
await page.goto(server.PREFIX + '/input/select.html');
await page.selectOption('body', '').catch(e => error = e);
expect(error.message).toContain('Element is not a <select> element.');
expect(error.message).toContain('Element is not a <select> element');
});
it('should return [] on no matched values', async ({page, server}) => {