Closed chambo-e closed 4 months ago
Hopefully, some folks would upvote this more and more.
is there any workaround that we could use currently? Some scripts that stub the Date via page.evaluate
maybe?
Is it possible to increase the time forward by x minutes to test token expiry as well. UseCase: After logging into web app , token gets expires in 2 hours if site stays idle, and user is force logged out. This requires us to forward time to 2 hours from logged in time to validate this scenario.
You can use sinon fake-timers for this.
To do so:
npm install sinon
beforeEach
hook that injects sinon
in all pages:
test.beforeEach(async ({ context }) => {
// Install Sinon in all the pages in the context
await context.addInitScript({
path: path.join(__dirname, '..', './node_modules/sinon/pkg/sinon.js'),
});
// Auto-enable sinon right away
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers();
});
});
await page.evaluate(() => window.__clock.tick(1000))
to tick time inside tests.A full example would look like this:
// e2e/fakeTime.spec.ts
import { test, expect } from '@playwright/test';
import path from 'path';
// Install Sinon in all the pages in the context
test.beforeEach(async ({ context }) => {
await context.addInitScript({
path: path.join(__dirname, '..', './node_modules/sinon/pkg/sinon.js'),
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers();
});
});
test('fake time test', async ({ page }) => {
// Implement a small time on the page
await page.setContent(`
<h1>UTC Time: <x-time></x-time></h1>
<script>
const time = document.querySelector('x-time');
(function renderLoop() {
const date = new Date();
time.textContent = [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()]
.map(number => String(number).padStart(2, '0'))
.join(':');
setTimeout(renderLoop, 1000);
})();
</script>
`);
// Ensure controlled time
await expect(page.locator('x-time')).toHaveText('00:00:00');
await page.evaluate(() => window.__clock.tick(1000));
await expect(page.locator('x-time')).toHaveText('00:00:01');
});
This feature is critical for visual regression testing when an application's clock is controlled by the os.
Note, for everyone who is discovering the example that @aslushnikov provided, you may need to explicitly set the window clock like so:
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 1483228800000,
shouldAdvanceTime: true
});
});
Here's a very simple and robust solution to set the Time/Date in your tests:
// Pick the new/fake "now" for you test pages.
const fakeNow = new Date("March 14 2042 13:37:11").valueOf();
// Update the Date accordingly in your test pages
await page.addInitScript(`{
// Extend Date constructor to default to fakeNow
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${fakeNow});
} else {
super(...args);
}
}
}
// Override Date.now() to start from fakeNow
const __DateNowOffset = ${fakeNow} - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`);
That's all! No need for a library or to dig into your node_modules folder to inject into the pages.
Hope that helps,
Very clever!
You can use sinon fake-timers for this.
To do so:
- Install sinon:
npm install sinon
- Setup a
beforeEach
hook that injectssinon
in all pages:test.beforeEach(async ({ context }) => { // Install Sinon in all the pages in the context await context.addInitScript({ path: path.join(__dirname, '..', './node_modules/sinon/pkg/sinon.js'), }); // Auto-enable sinon right away await context.addInitScript(() => { window.__clock = sinon.useFakeTimers(); }); });
- Use
await page.evaluate(() => window.__clock.tick(1000))
to tick time inside tests.A full example would look like this:
// e2e/fakeTime.spec.ts import { test, expect } from '@playwright/test'; import path from 'path'; // Install Sinon in all the pages in the context test.beforeEach(async ({ context }) => { await context.addInitScript({ path: path.join(__dirname, '..', './node_modules/sinon/pkg/sinon.js'), }); await context.addInitScript(() => { window.__clock = sinon.useFakeTimers(); }); }); test('fake time test', async ({ page }) => { // Implement a small time on the page await page.setContent(` <h1>UTC Time: <x-time></x-time></h1> <script> const time = document.querySelector('x-time'); (function renderLoop() { const date = new Date(); time.textContent = [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()] .map(number => String(number).padStart(2, '0')) .join(':'); setTimeout(renderLoop, 1000); })(); </script> `); // Ensure controlled time await expect(page.locator('x-time')).toHaveText('00:00:00'); await page.evaluate(() => window.__clock.tick(1000)); await expect(page.locator('x-time')).toHaveText('00:00:01'); });
Note that @sinonjs/fake-timers
conflicts with playwright waitForFunction
.
You might want to explicitly set which functions to fake.
https://github.com/sinonjs/fake-timers#var-clock--faketimersinstallconfig
await page.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
toFake: [
'setTimeout',
'clearTimeout',
// 'setImmediate',
// 'clearImmediate',
'setInterval',
'clearInterval',
// 'Date',
// 'requestAnimationFrame',
// 'cancelAnimationFrame',
// 'requestIdleCallback',
// 'cancelIdleCallback',
// 'hrtime',
// 'performance',
],
});
});
that makes it a bit cumbersome if your application mostly uses performance.now()
, in case you can't mock that using sinon because of playwright
What are your scenarios related to Time/Date ?
The scenarios I was dealing with were: showing absolute and relative dates/times. I didn't need to "stop" or slow down time. The code snippet I posted above solved ALL our scenarios.
basically measuring performance. call performance.now(), do something, call performance.now() again, then produce data depending on how long that took. i'm aware that this is also possible using Date.now(), but a) not in that much detail and b) it doesn't depend on the system clock
@DerGernTod from what you describe, I think the code snippet I posted above would work. It's very light weight and simply allows to se the current Date, time to what you need. It only offset new Date()
, and Date.now()
. All other Date methods and performance.now()
are left untouched and work exactly as expected.
@p01 I think you misunderstood. I want to test that my measurements are correct if x time passed and different actions happened in between. For that I need performance.now to be properly emulated
@p01 I think you misunderstood. I want to test that my measurements are correct if x time passed and different actions happened in between. For that I need performance.now to be properly emulated
I'm curious. Can you provide an example? We're implementing both performance.now() and Sinon in our tests for performance testing.
Do you have a use case and code example?
performance.now()
returns a timestamp starting from the life cycle of the current page, so unless you need to "stop" time, there is no need to "emulate" or modify it and the snippet I posted should work.
Could you please share an example of test so we can figure together how to make your scenarios work, and bring clear new scenarios/use cases to the Playwright team so they know exactly what the community needs help with.
@p01 all of our visual tests need a "fixed time" since the Date.now() method is used on all pages.
ok now this is going to be a bit complex 😅 i don't have a simple code example but i guess i can explain better what i want to test:
let's say i have a web app that opens a hint text after a short delay after you hover over a specific element. that hint text fires a request before showing the result. i have code that measures exactly how long it took between the hover event and the result being printed in the hint text. i have performance.now() (or performance.measure, doesn't really matter) to measure this time, and i have the setTimeout and/or Date.now that delays showing the hint text.
now, in my test, i want to make sure that the time my measurement mechanism captured matches the time this whole operation took. if i emulate only the Date object but not the performance object, these values differ a lot. if i don't emulate the date object, the test takes a long time since the app not only waits for the response (which i would also mock to increase test execution performance), but also for the "show hint text"-delay.
this is an example i came up with just now, nothing from our real world tests (since those would be even more complex...). in reality i have no control over what exactly my code measures, which means it needs a lot of different defer/async test scenarios to be reliable. the scenario above is just a simple one. imagine an end-to-end shop cart checkout scenario where i want to test my measuring code... there's a lot of deferred and async code involved (depending on how the shop is implemented, of course)
Hi, if iam using the code above, i get a problem with ts bcs it is telling me, that window.__clock is not a propertie of window. Any solution for this problem?
Hi, if iam using the code above, i get a problem with ts bcs it is telling me, that window.__clock is not a propertie of window. Any solution for this problem?
@aw492267 if you want to extend existing interfaces/types in TypeScript, you have to do something called "Module Augmentation", see typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation.
I have put this block of code into my code base:
import * as sinon from 'sinon';
declare global {
interface Window {
__clock: sinon.SinonFakeTimers;
}
}
Would anyone have any idea why https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 approach wouldn't work with component testing?
Replacing @playwright/test
with @playwright/experimental-ct-react
, addInitScript
is executed but sinon
is not present. Is there some limitation with addInitScript
for components testing? I assume component testing is interfering with the browser context and the added script but I don't really know and I cannot find any clues.
I think this relates to how the context is defined in the ComponentFixtures (mount).
So for component testing I found a simpler solution by adding sinon.js to the playwright/index.ts:
// playwright/index.ts
import sinon from 'sinon'
window.sinon = sinon
Then:
import { test, expect } from '@playwright/experimental-ct-react'
test('fake timer with sinon', async ({ page }) => {
await page.evaluate(() => (window.__clock = window.sinon.useFakeTimers()))
// Implement a small time on the page
await page.setContent(`
<h1>UTC Time: <x-time></x-time></h0>
<script>
const time = document.querySelector('x-time');
(function renderLoop() {
const date = new Date();
time.textContent = [date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()]
.map(number => String(number).padStart(2, '0'))
.join(':');
setTimeout(renderLoop, 1000);
})();
</script>
`)
await expect(page.locator('x-time')).toHaveText('00:00:00')
await page.evaluate(() => window.__clock.tick(2000))
await expect(page.locator('x-time')).toHaveText('00:00:02')
})
Or with a mounted component:
import { test, expect } from '@playwright/experimental-ct-react'
import TestTimer from './TestTimer'
test('fake timer with sinon', async ({ page, mount }) => {
await page.evaluate(() => (window.__clock = window.sinon.useFakeTimers()))
const component = await mount(<TestTimer />)
await expect(component).toHaveText('00:00:00')
await page.evaluate(() => window.__clock.tick(2000))
await expect(component).toHaveText('00:00:02')
})
Here's a very simple and robust solution to set the Time/Date in your tests:
// Pick the new/fake "now" for you test pages. const fakeNow = new Date("March 14 2042 13:37:11").valueOf(); // Update the Date accordingly in your test pages await page.addInitScript(`{ // Extend Date constructor to default to fakeNow Date = class extends Date { constructor(...args) { if (args.length === 0) { super(${fakeNow}); } else { super(...args); } } } // Override Date.now() to start from fakeNow const __DateNowOffset = ${fakeNow} - Date.now(); const __DateNow = Date.now; Date.now = () => __DateNow() + __DateNowOffset; }`);
That's all! No need for a library or to dig into your node_modules folder to inject into the pages.
Hope that helps,
Hi, I combine this fakeNow with emulate timeZone and set it to US/Pacific for example.
const context = await browser.newContext({
timezoneId: 'US/Pacific'
});
const fakeNow = new Date("March 1 2022 13:37:11").valueOf();
The new Date() on browser become
Mon Feb 28 2022 22:37:11 GMT-0800 (Pacific Standard Time)
Is that correct?
Here's a very simple and robust solution to set the Time/Date in your tests:
// Pick the new/fake "now" for you test pages. const fakeNow = new Date("March 14 2042 13:37:11").valueOf(); // Update the Date accordingly in your test pages await page.addInitScript(`{ // Extend Date constructor to default to fakeNow Date = class extends Date { constructor(...args) { if (args.length === 0) { super(${fakeNow}); } else { super(...args); } } } // Override Date.now() to start from fakeNow const __DateNowOffset = ${fakeNow} - Date.now(); const __DateNow = Date.now; Date.now = () => __DateNow() + __DateNowOffset; }`);
That's all! No need for a library or to dig into your node_modules folder to inject into the pages. Hope that helps,
Hi, I combine this fakeNow with emulate timeZone and set it to US/Pacific for example.
const context = await browser.newContext({ timezoneId: 'US/Pacific' }); const fakeNow = new Date("March 1 2022 13:37:11").valueOf();
The new Date() on browser become
Mon Feb 28 2022 22:37:11 GMT-0800 (Pacific Standard Time)
Is that correct?
:) Ha! Nice I didn't think about time zone offset. But that should be easy to fix, by adding Z-07:00
or similar when getting the fakeNow
.
e.g.:
const fakeNow = new Date("March 1 2022 13:37:11Z-07:00").valueOf();
Another way could be to get the timeZomeOffset rather than "hardcoding" it, e.g.:
// Get fakeNow from UTC to extract the timeZone offset used in the test
const fakeNowDateTime = "March 1 2022 13:37:11";
const fakeNowFromUTC = new Date(fakeNowDateTime);
const timeZomeOffset = fakeNowFromUTC.getTimeZoneOffset();
const timeZoneOffsetHours = `${Math.abs(Math.floor(timeZomeOffset / 60))}`;
const timeZoneOffsetMinutes = `${Math.abs(timeZomeOffset % 30)}`;
const timeZoneOffsetText = `${timeZomeOffset < 0 ? "-" : "+"}${timeZoneOffsetHours.paddStart(2,"0")}:${timeZoneOffsetMinutes.padStart(2,"0")}`;
// Get fakeNow from the test timeZone
const fakeNow = new Date(`${fakeNowDateTime}Z${timeZoneOffsetText}`).valueOf();
⚠️ I didn't get the chance to try the code above, but the general idea should work.
Hope that helps,
Here's a very simple and robust solution to set the Time/Date in your tests:
// Pick the new/fake "now" for you test pages. const fakeNow = new Date("March 14 2042 13:37:11").valueOf(); // Update the Date accordingly in your test pages await page.addInitScript(`{ // Extend Date constructor to default to fakeNow Date = class extends Date { constructor(...args) { if (args.length === 0) { super(${fakeNow}); } else { super(...args); } } } // Override Date.now() to start from fakeNow const __DateNowOffset = ${fakeNow} - Date.now(); const __DateNow = Date.now; Date.now = () => __DateNow() + __DateNowOffset; }`);
That's all! No need for a library or to dig into your node_modules folder to inject into the pages. Hope that helps,
Hi, I combine this fakeNow with emulate timeZone and set it to US/Pacific for example.
const context = await browser.newContext({ timezoneId: 'US/Pacific' }); const fakeNow = new Date("March 1 2022 13:37:11").valueOf();
The new Date() on browser become
Mon Feb 28 2022 22:37:11 GMT-0800 (Pacific Standard Time)
Is that correct?
:) Ha! Nice I didn't think about time zone offset. But that should be easy to fix, by adding
Z-07:00
or similar when getting thefakeNow
.e.g.:
const fakeNow = new Date("March 1 2022 13:37:11Z-07:00").valueOf();
Another way could be to get the timeZomeOffset rather than "hardcoding" it, e.g.:
// Get fakeNow from UTC to extract the timeZone offset used in the test const fakeNowDateTime = "March 1 2022 13:37:11"; const fakeNowFromUTC = new Date(fakeNowDateTime); const timeZomeOffset = fakeNowFromUTC.getTimeZoneOffset(); const timeZoneOffsetHours = `${Math.abs(Math.floor(timeZomeOffset / 60))}`; const timeZoneOffsetMinutes = `${Math.abs(timeZomeOffset % 30)}`; const timeZoneOffsetText = `${timeZomeOffset < 0 ? "-" : "+"}${timeZoneOffsetHours.paddStart(2,"0")}:${timeZoneOffsetMinutes.padStart(2,"0")}`; // Get fakeNow from the test timeZone const fakeNow = new Date(`${fakeNowDateTime}Z${timeZoneOffsetText}`).valueOf();
⚠️ I didn't get the chance to try the code above, but the general idea should work.
Hope that helps,
Thank you @p01 I will try that later :)
If anyone is still looking for a solution to mock date we found a simple way to do it with before mount method in index.ts.
import { beforeMount } from "@playwright/experimental-ct-react/hooks"
beforeMount(async ({ hooksConfig }) => {
if (hooksConfig && hooksConfig.mockDateNow) {
Date.now = () => hooksConfig.mockDateNow as number
}
})
Then we can just pass any date to the hookConfig in the mount of the test
test.describe('component test', () => {
test('renders a component', async ({ mount }) => {
const mokedTime = new Date(2022, 1, 1).getTime()
const component = await mount(<TestComponent />, {
hooksConfig: { mockDateNow: mokedTime },
})
await verifyScreenshot(component, 'default')
})
I wrote something like this:
import { test as base } from '@playwright/experimental-ct-vue';
import '@playwright/test';
import sinon from 'sinon';
declare global {
interface Window {
__clock: sinon.SinonFakeTimers;
}
}
interface SinonFakeTimersWrapper {
tick(time: string | number): Promise<number>;
tickAsync(time: string | number): Promise<number>;
next(): Promise<number>;
nextAsync(): Promise<number>;
runAll(): Promise<number>;
runAllAsync(): Promise<number>;
}
export const test = base.extend<{ clock: SinonFakeTimersWrapper }>({
clock: async ({ page }, use) => {
await page.evaluate(() => {
window.__clock = window.sinon.useFakeTimers();
});
await use({
tick: async (time) => {
return page.evaluate((time) => {
return window.__clock.tick(time);
}, time);
},
tickAsync: async (time: string | number): Promise<number> => {
return page.evaluate((time) => {
return window.__clock.tickAsync(time);
}, time);
},
next: async (): Promise<number> => {
return page.evaluate(() => {
return window.__clock.next();
});
},
nextAsync: async (): Promise<number> => {
return page.evaluate(() => {
return window.__clock.nextAsync();
});
},
runAll: async (): Promise<number> => {
return page.evaluate(() => {
return window.__clock.runAll();
});
},
runAllAsync: async (): Promise<number> => {
return page.evaluate(() => {
return window.__clock.runAllAsync();
});
},
});
},
});
test.afterEach(() => {
sinon.restore();
});
export { expect } from '@playwright/experimental-ct-vue';
And adding to playwright/index.ts
for component testing:
import sinon from 'sinon'
window.sinon = sinon
You then have a clock fixture for fake timers. (The sinon.restore
is because I'm also using Sinon's spy/stub in places). Still missing better expect
assertions for sinon, need some kind of a plugin to extend expect
with.
It would be nice to have this feature. This helps with tests that have toast messages. We could speed up the clock to get the toasts to disappear faster
Could we have a built-in mock timers like jest/vite have?
This is a fundamental feature, can't see why not still fixed since 2021. It is fundamental to be able to make tests independent from time.
Up
I tried to create a setup with the workaround posted above, but it seems to be completely ignored :/
import { test as setup } from "@playwright/test";
// Fake date for the tests
const fakeNow = new Date("March 14 2042 13:37:11").valueOf();
setup("fake date", async ({ page }) => {
await page.addInitScript(`{
// Extend Date constructor to default to fakeNow
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${fakeNow});
} else {
super(...args);
}
}
}
// Override Date.now() to start from fakeNow
const __DateNowOffset = ${fakeNow} - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`);
})
@p01 -- any reason this wouldn't work? :/
edit: oh. there is no continuity between tests.. if I put it in a util function and use it in the test itself it works..
Here's a very simple and robust solution to set the Time/Date in your tests:
// Pick the new/fake "now" for you test pages. const fakeNow = new Date("March 14 2042 13:37:11").valueOf(); // Update the Date accordingly in your test pages await page.addInitScript(`{ // Extend Date constructor to default to fakeNow Date = class extends Date { constructor(...args) { if (args.length === 0) { super(${fakeNow}); } else { super(...args); } } } // Override Date.now() to start from fakeNow const __DateNowOffset = ${fakeNow} - Date.now(); const __DateNow = Date.now; Date.now = () => __DateNow() + __DateNowOffset; }`);
That's all! No need for a library or to dig into your node_modules folder to inject into the pages.
Hope that helps,
@p01 Could you please provide the right way to use this with a example test?
I am using cucumber to write the tests and I tried like this
Given('...', async function(){
const updated_date = new Date(year, month, day).valueOf();
// Pick the new/fake "now" for you test pages.
const fakeNow = new Date("March 14 2042 13:37:11").valueOf();
// Update the Date accordingly in your test pages
await page.addInitScript(`{
// Extend Date constructor to default to fakeNow
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${fakeNow});
} else {
super(...args);
}
}
}
// Override Date.now() to start from fakeNow
const __DateNowOffset = ${fakeNow} - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`);
Then ('....', async function(){
Date.now() // is still today not the updated date.
}
);
Another use case: Calculate age based on today's date
My application has dob and calculate age logic to make application suitable for certain age groups. So if I write some tests with hardcoded dob, those tests would become invalid after a year 🤷♂️🤷♂️
I tried to create a setup with the workaround posted above, but it seems to be completely ignored :/
import { test as setup } from "@playwright/test"; // Fake date for the tests const fakeNow = new Date("March 14 2042 13:37:11").valueOf(); setup("fake date", async ({ page }) => { await page.addInitScript(`{ // Extend Date constructor to default to fakeNow Date = class extends Date { constructor(...args) { if (args.length === 0) { super(${fakeNow}); } else { super(...args); } } } // Override Date.now() to start from fakeNow const __DateNowOffset = ${fakeNow} - Date.now(); const __DateNow = Date.now; Date.now = () => __DateNow() + __DateNowOffset; }`); })
@p01 -- any reason this wouldn't work? :/
edit: oh. there is no continuity between tests.. if I put it in a util function and use it in the test itself it works..
@pongells I tried adding this to globalSetup and also tried it as a separate utility method and added in beforeEach() hook, but couldn't get it to work. Can you help me with a detailed implementation of how to add it?
@deepakgupta25 I have a demo based on using that code that is set up as an automatic fixture.
It's part of the fixtures
demo of the edumserrano/playwright-adventures repo. You can find a README for it here. The part you want to focus is the Time/Date emulation section.
The code for the demo is at /demos/fixtures. The files that matter are:
setDate
fixture.window.__fakeNow
which is set by the /demos/fixtures/tests/_shared/fixtures/set-date.ts
file.Lastly, for the automatic fixture setDate to work, you need to import
the test
and expect
functions from the /demos/fixtures/tests/_shared/app-fixtures.ts
instead of from @playwright/test
. For instance, at /demos/fixtures/tests/example.spec.ts you can see:
import { expect, test } from "tests/_shared/app-fixtures";
instead of the usual:
import { expect, test } from @playwright/test
The test that shows that the time/date emulation is working is the setDate test. The app being tested is displaying the current date, which you can see being set at /demos/fixtures/src/app/app.component.ts but then the setDate
fixture is setting it to January 20 2024 09:00:00 and that's why this assert always works:
await expect(messageLocator).toHaveText( "Congratulations! Your app is running and it's Sat Jan 20 2024.");
This issue has almost 200 upvotes.
Can we get an update on whether this will be planned for the near future or not?
FYI, you can also use the context.addInitScript()
solution with Playwright test generation by using a custom setup:
import { chromium } from '@playwright/test'
async function playwrightCodegen({ url }: { url: string }) {
const browser = await chromium.launch({ headless: false })
const context = await browser.newContext()
// Mock the current date to 2024-01-01
const mockedDate = new Date('2024-01-01')
await context.addInitScript(`{
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${mockedDate.getTime()})
} else {
super(...args)
}
}
}
const __DateNowOffset = ${mockedDate.getTime()} - Date.now()
const __DateNow = Date.now
Date.now = () => __DateNow() + __DateNowOffset
}`)
const page = await context.newPage()
await page.goto(url)
await page.pause() // Start recording
}
I tried the Sinon JS method, couldn't get it to work for my end to end tests
Here's what I've made so far - works perfectly for me so far 👍
Fake Time Helper Class
// Playwright as of now has no built in functionality to be able to alter time, mock time etc
// Hence using Sinon JS
import { BrowserContext, Page } from '@playwright/test';
export class ClockHelper {
readonly page: Page;
readonly context: BrowserContext;
constructor(page: Page, context: BrowserContext) {
this.page = page;
this.context = context;
}
// Set up fake timers in the browser context (needs to be set up before trying to change the time)
async setupFakeTimers(startDate: Date = new Date()) {
// Add Sinon.js to the browser context by injecting the script file
await this.context.addInitScript({
path: require.resolve('sinon/pkg/sinon.js'),
});
// Inject script content into the browser context to set up fake timers
await this.context.addInitScript({
content: `
window.__clock = sinon.useFakeTimers({
now: ${startDate.getTime()}, // Start the fake clock at the specified start date
shouldAdvanceTime: true, // Automatically advance time when setTimeout/setInterval is called
shouldClearNativeTimers: true, // Clear native timers when fake timers are advanced
toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'], // Fake these timer functions
});`,
});
}
// Simulate the passage of time
async advanceTime(milliseconds: number): Promise<void> {
await this.page.evaluate(milliseconds => {
window.__clock.tick(milliseconds);
}, milliseconds);
}
}
Then in my test file
import { ClockHelper } from '@/tests/utils/fake-time';
test.describe('Testing Clock Helper', () => {
let clockHelper: ClockHelper;
test.beforeEach(async ({ page, context }) => {
// Arrange
clockHelper = new ClockHelper(page, context);
await clockHelper.setupFakeTimers();
});
test('simulate a day passing', async ({ page }) => {
// Arrange
const currentDateBeforeSimulation = await page.evaluate(() => {
return new Date().toString();
});
console.log('Current date before simulation:', currentDateBeforeSimulation);
// Advance time by 25 hours so date becomes invalid
await clockHelper.advanceTime(25 * 60 * 60 * 1000);
// Assert
const currentDateAfterSimulation = await page.evaluate(() => {
return new Date().toString();
});
console.log('Current date before simulation:', currentDateAfterSimulation);
});
});
Also make sure to add the clock using sinon fake timers to your global set up
import * as sinon from 'sinon';
export {};
// We need to add a global declaration for the custom matchers we created in custom-playwright.ts.
// This prevents TS compile errors when using the custom matchers in the test files.
declare global {
interface Window {
__clock: sinon.SinonFakeTimers;
}
namespace PlaywrightTest {
interface Matchers<R> {
toHavePercentageInRange(min: number, max: number): R;
toBeInRange(min: number, max: number): R;
}
}
}
This would really be a useful feature to have
I've modified @p01's idea a bit to add the ability to control time during tests! Writeup: https://mitchinson.dev/playwright-mock-time
test.beforeEach(async ({ page }) => {
// Set the date that you'd like your tests to start at
/////////////////////////////////////////////////
const fakeNow = new Date("2023-05-12T01:30").valueOf();
await page.addInitScript(`{
window.__minutesPassed = 0;
// create functions to modify "minutesPassed"
/////////////////////////////////////////////////
window.advanceTimeOneMinute = () => {
console.log("TIME ADVANCING TO " + ++window.__minutesPassed + " MINUTE(S) PASSED.");
}
// mock date.now
/////////////////////////////////////////////////
Date.now = () => {
return ${fakeNow} + window.__minutesPassed * 60000;
}
// mock constructor
/////////////////////////////////////////////////
Date = class extends Date {
constructor(...args) {
(args.length === 0) ? super(${fakeNow} + window.__minutesPassed * 60000) : super(...args)
}
}
}`);
//////////////////////////////
////// in a test util file elsewhere
// export const advanceTimeOneMinute = async (page: Page) =>
// await page.evaluate(() => {
// (window as any).advanceTimeOneMinute();
// });
//////////////////////////////
test("Defaults to the current datetime", async ({ page }) => {
await advanceTimeOneMinute(page);
await advanceTimeOneMinute(page);
await page.getByText("Submit").click();
const expectedTime = add(mockedClockDate, { minutes: 2 });
await expect(page.getByTestId("datetime-input")).toHaveValue(
new RegExp(dateToDatetimeFieldValue(expectedTime))
);
});
It's 2024 and we still have to rewrite Data constructor to mock the date. Disappointing
Also looking for this feature as we migrate our tests from cypress to playwright.
page.clock
is scheduled to release in 1.45:
You can give it a try via installing @playwright/test@next
. Please tell us if this does or does not work for you!
@pavelfeldman
Will it work the same way as addInitialScript? So that when the page is reloaded, the mocks are automatically applied again.
Yes and no, we don't want you to think about it.
Will the time reset to the initial time, after the page is reloaded, or will it be the same as before?
No, it will not, your manual clock controller is outside of the page.
I'm so excited about this feature! ❤️
Just wanted to let you know:
I've installed @playwright/test@^1.45.0-alpha-2024-06-03
and when I run the test case using the playwright VS Code extension (the green play button) the example from the docs which is:
await page.clock.install({ now: new Date('2020-02-02') })
unfortunately yields this:
Running 1 test using 1 worker
1) [chromium] › file.spec.ts:135:5 › test case foo bar ────────────────
Error: clock.install: now: expected number, got object
134 |
135 | test('test case foo bar', async ({ page }) => {
> 136 | await page.clock.install({ now: new Date('2020-02-02') })
| ^
But when run from the CLI, everything is fine so I'll take this as a win! 🎉 Thank you!
~@julisch94: thanks for the heads up, I have an idea on what might be wrong with the extension mode!~ Works for me locally :shrug:
page.clock
is scheduled to release in 1.45:You can give it a try via installing
@playwright/test@next
. Please tell us if this does or does not work for you!
i had a quick look at the doc and couldn't find it: this doesn't include all features related to time, does it? there's no mention of performance.now() or performance.timeOrigin (and the rest of the performance api). are there plans to make page.clock
also influence these? it feels somewhat incomplete without them.
@DerGernTod could you share your use case for performance API mocking?
See https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 for a current workaround.
Edited by the Playwright team.
Hello,
We are using playwright to run automated tests on some websites, we record external requests to be able to replace the tests in isolation. We would love have a way to set the internal clock, same as
clock()
from cypress to improve reproductibilityThis has already been mentioned here https://github.com/microsoft/playwright/issues/820 but I did not found any follow up issues :)