// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var g_webview = null;
var embedder = {};
var seenFocusCount = 0;
embedder.tests = {};
embedder.guestURL =
'data:text/html,<html><body>Guest<body></html>';
var g_inputMethodTestHelper = null;
var g_focusRestoredTestHelper = null;
window.runTest = function(testName) {
if (!embedder.test.testList[testName]) {
console.log('Incorrect testName: ' + testName);
embedder.test.fail();
return;
}
// Run the test.
embedder.test.testList[testName]();
};
window.runCommand = function(command, opt_step) {
window.console.log('window.runCommand: ' + command);
switch (command) {
case 'testFocusTracksEmbedderRunNextStep':
testFocusTracksEmbedderRunNextStep();
break;
case 'testInputMethodRunNextStep':
testInputMethodRunNextStep(opt_step);
break;
case 'testFocusRestoredRunNextStep':
testFocusRestoredRunNextStep(opt_step);
break;
case 'testKeyboardFocusRunNextStep':
testKeyboardFocusRunNextStep(opt_step);
break;
case 'monitorGuestEvent':
monitorGuestEvent(opt_step);
case 'waitGuestEvent':
waitGuestEvent(opt_step);
default:
embedder.test.fail();
}
};
// window.* exported functions end.
var LOG = function(msg) {
window.console.log(msg);
};
embedder.test = {};
embedder.test.succeed = function() {
chrome.test.sendMessage('TEST_PASSED');
};
embedder.test.fail = function() {
chrome.test.sendMessage('TEST_FAILED');
};
embedder.test.assertEq = function(a, b) {
if (a != b) {
console.log('assertion failed: ' + a + ' != ' + b);
embedder.test.fail();
}
};
embedder.test.assertTrue = function(condition) {
if (!condition) {
console.log('assertion failed: true != ' + condition);
embedder.test.fail();
}
};
embedder.test.assertFalse = function(condition) {
if (condition) {
console.log('assertion failed: false != ' + condition);
embedder.test.fail();
}
};
/** @private */
embedder.setUpGuest_ = function() {
document.querySelector('#webview-tag-container').innerHTML =
'<webview style="width: 100px; height: 100px;"></webview>';
var webview = document.querySelector('webview');
if (!webview) {
embedder.test.fail('No <webview> element created');
}
return webview;
};
/**
* @private
*
* A test helper for focus related tests.
* It does the following steps:
* 1. It navigates |webview|.
* 2. On 'loadstop', it injects the script |inject_js_guest_url|.
* 3. When the injection has completed, it sends a ['connect'] message to the
* guest to initiate a two-way communication channel.
* 4. When the two way channel has been established |channelCreationCallback| is
* called.
* 5. It ignores all messages from the guest until it gets an
* |expectedResponse|.
* If there is no |expectedResponse|, the method is done.
* 6. Once the expected result is received, call |responseCallback|.
*/
embedder.waitForResponseFromGuest_ =
function(webview,
inject_js_guest_url,
channelCreationCallback,
expectedResponse,
responseCallback) {
var onPostMessageReceived = function(e) {
var data = JSON.parse(e.data);
var response = data[0];
if (response == 'connected') {
channelCreationCallback(webview);
if (!expectedResponse) {
// We are done.
window.removeEventListener('message', onPostMessageReceived);
}
return;
}
if (response != expectedResponse) {
return;
}
responseCallback(data);
window.removeEventListener('message', onPostMessageReceived);
};
window.addEventListener('message', onPostMessageReceived);
webview.addEventListener('consolemessage', function(e) {
LOG('g: ' + e.message);
});
var onWebViewLoadStop = function(e) {
console.log('loadstop');
webview.executeScript(
{file: inject_js_guest_url},
function(results) {
console.log('Injected script into webview.');
// Establish a communication channel with the webview1's guest.
var msg = ['connect'];
webview.contentWindow.postMessage(JSON.stringify(msg), '*');
});
webview.removeEventListener('loadstop', onWebViewLoadStop);
};
webview.addEventListener('loadstop', onWebViewLoadStop);
webview.src = embedder.guestURL;
};
// Helper class for testFocusRestored.
//
// This test has multiple steps, WebViewTest instructs this test to advance to
// each step and then performs some action and verification and runs the next
// step.
// See WebViewInteractiveTest.Focus_FocusRestored for details.
function FocusRestoredTestHelper() {
// Total number of steps for this test that we run thru
// testFocusRestoredRunNextStep.
this.TOTAL_STEPS = 3;
// Currently running step index.
this.step_ = 0;
this.messageHandlerRegistered_ = false;
this.doneCallback_ = null;
}
FocusRestoredTestHelper.prototype.runStep = function(step, doneCallback) {
LOG('runStep: ' + step);
this.doneCallback_ = doneCallback;
if (step != this.step_ + 1 || step < 0 || step > this.TOTAL_STEPS) {
LOG('Incorrect step, expected:', this.step_ + 1, 'got', step);
this.passStep_(false);
return;
}
this.step_ = step;
if (!this.messageHandlerRegistered_) {
this.messageHandlerRegistered_ = true;
window.addEventListener('message', this.messageHandler_.bind(this));
}
var msgToSend = '';
if (step == 1) {
msgToSend = 'request-waitForFocus';
} else if (step == 2) {
msgToSend = 'request-waitForBlur';
} else if (step == 3) {
msgToSend = 'request-waitForFocusAgain';
}
if (!msgToSend) {
this.passStep_(false);
return;
}
g_webview.contentWindow.postMessage(JSON.stringify([msgToSend]), '*');
};
FocusRestoredTestHelper.prototype.messageHandler_ = function(e) {
var data = JSON.parse(e.data);
LOG('FocusRestoredTestHelper.message, data: ' + data);
switch (this.step_) {
case 1:
this.passStep_(data[0] == 'response-focus');
break;
case 2:
this.passStep_(data[0] == 'response-blur');
g_webview.focus();
break;
case 3:
this.passStep_(data[0] == 'response-focusAgain');
break;
default:
LOG('Unexpected message: ' + data);
this.passStep_(false);
}
};
FocusRestoredTestHelper.prototype.passStep_ = function(passed) {
if (!this.doneCallback_) {
LOG('Expected doneCallback_ in FocusRestoredTestHelper');
embedder.test.fail();
return;
}
this.doneCallback_(passed);
};
// Helper class for testInputMethod.
// This test has multiple steps, WebViewTest instructs this test to advance to
// each step and then performs some action and verification and runs the next
// step.
//
// See WebViewInteractiveTest.Focus_InputMethod for details about these steps.
function InputMethodTestHelper() {
// Total number of steps for this test that we run thru
// testInputMethodRunNextStep.
this.TOTAL_STEPS = 3;
// Currently running step index.
this.step_ = 0;
// True iff post message handler has been regsitered.
this.messageHandlerRegistered_ = false;
this.doneCallback_ = null;
};
InputMethodTestHelper.prototype.registerMessageHandler = function() {
if (!this.messageHandlerRegistered_) {
window.addEventListener('message', this.messageHandler_.bind(this));
this.messageHandlerRegistered_ = true;
}
};
InputMethodTestHelper.prototype.passStep_ = function(passed) {
if (!this.doneCallback_) {
LOG('Expected doneCallback_ in InputMethodTestHelper');
embedder.test.fail();
return;
}
this.doneCallback_(passed);
};
InputMethodTestHelper.prototype.runStep = function(step, doneCallback) {
LOG('runStep: ' + step);
this.doneCallback_ = doneCallback;
if (step != this.step_ + 1 || step < 0 || step > this.TOTAL_STEPS) {
LOG('Incorrect step, expected:', this.step_ + 1, 'got', step);
this.passStep_(false);
return;
}
this.step_ = step;
var msgToSend = '';
if (step == 1) {
msgToSend = 'request-waitForOnInput';
} else if (step == 2) {
msgToSend = 'request-waitForOnInputAndSelect';
} else if (step == 3) {
msgToSend = 'request-valueAfterExtendSelection';
}
if (!msgToSend) {
this.passStep_(false);
return;
}
g_webview.contentWindow.postMessage(JSON.stringify([msgToSend]), '*');
};
InputMethodTestHelper.prototype.messageHandler_ = function(e) {
var data = JSON.parse(e.data);
LOG('InputMethodTestHelper.message, data: ' + data);
if (data[0]=='response-seenFocus') {
embedder.test.succeed();
return;
}
switch (this.step_) {
case 1:
this.passStep_(data[0] == 'response-waitForOnInput' &&
data[1] == 'InputTest123');
break;
case 2:
this.passStep_(data[0] == 'response-waitForOnInputAndSelect' &&
data[1] == 'InputTest456');
break;
case 3:
this.passStep_(data[0] == 'response-valueAfterExtendSelection' &&
data[1] == 'Input456');
break;
default:
LOG('Unexpected message: ' + data);
this.passStep_(false);
}
};
// Tests begin.
// The embedder has to initiate a post message so that the guest can get a
// reference to embedder to send the reply back.
embedder.testFocus_ = function(channelCreationCallback,
expectedResponse,
responseCallback) {
var webview = embedder.setUpGuest_();
embedder.waitForResponseFromGuest_(webview,
'inject_focus.js',
channelCreationCallback,
expectedResponse,
responseCallback);
};
// Verifies that if a <webview> is focused before navigation then the guest
// starts off focused.
//
// We create a <webview> element and make it focused before navigating it.
// Then we load a URL in it and make sure document.hasFocus() returns true
// for the <webview>.
function testFocusBeforeNavigation() {
var webview = document.createElement('webview');
document.body.appendChild(webview);
var onChannelEstablished = function(webview) {
// Query the guest if it has focus.
var msg = ['request-hasFocus'];
webview.contentWindow.postMessage(JSON.stringify(msg), '*');
};
// Focus the <webview> before navigating it.
webview.focus();
embedder.waitForResponseFromGuest_(
webview,
'inject_focus.js',
onChannelEstablished,
'response-hasFocus',
function(data) {
LOG('data, hasFocus: ' + data[1]);
embedder.test.assertEq(true, data[1]);
embedder.test.succeed();
});
}
function testFocusEvent() {
var seenResponse = false;
embedder.testFocus_(function(webview) {
webview.focus();
}, 'focused', function() {
// The focus event fires three times on first focus. We only care about
// the first focus.
if (seenResponse) {
return;
}
seenResponse = true;
embedder.test.succeed();
});
}
function testBlurEvent() {
var seenResponse = false;
embedder.testFocus_(function(webview) {
webview.focus();
webview.blur();
}, 'blurred', function() {
if (seenResponse) {
return;
}
seenResponse = true;
embedder.test.succeed();
});
}
// This test verifies that keyboard input is correctly routed into the guest.
//
// 1) Load the guest and attach an <input> to the guest dom. Count the number of
// input events sent to that element.
// 2) C++ simulates a mouse over and click of the <input> element and waits for
// the browser to see the guest main frame as focused.
// 3) Injects the key sequence: a, Shift+b, c.
// 4) In the second step, the test waits for the input events to be processed
// and then expects the vaue of the <input> to be what the test sent, notably:
// aBc.
function testKeyboardFocusImpl(input_length) {
embedder.testFocus_(function(webview) {
var created = function(e) {
var data = JSON.parse(e.data);
if (data[0] === 'response-createdInput') {
chrome.test.sendMessage('TEST_PASSED');
window.removeEventListener('message', created);
}
};
window.addEventListener('message', created);
g_webview = webview;
var msg = ['request-createInput', input_length];
webview.contentWindow.postMessage(JSON.stringify(msg), '*');
}, 'response-elementClicked', function() {
chrome.test.sendMessage('TEST_STEP_PASSED');
});
}
function testKeyboardFocusRunNextStep(expected) {
window.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
LOG('send window.message, data: ' + data);
if (data[0] == 'response-inputValue') {
if (data[1] == expected) {
chrome.test.sendMessage('TEST_STEP_PASSED');
} else {
chrome.test.sendMessage('TEST_STEP_FAILED');
}
}
});
g_webview.contentWindow.postMessage(
JSON.stringify(['request-getInputValue']), '*');
}
function testKeyboardFocusSimple() {
testKeyboardFocusImpl(3);
}
function testKeyboardFocusWindowFocusCycle() {
testKeyboardFocusImpl(6);
}
// This test verifies IME related stuff for guest.
//
// Briefly:
// 1) We load a guest, the guest gets initial focus and sends message
// back to the embedder.
// 2) In InputMethodTestHelper's step 1, we receive some text via cpp, the
// text is InputTest123, we verify we've seen the change in the guest.
// 3) In InputMethodTestHelper's step 2, we expect the text to be changed
// to InputTest456, this is done from cpp via committing an IME composition.
// 4) In InputMethodTestHelper's step 3, we have a composition (InputTest789)
// on an input element but we move the focus to another input element, we
// make sure the first element gets the composition commit.
// 5) In InputMethodTestHelper's step 4, we verify extending and deleting
// selection through caret works properly.
function testInputMethod() {
var webview = document.createElement('webview');
g_webview = webview;
document.body.appendChild(webview);
var onChannelEstablished = function(webview) {};
if (!g_inputMethodTestHelper) {
g_inputMethodTestHelper = new InputMethodTestHelper();
}
embedder.waitForResponseFromGuest_(
webview,
'inject_input_method.js',
onChannelEstablished,
'response-inputMethodPreparedForFocus',
function(data) {
g_inputMethodTestHelper.registerMessageHandler();
webview.focus();
webview.contentWindow.postMessage(
JSON.stringify(['request-waitForFocus']), '*');
});
}
// Runs additional test steps for testInputMethod.
function testInputMethodRunNextStep(step) {
LOG('testInputMethodRunNextStep, step: ' + step);
if (!g_inputMethodTestHelper) {
g_inputMethodTestHelper = new InputMethodTestHelper();
}
g_inputMethodTestHelper.runStep(step, function(stepPassed) {
LOG('runStep callback, stepPassed: ' + stepPassed);
chrome.test.sendMessage(stepPassed ? 'TEST_STEP_PASSED'
: 'TEST_STEP_FAILED');
});
}
// This test ensures we get TextInputTypeChanged event if we bring
// back focus to a guest's <input> after it was initially focused.
//
// Briefly:
// 1) We load a guest.
// 2) In FocusRestoredTestHelper's step 1, we click on the guest to
// focus its <input> element.
// 3) In FocusRestoredTestHelper's step 2, we click outside the guest
// so that the <input> gets a blur event.
// 4. In FocusRestoredTestHelper's step 3, we click on the guest again
// to bring the focus back.
// In the end we check the guest rvh's TextInputType in cpp to make
// sure it initialises properly.
function testFocusRestored() {
var webview = document.createElement('webview');
webview.style.width = '100px';
webview.style.height = '100px';
g_webview = webview;
document.body.appendChild(webview);
webview.focus();
var onChannelEstablished = function(webview) {
chrome.test.sendMessage('TEST_PASSED');
};
embedder.waitForResponseFromGuest_(webview,
'inject_focus_restored.js',
onChannelEstablished,
undefined,
undefined);
}
// Runs additional test steps for testFocusRestored.
function testFocusRestoredRunNextStep(step) {
LOG('testFocusRestoredRunNextStep, step: ' + step);
if (!g_focusRestoredTestHelper) {
g_focusRestoredTestHelper = new FocusRestoredTestHelper();
}
g_focusRestoredTestHelper.runStep(step, function(stepPassed) {
LOG('runStep callback, stepPassed: ' + stepPassed);
chrome.test.sendMessage(stepPassed ? 'TEST_STEP_PASSED'
: 'TEST_STEP_FAILED');
});
}
// Ensures that the tab key can be used to navigate out of the webview. There is
// a corner case where focus can be trapped in the webview if the next focusable
// element in the embedder is focused when trying to tab or the previous element
// when using shift-tab.
//
// Briefly:
// 1) Start with the embedder input focused
// 2) Click the guest input and wait for it to be focused
// 3) Send a tab key event
// 4) Wait for the embedder input to receive another input event.
function testFocusTakeFocus() {
var input = document.createElement('input');
var webview = embedder.setUpGuest_();
g_webview = webview;
document.body.appendChild(input);
var onChannelEstablished = function(webview) {
input.focus();
var msg = ['request-coords'];
webview.contentWindow.postMessage(JSON.stringify(msg), '*');
};
var inputFocusedHandler = function(e) {
chrome.test.sendMessage('TEST_STEP_PASSED');
}
var guestFocusedHandler = function(e) {
console.log('input focused in guest');
window.removeEventListener('message', guestFocusedHandler);
input.addEventListener('focus', inputFocusedHandler);
chrome.test.sendMessage('TEST_STEP_PASSED');
};
var coordHandler = function(response) {
var rect = g_webview.getBoundingClientRect();
window.clickX = rect.left + response[1];
window.clickY = rect.top + response[2];
window.addEventListener('message', guestFocusedHandler);
chrome.test.sendMessage('TEST_PASSED');
};
embedder.waitForResponseFromGuest_(webview,
'inject_focus_take_focus.js',
onChannelEstablished,
'response-coords',
coordHandler);
}
// Tests that if we focus/blur the embedder, it also gets reflected in the
// guest.
//
// This test has two steps:
// 1) testFocusTracksEmbedder(), in this step we create a <webview> and
// focus it before navigating. After navigating it to a URL, we focus an input
// element inside the <webview>, and wait for its 'focus' event to fire.
// 2) testFocusTracksEmbedderRunNextStep(), in this step, we have already called
// Blur() on the embedder's RVH (see WebViewTest.Focus_FocusTracksEmbedder),
// we make sure we see a 'blur' event on the <webview>'s input element.
function testFocusTracksEmbedder() {
var webview = document.createElement('webview');
g_webview = webview;
document.body.appendChild(webview);
var onChannelEstablished = function(webview) {
var msg = ['request-waitForFocus'];
webview.contentWindow.postMessage(JSON.stringify(msg), '*');
};
// Focus the <webview> before navigating it.
// This is necessary so that 'blur' event on guest's <input> element fires.
webview.focus();
embedder.waitForResponseFromGuest_(
webview,
'inject_focus.js',
onChannelEstablished,
'response-seenFocus',
function(data) { embedder.test.succeed(); });
}
// Runs the second step for testFocusTracksEmbedder().
// See WebViewTest.Focus_FocusTracksEmbedder() to see how this is invoked.
function testFocusTracksEmbedderRunNextStep() {
g_webview.contentWindow.postMessage(
JSON.stringify(['request-waitForBlurAfterFocus']), '*');
window.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
LOG('send window.message, data: ' + data);
if (data[0] == 'response-seenBlurAfterFocus')
chrome.test.sendMessage('TEST_STEP_PASSED');
});
}
// Tests that <webview> sees advanceFocus() call when we cycle through the
// elements inside it using tab key.
//
// This test has two steps:
// 1) testAdvanceFocus(), in this step, we focus the embedder and press a
// tab key, we expect the input element inside the <webview> to be focused.
// 2) POST_testAdvanceFocus(), in this step we send additional tab keypress
// to the embedder/app (from WebViewInteractiveTest.Focus_AdvanceFocus), this
// would cycle the focus within the elements and will bring focus back to
// the input element present in the <webview> mentioned in step 1.
function testAdvanceFocus() {
var webview = document.createElement('webview');
g_webview = webview;
document.body.appendChild(webview);
webview.addEventListener('consolemessage', function(e) {
LOG('g: ' + e.message);
});
webview.addEventListener('loadstop', function(e) {
LOG('loadstop');
window.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
LOG('message, data: ' + data);
if (data[0] == 'connected') {
embedder.test.succeed();
} else if (data[0] == 'button1-focused') {
var focusCount = data[1];
LOG('focusCount: ' + focusCount);
seenFocusCount++;
if (focusCount == 1) {
chrome.test.sendMessage('button1-focused');
} else {
chrome.test.sendMessage('button1-advance-focus');
}
}
});
webview.executeScript(
{file: 'inject_advance_focus_test.js'},
function(results) {
window.console.log('webview.executeScript response');
if (!results || !results.length) {
LOG('Inject script failure.');
embedder.test.fail();
return;
}
webview.contentWindow.postMessage(JSON.stringify(['connect']), '*');
});
});
webview.src = embedder.guestURL;
}
function monitorGuestEvent(type) {
g_webview.contentWindow.postMessage(
JSON.stringify(['request-monitorEvent', type]), '*');
}
function waitGuestEvent(type) {
var listener = function(e) {
var data = JSON.parse(e.data);
if (data[0] == 'response-waitEvent') {
window.removeEventListener('message', listener);
if (data[1] == type) {
chrome.test.sendMessage('TEST_STEP_PASSED');
} else {
chrome.test.sendMessage('TEST_STEP_FAILED');
}
}
}
window.addEventListener('message', listener);
g_webview.contentWindow.postMessage(
JSON.stringify(['request-waitEvent', type]), '*');
}
embedder.test.testList = {
'testAdvanceFocus': testAdvanceFocus,
'testBlurEvent': testBlurEvent,
'testFocusBeforeNavigation': testFocusBeforeNavigation,
'testFocusEvent': testFocusEvent,
'testFocusTracksEmbedder': testFocusTracksEmbedder,
'testInputMethod': testInputMethod,
'testKeyboardFocusSimple': testKeyboardFocusSimple,
'testKeyboardFocusWindowFocusCycle': testKeyboardFocusWindowFocusCycle,
'testFocusRestored': testFocusRestored,
'testFocusTakeFocus': testFocusTakeFocus
};
onload = function() {
chrome.test.getConfig(function(config) {
chrome.test.sendMessage('Launched');
});
};