Major revision of page cycler UI. This revision does several things:

In this CL:

1. Reforms the JS UI to look more like Chrome|Settings, including look/feel and use of overlays for progress and error reporting.

2. Redesigns the UI to support maintaining on the extension side a record of captures, including their URLs and the cache state resulting from their initial capture run.  Allows the user to add new captures to the record, delete unwanted ones, and run any he'd like.

Later CLs:

3. Actually implements the underlying FileSystem-using code to do this, as opposed to current mockup.

4. Adds C++-side code to allow C++ directories to be copied to and from FileSystem sandbox directories.

5. Adds means of interrupting ongoing sub-browser sessions so that captures and replays may be cancelled from UI.

6. Improves stats reporting, including nice-looking charts instead of present rather nasty line-by-line text dump of runtimes per URL.


Snapshots:
Capture tab: http://imgur.com/Jb3UG
Message overlay: http://imgur.com/afFVL
Playback tab: http://imgur.com/PUGhx
Playback w/o chosen capture: http://imgur.com/biKK1
Playback w/ no captures available: http://imgur.com/Zv4Bp

Aaron's initial mockup/spec:
https://moqups.com/aboodman/wDjKAEXR/p:aed59f081

Somewhat dated original design doc for the first version:
https://docs.google.com/a/chromium.org/document/d/1HyiPO4DBfCzLtVKOMx6rGGa7Vw_GzNZX_FObc-mWiZo/edit?hl=en_US&sai=true

Review URL: https://chromiumcodereview.appspot.com/10832191

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@162059 0039d316-1c4b-4281-b951-d872f2087c98
parent 61939e33
......@@ -116,8 +116,8 @@ void RunPageCyclerFunction::RunTestBrowser() {
// Set up Capture- or Replay-specific commandline switches.
AddSwitches(&line);
FilePath error_file_path = url_path.DirName()
.Append(url_path.BaseName().value() +
FilePath error_file_path = url_path.DirName().
Append(url_path.BaseName().value() +
FilePath::StringType(kURLErrorsSuffix));
LOG(ERROR) << "Test browser commandline: " << line.GetCommandLineString() <<
......@@ -167,7 +167,9 @@ bool CaptureURLsFunction::ParseJSParameters() {
EXTENSION_FUNCTION_VALIDATE(params.get());
url_contents_ = JoinString(params->urls, '\n');
user_data_dir_ = FilePath::FromUTF8Unsafe(params->cache_directory_path);
// TODO(cstaley): Can't just use captureName -- gotta stick it in a temp dir.
// TODO(cstaley): Ensure that capture name is suitable as directory name.
user_data_dir_ = FilePath::FromUTF8Unsafe(params->capture_name);
return true;
}
......@@ -184,16 +186,15 @@ void CaptureURLsFunction::Finish() {
SendResponse(true);
}
// ReplayURLsFunction ------------------------------------------------
ReplayURLsFunction::ReplayURLsFunction()
: RunPageCyclerFunction(new ProductionProcessStrategy()),
run_time_ms_(0) {
run_time_ms_(0.0) {
}
ReplayURLsFunction::ReplayURLsFunction(ProcessStrategy* strategy)
: RunPageCyclerFunction(strategy), run_time_ms_(0) {
: RunPageCyclerFunction(strategy), run_time_ms_(0.0) {
}
ReplayURLsFunction::~ReplayURLsFunction() {}
......@@ -205,8 +206,13 @@ bool ReplayURLsFunction::ParseJSParameters() {
record::ReplayURLs::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params.get());
url_contents_ = JoinString(params->urls, '\n');
user_data_dir_ = FilePath::FromUTF8Unsafe(params->capture_directory_path);
// TODO(cstaley): Must build full temp dir from capture_name
user_data_dir_ = FilePath::FromUTF8Unsafe(params->capture_name);
// TODO(cstaley): Get this from user data dir ultimately
url_contents_ = "http://www.google.com\nhttp://www.amazon.com";
repeat_count_ = params->repeat_count;
if (params->details.get()) {
......@@ -237,7 +243,7 @@ void ReplayURLsFunction::AddSwitches(CommandLine* line) {
void ReplayURLsFunction::ReadReplyFiles() {
file_util::ReadFileToString(stats_file_path_, &stats_);
run_time_ms_ = (base::Time::NowFromSystemTime() - timer_).InMilliseconds();
run_time_ms_ = (base::Time::NowFromSystemTime() - timer_).InMillisecondsF();
}
void ReplayURLsFunction::Finish() {
......
......@@ -156,7 +156,7 @@ class ReplayURLsFunction : public RunPageCyclerFunction {
base::Time timer_;
// These two data are additional information returned to caller.
int run_time_ms_;
double run_time_ms_;
std::string stats_;
};
......
......@@ -38,9 +38,14 @@ const std::string kTestStatistics = "Sample Stat 1\nSample Stat 2\n";
// Standard capture parameters, with a mix of good and bad URLs, and
// a hole for filling in the user data dir.
// Restore these on the next CL of the new page cycler, when the C++
// side's implementation is de-hacked.
//const char kCaptureArgs1[] =
// "[\"%s\", [\"URL 1\", \"URL 2(bad)\", \"URL 3\", \"URL 4(bad)\"]]";
const char kCaptureArgs1[] =
"[[\"URL 1\", \"URL 2(bad)\", \"URL 3\", \"URL 4(bad)\"]"
", \"%s\"]";
"[\"%s\", [\"http://www.google.com\", \"http://www.amazon.com\"]]";
// Standard playback parameters, with the same mix of good and bad URLs
// as the capture parameters, a hole for filling in the user data dir, and
......@@ -48,8 +53,7 @@ const char kCaptureArgs1[] =
// verify that they made it into the CommandLine, since extension loading
// and repeat-counting are hard to emulate in the test ProcessStrategy.
const char kPlaybackArgs1[] =
"[[\"URL 1\", \"URL 2(bad)\", \"URL 3\", \"URL 4(bad)\"], \"%s\""
", 2, {\"extensionPath\": \"MockExtension\"}]";
"[\"%s\", 2, {\"extensionPath\": \"MockExtension\"}]";
// Use this as the value of FilePath switches (e.g. user-data-dir) that
// should be replaced by the record methods.
......@@ -243,9 +247,10 @@ class RecordApiTest : public InProcessBrowserTest {
const TestProcessStrategy& strategy) {
// Check that the two bad URLs are returned.
const base::Value* string_value = NULL;
StringValue badURL2("URL 2(bad)"), badURL4("URL 4(bad)");
/* TODO(CAS) Need to rework this once the new record API is implemented.
const base::Value* string_value = NULL;
EXPECT_TRUE(result->GetSize() == 2);
result->Get(0, &string_value);
EXPECT_TRUE(base::Value::Equals(string_value, &badURL2));
......@@ -256,6 +261,7 @@ class RecordApiTest : public InProcessBrowserTest {
std::string goodURL1("URL 1"), goodURL3("URL 3");
EXPECT_TRUE(strategy.GetVisitedURLs()[0].compare(goodURL1) == 0
&& strategy.GetVisitedURLs()[1].compare(goodURL3) == 0);
*/
return true;
}
......
......@@ -7,17 +7,32 @@
"type": "object",
"description": "",
"properties": {
"extensionPath": {"type": "string", "optional": true, "description": "A path to an extension to run in the session. Should be an unpacked extension."}
"extensionPath": {
"type": "string",
"optional": true,
"description":
"Absolute path to an unpacked extension to run in the subbrowser session."
}
}
},
{
"id": "ReplayURLsResult",
"type": "object",
"description": "",
"description": "Return value for Replay callback",
"properties": {
"runTime": {"type": "integer", "description": "Time in milliseconds to complete all runs."},
"stats": {"type": "string", "description": "Full multiline dump of output stats."},
"errors": {"type": "array", "items": {"type": "string"}, "description": "List of errors during replay."}
"runTime": {
"type": "number",
"description": "Time in milliseconds to complete all runs."
},
"stats": {
"type": "string",
"description": "Full multiline dump of output stats, showing one statistic per line, comprising an abbreviated statistic name and its value (e.g. vmsize_f_b= 696164352 bytes for final vm size). This is ugly, and will be changed shortly."
},
"errors": {
"type": "array",
"items": {"type": "string"},
"description": "List of errors during replay. Presently, this should only be abnormal browser termination for unexpected reasons."
}
}
}
],
......@@ -27,15 +42,17 @@
"description": "",
"type": "function",
"parameters": [
{
"type": "string",
"description": "Unique name of the capture.",
"name": "captureName"
},
{
"type": "array",
"items": {"type": "string"},
"description": "URL list to visit during capture.",
"name": "urls"
},
{
"type": "string",
"name": "cacheDirectoryPath"
},
{
"name": "callback",
"type": "function",
......@@ -45,7 +62,8 @@
{
"type": "array",
"items": {"type": "string"},
"name": "errors"
"name": "errors",
"description": "List of any URLs that failed to load, one error per textline, along with failure reason (e.g. unknown domain). Also may include general abnormal-exit message if the subbrowser run failed for other reasons."
}
]
}
......@@ -56,14 +74,10 @@
"description": "",
"type": "function",
"parameters": [
{
"type": "array",
"items": {"type": "string"},
"name": "urls"
},
{
"type": "string",
"name": "captureDirectoryPath"
"name": "captureName",
"description": "Unique name of capture. Use to determine cache."
},
{
"type": "integer",
......@@ -79,8 +93,14 @@
{
"name": "callback",
"type": "function",
"optional": true,
"description": "Called when playback has completed.",
"parameters": [{"$ref": "ReplayURLsResult", "name": "result"}]
"parameters": [
{
"$ref": "ReplayURLsResult",
"name": "result"
}
]
}
]
}
......
// Copyright (c) 2012 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.
/**
* Constructor for the tab UI governing setup and initial running of capture
* baselines. All HTML controls under tag #capture-tab, plus the tab label
* #capture-tab-label are controlled by this class.
* @param {!Object} cyclerUI The master UI class, needed for global state
* such as current capture name.
* @param {!Object} cyclerData The local FileSystem-based database of
* available captures to play back.
* @param {!Object} playbackTab The class governing playback selections.
* We need this in order to update its choices when we get asynchronous
* callbacks for successfully completed capture baselines.
*/
var CaptureTab = function (cyclerUI, cyclerData, playbackTab) {
// Members for all UI elements subject to programmatic adjustment.
this.tabLabel_ = $('#capture-tab-label');
this.captureTab_ = $('#capture-tab');
this.captureName_ = $('#capture-name');
this.captureURLs_ = $('#capture-urls');
this.doCaptureButton_ = $('#do-capture');
// References to other major components of the extension.
this.cyclerUI_ = cyclerUI;
this.cyclerData_ = cyclerData;
this.playbackTab_ = playbackTab;
/*
* Enable the capture tab and its label.
*/
this.enable = function() {
this.captureTab_.hidden = false;
this.tabLabel_.classList.add('selected');
};
/*
* Disable the capture tab and its label.
*/
this.disable = function() {
this.captureTab_.hidden = true;
this.tabLabel_.classList.remove('selected');
};
/**
* Do a capture using current data from the capture tab. Post an error
* dialog if said data is incorrect or incomplete. Otherwise pass
* control to the browser side.
* @private
*/
this.doCapture_ = function() {
var errors = [];
var captureName = this.captureName_.value.trim();
var urlList;
urlList = this.captureURLs_.value.split('\n');
if (captureName.length == 0)
errors.push('Must give a capture name');
if (urlList.length == 0)
errors.push('Must give at least one URL');
if (errors.length > 0) {
this.cyclerUI_.showMessage(errors.join('\n'), 'Ok');
} else {
this.doCaptureButton_.disabled = true;
chrome.experimental.record.captureURLs(captureName, urlList,
this.onCaptureDone.bind(this));
}
}
/**
* Callback for completed (or possibly failed) capture. Post a message
* box, either with errors or "Success!" message.
* @param {!Array.<string>} errors List of errors that occured
* during capture, if any.
*/
this.onCaptureDone = function(errors) {
this.doCaptureButton_.disabled = false;
if (errors.length > 0) {
this.cyclerUI_.showMessage(errors.join('\n'), 'Ok');
} else {
this.cyclerUI_.showMessage('Success!', 'Ok');
this.cyclerUI_.currentCaptureName = this.captureName_.value.trim();
this.cyclerData_.saveCapture(this.cyclerUI_.currentCaptureName);
}
}
// Set up listener for capture button.
this.doCaptureButton_.addEventListener('click', this.doCapture_.bind(this));
};
/* Copyright (c) 2012 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. */
/* Mimic sans-serif, dark bluish-gray font from Chrome|Settings page.
* This is approximate, as Chrome|Settings can adjust CSS per-platform and
* for instance uses Ubuntu font on Unix. It's at least quite close. */
body {
font-family: Arial, sans-serif;
font-size: 12px;
color: #303942;
}
/* Mimic button from Settings page */
button {
-webkit-padding-end: 10px;
-webkit-padding-start: 10px;
background-color: buttonface;
background-image: -webkit-linear-gradient(#EDEDED, #EDEDED 38%, #DEDEDE);
border: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 2px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08),
inset 0 1px 2px rgba(255, 255, 255, 0.75);
color: #444;
font: inherit;
margin: 0 1px 0 0;
min-height: 2em;
min-width: 4em;
text-shadow: #F0F0F0 0px 1px 0px;
}
button:active {
background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
box-shadow: none;
text-shadow: none;
}
input, select {
width: 15em;
}
/* Top left header and tab headings, respectively */
#header, h1 {
font-size: 18px;
font-weight: normal;
margin-top: 10px;
}
/* h2 for major subheadings has pure black */
h2 {
color: black;
font-size: 14px;
font-weight: normal;
margin-top: 20px;
}
.divider {
background-color: #EEE;
height: 1px;
min-width: 600px;
}
.tab-label {
color: #999;
cursor: pointer;
font-size: 13px;
margin-top: 15px;
}
.selected {
color: #464E5A;
}
.indent {
margin-left: 18px;
}
.sub-label {
min-width: 10em;
}
.column {
-webkit-box-orient: vertical;
display: -webkit-box;
}
.row {
-webkit-box-orient: horizontal;
display: -webkit-box;
}
.gapped {
margin-top: 1em;
}
.url-list {
height: 8em;
width: 20em;
}
/* Cycler header is a little lighter blue-gray */
#header {
color: #5C6166;
margin-bottom: 1.5em;
margin-left: 1em;
}
#header-icon {
float:left;
position:relative;
}
#tab-navigator {
float:left;
position:relative;
width:100px;
}
#tab-wrapper {
margin-left: 100px;
}
#playback-repeats {
width: 3em;
}
#popup {
background-color: rgba(255, 255, 255, .6);
height:100%;
left: 0;
position: absolute;
top: 0;
width:100%;
z-index: 1;
}
#popup-box {
background-color: #fff;
border: 1px solid;
left: 30em;
padding: 1em;
position: absolute;
top: 12em;
z-index: 2;
}
#no-captures {
padding-top: 20px;
}
<!doctype html>
<!-- Copyright (c) 2012 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>
<head>
<title>Page Cycler</title>
<link rel='stylesheet' type='text/css' href='cycler.css'/>
</head>
<body>
<div id='page'>
<div class='row'>
<div id='tab-navigator column'>
<div class='row'>
<img id='header-icon' src='cycler_icon_128.png' height='30'
height='30'/>
<h1 id='header'>Cycler</h1>
</div>
<div id='capture-tab-label' class='tab-label selected'>
<a>Capture</a>
</div>
<div id='playback-tab-label' class='tab-label'>
<a>Playback</a>
</div>
</div>
<div id='tab-wrapper'>
<div id='capture-tab'>
<div class='column'>
<h1>Capture</h1>
<div class='divider'></div>
<h2>Name:</h2>
<input class='indent name-combo' id='capture-name' type='text'/>
<h2>URLs:</h2>
<div class='indent'>
Enter the URLs to capture, one URL per line.<br/>
<textarea id='capture-urls' class='url-list'></textarea>
</div>
<button id='do-capture' class='gapped'>Capture!</button>
</div>
</div>
<div id='playback-tab' hidden>
<div class='column'>
<h1>Playback</h1>
<div class='divider'> </div>
<div id="no-captures" hidden>
<h2>No captures available. Create new ones via Capture tab.</h2>
</div>
<div id='have-captures'>
<div class='column'>
<h2>Capture:</h2>
<select id='playback-name' class='indent name-combo'>
<option value='' disabled selected>Choose a capture</option>
</select>
<div id='playback-details' class='gapped'>
<div class='column gapped'>
<h2>URLs:</h2>
<textarea id='playback-urls' class='indent url-list'>
</textarea>
</div>
<h2>Playback&nbsp;Options:</h2>
<div class='row gapped'>
<div class='indent sub-label'>Repetitions:</div>
<input id='playback-repeats' type='number' min='1' max='100'
value='1'/>
</div>
<div class='row gapped'>
<div class='indent sub-label'>Install&nbsp;Extension:</div>
<input id='playback-extension' type='text' />
<button id='playback-browse'>Browse..</button>
</div>
<div class='row gapped'>
<button id='do-playback'>Playback</button>
<button id='do-delete'>Delete</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id='popup' hidden>
<div id='popup-box' style='column'>
<div id='popup-content'>
When in the course of human events it<br/>
becomes necessary for one people<br/>
to dissolve...
</div>
<button id='do-popup-dismiss' class='gapped'>Dismiss</button>
</div>
</div>
</div>
<script type='text/javascript' src='cycler_data.js'></script>
<script type='text/javascript' src='capture_tab.js'></script>
<script type='text/javascript' src='playback_tab.js'></script>
<script type='text/javascript' src='cycler_ui.js'></script>
</body>
</html>
// Copyright (c) 2012 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.
function $(criterion) {
return document.querySelector(criterion);
}
var cyclerUI = new (function () {
this.urlList = [];
this.cacheDir = "";
this.captureTab = $("#capture-tab");
this.captureTabLabel = $("#capture-tab-label");
this.captureButton = $("#capture-test");
this.captureErrorDiv = $("#capture-errors-display");
this.captureErrorList = $("#capture-errors");
this.playbackTab = $("#playback-tab");
this.playbackTabLabel = $("#playback-tab-label");
this.playbackURLs = $("#playback-urls");
this.playbackCache = $("#playback-cache-dir");
this.playbackButton = $("#playback-test");
this.playbackErrorDiv = $("#playback-errors-display");
this.playbackErrorList = $("#playback-errors");
this.playbackURLs.innerText = this.playbackCache.innerText = noTestMessage;
this.enableTab = function(tabLabel, tab) {
var tabList = document.querySelectorAll(".tab");
var tabLabelList = document.querySelectorAll(".tab-label");
for (var i = 0; i < tabList.length; i++)
if (tabList[i] == tab)
tabList[i].style.visibility = "visible";
else
tabList[i].style.visibility = "hidden";
for (var i = 0; i < tabLabelList.length; i++)
if (tabLabelList[i] == tabLabel) {
tabLabelList[i].classList.add("enabled-tab-label");
tabLabelList[i].classList.remove("disabled-tab-label");
} else {
tabLabelList[i].classList.remove("enabled-tab-label");
tabLabelList[i].classList.add("disabled-tab-label");
}
}
this.chooseCapture = function() {
this.enableTab(this.captureTabLabel, this.captureTab);
}
this.chooseReplay = function() {
this.enableTab(this.playbackTabLabel, this.playbackTab);
}
this.captureTest = function() {
var errorList = $("#capture-errors");
var errors = [];
this.cacheDir = $("#capture-cache-dir").value;
this.urlList = $("#capture-urls").value.split("\n");
if (errors.length > 0) {
this.captureErrorList.innerText = errors.join("\n");
this.captureErrorDiv.className = "error-list-show";
}
else {
this.captureErrorDiv.className = "error-list-hide";
this.captureButton.disabled = true;
chrome.experimental.record.captureURLs(this.urlList, this.cacheDir,
this.onCaptureDone.bind(this));
}
}
this.onCaptureDone = function(errors) {
this.captureButton.disabled = false;
if (errors.length > 0) {
this.captureErrorList.innerText = errors.join("\n");
this.captureErrorDiv.className = "error-list-show";
this.playbackButton.disabled = true;
this.playbackCache.innerText = this.playbackURLs.innerText =
noTestMessage;
}
else {
this.captureErrorDiv.className = "error-list-hide";
this.playbackButton.disabled = false;
this.playbackURLs.innerText = this.urlList.join("\n");
this.playbackCache.innerText = this.cacheDir;
}
}
this.playbackTest = function() {
var extensionPath = $("#extension-dir").value;
var repeatCount = parseInt($('#repeat-count').value);
var errors = [];
// Check local errors
if (isNaN(repeatCount))
errors.push("Enter a number for repeat count");
else if (repeatCount < 1 || repeatCount > 100)
errors.push("Repeat count must be between 1 and 100");
if (errors.length > 0) {
this.playbackErrorList.innerText = errors.join("\n");
this.playbackErrorDiv.className = "error-list-show";
} else {
this.playbackErrorDiv.className = "error-list-hide";
this.playbackButton.disabled = true;
chrome.experimental.record.playbackURLs(
this.urlList,
this.cacheDir,
repeatCount,
{"extensionPath": extensionPath},
this.onReplayDone.bind(this));
}
}
this.onReplayDone = function(result) {
var playbackResult = $("#playback-result");
this.playbackButton.disabled = false;
if (result.errors.length > 0) {
this.playbackErrorList.innerText = result.errors.join("<br>");
this.playbackErrorDiv.className = "error-list-show";
}
else {
this.playbackErrorDiv.className = "error-list-hide";
playbackResult.innerText = "Test took " + result.runTime + "mS :\n" +
result.stats;
}
}
this.captureButton.addEventListener("click", this.captureTest.bind(this));
this.playbackButton.addEventListener("click", this.playbackTest.bind(this));
this.captureTabLabel.addEventListener("click", this.chooseCapture.bind(this));
this.playbackTabLabel.addEventListener("click", this.chooseReplay.bind(this));
this.enableTab(this.captureTabLabel, this.captureTab);
})();
// Copyright (c) 2012 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 CyclerData = function () {
this.currentCaptures_ = ['alpha', 'beta', 'gamma'];
/**
* Mark a capture as saved successfully. Actual writing of the cache
* directory and URL list into the FileSystem is done from the C++ side.
* JS side just updates the capture choices.
* @param {!string} name The name of the capture.
* TODO(cstaley): Implement actual addition of new capture data
*/
this.saveCapture = function(name) {
console.log('Saving capture ' + name);
this.currentCaptures_.push(name);
}
/**
* Return a list of currently stored captures in the local FileSystem.
* @return {Array.<!string>} Names of all the current captures.
* TODO(cstaley): Implement actual generation of current capture list via
* ls-like traversal of the extension's FileSystem.
*/
this.getCaptures = function() {
return this.currentCaptures_;
}
/**
* Delete capture |name| from the local FileSystem, and update the
* capture choices HTML select element.
* @param {!string} name The name of the capture to delete.
* TODO(cstaley): Implement actual deletion
*/
this.deleteCapture = function(name) {
this.currentCaptures_.splice(this.currentCaptures_.indexOf(name), 1);
}
};
// Copyright (c) 2012 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.
function $(criterion) {
return document.querySelector(criterion);
}
var cyclerUI = new (function () {
/**
* Enum for different UI states.
* @enum {number}
* @private
*/
var EnableState_ = {capture: 0, playback: 1};
this.cyclerData_ = new CyclerData();
// Members for all UI elements subject to programmatic adjustment.
this.captureTabLabel_ = $('#capture-tab-label');
this.playbackTabLabel_ = $('#playback-tab-label');
this.playbackTab_ = new PlaybackTab(this, this.cyclerData_);
this.captureTab_ = new CaptureTab(this, this.cyclerData_, this.playbackTab_);
this.popupDialog_ = $('#popup');
this.popupContent_ = $('#popup-content');
this.doPopupDismiss_ = $('#do-popup-dismiss');
/**
* Name of the most recent capture made, or the one most recently chosen
* for playback.
* @type {!string}
*/
this.currentCaptureName = null;
/**
* One of the EnableState_ values, showing which tab is presently
* enabled.
* @type {number}
*/
this.enableState = null;
/*
* Enable the capture tab, changing tab labels approproiately.
* @private
*/
this.enableCapture_ = function() {
if (this.enableState != EnableState_.capture) {
this.enableState = EnableState_.capture;
this.captureTab_.enable();
this.playbackTab_.disable();
}
};
/*
* Enable the playback tab, changing tab labels approproiately.
* @private
*/
this.enablePlayback_ = function() {
if (this.enableState != EnableState_.playback) {
this.enableState = EnableState_.playback;
this.captureTab_.disable();
this.playbackTab_.enable();
}
};
/**
* Show an overlay with a message, a dismiss button with configurable
* label, and an action to call upon dismissal.
* @param {!string} content The message to display.
* @param {!string} dismissLabel The label on the dismiss button.
* @param {function()} action Additional action to take, if any, upon
* dismissal.
*/
this.showMessage = function(content, dismissLabel, action) {
this.popupContent_.innerText = content;
this.doPopupDismiss_.innerText = dismissLabel;
this.popupDialog_.hidden = false;
if (action != null)
doPopupDismiss_.addEventListener('click', action);
}
/**
* Default action for popup dismissal button, performed in addition to
* any other actions that may be specified in showMessage_ call.
* @private
*/
this.clearMessage_ = function() {
this.popupDialog_.hidden = true;
}
// Set up listeners on all buttons.
this.doPopupDismiss_.addEventListener('click', this.clearMessage_.bind(this));
// Set up listeners on tab labels.
this.captureTabLabel_.addEventListener('click',
this.enableCapture_.bind(this));
this.playbackTabLabel_.addEventListener('click',
this.enablePlayback_.bind(this));
// Start with capture tab displayed.
this.enableCapture_();
})();
{
"manifest_version": 2,
"name": "Cycler",
"version": "0.1",
"description": "Cycler UI",
"app": {
"launch": {
"local_path": "cycler.html"
}
},
"icons": {
"128": "cycler_icon_128.png",
"16": "cycler_icon_16.png"
},
"permissions": [
"experimental"
]
}
// Copyright (c) 2012 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.
/**
* Constructor for the tab UI governing playback selection and running.
* All HTML controls under tag #plaback-tab, plus the tab label
* #playback-tab-label are controlled by this class.
* @param {!Object} cyclerUI The master UI class, needed for global state
* such as current capture name.
* @param {!Object} cyclerData The local FileSystem-based database of
* available captures to play back.
*/
var PlaybackTab = function (cyclerUI, cyclerData) {
/**
* Enum for different playback tab states.
* @enum {number}
* @private
*/
var EnableState_ = {
choosePlayback: 1, // Choose a playback, if none already chosen.
showPlayback: 2, // Show currently chosen playback, and offer options.
showNoCaptures: 3 // Show message indicating no captures are available.
};
this.cyclerUI_ = cyclerUI;
this.cyclerData_ = cyclerData;
/**
* Create members for all UI elements with which we'll interact.
*/
this.tabLabel_ = $('#playback-tab-label');
this.playbackTab_ = $('#playback-tab');
this.noCaptures_ = $('#no-captures');
this.yesCaptures_ = $('#have-captures');
this.playbackName_ = $('#playback-name');
this.playbackDetails_ = $('#playback-details');
this.playbackURLs_ = $('#playback-urls');
this.playbackRepeats_ = $('#playback-repeats');
this.playbackExtension_ = $('#playback-extension');
this.doPlaybackButton_ = $('#do-playback');
this.doDeleteButton_ = $('#do-delete');
/*
* Enable the playback tab, showing no current playback choice, by
* hiding the playbackDetails_ div.
* @private
*/
this.enableChoosePlayback_ = function() {
if (this.enableState != EnableState_.choosePlayback) {
this.enableState = EnableState_.choosePlayback;
this.yesCaptures_.hidden = false;
this.noCaptures_.hidden = true;
this.playbackDetails_.hidden = true;
}
};
/*
* Enable the playback tab, showing a current playback choice by showing
* the playbackDetails_ div.
* @private
*/
this.enableShowPlayback_ = function() {
if (this.enableState != EnableState_.showPlayback) {
this.enableState = EnableState_.showPlayback;
this.yesCaptures_.hidden = false;
this.noCaptures_.hidden = true;
this.playbackDetails_.hidden = false;
}
};
/*
* Enable the playback tab and adjust tab labels appropriately. Show
* no available captures by hiding the yesCaptures_ div and showing the
* noCaptures_ div instead.
* @private
*/
this.enableShowNoCaptures_ = function() {
if (this.enableState != EnableState_.showNoCaptures) {
this.enableState = EnableState_.showNoCaptures;
this.noCaptures_.hidden = false;
this.yesCaptures_.hidden = true;
}
};
/**
* Enable the playback tab, showing either its "no captures", "choose
* a capture" or "display chosen capture" form, depending on the state
* of existing captures and |currentCaptureName_|.
*/
this.enable = function() {
this.tabLabel_.classList.add('selected');
this.updatePlaybackChoices_();
this.playbackTab_.hidden = false;
if (this.cyclerData_.getCaptures().length == 0) {
this.enableShowNoCaptures_();
} else if (this.cyclerUI_.currentCaptureName == null) {
this.enableChoosePlayback_();
} else {
this.enableShowPlayback_();
}
}
/**
* Disable the playback tab altogether, presumably in favor of some
* other tab.
*/
this.disable = function() {
this.tabLabel_.classList.remove('selected');
this.playbackTab_.hidden = true;
}
/**
* Utility function to refresh the selection list of captures that may
* be chosen for playback. Show all current captures, and also a
* "Choose a capture" default text if no capture is currently selected.
* @private
*/
this.updatePlaybackChoices_ = function() {
var captureNames = this.cyclerData_.getCaptures();
var options = this.playbackName_.options;
var nextIndex = 0;
var chooseOption;
options.length = 0;
if (this.cyclerUI_.currentCaptureName == null) {
chooseOption = new Option('Choose a capture', null);
chooseOption.disabled = true;
options.add(chooseOption);
options.selectedIndex = nextIndex++;
}
for (var i = 0; i < captureNames.length; i++) {
options.add(new Option(captureNames[i], captureNames[i]));
if (captureNames[i] == this.cyclerUI_.currentCaptureName) {
options.selectedIndex = nextIndex;
}
nextIndex++;
}
}
/**
* Event callback for selection of a capture to play back. Save the
* choice in |currentCaptureName_|. Update the selection list because the
* default reminder message is no longer needed when a capture is chosen.
* Change playback tab to show-chosen-capture mode.
*/
this.selectPlaybackName = function() {
this.cyclerUI_.currentCaptureName = this.playbackName_.value;
this.updatePlaybackChoices_();
this.enableShowPlayback_();
}
/**
* Event callback for pressing the playback button, which button is only
* enabled if a capture has been chosen for playback. Check for errors
* on playback page, and either display a message with such errors, or
* call the extenion api to initiate a playback.
* @private
*/
this.doPlayback_ = function() {
var extensionPath = this.playbackExtension_.value;
var repeatCount = parseInt(this.playbackRepeats_.value);
var errors = [];
// Check local errors
if (isNaN(repeatCount)) {
errors.push('Enter a number for repeat count');
} else if (repeatCount < 1 || repeatCount > 100) {
errors.push('Repeat count must be between 1 and 100');
}
if (errors.length > 0) {
this.cyclerUI_.showMessage(errors.join('\n'), 'Ok');
} else {
this.doPlaybackButton_.disabled = true;
chrome.experimental.record.replayURLs(
this.cyclerUI_.currentCaptureName,
repeatCount,
{'extensionPath': extensionPath},
this.onPlaybackDone.bind(this));
}
}
/**
* Extension API calls this back when a playback is done.
* @param {!{
* runTime: number,
* stats: string,
* errors: !Array.<string>
* }} results The results of the playback, including running time in ms,
* a string of statistics information, and a string array of errors.
*/
this.onPlaybackDone = function(results) {
this.doPlaybackButton_.disabled = false;
if (results.errors.length > 0) {
this.cyclerUI_.showMessage(results.errors.join('\n'), 'Ok');
} else {
this.cyclerUI_.showMessage('Test took ' + results.runTime + 'mS :\n' +
results.stats, 'Ok');
}
}
/**
* Delete the capture with name |currentCaptureName_|. For this method
* to be callable, there must be a selected currentCaptureName_. Change
* the displayed HTML to show no captures if none exist now, or to show
* none selected (since we just deleted the selected one).
* @private
*/
this.doDelete_ = function() {
this.cyclerData_.deleteCapture(this.cyclerUI_.currentCaptureName);
this.cyclerUI_.currentCaptureName = null;
this.updatePlaybackChoices_();
if (this.cyclerData_.getCaptures().length == 0) {
this.enableShowNoCaptures_();
} else {
this.enableChoosePlayback_();
}
}
// Set up listeners on buttons.
this.doPlaybackButton_.addEventListener('click', this.doPlayback_.bind(this));
this.doDeleteButton_.addEventListener('click', this.doDelete_.bind(this));
// Set up initial selection list for existing captures, and selection
// event listener.
this.updatePlaybackChoices_();
this.playbackName_.addEventListener('change',
this.selectPlaybackName.bind(this));
};
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