Require a user gesture for the Contact API

Also provide a series of new tests to cover the implementation,
particularly this requirement.

Bug: 860467
Change-Id: I274e433fadba5228ac5dc0853b5a30354904d43e
Reviewed-on: https://chromium-review.googlesource.com/c/1430014
Commit-Queue: Peter Beverloo <peter@chromium.org>
Reviewed-by: Finnur Thorarinsson <finnur@chromium.org>
Cr-Commit-Position: refs/heads/master@{#625333}
diff --git a/third_party/blink/renderer/modules/contacts_picker/contacts_manager.cc b/third_party/blink/renderer/modules/contacts_picker/contacts_manager.cc
index 75e6642d..2512851 100644
--- a/third_party/blink/renderer/modules/contacts_picker/contacts_manager.cc
+++ b/third_party/blink/renderer/modules/contacts_picker/contacts_manager.cc
@@ -8,8 +8,10 @@
 #include "mojo/public/cpp/bindings/interface_request.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
+#include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/modules/contacts_picker/contact_info.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
 #include "third_party/blink/renderer/platform/heap/visitor.h"
@@ -80,6 +82,15 @@
 
 ScriptPromise ContactsManager::select(ScriptState* script_state,
                                       ContactsSelectOptions* options) {
+  Document* document = To<Document>(ExecutionContext::From(script_state));
+  if (!LocalFrame::HasTransientUserActivation(document ? document->GetFrame()
+                                                       : nullptr)) {
+    return ScriptPromise::Reject(
+        script_state, V8ThrowException::CreateTypeError(
+                          script_state->GetIsolate(),
+                          "A user gesture is required to call this method"));
+  }
+
   if (!options->hasProperties() || !options->properties().size()) {
     return ScriptPromise::Reject(script_state,
                                  V8ThrowException::CreateTypeError(
@@ -114,14 +125,15 @@
 void ContactsManager::OnContactsSelected(
     ScriptPromiseResolver* resolver,
     base::Optional<Vector<mojom::blink::ContactInfoPtr>> contacts) {
+  ScriptState* script_state = resolver->GetScriptState();
+  ScriptState::Scope scope(script_state);
+
   if (!contacts.has_value()) {
-    resolver->Reject(DOMException::Create(DOMExceptionCode::kAbortError,
-                                          "Unable to open a contact selector"));
+    resolver->Reject(V8ThrowException::CreateTypeError(
+        script_state->GetIsolate(), "Unable to open a contact selector"));
     return;
   }
 
-  ScriptState::Scope scope(resolver->GetScriptState());
-
   HeapVector<Member<ContactInfo>> contacts_list;
   for (const auto& contact : *contacts)
     contacts_list.push_back(contact.To<blink::ContactInfo*>());
diff --git a/third_party/blink/web_tests/http/tests/contacts/idl-NavigatorContacts.html b/third_party/blink/web_tests/http/tests/contacts/idl-NavigatorContacts.html
deleted file mode 100644
index b942de5..0000000
--- a/third_party/blink/web_tests/http/tests/contacts/idl-NavigatorContacts.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!DOCTYPE html>
-<script src="../resources/testharness.js"></script>
-<script src="../resources/testharnessreport.js"></script>
-<script>
-test(function() {
-  assert_true('contacts' in navigator,
-              'navigator.contacts exists in navigator.');
-}, 'navigator.contacts IDL test');
-</script>
diff --git a/third_party/blink/web_tests/http/tests/contacts/select-function.html b/third_party/blink/web_tests/http/tests/contacts/select-function.html
index 0fcf211..346017a 100644
--- a/third_party/blink/web_tests/http/tests/contacts/select-function.html
+++ b/third_party/blink/web_tests/http/tests/contacts/select-function.html
@@ -1,13 +1,74 @@
-<!DOCTYPE html>
-<script src="../resources/testharness.js"></script>
-<script src="../resources/testharnessreport.js"></script>
-<script src="resources.js"></script>
-
+<!doctype html>
+<meta charset="utf-8">
+<title>Contact API: Behaviour of the select() function</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
 <script>
-promise_test(function() {
-  return assert_promise_rejects_with_message(
-      navigator.contacts.select({multiple: true, properties: ['name', 'email', 'tel']}),
-      new DOMException('Unable to open a contact selector', 'AbortError'),
-      'navigator.contacts.select');
-}, 'Select function throws exception.');
+'use strict';
+
+// Creates a "user gesture" using Blink's test-only eventSender.
+function triggerUserGesture() {
+  if (!window.eventSender)
+    throw new Error('The `eventSender` must be available for this test');
+
+  eventSender.mouseDown();
+  eventSender.mouseUp();
+}
+
+// Verifies that |func|, when invoked, throws a TypeError exception.
+async function expectTypeError(func) {
+  try {
+    await func();
+  } catch (e) {
+    assert_equals(e.name, 'TypeError');
+    return;
+  }
+
+  assert_unreached('expected a TypeError, but none was thrown');
+}
+
+test(() => {
+  // Exposure of the interface and method.
+  assert_own_property(window, 'ContactsManager');
+  assert_own_property(ContactsManager.prototype, 'select');
+
+  // Exposure of the instance.
+  assert_idl_attribute(navigator, 'contacts');
+  assert_idl_attribute(navigator.contacts, 'select');
+
+}, 'The Contact API is exposed on the Window context');
+
+promise_test(async () => {
+  await expectTypeError(() =>
+      navigator.contacts.select({ properties: ['name'] }));
+
+}, 'The Contact API requires a user gesture')
+
+promise_test(async () => {
+  triggerUserGesture();
+
+  // At least one property must be provided.
+  await expectTypeError(() => navigator.contacts.select());
+  await expectTypeError(() => navigator.contacts.select({ properties: [] }));
+
+  // Per WebIDL parsing, no invalid values may be provided.
+  await expectTypeError(() =>
+      navigator.contacts.select({ properties: [''] }));
+  await expectTypeError(() =>
+      navigator.contacts.select({ properties: ['foo'] }));
+  await expectTypeError(() =>
+      navigator.contacts.select({ properties: ['name', 'photo'] }));
+
+}, 'The Contact API requires at least one valid property to be provided');
+
+promise_test(async () => {
+  triggerUserGesture();
+
+  // TODO(peter): Fake the Mojo interface so that we can extend this test with
+  // valid behaviour, and actually verify the API's functionality.
+  await expectTypeError(() =>
+      navigator.contacts.select({ properties: ['name'] }));
+
+}, 'The Contact API can fail when the selector cannot be opened');
+
 </script>