fix(isVisible): return false during navigation (#22943)

Instead of throwing "Execution context was destroyed" error.

Drive-by: improve internal error messages for `ScopedRace` errors.

Fixes #22925.
This commit is contained in:
Dmitry Gozman 2023-05-10 16:56:59 -07:00 committed by GitHub
parent c9dad439cd
commit f2ad5bbfbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 34 additions and 5 deletions

View File

@ -1288,7 +1288,11 @@ export class Frame extends SdkObject {
const state = element ? injected.elementState(element, 'visible') : false;
return state === 'error:notconnected' ? false : state;
}, { info: resolved.info });
}, this._page._timeoutSettings.timeout({}));
}, this._page._timeoutSettings.timeout({})).catch(e => {
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e) || isSessionClosedError(e))
throw e;
return false;
});
}
async isHidden(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}): Promise<boolean> {

View File

@ -14,6 +14,8 @@
* limitations under the License.
*/
import { rewriteErrorMessage } from './stackTrace';
export class ManualPromise<T = void> extends Promise<T> {
private _resolve!: (t: T) => void;
private _reject!: (e: Error) => void;
@ -56,12 +58,14 @@ export class ManualPromise<T = void> extends Promise<T> {
export class ScopedRace {
private _terminateError: Error | undefined;
private _terminatePromises = new Set<ManualPromise<Error>>();
private _terminatePromises = new Map<ManualPromise<Error>, Error>();
scopeClosed(error: Error) {
this._terminateError = error;
for (const p of this._terminatePromises)
p.resolve(error);
for (const [p, e] of this._terminatePromises) {
rewriteErrorMessage(e, error.message);
p.resolve(e);
}
}
async race<T>(promise: Promise<T>): Promise<T> {
@ -76,7 +80,8 @@ export class ScopedRace {
const terminatePromise = new ManualPromise<Error>();
if (this._terminateError)
terminatePromise.resolve(this._terminateError);
this._terminatePromises.add(terminatePromise);
const error = new Error('');
this._terminatePromises.set(terminatePromise, error);
try {
return await Promise.race([
terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),

View File

@ -85,3 +85,23 @@ it('isVisible inside a role=button', async ({ page }) => {
await span.waitFor({ state: 'hidden' });
await page.locator('[role=button]').waitFor({ state: 'visible' });
});
it('isVisible during navigation should not throw', async ({ page, server }) => {
for (let i = 0; i < 20; i++) {
// Make sure previous navigation finishes, to avoid page.setContent throwing.
await page.waitForTimeout(100);
await page.setContent(`
<script>
setTimeout(() => {
window.location.href = ${JSON.stringify(server.EMPTY_PAGE)};
}, Math.random(50));
</script>
`);
expect(await page.locator('div').isVisible()).toBe(false);
}
});
it('isVisible with invalid selector should throw', async ({ page }) => {
const error = await page.locator('hey=what').isVisible().catch(e => e);
expect(error.message).toContain('Unknown engine "hey" while parsing selector hey=what');
});