From 8bfd0eb6e4188985c6a5f0ad57306eda10a37266 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 31 May 2024 14:44:26 -0700 Subject: [PATCH] chore: introduce clock test mode (#31110) --- .github/workflows/tests_clock.yml | 62 +++++++++++++++++++ docs/src/api/class-clock.md | 2 +- .../playwright-core/src/client/browser.ts | 2 + packages/playwright-core/src/client/frame.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 1 - .../src/server/dispatchers/frameDispatcher.ts | 2 +- packages/playwright-core/src/server/dom.ts | 4 +- packages/playwright-core/src/server/frames.ts | 8 +-- .../src/server/injected/fakeTimers.ts | 19 +++++- .../src/server/injected/highlight.ts | 2 +- .../src/server/injected/injectedScript.ts | 29 ++++++++- .../src/server/injected/recorder/recorder.ts | 8 +-- .../src/server/injected/utilityScript.ts | 52 +++++++++++++++- .../playwright-core/src/server/javascript.ts | 7 ++- packages/playwright-core/types/types.d.ts | 3 +- packages/protocol/src/channels.ts | 2 - packages/protocol/src/protocol.yml | 1 - tests/assets/input/handle-locator.html | 2 +- tests/config/baseTest.ts | 13 ++++ .../browsercontext-add-cookies.spec.ts | 1 + tests/library/browsercontext-events.spec.ts | 2 +- tests/library/browsertype-launch.spec.ts | 2 +- tests/library/chromium/css-coverage.spec.ts | 2 +- tests/library/chromium/js-coverage.spec.ts | 1 + tests/library/headful.spec.ts | 1 + tests/library/popup.spec.ts | 8 +-- tests/library/trace-viewer.spec.ts | 4 +- tests/library/tracing.spec.ts | 6 +- tests/library/video.spec.ts | 4 +- tests/page/elementhandle-screenshot.spec.ts | 11 ++-- .../elementhandle-scroll-into-view.spec.ts | 2 +- tests/page/elementhandle-select-text.spec.ts | 2 +- ...ementhandle-wait-for-element-state.spec.ts | 10 ++- tests/page/page-click-timeout-4.spec.ts | 4 +- tests/page/page-click.spec.ts | 12 ++-- tests/page/page-clock.spec.ts | 2 + tests/page/page-close.spec.ts | 3 +- tests/page/page-dialog.spec.ts | 2 +- tests/page/page-dispatchevent.spec.ts | 2 +- tests/page/page-drag.spec.ts | 2 +- tests/page/page-evaluate.spec.ts | 20 ++---- tests/page/page-event-console.spec.ts | 4 +- tests/page/page-event-pageerror.spec.ts | 16 +++-- tests/page/page-expose-function.spec.ts | 2 +- tests/page/page-fill.spec.ts | 8 +-- tests/page/page-goto.spec.ts | 5 +- tests/page/page-history.spec.ts | 1 + tests/page/page-mouse.spec.ts | 4 +- tests/page/page-screenshot.spec.ts | 14 +---- tests/page/page-select-option.spec.ts | 8 +-- tests/page/page-set-input-files.spec.ts | 2 +- tests/page/page-wait-for-function.spec.ts | 4 +- tests/page/page-wait-for-navigation.spec.ts | 2 +- tests/page/page-wait-for-request.spec.ts | 2 +- tests/page/page-wait-for-response.spec.ts | 2 +- tests/page/page-wait-for-selector-1.spec.ts | 7 +-- tests/page/pageTest.ts | 10 ++- tests/page/retarget.spec.ts | 7 +-- tests/page/wheel.spec.ts | 5 +- .../ui-mode-test-output.spec.ts | 4 +- 60 files changed, 291 insertions(+), 140 deletions(-) create mode 100644 .github/workflows/tests_clock.yml diff --git a/.github/workflows/tests_clock.yml b/.github/workflows/tests_clock.yml new file mode 100644 index 0000000000..cc9a519ee3 --- /dev/null +++ b/.github/workflows/tests_clock.yml @@ -0,0 +1,62 @@ +name: "tests Clock" + +on: + push: + branches: + - main + - release-* + pull_request: + paths-ignore: + - 'browser_patches/**' + - 'docs/**' + types: [ labeled ] + branches: + - main + - release-* + +env: + # Force terminal colors. @see https://www.npmjs.com/package/colors + FORCE_COLOR: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + +jobs: + frozen_time_linux: + name: Frozen time library + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + node-version: 20 + browsers-to-install: chromium + command: npm run test -- --project=chromium-* + bot-name: "frozen-time-library-chromium-linux" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + PW_FREEZE_TIME: 1 + + frozen_time_test_runner: + name: Frozen time test runner + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} + runs-on: ubuntu-22.04 + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/run-test + with: + node-version: 20 + command: npm run ttest + bot-name: "frozen-time-runner-chromium-linux" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} + env: + PW_FREEZE_TIME: 1 diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index a78f7b30ba..90c75de16a 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -21,7 +21,7 @@ Install fake timers with the specified unix epoch (default: 0). - `toFake` <[Array]<[FakeMethod]<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">>> An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: ['setTimeout'] })` will fake only `setTimeout()`. -By default, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` and `Date` are faked. +By default, all the methods are faked. ### option: Clock.install.loopLimit * since: v1.45 diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index be47ddeb51..802939295c 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -85,6 +85,8 @@ export class Browser extends ChannelOwner implements ap const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); + if (!forReuse && !!process.env.PW_FREEZE_TIME) + await this._wrapApiCall(async () => { await context.clock.install(); }, true); return context; } diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 582490d0af..a7ceeb9456 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -189,7 +189,7 @@ export class Frame extends ChannelOwner implements api.Fr async _evaluateExposeUtilityScript(pageFunction: structs.PageFunction, arg?: Arg): Promise { assertMaxArguments(arguments.length, 2); - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', exposeUtilityScript: true, arg: serializeArgument(arg) }); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 6e2993901f..ec1d4bffe2 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1429,7 +1429,6 @@ scheme.FrameDispatchEventResult = tOptional(tObject({})); scheme.FrameEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), - exposeUtilityScript: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.FrameEvaluateExpressionResult = tObject({ diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index b335f27ea0..6dcb9a7220 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -83,7 +83,7 @@ export class FrameDispatcher extends Dispatcher { - return { value: serializeResult(await this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction, exposeUtilityScript: params.exposeUtilityScript }, parseArgument(params.arg))) }; + return { value: serializeResult(await this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) }; } async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index eb2f2068f1..01e70f1637 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -71,11 +71,11 @@ export class FrameExecutionContext extends js.ExecutionContext { return js.evaluate(this, false /* returnByValue */, pageFunction, arg); } - async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise { + async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg?: any): Promise { return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg); } - async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise> { + async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg?: any): Promise> { return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index e2b1eec2a5..c2a11d27be 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -745,13 +745,13 @@ export class Frame extends SdkObject { return this._context('utility'); } - async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise { + async evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise { const context = await this._context(options.world ?? 'main'); const value = await context.evaluateExpression(expression, options, arg); return value; } - async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise> { + async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise> { const context = await this._context(options.world ?? 'main'); const value = await context.evaluateExpressionHandle(expression, options, arg); return value; @@ -1513,9 +1513,9 @@ export class Frame extends SdkObject { return; } if (typeof polling !== 'number') - requestAnimationFrame(next); + injected.builtinRequestAnimationFrame(next); else - setTimeout(next, polling); + injected.builtinSetTimeout(next, polling); } catch (e) { reject(e); } diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/fakeTimers.ts index 3e8b1ab05b..e3d8e5a54d 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/fakeTimers.ts @@ -19,5 +19,22 @@ import SinonFakeTimers from '../../third_party/fake-timers-src'; import type * as channels from '@protocol/channels'; export function install(params: channels.BrowserContextClockInstallOptions) { - return SinonFakeTimers.install(params); + // eslint-disable-next-line no-restricted-globals + const window = globalThis; + const builtin = { + setTimeout: window.setTimeout.bind(window), + clearTimeout: window.clearTimeout.bind(window), + setInterval: window.setInterval.bind(window), + clearInterval: window.clearInterval.bind(window), + requestAnimationFrame: window.requestAnimationFrame.bind(window), + cancelAnimationFrame: window.cancelAnimationFrame.bind(window), + requestIdleCallback: window.requestIdleCallback?.bind(window), + cancelIdleCallback: window.cancelIdleCallback?.bind(window), + performance: window.performance, + Intl: window.Intl, + Date: window.Date, + }; + const result = SinonFakeTimers.install(params); + result.builtin = builtin; + return result; } diff --git a/packages/playwright-core/src/server/injected/highlight.ts b/packages/playwright-core/src/server/injected/highlight.ts index dfc298cb24..c85216e106 100644 --- a/packages/playwright-core/src/server/injected/highlight.ts +++ b/packages/playwright-core/src/server/injected/highlight.ts @@ -101,7 +101,7 @@ export class Highlight { if (this._rafRequest) cancelAnimationFrame(this._rafRequest); this.updateHighlight(this._injectedScript.querySelectorAll(selector, this._injectedScript.document.documentElement), { tooltipText: asLocator(this._language, stringifySelector(selector)) }); - this._rafRequest = requestAnimationFrame(() => this.runHighlightOnRaf(selector)); + this._rafRequest = this._injectedScript.builtinRequestAnimationFrame(() => this.runHighlightOnRaf(selector)); } uninstall() { diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index dba8d188e4..347d5cb40c 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -124,6 +124,18 @@ export class InjectedScript { (this.window as any).__injectedScript = this; } + builtinSetTimeout(callback: Function, timeout: number) { + if (this.window.__pwFakeTimers?.builtin) + return this.window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + return setTimeout(callback, timeout); + } + + builtinRequestAnimationFrame(callback: FrameRequestCallback) { + if (this.window.__pwFakeTimers?.builtin) + return this.window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + return requestAnimationFrame(callback); + } + eval(expression: string): any { return this.window.eval(expression); } @@ -427,7 +439,7 @@ export class InjectedScript { observer.observe(element); // Firefox doesn't call IntersectionObserver callback unless // there are rafs. - requestAnimationFrame(() => {}); + this.builtinRequestAnimationFrame(() => {}); }); } @@ -536,12 +548,12 @@ export class InjectedScript { if (success !== continuePolling) fulfill(success); else - requestAnimationFrame(raf); + this.builtinRequestAnimationFrame(raf); } catch (e) { reject(e); } }; - requestAnimationFrame(raf); + this.builtinRequestAnimationFrame(raf); return result; } @@ -1510,3 +1522,14 @@ function deepEquals(a: any, b: any): boolean { return false; } + +declare global { + interface Window { + __pwFakeTimers?: { + builtin: { + setTimeout: Window['setTimeout'], + requestAnimationFrame: Window['requestAnimationFrame'], + } + } + } +} diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 8c02ddb507..3977d69aef 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -881,7 +881,7 @@ class Overlay { flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') { const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle; element.classList.add('succeeded'); - setTimeout(() => element.classList.remove('succeeded'), 2000); + this._recorder.injectedScript.builtinSetTimeout(() => element.classList.remove('succeeded'), 2000); } private _hideOverlay() { @@ -1312,7 +1312,7 @@ interface Embedder { export class PollingRecorder implements RecorderDelegate { private _recorder: Recorder; private _embedder: Embedder; - private _pollRecorderModeTimer: NodeJS.Timeout | undefined; + private _pollRecorderModeTimer: number | undefined; constructor(injectedScript: InjectedScript) { this._recorder = new Recorder(injectedScript); @@ -1333,7 +1333,7 @@ export class PollingRecorder implements RecorderDelegate { clearTimeout(this._pollRecorderModeTimer); const state = await this._embedder.__pw_recorderState().catch(() => {}); if (!state) { - this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod); + this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); return; } const win = this._recorder.document.defaultView!; @@ -1343,7 +1343,7 @@ export class PollingRecorder implements RecorderDelegate { state.actionPoint = undefined; } this._recorder.setUIState(state, this); - this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod); + this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); } async performAction(action: actions.Action) { diff --git a/packages/playwright-core/src/server/injected/utilityScript.ts b/packages/playwright-core/src/server/injected/utilityScript.ts index 92571ad8a2..e8dbbc6ba1 100644 --- a/packages/playwright-core/src/server/injected/utilityScript.ts +++ b/packages/playwright-core/src/server/injected/utilityScript.ts @@ -17,17 +17,20 @@ import { serializeAsCallArgument, parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers'; export class UtilityScript { + constructor(isUnderTest: boolean) { + if (isUnderTest) + this._setBuiltins(); + } + serializeAsCallArgument = serializeAsCallArgument; parseEvaluationResultValue = parseEvaluationResultValue; - evaluate(isFunction: boolean | undefined, returnByValue: boolean, exposeUtilityScript: boolean | undefined, expression: string, argCount: number, ...argsAndHandles: any[]) { + evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) { const args = argsAndHandles.slice(0, argCount); const handles = argsAndHandles.slice(argCount); const parameters = []; for (let i = 0; i < args.length; i++) parameters[i] = this.parseEvaluationResultValue(args[i], handles); - if (exposeUtilityScript) - parameters.unshift(this); // eslint-disable-next-line no-restricted-globals let result = globalThis.eval(expression); @@ -71,4 +74,47 @@ export class UtilityScript { } return safeJson(value); } + + private _setBuiltins() { + // eslint-disable-next-line no-restricted-globals + const window = (globalThis as any); + window.builtinSetTimeout = (callback: Function, timeout: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + return setTimeout(callback, timeout); + }; + + window.builtinClearTimeout = (id: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.clearTimeout(id); + return clearTimeout(id); + }; + + window.builtinSetInterval = (callback: Function, timeout: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.setInterval(callback, timeout); + return setInterval(callback, timeout); + }; + + window.builtinClearInterval = (id: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.clearInterval(id); + return clearInterval(id); + }; + + window.builtinRequestAnimationFrame = (callback: FrameRequestCallback) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + return requestAnimationFrame(callback); + }; + + window.builtinCancelAnimationFrame = (id: number) => { + if (window.__pwFakeTimers?.builtin) + return window.__pwFakeTimers.builtin.cancelAnimationFrame(id); + return cancelAnimationFrame(id); + }; + + window.builtinDate = window.__pwFakeTimers?.builtin.Date || Date; + window.builtinPerformance = window.__pwFakeTimers?.builtin.performance || performance; + } } diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index a1c52cc42b..663df78b5b 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -20,6 +20,7 @@ import { serializeAsCallArgument } from './isomorphic/utilityScriptSerializers'; import type { UtilityScript } from './injected/utilityScript'; import { SdkObject } from './instrumentation'; import { LongStandingScope } from '../utils/manualPromise'; +import { isUnderTest } from '../utils'; export type ObjectId = string; export type RemoteObject = { @@ -118,7 +119,7 @@ export class ExecutionContext extends SdkObject { (() => { const module = {}; ${utilityScriptSource.source} - return new (module.exports.UtilityScript())(); + return new (module.exports.UtilityScript())(${isUnderTest()}); })();`; this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', 'UtilityScript', objectId))); } @@ -257,7 +258,7 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean return evaluateExpression(context, String(pageFunction), { returnByValue, isFunction: typeof pageFunction === 'function' }, ...args); } -export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean, exposeUtilityScript?: boolean }, ...args: any[]): Promise { +export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise { const utilityScript = await context.utilityScript(); expression = normalizeEvaluationExpression(expression, options.isFunction); const handles: (Promise)[] = []; @@ -290,7 +291,7 @@ export async function evaluateExpression(context: ExecutionContext, expression: } // See UtilityScript for arguments. - const utilityScriptValues = [options.isFunction, options.returnByValue, options.exposeUtilityScript, expression, args.length, ...args]; + const utilityScriptValues = [options.isFunction, options.returnByValue, expression, args.length, ...args]; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; try { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 82538f2b50..2ed2bf7c97 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17274,8 +17274,7 @@ export interface Clock { /** * An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: - * ['setTimeout'] })` will fake only `setTimeout()`. By default, `setTimeout`, `clearTimeout`, `setInterval`, - * `clearInterval` and `Date` are faked. + * ['setTimeout'] })` will fake only `setTimeout()`. By default, all the methods are faked. */ toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">; }): Promise; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 8308477701..d5dd5c3e33 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2612,12 +2612,10 @@ export type FrameDispatchEventResult = void; export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, - exposeUtilityScript?: boolean, arg: SerializedArgument, }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, - exposeUtilityScript?: boolean, }; export type FrameEvaluateExpressionResult = { value: SerializedValue, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4151cf7104..8056debdcd 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1922,7 +1922,6 @@ Frame: parameters: expression: string isFunction: boolean? - exposeUtilityScript: boolean? arg: SerializedArgument returns: value: SerializedValue diff --git a/tests/assets/input/handle-locator.html b/tests/assets/input/handle-locator.html index f8f2111c91..a42849e951 100644 --- a/tests/assets/input/handle-locator.html +++ b/tests/assets/input/handle-locator.html @@ -57,7 +57,7 @@ }; if (interstitial.classList.contains('timeout')) - setTimeout(closeInterstitial, 3000); + builtinSetTimeout(closeInterstitial, 3000); else closeInterstitial(); }); diff --git a/tests/config/baseTest.ts b/tests/config/baseTest.ts index a71878c1c1..b585f28e56 100644 --- a/tests/config/baseTest.ts +++ b/tests/config/baseTest.ts @@ -41,3 +41,16 @@ export function step( } return replacementMethod; } + +declare global { + interface Window { + builtinSetTimeout: WindowOrWorkerGlobalScope['setTimeout'], + builtinClearTimeout: WindowOrWorkerGlobalScope['setTimeout'], + builtinSetInterval: WindowOrWorkerGlobalScope['setInterval'], + builtinClearInterval: WindowOrWorkerGlobalScope['clearInterval'], + builtinRequestAnimationFrame: AnimationFrameProvider['requestAnimationFrame'], + builtinCancelAnimationFrame: AnimationFrameProvider['cancelAnimationFrame'], + builtinPerformance: WindowOrWorkerGlobalScope['performance'], + builtinDate: typeof Date, + } +} diff --git a/tests/library/browsercontext-add-cookies.spec.ts b/tests/library/browsercontext-add-cookies.spec.ts index f35ea8b327..311084a837 100644 --- a/tests/library/browsercontext-add-cookies.spec.ts +++ b/tests/library/browsercontext-add-cookies.spec.ts @@ -391,6 +391,7 @@ it('should(not) block third party cookies', async ({ context, page, server, brow it('should not block third party SameSite=None cookies', async ({ contextFactory, httpsServer, browserName }) => { it.skip(browserName === 'webkit', 'No third party cookies in WebKit'); + it.skip(!!process.env.PW_FREEZE_TIME); const context = await contextFactory({ ignoreHTTPSErrors: true, }); diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts index 4ad1ccc1f0..f197436bdc 100644 --- a/tests/library/browsercontext-events.spec.ts +++ b/tests/library/browsercontext-events.spec.ts @@ -46,7 +46,7 @@ test('console event should work in popup 2', async ({ page, browserName }) => { const [, message, popup] = await Promise.all([ page.evaluate(async () => { const win = window.open('javascript:console.log("hello")')!; - await new Promise(f => setTimeout(f, 0)); + await new Promise(f => window.builtinSetTimeout(f, 0)); win.close(); }), page.context().waitForEvent('console', msg => msg.type() === 'log'), diff --git a/tests/library/browsertype-launch.spec.ts b/tests/library/browsertype-launch.spec.ts index bdd5128dd1..011bc8a50d 100644 --- a/tests/library/browsertype-launch.spec.ts +++ b/tests/library/browsertype-launch.spec.ts @@ -24,7 +24,7 @@ it('should reject all promises when browser is closed', async ({ browserType }) const page = await (await browser.newContext()).newPage(); let error: Error | undefined; const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e); - await page.evaluate(() => new Promise(f => setTimeout(f, 0))); + await page.evaluate(() => new Promise(f => window.builtinSetTimeout(f, 0))); await browser.close(); await neverResolves; // WebKit under task-set -c 1 is giving browser, rest are giving target. diff --git a/tests/library/chromium/css-coverage.spec.ts b/tests/library/chromium/css-coverage.spec.ts index 8dcb42d0b5..229f8baaa1 100644 --- a/tests/library/chromium/css-coverage.spec.ts +++ b/tests/library/chromium/css-coverage.spec.ts @@ -135,7 +135,7 @@ it('should work with a recently loaded stylesheet', async function({ page, serve link.href = url; document.head.appendChild(link); await new Promise(x => link.onload = x); - await new Promise(f => requestAnimationFrame(f)); + await new Promise(f => window.builtinRequestAnimationFrame(f)); }, server.PREFIX + '/csscoverage/stylesheet1.css'); const coverage = await page.coverage.stopCSSCoverage(); expect(coverage.length).toBe(1); diff --git a/tests/library/chromium/js-coverage.spec.ts b/tests/library/chromium/js-coverage.spec.ts index 09fcb69f11..f36e6cbcbb 100644 --- a/tests/library/chromium/js-coverage.spec.ts +++ b/tests/library/chromium/js-coverage.spec.ts @@ -43,6 +43,7 @@ it('should ignore eval() scripts by default', async function({ page, server }) { }); it('shouldn\'t ignore eval() scripts if reportAnonymousScripts is true', async function({ page, server }) { + it.skip(!!process.env.PW_FREEZE_TIME); await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); await page.goto(server.PREFIX + '/jscoverage/eval.html'); const coverage = await page.coverage.stopJSCoverage(); diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index 22cb8033b7..f6e4a28488 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -156,6 +156,7 @@ it('should(not) block third party cookies', async ({ page, server, allowsThirdPa it('should not block third party SameSite=None cookies', async ({ httpsServer, browserName, browser }) => { it.skip(browserName === 'webkit', 'No third party cookies in WebKit'); + it.skip(!!process.env.PW_FREEZE_TIME); const page = await browser.newPage({ ignoreHTTPSErrors: true, }); diff --git a/tests/library/popup.spec.ts b/tests/library/popup.spec.ts index 52e13c5175..56865bdb53 100644 --- a/tests/library/popup.spec.ts +++ b/tests/library/popup.spec.ts @@ -136,9 +136,9 @@ it('should use viewport size from window features', async function({ browser, se page.evaluate(async () => { const win = window.open(window.location.href, 'Title', 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=300,top=0,left=0'); await new Promise(resolve => { - const interval = setInterval(() => { + const interval = window.builtinSetInterval(() => { if (win.innerWidth === 600 && win.innerHeight === 300) { - clearInterval(interval); + window.builtinClearInterval(interval); resolve(); } }, 10); @@ -281,8 +281,8 @@ async function waitForRafs(page: Page, count: number): Promise { if (!count) resolve(); else - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }; - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }), count); } \ No newline at end of file diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 54988eb73c..e0c5055692 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -44,9 +44,9 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s console.error('Error'); return new Promise(f => { // Generate exception. - setTimeout(() => { + window.builtinSetTimeout(() => { // And then resolve. - setTimeout(() => f('return ' + a), 0); + window.builtinSetTimeout(() => f('return ' + a), 0); throw new Error('Unhandled exception'); }, 0); }); diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index e0477bd993..c82db377aa 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -426,7 +426,7 @@ for (const params of [ // Make sure we have a chance to paint. for (let i = 0; i < 10; ++i) { await page.setContent(''); - await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.evaluate(() => new Promise(window.builtinRequestAnimationFrame)); } await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }); @@ -709,7 +709,7 @@ test('should not flush console events', async ({ context, page, mode }, testInfo }); await page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { for (let i = 0; i < 100; ++i) console.log('hello ' + i); }, 10); @@ -749,7 +749,7 @@ test('should flush console events on tracing stop', async ({ context, page }, te }); }); await page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { for (let i = 0; i < 100; ++i) console.log('hello ' + i); }); diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 7229fb10ac..dd73efa876 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -829,8 +829,8 @@ async function waitForRafs(page: Page, count: number): Promise { if (!count) resolve(); else - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }; - requestAnimationFrame(onRaf); + window.builtinRequestAnimationFrame(onRaf); }), count); } diff --git a/tests/page/elementhandle-screenshot.spec.ts b/tests/page/elementhandle-screenshot.spec.ts index 3f172c9253..042fb7c565 100644 --- a/tests/page/elementhandle-screenshot.spec.ts +++ b/tests/page/elementhandle-screenshot.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { verifyViewport } from '../config/utils'; import path from 'path'; import fs from 'fs'; @@ -207,8 +207,7 @@ it.describe('element screenshot', () => { done = true; return buffer; }); - for (let i = 0; i < 10; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(f))); + await rafraf(page, 10); expect(done).toBe(false); await elementHandle.evaluate(e => e.style.visibility = 'visible'); const screenshot = await promise; @@ -233,10 +232,8 @@ it.describe('element screenshot', () => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); const elementHandle = await page.$('.box:nth-of-type(3)'); - await elementHandle.evaluate(e => { - e.classList.add('animation'); - return new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f))); - }); + await elementHandle.evaluate(e => e.classList.add('animation')); + await rafraf(page); const screenshot = await elementHandle.screenshot(); expect(screenshot).toMatchSnapshot('screenshot-element-bounding-box.png'); }); diff --git a/tests/page/elementhandle-scroll-into-view.spec.ts b/tests/page/elementhandle-scroll-into-view.spec.ts index 5fe53d0722..e2a401ccea 100644 --- a/tests/page/elementhandle-scroll-into-view.spec.ts +++ b/tests/page/elementhandle-scroll-into-view.spec.ts @@ -48,7 +48,7 @@ async function testWaiting(page, after) { const div = await page.$('div'); let done = false; const promise = div.scrollIntoViewIfNeeded().then(() => done = true); - await page.evaluate(() => new Promise(f => setTimeout(f, 1000))); + await page.waitForTimeout(1000); expect(done).toBe(false); await div.evaluate(after); await promise; diff --git a/tests/page/elementhandle-select-text.spec.ts b/tests/page/elementhandle-select-text.spec.ts index bc3630249b..b916feb3fc 100644 --- a/tests/page/elementhandle-select-text.spec.ts +++ b/tests/page/elementhandle-select-text.spec.ts @@ -65,7 +65,7 @@ it('should wait for visible', async ({ page, server }) => { await textarea.evaluate(e => e.style.display = 'none'); let done = false; const promise = textarea.selectText({ timeout: 3000 }).then(() => done = true); - await page.evaluate(() => new Promise(f => setTimeout(f, 1000))); + await page.waitForTimeout(1000); expect(done).toBe(false); await textarea.evaluate(e => e.style.display = 'block'); await promise; diff --git a/tests/page/elementhandle-wait-for-element-state.spec.ts b/tests/page/elementhandle-wait-for-element-state.spec.ts index e943d229d1..bbccf620c7 100644 --- a/tests/page/elementhandle-wait-for-element-state.spec.ts +++ b/tests/page/elementhandle-wait-for-element-state.spec.ts @@ -15,12 +15,10 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import type { Page } from '@playwright/test'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToResolve(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToResolve = (page: Page) => rafraf(page, 5); it('should wait for visible', async ({ page }) => { await page.setContent(`
content
`); @@ -124,7 +122,7 @@ it('should wait for stable position', async ({ page, server, browserName, platfo button.style.marginLeft = '20000px'; }); // rafraf for Firefox to kick in the animation. - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(page); let done = false; const promise = button.waitForElementState('stable').then(() => done = true); await giveItAChanceToResolve(page); diff --git a/tests/page/page-click-timeout-4.spec.ts b/tests/page/page-click-timeout-4.spec.ts index 0ee7942eef..d00cc62c37 100644 --- a/tests/page/page-click-timeout-4.spec.ts +++ b/tests/page/page-click-timeout-4.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; it('should timeout waiting for stable position', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -25,7 +25,7 @@ it('should timeout waiting for stable position', async ({ page, server }) => { button.style.marginLeft = '200px'; }); // rafraf for Firefox to kick in the animation. - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(page); const error = await button.click({ timeout: 3000 }).catch(e => e); expect(error.message).toContain('elementHandle.click: Timeout 3000ms exceeded.'); expect(error.message).toContain('waiting for element to be visible, enabled and stable'); diff --git a/tests/page/page-click.spec.ts b/tests/page/page-click.spec.ts index a774294c04..aa02d210eb 100644 --- a/tests/page/page-click.spec.ts +++ b/tests/page/page-click.spec.ts @@ -15,13 +15,11 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { attachFrame, detachFrame } from '../config/utils'; +import type { Page } from '@playwright/test'; -async function giveItAChanceToClick(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToClick = (page: Page) => rafraf(page, 5); it('should click the button @smoke', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -456,7 +454,7 @@ it('should wait for stable position', async ({ page, server }) => { document.body.style.margin = '0'; }); // rafraf for Firefox to kick in the animation. - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(page); await page.click('button'); expect(await page.evaluate(() => window['result'])).toBe('Clicked'); expect(await page.evaluate('pageX')).toBe(300); @@ -1072,7 +1070,7 @@ it('ensure events are dispatched in the individual tasks', async ({ page, browse function onClick(name) { console.log(`click ${name}`); - setTimeout(function() { + window.builtinSetTimeout(function() { console.log(`timeout ${name}`); }, 0); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 185443d4c7..7e001e6641 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -16,6 +16,8 @@ import { test, expect } from './pageTest'; +test.skip(!!process.env.PW_FREEZE_TIME); + declare global { interface Window { stub: (param?: any) => void diff --git a/tests/page/page-close.spec.ts b/tests/page/page-close.spec.ts index 5c7d24110a..c1123b8190 100644 --- a/tests/page/page-close.spec.ts +++ b/tests/page/page-close.spec.ts @@ -20,7 +20,8 @@ import { test as it, expect } from './pageTest'; it.skip(({ isWebView2 }) => isWebView2, 'Page.close() is not supported in WebView2'); it('should close page with active dialog', async ({ page }) => { - await page.setContent(``); + await page.evaluate('"trigger builtinSetTimeout"'); + await page.setContent(``); void page.click('button').catch(() => {}); await page.waitForEvent('dialog'); await page.close(); diff --git a/tests/page/page-dialog.spec.ts b/tests/page/page-dialog.spec.ts index fdaed252b8..6149989081 100644 --- a/tests/page/page-dialog.spec.ts +++ b/tests/page/page-dialog.spec.ts @@ -67,7 +67,7 @@ it('should dismiss the confirm prompt', async ({ page }) => { it('should be able to close context with open alert', async ({ page }) => { const alertPromise = page.waitForEvent('dialog'); await page.evaluate(() => { - setTimeout(() => alert('hello'), 0); + window.builtinSetTimeout(() => alert('hello'), 0); }); await alertPromise; }); diff --git a/tests/page/page-dispatchevent.spec.ts b/tests/page/page-dispatchevent.spec.ts index 3486eb3b1e..d4d780c190 100644 --- a/tests/page/page-dispatchevent.spec.ts +++ b/tests/page/page-dispatchevent.spec.ts @@ -92,7 +92,7 @@ it('should dispatch click when node is added in shadow dom', async ({ page, serv div.attachShadow({ mode: 'open' }); document.body.appendChild(div); }); - await page.evaluate(() => new Promise(f => setTimeout(f, 100))); + await page.waitForTimeout(100); await page.evaluate(() => { const span = document.createElement('span'); span.textContent = 'Hello from shadow'; diff --git a/tests/page/page-drag.spec.ts b/tests/page/page-drag.spec.ts index 58f82ba2d3..f7b60baf8c 100644 --- a/tests/page/page-drag.spec.ts +++ b/tests/page/page-drag.spec.ts @@ -357,7 +357,7 @@ it('should report event.buttons', async ({ page, browserName }) => { function onEvent(event) { logs.push({ type: event.type, buttons: event.buttons }); } - await new Promise(requestAnimationFrame); + await new Promise(window.builtinRequestAnimationFrame); return logs; }); await page.mouse.move(20, 20); diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index 5e101f1b7a..8414d23b2c 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -349,10 +349,10 @@ it('should properly serialize null fields', async ({ page }) => { it('should properly serialize PerformanceMeasure object', async ({ page }) => { expect(await page.evaluate(() => { - window.performance.mark('start'); - window.performance.mark('end'); - window.performance.measure('my-measure', 'start', 'end'); - return performance.getEntriesByType('measure'); + window.builtinPerformance.mark('start'); + window.builtinPerformance.mark('end'); + window.builtinPerformance.measure('my-measure', 'start', 'end'); + return window.builtinPerformance.getEntriesByType('measure'); })).toEqual([{ duration: expect.any(Number), entryType: 'measure', @@ -362,6 +362,8 @@ it('should properly serialize PerformanceMeasure object', async ({ page }) => { }); it('should properly serialize window.performance object', async ({ page }) => { + it.skip(!!process.env.PW_FREEZE_TIME); + expect(await page.evaluate(() => performance)).toEqual({ 'navigation': { 'redirectCount': 0, @@ -760,16 +762,6 @@ it('should work with overridden URL/Date/RegExp', async ({ page, server }) => { } }); -it('should expose utilityScript', async ({ page }) => { - const result = await (page.mainFrame() as any)._evaluateExposeUtilityScript((utilityScript, { a }) => { - return { utils: 'parseEvaluationResultValue' in utilityScript, a }; - }, { a: 42 }); - expect(result).toEqual({ - a: 42, - utils: true, - }); -}); - it('should work with Array.from/map', async ({ page }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28520' }); expect(await page.evaluate(() => { diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 3b5f652fc9..ee47e81e02 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -97,9 +97,9 @@ it('should format the message correctly with time/timeLog/timeEnd', async ({ pag page.on('console', msg => messages.push(msg)); await page.evaluate(async () => { console.time('foo time'); - await new Promise(x => setTimeout(x, 100)); + await new Promise(x => window.builtinSetTimeout(x, 100)); console.timeLog('foo time'); - await new Promise(x => setTimeout(x, 100)); + await new Promise(x => window.builtinSetTimeout(x, 100)); console.timeEnd('foo time'); }); expect(messages.length).toBe(2); diff --git a/tests/page/page-event-pageerror.spec.ts b/tests/page/page-event-pageerror.spec.ts index 2193bec598..1e91ce7b7f 100644 --- a/tests/page/page-event-pageerror.spec.ts +++ b/tests/page/page-event-pageerror.spec.ts @@ -70,7 +70,7 @@ it('should contain the Error.name property', async ({ page }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { const error = new Error('my-message'); error.name = 'my-name'; throw error; @@ -85,7 +85,7 @@ it('should support an empty Error.name property', async ({ page }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), page.evaluate(() => { - setTimeout(() => { + window.builtinSetTimeout(() => { const error = new Error('my-message'); error.name = ''; throw error; @@ -106,7 +106,9 @@ it('should handle odd values', async ({ page }) => { for (const [value, message] of cases) { const [error] = await Promise.all([ page.waitForEvent('pageerror'), - page.evaluate(value => setTimeout(() => { throw value; }, 0), value), + page.evaluate(value => { + window.builtinSetTimeout(() => { throw value; }, 0); + }, value), ]); expect(error.message).toBe(message); } @@ -115,7 +117,9 @@ it('should handle odd values', async ({ page }) => { it('should handle object', async ({ page, browserName }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), - page.evaluate(() => setTimeout(() => { throw {}; }, 0)), + page.evaluate(() => { + window.builtinSetTimeout(() => { throw {}; }, 0); + }), ]); expect(error.message).toBe(browserName === 'chromium' ? 'Object' : '[object Object]'); }); @@ -123,7 +127,9 @@ it('should handle object', async ({ page, browserName }) => { it('should handle window', async ({ page, browserName }) => { const [error] = await Promise.all([ page.waitForEvent('pageerror'), - page.evaluate(() => setTimeout(() => { throw window; }, 0)), + page.evaluate(() => { + window.builtinSetTimeout(() => { throw window; }, 0); + }), ]); expect(error.message).toBe(browserName === 'chromium' ? 'Window' : '[object Window]'); }); diff --git a/tests/page/page-expose-function.spec.ts b/tests/page/page-expose-function.spec.ts index c084e5ea21..ece4a602df 100644 --- a/tests/page/page-expose-function.spec.ts +++ b/tests/page/page-expose-function.spec.ts @@ -230,7 +230,7 @@ it('should not result in unhandled rejection', async ({ page, isAndroid, isWebVi await page.close(); }); await page.evaluate(() => { - setTimeout(() => (window as any).foo(), 0); + window.builtinSetTimeout(() => (window as any).foo(), 0); return undefined; }); await closedPromise; diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 0704de327a..88cd33d407 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -15,12 +15,10 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import type { Page } from '@playwright/test'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToFill(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToFill = (page: Page) => rafraf(page, 5); it('should fill textarea @smoke', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/textarea.html'); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 3deff339a3..6703d84498 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -481,6 +481,7 @@ it('js redirect overrides url bar navigation ', async ({ page, server, browserNa it('should succeed on url bar navigation when there is pending navigation', async ({ page, server, browserName }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/21574' }); + it.skip(!!process.env.PW_FREEZE_TIME); server.setRoute('/a', (req, res) => { res.writeHead(200, { 'content-type': 'text/html' }); res.end(` @@ -509,7 +510,7 @@ it('should succeed on url bar navigation when there is pending navigation', asyn events.push('finished c'); }); await page.goto(server.PREFIX + '/a'); - await new Promise(f => setTimeout(f, 1000)); + await page.waitForTimeout(1000); const error = await page.goto(server.PREFIX + '/b').then(r => null, e => e); const expectEvents = ['started c', 'started b', 'finished c', 'finished b']; await expect(() => expect(events).toEqual(expectEvents)).toPass({ timeout: 5000 }); @@ -753,6 +754,7 @@ it('should properly wait for load', async ({ page, server, browserName }) => { it('should not resolve goto upon window.stop()', async ({ browserName, page, server }) => { it.fixme(browserName === 'firefox', 'load/domcontentloaded events are flaky'); + it.skip(!!process.env.PW_FREEZE_TIME); let response; server.setRoute('/module.js', (req, res) => { @@ -795,6 +797,7 @@ it('should return when navigation is committed if commit is specified', async ({ }); it('should wait for load when iframe attaches and detaches', async ({ page, server }) => { + it.skip(!!process.env.PW_FREEZE_TIME); server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'content-type': 'text/html' }); res.end(` diff --git a/tests/page/page-history.spec.ts b/tests/page/page-history.spec.ts index 7e243c9b82..2bc01512dd 100644 --- a/tests/page/page-history.spec.ts +++ b/tests/page/page-history.spec.ts @@ -245,6 +245,7 @@ it('page.goForward during renderer-initiated navigation', async ({ page, server it('regression test for issue 20791', async ({ page, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20791' }); + it.skip(!!process.env.PW_FREEZE_TIME); server.setRoute('/iframe.html', (req, res) => { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); // iframe access parent frame to log a value from it. diff --git a/tests/page/page-mouse.spec.ts b/tests/page/page-mouse.spec.ts index cf93bd8bf1..5ae7de6508 100644 --- a/tests/page/page-mouse.spec.ts +++ b/tests/page/page-mouse.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; function dimensions() { const rect = document.querySelector('textarea').getBoundingClientRect(); @@ -150,7 +150,7 @@ it('should select the text with mouse', async ({ page, server }) => { const text = 'This is the text that we are going to try to select. Let\'s see how it goes.'; await page.keyboard.type(text); // Firefox needs an extra frame here after typing or it will fail to set the scrollTop - await page.evaluate(() => new Promise(requestAnimationFrame)); + await rafraf(page); await page.evaluate(() => document.querySelector('textarea').scrollTop = 0); const { x, y } = await page.evaluate(dimensions); await page.mouse.move(x + 2, y + 2); diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 529bb99a74..7f374ac59b 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -16,7 +16,7 @@ */ import os from 'os'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { verifyViewport, attachFrame } from '../config/utils'; import type { Route } from 'playwright-core'; import path from 'path'; @@ -589,14 +589,6 @@ it.describe('page screenshot', () => { }); }); -async function rafraf(page) { - // Do a double raf since single raf does not - // actually guarantee a new animation frame. - await page.evaluate(() => new Promise(x => { - requestAnimationFrame(() => requestAnimationFrame(x)); - })); -} - declare global { interface Window { animation?: Animation; @@ -732,9 +724,9 @@ it.describe('page screenshot animations', () => { const div = page.locator('div'); await div.evaluate(el => { el.addEventListener('transitionend', () => { - const time = Date.now(); + const time = window.builtinDate.now(); // Block main thread for 200ms, emulating heavy layout. - while (Date.now() - time < 200) {} + while (window.builtinDate.now() - time < 200) {} const h1 = document.createElement('h1'); h1.textContent = 'woof-woof'; document.body.append(h1); diff --git a/tests/page/page-select-option.spec.ts b/tests/page/page-select-option.spec.ts index 522c6270ff..0faeee5f51 100644 --- a/tests/page/page-select-option.spec.ts +++ b/tests/page/page-select-option.spec.ts @@ -15,12 +15,10 @@ * limitations under the License. */ -import { test as it, expect } from './pageTest'; +import type { Page } from '@playwright/test'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToResolve(page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToResolve = (page: Page) => rafraf(page, 5); it('should select single option @smoke', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/select.html'); diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index af6197c1c7..5777b1087a 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -399,7 +399,7 @@ it('should prioritize exact timeout over default timeout', async ({ page, playwr it('should work with no timeout', async ({ page, server }) => { const [chooser] = await Promise.all([ page.waitForEvent('filechooser', { timeout: 0 }), - page.evaluate(() => setTimeout(() => { + page.evaluate(() => window.builtinSetTimeout(() => { const el = document.createElement('input'); el.type = 'file'; el.click(); diff --git a/tests/page/page-wait-for-function.spec.ts b/tests/page/page-wait-for-function.spec.ts index 7f93f7b122..88b6d7cb14 100644 --- a/tests/page/page-wait-for-function.spec.ts +++ b/tests/page/page-wait-for-function.spec.ts @@ -44,10 +44,10 @@ it('should poll on interval', async ({ page, server }) => { const polling = 100; const timeDelta = await page.waitForFunction(() => { if (!window['__startTime']) { - window['__startTime'] = Date.now(); + window['__startTime'] = window.builtinDate.now(); return false; } - return Date.now() - window['__startTime']; + return window.builtinDate.now() - window['__startTime']; }, {}, { polling }); expect(await timeDelta.jsonValue()).not.toBeLessThan(polling); }); diff --git a/tests/page/page-wait-for-navigation.spec.ts b/tests/page/page-wait-for-navigation.spec.ts index 4d2eb8177f..d34251e977 100644 --- a/tests/page/page-wait-for-navigation.spec.ts +++ b/tests/page/page-wait-for-navigation.spec.ts @@ -255,7 +255,7 @@ it('should fail when frame detaches', async ({ page, server }) => { frame.waitForNavigation().catch(e => e), page.$eval('iframe', frame => { frame.contentWindow.location.href = '/one-style.html'; }), // Make sure policy checks pass and navigation actually begins before removing the frame to avoid other errors - server.waitForRequest('/one-style.css').then(() => page.$eval('iframe', frame => setTimeout(() => frame.remove(), 0))) + server.waitForRequest('/one-style.css').then(() => page.$eval('iframe', frame => window.builtinSetTimeout(() => frame.remove(), 0))) ]); expect(error.message).toContain('waiting for navigation until "load"'); expect(error.message).toContain('frame was detached'); diff --git a/tests/page/page-wait-for-request.spec.ts b/tests/page/page-wait-for-request.spec.ts index 3ba6e9304f..e7ae14ba66 100644 --- a/tests/page/page-wait-for-request.spec.ts +++ b/tests/page/page-wait-for-request.spec.ts @@ -70,7 +70,7 @@ it('should work with no timeout', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); const [request] = await Promise.all([ page.waitForRequest(server.PREFIX + '/digits/2.png', { timeout: 0 }), - page.evaluate(() => setTimeout(() => { + page.evaluate(() => window.builtinSetTimeout(() => { void fetch('/digits/1.png'); void fetch('/digits/2.png'); void fetch('/digits/3.png'); diff --git a/tests/page/page-wait-for-response.spec.ts b/tests/page/page-wait-for-response.spec.ts index b5735e1a8c..ccfc18c9eb 100644 --- a/tests/page/page-wait-for-response.spec.ts +++ b/tests/page/page-wait-for-response.spec.ts @@ -108,7 +108,7 @@ it('should work with no timeout', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); const [response] = await Promise.all([ page.waitForResponse(server.PREFIX + '/digits/2.png', { timeout: 0 }), - page.evaluate(() => setTimeout(() => { + page.evaluate(() => window.builtinSetTimeout(() => { void fetch('/digits/1.png'); void fetch('/digits/2.png'); void fetch('/digits/3.png'); diff --git a/tests/page/page-wait-for-selector-1.spec.ts b/tests/page/page-wait-for-selector-1.spec.ts index 3ab3e92b73..6ee01e29e8 100644 --- a/tests/page/page-wait-for-selector-1.spec.ts +++ b/tests/page/page-wait-for-selector-1.spec.ts @@ -16,12 +16,11 @@ */ import type { Frame } from '@playwright/test'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; import { attachFrame, detachFrame } from '../config/utils'; async function giveItTimeToLog(frame: Frame) { - await frame.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); - await frame.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); + await rafraf(frame, 2); } const addElement = (tag: string) => document.body.appendChild(document.createElement(tag)); @@ -189,7 +188,7 @@ it('should resolve promise when node is added in shadow dom', async ({ page, ser div.attachShadow({ mode: 'open' }); document.body.appendChild(div); }); - await page.evaluate(() => new Promise(f => setTimeout(f, 100))); + await page.waitForTimeout(100); await page.evaluate(() => { const span = document.createElement('span'); span.textContent = 'Hello from shadow'; diff --git a/tests/page/pageTest.ts b/tests/page/pageTest.ts index 54f3ec9233..533d901037 100644 --- a/tests/page/pageTest.ts +++ b/tests/page/pageTest.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { TestType } from '@playwright/test'; +import type { Frame, Page, TestType } from '@playwright/test'; import type { PlatformWorkerFixtures } from '../config/platformFixtures'; import type { TestModeTestFixtures, TestModeWorkerFixtures, TestModeWorkerOptions } from '../config/testModeFixtures'; import { androidTest } from '../android/androidTest'; @@ -35,3 +35,11 @@ if (process.env.PWPAGE_IMPL === 'webview2') impl = webView2Test; export const test = impl; + +export async function rafraf(target: Page | Frame, count = 1) { + for (let i = 0; i < count; i++) { + await target.evaluate(async () => { + await new Promise(f => window.builtinRequestAnimationFrame(() => window.builtinRequestAnimationFrame(f))); + }); + } +} diff --git a/tests/page/retarget.spec.ts b/tests/page/retarget.spec.ts index 4465643d77..65daf1e205 100644 --- a/tests/page/retarget.spec.ts +++ b/tests/page/retarget.spec.ts @@ -16,12 +16,9 @@ */ import type { Page } from '@playwright/test'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; -async function giveItAChanceToResolve(page: Page) { - for (let i = 0; i < 5; i++) - await page.evaluate(() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))); -} +const giveItAChanceToResolve = (page: Page) => rafraf(page, 5); it('element state checks should work as expected for label with zero-sized input', async ({ page, server }) => { await page.setContent(` diff --git a/tests/page/wheel.spec.ts b/tests/page/wheel.spec.ts index fb655c7e43..c8c634e16d 100644 --- a/tests/page/wheel.spec.ts +++ b/tests/page/wheel.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import type { Page } from 'playwright-core'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, rafraf } from './pageTest'; it.skip(({ isAndroid }) => { return isAndroid; @@ -209,8 +209,7 @@ it('should work when the event is canceled', async ({ page }) => { document.querySelector('div').addEventListener('wheel', e => e.preventDefault()); }); // Give wheel listener a chance to propagate through all the layers in Firefox. - for (let i = 0; i < 10; i++) - await page.evaluate(() => new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))); + await rafraf(page, 10); await page.mouse.wheel(0, 100); await expectEvent(page, { deltaX: 0, diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index d82e3b8f40..9e44804dcf 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -154,7 +154,7 @@ test('should format console messages in page', async ({ runUITest }, testInfo) = await expect(link).toHaveCSS('text-decoration', 'none solid rgb(0, 0, 255)'); }); -test('should stream console messages live', async ({ runUITest }, testInfo) => { +test('should stream console messages live', async ({ runUITest }) => { const { page } = await runUITest({ 'a.spec.ts': ` import { test, expect } from '@playwright/test'; @@ -162,7 +162,7 @@ test('should stream console messages live', async ({ runUITest }, testInfo) => { await page.setContent(''); const button = page.getByRole('button', { name: 'Click me' }); await button.evaluate(node => node.addEventListener('click', () => { - setTimeout(() => { console.log('I was clicked'); }, 1000); + builtinSetTimeout(() => { console.log('I was clicked'); }, 1000); })); console.log('I was logged'); await button.click();