Commit c4ae3ecb authored by lazyboy's avatar lazyboy Committed by Commit bot

<webview>: Add JavaScript test for IndexedDB isolation.

Remove the old and disabled test.

Previously we used to run the test partly in cpp and partly in JS.
This CL changes the test code to run entirely in JS, requiring less
context switches (== faster) and improving readability.

BUG=248500, 160361
Test=None, no visible changes.

Review URL: https://codereview.chromium.org/1144473003

Cr-Commit-Position: refs/heads/master@{#329863}
parent 604751d0
...@@ -1833,83 +1833,10 @@ IN_PROC_BROWSER_TEST_F(WebViewTest, MAYBE_DOMStorageIsolation) { ...@@ -1833,83 +1833,10 @@ IN_PROC_BROWSER_TEST_F(WebViewTest, MAYBE_DOMStorageIsolation) {
// This tests IndexedDB isolation for packaged apps with webview tags. It loads // This tests IndexedDB isolation for packaged apps with webview tags. It loads
// an app with multiple webview tags and each tag creates an IndexedDB record, // an app with multiple webview tags and each tag creates an IndexedDB record,
// which the test checks to ensure proper storage isolation is enforced. // which the test checks to ensure proper storage isolation is enforced.
// This test is flaky. See http://crbug.com/248500. IN_PROC_BROWSER_TEST_F(WebViewTest, IndexedDBIsolation) {
IN_PROC_BROWSER_TEST_F(WebViewTest, DISABLED_IndexedDBIsolation) {
ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(StartEmbeddedTestServer());
GURL regular_url = embedded_test_server()->GetURL("/title1.html"); ASSERT_TRUE(RunPlatformAppTest(
"platform_apps/web_view/isolation_indexeddb")) << message_;
content::WebContents* default_tag_contents1;
content::WebContents* default_tag_contents2;
content::WebContents* storage_contents1;
content::WebContents* storage_contents2;
NavigateAndOpenAppForIsolation(regular_url, &default_tag_contents1,
&default_tag_contents2, &storage_contents1,
&storage_contents2, NULL, NULL, NULL);
// Initialize the storage for the first of the two tags that share a storage
// partition.
ExecuteScriptWaitForTitle(storage_contents1, "initIDB()", "idb created");
ExecuteScriptWaitForTitle(storage_contents1, "addItemIDB(7, 'page1')",
"addItemIDB complete");
ExecuteScriptWaitForTitle(storage_contents1, "readItemIDB(7)",
"readItemIDB complete");
std::string output;
std::string get_value(
"window.domAutomationController.send(getValueIDB() || 'badval')");
EXPECT_TRUE(ExecuteScriptAndExtractString(storage_contents1,
get_value.c_str(), &output));
EXPECT_STREQ("page1", output.c_str());
// Initialize the db in the second tag.
ExecuteScriptWaitForTitle(storage_contents2, "initIDB()", "idb open");
// Since we share a partition, reading the value should return the existing
// one.
ExecuteScriptWaitForTitle(storage_contents2, "readItemIDB(7)",
"readItemIDB complete");
EXPECT_TRUE(ExecuteScriptAndExtractString(storage_contents2,
get_value.c_str(), &output));
EXPECT_STREQ("page1", output.c_str());
// Now write through the second tag and read it back.
ExecuteScriptWaitForTitle(storage_contents2, "addItemIDB(7, 'page2')",
"addItemIDB complete");
ExecuteScriptWaitForTitle(storage_contents2, "readItemIDB(7)",
"readItemIDB complete");
EXPECT_TRUE(ExecuteScriptAndExtractString(storage_contents2,
get_value.c_str(), &output));
EXPECT_STREQ("page2", output.c_str());
// Reset the document title, otherwise the next call will not see a change and
// will hang waiting for it.
EXPECT_TRUE(content::ExecuteScript(storage_contents1,
"document.title = 'foo'"));
// Read through the first tag to ensure we have the second value.
ExecuteScriptWaitForTitle(storage_contents1, "readItemIDB(7)",
"readItemIDB complete");
EXPECT_TRUE(ExecuteScriptAndExtractString(storage_contents1,
get_value.c_str(), &output));
EXPECT_STREQ("page2", output.c_str());
// Now, let's confirm there is no database in the main browser and another
// tag that doesn't share the same partition. Due to the IndexedDB API design,
// open will succeed, but the version will be 1, since it creates the database
// if it is not found. The two tags use database version 3, so we avoid
// ambiguity.
const char* script =
"indexedDB.open('isolation').onsuccess = function(e) {"
" if (e.target.result.version == 1)"
" document.title = 'db not found';"
" else "
" document.title = 'error';"
"}";
ExecuteScriptWaitForTitle(browser()->tab_strip_model()->GetWebContentsAt(0),
script, "db not found");
ExecuteScriptWaitForTitle(default_tag_contents1, script, "db not found");
} }
// This test ensures that closing app window on 'loadcommit' does not crash. // This test ensures that closing app window on 'loadcommit' does not crash.
......
<!--
* Copyright 2015 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.
-->
<html>
<body>
<h1>Web View Indexed DB Isolation Test</h1>
<div id="web_view_container"></div>
<script src="main.js"></script>
</body>
</html>
// Copyright 2015 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.
var LOG = function(msg) {
window.console.log(msg);
};
// Wrapper class for a <webview> guest.
function Guest(id, webview) {
this.id_ = id;
this.webview_ = webview;
};
// Runs a single verification |step| for this test.
//
// A step is asynchronous and involves sending postMessage to a <webview> and
// receiving a reply postMessage back from the <webview>.
Guest.prototype.sendStepRequestAndWait = function(
step, successCallback, failureCallback) {
this.expectedMessage_ = step.expect;
this.successCallback_ = successCallback;
this.failureCallback_ = failureCallback;
this.expectingResponse_ = true;
step.id = this.id_;
this.webview_.contentWindow.postMessage(JSON.stringify(step), '*');
};
// Runs verification steps for this test listed in |stepList|.
Guest.prototype.runIDBSteps = function(
stepList, successCallback, failureCallback) {
if (!stepList.length) {
LOG('error, runIDBSteps() require at least one step to run');
failureCallback();
return;
}
var currentStep = 0;
// Proceeds to next step upon success.
var stepSuccessCallback = function() {
++currentStep;
if (currentStep == stepList.length) {
// All steps succeeded.
successCallback();
return;
}
this.sendStepRequestAndWait(stepList[currentStep], stepSuccessCallback,
failureCallback);
}.bind(this);
this.sendStepRequestAndWait(stepList[currentStep], stepSuccessCallback,
failureCallback);
};
// Post message handler for this |Guest|.
Guest.prototype.onResponse = function(responseStr) {
if (!this.expectingResponse_) {
return;
}
if (this.expectedMessage_ !== responseStr) {
LOG('Expected response from guest[' + this.id_ + ']: ' +
this.expectedMessage_ + ' but received: ' +
responseStr);
this.failureCallback_();
} else {
this.successCallback_();
}
};
// Tester class to run test steps.
function Tester(port) {
this.port_ = port;
window.onmessage = this.onMessage_.bind(this);
this.failed_ = false;
};
Tester.prototype.getURL = function(filename) {
return 'http://localhost:' + this.port_ +
'/extensions/platform_apps/web_view/isolation_indexeddb/' + filename;
};
Tester.prototype.loadGuest = function(guestInfo, callback) {
var id = guestInfo.id;
var partition = guestInfo.partition;
var html = guestInfo.html;
var js = guestInfo.js;
var webview = document.createElement('webview');
if (partition) {
webview.setAttribute('partition', partition);
}
webview.src = this.getURL(html);
var loadFailed = false;
webview.onloadstop = function(e) {
LOG('webview.onloadstop: ' + id);
if (loadFailed) {
return;
}
var guest = new Guest(id, webview);
webview.executeScript({file: js} , function(results) {
if (!results || !results.length) {
loadFailed = true;
callback(null);
}
callback(id, new Guest(id, webview));
}.bind(this));
}.bind(this);
webview.onloadabort = function(e) {
loadFailed = true;
callback(undefined);
};
webview.onconsolemessage = function(e) {
LOG('G: ' + e.message);
};
document.body.appendChild(webview);
};
Tester.prototype.loadGuests = function(guestInfoList, doneCallback) {
var failed = false;
var responses = [];
var numResponses = 0;
var completedCallack = function(id, guest) {
if (failed) { // We've already failed.
return;
}
if (!guest) {
// guest didn't load.
failed = true;
doneCallback(undefined);
return;
}
responses[id] = guest;
++numResponses;
if (numResponses == guestInfoList.length) {
// We are done.
doneCallback(responses);
}
};
for (var i = 0; i < guestInfoList.length; ++i) {
this.loadGuest(guestInfoList[i], completedCallack);
}
};
Tester.prototype.runTest = function() {
var guestInfoList = [
{id: 1, html: 'storage1.html', js: 'storage.js', partition: 'partition1'},
{id: 2, html: 'storage2.html', js: 'storage.js', partition: 'partition1'},
{id: 3, html: 'storage3.html', js: 'storage.js'}
];
this.loadGuests(guestInfoList, function doneCallback(guests) {
if (!guests) {
LOG('One or all guests failed to load');
this.testFail();
return;
}
LOG('guests load complete');
this.guests_ = guests;
// Loaded all the guests.
this.runStep1();
}.bind(this));
};
// Initializes the storage for the first <webview>.
Tester.prototype.runStep1 = function() {
var guest = this.guests_[1];
guest.runIDBSteps([
{name: 'init', expect: 'idb created'},
{name: 'add', params: [7, 'page1'], expect: 'addItemIDB complete'},
{name: 'read', params: [7], expect: 'readItemIDB: page1'},
], this.runStep2.bind(this), this.testFail.bind(this));
};
// Initializes the storage for the second <webview>, which share a storage
// partition with the first <webview>.
Tester.prototype.runStep2 = function() {
var guest = this.guests_[2];
guest.runIDBSteps([
{name: 'init', expect: 'idb open'},
{name: 'add', params: [7, 'page2'], expect: 'addItemIDB complete'},
{name: 'read', params: [7], expect: 'readItemIDB: page2'},
], this.runStep3.bind(this), this.testFail.bind(this));
};
// Reads through the first <webview> to ensure we have the second value.
Tester.prototype.runStep3 = function() {
var guest = this.guests_[1];
guest.runIDBSteps([
{name: 'read', params: [7], expect: 'readItemIDB: page2'},
], this.runStep4.bind(this), this.testFail.bind(this));
};
// Confirms that the first two <webview>s do not affect the database
// of the main browser (embedder).
Tester.prototype.runStep4 = function() {
var request = indexedDB.open('isolation');
request.onsuccess = function(e) {
var version = e.target.result.version;
// Expect version = 1.
if (version == 1) {
this.runStep5(); // Continue to next step.
} else {
this.testFail();
}
}.bind(this);
request.onerror = this.testFail.bind(this);
request.onblocked = this.testFail.bind(this);
};
// Confirms that a third <webview>'s storage does not get affect by the
// other two <webview>s.
Tester.prototype.runStep5 = function() {
var guest = this.guests_[3];
guest.runIDBSteps([
{name: 'open', params: ['isolation'], expect: 'db not found'},
], this.testPass(this), this.testFail.bind(this));
};
// Central postMessage handler.
Tester.prototype.onMessage_ = function(e) {
var data = JSON.parse(e.data);
var id = data[0];
if (typeof id !== 'number' || id < 0 || id >= this.guests_.length) {
LOG('unexpeced guest id: ' + id);
this.testFail();
return;
}
// Re-route message to the appropriate guest.
this.guests_[id].onResponse(data[1]);
};
Tester.prototype.testFail = function() {
this.failed_ = true;
chrome.test.fail();
};
Tester.prototype.testPass = function() {
if (this.failed_) { // We've failed already.
return;
}
chrome.test.succeed();
};
chrome.test.getConfig(function(config) {
chrome.test.runTests([
function indexedDBIsolation() {
var tester = new Tester(config.testServer.port);
tester.runTest();
}]);
});
{
"name": "Platform App Test: web view Indexed DB isolation",
"version": "1",
"permissions": [
"webview"
],
"app": {
"background": {
"scripts": ["test.js"]
}
}
}
// Copyright 2015 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.
// This method initializes the two types of DOM storage.
function initDomStorage(value) {
window.localStorage.setItem('foo', 'local-' + value);
window.sessionStorage.setItem('bar', 'session-' + value);
}
// The code below is used for testing IndexedDB isolation.
// The test uses three basic operations -- open, read, write -- to verify proper
// isolation across webview tags with different storage partitions.
// Each of the basic functions below sends a postMessage to the embedder with
// the results.
var isolation = {};
isolation.db = null;
isolation.onerror = function(e) {
sendResponse('error');
};
// Used to send postMessage to the embedder.
var channel = null;
// Id that the embedder uses to identify this guest.
var id = -1;
// This method opens the database and creates the objectStore if it doesn't
// exist. It sends a postMessage to the embedder with a string referring to
// which operation has been performed - open vs create.
function initIDB() {
var v = 3;
var ranVersionChangeTransaction = false;
var request = indexedDB.open('isolation', v);
request.onupgradeneeded = function(e) {
isolation.db = e.target.result;
var store = isolation.db.createObjectStore(
'partitions', {keyPath: "id"});
e.target.transaction.oncomplete = function() {
ranVersionChangeTransaction = true;
};
}
request.onsuccess = function(e) {
isolation.db = e.target.result;
if (ranVersionChangeTransaction) {
sendResponse('idb created');
} else {
sendResponse('idb open');
}
};
request.onerror = isolation.onerror;
request.onblocked = isolation.onerror;
}
// This method adds a |value| to the database identified by |id|.
function addItemIDB(id, value) {
var trans = isolation.db.transaction(['partitions'], 'readwrite');
var store = trans.objectStore('partitions');
var data = {'partition': value, 'id': id };
var request = store.put(data);
request.onsuccess = function(e) {
sendResponse('addItemIDB complete');
};
request.onerror = isolation.onerror;
};
// This method reads an item from the database, and returns the result
// to the embedder using postMessage.
function readItemIDB(id) {
var storedValue = null;
var trans = isolation.db.transaction(['partitions'], 'readwrite');
var store = trans.objectStore('partitions');
var request = store.get(id);
request.onsuccess = function(e) {
if (!!e.target.result != false) {
storedValue = request.result.partition;
}
sendResponse('readItemIDB: ' + storedValue);
};
request.onerror = isolation.onerror;
}
function openIDB(name) {
var request = indexedDB.open('isolation');
request.onsuccess = function(e) {
e.target.result.version == 1 ? sendResponse('db not found')
: sendResponse('db found');
};
request.onerror = isolation.onerror;
request.onblocked = isolation.onerror;
}
function sendResponse(responseStr) {
if (channel) {
channel.postMessage(JSON.stringify([id, responseStr]), '*');
}
}
window.onmessage = function(e) {
if (!channel) {
channel = e.source;
}
var data = JSON.parse(e.data);
id = data.id;
window.console.log('onmessage: ' + data.name);
switch (data.name) {
case 'init':
initIDB();
break;
case 'add':
addItemIDB(data.params[0], data.params[1]);
break;
case 'read':
readItemIDB(data.params[0]);
break;
case 'open':
openIDB(data.params[0]);
break;
default:
sendResponse('bogus-command'); // Error.
break;
}
};
<!--
* Copyright 2015 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.
-->
<body>Empty page 1, used for testing storage isolation.</body>
<!--
* Copyright 2015 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.
-->
<body>Empty page 2, used for testing storage isolation.</body>
<!--
* Copyright 2015 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.
-->
<body>Empty page 3, used for testing storage isolation.</body>
// Copyright 2015 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.
chrome.app.runtime.onLaunched.addListener(function() {
chrome.app.window.create('main.html', {}, function () {});
});
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