feature(click): option to wait for 'stationary' or 'hittarget'
This commit is contained in:
parent
a8f9c627f1
commit
817836c836
71
src/dom.ts
71
src/dom.ts
|
|
@ -12,6 +12,7 @@ import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource';
|
|||
import { assert, helper, debugError } from './helper';
|
||||
import Injected from './injected/injected';
|
||||
import { Page } from './page';
|
||||
import { TimeoutError } from './errors';
|
||||
|
||||
type ScopedSelector = types.Selector & { scope?: ElementHandle };
|
||||
type ResolvedSelector = { scope?: ElementHandle, selector: string, visibility: types.Visibility, disposeScope?: boolean };
|
||||
|
|
@ -279,8 +280,66 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
return { point, scrollX, scrollY };
|
||||
}
|
||||
|
||||
async _performPointerAction(action: (point: types.Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> {
|
||||
private async _waitForStationary(options: types.TimeoutOptions) {
|
||||
const { timeout = this._page._timeoutSettings.timeout() } = options;
|
||||
const success = await helper.waitWithTimeout(this.evaluate((node: Node, injected: Injected, timeout: number) => {
|
||||
let elementState;
|
||||
return injected.pollRaf(() => {
|
||||
if (!node.isConnected || node.nodeType !== Node.ELEMENT_NODE)
|
||||
return false;
|
||||
const element = node as Element;
|
||||
const rect = element.getBoundingClientRect();
|
||||
let computedOpacity = 1;
|
||||
for (let parent: Element | undefined = element; parent; parent = injected.utils.parentElementOrShadowHost(parent))
|
||||
computedOpacity *= +getComputedStyle(parent).opacity;
|
||||
const newState = {
|
||||
x: rect.top,
|
||||
y: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
computedOpacity,
|
||||
iteration: elementState ? elementState.iteration + 1 : 1,
|
||||
};
|
||||
if (elementState &&
|
||||
elementState.iteration >= 2 &&
|
||||
newState.x === elementState.x &&
|
||||
newState.y === elementState.y &&
|
||||
newState.width === elementState.width &&
|
||||
newState.height === elementState.height &&
|
||||
newState.computedOpacity === elementState.computedOpacity) {
|
||||
return true;
|
||||
}
|
||||
elementState = newState;
|
||||
return false;
|
||||
}, timeout);
|
||||
}, await this._context._injected(), timeout), 'stationary', timeout);
|
||||
if (!success)
|
||||
throw new TimeoutError(`waiting for stationary failed: timeout ${timeout}ms exceeded`);
|
||||
}
|
||||
|
||||
private async _waitToBecomeHitTargetAt(point: types.Point, options: types.TimeoutOptions) {
|
||||
const { timeout = this._page._timeoutSettings.timeout() } = options;
|
||||
const success = await helper.waitWithTimeout(this.evaluate((node: Node, injected: Injected, timeout: number, point: types.Point) => {
|
||||
return injected.pollRaf(() => {
|
||||
for (let hitElement = injected.utils.deepElementFromPoint(document, point.x, point.y);
|
||||
hitElement;
|
||||
hitElement = injected.utils.parentElementOrShadowHost(hitElement)) {
|
||||
if (hitElement === node)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, timeout);
|
||||
}, await this._context._injected(), timeout, point), 'hit target', timeout);
|
||||
if (!success)
|
||||
throw new TimeoutError(`waiting for hit target failed: timeout ${timeout}ms exceeded`);
|
||||
}
|
||||
|
||||
async _performPointerAction(action: (point: types.Point) => Promise<void>, options?: input.PointerActionOptions & types.WaitForOptions<'stationary' | 'hittarget'>): Promise<void> {
|
||||
if (options && types.multipleContains(options.waitFor, 'stationary'))
|
||||
await this._waitForStationary(options);
|
||||
const point = await this._ensurePointerActionPoint(options ? options.relativePoint : undefined);
|
||||
if (options && types.multipleContains(options.waitFor, 'hittarget'))
|
||||
await this._waitToBecomeHitTargetAt(point, options);
|
||||
let restoreModifiers: input.Modifier[] | undefined;
|
||||
if (options && options.modifiers)
|
||||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||
|
|
@ -289,19 +348,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
||||
}
|
||||
|
||||
hover(options?: input.PointerActionOptions): Promise<void> {
|
||||
hover(options?: input.PointerActionOptions & types.WaitForOptions<'stationary' | 'hittarget'>): Promise<void> {
|
||||
return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options);
|
||||
}
|
||||
|
||||
click(options?: input.ClickOptions): Promise<void> {
|
||||
click(options?: input.ClickOptions & types.WaitForOptions<'stationary' | 'hittarget'>): Promise<void> {
|
||||
return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
dblclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
dblclick(options?: input.MultiClickOptions & types.WaitForOptions<'stationary' | 'hittarget'>): Promise<void> {
|
||||
return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
tripleclick(options?: input.MultiClickOptions): Promise<void> {
|
||||
tripleclick(options?: input.MultiClickOptions & types.WaitForOptions<'stationary' | 'hittarget'>): Promise<void> {
|
||||
return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options);
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +409,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
async type(text: string, options?: { delay?: number }) {
|
||||
await this.focus();
|
||||
await this._page.keyboard.type(text, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {
|
||||
export function normalizeWaitUntil(waitUntil: types.Multiple<frames.LifecycleEvent>): frames.LifecycleEvent[] {
|
||||
if (!Array.isArray(waitUntil))
|
||||
waitUntil = [waitUntil];
|
||||
for (const condition of waitUntil) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ type ContextData = {
|
|||
|
||||
export type NavigateOptions = {
|
||||
timeout?: number,
|
||||
waitUntil?: LifecycleEvent | LifecycleEvent[],
|
||||
waitUntil?: types.Multiple<LifecycleEvent>,
|
||||
};
|
||||
|
||||
export type GotoOptions = NavigateOptions & {
|
||||
|
|
@ -48,8 +48,6 @@ export type GotoOptions = NavigateOptions & {
|
|||
|
||||
export type LifecycleEvent = 'load' | 'domcontentloaded';
|
||||
|
||||
export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean };
|
||||
|
||||
export class Frame {
|
||||
_id: string;
|
||||
readonly _firedLifecycleEvents: Set<LifecycleEvent>;
|
||||
|
|
@ -308,43 +306,43 @@ export class Frame {
|
|||
return result;
|
||||
}
|
||||
|
||||
async click(selector: string | types.Selector, options?: WaitForOptions & ClickOptions) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.click(options);
|
||||
async click(selector: string | types.Selector, options?: ClickOptions & types.WaitForOptions<'selector' | 'stationary' | 'hittarget'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, types.toWaitFor(options));
|
||||
await handle.click(types.toWaitFor(options));
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
async dblclick(selector: string | types.Selector, options?: WaitForOptions & MultiClickOptions) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.dblclick(options);
|
||||
async dblclick(selector: string | types.Selector, options?: MultiClickOptions & types.WaitForOptions<'selector' | 'stationary' | 'hittarget'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, types.toWaitFor(options));
|
||||
await handle.dblclick(types.toWaitFor(options));
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
async tripleclick(selector: string | types.Selector, options?: WaitForOptions & MultiClickOptions) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.tripleclick(options);
|
||||
async tripleclick(selector: string | types.Selector, options?: MultiClickOptions & types.WaitForOptions<'selector' | 'stationary' | 'hittarget'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, types.toWaitFor(options));
|
||||
await handle.tripleclick(types.toWaitFor(options));
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
async fill(selector: string | types.Selector, value: string, options?: WaitForOptions) {
|
||||
async fill(selector: string | types.Selector, value: string, options?: types.WaitForOptions<'selector'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.fill(value);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
async focus(selector: string | types.Selector, options?: WaitForOptions) {
|
||||
async focus(selector: string | types.Selector, options?: types.WaitForOptions<'selector'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.focus();
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
async hover(selector: string | types.Selector, options?: WaitForOptions & PointerActionOptions) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.hover(options);
|
||||
async hover(selector: string | types.Selector, options?: PointerActionOptions & types.WaitForOptions<'selector' | 'stationary' | 'hittarget'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, types.toWaitFor(options));
|
||||
await handle.hover(types.toWaitFor(options));
|
||||
await handle.dispose();
|
||||
}
|
||||
|
||||
async select(selector: string | types.Selector, value: string | dom.ElementHandle | SelectOption | string[] | dom.ElementHandle[] | SelectOption[] | undefined, options?: WaitForOptions): Promise<string[]> {
|
||||
async select(selector: string | types.Selector, value: types.Multiple<string | dom.ElementHandle | SelectOption> | undefined, options?: types.WaitForOptions<'selector'>): Promise<string[]> {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
const toDispose: Promise<dom.ElementHandle>[] = [];
|
||||
const values = value === undefined ? [] : value instanceof Array ? value : [value];
|
||||
|
|
@ -363,10 +361,8 @@ export class Frame {
|
|||
return result;
|
||||
}
|
||||
|
||||
async type(selector: string | types.Selector, text: string, options: WaitForOptions & { delay: (number | undefined); } | undefined) {
|
||||
const context = await this._utilityContext();
|
||||
const handle = await context._$(types.clearSelector(selector));
|
||||
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
|
||||
async type(selector: string | types.Selector, text: string, options?: { delay?: number } & types.WaitForOptions<'selector'>) {
|
||||
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
|
||||
await handle.type(text, options);
|
||||
await handle.dispose();
|
||||
}
|
||||
|
|
@ -381,13 +377,13 @@ export class Frame {
|
|||
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
||||
}
|
||||
|
||||
private async _optionallyWaitForInUtilityContext(selector: string | types.Selector,options: WaitForOptions): Promise<dom.ElementHandle | null> {
|
||||
private async _optionallyWaitForInUtilityContext(selector: string | types.Selector, options?: types.WaitForOptions<'selector'>): Promise<dom.ElementHandle | null> {
|
||||
let handle: dom.ElementHandle | null;
|
||||
if (options && options.waitFor) {
|
||||
if (options && types.multipleContains(options.waitFor, 'selector')) {
|
||||
handle = await this._waitForSelectorInUtilityContext(selector, options);
|
||||
} else {
|
||||
const context = await this._utilityContext();
|
||||
handle = await context._$(types.clearSelector(selector));
|
||||
handle = await context._$(types.clearSelector(selector));
|
||||
}
|
||||
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
|
||||
return handle;
|
||||
|
|
@ -616,7 +612,7 @@ export class LifecycleWatcher {
|
|||
private _targetUrl?: string;
|
||||
private _expectedDocumentId?: string;
|
||||
|
||||
constructor(frame: Frame, waitUntil: LifecycleEvent | LifecycleEvent[], timeout: number) {
|
||||
constructor(frame: Frame, waitUntil: types.Multiple<LifecycleEvent>, timeout: number) {
|
||||
if (Array.isArray(waitUntil))
|
||||
waitUntil = waitUntil.slice();
|
||||
else if (typeof waitUntil === 'string')
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export class Keyboard {
|
|||
await this._raw.sendText(text);
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay: (number | undefined); } | undefined) {
|
||||
async type(text: string, options?: { delay?: number }) {
|
||||
const delay = (options && options.delay) || null;
|
||||
for (const char of text) {
|
||||
if (keyboardLayout.keyDefinitions[char]) {
|
||||
|
|
|
|||
10
src/types.ts
10
src/types.ts
|
|
@ -72,3 +72,13 @@ export type Viewport = {
|
|||
isLandscape?: boolean;
|
||||
hasTouch?: boolean;
|
||||
};
|
||||
|
||||
export type Multiple<T> = T | T[];
|
||||
export function multipleContains<T>(multiple: Multiple<T> | undefined, t: T): boolean {
|
||||
return multiple === t || (Array.isArray(multiple) && multiple.includes(t));
|
||||
}
|
||||
|
||||
export type WaitForOptions<T> = TimeoutOptions & { waitFor?: Multiple<T> };
|
||||
export function toWaitFor<A, B>(o: WaitForOptions<A>): A extends B ? WaitForOptions<B> : never {
|
||||
return o as any as (A extends B ? WaitForOptions<B> : never);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,43 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Button test</title>
|
||||
<style>
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.glasspane {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
div.glasspane.capture {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@keyframes leftright {
|
||||
from {
|
||||
margin-left: 0;
|
||||
}
|
||||
to {
|
||||
margin-left: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
button.moving {
|
||||
animation: 1s infinite alternate leftright;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="mouse-helper.js"></script>
|
||||
<button>Click target</button>
|
||||
<div class=glasspane></div>
|
||||
<script>
|
||||
window.result = 'Was not clicked';
|
||||
window.offsetX = undefined;
|
||||
|
|
@ -17,6 +50,16 @@
|
|||
offsetY = e.offsetY;
|
||||
shiftKey = e.shiftKey;
|
||||
}, false);
|
||||
function toggleGlassPane() {
|
||||
document.querySelector('div.glasspane').classList.toggle('capture');
|
||||
}
|
||||
if (window.location.search.includes('glasspane'))
|
||||
toggleGlassPane();
|
||||
function toggleMoving() {
|
||||
document.querySelector('button').classList.toggle('moving');
|
||||
}
|
||||
if (window.location.search.includes('moving'))
|
||||
toggleMoving();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -354,5 +354,112 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
|||
await page.click('button');
|
||||
expect(await page.evaluate('window.clicked')).toBe(true);
|
||||
});
|
||||
|
||||
describe('wait for option', () => {
|
||||
it('should wait for selector', async({page, server}) => {
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: 'selector' }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(clicked).toBe(false);
|
||||
await page.goto(server.PREFIX + '/input/button.html');
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
|
||||
it('should wait for hit target', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/button.html?glasspane');
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: 'hittarget' }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => 'dummy');
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleGlassPane());
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
|
||||
it('should wait for stationary', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/button.html?moving');
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: 'stationary' }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(f)));
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleMoving());
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
|
||||
it('should wait for selector and hit target', async({page, server}) => {
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: ['hittarget', 'selector'] }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(clicked).toBe(false);
|
||||
await page.goto(server.PREFIX + '/input/button.html?glasspane');
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => 'dummy');
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleGlassPane());
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
|
||||
it('should wait for selector and stationary', async({page, server}) => {
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: ['stationary', 'selector'] }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(clicked).toBe(false);
|
||||
await page.goto(server.PREFIX + '/input/button.html?moving');
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(f)));
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleMoving());
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
|
||||
it('should wait for stationary and hit target', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/button.html?moving&glasspane');
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: ['stationary', 'hittarget'] }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(f)));
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleMoving());
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(f)));
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleGlassPane());
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
|
||||
it('should wait for hit target and stationary', async({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/input/button.html?moving&glasspane');
|
||||
let clicked = false;
|
||||
const clickPromise = page.click('button', { waitFor: ['stationary', 'hittarget'] }).then(() => clicked = true);
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(f)));
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleGlassPane());
|
||||
expect(clicked).toBe(false);
|
||||
for (let i = 0; i < 5; i++)
|
||||
await page.evaluate(() => new Promise(f => requestAnimationFrame(f)));
|
||||
expect(clicked).toBe(false);
|
||||
await page.evaluate(() => toggleMoving());
|
||||
await clickPromise;
|
||||
expect(await page.evaluate(() => result)).toBe('Clicked');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -226,6 +226,11 @@ class TestServer {
|
|||
serveFile(request, response, pathName) {
|
||||
if (pathName === '/')
|
||||
pathName = '/index.html';
|
||||
try {
|
||||
const url = new URL('http://localhost' + pathName);
|
||||
pathName = url.pathname;
|
||||
} catch (e) {
|
||||
}
|
||||
const filePath = path.join(this._dirPath, pathName.substring(1));
|
||||
|
||||
if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue