Migrate credential-management WPTs to JS modules

Update the credential-management WPTs to use JS modules, including for
Mojo JS bindings.

This also refactors the supporting modules to isolate individual
tests from any browser-specific details.

Bug: 1004256
Change-Id: If5428dbd4f371fdc92c62da2a2378e8221c5c658
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2472201
Reviewed-by: Michael Moss <mmoss@chromium.org>
Reviewed-by: Robert Ma <robertma@chromium.org>
Commit-Queue: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/master@{#817715}
diff --git a/credential-management/otpcredential-get-basics.https.html b/credential-management/otpcredential-get-basics.https.html
index edeb42a..c0cac16 100644
--- a/credential-management/otpcredential-get-basics.https.html
+++ b/credential-management/otpcredential-get-basics.https.html
@@ -3,15 +3,12 @@
 <title>Tests OTPCredential</title>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
-<script src="/resources/test-only-api.js"></script>
-<script src="support/otpcredential-helper.js"></script>
-<script>
-'use strict';
+<script type="module">
+import {Status, expectOTPRequest} from './support/otpcredential-helper.js';
 
 promise_test(async t => {
-  await expect(receive).andReturn(async () => {
-      return {status: Status.kSuccess, otp: "ABC"};
-  });
+  await expectOTPRequest().andReturn(
+      () => ({status: Status.SUCCESS, otp: "ABC"}));
 
   let cred = await navigator.credentials.get({otp: {transport: ["sms"]}});
 
@@ -19,17 +16,15 @@
 }, 'Basic usage');
 
 promise_test(async t => {
-  await expect(receive).andReturn(async () => {
-      return {status: Status.kSuccess, otp: "ABC"};
-  });
-  await expect(receive).andReturn(async () => {
-      return {status: Status.kSuccess, otp: "ABC2"};
-  });
+  await expectOTPRequest().andReturn(
+      () => ({status: Status.SUCCESS, otp: "ABC"}));
+  await expectOTPRequest().andReturn(
+      () => ({status: Status.SUCCESS, otp: "ABC2"}));
 
   let sms1 = navigator.credentials.get({otp: {transport: ["sms"]}});
   let sms2 = navigator.credentials.get({otp: {transport: ["sms"]}});
 
-  let cred2= await sms2;
+  let cred2 = await sms2;
   let cred1 = await sms1;
 
   assert_equals(cred1.code, "ABC");
@@ -37,21 +32,18 @@
 }, 'Handle multiple requests in different order.');
 
 promise_test(async t => {
-  await expect(receive).andReturn(async () => {
-      return {status: Status.kCancelled};
-  });
-  await expect(receive).andReturn(async () => {
-      return {status: Status.kSuccess, otp: "success"};
-  });
+  await expectOTPRequest().andReturn(() => ({status: Status.CANCELLED}));
+  await expectOTPRequest().andReturn(
+      () => ({status: Status.SUCCESS, otp: "success"}));
 
-  let cancelled_sms = navigator.credentials.get({otp: {transport: ["sms"]}});
-  let successful_sms = navigator.credentials.get({otp: {transport: ["sms"]}});
+  let cancelledRequest = navigator.credentials.get({otp: {transport: ["sms"]}});
+  let successfulCred =
+      await navigator.credentials.get({otp: {transport: ["sms"]}});
 
-  let successful_cred = await successful_sms;
-  assert_equals(successful_cred.code, "success");
+  assert_equals(successfulCred.code, "success");
 
   try {
-    await cancelled_sms;
+    await cancelledRequest;
     assert_unreached('Expected AbortError to be thrown.');
   } catch (error) {
     assert_equals(error.name, "AbortError");
@@ -59,9 +51,7 @@
 }, 'Handle multiple requests with success and error.');
 
 promise_test(async t => {
-  await expect(receive).andReturn(async () => {
-      return {status: Status.kCancelled};
-  });
+  await expectOTPRequest().andReturn(() => ({status: Status.CANCELLED}));
 
   await promise_rejects_dom(t, 'AbortError', navigator.credentials.get(
     {otp: {transport: ["sms"]}}));
diff --git a/credential-management/support/otpcredential-helper.js b/credential-management/support/otpcredential-helper.js
index 0c6ce8b..e07e9f5 100644
--- a/credential-management/support/otpcredential-helper.js
+++ b/credential-management/support/otpcredential-helper.js
@@ -1,52 +1,114 @@
-'use strict';
-
 // These tests rely on the User Agent providing an implementation of
-// the sms retriever.
+// MockWebOTPService.
 //
 // In Chromium-based browsers this implementation is provided by a polyfill
 // in order to reduce the amount of test-only code shipped to users. To enable
 // these tests the browser must be run with these options:
 // //   --enable-blink-features=MojoJS,MojoJSTest
 
-const Status = {};
+import {isChromiumBased} from '/resources/test-only-api.m.js';
 
-async function loadChromiumResources() {
-  const resources = [
-    '/gen/mojo/public/mojom/base/time.mojom-lite.js',
-    '/gen/third_party/blink/public/mojom/sms/webotp_service.mojom-lite.js',
-  ];
-
-  await loadMojoResources(resources, true);
-  await loadScript('/resources/chromium/mock-sms-receiver.js');
-
-  Status.kSuccess = blink.mojom.SmsStatus.kSuccess;
-  Status.kTimeout = blink.mojom.SmsStatus.kTimeout;
-  Status.kCancelled = blink.mojom.SmsStatus.kCancelled;
+/**
+ * This enumeration is used by WebOTP WPTs to control mock backend behavior.
+ * See MockWebOTPService below.
+ */
+export const Status = {
+  SUCCESS: 0,
+  UNHANDLED_REQUEST: 1,
+  CANCELLED: 2,
+  ABORTED: 3,
 };
 
-async function create_sms_provider() {
-  if (typeof SmsProvider === 'undefined') {
-    if (isChromiumBased) {
-      await loadChromiumResources();
-    } else {
-      throw new Error('Mojo testing interface is not available.');
-    }
-  }
-  if (typeof SmsProvider === 'undefined') {
-    throw new Error('Failed to set up SmsProvider.');
-  }
-  return new SmsProvider();
+/**
+ * A interface which must be implemented by browsers to support WebOTP WPTs.
+ */
+export class MockWebOTPService {
+  /**
+   * Accepts a function to be invoked in response to the next OTP request
+   * received by the mock. The (optionally async) function, when executed, must
+   * return an object with a `status` field holding one of the `Status` values
+   * defined above, and -- if successful -- an `otp` field containing a
+   * simulated OTP string.
+   *
+   * Tests will call this method directly to inject specific response behavior
+   * into the browser-specific mock implementation.
+   */
+  async handleNextOTPRequest(responseFunc) {}
 }
 
-function receive() {
-  throw new Error("expected to be overriden by tests");
+/**
+ * Returns a Promise resolving to a browser-specific MockWebOTPService subclass
+ * instance if one is available.
+ */
+async function createBrowserSpecificMockImpl() {
+  if (isChromiumBased) {
+    return await createChromiumMockImpl();
+  }
+  throw new Error('Unsupported browser.');
 }
 
-function expect(call) {
+const asyncMock = createBrowserSpecificMockImpl();
+
+export function expectOTPRequest() {
   return {
     async andReturn(callback) {
-      const mock = await create_sms_provider();
-      mock.pushReturnValuesForTesting(call.name, callback);
+      const mock = await asyncMock;
+      mock.handleNextOTPRequest(callback);
     }
   }
 }
+
+/**
+ * Instantiates a Chromium-specific subclass of MockWebOTPService.
+ */
+async function createChromiumMockImpl() {
+  const {SmsStatus, WebOTPService, WebOTPServiceReceiver} = await import(
+      '/gen/third_party/blink/public/mojom/sms/webotp_service.mojom.m.js');
+  const MockWebOTPServiceChromium = class extends MockWebOTPService {
+    constructor() {
+      super();
+      this.mojoReceiver_ = new WebOTPServiceReceiver(this);
+      this.interceptor_ =
+          new MojoInterfaceInterceptor(WebOTPService.$interfaceName);
+      this.interceptor_.oninterfacerequest = (e) => {
+        this.mojoReceiver_.$.bindHandle(e.handle);
+      };
+      this.interceptor_.start();
+      this.requestHandlers_ = [];
+      Object.freeze(this);
+    }
+
+    handleNextOTPRequest(responseFunc) {
+      this.requestHandlers_.push(responseFunc);
+    }
+
+    async receive() {
+      if (this.requestHandlers_.length == 0) {
+        throw new Error('Mock received unexpected OTP request.');
+      }
+
+      const responseFunc = this.requestHandlers_.shift();
+      const response = await responseFunc();
+      switch (response.status) {
+        case Status.SUCCESS:
+          if (typeof response.otp != 'string') {
+            throw new Error('Mock success results require an OTP string.');
+          }
+          return {status: SmsStatus.kSuccess, otp: response.otp};
+        case Status.UNHANDLED_REQUEST:
+          return {status: SmsStatus.kUnhandledRequest};
+        case Status.CANCELLED:
+          return {status: SmsStatus.kCancelled};
+        case Status.ABORTED:
+          return {status: SmsStatus.kAborted};
+        default:
+          throw new Error(
+              `Mock result contains unknown status: ${response.status}`);
+      }
+    }
+
+    async abort() {}
+  };
+  return new MockWebOTPServiceChromium();
+}
+
diff --git a/credential-management/support/otpcredential-iframe.html b/credential-management/support/otpcredential-iframe.html
index 83f25d5..4affc00 100644
--- a/credential-management/support/otpcredential-iframe.html
+++ b/credential-management/support/otpcredential-iframe.html
@@ -1,8 +1,6 @@
 <!doctype html>
-<script src="/resources/test-only-api.js"></script>
-<script src="otpcredential-helper.js"></script>
-<script>
-'use strict';
+<script type="module">
+import {Status, expectOTPRequest} from './otpcredential-helper.js';
 
 // Loading otpcredential-iframe.html in the test will make an OTPCredentials
 // call on load, and trigger a postMessage upon completion.
@@ -13,24 +11,16 @@
 //   string errorType: error.name
 // }
 
-// Intercept successful calls and return mocked value.
-(async function() {
-    await expect(receive).andReturn(() => {
-        return Promise.resolve({
-            status: Status.kSuccess,
-            otp: "ABC123",
-        });
-    });
-}());
-
 window.onload = async () => {
-    try {
-        const credentials =
-            await navigator.credentials.get({otp: {transport: ["sms"]}});
-        window.parent.postMessage({result: "Pass", code: credentials.code}, '*');
-    } catch (error) {
-        window.parent.postMessage({result: "Fail", errorType: error.name}, '*');
-    }
+  try {
+    await expectOTPRequest().andReturn(
+        () => ({status: Status.SUCCESS, otp: "ABC123"}));
+    const credentials =
+        await navigator.credentials.get({otp: {transport: ["sms"]}});
+    window.parent.postMessage({result: "Pass", code: credentials.code}, '*');
+  } catch (error) {
+    window.parent.postMessage({result: "Fail", errorType: error.name}, '*');
+  }
 }
 
 </script>
diff --git a/resources/chromium/mock-sms-receiver.js b/resources/chromium/mock-sms-receiver.js
deleted file mode 100644
index 903456d..0000000
--- a/resources/chromium/mock-sms-receiver.js
+++ /dev/null
@@ -1,51 +0,0 @@
-'use strict';
-
-const SmsProvider = (() => {
-
-  class MockWebOTPService {
-
-    constructor() {
-      this.mojoReceiver_ = new blink.mojom.WebOTPServiceReceiver(this);
-
-      this.interceptor_ =
-          new MojoInterfaceInterceptor(blink.mojom.WebOTPService.$interfaceName);
-
-      this.interceptor_.oninterfacerequest = (e) => {
-        this.mojoReceiver_.$.bindHandle(e.handle);
-      }
-      this.interceptor_.start();
-
-      this.returnValues_ = {};
-    }
-
-    async receive() {
-      let call = this.returnValues_.receive ?
-          this.returnValues_.receive.shift() : null;
-      if (!call)
-        return;
-      return call();
-    }
-
-    async abort() {};
-
-    pushReturnValuesForTesting(callName, value) {
-      this.returnValues_[callName] = this.returnValues_[callName] || [];
-      this.returnValues_[callName].push(value);
-      return this;
-    }
-  }
-
-  const mockWebOTPService = new MockWebOTPService();
-
-  class SmsProviderChromium {
-    constructor() {
-      Object.freeze(this); // Make it immutable.
-    }
-
-    pushReturnValuesForTesting(callName, callback) {
-      mockWebOTPService.pushReturnValuesForTesting(callName, callback);
-    }
-  }
-
-  return SmsProviderChromium;
-})();
diff --git a/resources/test-only-api.m.js b/resources/test-only-api.m.js
new file mode 100644
index 0000000..984f635
--- /dev/null
+++ b/resources/test-only-api.m.js
@@ -0,0 +1,5 @@
+/* Whether the browser is Chromium-based with MojoJS enabled */
+export const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+export const isWebKitBased = !isChromiumBased && 'internals' in self;
diff --git a/resources/test-only-api.m.js.headers b/resources/test-only-api.m.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/resources/test-only-api.m.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600