microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
66.99k stars 3.68k forks source link

[Feature] Time/Date emulation via e.g. a `clock()` primitive #6347

Closed chambo-e closed 4 months ago

chambo-e commented 3 years ago

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 reproductibility

This has already been mentioned here https://github.com/microsoft/playwright/issues/820 but I did not found any follow up issues :)

idxn commented 3 years ago

Hopefully, some folks would upvote this more and more.

Shaddix commented 3 years ago

is there any workaround that we could use currently? Some scripts that stub the Date via page.evaluate maybe?

jithinjosejacob commented 3 years ago

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.

aslushnikov commented 3 years ago

You can use sinon fake-timers for this.

To do so:

  1. Install sinon: npm install sinon
  2. Setup a 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();
      });
    });
  3. 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');
});
unlikelyzero commented 3 years ago

This feature is critical for visual regression testing when an application's clock is controlled by the os.

unlikelyzero commented 2 years ago

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
        });
    });
p01 commented 2 years ago

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,

unlikelyzero commented 2 years ago

Very clever!

mizozobu commented 2 years ago

You can use sinon fake-timers for this.

To do so:

  1. Install sinon: npm install sinon
  2. Setup a 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();
     });
    });
  3. 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.

https://github.com/microsoft/playwright/blob/446de1a615cc440cabd7ef81a45d0ddee63a683d/packages/playwright-core/src/server/injected/injectedScript.ts#L321-L327

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',
    ],
  });
});
DerGernTod commented 2 years ago

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

p01 commented 2 years ago

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.

DerGernTod commented 2 years ago

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

p01 commented 2 years ago

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

DerGernTod commented 2 years ago

@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

unlikelyzero commented 2 years ago

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

p01 commented 2 years ago

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.

unlikelyzero commented 2 years ago

@p01 all of our visual tests need a "fixed time" since the Date.now() method is used on all pages.

https://github.com/nasa/openmct/blob/4b7bcf9c89800dd6b3ddd4c23a6a5ba8af7d64b1/e2e/tests/visual/default.spec.js#L50

https://percy.io/b2e34b17/openmct/builds/17336359/unchanged/973207390?browser=safari&viewLayout=overlay&viewMode=new&width=2000

Screen Shot 2022-04-14 at 12 45 30 PM
DerGernTod commented 2 years ago

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)

aw492267 commented 2 years ago

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?

pkerschbaum commented 2 years ago

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;
  }
}
sfuet3r commented 2 years ago

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.

sfuet3r commented 2 years ago

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')
})
sonyarianto commented 2 years ago

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?

p01 commented 2 years ago

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,

sonyarianto commented 2 years ago

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,

Thank you @p01 I will try that later :)

Simonlfr commented 1 year ago

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')
  })
segevfiner commented 1 year ago

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.

blessanm86 commented 1 year ago

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

raulfdm commented 1 year ago

Could we have a built-in mock timers like jest/vite have?

dan-the-dev commented 1 year ago

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.

209 commented 1 year ago

Up

pongells commented 1 year ago

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

phani17c commented 1 year ago

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.
}
);
phani17c commented 1 year ago

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 🤷‍♂️🤷‍♂️

deepakgupta25 commented 10 months ago

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?

edumserrano commented 10 months ago

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

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.");
A-ZC-Lau commented 9 months ago

This issue has almost 200 upvotes.

Can we get an update on whether this will be planned for the near future or not?

michaelhays commented 9 months ago

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
}
FazYas123 commented 9 months ago

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;
    }
  }
}
MaruschkaScheepersW commented 8 months ago

This would really be a useful feature to have

bmitchinson commented 7 months ago

I've modified @p01's idea a bit to add the ability to control time during tests! Writeup: https://mitchinson.dev/playwright-mock-time

Setup

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

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))
  );
});
HannaSyn commented 6 months ago

It's 2024 and we still have to rewrite Data constructor to mock the date. Disappointing

zayehalo commented 6 months ago

Also looking for this feature as we migrate our tests from cypress to playwright.

pavelfeldman commented 5 months ago

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!

MillerSvt commented 5 months ago

@pavelfeldman

  1. Will it work the same way as addInitialScript? So that when the page is reloaded, the mocks are automatically applied again.
  2. Will the time reset to the initial time, after the page is reloaded, or will it be the same as before?
pavelfeldman commented 5 months ago

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.

julisch94 commented 5 months ago

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!

pavelfeldman commented 5 months ago

~@julisch94: thanks for the heads up, I have an idea on what might be wrong with the extension mode!~ Works for me locally :shrug:

DerGernTod commented 5 months ago

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.

pavelfeldman commented 5 months ago

@DerGernTod could you share your use case for performance API mocking?