summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/browser/extensions/extension_function_dispatcher.cc3
-rw-r--r--chrome/browser/extensions/extension_tabs_module.cc162
-rw-r--r--chrome/browser/extensions/extension_tabs_module.h9
-rw-r--r--chrome/renderer/resources/extension_process_bindings.js23
-rw-r--r--chrome/test/data/extensions/test/TabsAPI/1/jstemplate_compiled.js1182
-rw-r--r--chrome/test/data/extensions/test/TabsAPI/1/manifest.json8
-rw-r--r--chrome/test/data/extensions/test/TabsAPI/1/tabs_api.html69
-rw-r--r--chrome/test/data/extensions/test/TabsAPI/Current Version1
8 files changed, 1449 insertions, 8 deletions
diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc
index f1f1379..a3625ab 100644
--- a/chrome/browser/extensions/extension_function_dispatcher.cc
+++ b/chrome/browser/extensions/extension_function_dispatcher.cc
@@ -47,7 +47,10 @@ FactoryRegistry::FactoryRegistry() {
// Register all functions here.
factories_["GetTabsForWindow"] =
&NewExtensionFunction<GetTabsForWindowFunction>;
+ factories_["GetTab"] = &NewExtensionFunction<GetTabFunction>;
factories_["CreateTab"] = &NewExtensionFunction<CreateTabFunction>;
+ factories_["UpdateTab"] = &NewExtensionFunction<UpdateTabFunction>;
+ factories_["RemoveTab"] = &NewExtensionFunction<RemoveTabFunction>;
}
void FactoryRegistry::GetAllNames(
diff --git a/chrome/browser/extensions/extension_tabs_module.cc b/chrome/browser/extensions/extension_tabs_module.cc
index 5d0f6f2..811bc97 100644
--- a/chrome/browser/extensions/extension_tabs_module.cc
+++ b/chrome/browser/extensions/extension_tabs_module.cc
@@ -14,6 +14,9 @@
static DictionaryValue* CreateTabValue(TabStripModel* tab_strip_model,
int tab_index);
+static bool GetIndexOfTabId(const TabStripModel* tab_strip, int tab_id,
+ int* tab_index);
+
bool GetTabsForWindowFunction::RunImpl() {
if (!args_->IsType(Value::TYPE_NULL))
return false;
@@ -41,20 +44,151 @@ bool CreateTabFunction::RunImpl() {
if (!browser)
return false;
- // TODO(aa): Handle all the other properties of the new tab.
+ TabStripModel *tab_strip = browser->tabstrip_model();
+ const DictionaryValue *args_hash = static_cast<const DictionaryValue*>(args_);
+
+ // TODO(rafaelw): handle setting remaining tab properties:
+ // -windowId
+ // -title
+ // -favIconUrl
+
std::string url;
- static_cast<const DictionaryValue*>(args_)->GetString(L"url", &url);
- browser->AddTabWithURL(GURL(url), GURL(), PageTransition::TYPED, true, -1,
- NULL);
+ args_hash->GetString(L"url", &url);
+
+ // Default to foreground for the new tab. The presence of 'selected' property
+ // will override this default.
+ bool selected = true;
+ args_hash->GetBoolean(L"selected", &selected);
+
+ // If index is specified, honor the value, but keep it bound to
+ // 0 <= index <= tab_strip->count()
+ int index = -1;
+ args_hash->GetInteger(L"index", &index);
+ if (index < 0) {
+ // Default insert behavior
+ index = -1;
+ }
+ if (index > tab_strip->count()) {
+ index = tab_strip->count();
+ }
+
+ TabContents* contents = browser->AddTabWithURL(GURL(url), GURL(),
+ PageTransition::TYPED, selected, index, NULL);
+ index = tab_strip->GetIndexOfTabContents(contents);
// Return data about the newly created tab.
if (has_callback())
- result_.reset(CreateTabValue(browser->tabstrip_model(),
- browser->tabstrip_model()->count() - 1));
+ result_.reset(CreateTabValue(tab_strip, index));
return true;
}
+bool GetTabFunction::RunImpl() {
+ if (!args_->IsType(Value::TYPE_INTEGER))
+ return false;
+
+ Browser* browser = BrowserList::GetLastActive();
+ if (!browser)
+ return false;
+
+ int tab_id;
+ args_->GetAsInteger(&tab_id);
+
+ int tab_index;
+ TabStripModel* tab_strip = browser->tabstrip_model();
+ // TODO(rafaelw): return an error if the tab is not found by |tab_id|
+ if (!GetIndexOfTabId(tab_strip, tab_id, &tab_index))
+ return false;
+
+ TabContents* tab_contents = tab_strip->GetTabContentsAt(tab_index);
+ NavigationController* controller = tab_contents->controller();
+ DCHECK(controller);
+ result_.reset(CreateTabValue(tab_strip, tab_index));
+ return true;
+}
+
+bool UpdateTabFunction::RunImpl() {
+ // TODO(aa): Do data-driven validation in JS.
+ if (!args_->IsType(Value::TYPE_DICTIONARY))
+ return false;
+
+ Browser* browser = BrowserList::GetLastActive();
+ if (!browser)
+ return false;
+
+ int tab_id;
+ const DictionaryValue *args_hash = static_cast<const DictionaryValue*>(args_);
+ if (!args_hash->GetInteger(L"id", &tab_id))
+ return false;
+
+ int tab_index;
+ TabStripModel* tab_strip = browser->tabstrip_model();
+ // TODO(rafaelw): return an error if the tab is not found by |tab_id|
+ if (!GetIndexOfTabId(tab_strip, tab_id, &tab_index))
+ return false;
+
+ TabContents* tab_contents = tab_strip->GetTabContentsAt(tab_index);
+ NavigationController* controller = tab_contents->controller();
+ DCHECK(controller);
+
+ // TODO(rafaelw): handle setting remaining tab properties:
+ // -index
+ // -windowId
+ // -title
+ // -favIconUrl
+
+ // Navigate the tab to a new location if the url different.
+ std::string url;
+ if (args_hash->GetString(L"url", &url)) {
+ GURL new_gurl(url);
+ if (new_gurl.is_valid()) {
+ controller->LoadURL(new_gurl, GURL(), PageTransition::TYPED);
+ } else {
+ // TODO(rafaelw): return some reasonable error?
+ }
+ }
+
+ bool selected;
+ // TODO(rafaelw): Setting |selected| from js doesn't make much sense.
+ // Move tab selection management up to window.
+ if (args_hash->GetBoolean(L"selected", &selected) &&
+ selected &&
+ tab_strip->selected_index() != tab_index) {
+ tab_strip->SelectTabContentsAt(tab_index, false);
+ }
+
+ return true;
+}
+
+
+bool RemoveTabFunction::RunImpl() {
+ // TODO(rafaelw): This should have a callback, but it can't because it could
+ // close it's own tab.
+
+ if (!args_->IsType(Value::TYPE_INTEGER))
+ return false;
+
+ Browser* browser = BrowserList::GetLastActive();
+ if (!browser)
+ return false;
+
+ int tab_id;
+ if (!args_->GetAsInteger(&tab_id)) {
+ return false;
+ }
+
+ int tab_index;
+ TabStripModel* tab_strip = browser->tabstrip_model();
+ if (GetIndexOfTabId(tab_strip, tab_id, &tab_index)) {
+ TabContents* tab_contents = tab_strip->GetTabContentsAt(tab_index);
+ NavigationController* controller = tab_contents->controller();
+ DCHECK(controller);
+ browser->CloseContents(tab_contents);
+ return true;
+ }
+
+ return false;
+}
// static helpers
static DictionaryValue* CreateTabValue(TabStripModel* tab_strip,
@@ -65,6 +199,7 @@ static DictionaryValue* CreateTabValue(TabStripModel* tab_strip,
DictionaryValue* result = new DictionaryValue();
result->SetInteger(L"id", controller->session_id().id());
+ result->SetInteger(L"index", tab_index);
result->SetInteger(L"windowId", controller->window_id().id());
result->SetString(L"url", contents->GetURL().spec());
result->SetString(L"title", UTF16ToWide(contents->GetTitle()));
@@ -78,3 +213,18 @@ static DictionaryValue* CreateTabValue(TabStripModel* tab_strip,
return result;
}
+
+static bool GetIndexOfTabId(const TabStripModel* tab_strip, int tab_id,
+ int* tab_index) {
+ for (int i = 0; i < tab_strip->count(); ++i) {
+ TabContents* tab_contents = tab_strip->GetTabContentsAt(i);
+ NavigationController* controller = tab_contents->controller();
+ DCHECK(controller); // TODO(aa): Is this a valid assumption?
+
+ if (controller->session_id().id() == tab_id) {
+ *tab_index = i;
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/chrome/browser/extensions/extension_tabs_module.h b/chrome/browser/extensions/extension_tabs_module.h
index 6dfffa5..c358529 100644
--- a/chrome/browser/extensions/extension_tabs_module.h
+++ b/chrome/browser/extensions/extension_tabs_module.h
@@ -13,5 +13,14 @@ class GetTabsForWindowFunction : public SyncExtensionFunction {
class CreateTabFunction : public SyncExtensionFunction {
virtual bool RunImpl();
};
+class GetTabFunction : public SyncExtensionFunction {
+ virtual bool RunImpl();
+};
+class UpdateTabFunction : public SyncExtensionFunction {
+ virtual bool RunImpl();
+};
+class RemoveTabFunction : public SyncExtensionFunction {
+ virtual bool RunImpl();
+};
#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_TABS_MODULE_H__
diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js
index 85d4d53..3a18c97 100644
--- a/chrome/renderer/resources/extension_process_bindings.js
+++ b/chrome/renderer/resources/extension_process_bindings.js
@@ -6,8 +6,15 @@ var chromium;
// callback handling
var callbacks = [];
chromium._dispatchCallback = function(callbackId, str) {
- callbacks[callbackId](goog.json.parse(str));
- delete callbacks[callbackId];
+ try {
+ if (str) {
+ callbacks[callbackId](goog.json.parse(str));
+ } else {
+ callbacks[callbackId]();
+ }
+ } finally {
+ delete callbacks[callbackId];
+ }
};
// Send an API request and optionally register a callback.
@@ -29,8 +36,20 @@ var chromium;
native function GetTabsForWindow();
sendRequest(GetTabsForWindow, null, callback);
};
+ chromium.tabs.getTab = function(tabId, callback) {
+ native function GetTab();
+ sendRequest(GetTab, tabId, callback);
+ };
chromium.tabs.createTab = function(tab, callback) {
native function CreateTab();
sendRequest(CreateTab, tab, callback);
};
+ chromium.tabs.updateTab = function(tab) {
+ native function UpdateTab();
+ sendRequest(UpdateTab, tab);
+ };
+ chromium.tabs.removeTab = function(tabId) {
+ native function RemoveTab();
+ sendRequest(RemoveTab, tabId);
+ };
})();
diff --git a/chrome/test/data/extensions/test/TabsAPI/1/jstemplate_compiled.js b/chrome/test/data/extensions/test/TabsAPI/1/jstemplate_compiled.js
new file mode 100644
index 0000000..2f62b31
--- /dev/null
+++ b/chrome/test/data/extensions/test/TabsAPI/1/jstemplate_compiled.js
@@ -0,0 +1,1182 @@
+/**
+ * @fileoverview This file contains miscellaneous basic functionality.
+ *
+ */
+
+/**
+ * Creates a DOM element with the given tag name in the document of the
+ * owner element.
+ *
+ * @param {String} tagName The name of the tag to create.
+ * @param {Element} owner The intended owner (i.e., parent element) of
+ * the created element.
+ * @param {Point} opt_position The top-left corner of the created element.
+ * @param {Size} opt_size The size of the created element.
+ * @param {Boolean} opt_noAppend Do not append the new element to the owner.
+ * @return {Element} The newly created element node.
+ */
+function createElement(tagName, owner, opt_position, opt_size, opt_noAppend) {
+ var element = ownerDocument(owner).createElement(tagName);
+ if (opt_position) {
+ setPosition(element, opt_position);
+ }
+ if (opt_size) {
+ setSize(element, opt_size);
+ }
+ if (owner && !opt_noAppend) {
+ appendChild(owner, element);
+ }
+
+ return element;
+}
+
+/**
+ * Creates a text node with the given value.
+ *
+ * @param {String} value The text to place in the new node.
+ * @param {Element} owner The owner (i.e., parent element) of the new
+ * text node.
+ * @return {Text} The newly created text node.
+ */
+function createTextNode(value, owner) {
+ var element = ownerDocument(owner).createTextNode(value);
+ if (owner) {
+ appendChild(owner, element);
+ }
+ return element;
+}
+
+/**
+ * Returns the document owner of the given element. In particular,
+ * returns window.document if node is null or the browser does not
+ * support ownerDocument.
+ *
+ * @param {Node} node The node whose ownerDocument is required.
+ * @returns {Document|Null} The owner document or null if unsupported.
+ */
+function ownerDocument(node) {
+ return (node ? node.ownerDocument : null) || document;
+}
+
+/**
+ * Wrapper function to create CSS units (pixels) string
+ *
+ * @param {Number} numPixels Number of pixels, may be floating point.
+ * @returns {String} Corresponding CSS units string.
+ */
+function px(numPixels) {
+ return round(numPixels) + "px";
+}
+
+/**
+ * Sets the left and top of the given element to the given point.
+ *
+ * @param {Element} element The dom element to manipulate.
+ * @param {Point} point The desired position.
+ */
+function setPosition(element, point) {
+ var style = element.style;
+ style.position = "absolute";
+ style.left = px(point.x);
+ style.top = px(point.y);
+}
+
+/**
+ * Sets the width and height style attributes to the given size.
+ *
+ * @param {Element} element The dom element to manipulate.
+ * @param {Size} size The desired size.
+ */
+function setSize(element, size) {
+ var style = element.style;
+ style.width = px(size.width);
+ style.height = px(size.height);
+}
+
+/**
+ * Sets display to none. Doing this as a function saves a few bytes for
+ * the 'style.display' property and the 'none' literal.
+ *
+ * @param {Element} node The dom element to manipulate.
+ */
+function displayNone(node) {
+ node.style.display = 'none';
+}
+
+/**
+ * Sets display to default.
+ *
+ * @param {Element} node The dom element to manipulate.
+ */
+function displayDefault(node) {
+ node.style.display = '';
+}
+
+/**
+ * Appends the given child to the given parent in the DOM
+ *
+ * @param {Element} parent The parent dom element.
+ * @param {Node} child The new child dom node.
+ */
+function appendChild(parent, child) {
+ parent.appendChild(child);
+}
+
+
+/**
+ * Wrapper for the eval() builtin function to evaluate expressions and
+ * obtain their value. It wraps the expression in parentheses such
+ * that object literals are really evaluated to objects. Without the
+ * wrapping, they are evaluated as block, and create syntax
+ * errors. Also protects against other syntax errors in the eval()ed
+ * code and returns null if the eval throws an exception.
+ *
+ * @param {String} expr
+ * @return {Object|Null}
+ */
+function jsEval(expr) {
+ try {
+ return eval('[' + expr + '][0]');
+ } catch (e) {
+ return null;
+ }
+}
+
+
+/**
+ * Wrapper for the eval() builtin function to execute statements. This
+ * guards against exceptions thrown, but doesn't return a
+ * value. Still, mostly for testability, it returns a boolean to
+ * indicate whether execution was successful. NOTE:
+ * javascript's eval semantics is murky in that it confounds
+ * expression evaluation and statement execution into a single
+ * construct. Cf. jsEval().
+ *
+ * @param {String} stmt
+ * @return {Boolean}
+ */
+function jsExec(stmt) {
+ try {
+ eval(stmt);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+
+/**
+ * Wrapper for eval with a context. NOTE: The style guide
+ * deprecates eval, so this is the exception that proves the
+ * rule. Notice also that since the value of the expression is
+ * returned rather than assigned to a local variable, one major
+ * objection aganist the use of the with() statement, namely that
+ * properties of the with() target override local variables of the
+ * same name, is void here.
+ *
+ * @param {String} expr
+ * @param {Object} context
+ * @return {Object|Null}
+ */
+function jsEvalWith(expr, context) {
+ try {
+ with (context) {
+ return eval('[' + expr + '][0]');
+ }
+ } catch (e) {
+ return null;
+ }
+}
+
+
+var DOM_ELEMENT_NODE = 1;
+var DOM_ATTRIBUTE_NODE = 2;
+var DOM_TEXT_NODE = 3;
+var DOM_CDATA_SECTION_NODE = 4;
+var DOM_ENTITY_REFERENCE_NODE = 5;
+var DOM_ENTITY_NODE = 6;
+var DOM_PROCESSING_INSTRUCTION_NODE = 7;
+var DOM_COMMENT_NODE = 8;
+var DOM_DOCUMENT_NODE = 9;
+var DOM_DOCUMENT_TYPE_NODE = 10;
+var DOM_DOCUMENT_FRAGMENT_NODE = 11;
+var DOM_NOTATION_NODE = 12;
+
+/**
+ * Traverses the element nodes in the DOM tree underneath the given
+ * node and finds the first node with elemId, or null if there is no such
+ * element. Traversal is in depth-first order.
+ *
+ * NOTE: The reason this is not combined with the elem() function is
+ * that the implementations are different.
+ * elem() is a wrapper for the built-in document.getElementById() function,
+ * whereas this function performs the traversal itself.
+ * Modifying elem() to take an optional root node is a possibility,
+ * but the in-built function would perform better than using our own traversal.
+ *
+ * @param {Element} node Root element of subtree to traverse.
+ * @param {String} elemId The id of the element to search for.
+ * @return {Element|Null} The corresponding element, or null if not found.
+ */
+function nodeGetElementById(node, elemId) {
+ for (var c = node.firstChild; c; c = c.nextSibling) {
+ if (c.id == elemId) {
+ return c;
+ }
+ if (c.nodeType == DOM_ELEMENT_NODE) {
+ var n = arguments.callee.call(this, c, elemId);
+ if (n) {
+ return n;
+ }
+ }
+ }
+ return null;
+}
+
+
+/**
+ * Get an attribute from the DOM. Simple redirect, exists to compress code.
+ *
+ * @param {Element} node Element to interrogate.
+ * @param {String} name Name of parameter to extract.
+ * @return {String} Resulting attribute.
+ */
+function domGetAttribute(node, name) {
+ return node.getAttribute(name);
+}
+
+/**
+ * Set an attribute in the DOM. Simple redirect to compress code.
+ *
+ * @param {Element} node Element to interrogate.
+ * @param {String} name Name of parameter to set.
+ * @param {String} value Set attribute to this value.
+ */
+function domSetAttribute(node, name, value) {
+ node.setAttribute(name, value);
+}
+
+/**
+ * Remove an attribute from the DOM. Simple redirect to compress code.
+ *
+ * @param {Element} node Element to interrogate.
+ * @param {String} name Name of parameter to remove.
+ */
+function domRemoveAttribute(node, name) {
+ node.removeAttribute(name);
+}
+
+/**
+ * Clone a node in the DOM.
+ *
+ * @param {Node} node Node to clone.
+ * @return {Node} Cloned node.
+ */
+function domCloneNode(node) {
+ return node.cloneNode(true);
+}
+
+
+/**
+ * Return a safe string for the className of a node.
+ * If className is not a string, returns "".
+ *
+ * @param {Element} node DOM element to query.
+ * @return {String}
+ */
+function domClassName(node) {
+ return node.className ? "" + node.className : "";
+}
+
+/**
+ * Adds a class name to the class attribute of the given node.
+ *
+ * @param {Element} node DOM element to modify.
+ * @param {String} className Class name to add.
+ */
+function domAddClass(node, className) {
+ var name = domClassName(node);
+ if (name) {
+ var cn = name.split(/\s+/);
+ var found = false;
+ for (var i = 0; i < jsLength(cn); ++i) {
+ if (cn[i] == className) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ cn.push(className);
+ }
+
+ node.className = cn.join(' ');
+ } else {
+ node.className = className;
+ }
+}
+
+/**
+ * Removes a class name from the class attribute of the given node.
+ *
+ * @param {Element} node DOM element to modify.
+ * @param {String} className Class name to remove.
+ */
+function domRemoveClass(node, className) {
+ var c = domClassName(node);
+ if (!c || c.indexOf(className) == -1) {
+ return;
+ }
+ var cn = c.split(/\s+/);
+ for (var i = 0; i < jsLength(cn); ++i) {
+ if (cn[i] == className) {
+ cn.splice(i--, 1);
+ }
+ }
+ node.className = cn.join(' ');
+}
+
+/**
+ * Checks if a node belongs to a style class.
+ *
+ * @param {Element} node DOM element to test.
+ * @param {String} className Class name to check for.
+ * @return {Boolean} Node belongs to style class.
+ */
+function domTestClass(node, className) {
+ var cn = domClassName(node).split(/\s+/);
+ for (var i = 0; i < jsLength(cn); ++i) {
+ if (cn[i] == className) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Inserts a new child before a given sibling.
+ *
+ * @param {Node} newChild Node to insert.
+ * @param {Node} oldChild Sibling node.
+ * @return {Node} Reference to new child.
+ */
+function domInsertBefore(newChild, oldChild) {
+ return oldChild.parentNode.insertBefore(newChild, oldChild);
+}
+
+/**
+ * Appends a new child to the specified (parent) node.
+ *
+ * @param {Element} node Parent element.
+ * @param {Node} child Child node to append.
+ * @return {Node} Newly appended node.
+ */
+function domAppendChild(node, child) {
+ return node.appendChild(child);
+}
+
+/**
+ * Remove a new child from the specified (parent) node.
+ *
+ * @param {Element} node Parent element.
+ * @param {Node} child Child node to remove.
+ * @return {Node} Removed node.
+ */
+function domRemoveChild(node, child) {
+ return node.removeChild(child);
+}
+
+/**
+ * Replaces an old child node with a new child node.
+ *
+ * @param {Node} newChild New child to append.
+ * @param {Node} oldChild Old child to remove.
+ * @return {Node} Replaced node.
+ */
+function domReplaceChild(newChild, oldChild) {
+ return oldChild.parentNode.replaceChild(newChild, oldChild);
+}
+
+/**
+ * Removes a node from the DOM.
+ *
+ * @param {Node} node The node to remove.
+ * @return {Node} The removed node.
+ */
+function domRemoveNode(node) {
+ return domRemoveChild(node.parentNode, node);
+}
+
+/**
+ * Creates a new text node in the given document.
+ *
+ * @param {Document} doc Target document.
+ * @param {String} text Text composing new text node.
+ * @return {Text} Newly constructed text node.
+ */
+function domCreateTextNode(doc, text) {
+ return doc.createTextNode(text);
+}
+
+/**
+ * Creates a new node in the given document
+ *
+ * @param {Document} doc Target document.
+ * @param {String} name Name of new element (i.e. the tag name)..
+ * @return {Element} Newly constructed element.
+ */
+function domCreateElement(doc, name) {
+ return doc.createElement(name);
+}
+
+/**
+ * Creates a new attribute in the given document.
+ *
+ * @param {Document} doc Target document.
+ * @param {String} name Name of new attribute.
+ * @return {Attr} Newly constructed attribute.
+ */
+function domCreateAttribute(doc, name) {
+ return doc.createAttribute(name);
+}
+
+/**
+ * Creates a new comment in the given document.
+ *
+ * @param {Document} doc Target document.
+ * @param {String} text Comment text.
+ * @return {Comment} Newly constructed comment.
+ */
+function domCreateComment(doc, text) {
+ return doc.createComment(text);
+}
+
+/**
+ * Creates a document fragment.
+ *
+ * @param {Document} doc Target document.
+ * @return {DocumentFragment} Resulting document fragment node.
+ */
+function domCreateDocumentFragment(doc) {
+ return doc.createDocumentFragment();
+}
+
+/**
+ * Redirect to document.getElementById
+ *
+ * @param {Document} doc Target document.
+ * @param {String} id Id of requested node.
+ * @return {Element|Null} Resulting element.
+ */
+function domGetElementById(doc, id) {
+ return doc.getElementById(id);
+}
+
+/**
+ * Redirect to window.setInterval
+ *
+ * @param {Window} win Target window.
+ * @param {Function} fun Callback function.
+ * @param {Number} time Time in milliseconds.
+ * @return {Object} Contract id.
+ */
+function windowSetInterval(win, fun, time) {
+ return win.setInterval(fun, time);
+}
+
+/**
+ * Redirect to window.clearInterval
+ *
+ * @param {Window} win Target window.
+ * @param {object} id Contract id.
+ * @return {any} NOTE: Return type unknown?
+ */
+function windowClearInterval(win, id) {
+ return win.clearInterval(id);
+}
+
+/**
+ * Determines whether one node is recursively contained in another.
+ * @param parent The parent node.
+ * @param child The node to look for in parent.
+ * @return parent recursively contains child
+ */
+function containsNode(parent, child) {
+ while (parent != child && child.parentNode) {
+ child = child.parentNode;
+ }
+ return parent == child;
+};
+/**
+ * @fileoverview This file contains javascript utility functions that
+ * do not depend on anything defined elsewhere.
+ *
+ */
+
+/**
+ * Returns the value of the length property of the given object. Used
+ * to reduce compiled code size.
+ *
+ * @param {Array | String} a The string or array to interrogate.
+ * @return {Number} The value of the length property.
+ */
+function jsLength(a) {
+ return a.length;
+}
+
+var min = Math.min;
+var max = Math.max;
+var ceil = Math.ceil;
+var floor = Math.floor;
+var round = Math.round;
+var abs = Math.abs;
+
+/**
+ * Copies all properties from second object to the first. Modifies to.
+ *
+ * @param {Object} to The target object.
+ * @param {Object} from The source object.
+ */
+function copyProperties(to, from) {
+ foreachin(from, function(p) {
+ to[p] = from[p];
+ });
+}
+
+/**
+ * Iterates over the array, calling the given function for each
+ * element.
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ */
+function foreach(array, fn) {
+ var I = jsLength(array);
+ for (var i = 0; i < I; ++i) {
+ fn(array[i], i);
+ }
+}
+
+/**
+ * Safely iterates over all properties of the given object, calling
+ * the given function for each property. If opt_all isn't true, uses
+ * hasOwnProperty() to assure the property is on the object, not on
+ * its prototype.
+ *
+ * @param {Object} object
+ * @param {Function} fn
+ * @param {Boolean} opt_all If true, also iterates over inherited properties.
+ */
+function foreachin(object, fn, opt_all) {
+ for (var i in object) {
+ if (opt_all || !object.hasOwnProperty || object.hasOwnProperty(i)) {
+ fn(i, object[i]);
+ }
+ }
+}
+
+/**
+ * Appends the second array to the first, copying its elements.
+ * Optionally only a slice of the second array is copied.
+ *
+ * @param {Array} a1 Target array (modified).
+ * @param {Array} a2 Source array.
+ * @param {Number} opt_begin Begin of slice of second array (optional).
+ * @param {Number} opt_end End (exclusive) of slice of second array (optional).
+ */
+function arrayAppend(a1, a2, opt_begin, opt_end) {
+ var i0 = opt_begin || 0;
+ var i1 = opt_end || jsLength(a2);
+ for (var i = i0; i < i1; ++i) {
+ a1.push(a2[i]);
+ }
+}
+
+/**
+ * Trim whitespace from begin and end of string.
+ *
+ * @see testStringTrim();
+ *
+ * @param {String} str Input string.
+ * @return {String} Trimmed string.
+ */
+function stringTrim(str) {
+ return stringTrimRight(stringTrimLeft(str));
+}
+
+/**
+ * Trim whitespace from beginning of string.
+ *
+ * @see testStringTrimLeft();
+ *
+ * @param {String} str Input string.
+ * @return {String} Trimmed string.
+ */
+function stringTrimLeft(str) {
+ return str.replace(/^\s+/, "");
+}
+
+/**
+ * Trim whitespace from end of string.
+ *
+ * @see testStringTrimRight();
+ *
+ * @param {String} str Input string.
+ * @return {String} Trimmed string.
+ */
+function stringTrimRight(str) {
+ return str.replace(/\s+$/, "");
+}
+
+/**
+ * Jscompiler wrapper for parseInt() with base 10.
+ *
+ * @param {String} s String repersentation of a number.
+ *
+ * @return {Number} The integer contained in s, converted on base 10.
+ */
+function parseInt10(s) {
+ return parseInt(s, 10);
+}
+/**
+ * @fileoverview A simple formatter to project JavaScript data into
+ * HTML templates. The template is edited in place. I.e. in order to
+ * instantiate a template, clone it from the DOM first, and then
+ * process the cloned template. This allows for updating of templates:
+ * If the templates is processed again, changed values are merely
+ * updated.
+ *
+ * NOTE: IE DOM doesn't have importNode().
+ *
+ * NOTE: The property name "length" must not be used in input
+ * data, see comment in jstSelect_().
+ */
+
+
+/**
+ * Names of jstemplate attributes. These attributes are attached to
+ * normal HTML elements and bind expression context data to the HTML
+ * fragment that is used as template.
+ */
+var ATT_select = 'jsselect';
+var ATT_instance = 'jsinstance';
+var ATT_display = 'jsdisplay';
+var ATT_values = 'jsvalues';
+var ATT_eval = 'jseval';
+var ATT_transclude = 'transclude';
+var ATT_content = 'jscontent';
+
+
+/**
+ * Names of special variables defined by the jstemplate evaluation
+ * context. These can be used in js expression in jstemplate
+ * attributes.
+ */
+var VAR_index = '$index';
+var VAR_this = '$this';
+
+
+/**
+ * Context for processing a jstemplate. The context contains a context
+ * object, whose properties can be referred to in jstemplate
+ * expressions, and it holds the locally defined variables.
+ *
+ * @param {Object} opt_data The context object. Null if no context.
+ *
+ * @param {Object} opt_parent The parent context, from which local
+ * variables are inherited. Normally the context object of the parent
+ * context is the object whose property the parent object is. Null for the
+ * context of the root object.
+ *
+ * @constructor
+ */
+function JsExprContext(opt_data, opt_parent) {
+ var me = this;
+
+ /**
+ * The local context of the input data in which the jstemplate
+ * expressions are evaluated. Notice that this is usually an Object,
+ * but it can also be a scalar value (and then still the expression
+ * $this can be used to refer to it). Notice this can be a scalar
+ * value, including undefined.
+ *
+ * @type {Object}
+ */
+ me.data_ = opt_data;
+
+ /**
+ * The context for variable definitions in which the jstemplate
+ * expressions are evaluated. Other than for the local context,
+ * which replaces the parent context, variable definitions of the
+ * parent are inherited. The special variable $this points to data_.
+ *
+ * @type {Object}
+ */
+ me.vars_ = {};
+ if (opt_parent) {
+ copyProperties(me.vars_, opt_parent.vars_);
+ }
+ this.vars_[VAR_this] = me.data_;
+}
+
+
+/**
+ * Evaluates the given expression in the context of the current
+ * context object and the current local variables.
+ *
+ * @param {String} expr A javascript expression.
+ *
+ * @param {Element} template DOM node of the template.
+ *
+ * @return The value of that expression.
+ */
+JsExprContext.prototype.jseval = function(expr, template) {
+ with (this.vars_) {
+ with (this.data_) {
+ try {
+ return (function() {
+ return eval('[' + expr + '][0]');
+ }).call(template);
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+}
+
+
+/**
+ * Clones the current context for a new context object. The cloned
+ * context has the data object as its context object and the current
+ * context as its parent context. It also sets the $index variable to
+ * the given value. This value usually is the position of the data
+ * object in a list for which a template is instantiated multiply.
+ *
+ * @param {Object} data The new context object.
+ *
+ * @param {Number} index Position of the new context when multiply
+ * instantiated. (See implementation of jstSelect().)
+ *
+ * @return {JsExprContext}
+ */
+JsExprContext.prototype.clone = function(data, index) {
+ var ret = new JsExprContext(data, this);
+ ret.setVariable(VAR_index, index);
+ if (this.resolver_) {
+ ret.setSubTemplateResolver(this.resolver_);
+ }
+ return ret;
+}
+
+
+/**
+ * Binds a local variable to the given value. If set from jstemplate
+ * jsvalue expressions, variable names must start with $, but in the
+ * API they only have to be valid javascript identifier.
+ *
+ * @param {String} name
+ *
+ * @param {Object} value
+ */
+JsExprContext.prototype.setVariable = function(name, value) {
+ this.vars_[name] = value;
+}
+
+
+/**
+ * Sets the function used to resolve the values of the transclude
+ * attribute into DOM nodes. By default, this is jstGetTemplate(). The
+ * value set here is inherited by clones of this context.
+ *
+ * @param {Function} resolver The function used to resolve transclude
+ * ids into a DOM node of a subtemplate. The DOM node returned by this
+ * function will be inserted into the template instance being
+ * processed. Thus, the resolver function must instantiate the
+ * subtemplate as necessary.
+ */
+JsExprContext.prototype.setSubTemplateResolver = function(resolver) {
+ this.resolver_ = resolver;
+}
+
+
+/**
+ * Resolves a sub template from an id. Used to process the transclude
+ * attribute. If a resolver function was set using
+ * setSubTemplateResolver(), it will be used, otherwise
+ * jstGetTemplate().
+ *
+ * @param {String} id The id of the sub template.
+ *
+ * @return {Node} The root DOM node of the sub template, for direct
+ * insertion into the currently processed template instance.
+ */
+JsExprContext.prototype.getSubTemplate = function(id) {
+ return (this.resolver_ || jstGetTemplate).call(this, id);
+}
+
+
+/**
+ * HTML template processor. Data values are bound to HTML templates
+ * using the attributes transclude, jsselect, jsdisplay, jscontent,
+ * jsvalues. The template is modifed in place. The values of those
+ * attributes are JavaScript expressions that are evaluated in the
+ * context of the data object fragment.
+ *
+ * @param {JsExprContext} context Context created from the input data
+ * object.
+ *
+ * @param {Element} template DOM node of the template. This will be
+ * processed in place. After processing, it will still be a valid
+ * template that, if processed again with the same data, will remain
+ * unchanged.
+ */
+function jstProcess(context, template) {
+ var processor = new JstProcessor();
+ processor.run_([ processor, processor.jstProcess_, context, template ]);
+}
+
+
+/**
+ * Internal class used by jstemplates to maintain context.
+ * NOTE: This is necessary to process deep templates in Safari
+ * which has a relatively shallow stack.
+ * @class
+ */
+function JstProcessor() {
+}
+
+
+/**
+ * Runs the state machine, beginning with function "start".
+ *
+ * @param {Array} start The first function to run, in the form
+ * [object, method, args ...]
+ */
+JstProcessor.prototype.run_ = function(start) {
+ var me = this;
+
+ me.queue_ = [ start ];
+ while (jsLength(me.queue_)) {
+ var f = me.queue_.shift();
+ f[1].apply(f[0], f.slice(2));
+ }
+}
+
+
+/**
+ * Appends a function to be called later.
+ * Analogous to calling that function on a subsequent line, or a subsequent
+ * iteration of a loop.
+ *
+ * @param {Array} f A function in the form [object, method, args ...]
+ */
+JstProcessor.prototype.enqueue_ = function(f) {
+ this.queue_.push(f);
+}
+
+
+/**
+ * Implements internals of jstProcess.
+ *
+ * @param {JsExprContext} context
+ *
+ * @param {Element} template
+ */
+JstProcessor.prototype.jstProcess_ = function(context, template) {
+ var me = this;
+
+ var transclude = domGetAttribute(template, ATT_transclude);
+ if (transclude) {
+ var tr = context.getSubTemplate(transclude);
+ if (tr) {
+ domReplaceChild(tr, template);
+ me.enqueue_([ me, me.jstProcess_, context, tr ]);
+ } else {
+ domRemoveNode(template);
+ }
+ return;
+ }
+
+ var select = domGetAttribute(template, ATT_select);
+ if (select) {
+ me.jstSelect_(context, template, select);
+ return;
+ }
+
+ var display = domGetAttribute(template, ATT_display);
+ if (display) {
+ if (!context.jseval(display, template)) {
+ displayNone(template);
+ return;
+ }
+
+ displayDefault(template);
+ }
+
+
+ var values = domGetAttribute(template, ATT_values);
+ if (values) {
+ me.jstValues_(context, template, values);
+ }
+
+ var expressions = domGetAttribute(template, ATT_eval);
+ if (expressions) {
+ foreach(expressions.split(/\s*;\s*/), function(expression) {
+ expression = stringTrim(expression);
+ if (jsLength(expression)) {
+ context.jseval(expression, template);
+ }
+ });
+ }
+
+ var content = domGetAttribute(template, ATT_content);
+ if (content) {
+ me.jstContent_(context, template, content);
+
+ } else {
+ var childnodes = [];
+ for (var i = 0; i < jsLength(template.childNodes); ++i) {
+ if (template.childNodes[i].nodeType == DOM_ELEMENT_NODE) {
+ me.enqueue_(
+ [ me, me.jstProcess_, context, template.childNodes[i] ]);
+ }
+ }
+ }
+}
+
+
+/**
+ * Implements the jsselect attribute: evalutes the value of the
+ * jsselect attribute in the current context, with the current
+ * variable bindings (see JsExprContext.jseval()). If the value is an
+ * array, the current template node is multiplied once for every
+ * element in the array, with the array element being the context
+ * object. If the array is empty, or the value is undefined, then the
+ * current template node is dropped. If the value is not an array,
+ * then it is just made the context object.
+ *
+ * @param {JsExprContext} context The current evaluation context.
+ *
+ * @param {Element} template The currently processed node of the template.
+ *
+ * @param {String} select The javascript expression to evaluate.
+ *
+ * @param {Function} process The function to continue processing with.
+ */
+JstProcessor.prototype.jstSelect_ = function(context, template, select) {
+ var me = this;
+
+ var value = context.jseval(select, template);
+ domRemoveAttribute(template, ATT_select);
+
+ var instance = domGetAttribute(template, ATT_instance);
+ var instance_last = false;
+ if (instance) {
+ if (instance.charAt(0) == '*') {
+ instance = parseInt10(instance.substr(1));
+ instance_last = true;
+ } else {
+ instance = parseInt10(instance);
+ }
+ }
+
+ var multiple = (value !== null &&
+ typeof value == 'object' &&
+ typeof value.length == 'number');
+ var multiple_empty = (multiple && value.length == 0);
+
+ if (multiple) {
+ if (multiple_empty) {
+ if (!instance) {
+ domSetAttribute(template, ATT_select, select);
+ domSetAttribute(template, ATT_instance, '*0');
+ displayNone(template);
+ } else {
+ domRemoveNode(template);
+ }
+
+ } else {
+ displayDefault(template);
+ if (instance === null || instance === "" || instance === undefined ||
+ (instance_last && instance < jsLength(value) - 1)) {
+ var templatenodes = [];
+ var instances_start = instance || 0;
+ for (var i = instances_start + 1; i < jsLength(value); ++i) {
+ var node = domCloneNode(template);
+ templatenodes.push(node);
+ domInsertBefore(node, template);
+ }
+ templatenodes.push(template);
+
+ for (var i = 0; i < jsLength(templatenodes); ++i) {
+ var ii = i + instances_start;
+ var v = value[ii];
+ var t = templatenodes[i];
+
+ me.enqueue_([ me, me.jstProcess_, context.clone(v, ii), t ]);
+ var instanceStr = (ii == jsLength(value) - 1 ? '*' : '') + ii;
+ me.enqueue_(
+ [ null, postProcessMultiple_, t, select, instanceStr ]);
+ }
+
+ } else if (instance < jsLength(value)) {
+ var v = value[instance];
+
+ me.enqueue_(
+ [me, me.jstProcess_, context.clone(v, instance), template]);
+ var instanceStr = (instance == jsLength(value) - 1 ? '*' : '')
+ + instance;
+ me.enqueue_(
+ [ null, postProcessMultiple_, template, select, instanceStr ]);
+ } else {
+ domRemoveNode(template);
+ }
+ }
+ } else {
+ if (value == null) {
+ domSetAttribute(template, ATT_select, select);
+ displayNone(template);
+ } else {
+ me.enqueue_(
+ [ me, me.jstProcess_, context.clone(value, 0), template ]);
+ me.enqueue_(
+ [ null, postProcessSingle_, template, select ]);
+ }
+ }
+}
+
+
+/**
+ * Sets ATT_select and ATT_instance following recursion to jstProcess.
+ *
+ * @param {Element} template The template
+ *
+ * @param {String} select The jsselect string
+ *
+ * @param {String} instanceStr The new value for the jsinstance attribute
+ */
+function postProcessMultiple_(template, select, instanceStr) {
+ domSetAttribute(template, ATT_select, select);
+ domSetAttribute(template, ATT_instance, instanceStr);
+}
+
+
+/**
+ * Sets ATT_select and makes the element visible following recursion to
+ * jstProcess.
+ *
+ * @param {Element} template The template
+ *
+ * @param {String} select The jsselect string
+ */
+function postProcessSingle_(template, select) {
+ domSetAttribute(template, ATT_select, select);
+ displayDefault(template);
+}
+
+
+/**
+ * Implements the jsvalues attribute: evaluates each of the values and
+ * assigns them to variables in the current context (if the name
+ * starts with '$', javascript properties of the current template node
+ * (if the name starts with '.'), or DOM attributes of the current
+ * template node (otherwise). Since DOM attribute values are always
+ * strings, the value is coerced to string in the latter case,
+ * otherwise it's the uncoerced javascript value.
+ *
+ * @param {JsExprContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {String} valuesStr Value of the jsvalues attribute to be
+ * processed.
+ */
+JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) {
+ var values = valuesStr.split(/\s*;\s*/);
+ for (var i = 0; i < jsLength(values); ++i) {
+ var colon = values[i].indexOf(':');
+ if (colon < 0) {
+ continue;
+ }
+ var label = stringTrim(values[i].substr(0, colon));
+ var value = context.jseval(values[i].substr(colon + 1), template);
+
+ if (label.charAt(0) == '$') {
+ context.setVariable(label, value);
+
+ } else if (label.charAt(0) == '.') {
+ var nameSpaceLabel = label.substr(1).split('.');
+ var nameSpaceObject = template;
+ var nameSpaceDepth = jsLength(nameSpaceLabel);
+ for (var j = 0, J = nameSpaceDepth - 1; j < J; ++j) {
+ var jLabel = nameSpaceLabel[j];
+ if (!nameSpaceObject[jLabel]) {
+ nameSpaceObject[jLabel] = {};
+ }
+ nameSpaceObject = nameSpaceObject[jLabel];
+ }
+ nameSpaceObject[nameSpaceLabel[nameSpaceDepth - 1]] = value;
+ } else if (label) {
+ if (typeof value == 'boolean') {
+ if (value) {
+ domSetAttribute(template, label, label);
+ } else {
+ domRemoveAttribute(template, label);
+ }
+ } else {
+ domSetAttribute(template, label, '' + value);
+ }
+ }
+ }
+}
+
+
+/**
+ * Implements the jscontent attribute. Evalutes the expression in
+ * jscontent in the current context and with the current variables,
+ * and assigns its string value to the content of the current template
+ * node.
+ *
+ * @param {JsExprContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {String} content Value of the jscontent attribute to be
+ * processed.
+ */
+JstProcessor.prototype.jstContent_ = function(context, template, content) {
+ var value = '' + context.jseval(content, template);
+ if (template.innerHTML == value) {
+ return;
+ }
+ while (template.firstChild) {
+ domRemoveNode(template.firstChild);
+ }
+ var t = domCreateTextNode(ownerDocument(template), value);
+ domAppendChild(template, t);
+}
+
+
+/**
+ * Helps to implement the transclude attribute, and is the initial
+ * call to get hold of a template from its ID.
+ *
+ * @param {String} name The ID of the HTML element used as template.
+ *
+ * @returns {Element} The DOM node of the template. (Only element
+ * nodes can be found by ID, hence it's a Element.)
+ */
+function jstGetTemplate(name) {
+ var section = domGetElementById(document, name);
+ if (section) {
+ var ret = domCloneNode(section);
+ domRemoveAttribute(ret, 'id');
+ return ret;
+ } else {
+ return null;
+ }
+}
+
+window['jstGetTemplate'] = jstGetTemplate;
+window['jstProcess'] = jstProcess;
+window['JsExprContext'] = JsExprContext;
diff --git a/chrome/test/data/extensions/test/TabsAPI/1/manifest.json b/chrome/test/data/extensions/test/TabsAPI/1/manifest.json
new file mode 100644
index 0000000..b4b5e4d
--- /dev/null
+++ b/chrome/test/data/extensions/test/TabsAPI/1/manifest.json
@@ -0,0 +1,8 @@
+{
+ "id": "1234567890123456789012345678901234567890",
+ "name": "TabsAPI",
+ "description": "Utility for working with the extension tabs api",
+ "version": "0.1",
+ "format_version": 1,
+ "toolstrips": ["tabs_api.html"]
+}
diff --git a/chrome/test/data/extensions/test/TabsAPI/1/tabs_api.html b/chrome/test/data/extensions/test/TabsAPI/1/tabs_api.html
new file mode 100644
index 0000000..1ee9417
--- /dev/null
+++ b/chrome/test/data/extensions/test/TabsAPI/1/tabs_api.html
@@ -0,0 +1,69 @@
+<html>
+<head>
+<script src="jstemplate_compiled.js" type="text/javascript"></script>
+<script>
+var tabs = {};
+
+function loadTabList() {
+ chromium.tabs.getTabsForWindow(function(tabList) {
+ tabs = tabList;
+
+ var input = new JsExprContext(tabList);
+ var output = document.getElementById('tabList');
+ jstProcess(input, output);
+ });
+}
+
+function getTabData(id) {
+ return {
+ 'id': id,
+ 'index': parseInt(document.getElementById('index_' + id).value),
+ 'windowId': document.getElementById('windowId_' + id).value,
+ 'title': document.getElementById('title_' + id).value,
+ 'url': document.getElementById('url_' + id).value,
+ 'selected': document.getElementById('selected_' + id).value ? true : false
+ }
+}
+
+function updateTab(id){
+ chromium.tabs.updateTab(getTabData(id));
+}
+
+function createTab() {
+ chromium.tabs.createTab(getTabData('new'));
+}
+
+function updateAll() {
+ for (var i = 0; i < tabs.length; i++) {
+ chromium.tabs.updateTab(getTabData(tabs[i].id));
+ }
+}
+</script>
+</head>
+ <body onload="loadTabList();">
+ <div id="tabList">
+ <div style="background-color: #EEEEEE; margin: 8px; padding: 4px" jsselect="$this">
+ <div jscontent="'TabId: ' + id"></div>
+ <div> index: <input style="width: 90%" type="text" jsvalues="value:$this.index;id:'index_' + id" /></div>
+ <div> windowId: <input style="width: 40px" type="text" jsvalues="value:windowId;id:'windowId_' + id" /></div>
+ <div> title: <input style="width: 90%" type="text" jsvalues="value:title;id:'title_' + id" /></div>
+ <div> url: <input style="width: 90%" type="text" jsvalues="value:url;id:'url_' + id" /></div>
+ <div jscontent="url"></div>
+ <div> <input type="checkbox" jsvalues="checked:selected; id:'selected_' + id" /> Selected</div>
+ <button onclick="updateTab(this.jstdata)" jsvalues=".jstdata:id">Update Tab</button>
+ <button onclick="chromium.tabs.removeTab(this.jstdata);" jsvalues=".jstdata:id">Close Tab</button>
+ </div>
+ </div>
+ <div style="background-color: #9999EE; margin: 8px; padding: 4px">
+ <div> index: <input style="width: 90%" type="text" id="index_new" /></div>
+ <div> windowId: <input style="width: 40px" type="text" id="windowId_new" /></div>
+ <div> title: <input style="width: 90%" type="text" id="title_new" /></div>
+ <div> url: <input style="width: 90%" type="text" id="url_new" /></div>
+ <div> <input type="checkbox" id="selected_new" /> Selected</div>
+ <button onclick="createTab();">Create Tab</button>
+ </div>
+ <button onclick="loadTabList();">Refresh</button>
+ <button onclick="updateAll();">Update All</button>
+ </body>
+</html>
+
diff --git a/chrome/test/data/extensions/test/TabsAPI/Current Version b/chrome/test/data/extensions/test/TabsAPI/Current Version
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/chrome/test/data/extensions/test/TabsAPI/Current Version
@@ -0,0 +1 @@
+1 \ No newline at end of file