chore: introduce clock test mode (#31110)

This commit is contained in:
Pavel Feldman 2024-05-31 14:44:26 -07:00 committed by GitHub
parent afa0bf2247
commit 8bfd0eb6e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 291 additions and 140 deletions

62
.github/workflows/tests_clock.yml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -85,6 +85,8 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> 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;
}

View File

@ -189,7 +189,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
async _evaluateExposeUtilityScript<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
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);
}

View File

@ -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({

View File

@ -83,7 +83,7 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
}
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionResult> {
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<channels.FrameEvaluateExpressionHandleResult> {

View File

@ -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<any> {
async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg?: any): Promise<any> {
return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg);
}
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise<js.JSHandle<any>> {
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg?: any): Promise<js.JSHandle<any>> {
return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg);
}

View File

@ -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<any> {
async evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<any> {
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<js.JSHandle<any>> {
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<js.JSHandle<any>> {
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);
}

View File

@ -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;
}

View File

@ -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() {

View File

@ -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'],
}
}
}
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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<any> {
export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise<any> {
const utilityScript = await context.utilityScript();
expression = normalizeEvaluationExpression(expression, options.isFunction);
const handles: (Promise<JSHandle>)[] = [];
@ -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 {

View File

@ -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<void>;

View File

@ -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,

View File

@ -1922,7 +1922,6 @@ Frame:
parameters:
expression: string
isFunction: boolean?
exposeUtilityScript: boolean?
arg: SerializedArgument
returns:
value: SerializedValue

View File

@ -57,7 +57,7 @@
};
if (interstitial.classList.contains('timeout'))
setTimeout(closeInterstitial, 3000);
builtinSetTimeout(closeInterstitial, 3000);
else
closeInterstitial();
});

View File

@ -41,3 +41,16 @@ export function step<This extends Object, Args extends any[], Return>(
}
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,
}
}

View File

@ -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,
});

View File

@ -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'),

View File

@ -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.

View File

@ -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);

View File

@ -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();

View File

@ -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,
});

View File

@ -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<void>(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<void> {
if (!count)
resolve();
else
requestAnimationFrame(onRaf);
window.builtinRequestAnimationFrame(onRaf);
};
requestAnimationFrame(onRaf);
window.builtinRequestAnimationFrame(onRaf);
}), count);
}

View File

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

View File

@ -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('<body style="box-sizing: border-box; width: 100%; height: 100%; margin:0; background: red; border: 50px solid blue"></body>');
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);
});

View File

@ -829,8 +829,8 @@ async function waitForRafs(page: Page, count: number): Promise<void> {
if (!count)
resolve();
else
requestAnimationFrame(onRaf);
window.builtinRequestAnimationFrame(onRaf);
};
requestAnimationFrame(onRaf);
window.builtinRequestAnimationFrame(onRaf);
}), count);
}

View File

@ -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');
});

View File

@ -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;

View File

@ -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;

View File

@ -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(`<div style='display:none'>content</div>`);
@ -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);

View File

@ -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');

View File

@ -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);

View File

@ -16,6 +16,8 @@
import { test, expect } from './pageTest';
test.skip(!!process.env.PW_FREEZE_TIME);
declare global {
interface Window {
stub: (param?: any) => void

View File

@ -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(`<button onclick="setTimeout(() => alert(1))">alert</button>`);
await page.evaluate('"trigger builtinSetTimeout"');
await page.setContent(`<button onclick="builtinSetTimeout(() => alert(1))">alert</button>`);
void page.click('button').catch(() => {});
await page.waitForEvent('dialog');
await page.close();

View File

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

View File

@ -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';

View File

@ -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);

View File

@ -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(() => {

View File

@ -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);

View File

@ -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]');
});

View File

@ -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;

View File

@ -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');

View File

@ -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(`

View File

@ -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.

View File

@ -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);

View File

@ -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);

View File

@ -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');

View File

@ -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();

View File

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

View File

@ -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');

View File

@ -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');

View File

@ -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');

View File

@ -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';

View File

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

View File

@ -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(`

View File

@ -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,

View File

@ -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('<button>Click me</button>');
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();