Commit 13184a2d authored by Zain Afzal's avatar Zain Afzal Committed by Commit Bot

Added message interface between chrome://media-app and chrome://media-app-guest

Todate, we've only passed a single, simple message to open a file from media-app
to media-app-guest. To better support our use-cases we need a bidirectional pipe,
and some abstractions around the dispatching of messages.

This CL introduces a MessagePipe class to encapsulate the communication channel
and manage dispatch of requests and responses between the privileged and
unprivileged JS contexts in chrome://media-app. It still requires explicit
definitions of what messages either side can send and receive but allows for
these messages to be defined and used with less friction.

Bug: 996088, 1040328
Change-Id: I6179a5d176d74f0f37fa1809363e87a0ed2976e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2049404
Commit-Queue: Zain Afzal <zafzal@google.com>
Reviewed-by: default avatarTrent Apted <tapted@chromium.org>
Cr-Commit-Position: refs/heads/master@{#746214}
parent 208ec7b2
...@@ -69,6 +69,7 @@ source_set("browser_test_support") { ...@@ -69,6 +69,7 @@ source_set("browser_test_support") {
"test/driver.js", "test/driver.js",
"test/driver_api.js", "test/driver_api.js",
"test/guest_query_receiver.js", "test/guest_query_receiver.js",
"test/receiver_api.js",
] ]
} }
...@@ -98,6 +99,7 @@ js_type_check("closure_compile_tests") { ...@@ -98,6 +99,7 @@ js_type_check("closure_compile_tests") {
":test_driver_api_js", ":test_driver_api_js",
":test_driver_js", ":test_driver_js",
":test_guest_query_receiver_js", ":test_guest_query_receiver_js",
":test_receiver_api_js",
] ]
} }
...@@ -106,10 +108,19 @@ js_library("test_driver_api_js") { ...@@ -106,10 +108,19 @@ js_library("test_driver_api_js") {
sources = [ "test/driver_api.js" ] sources = [ "test/driver_api.js" ]
} }
js_library("test_receiver_api_js") {
testonly = true
sources = [ "test/receiver_api.js" ]
}
js_library("test_guest_query_receiver_js") { js_library("test_guest_query_receiver_js") {
testonly = true testonly = true
sources = [ "test/guest_query_receiver.js" ] sources = [ "test/guest_query_receiver.js" ]
deps = [ ":test_driver_api_js" ] deps = [
":test_driver_api_js",
":test_receiver_api_js",
"//chromeos/components/media_app_ui/resources/js:message_pipe",
]
} }
js_library("test_driver_js") { js_library("test_driver_js") {
...@@ -117,6 +128,7 @@ js_library("test_driver_js") { ...@@ -117,6 +128,7 @@ js_library("test_driver_js") {
sources = [ "test/driver.js" ] sources = [ "test/driver.js" ]
deps = [ deps = [
":test_driver_api_js", ":test_driver_api_js",
"//chromeos/components/media_app_ui/resources/js:message_pipe",
"//ui/webui/resources/js:assert", "//ui/webui/resources/js:assert",
] ]
} }
...@@ -21,6 +21,7 @@ content::WebUIDataSource* MediaAppGuestUI::CreateDataSource() { ...@@ -21,6 +21,7 @@ content::WebUIDataSource* MediaAppGuestUI::CreateDataSource() {
// Add resources from chromeos_media_app_resources.pak. // Add resources from chromeos_media_app_resources.pak.
source->AddResourcePath("app.html", IDR_MEDIA_APP_APP_HTML); source->AddResourcePath("app.html", IDR_MEDIA_APP_APP_HTML);
source->AddResourcePath("receiver.js", IDR_MEDIA_APP_RECEIVER_JS); source->AddResourcePath("receiver.js", IDR_MEDIA_APP_RECEIVER_JS);
source->AddResourcePath("message_pipe.js", IDR_MEDIA_APP_MESSAGE_PIPE_JS);
// Add resources from chromeos_media_app_bundle_resources.pak that are also // Add resources from chromeos_media_app_bundle_resources.pak that are also
// needed for mocks. If enable_cros_media_app = true, then these calls will // needed for mocks. If enable_cros_media_app = true, then these calls will
......
...@@ -31,6 +31,7 @@ content::WebUIDataSource* CreateHostDataSource() { ...@@ -31,6 +31,7 @@ content::WebUIDataSource* CreateHostDataSource() {
IDR_MEDIA_APP_MOJO_API_BOOTSTRAP_JS); IDR_MEDIA_APP_MOJO_API_BOOTSTRAP_JS);
source->AddResourcePath("media_app.mojom-lite.js", source->AddResourcePath("media_app.mojom-lite.js",
IDR_MEDIA_APP_MEDIA_APP_MOJOM_JS); IDR_MEDIA_APP_MEDIA_APP_MOJOM_JS);
source->AddResourcePath("message_pipe.js", IDR_MEDIA_APP_MESSAGE_PIPE_JS);
// Add resources from chromeos_media_app_bundle_resources.pak. // Add resources from chromeos_media_app_bundle_resources.pak.
source->AddResourcePath("system_assets/app_icon_256.png", source->AddResourcePath("system_assets/app_icon_256.png",
......
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
} }
</style> </style>
<script src="/js/app_main.js"></script> <script src="/js/app_main.js"></script>
<script src="/message_pipe.js"></script>
<script src="/receiver.js"></script> <script src="/receiver.js"></script>
...@@ -61,4 +61,5 @@ ...@@ -61,4 +61,5 @@
<div id="prev-container" class="navigation-button"></div> <div id="prev-container" class="navigation-button"></div>
<div id="next-container" class="navigation-button"></div> <div id="next-container" class="navigation-button"></div>
</div> </div>
<script src="/message_pipe.js"></script>
<script src="/launch.js"></script> <script src="/launch.js"></script>
...@@ -19,6 +19,7 @@ js_type_check("closure_compile") { ...@@ -19,6 +19,7 @@ js_type_check("closure_compile") {
] ]
deps = [ deps = [
":launch", ":launch",
":message_pipe",
":mojo_api_bootstrap", ":mojo_api_bootstrap",
":receiver", ":receiver",
] ]
...@@ -29,10 +30,15 @@ js_library("launch") { ...@@ -29,10 +30,15 @@ js_library("launch") {
"web_app_file_handling.externs.js", "web_app_file_handling.externs.js",
"dom_draft.externs.js", "dom_draft.externs.js",
] ]
deps = [ ":message_pipe" ]
}
js_library("message_pipe") {
} }
js_library("receiver") { js_library("receiver") {
externs_list = [ "media_app.externs.js" ] externs_list = [ "media_app.externs.js" ]
deps = [ ":message_pipe" ]
} }
js_library("mojo_api_bootstrap") { js_library("mojo_api_bootstrap") {
......
...@@ -16,27 +16,16 @@ const currentFiles = []; ...@@ -16,27 +16,16 @@ const currentFiles = [];
*/ */
let entryIndex = -1; let entryIndex = -1;
/** /** A pipe through which we can send messages to the guest frame. */
* Helper to call postMessage on the guest frame. const guestMessagePipe = new MessagePipe('chrome://media-app-guest');
*
* @param {Object} message
*/
function postToGuestWindow(message) {
const guest =
/** @type{HTMLIFrameElement} */ (document.querySelector('iframe'));
// The next line should probably be passing a transfer argument, but that
// causes Chrome to send a "null" message. The transfer seems to work without
// the third argument (but inefficiently, perhaps).
guest.contentWindow.postMessage(message, 'chrome://media-app-guest');
}
/** /**
* Loads a file in the guest. * Loads a file in the guest.
* *
* @param {File} file * @param {File} file
*/ */
async function loadFile(file) { function loadFile(file) {
postToGuestWindow({'file': file}); guestMessagePipe.sendMessage('file', {'file': file});
} }
/** /**
......
// Copyright 2020 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.
/** The data structure the message pipe sends and receives. */
class MessageData {
constructor() {
/**
* The id of the message, this uniquely identifies a message
* and should only appear on the sent message and a response to that
* message.
*
* @type {number}
*/
this.messageId;
/**
* The message type. Indicates the structure of the data in
* `message` and is set to special reserved strings when the message is
* an generated messaged used to communicate between message pipe
* instances.
*
* @type {string}
*/
this.type;
/**
* The message being sent through the pipe, the structure of
* the object sent is implied by the type of the message.
*
* @type {!Object}
*/
this.message;
}
}
/**
* The type of a message handler function which gets called when the message
* pipe receives a message.
*
* @typedef {function(!Object): (!Object|undefined|!Promise<!Object|undefined>)}
*/
let MessageHandler;
/**
* Creates a new JavaScript native Promise and captures its resolve and reject
* callbacks. The promise, resolve, and reject are available as properties.
* Inspired by goog.promise.NativeResolver.
*/
class NativeResolver {
constructor() {
/** @type {function(!Object): void} */
this.resolve;
/** @type {function(!Object): void} */
this.reject;
/** @type {!Promise<!Object>} */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
/**
* Enum for reserved message types used in generated messages.
*
* @enum {string}
*/
const ReservedMessageTypes = {
/**
* Indicates a autogenerated response message for a previously received
* message.
*/
RESPONSE_TYPE: '___response',
/**
* Indicates a autogenerated error message for a previously received
* message.
*/
ERROR_TYPE: '___error'
};
/**
* Checks if a provided message type indicates a generated message.
*
* @param {string} messageType
* @return {boolean}
*/
function isGeneratedMessage(messageType) {
// Any message type with three underscores before it should only be used
// in generated messages.
return messageType.substr(0, 3) === '___';
}
/**
* Checks a message type is not reserved by generated messages, if it is, throws
* a error indicating this to the user.
*
* @param {string} messageType
*/
function throwIfReserved(messageType) {
if (isGeneratedMessage(messageType)) {
throw new Error(`Unexpected reserved message type: '${messageType}'`);
}
}
/**
* The message pipe allows two windows to communicate in 1 direction without
* having to handle the internals. The caller can send messages to the other
* window and receive async responses.
*/
class MessagePipe {
/**
* Constructs a new message pipe to the `target` window which has the
* `targetOrigin` origin.
*
* @param {string} targetOrigin
* @param {!Window=} target If not specified, the document tree will be
* queried for a iframe with src `targetOrigin` to target.
* @param {boolean=} rethrowErrors
*/
constructor(targetOrigin, target, rethrowErrors = true) {
if (!target) {
const frame = /** @type {!HTMLIFrameElement} */ (
document.querySelector(`iframe[src^='${targetOrigin}']`));
if (!frame || !frame.contentWindow) {
throw new Error('Unable to locate target content window.');
}
target = /** @type{!Window} */ (frame.contentWindow);
}
/** @private @const {!Window} */
this.target_ = /** @type {!Window} */ (target);
/** @private @const {string} */
this.targetOrigin_ = targetOrigin;
/**
* If true any errors thrown in a handler during message handling will be
* thrown again in addition to being sent over the pipe to the message
* sender. true by default.
*
* @type {boolean}
*/
this.rethrowErrors = rethrowErrors;
/**
* Maps a message type to a message handler, a function which takes in
* the message and returns a response message or a promise which resolves
* with a response message.
*
* @private @const {!Map<string, !MessageHandler>}
*/
this.messageHandlers_ = new Map();
/**
* Maps a message id to a resolver.
*
* @private @const {!Map<number, !NativeResolver>}
*/
this.pendingMessages_ = new Map();
/**
* The id the next message the object sends will have.
*
* @private
*/
this.nextMessageId_ = 0;
/**
* The message listener we attach to the window. We need a reference to the
* function for later removal.
*
* @private @const {function(!Event): void}
*/
this.messageListener_ = (m) => this.receiveMessage_(m);
// Make sure we aren't trying to send messages to ourselves.
console.assert(this.target_ !== window);
window.addEventListener('message', this.messageListener_);
}
/**
* Registers a handler to be called when a message of type `messageType` is
* received. The return value of this handler will automatically be sent to
* the message source as a response message. If the handler should throw an
* error while handling a message, the error message will be caught and sent
* to the message source automatically.
* NOTE: The message type can not be prefixed with 3 underscores as that is
* reserved for generated messages. i.e `___hello` is disallowed.
*
* @param {string} messageType
* @param {!MessageHandler} handler
*/
registerHandler(messageType, handler) {
throwIfReserved(messageType);
if (this.messageHandlers_.has(messageType)) {
throw new Error(`A handler already exists for ${messageType}`);
}
this.messageHandlers_.set(messageType, handler);
}
/**
* Sends a message to the target window and return a Promise that will resolve
* on response. If the target handler does not send a response the promise
* will resolve with a empty object.
*
* @param {string} messageType
* @param {!Object=} message
* @return {!Promise<!Object>}
*/
sendMessage(messageType, message = {}) {
throwIfReserved(messageType);
const messageId = this.nextMessageId_++;
this.postToTarget_(messageType, message, messageId);
const resolver = new NativeResolver();
this.pendingMessages_.set(messageId, resolver);
return resolver.promise;
}
/**
* Removes all listeners this object attaches to window in preparation for
* destruction.
*/
detach() {
window.removeEventListener('message', this.messageListener_);
}
/**
* Handles a message which represents the targets response to a previously
* sent message.
*
* @private
* @param {string} messageType
* @param {!Object} message
* @param {number} messageId
*/
handleMessageResponse_(messageType, message, messageId) {
const {RESPONSE_TYPE, ERROR_TYPE} = ReservedMessageTypes;
const resolver = this.pendingMessages_.get(messageId);
if (messageType === RESPONSE_TYPE) {
resolver.resolve(message);
} else if (messageType === ERROR_TYPE) {
resolver.reject(message);
} else {
console.error(`Response for message ${
messageId} received with invalid message type ${messageType}`);
}
this.pendingMessages_.delete(messageId);
}
/**
* Calls the relevant handler for a received message and generates the right
* response message to send back to the source.
*
* @private
* @param {string} messageType
* @param {!Object} message
* @param {number} messageId
* @return {!Promise<void>}
*/
async callHandlerForMessageType_(messageType, message, messageId) {
const {RESPONSE_TYPE, ERROR_TYPE} = ReservedMessageTypes;
/** @type {!Object|undefined} */
let response;
/** @type {?Error} */
let error = null;
try {
response = await this.messageHandlers_.get(messageType)(message);
} catch (/** @type {!Error} */ err) {
error = err;
// If an error happened capture the error and send it back.
response = {message: error.message || ''};
}
this.postToTarget_(error ? ERROR_TYPE : RESPONSE_TYPE, response, messageId);
if (error && this.rethrowErrors) {
// Rethrow the error so the current frame has visibility on its handler
// failures.
throw error;
}
}
/**
* @private
* @param {!Event} event
*/
receiveMessage_(event) {
const e = /** @type{!MessageEvent<!MessageData>} */ (event);
const {messageId, type, message} = e.data;
const {ERROR_TYPE} = ReservedMessageTypes;
// Ignore any messages that are not from the target origin unless we are
// explicitly accepting messages from any origin.
if (e.origin !== this.targetOrigin_ && this.targetOrigin_ !== '*') {
return;
}
// The case that the message is a response to a previously sent message.
if (isGeneratedMessage(type) && this.pendingMessages_.has(messageId)) {
this.handleMessageResponse_(type, message, messageId);
return;
}
if (isGeneratedMessage(type)) {
// Currently all generated messages are only sent in a response, so should
// have been handled above.
console.error(`Response with type ${type} for unknown message id ${
messageId} received.`);
return;
}
if (!this.messageHandlers_.has(type)) {
// If there is no listener for this event send a error message to source.
this.postToTarget_(
ERROR_TYPE,
{message: `No handler registered for message type '${type}'`},
messageId);
return;
}
this.callHandlerForMessageType_(type, message, messageId);
}
/**
* @private
* @param {string} messageType
* @param {!Object|undefined} message
* @param {number} messageId
*/
postToTarget_(messageType, message, messageId) {
// Message must be an object.
if (!message) {
message = {};
}
const messageWrapper = {messageId, type: messageType, message};
// The next line should probably be passing a transfer argument, but that
// causes Chrome to send a "null" message. The transfer seems to work
// without the third argument (but inefficiently, perhaps).
this.target_.postMessage(messageWrapper, this.targetOrigin_);
}
}
...@@ -15,6 +15,16 @@ class SingleArrayBufferFileList { ...@@ -15,6 +15,16 @@ class SingleArrayBufferFileList {
} }
} }
/** A pipe through which we can send messages to the parent frame. */
const parentMessagePipe = new MessagePipe('chrome://media-app', window.parent);
parentMessagePipe.registerHandler('file', (message) => {
const fileMessage = /** @type mediaApp.MessageEventData */ (message);
if (fileMessage.file) {
loadFile(fileMessage.file);
}
})
/** /**
* Loads files associated with a message received from the host. * Loads files associated with a message received from the host.
* @param {!File} file * @param {!File} file
...@@ -30,32 +40,12 @@ async function loadFile(file) { ...@@ -30,32 +40,12 @@ async function loadFile(file) {
const app = /** @type {?mediaApp.ClientApi} */ ( const app = /** @type {?mediaApp.ClientApi} */ (
document.querySelector('backlight-app')); document.querySelector('backlight-app'));
if (app) { if (app) {
app.loadFiles(fileList); await app.loadFiles(fileList);
} else { } else {
window.customLaunchData = {files: fileList}; window.customLaunchData = {files: fileList};
} }
} }
function receiveMessage(/** Event */ e) {
const event = /** @type{MessageEvent<Object>} */ (e);
if (event.origin !== 'chrome://media-app') {
return;
}
// First ensure the message is our MessageEventData type, then act on it
// appropriately. Note test messages won't have a file (and are not handled by
// this listener), so it's currently sufficient to just check for `file`.
if ('file' in event.data) {
const message =
/** @type{MessageEvent<mediaApp.MessageEventData>}*/ (event);
if (message.data.file) {
loadFile(message.data.file);
} else {
console.error('Unknown message:', message);
}
}
}
// Attempting to execute chooseFileSystemEntries is guaranteed to result in a // Attempting to execute chooseFileSystemEntries is guaranteed to result in a
// SecurityError due to the fact that we are running in a unprivileged iframe. // SecurityError due to the fact that we are running in a unprivileged iframe.
// Note, we can not do window.chooseFileSystemEntries due to the fact that // Note, we can not do window.chooseFileSystemEntries due to the fact that
...@@ -63,5 +53,3 @@ function receiveMessage(/** Event */ e) { ...@@ -63,5 +53,3 @@ function receiveMessage(/** Event */ e) {
// TODO(crbug/1040328): Remove this when we have a polyfill that allows us to // TODO(crbug/1040328): Remove this when we have a polyfill that allows us to
// talk to the privileged frame. // talk to the privileged frame.
window['chooseFileSystemEntries'] = null; window['chooseFileSystemEntries'] = null;
window.addEventListener('message', receiveMessage, false);
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
<!-- Unprivileged guest contents. --> <!-- Unprivileged guest contents. -->
<include name="IDR_MEDIA_APP_APP_HTML" file="app.html" type="BINDATA" /> <include name="IDR_MEDIA_APP_APP_HTML" file="app.html" type="BINDATA" />
<include name="IDR_MEDIA_APP_RECEIVER_JS" file="js/receiver.js" type="BINDATA" /> <include name="IDR_MEDIA_APP_RECEIVER_JS" file="js/receiver.js" type="BINDATA" />
<!-- Common contents. -->
<include name="IDR_MEDIA_APP_MESSAGE_PIPE_JS" file="js/message_pipe.js" type="BINDATA" />
</includes> </includes>
</release> </release>
</grit> </grit>
...@@ -4,51 +4,6 @@ ...@@ -4,51 +4,6 @@
/** Host-side of web-driver like controller for sandboxed guest frames. */ /** Host-side of web-driver like controller for sandboxed guest frames. */
class GuestDriver { class GuestDriver {
/** @param {string} origin */
constructor(origin) {
this.origin = origin;
/** @type {Array<MessageEvent<TestMessageResponseData>>} */
this.testMessageQueue = [];
/** @type {?function(MessageEvent<TestMessageResponseData>)} */
this.testMessageWaiter = null;
this.messageListener = (/** Event */ event) => {
const testEvent =
/** @type{MessageEvent<TestMessageResponseData>} */ (event);
console.log('Event from guest: ' + JSON.stringify(testEvent.data));
if (this.testMessageWaiter) {
this.testMessageWaiter(testEvent);
this.testMessageWaiter = null;
} else {
this.testMessageQueue.push(testEvent);
}
};
window.addEventListener('message', this.messageListener);
}
tearDown() {
window.removeEventListener('message', this.messageListener);
}
/**
* Returns the next message from the guest.
* @return {Promise<MessageEvent<TestMessageResponseData>>}
*/
popTestMessage() {
if (this.testMessageQueue.length > 0) {
const front = this.testMessageQueue[0];
this.testMessageQueue.shift();
return Promise.resolve(front);
}
return new Promise(resolve => {
this.testMessageWaiter = resolve;
});
}
/** /**
* Sends a query to the guest that repeatedly runs a query selector until * Sends a query to the guest that repeatedly runs a query selector until
* it returns an element. * it returns an element.
...@@ -60,13 +15,12 @@ class GuestDriver { ...@@ -60,13 +15,12 @@ class GuestDriver {
* tagName if unspecified. * tagName if unspecified.
*/ */
async waitForElementInGuest(query, opt_property, opt_commands = {}) { async waitForElementInGuest(query, opt_property, opt_commands = {}) {
const frame = assertInstanceof(
document.querySelector(`iframe[src^="${this.origin}"]`),
HTMLIFrameElement);
/** @type{TestMessageQueryData} */ /** @type{TestMessageQueryData} */
const message = {testQuery: query, property: opt_property}; const message = {testQuery: query, property: opt_property};
frame.contentWindow.postMessage({...message, ...opt_commands}, this.origin);
const event = await this.popTestMessage(); const result = /** @type{TestMessageResponseData} */ (
return event.data.testQueryResult; await guestMessagePipe.sendMessage(
'test', {...message, ...opt_commands}));
return result.testQueryResult;
} }
} }
...@@ -13,3 +13,6 @@ var TestMessageResponseData; ...@@ -13,3 +13,6 @@ var TestMessageResponseData;
* requestFullscreen: (boolean|undefined)}} * requestFullscreen: (boolean|undefined)}}
*/ */
var TestMessageQueryData; var TestMessageQueryData;
/** @type {MessagePipe} */
var guestMessagePipe;
\ No newline at end of file
...@@ -40,12 +40,13 @@ async function waitForNode(query, opt_path) { ...@@ -40,12 +40,13 @@ async function waitForNode(query, opt_path) {
/** /**
* Acts on received TestMessageQueryData. * Acts on received TestMessageQueryData.
* *
* @param {MessageEvent<TestMessageQueryData>} event * @param {TestMessageQueryData} data
* @return {!Promise<TestMessageResponseData>}
*/ */
async function runTestQuery(event) { async function runTestQuery(data) {
const data = event.data;
const element = await waitForNode(data.testQuery, data.pathToRoot || []); const element = await waitForNode(data.testQuery, data.pathToRoot || []);
let result = element.tagName; let result = element.tagName;
if (data.property) { if (data.property) {
result = JSON.stringify(element[data.property]); result = JSON.stringify(element[data.property]);
} else if (data.requestFullscreen) { } else if (data.requestFullscreen) {
...@@ -57,17 +58,23 @@ async function runTestQuery(event) { ...@@ -57,17 +58,23 @@ async function runTestQuery(event) {
} }
} }
const response = {testQueryResult: result}; return {testQueryResult: result};
event.source.postMessage(response, event.origin);
}
function receiveTestMessage(/** Event */ e) {
const event = /** @type{MessageEvent<TestMessageQueryData>} */ (e);
if (event.data.testQuery) {
runTestQuery(event);
}
} }
window.addEventListener('message', receiveTestMessage, false); // Wait until dom content has loaded to make sure receiver.js has been
// parsed and executed.
window.addEventListener('DOMContentLoaded', () => {
parentMessagePipe.registerHandler('test', (data) => {
return runTestQuery(/** @type{TestMessageQueryData} */ (data));
});
// Turn off error rethrowing for tests so the test runner doesn't mark
// our error handling tests as failed.
parentMessagePipe.rethrowErrors = false;
// Handler that will always error for helping to test the message pipe
// itself.
parentMessagePipe.registerHandler('bad-handler', () => {
throw Error('This is an error');
});
});
//# sourceURL=guest_query_reciever.js //# sourceURL=guest_query_receiver.js
...@@ -28,9 +28,14 @@ var MediaAppGuestUIBrowserTest = class extends testing.Test { ...@@ -28,9 +28,14 @@ var MediaAppGuestUIBrowserTest = class extends testing.Test {
preLoad() { preLoad() {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mojoBindingsLite = document.createElement('script'); const mojoBindingsLite = document.createElement('script');
mojoBindingsLite.src = 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js'; mojoBindingsLite.src =
'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js';
document.head.appendChild(mojoBindingsLite); document.head.appendChild(mojoBindingsLite);
}); });
// The guest will try and create a message pipe to its parent, since there
// is no containing frame here, window.parent === parent. This line mocks
// a wrapping iframe so the message pipe can still instantiate correctly.
window.parent = document.createElement('iframe');
} }
}; };
......
...@@ -61,12 +61,7 @@ var MediaAppUIBrowserTest = class extends testing.Test { ...@@ -61,12 +61,7 @@ var MediaAppUIBrowserTest = class extends testing.Test {
/** @override */ /** @override */
setUp() { setUp() {
super.setUp(); super.setUp();
driver = new GuestDriver(GUEST_ORIGIN); driver = new GuestDriver();
}
/** @override */
tearDown() {
driver.tearDown();
} }
}; };
...@@ -144,3 +139,31 @@ TEST_F('MediaAppUIBrowserTest', 'CanFullscreenVideo', async () => { ...@@ -144,3 +139,31 @@ TEST_F('MediaAppUIBrowserTest', 'CanFullscreenVideo', async () => {
testDone(); testDone();
}); });
// Tests that we receive an error if our message is unhandled.
TEST_F('MediaAppUIBrowserTest', 'ReceivesNoHandlerError', async () => {
let errorMessage = '';
try {
await guestMessagePipe.sendMessage('unknown-message', null);
} catch (error) {
errorMessage = error.message;
}
assertEquals(
errorMessage,
'No handler registered for message type \'unknown-message\'');
testDone();
});
// Tests that we receive an error if the handler fails.
TEST_F('MediaAppUIBrowserTest', 'ReceivesProxiedError', async () => {
let errorMessage = '';
try {
await guestMessagePipe.sendMessage('bad-handler', null);
} catch (error) {
errorMessage = error.message;
}
assertEquals(errorMessage, 'This is an error');
testDone();
});
// Copyright 2020 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.
/** @type {!MessagePipe} */
var parentMessagePipe;
\ No newline at end of file
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