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); + }); }); });