From e3dd5968cae196b64d958c161fed3d1b39aed3f6 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 18 Jul 2023 11:26:06 +0200 Subject: [PATCH] fix(locators): reject the race if there are only failures (#10567) --- packages/puppeteer-core/src/api/Locator.ts | 22 +++++++++- test/src/locator.spec.ts | 47 ++++++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/puppeteer-core/src/api/Locator.ts b/packages/puppeteer-core/src/api/Locator.ts index 62f41637a6a..85b98b193cf 100644 --- a/packages/puppeteer-core/src/api/Locator.ts +++ b/packages/puppeteer-core/src/api/Locator.ts @@ -722,7 +722,7 @@ class RaceLocatorImpl extends Locator { return abortController; }; - await Promise.allSettled( + const results = await Promise.allSettled( this.#locators.map(locator => { return action( locator.on(LocatorEmittedEvents.Action, handleLocatorAction(locator)), @@ -732,6 +732,26 @@ class RaceLocatorImpl extends Locator { ); options.signal?.throwIfAborted(); + + const rejected = results.filter( + (result): result is PromiseRejectedResult => { + return result.status === 'rejected'; + } + ); + + // If some locators are fulfilled, do not throw. + if (rejected.length !== results.length) { + return; + } + + for (const result of rejected) { + const reason = result.reason; + // AbortError is be an expected result of a race. + if (isErrorLike(reason) && reason.name === 'AbortError') { + continue; + } + throw reason; + } } override async click( diff --git a/test/src/locator.spec.ts b/test/src/locator.spec.ts index 728bc1d324f..db37b5fcd46 100644 --- a/test/src/locator.spec.ts +++ b/test/src/locator.spec.ts @@ -222,7 +222,9 @@ describe('Locator', function () { }); it('should time out', async () => { - const clock = sinon.useFakeTimers(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); try { const {page} = await getTestState(); @@ -243,7 +245,9 @@ describe('Locator', function () { it('should retry clicks on errors', async () => { const {page} = await getTestState(); - const clock = sinon.useFakeTimers(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); try { page.setDefaultTimeout(5000); await page.setViewport({width: 500, height: 500}); @@ -262,7 +266,9 @@ describe('Locator', function () { it('can be aborted', async () => { const {page} = await getTestState(); - const clock = sinon.useFakeTimers(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); try { page.setDefaultTimeout(5000); @@ -476,7 +482,9 @@ describe('Locator', function () { it('can be aborted', async () => { const {page} = await getTestState(); - const clock = sinon.useFakeTimers(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); try { await page.setViewport({width: 500, height: 500}); await page.setContent(` @@ -498,5 +506,36 @@ describe('Locator', function () { clock.restore(); } }); + + it('should time out when all locators do not match', async () => { + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + }); + try { + const {page} = await getTestState(); + page.setDefaultTimeout(5000); + await page.setContent(``); + const result = Locator.race([ + page.locator('not-found'), + page.locator('not-found'), + ]).click(); + clock.tick(5100); + await expect(result).rejects.toEqual( + new TimeoutError('waitForFunction timed out. The timeout is 5000ms.') + ); + } finally { + clock.restore(); + } + }); + + it('should not time out when one of the locators matches', async () => { + const {page} = await getTestState(); + await page.setContent(``); + const result = Locator.race([ + page.locator('not-found'), + page.locator('button'), + ]).click(); + await expect(result).resolves.toEqual(undefined); + }); }); });