Commit fb23f35f authored by Rayan Kanso's avatar Rayan Kanso Committed by Commit Bot

[Contacts] Add contacts wpt tests with a mock contacts implementation.

- Create a mocked contacts interface that requires user agents to set
the contacts to return when queried.
- Implement the mocked contacts interface for chromium.
- Move chromium web_tests to the wpt folder. I've added one test and
removed one test.

Change-Id: I8fe518eef37eedf2908b4b0ea9bec9df9ea4938f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1741915
Commit-Queue: Rayan Kanso <rayankans@chromium.org>
Reviewed-by: default avatarPeter Beverloo <peter@chromium.org>
Cr-Commit-Position: refs/heads/master@{#685182}
parent ebe4f13e
...@@ -84,11 +84,20 @@ ScriptPromise ContactsManager::select(ScriptState* script_state, ...@@ -84,11 +84,20 @@ ScriptPromise ContactsManager::select(ScriptState* script_state,
const Vector<String>& properties, const Vector<String>& properties,
ContactsSelectOptions* options) { ContactsSelectOptions* options) {
Document* document = To<Document>(ExecutionContext::From(script_state)); Document* document = To<Document>(ExecutionContext::From(script_state));
if (document->ParentDocument()) {
return ScriptPromise::RejectWithDOMException(
script_state,
MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"The contacts API can only be used in the top frame"));
}
if (!LocalFrame::HasTransientUserActivation(document ? document->GetFrame() if (!LocalFrame::HasTransientUserActivation(document ? document->GetFrame()
: nullptr)) { : nullptr)) {
return ScriptPromise::Reject( return ScriptPromise::RejectWithDOMException(
script_state, V8ThrowException::CreateTypeError( script_state, MakeGarbageCollected<DOMException>(
script_state->GetIsolate(), DOMExceptionCode::kSecurityError,
"A user gesture is required to call this method")); "A user gesture is required to call this method"));
} }
......
finnur@chromium.org finnur@chromium.org
peter@chromium.org peter@chromium.org
rayankans@chromium.org
# TEAM: platform-capabilities@chromium.org # TEAM: platform-capabilities@chromium.org
# COMPONENT: Blink>Contacts # COMPONENT: Blink>Contacts
\ No newline at end of file
<!doctype html> // META: script=/resources/testdriver.js
<meta charset="utf-8"> // META: script=/resources/testdriver-vendor.js
<title>Contact API: Behaviour of the select() function</title> // META: script=resources/helpers.js
<script src="/gen/layout_test_data/mojo/public/js/mojo_bindings.js"></script>
<script src="/gen/third_party/blink/public/mojom/contacts/contacts_manager.mojom.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script src="resources/mock_contacts_manager.js"></script>
<script>
'use strict'; 'use strict';
// Verifies that |func|, when invoked, throws a TypeError exception. // Verifies that |func|, when invoked, throws a TypeError exception.
...@@ -22,26 +15,16 @@ async function expectTypeError(func) { ...@@ -22,26 +15,16 @@ async function expectTypeError(func) {
assert_unreached('expected a TypeError, but none was thrown'); 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 () => { promise_test(async () => {
await expectTypeError(() => try {
navigator.contacts.select(['name'])); await navigator.contacts.select(['name']);
assert_unreached('expected a SecurityError, but none was thrown');
} catch (e) {
assert_equals(e.name, 'SecurityError');
}
}, 'The Contact API requires a user gesture') }, 'The Contact API requires a user gesture')
promise_test(async () => { contactsTestWithUserActivation(async (test, setSelectedContacts) => {
triggerUserGesture();
// At least one property must be provided. // At least one property must be provided.
await expectTypeError(() => navigator.contacts.select()); await expectTypeError(() => navigator.contacts.select());
await expectTypeError(() => navigator.contacts.select([])); await expectTypeError(() => navigator.contacts.select([]));
...@@ -56,122 +39,82 @@ promise_test(async () => { ...@@ -56,122 +39,82 @@ promise_test(async () => {
}, 'The Contact API requires valid properties to be provided'); }, 'The Contact API requires valid properties to be provided');
promise_test(async () => { contactsTestWithUserActivation(async (test, setSelectedContacts) => {
triggerUserGesture();
// Returns a NULL result, indicating that no results are available. // Returns a NULL result, indicating that no results are available.
mockContactsManager.setSelectCallback(async (options) => { setSelectedContacts(null);
return { contacts: null };
});
await expectTypeError(() => navigator.contacts.select(['name'])); await expectTypeError(() => navigator.contacts.select(['name']));
}, 'The Contact API can fail when the selector cannot be opened'); }, 'The Contact API can fail when the selector cannot be opened');
promise_test(async () => { contactsTestWithUserActivation(async (test, setSelectedContacts) => {
triggerUserGesture();
let storedOptions = null;
// Stores the |options| in |storedOptions| for inspection after the call
// completes. Deliberately fails the call with no data.
mockContactsManager.setSelectCallback(async (options) => {
storedOptions = options;
return { contacts: null };
});
await expectTypeError(() => {
return navigator.contacts.select(['name', 'email'], { multiple: true });
});
assert_not_equals(storedOptions, null);
assert_true(storedOptions.multiple);
assert_true(storedOptions.includeNames);
assert_true(storedOptions.includeEmails);
assert_false(storedOptions.includeTel);
}, 'The Contact API correctly interprets the JavaScript arguments');
promise_test(async () => {
triggerUserGesture();
// Returns two contacts with all information available. // Returns two contacts with all information available.
mockContactsManager.setSelectCallback(async (options) => { setSelectedContacts([
return { { name: ['Dwight Schrute'], email: ['dwight@schrutefarmsbnb.com'], tel: ['000-0000'] },
contacts: [ { name: ['Michael Scott', 'Prison Mike'], email: ['michael@dundermifflin.com'], tel: [] },
{ name: ['Finnur'], email: ['finnur@chromium.org'], tel: ['000-0000'] }, ]);
{ name: ['Peter', 'Test'], email: ['peter@chromium.org'], tel: [] },
]
};
});
const results = await navigator.contacts.select(['name', 'email', 'tel'], { multiple: true });
let results = await navigator.contacts.select(['name', 'email', 'tel'], { multiple: true });
assert_equals(results.length, 2); assert_equals(results.length, 2);
results = results.sort((c1, c2) => JSON.stringify(c1) < JSON.stringify(c2) ? -1 : 1);
{ {
const finnur = results[0]; const dwight = results[0];
assert_own_property(finnur, 'name'); assert_own_property(dwight, 'name');
assert_own_property(finnur, 'email'); assert_own_property(dwight, 'email');
assert_own_property(finnur, 'tel'); assert_own_property(dwight, 'tel');
assert_array_equals(finnur.name, ['Finnur']); assert_array_equals(dwight.name, ['Dwight Schrute']);
assert_array_equals(finnur.email, ['finnur@chromium.org']); assert_array_equals(dwight.email, ['dwight@schrutefarmsbnb.com']);
assert_array_equals(finnur.tel, ['000-0000']); assert_array_equals(dwight.tel, ['000-0000']);
} }
{ {
const peter = results[1]; const michael = results[1];
assert_own_property(peter, 'name'); assert_own_property(michael, 'name');
assert_own_property(peter, 'email'); assert_own_property(michael, 'email');
assert_own_property(peter, 'tel'); assert_own_property(michael, 'tel');
assert_array_equals(peter.name, ['Peter', 'Test']); assert_array_equals(michael.name, ['Michael Scott', 'Prison Mike']);
assert_array_equals(peter.email, ['peter@chromium.org']); assert_array_equals(michael.email, ['michael@dundermifflin.com']);
assert_array_equals(peter.tel, []); assert_array_equals(michael.tel, []);
} }
}, 'The Contact API correctly returns ContactInfo entries'); }, 'The Contact API correctly returns ContactInfo entries');
promise_test(async () => { contactsTestWithUserActivation(async (test, setSelectedContacts) => {
triggerUserGesture(); // Returns two contacts with all information available.
setSelectedContacts([
{ name: ['Dwight Schrute'], email: ['dwight@schrutefarmsbnb.com'], tel: ['000-0000'] },
{ name: ['Michael Scott', 'Prison Mike'], email: ['michael@dundermifflin.com'], tel: [] },
]);
const results = await navigator.contacts.select(['name', 'email', 'tel']);
assert_equals(results.length, 1);
}, 'Only one contact is returned if `multiple` is not set.');
contactsTestWithUserActivation(async (test, setSelectedContacts) => {
// Returns partial information since no e-mail addresses are requested. // Returns partial information since no e-mail addresses are requested.
mockContactsManager.setSelectCallback(async (options) => { setSelectedContacts([{ name: ['Creed'], email: ['creedthoughts@www.creedthoughts.gov.www'], tel: [] }]);
return {
contacts: [{ name: ['Tim'], email: null, tel: null }]
};
});
const results = await navigator.contacts.select(['name']); const results = await navigator.contacts.select(['name']);
assert_equals(results.length, 1); assert_equals(results.length, 1);
{ {
const tim = results[0]; const creed = results[0];
assert_own_property(tim, 'name');
assert_false('email' in tim);
assert_false('tel' in tim);
assert_array_equals(tim.name, ['Tim']); assert_array_equals(creed.name, ['Creed']);
assert_equals(tim.email, undefined); assert_equals(creed.email, undefined);
assert_equals(tim.tel, undefined); assert_equals(creed.tel, undefined);
} }
}, 'The Contact API does not include fields that were not requested'); }, 'The Contact API does not include fields that were not requested');
promise_test(async () => { contactsTestWithUserActivation(async (test, setSelectedContacts) => {
triggerUserGesture();
// Returns partial information since no e-mail addresses are requested. // Returns partial information since no e-mail addresses are requested.
mockContactsManager.setSelectCallback(async (options) => { setSelectedContacts([{ name: ['Kelly'], email: [], tel: [] }]);
return {
contacts: [{ name: ['Tim'], email: null, tel: null }]
};
});
// First request should work. // First request should work.
const promise1 = new Promise((resolve, reject) => { const promise1 = new Promise((resolve, reject) => {
...@@ -182,15 +125,24 @@ promise_test(async () => { ...@@ -182,15 +125,24 @@ promise_test(async () => {
// Second request should fail (since the first one didn't complete yet). // Second request should fail (since the first one didn't complete yet).
const promise2 = new Promise((resolve, reject) => { const promise2 = new Promise((resolve, reject) => {
navigator.contacts.select(['name']).then(contacts => reject('This was supposed to fail')) navigator.contacts.select(['name']).then(contacts => reject('This was supposed to fail'))
.catch(e => resolve(e.message)); .catch(e => resolve(e.name));
}); });
const results = await Promise.all([promise1, promise2]); const results = await Promise.all([promise1, promise2]);
const contacts = results[0]; const contacts = results[0];
assert_equals(contacts.length, 1); assert_equals(contacts.length, 1);
const contact = contacts[0]; const contact = contacts[0];
assert_equals(contact.name[0], 'Tim'); assert_equals(contact.name[0], 'Kelly');
assert_equals(results[1], 'Contacts Picker is already in use.'); assert_equals(results[1], 'InvalidStateError');
}, 'The Contact API cannot be used again until the first operation is complete.'); }, 'The Contact API cannot be used again until the first operation is complete.');
</script> contactsTestWithUserActivation(async (test, setSelectedContacts) => {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.src = 'resources/non-main-frame-select.html';
await new Promise(resolve => window.addEventListener('message', event => resolve(event.data)))
.then(data => assert_equals(data.errorMsg, 'InvalidStateError'))
.finally(() => iframe.remove())
}, 'Test contacts.select() throws an InvalidStateError in a sub-frame');
'use strict';
// These tests rely on the User Agent providing an implementation of
// platform contacts backends.
//
// 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 loadChromiumResources = async () => {
if (!window.MojoInterfaceInterceptor) {
// Do nothing on non-Chromium-based browsers or when the Mojo bindings are
// not present in the global namespace.
return;
}
const resources = [
'/gen/layout_test_data/mojo/public/js/mojo_bindings.js',
'/gen/third_party/blink/public/mojom/contacts/contacts_manager.mojom.js',
'/resources/chromium/contacts_manager_mock.js',
];
await Promise.all(resources.map(path => {
const script = document.createElement('script');
script.src = path;
script.async = false;
const promise = new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
document.head.appendChild(script);
return promise;
}));
};
// User Agents must provide their own implementation of `WebContacts`,
// which must contain the following this interface:
// class WebContactsTest {
// /** @param {?Array<!ContactInfo>} contacts */
// setSelectedContacts(contacts);
// }
async function createWebContactsTest() {
if (typeof WebContactsTest === 'undefined') {
await loadChromiumResources();
}
assert_true(
typeof WebContactsTest !== 'undefined',
'Mojo testing interface is not available.'
);
return new WebContactsTest();
}
// Creates a Promise test for |func| given the |description|. The |func| will
// be executed with `setSelectedContacts` which will allow tests to mock out
// the result of calling navigator.contacts.select. `setSelectedContacts`
// accepts a nullable Array of ContactInfos.
function contactsTestWithUserActivation(func, description) {
promise_test(async test => {
const webContactsTest = await createWebContactsTest();
await window.test_driver.bless('request contacts');
return func(test, contacts => webContactsTest.setSelectedContacts(contacts));
}, description);
}
<script>
'use strict';
window.onload = function() {
navigator.contacts.select(['name', 'email'], { multiple: true })
.then(results => parent.postMessage({ errorMsg: '' }, '*'))
.catch(exception => parent.postMessage({ errorMsg: exception.name }, '*'));
}
</script>
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
const WebContactsTest = (() => {
class MockContacts {
constructor() {
this.bindingSet_ = new mojo.BindingSet(blink.mojom.ContactsManager);
this.interceptor_ = new MojoInterfaceInterceptor(
blink.mojom.ContactsManager.name);
this.interceptor_.oninterfacerequest =
e => this.bindingSet_.addBinding(this, e.handle);
this.interceptor_.start();
this.selectedContacts_ = [];
}
async select(multiple, includeNames, includeEmails, includeTel) {
if (this.selectedContacts_ === null)
return {contacts: null};
const contactInfos = this.selectedContacts_.map(contact => {
const contactInfo = new blink.mojom.ContactInfo();
if (includeNames)
contactInfo.name = contact.name;
if (includeEmails)
contactInfo.email = contact.email;
if (includeTel)
contactInfo.tel = contact.tel;
return contactInfo;
});
if (!contactInfos.length) return {contacts: []};
if (!multiple) return {contacts: [contactInfos[0]]};
return {contacts: contactInfos};
}
setSelectedContacts(contacts) {
this.selectedContacts_ = contacts;
}
reset() {
this.bindingSet_.closeAllBindings();
this.interceptor_.stop();
}
}
const mockContacts = new MockContacts();
class ContactsTestChromium {
constructor() {
Object.freeze(this); // Make it immutable.
}
setSelectedContacts(contacts) {
mockContacts.setSelectedContacts(contacts);
}
}
return ContactsTestChromium;
})();
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
// Function to test that a promise rejects with the expected error type and
// message.
function assert_promise_rejects_with_message(promise, expected, description) {
return promise.then(() => {
assert_unreached('Promise should have rejected: ' + description);
}, error => {
assert_equals(error.name, expected.name, 'Unexpected Error Name:');
if (expected.message) {
assert_equals(error.message, expected.message, 'Unexpected Error Message:');
}
});
}
'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();
}
'use strict';
// Mock implementation of the blink.mojom.ContactsManager interface.
class MockContactsManager {
constructor() {
this.selectCallback_ = null;
this.binding_ = new mojo.Binding(blink.mojom.ContactsManager, this);
this.interceptor_ = new MojoInterfaceInterceptor(
blink.mojom.ContactsManager.name);
this.interceptor_.oninterfacerequest = e => this.binding_.bind(e.handle);
this.interceptor_.start();
}
// Sets |callback| to be invoked when the ContactsManager.Select() method is
// being called over the Mojo connection.
setSelectCallback(callback) {
this.selectCallback_ = callback;
}
async select(multiple, includeNames, includeEmails, includeTel) {
if (this.selectCallback_) {
return this.selectCallback_(
{ multiple, includeNames, includeEmails, includeTel });
}
return { contacts: null };
}
}
const mockContactsManager = new MockContactsManager();
<script>
'use strict';
window.onload = function() {
navigator.contacts.select(['name', 'email'], { multiple: true })
.then(results => parent.postMessage({ errorMsg: '' }, '*'))
.catch(exception => parent.postMessage({ errorMsg: exception.toString() }, '*'));
}
</script>
<!doctype html>
<meta charset="utf-8">
<title>Contact API: select() restricted to main frame</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<iframe></iframe>
<script>
'use strict';
promise_test(async () => {
triggerUserGesture();
var iframe = document.getElementsByTagName('iframe')[0];
iframe.src = "resources/non-main-frame-select.html";
return new Promise(function(resolve, reject) {
window.addEventListener('message', event => resolve(event.data));
}).then(data => {
assert_equals(data.errorMsg,
'TypeError: Unable to open a contact selector');
});
}, 'Test contacts.select() on a sub-frame')
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment