Skip to content

Commit fd01e7e

Browse files
authored
chore: remove legacy timeouts support in Progress (microsoft#36744)
1 parent 2733247 commit fd01e7e

File tree

6 files changed

+48
-142
lines changed

6 files changed

+48
-142
lines changed

.github/workflows/tests_others.yml

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -134,27 +134,6 @@ jobs:
134134
env:
135135
PW_CLOCK: ${{ matrix.clock }}
136136

137-
test_legacy_progress_timeouts:
138-
name: legacy progress timeouts
139-
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
140-
permissions:
141-
id-token: write # This is required for OIDC login (azure/login) to succeed
142-
contents: read # This is required for actions/checkout to succeed
143-
runs-on: ubuntu-22.04
144-
steps:
145-
- uses: actions/checkout@v4
146-
- uses: ./.github/actions/run-test
147-
with:
148-
node-version: 20
149-
browsers-to-install: chromium
150-
command: npm run test -- --project=chromium-*
151-
bot-name: "legacy-progress-timeouts-linux"
152-
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
153-
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
154-
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
155-
env:
156-
PLAYWRIGHT_LEGACY_TIMEOUTS: 1
157-
158137
test_electron:
159138
name: Electron - ${{ matrix.os }}
160139
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}

packages/playwright-core/src/server/dispatchers/frameDispatcher.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,7 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
266266
let expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
267267
if (params.expression === 'to.match.aria' && expectedValue)
268268
expectedValue = parseAriaSnapshotUnsafe(yaml, expectedValue);
269-
const result = await this._frame.expect(progress, params.selector, { ...params, expectedValue }, params.timeout, result => {
270-
if (result.received !== undefined)
271-
result.received = serializeResult(result.received);
272-
return result;
273-
});
269+
const result = await this._frame.expect(progress, params.selector, { ...params, expectedValue }, params.timeout);
274270
if (result.received !== undefined)
275271
result.received = serializeResult(result.received);
276272
return result;

packages/playwright-core/src/server/frames.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,35 +1345,14 @@ export class Frame extends SdkObject {
13451345
return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => progress.race(handle.ariaSnapshot(options)));
13461346
}
13471347

1348-
// TODO: remove errorResultHandler once legacy progress is removed.
1349-
async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams, timeout?: number, errorResultHandler?: (result: ExpectResult) => ExpectResult): Promise<ExpectResult> {
1348+
async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams, timeout?: number): Promise<ExpectResult> {
13501349
progress.log(`${renderTitleForCall(progress.metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`);
1351-
const result = await this._expectImpl(progress, selector, options, errorResultHandler);
1352-
return result;
1353-
}
1354-
1355-
private async _expectImpl(progress: Progress, selector: string | undefined, options: FrameExpectParams, errorResultHandler?: (result: ExpectResult) => ExpectResult): Promise<ExpectResult> {
13561350
const lastIntermediateResult: { received?: any, isSet: boolean } = { isSet: false };
13571351
const fixupMetadataError = (result: ExpectResult) => {
13581352
// Library mode special case for the expect errors which are return values, not exceptions.
13591353
if (result.matches === options.isNot)
13601354
progress.metadata.error = { error: { name: 'Expect', message: 'Expect failed' } };
13611355
};
1362-
const handleError = (e: any, isProgressError: boolean) => {
1363-
// Q: Why not throw upon isNonRetriableError(e) as in other places?
1364-
// A: We want user to receive a friendly message containing the last intermediate result.
1365-
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
1366-
throw e;
1367-
const result: ExpectResult = { matches: options.isNot, log: compressCallLog(progress.metadata.log) };
1368-
if (lastIntermediateResult.isSet)
1369-
result.received = lastIntermediateResult.received;
1370-
if (e instanceof TimeoutError)
1371-
result.timedOut = true;
1372-
fixupMetadataError(result);
1373-
return (isProgressError && errorResultHandler) ? errorResultHandler(result) : result;
1374-
};
1375-
progress.legacySetErrorHandler(e => handleError(e, true));
1376-
13771356
try {
13781357
// Step 1: perform locator handlers checkpoint with a specified timeout.
13791358
if (selector)
@@ -1383,7 +1362,6 @@ export class Frame extends SdkObject {
13831362
// Step 2: perform one-shot expect check without a timeout.
13841363
// Supports the case of `expect(locator).toBeVisible({ timeout: 1 })`
13851364
// that should succeed when the locator is already visible.
1386-
progress.legacyDisableTimeout();
13871365
try {
13881366
const resultOneShot = await this._expectInternal(progress, selector, options, lastIntermediateResult, true);
13891367
if (resultOneShot.matches !== options.isNot)
@@ -1393,7 +1371,6 @@ export class Frame extends SdkObject {
13931371
throw e;
13941372
// Ignore any other errors from one-shot, we'll handle them during retries.
13951373
}
1396-
progress.legacyEnableTimeout();
13971374

13981375
// Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time.
13991376
const result = await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => {
@@ -1410,7 +1387,17 @@ export class Frame extends SdkObject {
14101387
fixupMetadataError(result);
14111388
return result;
14121389
} catch (e) {
1413-
return handleError(e, false);
1390+
// Q: Why not throw upon isNonRetriableError(e) as in other places?
1391+
// A: We want user to receive a friendly message containing the last intermediate result.
1392+
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
1393+
throw e;
1394+
const result: ExpectResult = { matches: options.isNot, log: compressCallLog(progress.metadata.log) };
1395+
if (lastIntermediateResult.isSet)
1396+
result.received = lastIntermediateResult.received;
1397+
if (e instanceof TimeoutError)
1398+
result.timedOut = true;
1399+
fixupMetadataError(result);
1400+
return result;
14141401
}
14151402
}
14161403

packages/playwright-core/src/server/page.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -632,22 +632,6 @@ export class Page extends SdkObject {
632632
intermediateResult = { errorMessage: comparatorResult.errorMessage, diff: comparatorResult.diff, actual, previous };
633633
return false;
634634
};
635-
const handleError = (e: any) => {
636-
// Q: Why not throw upon isNonRetriableError(e) as in other places?
637-
// A: We want user to receive a friendly diff between actual and expected/previous.
638-
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
639-
throw e;
640-
let errorMessage = e.message;
641-
if (e instanceof TimeoutError && intermediateResult?.previous)
642-
errorMessage = `Failed to take two consecutive stable screenshots.`;
643-
return {
644-
log: compressCallLog(e.message ? [...progress.metadata.log, e.message] : progress.metadata.log),
645-
...intermediateResult,
646-
errorMessage,
647-
timedOut: (e instanceof TimeoutError),
648-
};
649-
};
650-
progress.legacySetErrorHandler(handleError);
651635

652636
try {
653637
let actual: Buffer | undefined;
@@ -700,7 +684,19 @@ export class Page extends SdkObject {
700684
}
701685
throw new Error(intermediateResult!.errorMessage);
702686
} catch (e) {
703-
return handleError(e);
687+
// Q: Why not throw upon isNonRetriableError(e) as in other places?
688+
// A: We want user to receive a friendly diff between actual and expected/previous.
689+
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
690+
throw e;
691+
let errorMessage = e.message;
692+
if (e instanceof TimeoutError && intermediateResult?.previous)
693+
errorMessage = `Failed to take two consecutive stable screenshots.`;
694+
return {
695+
log: compressCallLog(e.message ? [...progress.metadata.log, e.message] : progress.metadata.log),
696+
...intermediateResult,
697+
errorMessage,
698+
timedOut: (e instanceof TimeoutError),
699+
};
704700
}
705701
}
706702

packages/playwright-core/src/server/progress.ts

Lines changed: 22 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
*/
1616

1717
import { TimeoutError } from './errors';
18-
import { assert, monotonicTime } from '../utils';
18+
import { assert } from '../utils';
1919
import { ManualPromise } from '../utils/isomorphic/manualPromise';
2020

2121
import type { Progress } from '@protocol/progress';
22-
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
22+
import type { CallMetadata, SdkObject } from './instrumentation';
2323
import type { LogName } from './utils/debugLogger';
2424

2525
export type { Progress } from '@protocol/progress';
@@ -31,24 +31,15 @@ export class ProgressController {
3131
// Cleanups to be run only in the case of abort.
3232
private _cleanups: ((error: Error | undefined) => any)[] = [];
3333

34-
// Lenient mode races against the timeout. This guarantees that timeout is respected,
35-
// but may have some work being done after the timeout due to parallel control flow.
36-
//
37-
// Strict mode aborts the progress and requires the code to react to it. This way,
38-
// progress only finishes after the inner callback exits, guaranteeing no work after the timeout.
39-
private _strictMode = false;
40-
4134
private _logName: LogName;
4235
private _state: 'before' | 'running' | { error: Error } | 'finished' = 'before';
36+
private _sdkObject: SdkObject;
37+
4338
readonly metadata: CallMetadata;
44-
readonly instrumentation: Instrumentation;
45-
readonly sdkObject: SdkObject;
4639

4740
constructor(metadata: CallMetadata, sdkObject: SdkObject) {
48-
this._strictMode = !process.env.PLAYWRIGHT_LEGACY_TIMEOUTS;
4941
this.metadata = metadata;
50-
this.sdkObject = sdkObject;
51-
this.instrumentation = sdkObject.instrumentation;
42+
this._sdkObject = sdkObject;
5243
this._logName = sdkObject.logName || 'api';
5344
this._forceAbortPromise.catch(e => null); // Prevent unhandled promise rejection.
5445
}
@@ -63,54 +54,24 @@ export class ProgressController {
6354
this._state = { error };
6455
this._forceAbortPromise.reject(error);
6556
}
66-
if (this._strictMode)
67-
await this._donePromise;
57+
await this._donePromise;
6858
}
6959

7060
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
7161
assert(this._state === 'before');
7262
this._state = 'running';
73-
let customErrorHandler: ((error: Error) => any) | undefined;
74-
75-
const deadline = timeout ? Math.min(monotonicTime() + timeout, 2147483647) : 0; // 2^31-1 safe setTimeout in Node.
76-
const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded.`);
77-
78-
let timer: NodeJS.Timeout | undefined;
79-
const startTimer = () => {
80-
if (!deadline)
81-
return;
82-
const onTimeout = () => {
83-
if (this._state === 'running') {
84-
(timeoutError as any)[kAbortErrorSymbol] = true;
85-
this._state = { error: timeoutError };
86-
this._forceAbortPromise.reject(timeoutError);
87-
}
88-
};
89-
const remaining = deadline - monotonicTime();
90-
if (remaining <= 0)
91-
onTimeout();
92-
else
93-
timer = setTimeout(onTimeout, remaining);
94-
};
9563

9664
const progress: Progress = {
9765
log: message => {
9866
if (this._state === 'running')
9967
this.metadata.log.push(message);
10068
// Note: we might be sending logs after progress has finished, for example browser logs.
101-
this.instrumentation.onCallLog(this.sdkObject, this.metadata, this._logName, message);
69+
this._sdkObject.instrumentation.onCallLog(this._sdkObject, this.metadata, this._logName, message);
10270
},
10371
cleanupWhenAborted: (cleanup: (error: Error | undefined) => any) => {
104-
if (this._strictMode) {
105-
if (this._state !== 'running')
106-
throw new Error('Internal error: cannot register cleanup after operation has finished.');
107-
this._cleanups.push(cleanup);
108-
return;
109-
}
110-
if (this._state === 'running')
111-
this._cleanups.push(cleanup);
112-
else
113-
runCleanup(typeof this._state === 'object' ? this._state.error : undefined, cleanup);
72+
if (this._state !== 'running')
73+
throw new Error('Internal error: cannot register cleanup after operation has finished.');
74+
this._cleanups.push(cleanup);
11475
},
11576
metadata: this.metadata,
11677
race: <T>(promise: Promise<T> | Promise<T>[]) => {
@@ -131,35 +92,27 @@ export class ProgressController {
13192
const promise = new Promise<void>(f => timer = setTimeout(f, timeout));
13293
return progress.race(promise).finally(() => clearTimeout(timer));
13394
},
134-
legacyDisableTimeout: () => {
135-
if (this._strictMode)
136-
return;
137-
clearTimeout(timer);
138-
},
139-
legacyEnableTimeout: () => {
140-
if (this._strictMode)
141-
return;
142-
startTimer();
143-
},
144-
legacySetErrorHandler: (handler: (error: Error) => any) => {
145-
if (this._strictMode)
146-
return;
147-
customErrorHandler = handler;
148-
},
14995
};
15096

151-
startTimer();
97+
let timer: NodeJS.Timeout | undefined;
98+
if (timeout) {
99+
const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded.`);
100+
timer = setTimeout(() => {
101+
if (this._state === 'running') {
102+
(timeoutError as any)[kAbortErrorSymbol] = true;
103+
this._state = { error: timeoutError };
104+
this._forceAbortPromise.reject(timeoutError);
105+
}
106+
}, timeout);
107+
}
152108

153109
try {
154-
const promise = task(progress);
155-
const result = this._strictMode ? await promise : await Promise.race([promise, this._forceAbortPromise]);
110+
const result = await task(progress);
156111
this._state = 'finished';
157112
return result;
158113
} catch (error) {
159114
this._state = { error };
160115
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(error, cleanup)));
161-
if (customErrorHandler)
162-
return customErrorHandler(error);
163116
throw error;
164117
} finally {
165118
clearTimeout(timer);

packages/protocol/src/progress.d.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,4 @@ export interface Progress {
3838
raceWithCleanup<T>(promise: Promise<T>, cleanup: (result: T) => any): Promise<T>;
3939
wait(timeout: number): Promise<void>;
4040
metadata: CallMetadata;
41-
42-
// Legacy lenient mode api only. To be removed.
43-
legacyDisableTimeout(): void;
44-
legacyEnableTimeout(): void;
45-
legacySetErrorHandler(handler: (error: Error) => void): void;
4641
}

0 commit comments

Comments
 (0)