mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: explicitly plumb various errors through the retries (#2554)
We are about to have more errors, so explicit plumbing helps with visibility.
This commit is contained in:
parent
39019e8863
commit
d4c466868b
@ -35,7 +35,6 @@ import { CRPDF } from './crPdf';
|
||||
import { CRBrowserContext } from './crBrowser';
|
||||
import * as types from '../types';
|
||||
import { ConsoleMessage } from '../console';
|
||||
import { NotConnectedError } from '../errors';
|
||||
import * as sourceMap from '../utils/sourceMap';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
@ -245,7 +244,7 @@ export class CRPage implements PageDelegate {
|
||||
return this._sessionForHandle(handle)._getBoundingBox(handle);
|
||||
}
|
||||
|
||||
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
|
||||
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'notvisible' | 'notconnected' | 'done'> {
|
||||
return this._sessionForHandle(handle)._scrollRectIntoViewIfNeeded(handle, rect);
|
||||
}
|
||||
|
||||
@ -832,15 +831,15 @@ class FrameSession {
|
||||
return {x, y, width, height};
|
||||
}
|
||||
|
||||
async _scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
|
||||
async _scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'notvisible' | 'notconnected' | 'done'> {
|
||||
return await this._client.send('DOM.scrollIntoViewIfNeeded', {
|
||||
objectId: handle._objectId,
|
||||
rect,
|
||||
}).then(() => 'success' as const).catch(e => {
|
||||
}).then(() => 'done' as const).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
|
||||
return 'invisible';
|
||||
return 'notvisible';
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||
throw new NotConnectedError();
|
||||
return 'notconnected';
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
223
src/dom.ts
223
src/dom.ts
@ -28,7 +28,6 @@ import * as js from './javascript';
|
||||
import { Page } from './page';
|
||||
import { selectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import { NotConnectedError } from './errors';
|
||||
import { apiLog } from './logger';
|
||||
import { Progress, runAbortableTask } from './progress';
|
||||
import DebugScript from './debug/injected/debugScript';
|
||||
@ -208,15 +207,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
injected.dispatchEvent(node, type, eventInit), { type, eventInit });
|
||||
}
|
||||
|
||||
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<'success' | 'invisible'> {
|
||||
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<'notvisible' | 'notconnected' | 'done'> {
|
||||
return await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);
|
||||
}
|
||||
|
||||
async scrollIntoViewIfNeeded() {
|
||||
await this._scrollRectIntoViewIfNeeded();
|
||||
throwIfNotConnected(await this._scrollRectIntoViewIfNeeded());
|
||||
}
|
||||
|
||||
private async _clickablePoint(): Promise<types.Point | 'invisible' | 'outsideviewport'> {
|
||||
private async _clickablePoint(): Promise<types.Point | 'notvisible' | 'notinviewport'> {
|
||||
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
|
||||
return quad.map(point => ({
|
||||
x: Math.min(Math.max(point.x, 0), metrics.width),
|
||||
@ -241,11 +240,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
this._page._delegate.layoutViewport(),
|
||||
] as const);
|
||||
if (!quads || !quads.length)
|
||||
return 'invisible';
|
||||
return 'notvisible';
|
||||
|
||||
const filtered = quads.map(quad => intersectQuadWithViewport(quad)).filter(quad => computeQuadArea(quad) > 1);
|
||||
if (!filtered.length)
|
||||
return 'outsideviewport';
|
||||
return 'notinviewport';
|
||||
// Return the middle point of the first quad.
|
||||
const result = { x: 0, y: 0 };
|
||||
for (const point of filtered[0]) {
|
||||
@ -255,13 +254,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _offsetPoint(offset: types.Point): Promise<types.Point | 'invisible'> {
|
||||
private async _offsetPoint(offset: types.Point): Promise<types.Point | 'notvisible'> {
|
||||
const [box, border] = await Promise.all([
|
||||
this.boundingBox(),
|
||||
this._evaluateInUtility(([injected, node]) => injected.getElementBorderWidth(node), {}).catch(e => {}),
|
||||
]);
|
||||
if (!box || !border)
|
||||
return 'invisible';
|
||||
return 'notvisible';
|
||||
// Make point relative to the padding box to align with offsetX/offsetY.
|
||||
return {
|
||||
x: box.x + border.left + offset.x,
|
||||
@ -269,60 +268,73 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
};
|
||||
}
|
||||
|
||||
async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
|
||||
async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
let first = true;
|
||||
while (progress.isRunning()) {
|
||||
progress.log(apiLog, `${first ? 'attempting' : 'retrying'} ${progress.apiName} action`);
|
||||
const result = await this._performPointerAction(progress, action, options);
|
||||
if (result === 'done')
|
||||
return;
|
||||
first = false;
|
||||
if (result === 'notvisible') {
|
||||
if (options.force)
|
||||
throw new Error('Element is not visible');
|
||||
progress.log(apiLog, ' element is not visible');
|
||||
continue;
|
||||
}
|
||||
if (result === 'notinviewport') {
|
||||
if (options.force)
|
||||
throw new Error('Element is outside of the viewport');
|
||||
progress.log(apiLog, ' element is outside of the viewport');
|
||||
continue;
|
||||
}
|
||||
if (result === 'nothittarget') {
|
||||
if (options.force)
|
||||
throw new Error('Element does not receive pointer events');
|
||||
progress.log(apiLog, ' element does not receive pointer events');
|
||||
continue;
|
||||
}
|
||||
if (result === 'notconnected')
|
||||
return result;
|
||||
break;
|
||||
}
|
||||
return 'done';
|
||||
}
|
||||
|
||||
async _performPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'done' | 'retry'> {
|
||||
async _performPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notvisible' | 'notconnected' | 'notinviewport' | 'nothittarget' | 'done'> {
|
||||
const { force = false, position } = options;
|
||||
if (!force)
|
||||
await this._waitForDisplayedAtStablePositionAndEnabled(progress);
|
||||
if (!force) {
|
||||
const result = await this._waitForDisplayedAtStablePositionAndEnabled(progress);
|
||||
if (result === 'notconnected')
|
||||
return 'notconnected';
|
||||
}
|
||||
if ((options as any).__testHookAfterStable)
|
||||
await (options as any).__testHookAfterStable();
|
||||
|
||||
progress.log(apiLog, ' scrolling into view if needed');
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
|
||||
if (scrolled === 'invisible') {
|
||||
if (force)
|
||||
throw new Error('Element is not visible');
|
||||
progress.log(apiLog, ' element is not visible');
|
||||
return 'retry';
|
||||
}
|
||||
if (scrolled === 'notvisible')
|
||||
return 'notvisible';
|
||||
if (scrolled === 'notconnected')
|
||||
return 'notconnected';
|
||||
progress.log(apiLog, ' done scrolling');
|
||||
|
||||
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
|
||||
if (maybePoint === 'invisible') {
|
||||
if (force)
|
||||
throw new Error('Element is not visible');
|
||||
progress.log(apiLog, ' element is not visibile');
|
||||
return 'retry';
|
||||
}
|
||||
if (maybePoint === 'outsideviewport') {
|
||||
if (force)
|
||||
throw new Error('Element is outside of the viewport');
|
||||
progress.log(apiLog, ' element is outside of the viewport');
|
||||
return 'retry';
|
||||
}
|
||||
if (maybePoint === 'notvisible')
|
||||
return 'notvisible';
|
||||
if (maybePoint === 'notinviewport')
|
||||
return 'notinviewport';
|
||||
const point = roundPoint(maybePoint);
|
||||
|
||||
if (!force) {
|
||||
if ((options as any).__testHookBeforeHitTarget)
|
||||
await (options as any).__testHookBeforeHitTarget();
|
||||
progress.log(apiLog, ` checking that element receives pointer events at (${point.x},${point.y})`);
|
||||
const matchesHitTarget = await this._checkHitTargetAt(point);
|
||||
if (!matchesHitTarget) {
|
||||
progress.log(apiLog, ' element does not receive pointer events');
|
||||
return 'retry';
|
||||
}
|
||||
progress.log(apiLog, ` element does receive pointer events, continuing input action`);
|
||||
const hitTargetResult = await this._checkHitTargetAt(point);
|
||||
if (hitTargetResult === 'notconnected')
|
||||
return 'notconnected';
|
||||
if (hitTargetResult === 'nothittarget')
|
||||
return 'nothittarget';
|
||||
progress.log(apiLog, ` element does receive pointer events`);
|
||||
}
|
||||
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
@ -347,34 +359,42 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(progress => this._hover(progress, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._hover(progress, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
_hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
|
||||
_hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
return this._retryPointerAction(progress, point => this._page.mouse.move(point.x, point.y), options);
|
||||
}
|
||||
|
||||
click(options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(progress => this._click(progress, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._click(progress, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
_click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
|
||||
_click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
return this._retryPointerAction(progress, point => this._page.mouse.click(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
dblclick(options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(progress => this._dblclick(progress, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._dblclick(progress, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
_dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
|
||||
_dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
return this._retryPointerAction(progress, point => this._page.mouse.dblclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
|
||||
return runAbortableTask(progress => this._selectOption(progress, values, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return runAbortableTask(async progress => {
|
||||
return throwIfNotConnected(await this._selectOption(progress, values, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[]> {
|
||||
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[] | 'notconnected'> {
|
||||
let vals: string[] | ElementHandle[] | types.SelectOption[];
|
||||
if (values === null)
|
||||
vals = [];
|
||||
@ -394,61 +414,68 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
if (option.index !== undefined)
|
||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||
}
|
||||
return this._page._frameManager.waitForSignalsCreatedBy<string[]>(progress, options.noWaitAfter, async () => {
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const injectedResult = await this._evaluateInUtility(([injected, node, selectOptions]) => injected.selectOptions(node, selectOptions), selectOptions);
|
||||
return handleInjectedResult(injectedResult);
|
||||
return throwIfError(injectedResult);
|
||||
});
|
||||
}
|
||||
|
||||
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(progress => this._fill(progress, value, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._fill(progress, value, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<void> {
|
||||
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, `elementHandle.fill("${value}")`);
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.log(apiLog, ' waiting for element to be visible, enabled and editable');
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
|
||||
return injected.waitForEnabledAndFill(node, value);
|
||||
}, value);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
const injectedResult = await pollHandler.finish();
|
||||
const needsInput = handleInjectedResult(injectedResult);
|
||||
progress.log(apiLog, ' element is visible, enabled and editable');
|
||||
const filled = throwIfError(await pollHandler.finish());
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
if (needsInput) {
|
||||
if (filled === 'notconnected')
|
||||
return 'notconnected';
|
||||
progress.log(apiLog, ' element is visible, enabled and editable');
|
||||
if (filled === 'needsinput') {
|
||||
if (value)
|
||||
await this._page.keyboard.insertText(value);
|
||||
else
|
||||
await this._page.keyboard.press('Delete');
|
||||
}
|
||||
return 'done';
|
||||
}, 'input');
|
||||
}
|
||||
|
||||
async selectText(): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {});
|
||||
handleInjectedResult(injectedResult);
|
||||
const selected = throwIfError(await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {}));
|
||||
throwIfNotConnected(selected);
|
||||
}, this._page._logger, 0);
|
||||
}
|
||||
|
||||
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
|
||||
return runAbortableTask(async progress => this._setInputFiles(progress, files, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._setInputFiles(progress, files, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions) {
|
||||
const injectedResult = await this._evaluateInUtility(([injected, node]): types.InjectedScriptResult<boolean> => {
|
||||
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
const multiple = throwIfError(await this._evaluateInUtility(([injected, node]): types.InjectedScriptResult<'notconnected' | boolean> => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
|
||||
return { status: 'error', error: 'Node is not an HTMLInputElement' };
|
||||
return { error: 'Node is not an HTMLInputElement', value: false };
|
||||
if (!node.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
const input = node as Node as HTMLInputElement;
|
||||
return { status: 'success', value: input.multiple };
|
||||
}, {});
|
||||
const multiple = handleInjectedResult(injectedResult);
|
||||
return { value: input.multiple };
|
||||
}, {}));
|
||||
if (multiple === 'notconnected')
|
||||
return 'notconnected';
|
||||
let ff: string[] | types.FilePayload[];
|
||||
if (!Array.isArray(files))
|
||||
ff = [ files ] as string[] | types.FilePayload[];
|
||||
@ -472,41 +499,53 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads);
|
||||
});
|
||||
return 'done';
|
||||
}
|
||||
|
||||
async focus() {
|
||||
return runAbortableTask(progress => this._focus(progress), this._page._logger, 0);
|
||||
async focus(): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._focus(progress));
|
||||
}, this._page._logger, 0);
|
||||
}
|
||||
|
||||
async _focus(progress: Progress) {
|
||||
async _focus(progress: Progress): Promise<'notconnected' | 'done'> {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {});
|
||||
handleInjectedResult(injectedResult);
|
||||
return throwIfError(await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {}));
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
||||
return runAbortableTask(progress => this._type(progress, text, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._type(progress, text, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, `elementHandle.type("${text}")`);
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
await this._focus(progress);
|
||||
const focused = await this._focus(progress);
|
||||
if (focused === 'notconnected')
|
||||
return 'notconnected';
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await this._page.keyboard.type(text, options);
|
||||
return 'done';
|
||||
}, 'input');
|
||||
}
|
||||
|
||||
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
||||
return runAbortableTask(progress => this._press(progress, key, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._press(progress, key, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, `elementHandle.press("${key}")`);
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
await this._focus(progress);
|
||||
const focused = await this._focus(progress);
|
||||
if (focused === 'notconnected')
|
||||
return 'notconnected';
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await this._page.keyboard.press(key, options);
|
||||
return 'done';
|
||||
}, 'input');
|
||||
}
|
||||
|
||||
@ -562,32 +601,32 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<void> {
|
||||
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, ' waiting for element to be visible, enabled and not moving');
|
||||
const rafCount = this._page._delegate.rafCountForStablePosition();
|
||||
const poll = this._evaluateHandleInUtility(([injected, node, rafCount]) => {
|
||||
return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount);
|
||||
}, rafCount);
|
||||
const pollHandler = new InjectedScriptPollHandler<types.InjectedScriptResult>(progress, await poll);
|
||||
const injectedResult = await pollHandler.finish();
|
||||
handleInjectedResult(injectedResult);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
|
||||
const result = throwIfError(await pollHandler.finish());
|
||||
progress.log(apiLog, ' element is visible, enabled and does not move');
|
||||
return result;
|
||||
}
|
||||
|
||||
async _checkHitTargetAt(point: types.Point): Promise<boolean> {
|
||||
async _checkHitTargetAt(point: types.Point): Promise<'notconnected' | 'nothittarget' | 'done'> {
|
||||
const frame = await this.ownerFrame();
|
||||
if (frame && frame.parentFrame()) {
|
||||
const element = await frame.frameElement();
|
||||
const box = await element.boundingBox();
|
||||
if (!box)
|
||||
throw new NotConnectedError();
|
||||
return 'notconnected';
|
||||
// Translate from viewport coordinates to frame coordinates.
|
||||
point = { x: point.x - box.x, y: point.y - box.y };
|
||||
}
|
||||
const injectedResult = await this._evaluateInUtility(([injected, node, point]) => {
|
||||
return injected.checkHitTargetAt(node, point);
|
||||
}, point);
|
||||
return handleInjectedResult(injectedResult);
|
||||
return throwIfError(injectedResult);
|
||||
}
|
||||
}
|
||||
|
||||
@ -668,12 +707,16 @@ export function toFileTransferPayload(files: types.FilePayload[]): types.FileTra
|
||||
}));
|
||||
}
|
||||
|
||||
function handleInjectedResult<T = undefined>(injectedResult: types.InjectedScriptResult<T>): T {
|
||||
if (injectedResult.status === 'notconnected')
|
||||
throw new NotConnectedError();
|
||||
if (injectedResult.status === 'error')
|
||||
function throwIfError<T>(injectedResult: types.InjectedScriptResult<T>): T {
|
||||
if (injectedResult.error)
|
||||
throw new Error(injectedResult.error);
|
||||
return injectedResult.value as T;
|
||||
return injectedResult.value!;
|
||||
}
|
||||
|
||||
function throwIfNotConnected<T>(result: 'notconnected' | T): T {
|
||||
if (result === 'notconnected')
|
||||
throw new Error('Element is not attached to the DOM');
|
||||
return result;
|
||||
}
|
||||
|
||||
function roundPoint(point: types.Point): types.Point {
|
||||
|
||||
@ -23,10 +23,4 @@ class CustomError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class NotConnectedError extends CustomError {
|
||||
constructor() {
|
||||
super('Element is not attached to the DOM');
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutError extends CustomError {}
|
||||
|
||||
@ -31,7 +31,6 @@ import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
|
||||
import { FFNetworkManager, headersArray } from './ffNetworkManager';
|
||||
import { Protocol } from './protocol';
|
||||
import { selectors } from '../selectors';
|
||||
import { NotConnectedError } from '../errors';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||
@ -411,14 +410,14 @@ export class FFPage implements PageDelegate {
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
|
||||
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
|
||||
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'notvisible' | 'notconnected' | 'done'> {
|
||||
return await this._session.send('Page.scrollIntoViewIfNeeded', {
|
||||
frameId: handle._context.frame._id,
|
||||
objectId: handle._objectId,
|
||||
rect,
|
||||
}).then(() => 'success' as const).catch(e => {
|
||||
}).then(() => 'done' as const).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||
throw new NotConnectedError();
|
||||
return 'notconnected';
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import { ConsoleMessage } from './console';
|
||||
import * as dom from './dom';
|
||||
import { NotConnectedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { assert, helper, RegisteredListener, assertMaxArguments, debugAssert } from './helper';
|
||||
import * as js from './javascript';
|
||||
@ -452,7 +451,7 @@ export class Frame {
|
||||
throw new Error('options.waitFor is not supported, did you mean options.state?');
|
||||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`Unsupported waitFor option "${state}"`);
|
||||
throw new Error(`Unsupported state option "${state}"`);
|
||||
const { world, task } = selectors._waitForSelectorTask(selector, state);
|
||||
return runAbortableTask(async progress => {
|
||||
progress.log(apiLog, `waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
@ -707,23 +706,21 @@ export class Frame {
|
||||
|
||||
private async _retryWithSelectorIfNotConnected<R>(
|
||||
selector: string, options: types.TimeoutOptions,
|
||||
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R>): Promise<R> {
|
||||
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'notconnected'>): Promise<R> {
|
||||
return runAbortableTask(async progress => {
|
||||
while (progress.isRunning()) {
|
||||
try {
|
||||
progress.log(apiLog, `waiting for selector "${selector}"`);
|
||||
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
|
||||
const handle = await this._scheduleRerunnableTask(progress, world, task);
|
||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||
progress.cleanupWhenAborted(() => element.dispose());
|
||||
const result = await action(progress, element);
|
||||
element.dispose();
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (!(e instanceof NotConnectedError))
|
||||
throw e;
|
||||
progress.log(apiLog, `waiting for selector "${selector}"`);
|
||||
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
|
||||
const handle = await this._scheduleRerunnableTask(progress, world, task);
|
||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||
progress.cleanupWhenAborted(() => element.dispose());
|
||||
const result = await action(progress, element);
|
||||
element.dispose();
|
||||
if (result === 'notconnected') {
|
||||
progress.log(apiLog, 'element was detached from the DOM, retrying');
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return undefined as any;
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
@ -803,7 +800,7 @@ export class Frame {
|
||||
else if (helper.isNumber(polling))
|
||||
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
||||
else
|
||||
throw new Error('Unknown polling options: ' + polling);
|
||||
throw new Error('Unknown polling option: ' + polling);
|
||||
const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(arg)';
|
||||
const task = async (context: dom.FrameExecutionContext) => {
|
||||
const injectedScript = await context.injectedScript();
|
||||
|
||||
@ -200,11 +200,11 @@ export default class InjectedScript {
|
||||
return { left: parseInt(style.borderLeftWidth || '', 10), top: parseInt(style.borderTopWidth || '', 10) };
|
||||
}
|
||||
|
||||
selectOptions(node: Node, optionsToSelect: (Node | types.SelectOption)[]): types.InjectedScriptResult<string[]> {
|
||||
selectOptions(node: Node, optionsToSelect: (Node | types.SelectOption)[]): types.InjectedScriptResult<string[] | 'notconnected'> {
|
||||
if (node.nodeName.toLowerCase() !== 'select')
|
||||
return { status: 'error', error: 'Element is not a <select> element.' };
|
||||
return { error: 'Element is not a <select> element.' };
|
||||
if (!node.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
const element = node as HTMLSelectElement;
|
||||
|
||||
const options = Array.from(element.options);
|
||||
@ -228,16 +228,16 @@ export default class InjectedScript {
|
||||
}
|
||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
return { status: 'success', value: options.filter(option => option.selected).map(option => option.value) };
|
||||
return { value: options.filter(option => option.selected).map(option => option.value) };
|
||||
}
|
||||
|
||||
waitForEnabledAndFill(node: Node, value: string): types.InjectedScriptPoll<types.InjectedScriptResult<boolean>> {
|
||||
waitForEnabledAndFill(node: Node, value: string): types.InjectedScriptPoll<types.InjectedScriptResult<'notconnected' | 'needsinput' | 'done'>> {
|
||||
return this.poll('raf', progress => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return { status: 'error', error: 'Node is not of type HTMLElement' };
|
||||
return { error: 'Node is not of type HTMLElement' };
|
||||
const element = node as HTMLElement;
|
||||
if (!element.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
if (!this.isVisible(element)) {
|
||||
progress.logRepeating(' element is not visible - waiting...');
|
||||
return false;
|
||||
@ -248,11 +248,11 @@ export default class InjectedScript {
|
||||
const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']);
|
||||
const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
|
||||
if (!kTextInputTypes.has(type) && !kDateTypes.has(type))
|
||||
return { status: 'error', error: 'Cannot fill input of type "' + type + '".' };
|
||||
return { error: 'Cannot fill input of type "' + type + '".' };
|
||||
if (type === 'number') {
|
||||
value = value.trim();
|
||||
if (isNaN(Number(value)))
|
||||
return { status: 'error', error: 'Cannot type text into input[type=number].' };
|
||||
return { error: 'Cannot type text into input[type=number].' };
|
||||
}
|
||||
if (input.disabled) {
|
||||
progress.logRepeating(' element is disabled - waiting...');
|
||||
@ -267,10 +267,10 @@ export default class InjectedScript {
|
||||
input.focus();
|
||||
input.value = value;
|
||||
if (input.value !== value)
|
||||
return { status: 'error', error: `Malformed ${type} "${value}"` };
|
||||
return { error: `Malformed ${type} "${value}"` };
|
||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
return { status: 'success', value: false }; // We have already changed the value, no need to input it.
|
||||
return { value: 'done' }; // We have already changed the value, no need to input it.
|
||||
}
|
||||
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
||||
const textarea = element as HTMLTextAreaElement;
|
||||
@ -283,54 +283,56 @@ export default class InjectedScript {
|
||||
return false;
|
||||
}
|
||||
} else if (!element.isContentEditable) {
|
||||
return { status: 'error', error: 'Element is not an <input>, <textarea> or [contenteditable] element.' };
|
||||
return { error: 'Element is not an <input>, <textarea> or [contenteditable] element.' };
|
||||
}
|
||||
const result = this.selectText(node);
|
||||
if (result.status === 'success')
|
||||
return { status: 'success', value: true }; // Still need to input the value.
|
||||
return result;
|
||||
if (result.error)
|
||||
return { error: result.error };
|
||||
if (result.value === 'notconnected')
|
||||
return { value: 'notconnected' };
|
||||
return { value: 'needsinput' }; // Still need to input the value.
|
||||
});
|
||||
}
|
||||
|
||||
selectText(node: Node): types.InjectedScriptResult {
|
||||
selectText(node: Node): types.InjectedScriptResult<'notconnected' | 'done'> {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return { status: 'error', error: 'Node is not of type HTMLElement' };
|
||||
return { error: 'Node is not of type HTMLElement' };
|
||||
if (!node.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
const element = node as HTMLElement;
|
||||
if (!this.isVisible(element))
|
||||
return { status: 'error', error: 'Element is not visible' };
|
||||
return { error: 'Element is not visible' };
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
const input = element as HTMLInputElement;
|
||||
input.select();
|
||||
input.focus();
|
||||
return { status: 'success' };
|
||||
return { value: 'done' };
|
||||
}
|
||||
if (element.nodeName.toLowerCase() === 'textarea') {
|
||||
const textarea = element as HTMLTextAreaElement;
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = textarea.value.length;
|
||||
textarea.focus();
|
||||
return { status: 'success' };
|
||||
return { value: 'done' };
|
||||
}
|
||||
const range = element.ownerDocument!.createRange();
|
||||
range.selectNodeContents(element);
|
||||
const selection = element.ownerDocument!.defaultView!.getSelection();
|
||||
if (!selection)
|
||||
return { status: 'error', error: 'Element belongs to invisible iframe.' };
|
||||
return { error: 'Element belongs to invisible iframe.' };
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
element.focus();
|
||||
return { status: 'success' };
|
||||
return { value: 'done' };
|
||||
}
|
||||
|
||||
focusNode(node: Node): types.InjectedScriptResult {
|
||||
focusNode(node: Node): types.InjectedScriptResult<'notconnected' | 'done'> {
|
||||
if (!node.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
if (!(node as any)['focus'])
|
||||
return { status: 'error', error: 'Node is not an HTML or SVG element.' };
|
||||
return { error: 'Node is not an HTML or SVG element.' };
|
||||
(node as HTMLElement | SVGElement).focus();
|
||||
return { status: 'success' };
|
||||
return { value: 'done' };
|
||||
}
|
||||
|
||||
isCheckboxChecked(node: Node) {
|
||||
@ -379,26 +381,26 @@ export default class InjectedScript {
|
||||
input.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
}
|
||||
|
||||
waitForDisplayedAtStablePositionAndEnabled(node: Node, rafCount: number): types.InjectedScriptPoll<types.InjectedScriptResult> {
|
||||
waitForDisplayedAtStablePositionAndEnabled(node: Node, rafCount: number): types.InjectedScriptPoll<types.InjectedScriptResult<'notconnected' | 'done'>> {
|
||||
return this._runCancellablePoll(async progress => {
|
||||
if (!node.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||
if (!element)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
|
||||
let lastRect: types.Rect | undefined;
|
||||
let counter = 0;
|
||||
let samePositionCounter = 0;
|
||||
let lastTime = 0;
|
||||
return this._pollRaf(progress, (): types.InjectedScriptResult | false => {
|
||||
return this._pollRaf(progress, (): types.InjectedScriptResult<'notconnected' | 'done'> | false => {
|
||||
// First raf happens in the same animation frame as evaluation, so it does not produce
|
||||
// any client rect difference compared to synchronous call. We skip the synchronous call
|
||||
// and only force layout during actual rafs as a small optimisation.
|
||||
if (++counter === 1)
|
||||
return false;
|
||||
if (!node.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
|
||||
// Drop frames that are shorter than 16ms - WebKit Win bug.
|
||||
const time = performance.now();
|
||||
@ -426,7 +428,7 @@ export default class InjectedScript {
|
||||
const isDisabled = ['BUTTON', 'INPUT', 'SELECT'].includes(elementOrButton.nodeName) && elementOrButton.hasAttribute('disabled');
|
||||
|
||||
if (isDisplayed && isStable && isVisible && !isDisabled)
|
||||
return { status: 'success' };
|
||||
return { value: 'done' };
|
||||
|
||||
if (!isDisplayed || !isVisible)
|
||||
progress.logRepeating(` element is not visible - waiting...`);
|
||||
@ -439,15 +441,15 @@ export default class InjectedScript {
|
||||
});
|
||||
}
|
||||
|
||||
checkHitTargetAt(node: Node, point: types.Point): types.InjectedScriptResult<boolean> {
|
||||
checkHitTargetAt(node: Node, point: types.Point): types.InjectedScriptResult<'notconnected' | 'nothittarget' | 'done'> {
|
||||
let element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||
if (!element || !element.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
return { value: 'notconnected' };
|
||||
element = element.closest('button, [role=button]') || element;
|
||||
let hitElement = this.deepElementFromPoint(document, point.x, point.y);
|
||||
while (hitElement && hitElement !== element)
|
||||
hitElement = this._parentElementOrShadowHost(hitElement);
|
||||
return { status: 'success', value: hitElement === element };
|
||||
return { value: hitElement === element ? 'done' : 'nothittarget' };
|
||||
}
|
||||
|
||||
dispatchEvent(node: Node, type: string, eventInit: Object) {
|
||||
|
||||
@ -69,7 +69,7 @@ export interface PageDelegate {
|
||||
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
|
||||
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
||||
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
|
||||
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'>;
|
||||
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'notvisible' | 'notconnected' | 'done'>;
|
||||
rafCountForStablePosition(): number;
|
||||
|
||||
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
|
||||
|
||||
@ -154,10 +154,7 @@ export type JSCoverageOptions = {
|
||||
reportAnonymousScripts?: boolean,
|
||||
};
|
||||
|
||||
export type InjectedScriptResult<T = undefined> =
|
||||
(T extends undefined ? { status: 'success', value?: T} : { status: 'success', value: T }) |
|
||||
{ status: 'notconnected' } |
|
||||
{ status: 'error', error: string };
|
||||
export type InjectedScriptResult<T> = { error?: string, value?: T };
|
||||
|
||||
export type InjectedScriptProgress = {
|
||||
canceled: boolean,
|
||||
|
||||
@ -36,7 +36,6 @@ import { WKBrowserContext } from './wkBrowser';
|
||||
import { selectors } from '../selectors';
|
||||
import * as jpeg from 'jpeg-js';
|
||||
import * as png from 'pngjs';
|
||||
import { NotConnectedError } from '../errors';
|
||||
import { ConsoleMessageLocation } from '../console';
|
||||
import { JSHandle } from '../javascript';
|
||||
|
||||
@ -746,15 +745,15 @@ export class WKPage implements PageDelegate {
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
|
||||
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
|
||||
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'notvisible' | 'notconnected' | 'done'> {
|
||||
return await this._session.send('DOM.scrollIntoViewIfNeeded', {
|
||||
objectId: handle._objectId,
|
||||
rect,
|
||||
}).then(() => 'success' as const).catch(e => {
|
||||
}).then(() => 'done' as const).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
|
||||
return 'invisible';
|
||||
return 'notvisible';
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||
throw new NotConnectedError();
|
||||
return 'notconnected';
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
@ -621,7 +621,6 @@ describe('Page.click', function() {
|
||||
const error = await promise;
|
||||
expect(await page.evaluate(() => window.clicked)).toBe(undefined);
|
||||
expect(error.message).toContain('Element is not attached to the DOM');
|
||||
expect(error.name).toContain('NotConnectedError');
|
||||
});
|
||||
it('should fail when element detaches after animation', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/animating-button.html');
|
||||
@ -632,7 +631,6 @@ describe('Page.click', function() {
|
||||
const error = await promise;
|
||||
expect(await page.evaluate(() => window.clicked)).toBe(undefined);
|
||||
expect(error.message).toContain('Element is not attached to the DOM');
|
||||
expect(error.name).toContain('NotConnectedError');
|
||||
});
|
||||
it('should retry when element detaches after animation', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/animating-button.html');
|
||||
|
||||
@ -463,25 +463,25 @@ describe('Frame.waitForSelector', function() {
|
||||
await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e);
|
||||
expect(error.stack).toContain('waittask.spec.js');
|
||||
});
|
||||
it('should throw for unknown waitFor option', async({page, server}) => {
|
||||
it('should throw for unknown state option', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const error = await page.waitForSelector('section', { state: 'foo' }).catch(e => e);
|
||||
expect(error.message).toContain('Unsupported waitFor option');
|
||||
expect(error.message).toContain('Unsupported state option "foo"');
|
||||
});
|
||||
it('should throw for visibility option', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const error = await page.waitForSelector('section', { visibility: 'hidden' }).catch(e => e);
|
||||
expect(error.message).toBe('options.visibility is not supported, did you mean options.state?');
|
||||
});
|
||||
it('should throw for true waitFor option', async({page, server}) => {
|
||||
it('should throw for true state option', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const error = await page.waitForSelector('section', { state: true }).catch(e => e);
|
||||
expect(error.message).toContain('Unsupported waitFor option');
|
||||
expect(error.message).toContain('Unsupported state option "true"');
|
||||
});
|
||||
it('should throw for false waitFor option', async({page, server}) => {
|
||||
it('should throw for false state option', async({page, server}) => {
|
||||
await page.setContent('<section>test</section>');
|
||||
const error = await page.waitForSelector('section', { state: false }).catch(e => e);
|
||||
expect(error.message).toContain('Unsupported waitFor option');
|
||||
expect(error.message).toContain('Unsupported state option "false"');
|
||||
});
|
||||
it('should support >> selector syntax', async({page, server}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user