How to change timezone in chrome for testing

Send Emulation.setTimezoneOverride with an IANA id over the DevTools Protocol. Chrome 152 moved Intl.DateTimeFormat().resolvedOptions().timeZone from Europe/Kiev to America/New_York and the offset from -180 to 240, with no change to the machine clock. The timestamp the server had rendered stayed where it was.

Why check this

Run this whenever a bug report names a time that is wrong by a whole number of hours, and in regression on any screen that shows a date to a user in another country.

The defect this finds is a page that renders half its times in the viewer's zone and half in the server's. It is invisible from one zone, because both halves agree there. Setting the browser to a second zone separates them in one reload, and the step below shows the two lines drifting apart while sitting next to each other in the same paragraph.

Prerequisites

// server.mjs: one timestamp, rendered once by the server and once by the browser.
import { createServer } from 'node:http';
const PORT = 8913;
createServer((req, res) => {
  const serverZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  const iso = '2026-07-04T22:30:00Z';
  if (req.url === '/zone') {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ serverZone, rendered: new Date(iso).toLocaleString('en-US') }));
    return;
  }
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
  res.end(`<!doctype html><meta charset="utf-8"><title>zone</title>
<p id="server">server rendered: ${new Date(iso).toLocaleString('en-US')} (${serverZone})</p>
<p id="client"></p>
<script>
  document.getElementById('client').textContent =
    'browser rendered: ' + new Date('${iso}').toLocaleString('en-US') +
    ' (' + Intl.DateTimeFormat().resolvedOptions().timeZone + ')';
</script>`);
}).listen(PORT, '127.0.0.1', () => console.log('listening on ' + PORT));

Steps

  1. Step 1.

    Open the page and read what the browser thinks the zone is.

    await s.goto('http://127.0.0.1:8913/');
    await s.page.evaluate(() => ({
      'Intl…timeZone': Intl.DateTimeFormat().resolvedOptions().timeZone,
      'getTimezoneOffset()': new Date().getTimezoneOffset(),
      'Date.toString()': new Date('2026-07-04T22:30:00Z').toString(),
      'navigator.language': navigator.language,
    }));
    
    --- 1 before any override
    {
    "Intl…timeZone": "Europe/Kiev",
    "getTimezoneOffset()": -180,
    "Date.toString()": "Sun Jul 05 2026 01:30:00 GMT+0300 (за східноєвропейським літнім часом)",
    "navigator.language": "uk-UA"
    }

    Four readings, all from the machine. getTimezoneOffset counts minutes to add to local time to reach UTC, so a zone ahead of UTC gives a negative number.

  2. Step 2.

    Set the override and take the same four readings again.

    await s.cdp.send('Emulation.setTimezoneOverride', { timezoneId: 'America/New_York' });
    
    --- 2 Emulation.setTimezoneOverride America/New_York: accepted
    --- 2 after the override
    {
    "Intl…timeZone": "America/New_York",
    "getTimezoneOffset()": 240,
    "Date.toString()": "Sat Jul 04 2026 18:30:00 GMT-0400 (за північноамериканським східним літнім часом)",
    "navigator.language": "uk-UA"
    }

    The zone, the offset and the rendered date all moved, with no reload and no change to the machine. navigator.language did not move: this command changes the clock, not the locale.

  3. Step 3.

    Read the two paragraphs on the page, then reload and read them again.

    await s.page.evaluate(() => ({
      '#server': document.getElementById('server').textContent,
      '#client': document.getElementById('client').textContent,
    }));
    await s.page.reload({ waitUntil: 'networkidle2' });
    
    --- 3 the two lines on the page, no reload yet
    {
    "#server": "server rendered: 7/5/2026, 1:30:00 AM (Europe/Kiev)",
    "#client": "browser rendered: 7/5/2026, 1:30:00 AM (Europe/Kiev)"
    }
    --- 3 the two lines after a reload
    {
    "#server": "server rendered: 7/5/2026, 1:30:00 AM (Europe/Kiev)",
    "#client": "browser rendered: 7/4/2026, 6:30:00 PM (America/New_York)"
    }

    Before the reload both lines still read Europe/Kiev: the client line was written at load time and the override arrived after it. After the reload the browser line moves seven hours and the server line does not move at all.

  4. Step 4.

    Ask the server directly, so the previous step is not read as a caching artefact.

    await s.page.evaluate(() => fetch('/zone').then((r) => r.json()));
    
    --- 3 GET /zone
    {
    "serverZone": "Europe/Kiev",
    "rendered": "7/5/2026, 1:30:00 AM"
    }

    The request came from the overridden page and the response still carries the server's zone. Nothing about the override crosses the network.

  5. Step 5.

    Send an id the tzdata does not have, once with no override in force and once with one.

    const set = async (id) => {
      try { await s.cdp.send('Emulation.setTimezoneOverride', { timezoneId: id }); return 'accepted'; }
      catch (e) { return 'rejected: ' + e.message.replace(/^Protocol error \(.*?\): /, '').split('\n')[0]; }
    };
    
    --- 4 invalid and alias ids
    America/Atlantis, no override in force : rejected: Invalid timezone id -> Europe/Kiev
    America/New_York                       : accepted -> America/New_York
    America/Atlantis, override in force    : accepted -> America/New_York
    EST                                    : accepted -> America/Panama

    The same bad id is rejected on the first call and accepted on the third, leaving America/New_York in force. EST is accepted and resolves to America/Panama, a zone with no daylight saving.

  6. Step 6.

    Clear the override with an empty string and confirm the readings return.

    await s.cdp.send('Emulation.setTimezoneOverride', { timezoneId: '' });
    
    --- 5 clear: accepted
    --- 5 after clearing
    {
    "Intl…timeZone": "Europe/Kiev",
    "getTimezoneOffset()": -180,
    "Date.toString()": "Sun Jul 05 2026 01:30:00 GMT+0300 (за східноєвропейським літнім часом)",
    "navigator.language": "uk-UA"
    }

    All four readings match step 1. A suite that leaves an override in place hands it to whatever runs next in the same tab.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Intl…timeZone reports the id you sent | The override is in force for this target | Reload before reading anything the page rendered at load time. | | A rendered line does not move after a reload | That timestamp was formatted on the server | Send the instant and format it in the browser, or the viewer sees the server's zone. | | getTimezoneOffset() is positive | The emulated zone is behind UTC | Expected for the Americas. The sign is the opposite of the GMT label. | | The read-back id differs from the one you sent | A link or an alias was resolved | EST came back as America/Panama. Assert on the read-back, not on the string you sent. | | navigator.language is unchanged | Only the clock moved | Change the locale separately if the format is also under test. |

Common mistakes

Sign: A test that sets a misspelled zone passes, and passes for the wrong zone.Cause: Emulation.setTimezoneOverride rejects America/Atlantis when no override is in force and accepts the identical call when one is, leaving the previous zone active. The run above shows both outcomes in one session. The protocol documentation records neither. Read the zone back after every call.
Sign: The page still shows the old time right after the override is applied.Cause: The override changes what new JavaScript reads. Text already written into the DOM at load time is a string and does not recompute. In the capture the client line held Europe/Kiev until the reload, then moved seven hours.
Sign: A second tab in the same run reports the machine zone.Cause: The override is set on one target. Opening a fresh page in the same browser here returned Europe/Kiev while the first page was on Asia/Tokyo. Apply the override to every target the flow opens, including popups.
Sign: The zone is set for the test and every timestamp still shows the server's.Cause: Nothing about the override is sent to the server. GET /zone from the overridden page returned serverZone Europe/Kiev. A page that formats dates on the server cannot be tested this way, and that itself is the finding.

What to check next

FAQ

How do I check the browser timezone?

Run Intl.DateTimeFormat().resolvedOptions().timeZone in the console. It returns an IANA id such as Europe/Kiev. new Date().getTimezoneOffset() gives the current offset in minutes, with the sign inverted against the GMT label.

Can I change the timezone in DevTools without a script?

Any DevTools Protocol client reaches the same command. Puppeteer wraps it as page.emulateTimezone('Asia/Tokyo'), which returned Asia/Tokyo here, and Playwright takes timezoneId when a context is created. All three end at Emulation.setTimezoneOverride.

Does the override survive a reload?

Yes. It is set on the target, not on the document. Step 3 reloads the page and the browser line comes back in the emulated zone. It ends when the empty string clears it or the target closes.

Why does the date still look wrong after I set the zone?

Check whether the value was rendered by the server. In the capture the server paragraph stayed at 1:30 AM Europe/Kiev through the override and the reload, because the string was built before the response left the server.

Is changing the machine clock equivalent?

No, and it is worse. The override affects one browser target for the length of the run. The system setting moves certificates, tokens and every other process on the machine, and it outlives the test.

Verified

Verified by Maks VernyChrome 152.0.7977.76Node 22.23.2puppeteer-core 25.10.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate9 minpublished updated Maks Verny