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