Skip to content

Commit 1afa38d

Browse files
authored
chore(expect): extract polling from expect.poll and expect().toPass (#19882)
This extracts & unifies polling machinery from `expect.poll` and `expect.toPass` methods.
1 parent 90af7a7 commit 1afa38d

File tree

4 files changed

+77
-64
lines changed

4 files changed

+77
-64
lines changed

packages/playwright-core/src/utils/timeoutRunner.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,24 @@ export async function raceAgainstTimeout<T>(cb: () => Promise<T>, timeout: numbe
109109
throw e;
110110
}
111111
}
112+
113+
export async function pollAgainstTimeout<T>(callback: () => Promise<{ continuePolling: boolean, result: T }>, timeout: number, pollIntervals: number[] = [100, 250, 500, 1000]): Promise<{ result?: T, timedOut: boolean }> {
114+
const startTime = monotonicTime();
115+
const lastPollInterval = pollIntervals.pop() ?? 1000;
116+
let lastResult: T|undefined;
117+
const wrappedCallback = () => Promise.resolve().then(callback);
118+
while (true) {
119+
const elapsed = monotonicTime() - startTime;
120+
if (timeout !== 0 && elapsed > timeout)
121+
break;
122+
const received = timeout !== 0 ? await raceAgainstTimeout(wrappedCallback, timeout - elapsed)
123+
: await wrappedCallback().then(value => ({ result: value, timedOut: false }));
124+
if (received.timedOut)
125+
break;
126+
lastResult = received.result.result;
127+
if (!received.result.continuePolling)
128+
return { result: received.result.result, timedOut: false };
129+
await new Promise(x => setTimeout(x, pollIntervals!.shift() ?? lastPollInterval));
130+
}
131+
return { timedOut: true, result: lastResult };
132+
}

packages/playwright-test/src/expect.ts

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
17+
import { pollAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
1818
import path from 'path';
1919
import {
2020
toBeChecked,
@@ -44,7 +44,6 @@ import { toMatchSnapshot, toHaveScreenshot } from './matchers/toMatchSnapshot';
4444
import type { Expect } from './types';
4545
import { currentTestInfo } from './globals';
4646
import { serializeError, captureStackTrace, currentExpectTimeout } from './util';
47-
import { monotonicTime } from 'playwright-core/lib/utils';
4847
import {
4948
expect as expectLibrary,
5049
INVERTED_COLOR,
@@ -253,38 +252,30 @@ class ExpectMetaInfoProxyHandler {
253252
}
254253

255254
async function pollMatcher(matcherName: any, isNot: boolean, pollIntervals: number[] | undefined, timeout: number, generator: () => any, ...args: any[]) {
256-
let matcherError;
257-
const startTime = monotonicTime();
258-
pollIntervals = pollIntervals || [100, 250, 500, 1000];
259-
const lastPollInterval = pollIntervals[pollIntervals.length - 1] || 1000;
260-
while (true) {
261-
const elapsed = monotonicTime() - startTime;
262-
if (timeout !== 0 && elapsed > timeout)
263-
break;
264-
const received = timeout !== 0 ? await raceAgainstTimeout(generator, timeout - elapsed) : await generator();
265-
if (received.timedOut)
266-
break;
255+
const result = await pollAgainstTimeout<Error|undefined>(async () => {
256+
const value = await generator();
257+
let expectInstance = expectLibrary(value) as any;
258+
if (isNot)
259+
expectInstance = expectInstance.not;
267260
try {
268-
let expectInstance = expectLibrary(received.result) as any;
269-
if (isNot)
270-
expectInstance = expectInstance.not;
271261
expectInstance[matcherName].call(expectInstance, ...args);
272-
return;
273-
} catch (e) {
274-
matcherError = e;
262+
return { continuePolling: false, result: undefined };
263+
} catch (error) {
264+
return { continuePolling: true, result: error };
275265
}
276-
await new Promise(x => setTimeout(x, pollIntervals!.shift() ?? lastPollInterval));
277-
}
266+
}, timeout, pollIntervals ?? [100, 250, 500, 1000]);
278267

279-
const timeoutMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
280-
const message = matcherError ? [
281-
matcherError.message,
282-
'',
283-
`Call Log:`,
284-
`- ${timeoutMessage}`,
285-
].join('\n') : timeoutMessage;
268+
if (result.timedOut) {
269+
const timeoutMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
270+
const message = result.result ? [
271+
result.result.message,
272+
'',
273+
`Call Log:`,
274+
`- ${timeoutMessage}`,
275+
].join('\n') : timeoutMessage;
286276

287-
throw new Error(message);
277+
throw new Error(message);
278+
}
288279
}
289280

290281
expectLibrary.extend(customMatchers);

packages/playwright-test/src/matchers/matchers.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ import { toEqual } from './toEqual';
2525
import { toExpectedTextValues, toMatchText } from './toMatchText';
2626
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
2727
import { isTextualMimeType } from 'playwright-core/lib/utils/mimeType';
28-
import { monotonicTime } from 'playwright-core/lib/utils';
29-
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
28+
import { pollAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
3029

3130
interface LocatorEx extends Locator {
3231
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
@@ -320,43 +319,27 @@ export async function toPass(
320319
timeout?: number,
321320
} = {},
322321
) {
323-
let matcherError: Error | undefined;
324-
const startTime = monotonicTime();
325-
const pollIntervals = options.intervals || [100, 250, 500, 1000];
326-
const lastPollInterval = pollIntervals[pollIntervals.length - 1] || 1000;
327322
const timeout = options.timeout !== undefined ? options.timeout : 0;
328-
const isNot = this.isNot;
329323

330-
while (true) {
331-
const elapsed = monotonicTime() - startTime;
332-
if (timeout !== 0 && elapsed > timeout)
333-
break;
324+
const result = await pollAgainstTimeout<Error|undefined>(async () => {
334325
try {
335-
const wrappedCallback = () => Promise.resolve().then(callback);
336-
const received = timeout !== 0 ? await raceAgainstTimeout(wrappedCallback, timeout - elapsed)
337-
: await wrappedCallback().then(() => ({ timedOut: false }));
338-
if (received.timedOut)
339-
break;
340-
// The check passed, exit sucessfully.
341-
if (isNot)
342-
matcherError = new Error('Expected to fail, but passed');
343-
else
344-
return { message: () => '', pass: true };
326+
await callback();
327+
return { continuePolling: this.isNot, result: undefined };
345328
} catch (e) {
346-
if (isNot)
347-
return { message: () => '', pass: false };
348-
matcherError = e;
329+
return { continuePolling: !this.isNot, result: e };
349330
}
350-
await new Promise(x => setTimeout(x, pollIntervals!.shift() ?? lastPollInterval));
351-
}
331+
}, timeout, options.intervals || [100, 250, 500, 1000]);
352332

353-
const timeoutMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
354-
const message = () => matcherError ? [
355-
matcherError.message,
356-
'',
357-
`Call Log:`,
358-
`- ${timeoutMessage}`,
359-
].join('\n') : timeoutMessage;
333+
if (result.timedOut) {
334+
const timeoutMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
335+
const message = () => result.result ? [
336+
result.result.message,
337+
'',
338+
`Call Log:`,
339+
`- ${timeoutMessage}`,
340+
].join('\n') : timeoutMessage;
360341

361-
return { message, pass: isNot ? true : false };
362-
}
342+
return { message, pass: this.isNot };
343+
}
344+
return { pass: !this.isNot, message: () => '' };
345+
}

tests/playwright-test/expect-poll.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,24 @@ test('should show error that is thrown from predicate', async ({ runInlineTest }
137137
expect(stripAnsi(result.output)).toContain('foo bar baz');
138138
});
139139

140+
test('should not retry predicate that threw an error', async ({ runInlineTest }) => {
141+
const result = await runInlineTest({
142+
'a.spec.ts': `
143+
const { test } = pwt;
144+
test('should fail', async ({ page }) => {
145+
let iteration = 0;
146+
await test.expect.poll(() => {
147+
if (iteration++ === 0)
148+
throw new Error('foo bar baz');
149+
return 42;
150+
}).toBe(42);
151+
});
152+
`
153+
});
154+
expect(result.exitCode).toBe(1);
155+
expect(stripAnsi(result.output)).toContain('foo bar baz');
156+
});
157+
140158
test('should support .not predicate', async ({ runInlineTest }) => {
141159
const result = await runInlineTest({
142160
'a.spec.ts': `

0 commit comments

Comments
 (0)