Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
};
}

async _retryAction(progress: Progress, actionName: string, action: (progress: Progress, retry: number) => Promise<PerformActionResult>, options: { trial?: boolean, force?: boolean, skipActionPreChecks?: boolean, noAutoWaiting?: boolean }): Promise<'error:notconnected' | 'done'> {
async _retryAction(progress: Progress, actionName: string, action: (progress: Progress, retry: number) => Promise<PerformActionResult>, options: { trial?: boolean, force?: boolean, skipActionPreChecks?: boolean }): Promise<'error:notconnected' | 'done'> {
let retry = 0;
// We progressively wait longer between retries, up to 500ms.
const waitTime = [0, 20, 100, 100, 500];
const noAutoWaiting = (options as any).__testHookNoAutoWaiting ?? options.noAutoWaiting;

while (true) {
if (retry) {
Expand All @@ -333,43 +332,35 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} else {
progress.log(`attempting ${actionName} action${options.trial ? ' (trial run)' : ''}`);
}
if (!options.skipActionPreChecks && !options.force && !noAutoWaiting)
if (!options.skipActionPreChecks && !options.force)
await this._frame._page.performActionPreChecks(progress);
const result = await action(progress, retry);
++retry;
if (result === 'error:notvisible') {
if (options.force || noAutoWaiting)
if (options.force)
throw new NonRecoverableDOMError('Element is not visible');
progress.log(' element is not visible');
continue;
}
if (result === 'error:notinviewport') {
if (options.force || noAutoWaiting)
if (options.force)
throw new NonRecoverableDOMError('Element is outside of the viewport');
progress.log(' element is outside of the viewport');
continue;
}
if (result === 'error:optionsnotfound') {
if (noAutoWaiting)
throw new NonRecoverableDOMError('Did not find some options');
progress.log(' did not find some options');
continue;
}
if (result === 'error:optionnotenabled') {
if (noAutoWaiting)
throw new NonRecoverableDOMError('Option being selected is not enabled');
progress.log(' option being selected is not enabled');
continue;
}
if (typeof result === 'object' && 'hitTargetDescription' in result) {
if (noAutoWaiting)
throw new NonRecoverableDOMError(`${result.hitTargetDescription} intercepts pointer events`);
progress.log(` ${result.hitTargetDescription} intercepts pointer events`);
continue;
}
if (typeof result === 'object' && 'missingState' in result) {
if (noAutoWaiting)
throw new NonRecoverableDOMError(`Element is not ${result.missingState}`);
progress.log(` element is not ${result.missingState}`);
continue;
}
Expand Down
26 changes: 8 additions & 18 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,11 +1193,10 @@ export class Frame extends SdkObject<FrameEventMap> {
private async _retryWithProgressIfNotConnected<R>(
progress: Progress,
selector: string,
options: { strict?: boolean, noAutoWaiting?: boolean, force?: boolean, performActionPreChecks?: boolean, waitForFrameVisible?: boolean },
options: { strict?: boolean, force?: boolean, performActionPreChecks?: boolean, waitForFrameVisible?: boolean },
action: (progress: Progress, handle: dom.ElementHandle<Element>, box: types.Rect | undefined, frameVisible: boolean) => Promise<R | 'error:notconnected'>): Promise<R> {
progress.log(`waiting for ${this._asLocator(selector)}`);
const noAutoWaiting = (options as any).__testHookNoAutoWaiting ?? options.noAutoWaiting;
const performActionPreChecks = (options.performActionPreChecks ?? !options.force) && !noAutoWaiting;
const performActionPreChecks = options.performActionPreChecks ?? !options.force;
const waitForFrameVisible = options.waitForFrameVisible && !options.force;
return this.retryWithProgressAndBackoff(progress, async (progress, continuePolling) => {
if (performActionPreChecks)
Expand All @@ -1213,24 +1212,17 @@ export class Frame extends SdkObject<FrameEventMap> {
const rect = element?.getBoundingClientRect();
return { log, success: !!element, element, box: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : undefined };
}, {}));
if (!resolved) {
if (noAutoWaiting)
throw new dom.NonRecoverableDOMError('Element(s) not found');
if (!resolved)
return continuePolling;
}
const result = resolved.result;
const { log, success, box } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success, box: r.box })));
if (log)
progress.log(log);
if (!success) {
if (noAutoWaiting)
throw new dom.NonRecoverableDOMError('Element(s) not found');
result.dispose();
return continuePolling;
}
if (waitForFrameVisible && !resolved.frameVisible) {
if (noAutoWaiting)
throw new dom.NonRecoverableDOMError('Element is not visible');
progress.log(' element is inside a hidden frame, retrying');
result.dispose();
return continuePolling;
Expand All @@ -1240,8 +1232,6 @@ export class Frame extends SdkObject<FrameEventMap> {
try {
const result = await action(progress, element, box, resolved.frameVisible);
if (result === 'error:notconnected') {
if (noAutoWaiting)
throw new dom.NonRecoverableDOMError('Element is not attached to the DOM');
progress.log('element was detached from the DOM, retrying');
return continuePolling;
}
Expand Down Expand Up @@ -1301,11 +1291,11 @@ export class Frame extends SdkObject<FrameEventMap> {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._fill(progress, value, options, frameVisible, box)));
}

async focus(progress: Progress, selector: string, options: types.StrictOptions & { noAutoWaiting?: boolean }) {
async focus(progress: Progress, selector: string, options: types.StrictOptions) {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._focus(progress)));
}

async blur(progress: Progress, selector: string, options: types.StrictOptions & { noAutoWaiting?: boolean }) {
async blur(progress: Progress, selector: string, options: types.StrictOptions) {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._blur(progress)));
}

Expand Down Expand Up @@ -1430,7 +1420,7 @@ export class Frame extends SdkObject<FrameEventMap> {
return await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._selectOption(progress, elements, values, options, frameVisible, box));
}

async setInputFiles(progress: Progress, selector: string, params: Omit<channels.FrameSetInputFilesParams, 'timeout'> & { noAutoWaiting?: boolean }): Promise<channels.FrameSetInputFilesResult> {
async setInputFiles(progress: Progress, selector: string, params: Omit<channels.FrameSetInputFilesParams, 'timeout'>): Promise<channels.FrameSetInputFilesResult> {
const inputFileItems = await progress.race(prepareFilesForUpload(this, params));
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params, (progress, handle, box) => handle._setInputFiles(progress, inputFileItems, box)));
}
Expand All @@ -1445,11 +1435,11 @@ export class Frame extends SdkObject<FrameEventMap> {
dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._drop(progress, inputFileItems, data, options, frameVisible)));
}

async type(progress: Progress, selector: string, text: string, options: { delay?: number, noAutoWaiting?: boolean } & types.StrictOptions) {
async type(progress: Progress, selector: string, text: string, options: { delay?: number } & types.StrictOptions) {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle, box) => handle._type(progress, text, options, box)));
}

async press(progress: Progress, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean, noAutoWaiting?: boolean } & types.StrictOptions) {
async press(progress: Progress, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions) {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle, box) => handle._press(progress, key, options, box)));
}

Expand Down
1 change: 0 additions & 1 deletion packages/playwright-core/src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export type NavigateOptions = {

export type CommonActionOptions = StrictOptions & {
force?: boolean,
noAutoWaiting?: boolean,
scroll?: 'auto' | 'none',
};

Expand Down
27 changes: 0 additions & 27 deletions tests/page/page-click.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,33 +1352,6 @@ it('should click with tweened mouse movement', async ({ page, browserName, isAnd
]);
});

it('should not wait with noAutoWaiting', async ({ page }) => {
await page.setContent(`<button>click me</button>`);
const error = await page.locator('#target').click({ __testHookNoAutoWaiting: true } as any).catch(e => e);
expect(error.message).toContain('locator.click: Element(s) not found');
});

it('should not wait with noAutoWaiting 2', async ({ page }) => {
await page.setContent(`
<style>
div:hover button {
margin-left: 200px;
}
</style>
<div>
<button>click me</button>
</div>
`);
const error = await page.locator('button').click({ __testHookNoAutoWaiting: true } as any).catch(e => e);
expect(error.message).toContain('locator.click: <div>…</div> intercepts pointer events');
});

it('should not wait with noAutoWaiting 3', async ({ page }) => {
await page.setContent(`<button disabled>click me</button>`);
const error = await page.locator('button').click({ __testHookNoAutoWaiting: true } as any).catch(e => e);
expect(error.message).toContain('locator.click: Element is not enabled');
});

it('should abort via signal', async ({ page }) => {
await page.setContent(`<button style="display:none">click me</button>`);
const controller = new AbortController();
Expand Down
1 change: 0 additions & 1 deletion tests/webview/expectations/webkit-webview-page.txt
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,6 @@ page/page-aria-snapshot-ai.spec.ts › should snapshot a locator inside an ifram
# Unexpected pass: test.fail(webkit) expects ephemeral CacheStorage, but the iOS
# WebView uses the persistent default data store, so the entry survives reload.
page/page-cache-storage.spec.ts › CacheStorage entry should survive page.reload() [fail]
page/page-click.spec.ts › should not wait with noAutoWaiting 2 [fail]
page/page-emulate-media.spec.ts › should report hover and fine pointer for desktop [fail]
# evaluate() function arguments (#41801) work in the main frame, but the
# callback bridge is not wired for subframes, so the evaluate never resolves.
Expand Down
Loading