Prototype of the "Set Timezone WebDrive extension"

This CL is NOT intended to be merge into chromium.
I just want to use the chromium tryBot to validate the code is correct.
The WPT PR is in https://github.com/web-platform-tests/wpt/pull/26555

Bug: 1144403
Change-Id: Ie3c062d1d9e59ee3f9087c7af99df7d2d29b056d
diff --git a/docs/writing-tests/testdriver.md b/docs/writing-tests/testdriver.md
index 931499d..5214cfb 100644
--- a/docs/writing-tests/testdriver.md
+++ b/docs/writing-tests/testdriver.md
@@ -182,6 +182,23 @@
 await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted", true);
 ```
 
+### set_time_zone
+
+Usage: `test_driver.set_time_zone(time_zone)`
+ * _timezone_: a string matching one of the
+   [Zone or Link names of the IANA Time Zone Database](https://www.iana.org/time-zones)
+
+This function set the host default time zone to the given time zone ID.
+It returns a promise that resolves after the time zone has
+been set to be overridden with _time_zone_.
+
+Example:
+
+``` js
+await test_driver.set_time_zone("Asia/Taipei");
+await test_driver.set_time_zone("Asia/Hong_Kong");
+```
+
 ## Using testdriver in Other Browsing Contexts
 
 Testdriver can be used in browsing contexts (i.e. windows or frames)
diff --git a/infrastructure/testdriver/set_timezone.html b/infrastructure/testdriver/set_timezone.html
new file mode 100644
index 0000000..0d514f5
--- /dev/null
+++ b/infrastructure/testdriver/set_timezone.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>TestDriver set_time_zone method</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script>
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+async_test(t => {
+  let timeZone = "Asia/Taipei";
+  test_driver
+    .set_time_zone(timeZone)
+    .then(() => {
+      assert_equals(defaultTimeZone(), timeZone);
+      t.done()
+    })
+    .catch(() => assert_unreached("set_time_zone failed"));
+});
+</script>
diff --git a/resources/testdriver.js b/resources/testdriver.js
index f3cee98..d1aae9e 100644
--- a/resources/testdriver.js
+++ b/resources/testdriver.js
@@ -455,6 +455,26 @@
             const blocked = state === "blocked";
             return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
         },
+
+        /**
+         * Set time zone.
+         *
+         * The set_time_zone function set the host default time zone to a new
+         * timezone id.
+         *
+         * This matches the behavior of the {@link
+         * https://github.com/whatwg/html/pull/3047
+         * Set Time Zone command}.
+         *
+         * @param {String} time_zone - IANA Time Zone ID.
+         *
+         * @returns {Promise} fulfilled after the time zone is set, or
+         *                    rejected if the set of time zone fails
+         */
+        set_time_zone: function(time_zone) {
+            return window.test_driver_internal.set_time_zone(time_zone);
+        },
+
     };
 
     window.test_driver_internal = {
@@ -560,5 +580,9 @@
         set_storage_access: function(origin, embedding_origin, blocked, context=null) {
             return Promise.reject(new Error("unimplemented"));
         },
+
+        set_time_zone: function(time_zone) {
+            return Promise.reject(new Error("unimplemented"));
+        },
     };
 })();
diff --git a/service-workers/timezonechange/resources/service-worker-timezonechange.js b/service-workers/timezonechange/resources/service-worker-timezonechange.js
new file mode 100644
index 0000000..12a9ae0
--- /dev/null
+++ b/service-workers/timezonechange/resources/service-worker-timezonechange.js
@@ -0,0 +1,14 @@
+self.addEventListener('message', function(e) {
+    const message = e.data;
+    if ('port' in message) {
+      const port = message.port;
+      const oldTimeZone =
+          (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+      self.addEventListener('timezonechange', function(evt) {
+        const newTimeZone =
+          (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+        port.postMessage('SUCCESS:' + newTimeZone);
+      });
+      port.postMessage('READY:' + oldTimeZone);
+    }
+});
diff --git a/service-workers/timezonechange/service-worker-timezonechange.https.html b/service-workers/timezonechange/service-worker-timezonechange.https.html
new file mode 100644
index 0000000..b7a278c
--- /dev/null
+++ b/service-workers/timezonechange/service-worker-timezonechange.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Service Worker: timezonechange event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="../service-worker/resources/test-helpers.sub.js"></script>
+<script>
+
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, we set to a fixed time zone.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+
+  const script = 'resources/service-worker-timezonechange.js';
+  const scope = 'resources/blank.html';
+  let worker;
+  let port;
+
+  return service_worker_unregister_and_register(t, script, scope)
+    .then(registration => {
+        t.add_cleanup(() => registration.unregister());
+        worker = registration.installing;
+
+        const messageChannel = new MessageChannel();
+        port = messageChannel.port1;
+        return new Promise(resolve => {
+            port.onmessage = resolve;
+            worker.postMessage({port: messageChannel.port2},
+                               [messageChannel.port2]);
+          });
+      })
+    .then(e => {
+        assert_equals(e.data, 'READY:' + oldTimeZone);
+        return new Promise(async resolve => {
+          port.onmessage = resolve;
+          // Change the time zone once the service worker is ready.
+          await window.test_driver.set_time_zone(newTimeZone);
+        });
+      })
+    .then(e => {
+        assert_equals(e.data, 'SUCCESS:' + newTimeZone);
+      });
+}, 'timezonechange event work in ServiceWorker');
+</script>
diff --git a/timezonechange/addeventlistener-timezonechange-fired.html b/timezonechange/addeventlistener-timezonechange-fired.html
new file mode 100644
index 0000000..2477077
--- /dev/null
+++ b/timezonechange/addeventlistener-timezonechange-fired.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, setup the time zone in a fixed place.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+  return new Promise(r => {
+    window.addEventListener('timezonechange', r);
+    window.test_driver.set_time_zone(newTimeZone);
+  }).then(e => {
+      assert_equals(defaultTimeZone(), newTimeZone);
+  });
+}, "Test that the timezonechange event fires on window.addEventListener('timezonechange')");
+</script>
+</body>
+</html>
diff --git a/timezonechange/ontimezonechange-event-property.html b/timezonechange/ontimezonechange-event-property.html
new file mode 100644
index 0000000..4025209
--- /dev/null
+++ b/timezonechange/ontimezonechange-event-property.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, setup the time zone in a fixed place.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+  return new Promise(async r => {
+    window.addEventListener('timezonechange', r);
+    await window.test_driver.set_time_zone(newTimeZone);
+  }).then(e => {
+    assert_false(e.cancelable);
+    assert_false(e.bubbles);
+    assert_equals(defaultTimeZone(), newTimeZone);
+  });
+}, "Test properties of the fired event.");
+
+</script>
+</body>
+</html>
diff --git a/timezonechange/ontimezonechange-over-setattribute.html b/timezonechange/ontimezonechange-over-setattribute.html
new file mode 100644
index 0000000..8a23e8b
--- /dev/null
+++ b/timezonechange/ontimezonechange-over-setattribute.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, setup the time_zone in a fixed place.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+  window.received = 0; // We need a global variable here.
+  var fromWindowHandler = false;
+  document.body.setAttribute('ontimezonechange', 'window.received++;');
+
+  return new Promise(async r => {
+    window.ontimezonechange = r;
+    await window.test_driver.set_time_zone(newTimeZone);
+  }).then(evt => {
+    received++;
+    assert_equals(window.received, 1);
+    assert_equals(defaultTimeZone(), newTimeZone);
+  });
+}, "Test that the timezonechange event fires on window.ontimezonechange but not body ontimezonechange attribute");
+</script>
+</body>
+</html>
diff --git a/timezonechange/ontimezonechange.html b/timezonechange/ontimezonechange.html
new file mode 100644
index 0000000..a0c7ca3
--- /dev/null
+++ b/timezonechange/ontimezonechange.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+
+test(function() {
+  assert_true('ontimezonechange' in window);
+}, "Test that timezonechange event handler API is present in window");
+
+</script>
+</body>
+</html>
diff --git a/timezonechange/resources/shared-worker-timezonechange.js b/timezonechange/resources/shared-worker-timezonechange.js
new file mode 100644
index 0000000..48c280f
--- /dev/null
+++ b/timezonechange/resources/shared-worker-timezonechange.js
@@ -0,0 +1,9 @@
+onconnect = connectEvent => {
+  let oldtimezone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+  const port = connectEvent.ports[0];
+  ontimezonechange = () => {
+    let timezone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+    port.postMessage("SUCCESS:" + timezone);
+  };
+  port.postMessage("READY:" + oldtimezone);  // (the html will change the timezone)
+}
diff --git a/timezonechange/resources/worker-timezonechange.js b/timezonechange/resources/worker-timezonechange.js
new file mode 100644
index 0000000..a0aeb0b
--- /dev/null
+++ b/timezonechange/resources/worker-timezonechange.js
@@ -0,0 +1,6 @@
+let oldtimezone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+ontimezonechange = evt => {
+  let timezone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+  postMessage("SUCCESS:" + timezone);
+}
+postMessage("READY:" + oldtimezone);
diff --git a/timezonechange/setattribute-over-ontimezonechange.html b/timezonechange/setattribute-over-ontimezonechange.html
new file mode 100644
index 0000000..22de3e2
--- /dev/null
+++ b/timezonechange/setattribute-over-ontimezonechange.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, setup the time zone in a fixed place.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+  window.received = 0; // We need a global variable here.
+  window.ontimezonechange = () => received++;
+  document.body.setAttribute('ontimezonechange', () => {
+    received++;
+    assert_equals(window.received, 1);
+    assert_equals(defaultTimeZone(), newTimeZone);
+  });
+  await window.test_driver.set_time_zone(newTimeZone);
+}, "Test that the timezonechange event fires on body ontimezonechange attribute but not window.ontimezonechange");
+</script>
+</body>
+</html>
diff --git a/timezonechange/setattribute.html b/timezonechange/setattribute.html
new file mode 100644
index 0000000..0af1284
--- /dev/null
+++ b/timezonechange/setattribute.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, setup the time zone in a fixed place.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+  window.received = false; // We need a global variable here.
+  document.body.setAttribute('ontimezonechange', 'window.received = true;');
+
+  await window.test_driver.set_time_zone(newTimeZone);
+  assert_true(window.received);
+  assert_equals(defaultTimeZone(), newTimeZone);
+}, "Test that the timezonechange event fires on body ontimezonechange attribute");
+
+</script>
+</body>
+</html>
diff --git a/timezonechange/shared-worker-timezonechange.html b/timezonechange/shared-worker-timezonechange.html
new file mode 100644
index 0000000..6040c90
--- /dev/null
+++ b/timezonechange/shared-worker-timezonechange.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>Test shared worker handle ontimezonechange event.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First we set the time zone to a fixed time zone.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  const worker = new SharedWorker('resources/shared-worker-timezonechange.js', 'name');
+  return new Promise(r => { worker.port.onmessage = r; })
+    .then(e => {
+      // Once we know the worker is ready, we change the time zone.
+      assert_equals(e.data, "READY:" + oldTimeZone);
+      return new Promise(async r => {
+        worker.port.onmessage = r;
+        await window.test_driver.set_time_zone(newTimeZone);
+      }).then(e => {
+        assert_equals(e.data, "SUCCESS:" + newTimeZone);
+      })
+    });
+}, "Test a shared worker handles ontimezonechange event.");
+</script>
diff --git a/timezonechange/window-ontimezonechange-fired.html b/timezonechange/window-ontimezonechange-fired.html
new file mode 100644
index 0000000..150f6d5
--- /dev/null
+++ b/timezonechange/window-ontimezonechange-fired.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+function defaultTimeZone() {
+  return (new Intl.DateTimeFormat()).resolvedOptions().timeZone;
+}
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First, setup the time zone in a fixed place.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  assert_equals(defaultTimeZone(), oldTimeZone);
+  return new Promise(async r => {
+    window.ontimezonechange = r;
+    await window.test_driver.set_time_zone(newTimeZone);
+  }).then(e => {
+    assert_equals(defaultTimeZone(), newTimeZone);
+  });
+}, "Test that the timezonechange event fires on window.ontimezonechange");
+
+</script>
+</body>
+</html>
diff --git a/timezonechange/worker-timezonechange.html b/timezonechange/worker-timezonechange.html
new file mode 100644
index 0000000..cb673b9
--- /dev/null
+++ b/timezonechange/worker-timezonechange.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>Test worker handle ontimezonechange event.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+promise_test(async t => {
+  const oldTimeZone = "Pacific/Fakaofo";
+  const newTimeZone = "Asia/Taipei";
+  // First we set the time zone to a fixed time zone.
+  await window.test_driver.set_time_zone(oldTimeZone);
+  const worker = new Worker('resources/worker-timezonechange.js');
+  return new Promise(r => { worker.onmessage = r; })
+    .then(e => {
+      // Once we know the worker is ready, we change the timezone.
+      assert_equals(e.data, "READY:" + oldTimeZone);
+      return new Promise(async r => {
+        worker.onmessage = r;
+        await window.test_driver.set_time_zone(newTimeZone);
+      }).then(e => {
+        assert_equals(e.data, "SUCCESS:" + newTimeZone);
+      })
+    });
+}, "Test a dedicated worker handles ontimezonechange event.");
+</script>
diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py
index 963ed33..db41d73 100644
--- a/tools/wptrunner/wptrunner/executors/actions.py
+++ b/tools/wptrunner/wptrunner/executors/actions.py
@@ -181,6 +181,18 @@
             "Setting user verified flag on authenticator %s to %s" % (authenticator_id, uv["isUserVerified"]))
         return self.protocol.virtual_authenticator.set_user_verified(authenticator_id, uv)
 
+class SetTimeZoneAction(object):
+    name = "set_time_zone"
+
+    def __init__(self, logger, protocol):
+        self.logger = logger
+        self.protocol = protocol
+
+    def __call__(self, payload):
+        time_zone = payload["time_zone"]
+        self.logger.debug("Setting time_zone to %s" % time_zone)
+        self.protocol.set_time_zone.set_time_zone(time_zone)
+
 
 actions = [ClickAction,
            DeleteAllCookiesAction,
@@ -194,4 +206,5 @@
            GetCredentialsAction,
            RemoveCredentialAction,
            RemoveAllCredentialsAction,
-           SetUserVerifiedAction]
+           SetUserVerifiedAction,
+           SetTimeZoneAction]
diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py
index 43fc910..df13dbe 100644
--- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py
+++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py
@@ -26,7 +26,8 @@
                        TestDriverProtocolPart,
                        GenerateTestReportProtocolPart,
                        SetPermissionProtocolPart,
-                       VirtualAuthenticatorProtocolPart)
+                       VirtualAuthenticatorProtocolPart,
+                       SetTimeZoneProtocolPart)
 from ..testrunner import Stop
 
 import webdriver as client
@@ -293,6 +294,14 @@
         return self.webdriver.send_session_command("POST", "webauthn/authenticator/%s/uv" % authenticator_id, uv)
 
 
+class WebDriverSetTimeZoneProtocolPart(SetTimeZoneProtocolPart):
+    def setup(self):
+        self.webdriver = self.parent.webdriver
+
+    def set_time_zone(self, time_zone):
+        return self.webdriver.send_session_command("POST", "time_zone", {"time_zone": time_zone})
+
+
 class WebDriverProtocol(Protocol):
     implements = [WebDriverBaseProtocolPart,
                   WebDriverTestharnessProtocolPart,
@@ -304,7 +313,8 @@
                   WebDriverTestDriverProtocolPart,
                   WebDriverGenerateTestReportProtocolPart,
                   WebDriverSetPermissionProtocolPart,
-                  WebDriverVirtualAuthenticatorProtocolPart]
+                  WebDriverVirtualAuthenticatorProtocolPart,
+                  WebDriverSetTimeZoneProtocolPart]
 
     def __init__(self, executor, browser, capabilities, **kwargs):
         super(WebDriverProtocol, self).__init__(executor, browser)
diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py
index 8bdc8b0..497fa57 100644
--- a/tools/wptrunner/wptrunner/executors/protocol.py
+++ b/tools/wptrunner/wptrunner/executors/protocol.py
@@ -515,3 +515,16 @@
     def render_as_pdf(self, width, height):
         """Output document as PDF"""
         pass
+
+class SetTimeZoneProtocolPart(ProtocolPart):
+    """Protocol part for setting time zone"""
+    __metaclass__ = ABCMeta
+
+    name = "set_time_zone"
+
+    @abstractmethod
+    def set_time_zone(self, time_zone):
+        """Set time_zone as default time zone."""
+        print("SetTimeZoneProtocolPart" + time_zone)
+        pass
+
diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js
index e15652d..f3b1c89 100644
--- a/tools/wptrunner/wptrunner/testdriver-extra.js
+++ b/tools/wptrunner/wptrunner/testdriver-extra.js
@@ -233,4 +233,9 @@
     window.test_driver_internal.set_user_verified = function(authenticator_id, uv, context=null) {
         return create_action("set_user_verified", {authenticator_id, uv, context});
     };
+
+    window.test_driver_internal.set_time_zone = function(time_zone) {
+      console.log("Internal set_time_zone " + time_zone);
+        return create_action("set_time_zone", {time_zone});
+    };
 })();