Skip to content
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

Closed
chambo-e opened this issue Apr 28, 2021 · 58 comments
Closed

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

chambo-e opened this issue Apr 28, 2021 · 58 comments

Comments

@chambo-e
Copy link
Contributor

chambo-e commented Apr 28, 2021

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 reproductibility

This has already been mentioned here #820 but I did not found any follow up issues :)

@mxschmitt mxschmitt changed the title [Feature] clock() primitive [Feature] Time/Date emulation via e.g. a clock() primitive Apr 28, 2021
@idxn
Copy link

idxn commented Aug 21, 2021

Hopefully, some folks would upvote this more and more.

@Shaddix
Copy link

Shaddix commented Aug 25, 2021

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

@jithinjosejacob
Copy link

jithinjosejacob commented Oct 24, 2021

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
Copy link
Collaborator

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
Copy link
Contributor

unlikelyzero commented Nov 12, 2021

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

@unlikelyzero
Copy link
Contributor

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
Copy link
Contributor

p01 commented Apr 1, 2022

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
Copy link
Contributor

Very clever!

@mizozobu
Copy link

mizozobu commented Apr 8, 2022

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.

pollRaf<T>(predicate: Predicate<T>): InjectedScriptPoll<T> {
return this.poll(predicate, next => requestAnimationFrame(next));
}
pollInterval<T>(pollInterval: number, predicate: Predicate<T>): InjectedScriptPoll<T> {
return this.poll(predicate, next => setTimeout(next, pollInterval));
}

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
Copy link

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
Copy link
Contributor

p01 commented Apr 12, 2022

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
Copy link

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
Copy link
Contributor

p01 commented Apr 13, 2022

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

@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
Copy link
Contributor

unlikelyzero commented Apr 13, 2022

@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
Copy link
Contributor

p01 commented Apr 14, 2022

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.

@DerGernTod
Copy link

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
Copy link

aw492267 commented Jun 6, 2022

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
Copy link
Contributor

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
Copy link

sfuet3r commented Jun 9, 2022

Would anyone have any idea why #6347 (comment) 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
Copy link

sfuet3r commented Jun 9, 2022

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
Copy link

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
Copy link
Contributor

p01 commented Jun 27, 2022

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,

@deepakgupta25
Copy link

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
Copy link

edumserrano commented Jan 10, 2024

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

This issue has almost 200 upvotes.

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

@michaelhays
Copy link

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
Copy link

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
Copy link

This would really be a useful feature to have

@bmitchinson
Copy link

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
Copy link

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

@zayehalo
Copy link

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

@pavelfeldman
Copy link
Member

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
Copy link

MillerSvt commented Jun 3, 2024

@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
Copy link
Member

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
Copy link

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
Copy link
Member

pavelfeldman commented Jun 3, 2024

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

@DerGernTod
Copy link

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
Copy link
Member

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

@DerGernTod
Copy link

especially relevant for performance monitoring tools (obviously). the simplest use case is

async function callAndMeasureTask() {
  performance.mark("foo");
  await someTaskThatDoesSomething();
  const measure = performance.measure("something", "foo")
  reportSomeMetric(measure.duration); // sends a request to an endpoint with the measured body
}

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 performance.now() rather than Date.now() since it isn't affected by system clock changes: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#performance.now_vs._date.now

if performance.timeOrigin is mocked, this would also affect all PerformanceEntry instances, since their timings are relative to it. if you have a test where you mock the timers, jump to 2 seconds and then send a request that generates a PerformanceEntry, you expect that request's entry to have a start time with 2 seconds. following that, a performance monitoring tool can capture the entries and build reports on top of them that can then be asserted on in a test (without having to wait the 2 seconds)

@pavelfeldman
Copy link
Member

DerGernTod

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 performance.now(). I asked because I was thinking of undoing it.

@MathiasCiarlo
Copy link

MathiasCiarlo commented Jun 11, 2024

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!

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:

  1. Click "remind me later" which sets a js-cookie expiring in 90 days.
  2. await page.clock.setTime(dateNinetyOneDaysAhead);
  3. refresh page and expect the expired cookie to have been deleted
  4. Cookie was not deleted 😢

I verified new Date() was indeed showing a date 91 days ahead while debugging. Could it be the browser's handling of cookie expiration is not reachable with page.clock()?

@pavelfeldman
Copy link
Member

@MathiasCiarlo: Is it supposed to work with cookies?

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?

@MathiasCiarlo
Copy link

MathiasCiarlo commented Jun 20, 2024

@pavelfeldman Thanks for the info! I understand 😀

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?

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:

  1. Verify popup is shown
  2. Click "ask me later"
  3. Refresh
  4. Verify popup is not shown
  5. await page.waitFor(90 days) 😉 (some logic which forwards time 90 days)
  6. Refresh
  7. Verify popup is shown again

@pavelfeldman
Copy link
Member

Closing as fixed

@odinho
Copy link

odinho commented Jul 15, 2024

...
You can give it a try via installing @playwright/test@next. Please tell us if this does or does not work for you!

I know it is released. But as a quick feedback I was surprised that setSystemTime() installed fake timers. I only wanted to set the time on Date.now and new Date() and was very surprised when requestIdleCallback stopped working since the simple mock-version deadlocked.

I would have expected needing to call install() to actually get those.

Luckily I found Mathieu's <3 earlier workaround which works great for a minimal setSystemTime.

@sidharthv96
Copy link
Contributor

When used in a page with Monaco editor, the following error is thrown.
Will try to setup a repro soon.

Error: clock.runFor: Error: Cannot read properties of undefined (reading 'duration')

    TypeError: Cannot read properties of undefined (reading 'duration')
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests