-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Time/Date emulation via e.g. a clock()
primitive
#6347
Comments
clock()
primitiveclock()
primitive
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 |
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:
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! Hope that helps, |
Very clever! |
Note that playwright/packages/playwright-core/src/server/injected/injectedScript.ts Lines 321 to 327 in 446de1a
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 |
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. |
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 |
@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? |
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? |
@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 #6347 (comment) approach wouldn't work with component testing? Replacing |
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')
}) |
Hi, I combine this fakeNow with emulate timeZone and set it to US/Pacific for example.
The new Date() on browser become
Is that correct? |
:) Ha! Nice I didn't think about time zone offset. But that should be easy to fix, by adding 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(); Hope that helps, |
@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 The code for the demo is at /demos/fixtures. The files that matter are:
Lastly, for the automatic fixture setDate to work, you need to 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 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 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
Then in my test file
Also make sure to add the clock using sinon fake timers to your global set up
|
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! Setuptest.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)
}
}
}`); Using your setup to advance time//////////////////////////////
////// 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. |
You can give it a try via installing |
|
Yes and no, we don't want you to think about it.
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: await page.clock.install({ now: new Date('2020-02-02') }) unfortunately yields this:
But when run from the CLI, everything is fine so I'll take this as a win! 🎉 Thank 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 |
@DerGernTod could you share your use case for performance API mocking? |
especially relevant for performance monitoring tools (obviously). the simplest use case is
i want to make sure that if i run the code that generates measures, that a metric has been reported with the correct time. note that for performance monitoring it's important to use if |
So you are testing your perf monitoring subsystem, I see. I'd say this is narrow enough to deserve an ad-hock performance mock. Having said that, present version of the clock() in Playwright mocks |
This looks great! Is it supposed to work with cookies? I think this is a great use case and tried it out, but my cookie was not deleted by the browser. Here's what I did:
I verified |
Not really - cookies are managed by the network stack, think operating system in case of macOS, which is outside of the browser context. We only simulate the browser context time, so network is largely unaware of the changes to the time. We could shift expires in the cookies within the context though. Is your test pretty much checking that the cookie will be gone in 91 days? Do you have more existing use cases around cookies? |
@pavelfeldman Thanks for the info! I understand 😀
I don't really care about the cookie itself. I want to verify the functionality which relies on the cookie - that a popup is shown again after 90 days when the user dismisses it. Yes, I can do this by modifying the cookie expiration, but I feel the test would be better if I did not have to edit the cookie. We don't have any other cookie use cases except "for show this thing every" x days as explained above:
|
Closing as fixed |
I know it is released. But as a quick feedback I was surprised that I would have expected needing to call Luckily I found Mathieu's <3 earlier workaround which works great for a minimal setSystemTime. |
See #6347 (comment) 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 #820 but I did not found any follow up issues :)
The text was updated successfully, but these errors were encountered: