How to test date pickers and calendar popups in Playwright

Reliable ways to automate native date inputs, typed date fields and calendar popups, with month navigation, locales and frozen clocks.

Date pickers are one of the most common reasons recorded tests break. "Click the 14th" works in November and fails in December. "Click next month twice" breaks when the test runs on a different date. And every component library builds its calendar differently.

This guide covers the three kinds of date fields you'll meet, how to set each one reliably, and how to keep date tests stable over time.

Three kinds of date fields

  1. Native inputs: <input type="date">, datetime-local, month and time. The browser draws the picker.
  2. Typed text fields: a plain text box that accepts "11/14/2026" or "14 Nov 2026", sometimes with a calendar as an optional helper.
  3. Calendar popups: a read-only field that opens a custom calendar, such as React DayPicker, MUI, flatpickr or jQuery UI. You have to navigate to the month and click the day.

The right technique depends on which one you're facing, so check first: open the browser's dev tools and look at the input's type and whether it has a readonly attribute.

Native date inputs: fill with ISO format

Native date inputs always take the ISO format in automation, whatever the browser displays:

await page.getByLabel('Check-in').fill('2026-11-14');      // type="date"
await page.getByLabel('Meeting').fill('2026-11-14T09:30');  // type="datetime-local"
await page.getByLabel('Card expiry').fill('2027-03');       // type="month"

Don't try to click through the browser's built-in picker. It isn't part of the page, and it looks different in every browser.

Typed date fields: fill, blur, verify

For text fields, type the date in the format the field expects, move focus away so the app parses it, then check what it kept:

const due = page.getByLabel('Due date');
await due.fill('11/14/2026');
await due.press('Tab');
await expect(due).toHaveValue('11/14/2026');

The final assertion matters. Many fields silently reformat or reject input, and without it the test continues with an empty date and fails three steps later with a confusing error.

Calendar popups: navigate, then click a labelled day

For read-only fields with a custom calendar:

  1. Click the field to open the popup.
  2. Read the visible month and year from the calendar header.
  3. Click next or previous until the target month is showing.
  4. Click the day, preferably by its full accessible label, not its number.
await page.getByLabel('Check-out').click();
const calendar = page.getByRole('dialog');
const header = calendar.getByRole('heading');

while (!(await header.textContent())?.includes('December 2026')) {
  await calendar.getByRole('button', { name: /next month/i }).click();
}
await calendar.getByRole('gridcell', { name: 'December 3, 2026' }).click();
await expect(page.getByLabel('Check-out')).toHaveValue(/2026/);

Why the full label? Most calendars show days from the previous and next month to fill the grid, so the number "3" can appear twice. Good calendar components give each day an aria-label such as "Thursday, December 3, 2026". If yours only shows the number, scope the click to days that aren't marked as outside the current month (often a class such as outside or disabled).

Some calendars have month and year dropdowns instead of, or as well as, arrows. Selecting the month and year directly is faster and more reliable than clicking an arrow 20 times:

await calendar.getByRole('combobox', { name: 'Month' }).selectOption('December');
await calendar.getByRole('combobox', { name: 'Year' }).selectOption('2026');

Always put a limit on any "click next until" loop. If the header text never matches, for example because the locale shows "Dezember", you want a clear failure, not a test that hangs until it times out.

Keep date tests stable over time

Don't hard-code relative dates in recordings. A recording made on 1 November that picks "tomorrow" means 2 November forever, so that test will fail once that date is in the past and the calendar disables it. Either:

  • Freeze the clock so "today" is always the same:
await page.clock.setFixedTime(new Date('2026-11-01T10:00:00'));
  • Or compute the date in the test and select it by its full label.

Pin locale and time zone. "11/04/2026" is 4 November in the US and 11 April in the UK. Set both in your Playwright config:

use: { locale: 'en-US', timezoneId: 'America/New_York' }

Watch for disabled days. Booking widgets often disable past dates, weekends or sold-out days. A test that tries to click a disabled day fails in a confusing way, so assert that the day is enabled first, or choose dates the app always allows.

How ReplayQA handles dates

When ReplayQA turns a screen recording into a test, a date chosen from a calendar becomes a single step, "set Check-out to 3 Dec 2026", instead of a fragile chain of clicks. At run time it works out what kind of field it's facing:

  • Native inputs get the ISO value directly.
  • Typed fields get the date in the page's locale, then the value is checked.
  • Calendar popups are opened and navigated using the month and year dropdowns if there are any, otherwise the next and previous arrows (even icon-only ones). Then the correctly labelled day is clicked, with outside-month days skipped.

The date also reads naturally when you review the test, so changing it later is a one-field edit.