mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
chore: refactor actionability checks (#5368)
This commit is contained in:
parent
38209c675c
commit
b4b14eab69
@ -22,6 +22,7 @@ export type FatalDOMError =
|
||||
'error:notfillablenumberinput' |
|
||||
'error:notvaliddate' |
|
||||
'error:notinput' |
|
||||
'error:notselect';
|
||||
'error:notselect' |
|
||||
'error:notcheckbox';
|
||||
|
||||
export type RetargetableDOMError = 'error:notconnected';
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import * as frames from './frames';
|
||||
import { assert } from '../utils/utils';
|
||||
import type { InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
|
||||
import type { ElementStateWithoutStable, InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
|
||||
import * as injectedScriptSource from '../generated/injectedScriptSource';
|
||||
import * as js from './javascript';
|
||||
import { Page } from './page';
|
||||
@ -85,9 +85,11 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||
const source = `
|
||||
(() => {
|
||||
${injectedScriptSource.source}
|
||||
return new pwExport([
|
||||
${custom.join(',\n')}
|
||||
]);
|
||||
return new pwExport(
|
||||
${this.frame._page._delegate.rafCountForStablePosition()},
|
||||
${!!process.env.PW_USE_TIMEOUT_FOR_RAF},
|
||||
[${custom.join(',\n')}]
|
||||
);
|
||||
})();
|
||||
`;
|
||||
this._injectedScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId));
|
||||
@ -451,12 +453,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[] | 'error:notconnected'> {
|
||||
const selectOptions = [...elements, ...values];
|
||||
const optionsToSelect = [...elements, ...values];
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
progress.log(' selecting specified option(s)');
|
||||
await progress.checkpoint('before');
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, selectOptions]) => injected.waitForOptionsAndSelect(node, selectOptions), selectOptions);
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, optionsToSelect]) => {
|
||||
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], injected.selectOptions.bind(injected, optionsToSelect));
|
||||
}, optionsToSelect);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
const result = throwFatalDOMError(await pollHandler.finish());
|
||||
await this._page._doSlowMo();
|
||||
@ -477,7 +481,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.log(' waiting for element to be visible, enabled and editable');
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
|
||||
return injected.waitForEnabledAndFill(node, value);
|
||||
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled', 'editable'], injected.fill.bind(injected, value));
|
||||
}, value);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
const filled = throwFatalDOMError(await pollHandler.finish());
|
||||
@ -504,7 +508,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
return controller.run(async progress => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForVisibleAndSelectText(node);
|
||||
return injected.waitForElementStatesAndPerformAction(node, ['visible'], injected.selectText.bind(injected));
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
const result = throwFatalDOMError(await pollHandler.finish());
|
||||
@ -615,13 +619,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
|
||||
if (await this._evaluateInUtility(([injected, node]) => injected.isCheckboxChecked(node), {}) === state)
|
||||
const isChecked = async () => {
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
};
|
||||
if (await isChecked() === state)
|
||||
return 'done';
|
||||
const result = await this._click(progress, options);
|
||||
if (result !== 'done')
|
||||
return result;
|
||||
if (await this._evaluateInUtility(([injected, node]) => injected.isCheckboxChecked(node), {}) !== state)
|
||||
throw new Error('Unable to click checkbox');
|
||||
if (await isChecked() !== state)
|
||||
throw new Error('Clicking the checkbox did not change its state');
|
||||
return 'done';
|
||||
}
|
||||
|
||||
@ -661,94 +669,44 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async isVisible(): Promise<boolean> {
|
||||
return this._evaluateInUtility(([injected, node]) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Node as Element : node.parentElement;
|
||||
return element ? injected.isVisible(element) : false;
|
||||
}, {});
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'visible'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
}
|
||||
|
||||
async isHidden(): Promise<boolean> {
|
||||
return !(await this.isVisible());
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'hidden'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
}
|
||||
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return !(await this.isDisabled());
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'enabled'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
}
|
||||
|
||||
async isDisabled(): Promise<boolean> {
|
||||
return this._evaluateInUtility(([injected, node]) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Node as Element : node.parentElement;
|
||||
return element ? injected.isElementDisabled(element) : false;
|
||||
}, {});
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'disabled'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
}
|
||||
|
||||
async isEditable(): Promise<boolean> {
|
||||
return this._evaluateInUtility(([injected, node]) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Node as Element : node.parentElement;
|
||||
return element ? !injected.isElementDisabled(element) && !injected.isElementReadOnly(element) : false;
|
||||
}, {});
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'editable'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
}
|
||||
|
||||
async isChecked(): Promise<boolean> {
|
||||
return this._evaluateInUtility(([injected, node]) => {
|
||||
return injected.isCheckboxChecked(node);
|
||||
}, {});
|
||||
const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {});
|
||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||
}
|
||||
|
||||
async waitForElementState(metadata: CallMetadata, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise<void> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` waiting for element to be ${state}`);
|
||||
if (state === 'visible') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeVisible(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
if (state === 'hidden') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeHidden(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(await pollHandler.finish());
|
||||
return;
|
||||
}
|
||||
if (state === 'enabled') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeEnabled(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
if (state === 'disabled') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeDisabled(node);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
if (state === 'editable') {
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node]) => {
|
||||
return injected.waitForNodeEnabled(node, true /* waitForEnabled */);
|
||||
}, {});
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
if (state === 'stable') {
|
||||
const rafCount = this._page._delegate.rafCountForStablePosition();
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, rafOptions]) => {
|
||||
return injected.waitForDisplayedAtStablePosition(node, rafOptions, false /* waitForEnabled */);
|
||||
}, { rafCount, useTimeout: !!process.env.PW_USE_TIMEOUT_FOR_RAF });
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
return;
|
||||
}
|
||||
throw new Error(`state: expected one of (visible|hidden|stable|enabled|disabled|editable)`);
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, state]) => {
|
||||
return injected.waitForElementStatesAndPerformAction(node, [state], () => 'done' as const);
|
||||
}, state);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
assertDone(throwRetargetableDOMError(throwFatalDOMError(await pollHandler.finish())));
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
@ -785,20 +743,20 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
|
||||
async _waitForDisplayedAtStablePosition(progress: Progress, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> {
|
||||
if (waitForEnabled)
|
||||
progress.log(` waiting for element to be visible, enabled and not moving`);
|
||||
progress.log(` waiting for element to be visible, enabled and stable`);
|
||||
else
|
||||
progress.log(` waiting for element to be visible and not moving`);
|
||||
const rafCount = this._page._delegate.rafCountForStablePosition();
|
||||
const poll = this._evaluateHandleInUtility(([injected, node, { rafOptions, waitForEnabled }]) => {
|
||||
return injected.waitForDisplayedAtStablePosition(node, rafOptions, waitForEnabled);
|
||||
}, { rafOptions: { rafCount, useTimeout: !!process.env.PW_USE_TIMEOUT_FOR_RAF }, waitForEnabled });
|
||||
progress.log(` waiting for element to be visible and stable`);
|
||||
const poll = this._evaluateHandleInUtility(([injected, node, waitForEnabled]) => {
|
||||
return injected.waitForElementStatesAndPerformAction(node,
|
||||
waitForEnabled ? ['visible', 'stable', 'enabled'] : ['visible', 'stable'], () => 'done' as const);
|
||||
}, waitForEnabled);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
|
||||
const result = await pollHandler.finish();
|
||||
if (waitForEnabled)
|
||||
progress.log(' element is visible, enabled and does not move');
|
||||
progress.log(' element is visible, enabled and stable');
|
||||
else
|
||||
progress.log(' element is visible and does not move');
|
||||
return result;
|
||||
progress.log(' element is visible and stable');
|
||||
return throwFatalDOMError(result);
|
||||
}
|
||||
|
||||
async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> {
|
||||
@ -898,10 +856,12 @@ export function throwFatalDOMError<T>(result: T | FatalDOMError): T {
|
||||
throw new Error('Node is not an HTMLInputElement');
|
||||
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');
|
||||
return result;
|
||||
}
|
||||
|
||||
function throwRetargetableDOMError<T>(result: T | RetargetableDOMError): T {
|
||||
export function throwRetargetableDOMError<T>(result: T | RetargetableDOMError): T {
|
||||
if (result === 'error:notconnected')
|
||||
throw new Error('Element is not attached to the DOM');
|
||||
return result;
|
||||
@ -1032,50 +992,14 @@ export function getAttributeTask(selector: SelectorInfo, name: string): Schedula
|
||||
}, { parsed: selector.parsed, name });
|
||||
}
|
||||
|
||||
export function visibleTask(selector: SelectorInfo): SchedulableTask<boolean> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
|
||||
export function elementStateTask(selector: SelectorInfo, state: ElementStateWithoutStable): SchedulableTask<boolean | 'error:notconnected' | FatalDOMError> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, state }) => {
|
||||
return injected.pollRaf((progress, continuePolling) => {
|
||||
const element = injected.querySelector(parsed, document);
|
||||
if (!element)
|
||||
return continuePolling;
|
||||
progress.log(` selector resolved to ${injected.previewNode(element)}`);
|
||||
return injected.isVisible(element);
|
||||
return injected.checkElementState(element, state);
|
||||
});
|
||||
}, selector.parsed);
|
||||
}
|
||||
|
||||
export function disabledTask(selector: SelectorInfo): SchedulableTask<boolean> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
|
||||
return injected.pollRaf((progress, continuePolling) => {
|
||||
const element = injected.querySelector(parsed, document);
|
||||
if (!element)
|
||||
return continuePolling;
|
||||
progress.log(` selector resolved to ${injected.previewNode(element)}`);
|
||||
return injected.isElementDisabled(element);
|
||||
});
|
||||
}, selector.parsed);
|
||||
}
|
||||
|
||||
export function editableTask(selector: SelectorInfo): SchedulableTask<boolean> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
|
||||
return injected.pollRaf((progress, continuePolling) => {
|
||||
const element = injected.querySelector(parsed, document);
|
||||
if (!element)
|
||||
return continuePolling;
|
||||
progress.log(` selector resolved to ${injected.previewNode(element)}`);
|
||||
return !injected.isElementDisabled(element) && !injected.isElementReadOnly(element);
|
||||
});
|
||||
}, selector.parsed);
|
||||
}
|
||||
|
||||
export function checkedTask(selector: SelectorInfo): SchedulableTask<boolean> {
|
||||
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
|
||||
return injected.pollRaf((progress, continuePolling) => {
|
||||
const element = injected.querySelector(parsed, document);
|
||||
if (!element)
|
||||
return continuePolling;
|
||||
progress.log(` selector resolved to ${injected.previewNode(element)}`);
|
||||
return injected.isCheckboxChecked(element);
|
||||
});
|
||||
}, selector.parsed);
|
||||
}, { parsed: selector.parsed, state });
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import { Progress, ProgressController } from './progress';
|
||||
import { assert, makeWaitForNextTask } from '../utils/utils';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { CallMetadata, SdkObject } from './instrumentation';
|
||||
import { ElementStateWithoutStable } from './injected/injectedScript';
|
||||
|
||||
type ContextData = {
|
||||
contextPromise: Promise<dom.FrameExecutionContext>;
|
||||
@ -944,6 +945,17 @@ export class Frame extends SdkObject {
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.TimeoutOptions = {}): Promise<boolean> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
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));
|
||||
return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result));
|
||||
}
|
||||
|
||||
async isVisible(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
@ -958,37 +970,19 @@ export class Frame extends SdkObject {
|
||||
}
|
||||
|
||||
async isDisabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.disabledTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` checking disabled state of "${selector}"`);
|
||||
return this._scheduleRerunnableTask(progress, info.world, task);
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
return this._checkElementState(metadata, selector, 'disabled', options);
|
||||
}
|
||||
|
||||
async isEnabled(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
||||
return !(await this.isDisabled(metadata, selector, options));
|
||||
return this._checkElementState(metadata, selector, 'enabled', options);
|
||||
}
|
||||
|
||||
async isEditable(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.editableTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` checking editable state of "${selector}"`);
|
||||
return this._scheduleRerunnableTask(progress, info.world, task);
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
return this._checkElementState(metadata, selector, 'editable', options);
|
||||
}
|
||||
|
||||
async isChecked(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<boolean> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const info = this._page.selectors._parseSelector(selector);
|
||||
const task = dom.checkedTask(info);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` checking checked state of "${selector}"`);
|
||||
return this._scheduleRerunnableTask(progress, info.world, task);
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
return this._checkElementState(metadata, selector, 'checked', options);
|
||||
}
|
||||
|
||||
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
|
||||
|
@ -38,11 +38,16 @@ export type InjectedScriptPoll<T> = {
|
||||
cancel: () => void,
|
||||
};
|
||||
|
||||
export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked';
|
||||
export type ElementState = ElementStateWithoutStable | 'stable';
|
||||
|
||||
export class InjectedScript {
|
||||
private _enginesV1: Map<string, SelectorEngine>;
|
||||
_evaluator: SelectorEvaluatorImpl;
|
||||
private _stableRafCount: number;
|
||||
private _replaceRafWithTimeout: boolean;
|
||||
|
||||
constructor(customEngines: { name: string, engine: SelectorEngine}[]) {
|
||||
constructor(stableRafCount: number, replaceRafWithTimeout: boolean, customEngines: { name: string, engine: SelectorEngine}[]) {
|
||||
this._enginesV1 = new Map();
|
||||
this._enginesV1.set('xpath', XPathEngine);
|
||||
this._enginesV1.set('xpath:light', XPathEngine);
|
||||
@ -61,6 +66,8 @@ export class InjectedScript {
|
||||
|
||||
// No custom engines in V2 for now.
|
||||
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
||||
this._stableRafCount = stableRafCount;
|
||||
this._replaceRafWithTimeout = replaceRafWithTimeout;
|
||||
}
|
||||
|
||||
parseSelector(selector: string): ParsedSelector {
|
||||
@ -319,148 +326,206 @@ export class InjectedScript {
|
||||
return { left: parseInt(style.borderLeftWidth || '', 10), top: parseInt(style.borderTopWidth || '', 10) };
|
||||
}
|
||||
|
||||
waitForOptionsAndSelect(node: Node, optionsToSelect: (Node | { value?: string, label?: string, index?: number })[]): InjectedScriptPoll<string[] | 'error:notconnected' | FatalDOMError> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = this.findLabelTarget(node as Element);
|
||||
if (!element || !element.isConnected)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() !== 'select')
|
||||
return 'error:notselect';
|
||||
const select = element as HTMLSelectElement;
|
||||
const options = Array.from(select.options);
|
||||
const selectedOptions = [];
|
||||
let remainingOptionsToSelect = optionsToSelect.slice();
|
||||
for (let index = 0; index < options.length; index++) {
|
||||
const option = options[index];
|
||||
const filter = (optionToSelect: Node | { value?: string, label?: string, index?: number }) => {
|
||||
if (optionToSelect instanceof Node)
|
||||
return option === optionToSelect;
|
||||
let matches = true;
|
||||
if (optionToSelect.value !== undefined)
|
||||
matches = matches && optionToSelect.value === option.value;
|
||||
if (optionToSelect.label !== undefined)
|
||||
matches = matches && optionToSelect.label === option.label;
|
||||
if (optionToSelect.index !== undefined)
|
||||
matches = matches && optionToSelect.index === index;
|
||||
return matches;
|
||||
};
|
||||
if (!remainingOptionsToSelect.some(filter))
|
||||
private _retarget(node: Node): Element | null {
|
||||
let element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!element)
|
||||
return null;
|
||||
element = element.closest('button, [role=button], [role=checkbox], [role=radio]') || element;
|
||||
if (!element.matches('input, textarea, button, select, [role=button], [role=checkbox], [role=radio]') &&
|
||||
!(element as any).isContentEditable) {
|
||||
// Go up to the label that might be connected to the input/textarea.
|
||||
element = element.closest('label') || element;
|
||||
}
|
||||
if (element.nodeName === 'LABEL')
|
||||
element = (element as HTMLLabelElement).control || element;
|
||||
return element;
|
||||
}
|
||||
|
||||
waitForElementStatesAndPerformAction<T>(node: Node, states: ElementState[],
|
||||
callback: (element: Element | null, progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol): InjectedScriptPoll<T | 'error:notconnected' | FatalDOMError> {
|
||||
let lastRect: { x: number, y: number, width: number, height: number } | undefined;
|
||||
let counter = 0;
|
||||
let samePositionCounter = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
const predicate = (progress: InjectedScriptProgress, continuePolling: symbol) => {
|
||||
const element = this._retarget(node);
|
||||
|
||||
for (const state of states) {
|
||||
if (state !== 'stable') {
|
||||
const result = this._checkElementState(element, state);
|
||||
if (typeof result !== 'boolean')
|
||||
return result;
|
||||
if (!result) {
|
||||
progress.logRepeating(` element is not ${state} - waiting...`);
|
||||
return continuePolling;
|
||||
}
|
||||
continue;
|
||||
selectedOptions.push(option);
|
||||
if (select.multiple) {
|
||||
remainingOptionsToSelect = remainingOptionsToSelect.filter(o => !filter(o));
|
||||
} else {
|
||||
remainingOptionsToSelect = [];
|
||||
break;
|
||||
}
|
||||
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
|
||||
// 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 continuePolling;
|
||||
|
||||
// Drop frames that are shorter than 16ms - WebKit Win bug.
|
||||
const time = performance.now();
|
||||
if (this._stableRafCount > 1 && time - lastTime < 15)
|
||||
return continuePolling;
|
||||
lastTime = time;
|
||||
|
||||
const clientRect = element.getBoundingClientRect();
|
||||
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
|
||||
const samePosition = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height;
|
||||
if (samePosition)
|
||||
++samePositionCounter;
|
||||
else
|
||||
samePositionCounter = 0;
|
||||
const isStable = samePositionCounter >= this._stableRafCount;
|
||||
const isStableForLogs = isStable || !lastRect;
|
||||
lastRect = rect;
|
||||
if (!isStableForLogs)
|
||||
progress.logRepeating(` element is not stable - waiting...`);
|
||||
if (!isStable)
|
||||
return continuePolling;
|
||||
}
|
||||
if (remainingOptionsToSelect.length) {
|
||||
progress.logRepeating(' did not find some options - waiting... ');
|
||||
return continuePolling;
|
||||
}
|
||||
select.value = undefined as any;
|
||||
selectedOptions.forEach(option => option.selected = true);
|
||||
progress.log(' selected specified option(s)');
|
||||
select.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
select.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
return selectedOptions.map(option => option.value);
|
||||
});
|
||||
|
||||
return callback(element, progress, continuePolling);
|
||||
};
|
||||
|
||||
if (this._replaceRafWithTimeout)
|
||||
return this.pollInterval(16, predicate);
|
||||
else
|
||||
return this.pollRaf(predicate);
|
||||
}
|
||||
|
||||
waitForEnabledAndFill(node: Node, value: string): InjectedScriptPoll<FatalDOMError | 'error:notconnected' | 'needsinput' | 'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'error:notelement';
|
||||
private _checkElementState(element: Element | null, state: ElementStateWithoutStable): boolean | 'error:notconnected' | FatalDOMError {
|
||||
if (!element || !element.isConnected) {
|
||||
if (state === 'hidden')
|
||||
return true;
|
||||
return 'error:notconnected';
|
||||
}
|
||||
if (state === 'visible')
|
||||
return this.isVisible(element);
|
||||
if (state === 'hidden')
|
||||
return !this.isVisible(element);
|
||||
|
||||
if (node && node.nodeName.toLowerCase() !== 'input' &&
|
||||
node.nodeName.toLowerCase() !== 'textarea' &&
|
||||
!(node as any).isContentEditable) {
|
||||
// Go up to the label that might be connected to the input/textarea.
|
||||
node = (node as Element).closest('label') || node;
|
||||
}
|
||||
const element = this.findLabelTarget(node as Element);
|
||||
const disabled = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(element.nodeName) && element.hasAttribute('disabled');
|
||||
if (state === 'disabled')
|
||||
return disabled;
|
||||
if (state === 'enabled')
|
||||
return !disabled;
|
||||
|
||||
if (element && !element.isConnected)
|
||||
return 'error:notconnected';
|
||||
if (!element || !this.isVisible(element)) {
|
||||
progress.logRepeating(' element is not visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
const input = element as HTMLInputElement;
|
||||
const type = input.type.toLowerCase();
|
||||
const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local', 'month', 'week']);
|
||||
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';
|
||||
}
|
||||
if (type === 'number') {
|
||||
value = value.trim();
|
||||
if (isNaN(Number(value)))
|
||||
return 'error:notfillablenumberinput';
|
||||
}
|
||||
if (input.disabled) {
|
||||
progress.logRepeating(' element is disabled - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
if (input.readOnly) {
|
||||
progress.logRepeating(' element is readonly - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
if (kDateTypes.has(type)) {
|
||||
value = value.trim();
|
||||
input.focus();
|
||||
input.value = value;
|
||||
if (input.value !== value)
|
||||
return 'error:notvaliddate';
|
||||
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.
|
||||
}
|
||||
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
||||
const textarea = element as HTMLTextAreaElement;
|
||||
if (textarea.disabled) {
|
||||
progress.logRepeating(' element is disabled - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
if (textarea.readOnly) {
|
||||
progress.logRepeating(' element is readonly - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
} else if (!(element as HTMLElement).isContentEditable) {
|
||||
return 'error:notfillableelement';
|
||||
}
|
||||
const result = this._selectText(element);
|
||||
if (result === 'error:notvisible') {
|
||||
progress.logRepeating(' element is not visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'needsinput'; // Still need to input the value.
|
||||
});
|
||||
const editable = !(['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && element.hasAttribute('readonly'));
|
||||
if (state === 'editable')
|
||||
return !disabled && editable;
|
||||
|
||||
if (state === 'checked') {
|
||||
if (element.getAttribute('role') === 'checkbox')
|
||||
return element.getAttribute('aria-checked') === 'true';
|
||||
if (element.nodeName !== 'INPUT')
|
||||
return 'error:notcheckbox';
|
||||
if (!['radio', 'checkbox'].includes((element as HTMLInputElement).type.toLowerCase()))
|
||||
return 'error:notcheckbox';
|
||||
return (element as HTMLInputElement).checked;
|
||||
}
|
||||
throw new Error(`Unexpected element state "${state}"`);
|
||||
}
|
||||
|
||||
waitForVisibleAndSelectText(node: Node): InjectedScriptPoll<FatalDOMError | 'error:notconnected' | 'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'error:notelement';
|
||||
if (!node.isConnected)
|
||||
return 'error:notconnected';
|
||||
const element = node as Element;
|
||||
if (!this.isVisible(element)) {
|
||||
progress.logRepeating(' element is not visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
const result = this._selectText(element);
|
||||
if (result === 'error:notvisible') {
|
||||
progress.logRepeating(' element is not visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
checkElementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' | FatalDOMError {
|
||||
const element = this._retarget(node);
|
||||
return this._checkElementState(element, state);
|
||||
}
|
||||
|
||||
private _selectText(element: Element): 'error:notvisible' | 'error:notconnected' | 'done' {
|
||||
selectOptions(optionsToSelect: (Node | { value?: string, label?: string, index?: number })[],
|
||||
element: Element | null, progress: InjectedScriptProgress, continuePolling: symbol): string[] | 'error:notconnected' | FatalDOMError | symbol {
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() !== 'select')
|
||||
return 'error:notselect';
|
||||
const select = element as HTMLSelectElement;
|
||||
const options = Array.from(select.options);
|
||||
const selectedOptions = [];
|
||||
let remainingOptionsToSelect = optionsToSelect.slice();
|
||||
for (let index = 0; index < options.length; index++) {
|
||||
const option = options[index];
|
||||
const filter = (optionToSelect: Node | { value?: string, label?: string, index?: number }) => {
|
||||
if (optionToSelect instanceof Node)
|
||||
return option === optionToSelect;
|
||||
let matches = true;
|
||||
if (optionToSelect.value !== undefined)
|
||||
matches = matches && optionToSelect.value === option.value;
|
||||
if (optionToSelect.label !== undefined)
|
||||
matches = matches && optionToSelect.label === option.label;
|
||||
if (optionToSelect.index !== undefined)
|
||||
matches = matches && optionToSelect.index === index;
|
||||
return matches;
|
||||
};
|
||||
if (!remainingOptionsToSelect.some(filter))
|
||||
continue;
|
||||
selectedOptions.push(option);
|
||||
if (select.multiple) {
|
||||
remainingOptionsToSelect = remainingOptionsToSelect.filter(o => !filter(o));
|
||||
} else {
|
||||
remainingOptionsToSelect = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remainingOptionsToSelect.length) {
|
||||
progress.logRepeating(' did not find some options - waiting... ');
|
||||
return continuePolling;
|
||||
}
|
||||
select.value = undefined as any;
|
||||
selectedOptions.forEach(option => option.selected = true);
|
||||
progress.log(' selected specified option(s)');
|
||||
select.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
select.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
return selectedOptions.map(option => option.value);
|
||||
}
|
||||
|
||||
fill(value: string, element: Element | null, progress: InjectedScriptProgress): FatalDOMError | 'error:notconnected' | 'needsinput' | 'done' {
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
const input = element as HTMLInputElement;
|
||||
const type = input.type.toLowerCase();
|
||||
const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local', 'month', 'week']);
|
||||
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';
|
||||
}
|
||||
if (type === 'number') {
|
||||
value = value.trim();
|
||||
if (isNaN(Number(value)))
|
||||
return 'error:notfillablenumberinput';
|
||||
}
|
||||
if (kDateTypes.has(type)) {
|
||||
value = value.trim();
|
||||
input.focus();
|
||||
input.value = value;
|
||||
if (input.value !== value)
|
||||
return 'error:notvaliddate';
|
||||
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.
|
||||
}
|
||||
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
||||
// Nothing to check here.
|
||||
} else if (!(element as HTMLElement).isContentEditable) {
|
||||
return 'error:notfillableelement';
|
||||
}
|
||||
this.selectText(element);
|
||||
return 'needsinput'; // Still need to input the value.
|
||||
}
|
||||
|
||||
selectText(element: Element | null): 'error:notconnected' | 'done' {
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
if (element.nodeName.toLowerCase() === 'input') {
|
||||
const input = element as HTMLInputElement;
|
||||
input.select();
|
||||
@ -477,70 +542,14 @@ export class InjectedScript {
|
||||
const range = element.ownerDocument.createRange();
|
||||
range.selectNodeContents(element);
|
||||
const selection = element.ownerDocument.defaultView!.getSelection();
|
||||
if (!selection)
|
||||
return 'error:notvisible';
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
(element as HTMLElement | SVGElement).focus();
|
||||
return 'done';
|
||||
}
|
||||
|
||||
waitForNodeVisible(node: Node): InjectedScriptPoll<'error:notconnected' | 'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!node.isConnected || !element)
|
||||
return 'error:notconnected';
|
||||
if (!this.isVisible(element)) {
|
||||
progress.logRepeating(' element is not visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'done';
|
||||
});
|
||||
}
|
||||
|
||||
waitForNodeHidden(node: Node): InjectedScriptPoll<'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!node.isConnected || !element)
|
||||
return 'done';
|
||||
if (this.isVisible(element)) {
|
||||
progress.logRepeating(' element is visible - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'done';
|
||||
});
|
||||
}
|
||||
|
||||
waitForNodeEnabled(node: Node, waitForEditable?: boolean): InjectedScriptPoll<'error:notconnected' | 'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!node.isConnected || !element)
|
||||
return 'error:notconnected';
|
||||
if (this.isElementDisabled(element)) {
|
||||
progress.logRepeating(' element is not enabled - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
if (waitForEditable && this.isElementReadOnly(element)) {
|
||||
progress.logRepeating(' element is readonly - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'done';
|
||||
});
|
||||
}
|
||||
|
||||
waitForNodeDisabled(node: Node): InjectedScriptPoll<'error:notconnected' | 'done'> {
|
||||
return this.pollRaf((progress, continuePolling) => {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement;
|
||||
if (!node.isConnected || !element)
|
||||
return 'error:notconnected';
|
||||
if (!this.isElementDisabled(element)) {
|
||||
progress.logRepeating(' element is enabled - waiting...');
|
||||
return continuePolling;
|
||||
}
|
||||
return 'done';
|
||||
});
|
||||
}
|
||||
|
||||
focusNode(node: Node, resetSelectionIfNotFocused?: boolean): FatalDOMError | 'error:notconnected' | 'done' {
|
||||
if (!node.isConnected)
|
||||
return 'error:notconnected';
|
||||
@ -560,24 +569,6 @@ export class InjectedScript {
|
||||
return 'done';
|
||||
}
|
||||
|
||||
findLabelTarget(element: Element): Element | undefined {
|
||||
return element.nodeName === 'LABEL' ? (element as HTMLLabelElement).control || undefined : element;
|
||||
}
|
||||
|
||||
isCheckboxChecked(node: Node) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
throw new Error('Not a checkbox or radio button');
|
||||
const element = node as Element;
|
||||
if (element.getAttribute('role') === 'checkbox')
|
||||
return element.getAttribute('aria-checked') === 'true';
|
||||
const input = this.findLabelTarget(element);
|
||||
if (!input || input.nodeName !== 'INPUT')
|
||||
throw new Error('Not a checkbox or radio button');
|
||||
if (!['radio', 'checkbox'].includes((input as HTMLInputElement).type.toLowerCase()))
|
||||
throw new Error('Not a checkbox or radio button');
|
||||
return (input as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
setInputFiles(node: Node, payloads: { name: string, mimeType: string, buffer: string }[]) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return 'Node is not of type HTMLElement';
|
||||
@ -601,66 +592,6 @@ export class InjectedScript {
|
||||
input.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
}
|
||||
|
||||
waitForDisplayedAtStablePosition(node: Node, rafOptions: { rafCount: number, useTimeout?: boolean }, waitForEnabled: boolean): InjectedScriptPoll<'error:notconnected' | 'done'> {
|
||||
let lastRect: { x: number, y: number, width: number, height: number } | undefined;
|
||||
let counter = 0;
|
||||
let samePositionCounter = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
const predicate = (progress: InjectedScriptProgress, continuePolling: symbol) => {
|
||||
// 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 continuePolling;
|
||||
|
||||
if (!node.isConnected)
|
||||
return 'error:notconnected';
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||
if (!element)
|
||||
return 'error:notconnected';
|
||||
|
||||
// Drop frames that are shorter than 16ms - WebKit Win bug.
|
||||
const time = performance.now();
|
||||
if (rafOptions.rafCount > 1 && time - lastTime < 15)
|
||||
return continuePolling;
|
||||
lastTime = time;
|
||||
|
||||
// Note: this logic should be similar to isVisible() to avoid surprises.
|
||||
const clientRect = element.getBoundingClientRect();
|
||||
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
|
||||
const samePosition = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height;
|
||||
const isDisplayed = rect.width > 0 && rect.height > 0;
|
||||
if (samePosition)
|
||||
++samePositionCounter;
|
||||
else
|
||||
samePositionCounter = 0;
|
||||
const isStable = samePositionCounter >= rafOptions.rafCount;
|
||||
const isStableForLogs = isStable || !lastRect;
|
||||
lastRect = rect;
|
||||
|
||||
const style = element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element) : undefined;
|
||||
const isVisible = !!style && style.visibility !== 'hidden';
|
||||
|
||||
const isDisabled = waitForEnabled && this.isElementDisabled(element);
|
||||
|
||||
if (isDisplayed && isStable && isVisible && !isDisabled)
|
||||
return 'done';
|
||||
|
||||
if (!isDisplayed || !isVisible)
|
||||
progress.logRepeating(` element is not visible - waiting...`);
|
||||
else if (!isStableForLogs)
|
||||
progress.logRepeating(` element is moving - waiting...`);
|
||||
else if (isDisabled)
|
||||
progress.logRepeating(` element is disabled - waiting...`);
|
||||
return continuePolling;
|
||||
};
|
||||
if (rafOptions.useTimeout)
|
||||
return this.pollInterval(16, predicate);
|
||||
else
|
||||
return this.pollRaf(predicate);
|
||||
}
|
||||
|
||||
checkHitTargetAt(node: Node, point: { x: number, y: number }): 'error:notconnected' | 'done' | { hitTargetDescription: string } {
|
||||
let element: Element | null | undefined = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
|
||||
if (!element || !element.isConnected)
|
||||
@ -708,16 +639,6 @@ export class InjectedScript {
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
isElementDisabled(element: Element): boolean {
|
||||
const elementOrButton = element.closest('button, [role=button]') || element;
|
||||
return ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes(elementOrButton.nodeName) && elementOrButton.hasAttribute('disabled');
|
||||
}
|
||||
|
||||
isElementReadOnly(element: Element): boolean {
|
||||
const target = this.findLabelTarget(element);
|
||||
return !!target && ['INPUT', 'TEXTAREA'].includes(target.nodeName) && target.hasAttribute('readonly');
|
||||
}
|
||||
|
||||
deepElementFromPoint(document: Document, x: number, y: number): Element | undefined {
|
||||
let container: Document | ShadowRoot | null = document;
|
||||
let element: Element | undefined;
|
||||
|
@ -32,5 +32,5 @@ it('should timeout waiting for button to be enabled', async ({page, server}) =>
|
||||
const error = await page.click('text=Click target', { timeout: 3000 }).catch(e => e);
|
||||
expect(await page.evaluate('window.__CLICKED')).toBe(undefined);
|
||||
expect(error.message).toContain('page.click: Timeout 3000ms exceeded.');
|
||||
expect(error.message).toContain('element is disabled - waiting');
|
||||
expect(error.message).toContain('element is not enabled - waiting');
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ it('should timeout waiting for display:none to be gone', async ({page, server})
|
||||
await page.$eval('button', b => b.style.display = 'none');
|
||||
const error = await page.click('button', { timeout: 5000 }).catch(e => e);
|
||||
expect(error.message).toContain('page.click: Timeout 5000ms exceeded.');
|
||||
expect(error.message).toContain('waiting for element to be visible, enabled and not moving');
|
||||
expect(error.message).toContain('waiting for element to be visible, enabled and stable');
|
||||
expect(error.message).toContain('element is not visible - waiting');
|
||||
});
|
||||
|
||||
@ -31,6 +31,6 @@ it('should timeout waiting for visbility:hidden to be gone', async ({page, serve
|
||||
await page.$eval('button', b => b.style.visibility = 'hidden');
|
||||
const error = await page.click('button', { timeout: 5000 }).catch(e => e);
|
||||
expect(error.message).toContain('page.click: Timeout 5000ms exceeded.');
|
||||
expect(error.message).toContain('waiting for element to be visible, enabled and not moving');
|
||||
expect(error.message).toContain('waiting for element to be visible, enabled and stable');
|
||||
expect(error.message).toContain('element is not visible - waiting');
|
||||
});
|
||||
|
@ -26,6 +26,6 @@ it('should timeout waiting for stable position', async ({page, server}) => {
|
||||
});
|
||||
const error = await button.click({ timeout: 3000 }).catch(e => e);
|
||||
expect(error.message).toContain('elementHandle.click: Timeout 3000ms exceeded.');
|
||||
expect(error.message).toContain('waiting for element to be visible, enabled and not moving');
|
||||
expect(error.message).toContain('element is moving - waiting');
|
||||
expect(error.message).toContain('waiting for element to be visible, enabled and stable');
|
||||
expect(error.message).toContain('element is not stable - waiting');
|
||||
});
|
||||
|
@ -6,6 +6,7 @@
|
||||
"moduleResolution": "node",
|
||||
"target": "ESNext",
|
||||
"strictNullChecks": false,
|
||||
"strictBindCallApply": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"include": ["**/*.spec.js", "**/*.ts"]
|
||||
|
Loading…
Reference in New Issue
Block a user