summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
authorbattre@chromium.org <battre@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-03-14 10:24:13 +0000
committerbattre@chromium.org <battre@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-03-14 10:24:13 +0000
commit0583e2b917d85db0f301ac5994c64c4e30da678b (patch)
treea34bcfd795b2a6c449229f87f60b1a98daa1a39f /chrome
parentc6b65bfd7201db65f8b5f188e5688d2bdf93cb81 (diff)
downloadchromium_src-0583e2b917d85db0f301ac5994c64c4e30da678b.zip
chromium_src-0583e2b917d85db0f301ac5994c64c4e30da678b.tar.gz
chromium_src-0583e2b917d85db0f301ac5994c64c4e30da678b.tar.bz2
Sample extension demonstrating usage of the Proxy API.
BUG=62700 TEST=Install extension, ensure that nothing explodes, change proxy settings, profit! Review URL: http://codereview.chromium.org/6538077 Patch from Mike West <mkwst@chromium.org>. git-svn-id: svn://svn.chromium.org/chrome/trunk/src@78014 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration.zipbin0 -> 97150 bytes
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/_locales/en/messages.json50
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/background.html28
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/manifest.json16
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/popup.html297
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_error_handler.js105
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_form_controller.js744
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/jsunittest.js964
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.html113
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.js516
-rw-r--r--chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/unittest.css58
-rw-r--r--chrome/common/extensions/docs/samples.html78
-rw-r--r--chrome/common/extensions/docs/samples.json40
13 files changed, 3008 insertions, 1 deletions
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration.zip b/chrome/common/extensions/docs/examples/extensions/proxy_configuration.zip
new file mode 100644
index 0000000..07b7ad0
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration.zip
Binary files differ
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/_locales/en/messages.json b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/_locales/en/messages.json
new file mode 100644
index 0000000..b05ac21
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/_locales/en/messages.json
@@ -0,0 +1,50 @@
+{
+ "extName": {
+ "message": "Proxy Settings",
+ "description": "The extension name."
+ },
+ "extDescription": {
+ "message": "Set Chrome-specific proxies; a demonstration of Chrome's Proxy API",
+ "description": "The extension description."
+ },
+ "headerDirectConnection": {
+ "message": "Direct Connection",
+ "description": "Header for 'Direct Connection' configuration `fieldset`."
+ },
+ "errorNoExtensionAccess": {
+ "message": "Sorry. This browser's proxy settings cannot be controlled via extensions.",
+ "description": "Error message displayed when `levelOfControl` is 'NotControllable'."
+ },
+ "errorOtherExtensionControls": {
+ "message": "Sorry. This browser's proxy settings are being controlled by another extension. Please visit chrome://extensions for details.",
+ "description": "Error message displayed when `levelOfControl` is 'ControlledByOtherExtensions'."
+ },
+ "errorSettingRegularProxy": {
+ "message": "Setting regular proxy settings failed. Sorry!",
+ "description": "Error message, displayed when failing to set regular proxy settings."
+ },
+ "errorSettingIncognitoProxy": {
+ "message": "Setting incognito proxy settings failed. Sorry!",
+ "description": "Error message, displayed when failing to set incognito proxy settings."
+ },
+ "successfullySetProxy": {
+ "message": "Your proxy settings have been saved successfully; this window will close automagically. Have a nice day!",
+ "description": "Success message, displayed after proxy settings have been written."
+ },
+ "errorPopupTitle": {
+ "message": "Error: $1",
+ "description": "Error message used as popup title."
+ },
+ "errorProxyError": {
+ "message": "ProxyError: $1",
+ "description": "Error message displayed in popup when an error occurs."
+ },
+ "errorIdNotFound": {
+ "message": "Element with ID `$1` doesn't exist in the document",
+ "description": "Error message thrown when the given `id` doesn't exist"
+ },
+ "errorIdNotForm": {
+ "message": "Element with ID `$1` isn't a form element.",
+ "description": "Error message thrown when the given `id` isn't a form element."
+ }
+}
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/background.html b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/background.html
new file mode 100644
index 0000000..4c4fccb
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/background.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Proxy Configuration Extension Background Page</title>
+ </head>
+ <body>
+ <!--
+ Here, we'll hook into the proxy extension API's `onProxyError`
+ event, and use it to set a warning badge on the browser action's icon.
+ Additionally, we'll store proxy settings locally, and reset them when the
+ background page initializes. This is essential, as incognito settings
+ are wiped on restart.
+ -->
+ <script src="./proxy_form_controller.js"></script>
+ <script src="./proxy_error_handler.js"></script>
+ <script>
+ var errorHandler = new ProxyErrorHandler();
+
+ // If this extension has already set the proxy settings, then reset it
+ // once as the background page initializes. This is essential, as
+ // incognito settings are wiped on restart.
+ var persistedSettings = ProxyFormController.getPersistedSettings();
+ if (persistedSettings !== null) {
+ chrome.experimental.proxy.settings.set({'value': persistedSettings});
+ }
+ </script>
+ </body>
+</html>
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/manifest.json b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/manifest.json
new file mode 100644
index 0000000..1870843
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/manifest.json
@@ -0,0 +1,16 @@
+{
+ "name": "__MSG_extName__",
+ "version": "0.1",
+ "description": "__MSG_extDescription__",
+ "default_locale": "en",
+ "browser_action": {
+ "default_icon": "icon.png",
+ "popup": "popup.html"
+ },
+ "background_page": "background.html",
+ "permissions": [
+ "experimental",
+ "proxy",
+ "TTS"
+ ]
+}
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/popup.html b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/popup.html
new file mode 100644
index 0000000..8f0c39f
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/popup.html
@@ -0,0 +1,297 @@
+<!doctype html>
+<html>
+<head>
+ <title>Popup for Proxy API Test</title>
+ <style>
+ body {
+ margin: 5px 10px 10px;
+ }
+
+ h1 {
+ color: #53637D;
+ font: 26px/1.2 Helvetica, sans-serif;
+ font-size: 200%;
+ margin: 0;
+ padding-bottom: 4px;
+ text-shadow: white 0 1px 2px;
+ }
+
+ div[role='main'] {
+ border-radius: 5px;
+ background: #EAEEF3;
+ font: 14px/1 Arial,Sans Serif;
+ padding: 10px;
+ width: 563px;
+ box-shadow: inset 0px 2px 5px rgba(0,0,0,0.5);
+ -webkit-transition: background-color 0.5s ease-out;
+ overflow: hidden;
+ }
+
+ div[role='main'].incognito {
+ background: #496281 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAAAhCAYAAABeD2IVAAAFz0lEQVRYw8VYC0xcRRQtUEprERQ1VkohSGNpwaCYYNBUjS2K+KEVTUkDiqY0hWD5xxhDINgQqIZviRAkSiiWNDRxWypNpaatEQk/CwvlWyks3+XPAssu7I7nbmabx+suPzfrTU7yljfz5sy95947w5YtpjcLwBLYBuzgsAGs+DuzGZGwBh4BHIDdwHPA8xxuwBOcnIWpPUAL7wQeB54CngYcAWdO4kXgDeBoWFhYSk5OTtmFCxeuxsbGpuBv3sCTwFZTkrLmXtgH+AJ+QAAQCBzz8/P7MiUl5aebN29Kh4eHFVNTU2xoaIgNDAywe/fusYyMjO858R2mJEUe2ldZWfljd3f3P+3t7bK2trZBuVyuYCJbXFxkXV1drLOzU4e7d++yzMzMasz3AexMSYq89CoRYuuwvr6+B6Q6OjpYUVFRG+Yf4iE0iZYoc5woVK2trUPrIUXh05Mir9XW1qow/33gmf8idj0ZypjHuIgj5ubm1OshpdFoVoTwzp07zMbGJsTb23s/16fFZlKcyDjY29u7+fr6HoJFV1dXt7EN2MjIyANSUqmUubq6Jtna2r7OvWW7kUy04IR2+fj4HDx+/HhUYmJiIQQuhZ5mN0JKqVSuEHtWVlbd6dOns+3s7N7h9YtKynbuhFWNQuYQGBh4ODs7u2B0dHRauNDMzIx2eXmZabXadRG7f/++jhSylV25ckXOk2D4yJEjSVjnNV7ntq9Fily6Jzg4+CTEOqf/OJFoampSFRQUjDc3Ny+QF9RqtU47hozGE3mUDIbxOtTX1ytQt5T6MQipFGu9zDPbYq0i+ey5c+dyxAvBSxrsXHXixIlmVOsOaGykp6dnYXZ2lt49BHhIWVdXN1VaWipLS0trCQ8P/xseUwrD6+HhEUJO4BFa3VMREREx4+PjCkNhunjx4iAWuI0x16KjowlVYkA7uncYczUyMpJ+3z579mzXwsLCA9fimcXHx6djvb3cGatmHrnTBxr4jSZShaYwCQnC89PQXFNUVNSvMTExv6C/XYqLi6sg0DMISRISEq7h+QY81dHY2DgpDq9CoWCYm8Fbl8FmLaxLROoA8OGpU6dKL1++LCMdGdIPEkF569atIYlE0oNNdGFsNz03NDTI0QOVhrRG31GpVCR4JcrEV1jnFWOZaMVrBx07vIDDQBiQil3XQENDMplseWlpiW3WBOLXovWoy8vLBxA+CTT6NdZ5gR9vtooFvis1NfWTioqKn6uqqv6ihius4NCYZn5+Xuf2zRAi4OSgQQIsT05OrnA71qrmR6BtQlL0wwVpL1nPIqQzCgFpzlB50HuFxlF2TkxMUKiXxBKgb4Dooqen5zHeX63FWUfl/22k/aix+qM3IkEeS05Obs7Pz6+vqakZgAcUY2Njur8T2cHBwSXobbSwsPCPgICAAny7QJgstAaVjry8vEquKwexpiz5WccrJCTkDO1yLWIILcP474DPgQ8oMahpA9+C0FxJSUk7nr/h2vwMSAfhJWFJQAKN8Xku4tCJveWP3d8gb6zWToh0aGjoJYz/FHgToGYbTMmBUM0RnJyccvE7GkiEVlv1cylhpqenGbI7l4v8UWNVfRsn5efs7Jxx/vz5fr0+VvGWuqysrMbNzS0R88KDgoLyEMo+/XvyDI4tcmHC6GsUSkgL5rxFCWbsxGDFY/oSdn+mt7d3gj5w/fp15VrENmokbvRANTbyBdbbz8/tBgsn1agD/v7+cUh7lfAj2KmGmdAoI9PT0//kOnQ01vesedF8t7+/X27oQ0hbrSmJoXBKucD3GAsd3VS8cDIoY2YykgM6RS4/Zj8kckt+sfTDeaeHmdFwHxzj98Y94qKpOz+5u7ufZGY28haOyOX89rzCW9SZ3YuLi39g/4Mhy4ex/kHejFeQ2tvS0vI79ShqE+YEmjxLSkoK5aQsheGjW6snv8EeBT42Az4C3uP/l3DhyWYhvuPZ84PWbt6tzQFH7pCdvCzoSP0LtBi6oflBr2wAAAAASUVORK5CYII=') no-repeat 533px bottom;
+ }
+
+ form {
+ width: 563px;
+ -webkit-transition: -webkit-transform 0.25s ease;
+ }
+
+ form.offscreen {
+ -webkit-transform: translateX(-600px);
+ }
+
+ fieldset {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ }
+
+ legend {
+ position: absolute;
+ left: -999em;
+ }
+
+ form > fieldset {
+ border-radius: 5px;
+ border: 1px solid transparent;
+ padding: 10px 10px 10px 30px;
+ margin: 5px 0;
+ -webkit-transition: all 0.5s ease;
+ }
+
+ form > fieldset:hover {
+ background: rgba(255,255,255,0.1);
+ border-color: rgba(0,0,0,0.1);
+ }
+
+ form > fieldset.active {
+ background: rgba(255,255,255,0.25);
+ border-color: rgba(0,0,0,0.25);
+ }
+
+ form > fieldset > input {
+ margin-left: -20px;
+ }
+
+ section {
+ margin: 5px 0 0;
+ }
+
+ section fieldset:not(:first-child):not(:last-child) {
+ -webkit-transition: all 0.5s ease;
+ overflow: hidden;
+ max-height: 1.6em;
+ }
+
+ section.single fieldset:not(:first-child):not(:last-child) {
+ max-height: 0px;
+ }
+
+ section fieldset:last-child {
+ margin-top: 5px;
+ }
+
+ section fieldset:last-child label {
+ display: block;
+ }
+
+ section fieldset:last-child textarea {
+ width: 412px;
+ }
+
+ section > fieldset {
+ position: relative;
+ padding-left: 60px;
+ }
+
+ section > fieldset > legend {
+ left: 0;
+ top: 4px;
+ width: 53px;
+ text-align: right;
+ }
+
+ input[type='url']:invalid:not(:active):not(:focus) {
+ border-color: rgba(255,0,0,0.5);
+ background: rgba(255,0,0,0.25);
+ }
+
+ input:invalid:not(:active):not(:focus):after {
+ content: "This isn't a valid URL!";
+ display:block;
+ }
+
+ input[type="checkbox"] {
+ margin: 5px 0 5px 35px;
+ }
+
+ input[type="text"] {
+ width: 200px;
+ margin: 0 10px 0 0;
+ }
+
+ input[type="number"] {
+ width: 50px;
+ margin: 2px 10px 0 5px;
+ }
+
+ section label, section legend {
+ color: #999;
+ -webkit-transition: color 0.5s ease;
+ }
+
+ .incognito section label, .incognito section legend {
+ color: #BBB;
+ }
+
+ .active section label, .active section legend, form > fieldset > label {
+ color: #000;
+ -webkit-transition: color 0.5s ease;
+ }
+
+ .incognito .active section label, .incognito .active section legend, .incognito form > fieldset > label {
+ color: #FFF;
+ }
+
+ input[type="submit"], button {
+ border-radius: 2px;
+ box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
+ -webkit-user-select: none;
+ background: -webkit-linear-gradient(#FAFAFA, #F4F4F4 40%, #E5E5E5);
+ border: 1px solid #AAA;
+ color: #444;
+ margin-bottom: 0;
+ min-width: 4em;
+ padding: 3px 12px;
+ margin-top: 0;
+ font-size: 1.1em;
+ }
+
+ .overlay {
+ display: block;
+ text-align: center;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 350px;
+ padding: 20px;
+ margin: -80px 0 0 -195px;
+ opacity: 0;
+ background: rgba(0, 0, 0, 0.75);
+ border-radius: 5px;
+ color: #FFF;
+ font: 1.5em/1.2 Helvetica Neue, sans-serif;
+ -webkit-transition: all 1.0s ease;
+ }
+
+ .overlay.visible {
+ opacity: 1;
+ }
+ </style>
+</head>
+<body>
+ <h1>Proxy Configuration</h1>
+ <div role="main">
+ <form id="proxyForm">
+ <fieldset id="system">
+ <legend>System Settings</legend>
+ <input type="radio" name="proxyType" id="proxyTypeSystem" value="system">
+ <label for="proxyTypeSystem">Use the <em>system's proxy settings</em>.</label>
+ </fieldset>
+ <fieldset id="direct">
+ <legend>Direct Connection</legend>
+ <input type="radio" name="proxyType" id="proxyTypeDirect" value="direct">
+ <label for="proxyTypeDirect">Your computer is <em>directly connected</em> to the internet; no need for a proxy.</label>
+ </fieldset>
+ <fieldset id="pac_script">
+ <legend>Automatic Configuration</legend>
+ <input type="radio" name="proxyType" id="proxyTypeAutoconfig" value="autoconfig">
+ <label for="proxyTypeAutoconfig">Your proxy supports <em>automatic configuration</em>.</label>
+
+ <section>
+ <label for="autoconfigURL">Autoconfiguration URL (PAC file)</label>
+ <input type="url" name="autoconfigURL" id="autoconfigURL">
+ <input type="hidden" name="autoconfigData" id="autoconfigData">
+ </section>
+ </fieldset>
+ <fieldset id="fixed_servers">
+ <legend>Manual Proxy</legend>
+ <input type="radio" name="proxyType" id="proxyTypeManual" value="manual">
+ <label for="proxyTypeManual">Configure your proxy settings <em>manually</em>.</label>
+ <section>
+ <fieldset>
+ <legend>HTTP</legend>
+ <label for="proxyHostHttp">Host</label>
+ <select id="proxySchemeHttp" name="proxySchemeHttp">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostHttp" id="proxyHostHttp">
+
+ <label for="proxyPortHttp">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortHttp" id="proxyPortHttp">
+
+ <input type="checkbox" name="singleProxyForEverything" id="singleProxyForEverything">
+ <label for="singleProxyForEverything">Use the same proxy server for all protocols</label>
+ </fieldset>
+ <fieldset>
+ <legend>HTTPS</legend>
+ <label for="proxyHostHttps">Host</label>
+ <select id="proxySchemeHttps" name="proxySchemeHttps">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostHttps" id="proxyHostHttps">
+
+ <label for="proxyPortHttps">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortHttps" id="proxyPortHttps">
+ </fieldset>
+ <fieldset>
+ <legend>FTP</legend>
+ <label for="proxyHostFtp">Host</label>
+ <select id="proxySchemeFtp" name="proxySchemeFtp">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostFtp" id="proxyHostFtp">
+
+ <label for="proxyPortFtp">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortFtp" id="proxyPortFtp">
+ </fieldset>
+ <fieldset>
+ <legend>Fallback</legend>
+ <label for="proxyHostFallback">Host</label>
+ <select id="proxySchemeFallback" name="proxySchemeFallback">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostFallback" id="proxyHostFallback">
+
+ <label for="proxyPortFallback">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortFallback" id="proxyPortFallback">
+ </fieldset>
+ <fieldset>
+ <label for="bypassList">Bypass proxy for these hosts:</label>
+ <textarea id="bypassList" name="bypassList" placeholder="<local>,192.168.1.1/16, .example.com"></textarea>
+ </fieldset>
+ </section>
+ </fieldset>
+ <input type="submit" value="Save proxy settings">
+ <button value="incognito">Configure incognito window settings.</button>
+ </form>
+ </div>
+ <script src="./proxy_form_controller.js"></script>
+ <script>
+ var c = new ProxyFormController( 'proxyForm' );
+ </script>
+</body>
+</html>
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_error_handler.js b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_error_handler.js
new file mode 100644
index 0000000..d5fb404
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_error_handler.js
@@ -0,0 +1,105 @@
+// Copyright (c) 2011 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.
+
+/**
+ * @fileoverview This file implements the ProxyErrorHandler class, which will
+ * flag proxy errors in a visual way for the extension's user.
+ *
+ * @author Mike West <mkwst@google.com>
+ */
+
+
+/**
+ * The proxy error handling object. Binds to the 'onProxyError' event, and
+ * changes the extensions badge to reflect the error state (yellow for
+ * non-fatal errors, red for fatal).
+ *
+ * @constructor
+ */
+function ProxyErrorHandler() {
+ // Handle proxy error events.
+ chrome.experimental.proxy.onProxyError.addListener(
+ this.handleError.bind(this));
+
+ // Handle message events from popup.
+ chrome.extension.onRequest.addListener(
+ this.handleOnRequest_.bind(this));
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * @typedef {{fatal: boolean, error: string, details: string}}
+ */
+ProxyErrorHandler.ErrorDetails;
+
+///////////////////////////////////////////////////////////////////////////////
+
+ProxyErrorHandler.prototype = {
+ /**
+ * Details of the most recent error.
+ * @type {?ProxyErrorHandler.ErrorDetails}
+ * @private
+ */
+ lastError_: null,
+
+ /**
+ * Handle request messages from the popup.
+ *
+ * @param {!{type:string}} request The external request to answer.
+ * @param {!MessageSender} sender Info about the script context that sent
+ * the request.
+ * @param {!function} sendResponse Function to call to send a response.
+ * @private
+ */
+ handleOnRequest_: function(request, sender, sendResponse) {
+ if (request.type === 'getError') {
+ sendResponse({result: this.getErrorDetails()});
+ } else if (request.type === 'clearError') {
+ this.clearErrorDetails();
+ sendResponse({result: true});
+ }
+ },
+
+ /**
+ * Handles the error event, storing the error details for later use, and
+ * badges the browser action icon.
+ *
+ * @param {!ProxyErrorHandler.ErrorDetails} details The error details.
+ * @private
+ */
+ handleError_: function(details) {
+ var RED = [255, 0, 0, 255];
+ var YELLOW = [255, 205, 0, 255];
+
+ // Badge the popup icon.
+ var color = details.fatal ? RED : YELLOW;
+ chrome.browserAction.setBadgeBackgroundColor({color: color});
+ chrome.browserAction.setBadgeText({text: 'X'});
+ chrome.browserAction.setTitle({
+ 'title': chrome.i18n.getMessage('errorPopupTitle', details.error)
+ });
+
+ // Store the error for display in the popup.
+ this.lastError_ = JSON.stringify(details);
+ },
+
+
+ /**
+ * Returns details of the last error handled.
+ *
+ * @return {?ProxyErrorHandler.ErrorDetails}
+ */
+ getErrorDetails: function() {
+ return this.lastError_;
+ },
+
+
+ /**
+ * Clears last handled error.
+ */
+ clearErrorDetails: function() {
+ this.lastError_ = null;
+ }
+}
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_form_controller.js b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_form_controller.js
new file mode 100644
index 0000000..08223d4
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_form_controller.js
@@ -0,0 +1,744 @@
+// Copyright (c) 2011 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.
+
+/**
+ * @fileoverview This file implements the ProxyFormController class, which
+ * wraps a form element with logic that enables implementation of proxy
+ * settings.
+ *
+ * @author mkwst@google.com (Mike West)
+ */
+
+/**
+ * Wraps the proxy configuration form, binding proper handlers to its various
+ * `change`, `click`, etc. events in order to take appropriate action in
+ * response to user events.
+ *
+ * @param {string} id The form's DOM ID.
+ * @constructor
+ */
+var ProxyFormController = function(id) {
+ /**
+ * The wrapped form element
+ * @type {Node}
+ * @private
+ */
+ this.form_ = document.getElementById(id);
+
+ // Throw an error if the element either doesn't exist, or isn't a form.
+ if (!this.form_)
+ throw chrome.i18n.getMessage('errorIdNotFound', id);
+ else if (this.form_.nodeName !== 'FORM')
+ throw chrome.i18n.getMessage('errorIdNotForm', id);
+
+ /**
+ * Cached references to the `fieldset` groups that define the configuration
+ * options presented to the user.
+ *
+ * @type {NodeList}
+ * @private
+ */
+ this.configGroups_ = document.querySelectorAll('#' + id + ' > fieldset');
+
+ this.bindEventHandlers_();
+ this.readCurrentState_();
+
+ // Handle errors
+ this.handleProxyErrors_();
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * The proxy types we're capable of handling.
+ * @enum {string}
+ */
+ProxyFormController.ProxyTypes = {
+ AUTO: 'auto_detect',
+ PAC: 'pac_script',
+ DIRECT: 'direct',
+ FIXED: 'fixed_servers',
+ SYSTEM: 'system'
+};
+
+/**
+ * The window types we're capable of handling.
+ * @enum {int}
+ */
+ProxyFormController.WindowTypes = {
+ REGULAR: 1,
+ INCOGNITO: 2
+};
+
+/**
+ * The extension's level of control of Chrome's roxy setting
+ * @enum {string}
+ */
+ProxyFormController.LevelOfControl = {
+ NOT_CONTROLLABLE: 'NotControllable',
+ OTHER_EXTENSION: 'ControlledByOtherExtension',
+ AVAILABLE: 'ControllableByThisExtension',
+ CONTROLLING: 'ControlledByThisExtension'
+};
+
+/**
+ * The response type from 'proxy.settings.get'
+ *
+ * @typedef {{value: ProxyConfig,
+ * levelOfControl: ProxyFormController.LevelOfControl}}
+ */
+ProxyFormController.WrappedProxyConfig;
+
+///////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Retrieves proxy settings that have been persisted across restarts.
+ *
+ * @return {?ProxyConfig} The persisted proxy configuration, or null if no
+ * value has been persisted.
+ * @static
+ */
+ProxyFormController.getPersistedSettings = function() {
+ var result = JSON.parse(window.localStorage['proxyConfig']);
+ return result ? result : null;
+};
+
+
+/**
+ * Persists proxy settings across restarts.
+ *
+ * @param {!ProxyConfig} config The proxy config to persist.
+ * @static
+ */
+ProxyFormController.setPersistedSettings = function(config) {
+ window.localStorage['proxyConfig'] = JSON.stringify(config);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+ProxyFormController.prototype = {
+ /**
+ * The form's current state.
+ * @type {regular: ?ProxyConfig, incognito: ?ProxyConfig}
+ * @private
+ */
+ config_: {regular: null, incognito: null},
+
+
+ /**
+ * @return {string} The PAC file URL (or an empty string).
+ */
+ get pacURL() {
+ return document.getElementById('autoconfigURL').value;
+ },
+
+
+ /**
+ * @param {!string} value The PAC file URL.
+ */
+ set pacURL(value) {
+ document.getElementById('autoconfigURL').value = value;
+ },
+
+
+ /**
+ * @return {string} The PAC file data (or an empty string).
+ */
+ get manualPac() {
+ return document.getElementById('autoconfigData').value;
+ },
+
+
+ /**
+ * @param {!string} value The PAC file data.
+ */
+ set manualPac(value) {
+ document.getElementById('autoconfigData').value = value;
+ },
+
+
+ /**
+ * @return {Array.<string>} A list of hostnames that should bypass the proxy.
+ */
+ get bypassList() {
+ return document.getElementById('bypassList').value.split(/\s*(?:,|^)\s*/m);
+ },
+
+
+ /**
+ * @param {?Array.<string>} data A list of hostnames that should bypass
+ * the proxy. If empty, the bypass list is emptied.
+ */
+ set bypassList(data) {
+ if (!data)
+ data = [];
+ document.getElementById('bypassList').value = data.join(', ');
+ },
+
+
+ /**
+ * @see http://code.google.com/chrome/extensions/trunk/experimental.proxy.html
+ * @return {?ProxyServer} An object containing the proxy server host, port,
+ * and scheme. If null, there is no single proxy.
+ */
+ get singleProxy() {
+ var checkbox = document.getElementById('singleProxyForEverything');
+ return checkbox.checked ? this.httpProxy : null;
+ },
+
+
+ /**
+ * @see http://code.google.com/chrome/extensions/trunk/experimental.proxy.html
+ * @param {?ProxyServer} data An object containing the proxy server host,
+ * port, and scheme. If null, the single proxy checkbox will be unchecked.
+ */
+ set singleProxy(data) {
+ var checkbox = document.getElementById('singleProxyForEverything');
+ checkbox.checked = !!data;
+
+ if (data)
+ this.httpProxy = data;
+
+ if (checkbox.checked)
+ checkbox.parentNode.parentNode.classList.add('single');
+ else
+ checkbox.parentNode.parentNode.classList.remove('single');
+ },
+
+ /**
+ * @return {?ProxyServer} An object containing the proxy server host, port
+ * and scheme.
+ */
+ get httpProxy() {
+ return this.getProxyImpl_('Http');
+ },
+
+
+ /**
+ * @param {?ProxyServer} data An object containing the proxy server host,
+ * port, and scheme. If empty, empties the proxy setting.
+ */
+ set httpProxy(data) {
+ this.setProxyImpl_('Http', data);
+ },
+
+
+ /**
+ * @return {?ProxyServer} An object containing the proxy server host, port
+ * and scheme.
+ */
+ get httpsProxy() {
+ return this.getProxyImpl_('Https');
+ },
+
+
+ /**
+ * @param {?ProxyServer} data An object containing the proxy server host,
+ * port, and scheme. If empty, empties the proxy setting.
+ */
+ set httpsProxy(data) {
+ this.setProxyImpl_('Https', data);
+ },
+
+
+ /**
+ * @return {?ProxyServer} An object containing the proxy server host, port
+ * and scheme.
+ */
+ get ftpProxy() {
+ return this.getProxyImpl_('Ftp');
+ },
+
+
+ /**
+ * @param {?ProxyServer} data An object containing the proxy server host,
+ * port, and scheme. If empty, empties the proxy setting.
+ */
+ set ftpProxy(data) {
+ this.setProxyImpl_('Ftp', data);
+ },
+
+
+ /**
+ * @return {?ProxyServer} An object containing the proxy server host, port
+ * and scheme.
+ */
+ get fallbackProxy() {
+ return this.getProxyImpl_('Fallback');
+ },
+
+
+ /**
+ * @param {?ProxyServer} data An object containing the proxy server host,
+ * port, and scheme. If empty, empties the proxy setting.
+ */
+ set fallbackProxy(data) {
+ this.setProxyImpl_('Fallback', data);
+ },
+
+
+ /**
+ * @param {string} type The type of proxy that's being set ("Http",
+ * "Https", etc.).
+ * @return {?ProxyServer} An object containing the proxy server host,
+ * port, and scheme.
+ * @private
+ */
+ getProxyImpl_: function(type) {
+ var result = {
+ scheme: document.getElementById('proxyScheme' + type).value,
+ host: document.getElementById('proxyHost' + type).value,
+ port: parseInt(document.getElementById('proxyPort' + type).value, 10)
+ };
+ return (result.scheme && result.host && result.port) ? result : undefined;
+ },
+
+
+ /**
+ * A generic mechanism for setting proxy data.
+ *
+ * @see http://code.google.com/chrome/extensions/trunk/experimental.proxy.html
+ * @param {string} type The type of proxy that's being set ("Http",
+ * "Https", etc.).
+ * @param {?ProxyServer} data An object containing the proxy server host,
+ * port, and scheme. If empty, empties the proxy setting.
+ * @private
+ */
+ setProxyImpl_: function(type, data) {
+ if (!data)
+ data = {scheme: 'http', host: '', port: ''};
+
+ document.getElementById('proxyScheme' + type).value = data.scheme;
+ document.getElementById('proxyHost' + type).value = data.host;
+ document.getElementById('proxyPort' + type).value = data.port;
+ },
+
+///////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Calls the proxy API to read the current settings, and populates the form
+ * accordingly.
+ *
+ * @private
+ */
+ readCurrentState_: function() {
+ chrome.experimental.proxy.settings.get({incognito: false},
+ this.handleRegularState_.bind(this));
+ chrome.experimental.proxy.settings.get({incognito: true},
+ this.handleIncognitoState_.bind(this));
+ },
+
+ /**
+ * Handles the response from 'proxy.settings.get' for regular settings.
+ *
+ * @param {ProxyFormController.WrappedProxyConfig} c The proxy data and
+ * extension's level of control thereof.
+ * @private
+ */
+ handleRegularState_: function(c) {
+ if (c.levelOfControl === ProxyFormController.LevelOfControl.AVAILABLE ||
+ c.levelOfControl === ProxyFormController.LevelOfControl.CONTROLLING) {
+ this.recalcFormValues_(c.value);
+ this.config_.regular = c.value;
+ } else {
+ this.handleLackOfControl_(c.levelOfControl);
+ }
+ },
+
+ /**
+ * Handles the response from 'proxy.settings.get' for incognito settings.
+ *
+ * @param {ProxyFormController.WrappedProxyConfig} c The proxy data and
+ * extension's level of control thereof.
+ * @private
+ */
+ handleIncognitoState_: function(c) {
+ if (c.levelOfControl === ProxyFormController.LevelOfControl.AVAILABLE ||
+ c.levelOfControl === ProxyFormController.LevelOfControl.CONTROLLING) {
+ if (this.isIncognitoMode_())
+ this.recalcFormValues_(c.value);
+
+ this.config_.incognito = c.value;
+ } else {
+ this.handleLackOfControl_(c.levelOfControl);
+ }
+ },
+
+ /**
+ * Binds event handlers for the various bits and pieces of the form that
+ * are interesting to the controller.
+ *
+ * @private
+ */
+ bindEventHandlers_: function() {
+ this.form_.addEventListener('click', this.dispatchFormClick_.bind(this));
+ },
+
+
+ /**
+ * When a `click` event is triggered on the form, this function handles it by
+ * analyzing the context, and dispatching the click to the correct handler.
+ *
+ * @param {Event} e The event to be handled.
+ * @private
+ * @return {boolean} True if the event should bubble, false otherwise.
+ */
+ dispatchFormClick_: function(e) {
+ var t = e.target;
+
+ // Case 1: "Apply"
+ if (t.nodeName === 'INPUT' && t.getAttribute('type') === 'submit') {
+ return this.applyChanges_(e);
+
+ // Case 2: "Use the same proxy for all protocols" in an active section
+ } else if (t.nodeName === 'INPUT' &&
+ t.getAttribute('type') === 'checkbox' &&
+ t.parentNode.parentNode.parentNode.classList.contains('active')
+ ) {
+ return this.toggleSingleProxyConfig_(e);
+
+ // Case 3: "Flip to incognito mode."
+ } else if (t.nodeName === 'BUTTON') {
+ return this.toggleIncognitoMode_(e);
+
+ // Case 4: Click on something random: maybe changing active config group?
+ } else {
+ // Walk up the tree until we hit `form > fieldset` or fall off the top
+ while (t && (t.nodeName !== 'FIELDSET' ||
+ t.parentNode.nodeName !== 'FORM')) {
+ t = t.parentNode;
+ }
+ if (t) {
+ this.changeActive_(t);
+ return false;
+ }
+ }
+ return true;
+ },
+
+
+ /**
+ * Sets the form's active config group.
+ *
+ * @param {DOMElement} fieldset The configuration group to activate.
+ * @private
+ */
+ changeActive_: function(fieldset) {
+ for (var i = 0; i < this.configGroups_.length; i++) {
+ var el = this.configGroups_[i];
+ var radio = el.querySelector("input[type='radio']");
+ if (el === fieldset) {
+ el.classList.add('active');
+ radio.checked = true;
+ } else {
+ el.classList.remove('active');
+ }
+ }
+ this.recalcDisabledInputs_();
+ },
+
+
+ /**
+ * Recalculates the `disabled` state of the form's input elements, based
+ * on the currently active group, and that group's contents.
+ *
+ * @private
+ */
+ recalcDisabledInputs_: function() {
+ var i, j;
+ for (i = 0; i < this.configGroups_.length; i++) {
+ var el = this.configGroups_[i];
+ var inputs = el.querySelectorAll(
+ "input:not([type='radio']), select, textarea");
+ if (el.classList.contains('active')) {
+ for (j = 0; j < inputs.length; j++) {
+ inputs[j].removeAttribute('disabled');
+ }
+ } else {
+ for (j = 0; j < inputs.length; j++) {
+ inputs[j].setAttribute('disabled', 'disabled');
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Handler called in response to click on form's submission button. Generates
+ * the proxy configuration and passes it to `useCustomProxySettings`, or
+ * handles errors in user input.
+ *
+ * @param {Event} e DOM event generated by the user's click.
+ * @private
+ */
+ applyChanges_: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.isIncognitoMode_())
+ this.config_.incognito = this.generateProxyConfig_();
+ else
+ this.config_.regular = this.generateProxyConfig_();
+
+ chrome.experimental.proxy.settings.set(
+ {value: this.config_.regular, incognito: false},
+ this.callbackForRegularSettings_.bind(this));
+ },
+
+ /**
+ * Called in response to setting a regular window's proxy settings: checks
+ * for `lastError`, and then sets incognito settings (if they exist).
+ *
+ * @private
+ */
+ callbackForRegularSettings_: function() {
+ if (!chrome.extension.lastError) {
+ this.generateAlert(chrome.i18n.getMessage('errorSettingRegularProxy'));
+ return;
+ }
+ if (this.config_.incognito) {
+ chrome.experimental.proxy.settings.set(
+ {value: this.config_.incognito, incognito: true},
+ this.callbackForIncognitoSettings_.bind(this));
+ } else {
+ ProxyFormController.setPersistedSettings(this.config_);
+ this.generateAlert_(chrome.i18n.getMessage('successfullySetProxy'));
+ }
+ },
+
+ /**
+ * Called in response to setting an incognito window's proxy settings: checks
+ * for `lastError` and sets a success message.
+ *
+ * @private
+ */
+ callbackForIncognitoSettings_: function() {
+ if (!chrome.extension.lastError) {
+ this.generateAlert(chrome.i18n.getMessage('errorSettingIncognitoProxy'));
+ return;
+ }
+ ProxyFormController.setPersistedSettings(this.config_);
+ this.generateAlert_(
+ chrome.i18n.getMessage('successfullySetProxy'));
+ },
+
+ /**
+ * Generates an alert overlay inside the proxy's popup, then closes the popup
+ * after a short delay.
+ *
+ * @param {string} msg The message to be displayed in the overlay.
+ * @param {?boolean} close Should the window be closed? Defaults to true.
+ * @private
+ */
+ generateAlert_: function(msg, close) {
+ var success = document.createElement('p');
+ success.classList.add('overlay');
+ success.setAttribute('role', 'alert');
+ success.innerText = msg;
+ document.body.appendChild(success);
+
+ setTimeout(function() { success.classList.add('visible'); }, 10);
+ setTimeout(function() {
+ if (close === false)
+ success.classList.remove('visible');
+ else
+ window.close();
+ }, 3000);
+ },
+
+
+ /**
+ * Parses the proxy configuration form, and generates a ProxyConfig object
+ * that can be passed to `useCustomProxyConfig`.
+ *
+ * @see http://code.google.com/chrome/extensions/trunk/experimental.proxy.html
+ * @return {ProxyConfig} The proxy configuration represented by the form.
+ * @private
+ */
+ generateProxyConfig_: function() {
+ var active = document.getElementsByClassName('active')[0];
+ switch (active.id) {
+ case ProxyFormController.ProxyTypes.SYSTEM:
+ return {mode: 'system'};
+ case ProxyFormController.ProxyTypes.DIRECT:
+ return {mode: 'direct'};
+ case ProxyFormController.ProxyTypes.PAC:
+ var pacScriptURL = this.pacURL;
+ var pacManual = this.manualPac;
+ if (pacScriptURL)
+ return {mode: 'pac_script', pacScript: {url: pacScriptURL}};
+ else if (pacManual)
+ return {mode: 'pac_script', pacScript: {data: pacManual}};
+ else
+ return {mode: 'auto_detect'};
+ case ProxyFormController.ProxyTypes.FIXED:
+ var config = {mode: 'fixed_servers'};
+ if (this.singleProxy) {
+ config.rules = {singleProxy: this.singleProxy};
+ } else {
+ config.rules = {
+ proxyForHttp: this.httpProxy,
+ proxyForHttps: this.httpsProxy,
+ proxyForFtp: this.ftpProxy,
+ fallbackProxy: this.fallbackProxy,
+ bypassList: this.bypassList
+ };
+ }
+ return config;
+ }
+ },
+
+
+ /**
+ * Sets the proper display classes based on the "Use the same proxy server
+ * for all protocols" checkbox. Expects to be called as an event handler
+ * when that field is clicked.
+ *
+ * @param {Event} e The `click` event to respond to.
+ * @private
+ */
+ toggleSingleProxyConfig_: function(e) {
+ var checkbox = e.target;
+ if (checkbox.nodeName === 'INPUT' &&
+ checkbox.getAttribute('type') === 'checkbox') {
+ if (checkbox.checked)
+ checkbox.parentNode.parentNode.classList.add('single');
+ else
+ checkbox.parentNode.parentNode.classList.remove('single');
+ }
+ },
+
+
+ /**
+ * Returns the form's current incognito status.
+ *
+ * @return {boolean} True if the form is in incognito mode, false otherwise.
+ * @private
+ */
+ isIncognitoMode_: function(e) {
+ return this.form_.parentNode.classList.contains('incognito');
+ },
+
+
+ /**
+ * Toggles the form's incognito mode. Saves the current state to an object
+ * property for later use, clears the form, and toggles the appropriate state.
+ *
+ * @param {Event} e The `click` event to respond to.
+ * @private
+ */
+ toggleIncognitoMode_: function(e) {
+ var div = this.form_.parentNode;
+ var button = document.getElementsByTagName('button')[0];
+ if (this.isIncognitoMode_()) {
+ // In incognito mode, switching to cognito.
+ this.config_.incognito = this.generateProxyConfig_();
+ div.classList.remove('incognito');
+ this.recalcFormValues_(this.config_.regular);
+ button.innerText = 'Configure incognito window settings.';
+ } else {
+ // In cognito mode, switching to incognito.
+ this.config_.regular = this.generateProxyConfig_();
+ div.classList.add('incognito');
+ this.recalcFormValues_(this.config_.incognito);
+ button.innerText = 'Configure cognito window settings.';
+ }
+
+ // Cancel the button click.
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+
+ /**
+ * Sets the form's values based on a ProxyConfig.
+ *
+ * @param {!ProxyConfig} c The ProxyConfig object.
+ * @private
+ */
+ recalcFormValues_: function(c) {
+ // Normalize `auto_detect`
+ if (c.mode === 'auto_detect')
+ c.mode = 'pac_script';
+ // Activate one of the groups, based on `mode`.
+ this.changeActive_(document.getElementById(c.mode));
+ // Populate the PAC script
+ if (c.pacScript) {
+ if (c.pacScript.url)
+ this.pacURL = c.pacScript.url;
+ } else {
+ this.pacURL = '';
+ }
+ // Evaluate the `rules`
+ if (c.rules) {
+ var rules = c.rules;
+ if (rules.singleProxy) {
+ this.singleProxy = rules.singleProxy;
+ } else {
+ this.singleProxy = null;
+ this.httpProxy = rules.proxyForHttp;
+ this.httpsProxy = rules.proxyForHttps;
+ this.ftpProxy = rules.proxyForFtp;
+ this.fallbackProxy = rules.fallbackProxy;
+ }
+ this.bypassList = rules.bypassList;
+ } else {
+ this.singleProxy = null;
+ this.httpProxy = null;
+ this.httpsProxy = null;
+ this.ftpProxy = null;
+ this.fallbackProxy = null;
+ this.bypassList = '';
+ }
+ },
+
+
+ /**
+ * Handles the case in which this extension doesn't have the ability to
+ * control the Proxy settings, either because of an overriding policy
+ * or an extension with higher priority.
+ *
+ * @param {ProxyFormController.LevelOfControl} l The level of control this
+ * extension has over the proxy settings.
+ * @private
+ */
+ handleLackOfControl_: function(l) {
+ var msg;
+ if (l === ProxyFormController.LevelOfControl.NO_ACCESS)
+ msg = chrome.i18n.getMessage('errorNoExtensionAccess');
+ else if (l === ProxyFormController.LevelOfControl.OTHER_EXTENSION)
+ msg = chrome.i18n.getMessage('errorOtherExtensionControls');
+ this.generateAlert_(msg);
+ },
+
+
+ /**
+ * Handle the case in which errors have been generated outside the context
+ * of this popup.
+ *
+ * @private
+ */
+ handleProxyErrors_: function() {
+ chrome.extension.sendRequest(
+ {type: 'getError'},
+ this.handleProxyErrorHandlerResponse_.bind(this));
+ },
+
+ /**
+ * Handles response from ProxyErrorHandler
+ *
+ * @param {{result: !string}} response The message sent in response to this
+ * popup's request.
+ */
+ handleProxyErrorHandlerResponse_: function(response) {
+ if (response.result !== null) {
+ var error = response.result;
+ console.error(error); // TODO(mkwst): Do something more interesting
+ this.generateAlert_(
+ chrome.i18n.getMessage('errorProxyError', error.error),
+ false);
+ }
+ chrome.extension.sendRequest({type: 'clearError'});
+ }
+};
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/jsunittest.js b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/jsunittest.js
new file mode 100644
index 0000000..8e1eab0
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/jsunittest.js
@@ -0,0 +1,964 @@
+/* Jsunittest, version 0.6.0
+ * (c) 2008 Dr Nic Williams
+ *
+ * Jsunittest is freely distributable under
+ * the terms of an MIT-style license.
+ * For details, see the web site: http://jsunittest.rubyforge.org
+ *
+ *--------------------------------------------------------------------------*/
+
+var JsUnitTest = {
+ Version: '0.6.0',
+};
+
+var DrNicTest = {
+ Unit: {},
+ inspect: function(object) {
+ try {
+ if (typeof object == "undefined") return 'undefined';
+ if (object === null) return 'null';
+ if (typeof object == "string") {
+ var useDoubleQuotes = arguments[1];
+ var escapedString = this.gsub(object, /[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ };
+ return String(object);
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+ $: function(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push(this.$(arguments[i]));
+ return elements;
+ }
+ if (typeof element == "string")
+ element = document.getElementById(element);
+ return element;
+ },
+ gsub: function(source, pattern, replacement) {
+ var result = '', match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += DrNicTest.String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+ scan: function(source, pattern, iterator) {
+ this.gsub(source, pattern, iterator);
+ return String(source);
+ },
+ escapeHTML: function(data) {
+ return data.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ arrayfromargs: function(args) {
+ var myarray = new Array();
+ var i;
+
+ for (i=0;i<args.length;i++)
+ myarray[i] = args[i];
+
+ return myarray;
+ },
+ hashToSortedArray: function(hash) {
+ var results = [];
+ for (key in hash) {
+ results.push([key, hash[key]]);
+ }
+ return results.sort();
+ },
+ flattenArray: function(array) {
+ var results = arguments[1] || [];
+ for (var i=0; i < array.length; i++) {
+ var object = array[i];
+ if (object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object) {
+ this.flattenArray(object, results);
+ } else {
+ results.push(object);
+ }
+ };
+ return results;
+ },
+ selectorMatch: function(expression, element) {
+ var tokens = [];
+ var patterns = {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo:
+ /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
+ attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+ };
+
+ var assertions = {
+ tagName: function(element, matches) {
+ return matches[1].toUpperCase() == element.tagName.toUpperCase();
+ },
+
+ className: function(element, matches) {
+ return Element.hasClassName(element, matches[1]);
+ },
+
+ id: function(element, matches) {
+ return element.id === matches[1];
+ },
+
+ attrPresence: function(element, matches) {
+ return Element.hasAttribute(element, matches[1]);
+ },
+
+ attr: function(element, matches) {
+ var nodeValue = Element.readAttribute(element, matches[1]);
+ return nodeValue && operators[matches[2]](nodeValue, matches[5] || matches[6]);
+ }
+ };
+ var e = this.expression, ps = patterns, as = assertions;
+ var le, p, m;
+
+ while (e && le !== e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ // use the Selector.assertions methods unless the selector
+ // is too complex.
+ if (as[i]) {
+ tokens.push([i, Object.clone(m)]);
+ e = e.replace(m[0], '');
+ }
+ }
+ }
+ }
+
+ var match = true, name, matches;
+ for (var i = 0, token; token = tokens[i]; i++) {
+ name = token[0], matches = token[1];
+ if (!assertions[name](element, matches)) {
+ match = false; break;
+ }
+ }
+
+ return match;
+ },
+ toQueryParams: function(query, separator) {
+ var query = query || window.location.search;
+ var match = query.replace(/^\s+/, '').replace(/\s+$/, '').match(/([^?#]*)(#.*)?$/);
+ if (!match) return { };
+
+ var hash = {};
+ var parts = match[1].split(separator || '&');
+ for (var i=0; i < parts.length; i++) {
+ var pair = parts[i].split('=');
+ if (pair[0]) {
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ var object = hash[key];
+ var isArray = object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object
+ if (!isArray) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ };
+ return hash;
+ },
+
+ String: {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ }
+ }
+};
+
+DrNicTest.gsub.prepareReplacement = function(replacement) {
+ if (typeof replacement == "function") return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+};
+
+DrNicTest.Template = function(template, pattern) {
+ this.template = template; //template.toString();
+ this.pattern = pattern || DrNicTest.Template.Pattern;
+};
+
+DrNicTest.Template.prototype.evaluate = function(object) {
+ if (typeof object.toTemplateReplacements == "function")
+ object = object.toTemplateReplacements();
+
+ return DrNicTest.gsub(this.template, this.pattern, function(match) {
+ if (object == null) return '';
+
+ var before = match[1] || '';
+ if (before == '\\') return match[2];
+
+ var ctx = object, expr = match[3];
+ var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+ match = pattern.exec(expr);
+ if (match == null) return before;
+
+ while (match != null) {
+ var comp = (match[1].indexOf('[]') === 0) ? match[2].gsub('\\\\]', ']') : match[1];
+ ctx = ctx[comp];
+ if (null == ctx || '' == match[3]) break;
+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+ match = pattern.exec(expr);
+ }
+
+ return before + DrNicTest.String.interpret(ctx);
+ });
+}
+
+DrNicTest.Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+DrNicTest.Event = {};
+// written by Dean Edwards, 2005
+// with input from Tino Zijdel, Matthias Miller, Diego Perini
+// namespaced by Dr Nic Williams 2008
+
+// http://dean.edwards.name/weblog/2005/10/add-event/
+// http://dean.edwards.name/weblog/2005/10/add-event2/
+DrNicTest.Event.addEvent = function(element, type, handler) {
+ if (element.addEventListener) {
+ element.addEventListener(type, handler, false);
+ } else {
+ // assign each event handler a unique ID
+ if (!handler.$$guid) handler.$$guid = addEvent.guid++;
+ // create a hash table of event types for the element
+ if (!element.events) element.events = {};
+ // create a hash table of event handlers for each element/event pair
+ var handlers = element.events[type];
+ if (!handlers) {
+ handlers = element.events[type] = {};
+ // store the existing event handler (if there is one)
+ if (element["on" + type]) {
+ handlers[0] = element["on" + type];
+ }
+ }
+ // store the event handler in the hash table
+ handlers[handler.$$guid] = handler;
+ // assign a global event handler to do all the work
+ element["on" + type] = handleEvent;
+ }
+};
+// a counter used to create unique IDs
+DrNicTest.Event.addEvent.guid = 1;
+
+DrNicTest.Event.removeEvent = function(element, type, handler) {
+ if (element.removeEventListener) {
+ element.removeEventListener(type, handler, false);
+ } else {
+ // delete the event handler from the hash table
+ if (element.events && element.events[type]) {
+ delete element.events[type][handler.$$guid];
+ }
+ }
+};
+
+DrNicTest.Event.handleEvent = function(event) {
+ var returnValue = true;
+ // grab the event object (IE uses a global event object)
+ event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
+ // get a reference to the hash table of event handlers
+ var handlers = this.events[event.type];
+ // execute each event handler
+ for (var i in handlers) {
+ this.$$handleEvent = handlers[i];
+ if (this.$$handleEvent(event) === false) {
+ returnValue = false;
+ }
+ }
+ return returnValue;
+};
+
+DrNicTest.Event.fixEvent = function(event) {
+ // add W3C standard event methods
+ event.preventDefault = fixEvent.preventDefault;
+ event.stopPropagation = fixEvent.stopPropagation;
+ return event;
+};
+DrNicTest.Event.fixEvent.preventDefault = function() {
+ this.returnValue = false;
+};
+DrNicTest.Event.fixEvent.stopPropagation = function() {
+ this.cancelBubble = true;
+};
+
+DrNicTest.Unit.Logger = function(element) {
+ this.element = DrNicTest.$(element);
+ if (this.element) this._createLogTable();
+};
+
+DrNicTest.Unit.Logger.prototype.start = function(testName) {
+ if (!this.element) return;
+ var tbody = this.element.getElementsByTagName('tbody')[0];
+ tbody.innerHTML = tbody.innerHTML + '<tr><td>' + testName + '</td><td></td><td></td></tr>';
+};
+
+DrNicTest.Unit.Logger.prototype.setStatus = function(status) {
+ var logline = this.getLastLogLine();
+ logline.className = status;
+ var statusCell = logline.getElementsByTagName('td')[1];
+ statusCell.innerHTML = status;
+};
+
+DrNicTest.Unit.Logger.prototype.finish = function(status, summary) {
+ if (!this.element) return;
+ this.setStatus(status);
+ this.message(summary);
+};
+
+DrNicTest.Unit.Logger.prototype.message = function(message) {
+ if (!this.element) return;
+ var cell = this.getMessageCell();
+ cell.innerHTML = this._toHTML(message);
+};
+
+DrNicTest.Unit.Logger.prototype.summary = function(summary) {
+ if (!this.element) return;
+ var div = this.element.getElementsByTagName('div')[0];
+ div.innerHTML = this._toHTML(summary);
+};
+
+DrNicTest.Unit.Logger.prototype.getLastLogLine = function() {
+ var tbody = this.element.getElementsByTagName('tbody')[0];
+ var loglines = tbody.getElementsByTagName('tr');
+ return loglines[loglines.length - 1];
+};
+
+DrNicTest.Unit.Logger.prototype.getMessageCell = function() {
+ var logline = this.getLastLogLine();
+ return logline.getElementsByTagName('td')[2];
+};
+
+DrNicTest.Unit.Logger.prototype._createLogTable = function() {
+ var html = '<div class="logsummary">running...</div>' +
+ '<table class="logtable">' +
+ '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
+ '<tbody class="loglines"></tbody>' +
+ '</table>';
+ this.element.innerHTML = html;
+};
+
+DrNicTest.Unit.Logger.prototype.appendActionButtons = function(actions) {
+ // actions = $H(actions);
+ // if (!actions.any()) return;
+ // var div = new Element("div", {className: 'action_buttons'});
+ // actions.inject(div, function(container, action) {
+ // var button = new Element("input").setValue(action.key).observe("click", action.value);
+ // button.type = "button";
+ // return container.insert(button);
+ // });
+ // this.getMessageCell().insert(div);
+};
+
+DrNicTest.Unit.Logger.prototype._toHTML = function(txt) {
+ return DrNicTest.escapeHTML(txt).replace(/\n/g,"<br/>");
+};
+DrNicTest.Unit.MessageTemplate = function(string) {
+ var parts = [];
+ var str = DrNicTest.scan((string || ''), /(?=[^\\])\?|(?:\\\?|[^\?])+/, function(part) {
+ parts.push(part[0]);
+ });
+ this.parts = parts;
+};
+
+DrNicTest.Unit.MessageTemplate.prototype.evaluate = function(params) {
+ var results = [];
+ for (var i=0; i < this.parts.length; i++) {
+ var part = this.parts[i];
+ var result = (part == '?') ? DrNicTest.inspect(params.shift()) : part.replace(/\\\?/, '?');
+ results.push(result);
+ };
+ return results.join('');
+};
+// A generic function for performming AJAX requests
+// It takes one argument, which is an object that contains a set of options
+// All of which are outline in the comments, below
+// From John Resig's book Pro JavaScript Techniques
+// published by Apress, 2006-8
+DrNicTest.ajax = function( options ) {
+
+ // Load the options object with defaults, if no
+ // values were provided by the user
+ options = {
+ // The type of HTTP Request
+ type: options.type || "POST",
+
+ // The URL the request will be made to
+ url: options.url || "",
+
+ // How long to wait before considering the request to be a timeout
+ timeout: options.timeout || 5000,
+
+ // Functions to call when the request fails, succeeds,
+ // or completes (either fail or succeed)
+ onComplete: options.onComplete || function(){},
+ onError: options.onError || function(){},
+ onSuccess: options.onSuccess || function(){},
+
+ // The data type that'll be returned from the server
+ // the default is simply to determine what data was returned from the
+ // and act accordingly.
+ data: options.data || ""
+ };
+
+ // Create the request object
+ var xml = new XMLHttpRequest();
+
+ // Open the asynchronous POST request
+ xml.open(options.type, options.url, true);
+
+ // We're going to wait for a request for 5 seconds, before giving up
+ var timeoutLength = 5000;
+
+ // Keep track of when the request has been succesfully completed
+ var requestDone = false;
+
+ // Initalize a callback which will fire 5 seconds from now, cancelling
+ // the request (if it has not already occurred).
+ setTimeout(function(){
+ requestDone = true;
+ }, timeoutLength);
+
+ // Watch for when the state of the document gets updated
+ xml.onreadystatechange = function(){
+ // Wait until the data is fully loaded,
+ // and make sure that the request hasn't already timed out
+ if ( xml.readyState == 4 && !requestDone ) {
+
+ // Check to see if the request was successful
+ if ( httpSuccess( xml ) ) {
+
+ // Execute the success callback with the data returned from the server
+ options.onSuccess( httpData( xml, options.type ) );
+
+ // Otherwise, an error occurred, so execute the error callback
+ } else {
+ options.onError();
+ }
+
+ // Call the completion callback
+ options.onComplete();
+
+ // Clean up after ourselves, to avoid memory leaks
+ xml = null;
+ }
+ };
+
+ // Establish the connection to the server
+ xml.send();
+
+ // Determine the success of the HTTP response
+ function httpSuccess(r) {
+ try {
+ // If no server status is provided, and we're actually
+ // requesting a local file, then it was successful
+ return !r.status && location.protocol == "file:" ||
+
+ // Any status in the 200 range is good
+ ( r.status >= 200 && r.status < 300 ) ||
+
+ // Successful if the document has not been modified
+ r.status == 304 ||
+
+ // Safari returns an empty status if the file has not been modified
+ navigator.userAgent.indexOf("Safari") >= 0 && typeof r.status == "undefined";
+ } catch(e){}
+
+ // If checking the status failed, then assume that the request failed too
+ return false;
+ }
+
+ // Extract the correct data from the HTTP response
+ function httpData(r,type) {
+ // Get the content-type header
+ var ct = r.getResponseHeader("content-type");
+
+ // If no default type was provided, determine if some
+ // form of XML was returned from the server
+ var data = !type && ct && ct.indexOf("xml") >= 0;
+
+ // Get the XML Document object if XML was returned from
+ // the server, otherwise return the text contents returned by the server
+ data = type == "xml" || data ? r.responseXML : r.responseText;
+
+ // If the specified type is "script", execute the returned text
+ // response as if it was JavaScript
+ if ( type == "script" )
+ eval.call( window, data );
+
+ // Return the response data (either an XML Document or a text string)
+ return data;
+ }
+
+}
+DrNicTest.Unit.Assertions = {
+ buildMessage: function(message, template) {
+ var args = DrNicTest.arrayfromargs(arguments).slice(2);
+ return (message ? message + '\n' : '') +
+ new DrNicTest.Unit.MessageTemplate(template).evaluate(args);
+ },
+
+ flunk: function(message) {
+ this.assertBlock(message || 'Flunked', function() { return false });
+ },
+
+ assertBlock: function(message, block) {
+ try {
+ block.call(this) ? this.pass() : this.fail(message);
+ } catch(e) { this.error(e) }
+ },
+
+ assert: function(expression, message) {
+ message = this.buildMessage(message || 'assert', 'got <?>', expression);
+ this.assertBlock(message, function() { return expression });
+ },
+
+ assertEqual: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertEqual', 'expected <?>, actual: <?>', expected, actual);
+ this.assertBlock(message, function() { return expected == actual });
+ },
+
+ assertNotEqual: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertNotEqual', 'expected <?>, actual: <?>', expected, actual);
+ this.assertBlock(message, function() { return expected != actual });
+ },
+
+ assertEnumEqual: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertEnumEqual', 'expected <?>, actual: <?>', expected, actual);
+ var expected_array = DrNicTest.flattenArray(expected);
+ var actual_array = DrNicTest.flattenArray(actual);
+ this.assertBlock(message, function() {
+ if (expected_array.length == actual_array.length) {
+ for (var i=0; i < expected_array.length; i++) {
+ if (expected_array[i] != actual_array[i]) return false;
+ };
+ return true;
+ }
+ return false;
+ });
+ },
+
+ assertEnumNotEqual: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertEnumNotEqual', '<?> was the same as <?>', expected, actual);
+ var expected_array = DrNicTest.flattenArray(expected);
+ var actual_array = DrNicTest.flattenArray(actual);
+ this.assertBlock(message, function() {
+ if (expected_array.length == actual_array.length) {
+ for (var i=0; i < expected_array.length; i++) {
+ if (expected_array[i] != actual_array[i]) return true;
+ };
+ return false;
+ }
+ return true;
+ });
+ },
+
+ assertHashEqual: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertHashEqual', 'expected <?>, actual: <?>', expected, actual);
+ var expected_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(expected));
+ var actual_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(actual));
+ var block = function() {
+ if (expected_array.length == actual_array.length) {
+ for (var i=0; i < expected_array.length; i++) {
+ if (expected_array[i] != actual_array[i]) return false;
+ };
+ return true;
+ }
+ return false;
+ };
+ this.assertBlock(message, block);
+ },
+
+ assertHashNotEqual: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertHashNotEqual', '<?> was the same as <?>', expected, actual);
+ var expected_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(expected));
+ var actual_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(actual));
+ // from now we recursively zip & compare nested arrays
+ var block = function() {
+ if (expected_array.length == actual_array.length) {
+ for (var i=0; i < expected_array.length; i++) {
+ if (expected_array[i] != actual_array[i]) return true;
+ };
+ return false;
+ }
+ return true;
+ };
+ this.assertBlock(message, block);
+ },
+
+ assertIdentical: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertIdentical', 'expected <?>, actual: <?>', expected, actual);
+ this.assertBlock(message, function() { return expected === actual });
+ },
+
+ assertNotIdentical: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertNotIdentical', 'expected <?>, actual: <?>', expected, actual);
+ this.assertBlock(message, function() { return expected !== actual });
+ },
+
+ assertNull: function(obj, message) {
+ message = this.buildMessage(message || 'assertNull', 'got <?>', obj);
+ this.assertBlock(message, function() { return obj === null });
+ },
+
+ assertNotNull: function(obj, message) {
+ message = this.buildMessage(message || 'assertNotNull', 'got <?>', obj);
+ this.assertBlock(message, function() { return obj !== null });
+ },
+
+ assertUndefined: function(obj, message) {
+ message = this.buildMessage(message || 'assertUndefined', 'got <?>', obj);
+ this.assertBlock(message, function() { return typeof obj == "undefined" });
+ },
+
+ assertNotUndefined: function(obj, message) {
+ message = this.buildMessage(message || 'assertNotUndefined', 'got <?>', obj);
+ this.assertBlock(message, function() { return typeof obj != "undefined" });
+ },
+
+ assertNullOrUndefined: function(obj, message) {
+ message = this.buildMessage(message || 'assertNullOrUndefined', 'got <?>', obj);
+ this.assertBlock(message, function() { return obj == null });
+ },
+
+ assertNotNullOrUndefined: function(obj, message) {
+ message = this.buildMessage(message || 'assertNotNullOrUndefined', 'got <?>', obj);
+ this.assertBlock(message, function() { return obj != null });
+ },
+
+ assertMatch: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertMatch', 'regex <?> did not match <?>', expected, actual);
+ this.assertBlock(message, function() { return new RegExp(expected).exec(actual) });
+ },
+
+ assertNoMatch: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertNoMatch', 'regex <?> matched <?>', expected, actual);
+ this.assertBlock(message, function() { return !(new RegExp(expected).exec(actual)) });
+ },
+
+ assertHidden: function(element, message) {
+ message = this.buildMessage(message || 'assertHidden', '? isn\'t hidden.', element);
+ this.assertBlock(message, function() { return element.style.display == 'none' });
+ },
+
+ assertInstanceOf: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertInstanceOf', '<?> was not an instance of the expected type', actual);
+ this.assertBlock(message, function() { return actual instanceof expected });
+ },
+
+ assertNotInstanceOf: function(expected, actual, message) {
+ message = this.buildMessage(message || 'assertNotInstanceOf', '<?> was an instance of the expected type', actual);
+ this.assertBlock(message, function() { return !(actual instanceof expected) });
+ },
+
+ assertRespondsTo: function(method, obj, message) {
+ message = this.buildMessage(message || 'assertRespondsTo', 'object doesn\'t respond to <?>', method);
+ this.assertBlock(message, function() { return (method in obj && typeof obj[method] == 'function') });
+ },
+
+ assertRaise: function(exceptionName, method, message) {
+ message = this.buildMessage(message || 'assertRaise', '<?> exception expected but none was raised', exceptionName);
+ var block = function() {
+ try {
+ method();
+ return false;
+ } catch(e) {
+ if (e.name == exceptionName) return true;
+ else throw e;
+ }
+ };
+ this.assertBlock(message, block);
+ },
+
+ assertNothingRaised: function(method, message) {
+ try {
+ method();
+ this.assert(true, "Expected nothing to be thrown");
+ } catch(e) {
+ message = this.buildMessage(message || 'assertNothingRaised', '<?> was thrown when nothing was expected.', e);
+ this.flunk(message);
+ }
+ },
+
+ _isVisible: function(element) {
+ element = DrNicTest.$(element);
+ if(!element.parentNode) return true;
+ this.assertNotNull(element);
+ if(element.style && element.style.display == 'none')
+ return false;
+
+ return arguments.callee.call(this, element.parentNode);
+ },
+
+ assertVisible: function(element, message) {
+ message = this.buildMessage(message, '? was not visible.', element);
+ this.assertBlock(message, function() { return this._isVisible(element) });
+ },
+
+ assertNotVisible: function(element, message) {
+ message = this.buildMessage(message, '? was not hidden and didn\'t have a hidden parent either.', element);
+ this.assertBlock(message, function() { return !this._isVisible(element) });
+ },
+
+ assertElementsMatch: function() {
+ var pass = true, expressions = DrNicTest.arrayfromargs(arguments);
+ var elements = expressions.shift();
+ if (elements.length != expressions.length) {
+ message = this.buildMessage('assertElementsMatch', 'size mismatch: ? elements, ? expressions (?).', elements.length, expressions.length, expressions);
+ this.flunk(message);
+ pass = false;
+ }
+ for (var i=0; i < expressions.length; i++) {
+ var expression = expressions[i];
+ var element = DrNicTest.$(elements[i]);
+ if (DrNicTest.selectorMatch(expression, element)) {
+ pass = true;
+ break;
+ }
+ message = this.buildMessage('assertElementsMatch', 'In index <?>: expected <?> but got ?', index, expression, element);
+ this.flunk(message);
+ pass = false;
+ };
+ this.assert(pass, "Expected all elements to match.");
+ },
+
+ assertElementMatches: function(element, expression, message) {
+ this.assertElementsMatch([element], expression);
+ }
+};
+DrNicTest.Unit.Runner = function(testcases) {
+ var argumentOptions = arguments[1] || {};
+ var options = this.options = {};
+ options.testLog = ('testLog' in argumentOptions) ? argumentOptions.testLog : 'testlog';
+ options.resultsURL = this.queryParams.resultsURL;
+ options.testLog = DrNicTest.$(options.testLog);
+
+ this.tests = this.getTests(testcases);
+ this.currentTest = 0;
+ this.logger = new DrNicTest.Unit.Logger(options.testLog);
+
+ var self = this;
+ DrNicTest.Event.addEvent(window, "load", function() {
+ setTimeout(function() {
+ self.runTests();
+ }, 0.1);
+ });
+};
+
+DrNicTest.Unit.Runner.prototype.queryParams = DrNicTest.toQueryParams();
+
+DrNicTest.Unit.Runner.prototype.portNumber = function() {
+ if (window.location.search.length > 0) {
+ var matches = window.location.search.match(/\:(\d{3,5})\//);
+ if (matches) {
+ return parseInt(matches[1]);
+ }
+ }
+ return null;
+};
+
+DrNicTest.Unit.Runner.prototype.getTests = function(testcases) {
+ var tests = [], options = this.options;
+ if (this.queryParams.tests) tests = this.queryParams.tests.split(',');
+ else if (options.tests) tests = options.tests;
+ else if (options.test) tests = [option.test];
+ else {
+ for (testname in testcases) {
+ if (testname.match(/^test/)) tests.push(testname);
+ }
+ }
+ var results = [];
+ for (var i=0; i < tests.length; i++) {
+ var test = tests[i];
+ if (testcases[test])
+ results.push(
+ new DrNicTest.Unit.Testcase(test, testcases[test], testcases.setup, testcases.teardown)
+ );
+ };
+ return results;
+};
+
+DrNicTest.Unit.Runner.prototype.getResult = function() {
+ var results = {
+ tests: this.tests.length,
+ assertions: 0,
+ failures: 0,
+ errors: 0
+ };
+
+ for (var i=0; i < this.tests.length; i++) {
+ var test = this.tests[i];
+ results.assertions += test.assertions;
+ results.failures += test.failures;
+ results.errors += test.errors;
+ };
+ return results;
+};
+
+DrNicTest.Unit.Runner.prototype.postResults = function() {
+ if (this.options.resultsURL) {
+ // new Ajax.Request(this.options.resultsURL,
+ // { method: 'get', parameters: this.getResult(), asynchronous: false });
+ var results = this.getResult();
+ var url = this.options.resultsURL + "?";
+ url += "assertions="+ results.assertions + "&";
+ url += "failures=" + results.failures + "&";
+ url += "errors=" + results.errors;
+ DrNicTest.ajax({
+ url: url,
+ type: 'GET'
+ })
+ }
+};
+
+DrNicTest.Unit.Runner.prototype.runTests = function() {
+ var test = this.tests[this.currentTest], actions;
+
+ if (!test) return this.finish();
+ if (!test.isWaiting) this.logger.start(test.name);
+ test.run();
+ var self = this;
+ if(test.isWaiting) {
+ this.logger.message("Waiting for " + test.timeToWait + "ms");
+ // setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
+ setTimeout(function() {
+ self.runTests();
+ }, test.timeToWait || 1000);
+ return;
+ }
+
+ this.logger.finish(test.status(), test.summary());
+ if (actions = test.actions) this.logger.appendActionButtons(actions);
+ this.currentTest++;
+ // tail recursive, hopefully the browser will skip the stackframe
+ this.runTests();
+};
+
+DrNicTest.Unit.Runner.prototype.finish = function() {
+ this.postResults();
+ this.logger.summary(this.summary());
+};
+
+DrNicTest.Unit.Runner.prototype.summary = function() {
+ return new DrNicTest.Template('#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors').evaluate(this.getResult());
+};
+DrNicTest.Unit.Testcase = function(name, test, setup, teardown) {
+ this.name = name;
+ this.test = test || function() {};
+ this.setup = setup || function() {};
+ this.teardown = teardown || function() {};
+ this.messages = [];
+ this.actions = {};
+};
+// import DrNicTest.Unit.Assertions
+
+for (method in DrNicTest.Unit.Assertions) {
+ DrNicTest.Unit.Testcase.prototype[method] = DrNicTest.Unit.Assertions[method];
+}
+
+DrNicTest.Unit.Testcase.prototype.isWaiting = false;
+DrNicTest.Unit.Testcase.prototype.timeToWait = 1000;
+DrNicTest.Unit.Testcase.prototype.assertions = 0;
+DrNicTest.Unit.Testcase.prototype.failures = 0;
+DrNicTest.Unit.Testcase.prototype.errors = 0;
+// DrNicTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port == 4711;
+DrNicTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port;
+
+DrNicTest.Unit.Testcase.prototype.wait = function(time, nextPart) {
+ this.isWaiting = true;
+ this.test = nextPart;
+ this.timeToWait = time;
+};
+
+DrNicTest.Unit.Testcase.prototype.run = function(rethrow) {
+ try {
+ try {
+ if (!this.isWaiting) this.setup();
+ this.isWaiting = false;
+ this.test();
+ } finally {
+ if(!this.isWaiting) {
+ this.teardown();
+ }
+ }
+ }
+ catch(e) {
+ if (rethrow) throw e;
+ this.error(e, this);
+ }
+};
+
+DrNicTest.Unit.Testcase.prototype.summary = function() {
+ var msg = '#{assertions} assertions, #{failures} failures, #{errors} errors\n';
+ return new DrNicTest.Template(msg).evaluate(this) +
+ this.messages.join("\n");
+};
+
+DrNicTest.Unit.Testcase.prototype.pass = function() {
+ this.assertions++;
+};
+
+DrNicTest.Unit.Testcase.prototype.fail = function(message) {
+ this.failures++;
+ var line = "";
+ try {
+ throw new Error("stack");
+ } catch(e){
+ line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1];
+ }
+ this.messages.push("Failure: " + message + (line ? " Line #" + line : ""));
+};
+
+DrNicTest.Unit.Testcase.prototype.info = function(message) {
+ this.messages.push("Info: " + message);
+};
+
+DrNicTest.Unit.Testcase.prototype.error = function(error, test) {
+ this.errors++;
+ this.actions['retry with throw'] = function() { test.run(true) };
+ this.messages.push(error.name + ": "+ error.message + "(" + DrNicTest.inspect(error) + ")");
+};
+
+DrNicTest.Unit.Testcase.prototype.status = function() {
+ if (this.failures > 0) return 'failed';
+ if (this.errors > 0) return 'error';
+ return 'passed';
+};
+
+DrNicTest.Unit.Testcase.prototype.benchmark = function(operation, iterations) {
+ var startAt = new Date();
+ (iterations || 1).times(operation);
+ var timeTaken = ((new Date())-startAt);
+ this.info((arguments[2] || 'Operation') + ' finished ' +
+ iterations + ' iterations in ' + (timeTaken/1000)+'s' );
+ return timeTaken;
+};
+
+Test = DrNicTest
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.html b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.html
new file mode 100644
index 0000000..096e709
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.html
@@ -0,0 +1,113 @@
+<!doctype html>
+<html>
+<head>
+ <title>Popup for Proxy API Test</title>
+ <link rel="stylesheet" type="text/css" href="./unittest.css">
+ <script src="./jsunittest.js"></script>
+ <script src="../proxy_form_controller.js"></script>
+</head>
+<body>
+ <h1>Proxy Configuration Unit Tests</h1>
+
+ <h2>ProxyFormController</h2>
+ <div id="proxyformcontrollerlog"></div>
+
+ <div id="fixture">
+ <form id="proxyForm">
+ <fieldset id="system">
+ <legend>System Settings</legend>
+ <input type="radio" name="proxyType" id="proxyTypeSystem" value="system">
+ <label for="proxyTypeSystem">Use the <em>system's proxy settings</em>.</label>
+ </fieldset>
+ <fieldset id="direct">
+ <legend>Direct Connection</legend>
+ <input type="radio" name="proxyType" id="proxyTypeDirect" value="direct">
+ <label for="proxyTypeDirect">Your computer is <em>directly connected</em> to the internet; no need for a proxy.</label>
+ </fieldset>
+ <fieldset id="pac_script">
+ <legend>Automatic Configuration</legend>
+ <input type="radio" name="proxyType" id="proxyTypeAutoconfig" value="autoconfig">
+ <label for="proxyTypeAutoconfig">Your proxy supports <em>automatic configuration</em>.</label>
+
+ <section>
+ <label for="autoconfigURL">Autoconfiguration URL (PAC file)</label>
+ <input type="url" name="autoconfigURL" id="autoconfigURL">
+ <input type="hidden" name="autoconfigData" id="autoconfigData">
+ </section>
+ </fieldset>
+ <fieldset id="fixed_servers">
+ <legend>Manual Proxy</legend>
+ <input type="radio" name="proxyType" id="proxyTypeManual" value="manual">
+ <label for="proxyTypeManual">Configure your proxy settings <em>manually</em>.</label>
+ <section>
+ <fieldset>
+ <legend>HTTP</legend>
+ <label for="proxyHostHttp">Host</label>
+ <select id="proxySchemeHttp" name="proxySchemeHttp">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostHttp" id="proxyHostHttp">
+
+ <label for="proxyPortHttp">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortHttp" id="proxyPortHttp">
+
+ <input type="checkbox" name="singleProxyForEverything" id="singleProxyForEverything">
+ <label for="singleProxyForEverything">Use the same proxy server for all protocols</label>
+ </fieldset>
+ <fieldset>
+ <legend>HTTPS</legend>
+ <label for="proxyHostHttps">Host</label>
+ <select id="proxySchemeHttps" name="proxySchemeHttps">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostHttps" id="proxyHostHttps">
+
+ <label for="proxyPortHttps">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortHttps" id="proxyPortHttps">
+ </fieldset>
+ <fieldset>
+ <legend>FTP</legend>
+ <label for="proxyHostFtp">Host</label>
+ <select id="proxySchemeFtp" name="proxySchemeFtp">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostFtp" id="proxyHostFtp">
+
+ <label for="proxyPortFtp">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortFtp" id="proxyPortFtp">
+ </fieldset>
+ <fieldset>
+ <legend>Fallback</legend>
+ <label for="proxyHostFallback">Host</label>
+ <select id="proxySchemeFallback" name="proxySchemeFallback">
+ <option selected value="http">http://</option>
+ <option value="https">https://</option>
+ <option value="socks4">socks4://</option>
+ <option value="socks5">socks5://</option>
+ </select>
+ <input type="text" name="proxyHostFallback" id="proxyHostFallback">
+
+ <label for="proxyPortFallback">Port</label>
+ <input type="number" min="1" step="1" name="proxyPortFallback" id="proxyPortFallback">
+ </fieldset>
+ <fieldset>
+ <label for="bypassList">Bypass proxy for these hosts:</label>
+ <textarea id="bypassList" name="bypassList" placeholder="localhost,192.168.1.1/16, .example.com"></textarea>
+ </fieldset>
+ </section>
+ </fieldset>
+ <input type="submit" value="Save proxy settings">
+ </form>
+ </div>
+ <script src="./proxy_form_controller_test.js"></script>
+</body>
+</html>
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.js b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.js
new file mode 100644
index 0000000..35fd424
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.js
@@ -0,0 +1,516 @@
+// Copyright (c) 2011 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.
+
+// Stub out the `chrome.experimental.proxy` API
+chrome.experimental = chrome.experimental || {
+ proxy: {
+ settings: {
+ get: function() {},
+ clear: function() {},
+ set: function() {}
+ }
+ }
+};
+// Stub out i18n
+chrome.i18n = chrome.i18n || {
+ getMessage: function(x) { return x; }
+};
+// Stub out messaging
+chrome.extension = chrome.extension || {
+ sendRequest: function() {}
+};
+var fixture = document.getElementById('fixture');
+var baselineHTML = fixture.innerHTML;
+var groupIDs = [ProxyFormController.ProxyTypes.DIRECT,
+ ProxyFormController.ProxyTypes.SYSTEM,
+ ProxyFormController.ProxyTypes.PAC,
+ ProxyFormController.ProxyTypes.FIXED];
+
+var mockFunctionFactory = function(returnValue, logging) {
+ var called = [];
+ returnValue = returnValue || null;
+
+ var funky = function() {
+ called.push(arguments);
+ if (arguments[1] && typeof(arguments[1]) === 'function') {
+ var funk = arguments[1];
+ funk(returnValue);
+ }
+ return returnValue;
+ };
+ funky.getCallList = function() { return called; };
+ funky.getValue = function() { return returnValue; };
+ return funky;
+};
+
+var chrome = chrome || {};
+
+var proxyform = new Test.Unit.Runner({
+ setup: function() {
+ fixture.innerHTML = baselineHTML;
+ this.controller_ = new ProxyFormController('proxyForm');
+ this.clickEvent_ = document.createEvent('MouseEvents');
+ this.clickEvent_.initMouseEvent('click', true, true, window,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ // Reset mock functions.
+ chrome.experimental = {
+ proxy: {
+ settings: {
+ get: mockFunctionFactory({
+ value: {mode: 'system' },
+ levelOfControl: 'ControllableByThisExtension' }),
+ clear: mockFunctionFactory({
+ value: {mode: 'system' },
+ levelOfControl: 'ControllableByThisExtension' }),
+ set: mockFunctionFactory({
+ value: {mode: 'system' },
+ levelOfControl: 'ControllableByThisExtension' })
+ }
+ }
+ };
+ },
+
+ teardown: function() {
+ fixture.removeChild(fixture.childNodes[0]);
+ delete(this.controller_);
+ },
+
+ // Clicking on various bits of the interface should set correct classes,
+ // and select correct radio buttons.
+ testActivationClicks: function() {
+ var self = this;
+ var i;
+ groupIDs.forEach(function(id) {
+ var group = document.getElementById(id);
+ var all = group.querySelectorAll('*');
+ for (i = 0; i < all.length; i++) {
+ group.classList.remove('active');
+ all[i].dispatchEvent(self.clickEvent_);
+ self.assert(group.classList.contains('active'));
+ }
+ });
+ },
+
+ // Elements inside an active group should not be disabled, and vice versa
+ testDisabledElements: function() {
+ var self = this;
+ var i, j;
+ groupIDs.forEach(function(id) {
+ var group = document.getElementById(id);
+ var all = group.querySelectorAll('*');
+ // First, check that activating a group enables its form elements
+ for (i = 0; i < all.length; i++) {
+ group.classList.remove('active');
+ var inputs = group.querySelectorAll('input:not([type="radio"]),select');
+ for (j = 0; j < inputs.length; j++) {
+ inputs[j].setAttribute('disabled', 'disabled');
+ }
+ all[i].dispatchEvent(self.clickEvent_);
+ for (j = 0; j < inputs.length; j++) {
+ self.assert(!inputs[j].hasAttribute('disabled'));
+ }
+ }
+ });
+ },
+
+ // Clicking the "Use single proxy" checkbox should set the correct
+ // classes on the form.
+ testSingleProxyToggle: function() {
+ var group = document.getElementById(
+ ProxyFormController.ProxyTypes.FIXED);
+ var checkbox = document.getElementById('singleProxyForEverything');
+ var section = checkbox.parentNode.parentNode;
+ // Checkbox only works in active group, `testActivationClicks` tests
+ // the inactive click behavior.
+ group.classList.add('active');
+
+ checkbox.checked = false;
+ checkbox.dispatchEvent(this.clickEvent_);
+ this.assert(section.classList.contains('single'));
+ checkbox.dispatchEvent(this.clickEvent_);
+ this.assert(!section.classList.contains('single'));
+ },
+
+ // On instantiation, ProxyFormController should read the current state
+ // from `chrome.experimental.getCurrentProxySettings`, and react
+ // accordingly. Let's see if that happens with the next four sets of
+ // assertsions.
+ testSetupFormSystem: function() {
+ chrome.experimental.proxy.settings.get = mockFunctionFactory({
+ value: {mode: 'system'},
+ levelOfControl: 'ControllableByThisExtension'
+ });
+
+ fixture.innerHTML = baselineHTML;
+ this.controller_ = new ProxyFormController('proxyForm');
+ // Wait for async calls to fire
+ this.wait(100, function() {
+ this.assertEqual(
+ 2,
+ chrome.experimental.proxy.settings.get.getCallList().length);
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.SYSTEM)
+ .classList.contains('active'));
+ });
+ },
+
+ testSetupFormDirect: function() {
+ chrome.experimental.proxy.settings.get =
+ mockFunctionFactory({value: {mode: 'direct'},
+ levelOfControl: 'ControllableByThisExtension'}, true);
+
+ fixture.innerHTML = baselineHTML;
+ this.controller_ = new ProxyFormController('proxyForm');
+ // Wait for async calls to fire
+ this.wait(100, function() {
+ this.assertEqual(
+ 2,
+ chrome.experimental.proxy.settings.get.getCallList().length);
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.DIRECT)
+ .classList.contains('active'));
+ });
+ },
+
+ testSetupFormPac: function() {
+ chrome.experimental.proxy.settings.get =
+ mockFunctionFactory({value: {mode: 'pac_script' },
+ levelOfControl: 'ControllableByThisExtension'});
+
+ fixture.innerHTML = baselineHTML;
+ this.controller_ = new ProxyFormController('proxyForm');
+ // Wait for async calls to fire
+ this.wait(100, function() {
+ this.assertEqual(
+ 2,
+ chrome.experimental.proxy.settings.get.getCallList().length);
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC)
+ .classList.contains('active'));
+ });
+ },
+
+ testSetupFormFixed: function() {
+ chrome.experimental.proxy.settings.get =
+ mockFunctionFactory({value: {mode: 'fixed_servers' },
+ levelOfControl: 'ControllableByThisExtension'});
+
+ fixture.innerHTML = baselineHTML;
+ this.controller_ = new ProxyFormController('proxyForm');
+ // Wait for async calls to fire
+ this.wait(100, function() {
+ this.assertEqual(
+ 2,
+ chrome.experimental.proxy.settings.get.getCallList().length);
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.FIXED)
+ .classList.contains('active'));
+ });
+ },
+
+ // Test that `recalcFormValues_` correctly sets DOM field values when
+ // given a `ProxyConfig` structure
+ testRecalcFormValuesGroups: function() {
+ // Test `AUTO` normalization to `PAC`
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.AUTO,
+ rules: {},
+ pacScript: ''
+ });
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC)
+ .classList.contains('active'));
+
+ // DIRECT
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.DIRECT,
+ rules: {},
+ pacScript: ''
+ });
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.DIRECT)
+ .classList.contains('active'));
+
+ // FIXED
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.FIXED,
+ rules: {},
+ pacScript: ''
+ });
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.FIXED)
+ .classList.contains('active'));
+
+ // PAC
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.PAC,
+ rules: {},
+ pacScript: ''
+ });
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC)
+ .classList.contains('active'));
+
+ // SYSTEM
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.SYSTEM,
+ rules: {},
+ pacScript: ''
+ });
+ this.assert(
+ document.getElementById(ProxyFormController.ProxyTypes.SYSTEM)
+ .classList.contains('active'));
+ },
+
+ testRecalcFormValuesFixedSingle: function() {
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.FIXED,
+ rules: {
+ singleProxy: {
+ scheme: 'socks5',
+ host: 'singleproxy.example.com',
+ port: '1234'
+ }
+ }
+ });
+ var single = this.controller_.singleProxy;
+ this.assertEqual('socks5', single.scheme);
+ this.assertEqual('singleproxy.example.com', single.host);
+ this.assertEqual(1234, single.port);
+ },
+
+ testRecalcFormValuesPacScript: function() {
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.PAC,
+ rules: {},
+ pacScript: {url: 'http://example.com/this/is/a/pac.script'}
+ });
+ this.assertEqual(
+ 'http://example.com/this/is/a/pac.script',
+ document.getElementById('autoconfigURL').value);
+ },
+
+ testRecalcFormValuesSingle: function() {
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.FIXED,
+ rules: {
+ singleProxy: {
+ scheme: 'https',
+ host: 'example.com',
+ port: 80
+ }
+ }
+ });
+ // Single!
+ this.assert(
+ document.querySelector('#' + ProxyFormController.ProxyTypes.FIXED +
+ ' > section').classList.contains('single'));
+
+ var single = this.controller_.singleProxy;
+ this.assertEqual('https', single.scheme);
+ this.assertEqual('example.com', single.host);
+ this.assertEqual(80, single.port);
+ },
+
+ testRecalcFormValuesMultiple: function() {
+ this.controller_.recalcFormValues_({
+ mode: ProxyFormController.ProxyTypes.FIXED,
+ rules: {
+ proxyForHttp: {
+ scheme: 'http',
+ host: 'http.example.com',
+ port: 1
+ },
+ proxyForHttps: {
+ scheme: 'https',
+ host: 'https.example.com',
+ port: 2
+ },
+ proxyForFtp: {
+ scheme: 'socks4',
+ host: 'socks4.example.com',
+ port: 3
+ },
+ fallbackProxy: {
+ scheme: 'socks5',
+ host: 'socks5.example.com',
+ port: 4
+ }
+ }
+ });
+ // Not Single!
+ this.assert(
+ !document.querySelector('#' + ProxyFormController.ProxyTypes.FIXED
+ + ' > section').classList.contains('single'));
+ var server = this.controller_.singleProxy;
+ this.assertNull(server);
+
+ server = this.controller_.httpProxy;
+ this.assertEqual('http', server.scheme);
+ this.assertEqual('http.example.com', server.host);
+ this.assertEqual(1, server.port);
+
+ server = this.controller_.httpsProxy;
+ this.assertEqual('https', server.scheme);
+ this.assertEqual('https.example.com', server.host);
+ this.assertEqual(2, server.port);
+
+ server = this.controller_.ftpProxy;
+ this.assertEqual('socks4', server.scheme);
+ this.assertEqual('socks4.example.com', server.host);
+ this.assertEqual(3, server.port);
+
+ server = this.controller_.fallbackProxy;
+ this.assertEqual('socks5', server.scheme);
+ this.assertEqual('socks5.example.com', server.host);
+ this.assertEqual(4, server.port);
+ },
+
+ testBypassList: function() {
+ this.controller_.bypassList = ['1.example.com',
+ '2.example.com',
+ '3.example.com'];
+ this.assertEnumEqual(
+ document.getElementById('bypassList').value,
+ '1.example.com, 2.example.com, 3.example.com');
+ this.assertEnumEqual(
+ this.controller_.bypassList,
+ ['1.example.com', '2.example.com', '3.example.com']);
+ },
+
+ // Test that "system" rules are correctly generated
+ testProxyRulesGenerationSystem: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.SYSTEM));
+
+ this.assertHashEqual(
+ {mode: 'system'},
+ this.controller_.generateProxyConfig_());
+ },
+
+ // Test that "direct" rules are correctly generated
+ testProxyRulesGenerationDirect: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.DIRECT));
+
+ this.assertHashEqual(
+ {mode: 'direct'},
+ this.controller_.generateProxyConfig_());
+ },
+
+ // Test that auto detection rules are correctly generated when "automatic"
+ // is selected, and no PAC file URL is given
+ testProxyRulesGenerationAuto: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC));
+
+ this.assertHashEqual(
+ {mode: 'auto_detect'},
+ this.controller_.generateProxyConfig_());
+ },
+
+ // Test that PAC URL rules are correctly generated when "automatic"
+ // is selected, and a PAC file URL is given
+ testProxyRulesGenerationPacURL: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC));
+ this.controller_.pacURL = 'http://example.com/pac.pac';
+ var result = this.controller_.generateProxyConfig_();
+ this.assertEqual('pac_script', result.mode);
+ this.assertEqual('http://example.com/pac.pac', result.pacScript.url);
+ },
+
+ // Manual PAC definitions
+ testProxyRulesGenerationPacData: function() {
+ var pacData = 'function FindProxyForURL(url,host) { return "DIRECT"; }';
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC));
+ this.controller_.manualPac = pacData;
+ var result = this.controller_.generateProxyConfig_();
+ this.assertEqual('pac_script', result.mode);
+ this.assertEqual(pacData, result.pacScript.data);
+ },
+
+ // PAC URLs override manual PAC definitions
+ testProxyRulesGenerationPacURLOverridesData: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.PAC));
+ this.controller_.pacURL = 'http://example.com/pac.pac';
+ this.controller_.manualPac =
+ 'function FindProxyForURL(url,host) { return "DIRECT"; }';
+ var result = this.controller_.generateProxyConfig_();
+ this.assertEqual('pac_script', result.mode);
+ this.assertEqual('http://example.com/pac.pac', result.pacScript.url);
+ },
+
+ // Test that fixed, manual servers are correctly transformed into a
+ // `ProxyRules` structure.
+ testProxyRulesGenerationSingle: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.FIXED));
+
+ this.controller_.singleProxy = {
+ scheme: 'http',
+ host: 'example.com',
+ port: '80'
+ };
+
+ var result = this.controller_.generateProxyConfig_();
+ this.assertEqual('fixed_servers', result.mode);
+ this.assertEqual('http', result.rules.singleProxy.scheme);
+ this.assertEqual('example.com', result.rules.singleProxy.host);
+ this.assertEqual(80, result.rules.singleProxy.port);
+ this.assertEqual(undefined, result.rules.proxyForHttp);
+ this.assertEqual(undefined, result.rules.proxyForHttps);
+ this.assertEqual(undefined, result.rules.proxyForFtp);
+ this.assertEqual(undefined, result.rules.fallbackProxy);
+ },
+
+ // Test that proxy configuration rules are correctly generated
+ // for separate manually entered servers.
+ testProxyRulesGenerationSeparate: function() {
+ this.controller_.changeActive_(
+ document.getElementById(ProxyFormController.ProxyTypes.FIXED));
+
+ this.controller_.singleProxy = false;
+ this.controller_.httpProxy = {
+ scheme: 'http',
+ host: 'http.example.com',
+ port: 80
+ };
+ this.controller_.httpsProxy = {
+ scheme: 'https',
+ host: 'https.example.com',
+ port: 443
+ };
+ this.controller_.ftpProxy = {
+ scheme: 'socks4',
+ host: 'ftp.example.com',
+ port: 80
+ };
+ this.controller_.fallbackProxy = {
+ scheme: 'socks5',
+ host: 'fallback.example.com',
+ port: 80
+ };
+
+ var result = this.controller_.generateProxyConfig_();
+ this.assertEqual('fixed_servers', result.mode);
+ this.assertEqual(undefined, result.rules.singleProxy);
+ this.assertEqual('http', result.rules.proxyForHttp.scheme);
+ this.assertEqual('http.example.com', result.rules.proxyForHttp.host);
+ this.assertEqual('80', result.rules.proxyForHttp.port);
+ this.assertEqual('https', result.rules.proxyForHttps.scheme);
+ this.assertEqual('https.example.com', result.rules.proxyForHttps.host);
+ this.assertEqual('443', result.rules.proxyForHttps.port);
+ this.assertEqual('socks4', result.rules.proxyForFtp.scheme);
+ this.assertEqual('ftp.example.com', result.rules.proxyForFtp.host);
+ this.assertEqual('80', result.rules.proxyForFtp.port);
+ this.assertEqual('socks5', result.rules.fallbackProxy.scheme);
+ this.assertEqual('fallback.example.com', result.rules.fallbackProxy.host);
+ this.assertEqual('80', result.rules.fallbackProxy.port);
+ }
+}, { testLog: 'proxyformcontrollerlog' });
+
+var c = new ProxyFormController('proxyForm');
diff --git a/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/unittest.css b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/unittest.css
new file mode 100644
index 0000000..d62b1c5
--- /dev/null
+++ b/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/unittest.css
@@ -0,0 +1,58 @@
+body, div, p, h1, h2, h3, ul, ol, span, a, table, td, form, img, li {
+ font-family: sans-serif;
+}
+
+body {
+ font-size:0.8em;
+}
+
+#log {
+ padding-bottom: 1em;
+ border-bottom: 2px solid #000;
+ margin-bottom: 2em;
+}
+
+.logsummary {
+ margin-top: 1em;
+ margin-bottom: 1em;
+ padding: 1ex;
+ border: 1px solid #000;
+ font-weight: bold;
+}
+
+.logtable {
+ width:100%;
+ border-collapse: collapse;
+ border: 1px dotted #666;
+}
+
+.logtable td, .logtable th {
+ text-align: left;
+ padding: 3px 8px;
+ border: 1px dotted #666;
+}
+
+.logtable .passed {
+ background-color: #cfc;
+}
+
+.logtable .failed, .logtable .error {
+ background-color: #fcc;
+}
+
+.logtable .warning {
+ background-color: #FC6;
+}
+
+.logtable td div.action_buttons {
+ display: inline;
+}
+
+.logtable td div.action_buttons input {
+ margin: 0 5px;
+ font-size: 10px;
+}
+
+#fixture {
+ display: none;
+}
diff --git a/chrome/common/extensions/docs/samples.html b/chrome/common/extensions/docs/samples.html
index bda40f4..7efcbc4 100644
--- a/chrome/common/extensions/docs/samples.html
+++ b/chrome/common/extensions/docs/samples.html
@@ -313,7 +313,7 @@
<!-- STATIC CONTENT PLACEHOLDER -->
<div id="static"><link rel="stylesheet" href="css/samples.css">
-<script>var search_data = {"0262260daf0c8f7b28feff2ef23b05e7abf9d1e0":"A BROWSER ACTION WHICH CHANGES ITS ICON WHEN CLICKED. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETICON","ea2894c41cb8e80a4433a3e6c5772dadce9be90d":"A BROWSER ACTION WITH A POPUP THAT CHANGES THE PAGE COLOR. BROWSER_ACTION POPUP TABS CHROME.TABS.EXECUTESCRIPT","ede3c47b7757245be42ec33fd5ca63df4b490066":"A BROWSER ACTION WITH NO ICON THAT MAKES THE PAGE RED BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.TABS.EXECUTESCRIPT","fbf0aa1a09a15ff8cc4fc7de4fd176d6c663d07a":"ACCEPTLANGUAGE RETURNS ACCEPT LANGUAGES OF THE BROWSER BROWSER_ACTION POPUP CHROME.I18N.GETACCEPTLANGUAGES CHROME.I18N.GETMESSAGE","9a6e4ec46997fb92b324974afa08a3d007e2537f":"ANIMATED PAGE ACTION THIS EXTENSION ADDS AN ANIMATED BROWSER ACTION TO THE TOOLBAR. BACKGROUND_PAGE PAGE_ACTION TABS CHROME.PAGEACTION.HIDE CHROME.PAGEACTION.ONCLICKED CHROME.PAGEACTION.SETICON CHROME.PAGEACTION.SETTITLE CHROME.PAGEACTION.SHOW CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONSELECTIONCHANGED","a1f7cf79dd555b04fa8d603247a040e644996293":"APP LAUNCHER BROWSER_ACTION MANAGEMENT CHROME.EXTENSION.GETURL CHROME.MANAGEMENT.GET CHROME.MANAGEMENT.GETALL CHROME.MANAGEMENT.LAUNCHAPP CHROME.TABS.CREATE","9747e3d6a3eab39bc7c17f11a80573c62d44c7e5":"BLANK NEW TAB PAGE CHROME_URL_OVERRIDES","903e7277139e1e6caec123d3319cab295d8d1b3a":"CHROME SOUNDS ENJOY A MORE MAGICAL AND IMMERSIVE EXPERIENCE WHEN BROWSING THE WEB USING THE POWER OF SOUND. BACKGROUND_PAGE BOOKMARKS OPTIONS_PAGE TABS CHROME.BOOKMARKS.ONCREATED CHROME.BOOKMARKS.ONMOVED CHROME.BOOKMARKS.ONREMOVED CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.TABS.GET CHROME.TABS.ONATTACHED CHROME.TABS.ONCREATED CHROME.TABS.ONDETACHED CHROME.TABS.ONMOVED CHROME.TABS.ONREMOVED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED CHROME.WINDOWS.ONCREATED CHROME.WINDOWS.ONFOCUSCHANGED CHROME.WINDOWS.ONREMOVED","0e790e035a4a00b6f1def5ef9a7d7be1bce95ab5":"CHROMIUM BUILDBOT MONITOR DISPLAYS THE STATUS OF THE CHROMIUM BUILDBOT IN THE TOOLBAR. CLICK TO SEE MORE DETAILED STATUS IN A POPUP. BACKGROUND_PAGE BROWSER_ACTION NOTIFICATIONS OPTIONS_PAGE POPUP CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.GETURL","ac31228200b41a87982e386cc90d3a6eee4ad885":"CHROMIUM SEARCH ADD SUPPORT TO THE OMNIBOX TO SEARCH THE CHROMIUM SOURCE CODE. BACKGROUND_PAGE TABS CHROME.OMNIBOX.ONINPUTCANCELLED CHROME.OMNIBOX.ONINPUTCHANGED CHROME.OMNIBOX.ONINPUTENTERED CHROME.OMNIBOX.ONINPUTSTARTED CHROME.OMNIBOX.SETDEFAULTSUGGESTION CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.UPDATE","7d5d6cf195bc25480256618e360aa38c6e6fba82":"CLD DISPLAYS THE LANGUAGE OF A TAB BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.SETBADGETEXT CHROME.TABS.DETECTLANGUAGE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED","5d81304a17cf7ac2887484f730fbd2b01e51e166":"CONTEXT MENUS SAMPLE SHOWS SOME OF THE FEATURES OF THE CONTEXT MENUS API BACKGROUND_PAGE CONTEXTMENUS CHROME.CONTEXTMENUS.CREATE","4daa6becd0899a54776d9cf7f09613ed1a9f4d77":"COOKIE API TEST EXTENSION TESTING COOKIE API BACKGROUND_PAGE BROWSER_ACTION COOKIES TABS CHROME.BROWSERACTION.ONCLICKED CHROME.COOKIES.GET CHROME.COOKIES.GETALL CHROME.COOKIES.ONCHANGED CHROME.COOKIES.REMOVE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL","6871d09f4a96bf9d4b6cc724d00e909cee0f3902":"CROSS-DOMAIN XMLHTTPREQUEST FROM A CONTENT SCRIPT DEMONSTRATES A METHOD TO MAKE A CROSS-DOMAIN XMLHTTPREQUEST FETCH FROM A CONTENT SCRIPT. THIS EXTENSION FETCHES THE CURRENT TRENDING TOPICS FROM TWITTER AND INSERTS THEM IN AN OVERLAY AT THE TOP OF GOOGLE NEWS. VISIT HTTP://NEWS.GOOGLE.COM TO TEST THIS EXTENSION. BACKGROUND_PAGE CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST","028eb5364924344029bcbe1d527f132fc72b34e5":"EMAIL THIS PAGE (BY GOOGLE) THIS EXTENSION ADDS AN EMAIL BUTTON TO THE TOOLBAR WHICH ALLOWS YOU TO EMAIL THE PAGE LINK USING YOUR DEFAULT MAIL CLIENT OR GMAIL. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.CONNECT CHROME.EXTENSION.ONCONNECT CHROME.TABS.CREATE CHROME.TABS.EXECUTESCRIPT CHROME.TABS.UPDATE","763a08e9b06595d785568a8d392b95a2f3700258":"EVENT TRACKING WITH GOOGLE ANALYTICS A SAMPLE EXTENSION WHICH USES GOOGLE ANALYTICS TO TRACK USAGE. BACKGROUND_PAGE BROWSER_ACTION POPUP","e3df888a89e35bdeb9c8bc8d03be5e1851b97c68":"EXTENSION DOCS SEARCH SEARCH THE CHROME EXTENSIONS DOCUMENTATION. TO USE, TYPE CRDOC PLUS A SEARCH TERM INTO THE OMNIBOX. BACKGROUND_PAGE TABS CHROME.OMNIBOX.ONINPUTCHANGED CHROME.OMNIBOX.ONINPUTENTERED CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.ONREMOVED CHROME.TABS.UPDATE","8b0dd31216235941bdd8eb33fda915ef5cf79a82":"GOOGLE CALENDAR CHECKER (BY GOOGLE) QUICKLY SEE THE TIME UNTIL YOUR NEXT MEETING FROM ANY OF YOUR CALENDARS. CLICK ON THE BUTTON TO BE TAKEN TO YOUR CALENDAR. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETICON CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.ONUPDATED CHROME.TABS.UPDATE","4e35caa9742fb82dbd628892d23a781614f6eff6":"GOOGLE DOCUMENT LIST VIEWER DEMONSTRATES HOW TO USE OAUTH TO CONNECT THE GOOGLE DOCUMENTS LIST DATA API. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE POPUP TABS CHROME.BROWSERACTION.SETBADGETEXT CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE","bb57f7a0132cbeb36ad7e7bb0ab75c21704234ca":"GOOGLE MAIL CHECKER DISPLAYS THE NUMBER OF UNREAD MESSAGES IN YOUR GOOGLE MAIL INBOX. YOU CAN ALSO CLICK THE BUTTON TO OPEN YOUR INBOX. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETICON CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.ONUPDATED CHROME.TABS.UPDATE","1682e05ea9a1bde985123b04f6f8ac50a8a64033":"GOOGLE WAVE NOTIFIER FIND OUT WHEN YOU HAVE NEW WAVES AND PREVIEW THEM FAST. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE POPUP TABS CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE","14b9651fda4e57b2a5914ba73a779812201b750a":"HELLO WORLD THE FIRST EXTENSION THAT I MADE. BROWSER_ACTION POPUP","2020d72f2577f53caf8e94e3dbac0fb849ceaa4d":"IDLE - SIMPLE EXAMPLE DEMONSTRATES THE IDLE API BACKGROUND_PAGE BROWSER_ACTION IDLE CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.IDLE.ONSTATECHANGED CHROME.IDLE.QUERYSTATE","0ea1588bd07b20338fc21f725de1542a5fdf9726":"IGOOGLE NEW TAB PAGE CHROME_URL_OVERRIDES","646325c25f572a1d15edc73d057f821d847a4fbe":"IMAGEINFO GET IMAGE INFO FOR IMAGES, INCLUDING EXIF DATA BACKGROUND_PAGE CONTEXTMENUS TABS CHROME.CONTEXTMENUS.CREATE CHROME.TABS.GET CHROME.TABS.GETCURRENT CHROME.WINDOWS.CREATE CHROME.WINDOWS.UPDATE","ec97ec20ca2f095d081e39f1565fc12af09ef067":"MAPPY FINDS ADDRESSES IN THE WEB PAGE YOURE ON AND POPS UP A MAP WINDOW. BACKGROUND_PAGE PAGE_ACTION POPUP TABS CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.ONREQUEST CHROME.PAGEACTION.HIDE CHROME.PAGEACTION.SETTITLE CHROME.PAGEACTION.SHOW CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED CHROME.TABS.SENDREQUEST","b2f5f8a790e16f091a7e4e0a39b2d0a6d32e3a6d":"MERGE WINDOWS MERGES ALL OF THE BROWSERS WINDOWS INTO THE CURRENT WINDOW BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.MOVE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL CHROME.WINDOWS.GETCURRENT","51a83d2ba3a32e3ff1bdb624d4e18ccec4c4038e":"MESSAGE TIMER TIMES HOW LONG IT TAKES TO SEND A MESSAGE TO A CONTENT SCRIPT AND BACK. BROWSER_ACTION POPUP TABS CHROME.EXTENSION.ONCONNECT CHROME.EXTENSION.ONREQUEST CHROME.TABS.CONNECT CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.SENDREQUEST","4f6785ec4f937add6728615682dd37c9a42d9548":"MY BOOKMARKS A BROWSER ACTION WITH A POPUP DUMP OF ALL BOOKMARKS, INCLUDING SEARCH, ADD, EDIT AND DELETE. BOOKMARKS BROWSER_ACTION POPUP TABS CHROME.BOOKMARKS.CREATE CHROME.BOOKMARKS.GET CHROME.BOOKMARKS.GETTREE CHROME.BOOKMARKS.REMOVE CHROME.BOOKMARKS.UPDATE CHROME.TABS.CREATE","597015d3bcce3da693b02314afd607bec4f55291":"NEWS READER DISPLAYS THE FIRST 5 ITEMS FROM THE GOOGLE NEWS - TOP NEWS RSS FEED IN A POPUP. BROWSER_ACTION POPUP TABS CHROME.TABS.CREATE","6444e5c8ae112a6a433909c5e770669cd16e2e5f":"NEWS READER DISPLAYS THE FIRST 5 ITEMS FROM THE GOOGLE NEWS - TOP NEWS RSS FEED IN A POPUP. BROWSER_ACTION POPUP TABS CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE","3aea027164cb9b732ba4a8c51cb93708891726ef":"NEWS READER (BY GOOGLE) DISPLAYS THE LATEST STORIES FROM GOOGLE NEWS IN A POPUP. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE POPUP TABS CHROME.EXTENSION.GETURL CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE","f799e26ceef2367cf836f24bcb47df4398b0df58":"NOTIFICATION DEMO SHOWS OFF DESKTOP NOTIFICATIONS, WHICH ARE TOAST WINDOWS THAT POP UP ON THE DESKTOP. BACKGROUND_PAGE NOTIFICATIONS OPTIONS_PAGE TABS CHROME.TABS.CREATE","e787b322bddbc6289bb31b7d7550b1bf6456a80b":"OMNIBOX EXAMPLE TO USE, TYPE OMNIX PLUS A SEARCH TERM INTO THE OMNIBOX. BACKGROUND_PAGE CHROME.OMNIBOX.ONINPUTCHANGED CHROME.OMNIBOX.ONINPUTENTERED","8d0a50b57c26bb498be592e871001ffed91541b4":"PAGE ACTION BY CONTENT SHOWS A PAGE ACTION FOR HTML PAGES CONTAINING THE WORD SANDWICH BACKGROUND_PAGE PAGE_ACTION CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.PAGEACTION.SHOW","80b86ccc6e8520660fa591caa565826f0ed1b12c":"PAGE ACTION BY URL SHOWS A PAGE ACTION FOR URLS WHICH HAVE THE LETTER G IN THEM. BACKGROUND_PAGE PAGE_ACTION TABS CHROME.PAGEACTION.SHOW CHROME.TABS.ONUPDATED","d74c3c18a1c1dd18b035149105a306f837c8823e":"PAGE BENCHMARKER CHROMIUM PAGE BENCHMARKER. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.CONNECT CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETEXTENSIONTABS CHROME.EXTENSION.GETURL CHROME.EXTENSION.ONCONNECT CHROME.TABS.CREATE CHROME.TABS.EXECUTESCRIPT CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.GETSELECTED CHROME.TABS.REMOVE CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETCURRENT","e6ae17ab4ccfd7e059c8c01f25760ca5d894c7fd":"PRINT THIS PAGE ADDS A PRINT BUTTON TO THE BROWSER. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.TABS.UPDATE","beff6ecd9677dea0a7c648c5042165b48bb66f09":"PROCESS MONITOR ADDS A BROWSER ACTION THAT MONITORS RESOURCE USAGE OF ALL BROWSER PROCESSES. BROWSER_ACTION EXPERIMENTAL POPUP TABS CHROME.EXPERIMENTAL.PROCESSES.ONUPDATED","56a8d2ac24ca7bba78fd88ad57f43fc13c784497":"SAMPLE - OAUTH CONTACTS USES OAUTH TO CONNECT TO GOOGLES CONTACTS SERVICE AND DISPLAY A LIST OF YOUR CONTACTS. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETICON CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE","38f6e1e17756ede38b1364c7114a738ca717dcbb":"SANDWICHBAR SHOWS AN INFOBAR ON PAGES WHICH CONTAIN THE WORD SANDWICH BACKGROUND_PAGE EXPERIMENTAL CHROME.EXPERIMENTAL.INFOBARS.SHOW CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST","fc89b35755483af30b66cd72cefa34a43a3e8312":"SHOW TABS IN PROCESS ADDS A BROWSER ACTION SHOWING WHICH TABS SHARE THE CURRENT TABS PROCESS. BROWSER_ACTION EXPERIMENTAL POPUP TABS CHROME.EXPERIMENTAL.PROCESSES.GETPROCESSIDFORTAB CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL CHROME.WINDOWS.GETCURRENT CHROME.WINDOWS.UPDATE","230463f2d5c3d4d0ca13c230e1f00f2aae0a8a64":"TAB INSPECTOR UTILITY FOR WORKING WITH THE EXTENSION TABS API BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.GETSELECTED CHROME.TABS.MOVE CHROME.TABS.ONATTACHED CHROME.TABS.ONCREATED CHROME.TABS.ONDETACHED CHROME.TABS.ONMOVED CHROME.TABS.ONREMOVED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE CHROME.TABS.UPDATE CHROME.WINDOWS.CREATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL CHROME.WINDOWS.GETCURRENT CHROME.WINDOWS.GETLASTFOCUSED CHROME.WINDOWS.ONCREATED CHROME.WINDOWS.ONFOCUSCHANGED CHROME.WINDOWS.ONREMOVED CHROME.WINDOWS.REMOVE CHROME.WINDOWS.UPDATE","e1697cacebad05218798bf3e8a0f724517f0e8c3":"TEST SCREENSHOT EXTENSION DEMONSTRATE SCREENSHOT FUNCTIONALITY IN THE CHROME.TABS API. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.GETURL CHROME.EXTENSION.GETVIEWS CHROME.TABS.CAPTUREVISIBLETAB CHROME.TABS.CREATE CHROME.TABS.ONUPDATED","b3de91ab04b7d7a2670ca7ee9d740eb42cead0b6":"TYPED URL HISTORY READS YOUR HISTORY, AND SHOWS THE TOP TEN PAGES YOU GO TO BY TYPING THE URL. BROWSER_ACTION HISTORY TABS CHROME.HISTORY.GETVISITS CHROME.HISTORY.SEARCH CHROME.TABS.CREATE"}</script>
+<script>var search_data = {"0262260daf0c8f7b28feff2ef23b05e7abf9d1e0":"A BROWSER ACTION WHICH CHANGES ITS ICON WHEN CLICKED. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETICON","ea2894c41cb8e80a4433a3e6c5772dadce9be90d":"A BROWSER ACTION WITH A POPUP THAT CHANGES THE PAGE COLOR. BROWSER_ACTION POPUP TABS CHROME.TABS.EXECUTESCRIPT","ede3c47b7757245be42ec33fd5ca63df4b490066":"A BROWSER ACTION WITH NO ICON THAT MAKES THE PAGE RED BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.TABS.EXECUTESCRIPT","fbf0aa1a09a15ff8cc4fc7de4fd176d6c663d07a":"ACCEPTLANGUAGE RETURNS ACCEPT LANGUAGES OF THE BROWSER BROWSER_ACTION POPUP CHROME.I18N.GETACCEPTLANGUAGES CHROME.I18N.GETMESSAGE","9a6e4ec46997fb92b324974afa08a3d007e2537f":"ANIMATED PAGE ACTION THIS EXTENSION ADDS AN ANIMATED BROWSER ACTION TO THE TOOLBAR. BACKGROUND_PAGE PAGE_ACTION TABS CHROME.PAGEACTION.HIDE CHROME.PAGEACTION.ONCLICKED CHROME.PAGEACTION.SETICON CHROME.PAGEACTION.SETTITLE CHROME.PAGEACTION.SHOW CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONSELECTIONCHANGED","a1f7cf79dd555b04fa8d603247a040e644996293":"APP LAUNCHER BROWSER_ACTION MANAGEMENT CHROME.EXTENSION.GETURL CHROME.MANAGEMENT.GET CHROME.MANAGEMENT.GETALL CHROME.MANAGEMENT.LAUNCHAPP CHROME.TABS.CREATE","9747e3d6a3eab39bc7c17f11a80573c62d44c7e5":"BLANK NEW TAB PAGE CHROME_URL_OVERRIDES","903e7277139e1e6caec123d3319cab295d8d1b3a":"CHROME SOUNDS ENJOY A MORE MAGICAL AND IMMERSIVE EXPERIENCE WHEN BROWSING THE WEB USING THE POWER OF SOUND. BACKGROUND_PAGE BOOKMARKS OPTIONS_PAGE TABS CHROME.BOOKMARKS.ONCREATED CHROME.BOOKMARKS.ONMOVED CHROME.BOOKMARKS.ONREMOVED CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.TABS.GET CHROME.TABS.ONATTACHED CHROME.TABS.ONCREATED CHROME.TABS.ONDETACHED CHROME.TABS.ONMOVED CHROME.TABS.ONREMOVED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED CHROME.WINDOWS.ONCREATED CHROME.WINDOWS.ONFOCUSCHANGED CHROME.WINDOWS.ONREMOVED","0e790e035a4a00b6f1def5ef9a7d7be1bce95ab5":"CHROMIUM BUILDBOT MONITOR DISPLAYS THE STATUS OF THE CHROMIUM BUILDBOT IN THE TOOLBAR. CLICK TO SEE MORE DETAILED STATUS IN A POPUP. BACKGROUND_PAGE BROWSER_ACTION NOTIFICATIONS OPTIONS_PAGE POPUP CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.GETURL","ac31228200b41a87982e386cc90d3a6eee4ad885":"CHROMIUM SEARCH ADD SUPPORT TO THE OMNIBOX TO SEARCH THE CHROMIUM SOURCE CODE. BACKGROUND_PAGE TABS CHROME.OMNIBOX.ONINPUTCANCELLED CHROME.OMNIBOX.ONINPUTCHANGED CHROME.OMNIBOX.ONINPUTENTERED CHROME.OMNIBOX.ONINPUTSTARTED CHROME.OMNIBOX.SETDEFAULTSUGGESTION CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.UPDATE","7d5d6cf195bc25480256618e360aa38c6e6fba82":"CLD DISPLAYS THE LANGUAGE OF A TAB BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.SETBADGETEXT CHROME.TABS.DETECTLANGUAGE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED","5d81304a17cf7ac2887484f730fbd2b01e51e166":"CONTEXT MENUS SAMPLE SHOWS SOME OF THE FEATURES OF THE CONTEXT MENUS API BACKGROUND_PAGE CONTEXTMENUS CHROME.CONTEXTMENUS.CREATE","4daa6becd0899a54776d9cf7f09613ed1a9f4d77":"COOKIE API TEST EXTENSION TESTING COOKIE API BACKGROUND_PAGE BROWSER_ACTION COOKIES TABS CHROME.BROWSERACTION.ONCLICKED CHROME.COOKIES.GET CHROME.COOKIES.GETALL CHROME.COOKIES.ONCHANGED CHROME.COOKIES.REMOVE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL","6871d09f4a96bf9d4b6cc724d00e909cee0f3902":"CROSS-DOMAIN XMLHTTPREQUEST FROM A CONTENT SCRIPT DEMONSTRATES A METHOD TO MAKE A CROSS-DOMAIN XMLHTTPREQUEST FETCH FROM A CONTENT SCRIPT. THIS EXTENSION FETCHES THE CURRENT TRENDING TOPICS FROM TWITTER AND INSERTS THEM IN AN OVERLAY AT THE TOP OF GOOGLE NEWS. VISIT HTTP://NEWS.GOOGLE.COM TO TEST THIS EXTENSION. BACKGROUND_PAGE CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST","028eb5364924344029bcbe1d527f132fc72b34e5":"EMAIL THIS PAGE (BY GOOGLE) THIS EXTENSION ADDS AN EMAIL BUTTON TO THE TOOLBAR WHICH ALLOWS YOU TO EMAIL THE PAGE LINK USING YOUR DEFAULT MAIL CLIENT OR GMAIL. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.CONNECT CHROME.EXTENSION.ONCONNECT CHROME.TABS.CREATE CHROME.TABS.EXECUTESCRIPT CHROME.TABS.UPDATE","763a08e9b06595d785568a8d392b95a2f3700258":"EVENT TRACKING WITH GOOGLE ANALYTICS A SAMPLE EXTENSION WHICH USES GOOGLE ANALYTICS TO TRACK USAGE. BACKGROUND_PAGE BROWSER_ACTION POPUP","e3df888a89e35bdeb9c8bc8d03be5e1851b97c68":"EXTENSION DOCS SEARCH SEARCH THE CHROME EXTENSIONS DOCUMENTATION. TO USE, TYPE CRDOC PLUS A SEARCH TERM INTO THE OMNIBOX. BACKGROUND_PAGE TABS CHROME.OMNIBOX.ONINPUTCHANGED CHROME.OMNIBOX.ONINPUTENTERED CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.ONREMOVED CHROME.TABS.UPDATE","8b0dd31216235941bdd8eb33fda915ef5cf79a82":"GOOGLE CALENDAR CHECKER (BY GOOGLE) QUICKLY SEE THE TIME UNTIL YOUR NEXT MEETING FROM ANY OF YOUR CALENDARS. CLICK ON THE BUTTON TO BE TAKEN TO YOUR CALENDAR. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETICON CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.ONUPDATED CHROME.TABS.UPDATE","4e35caa9742fb82dbd628892d23a781614f6eff6":"GOOGLE DOCUMENT LIST VIEWER DEMONSTRATES HOW TO USE OAUTH TO CONNECT THE GOOGLE DOCUMENTS LIST DATA API. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE POPUP TABS CHROME.BROWSERACTION.SETBADGETEXT CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE","bb57f7a0132cbeb36ad7e7bb0ab75c21704234ca":"GOOGLE MAIL CHECKER DISPLAYS THE NUMBER OF UNREAD MESSAGES IN YOUR GOOGLE MAIL INBOX. YOU CAN ALSO CLICK THE BUTTON TO OPEN YOUR INBOX. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETICON CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.ONUPDATED CHROME.TABS.UPDATE","1682e05ea9a1bde985123b04f6f8ac50a8a64033":"GOOGLE WAVE NOTIFIER FIND OUT WHEN YOU HAVE NEW WAVES AND PREVIEW THEM FAST. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE POPUP TABS CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE","14b9651fda4e57b2a5914ba73a779812201b750a":"HELLO WORLD THE FIRST EXTENSION THAT I MADE. BROWSER_ACTION POPUP","2020d72f2577f53caf8e94e3dbac0fb849ceaa4d":"IDLE - SIMPLE EXAMPLE DEMONSTRATES THE IDLE API BACKGROUND_PAGE BROWSER_ACTION IDLE CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.IDLE.ONSTATECHANGED CHROME.IDLE.QUERYSTATE","0ea1588bd07b20338fc21f725de1542a5fdf9726":"IGOOGLE NEW TAB PAGE CHROME_URL_OVERRIDES","646325c25f572a1d15edc73d057f821d847a4fbe":"IMAGEINFO GET IMAGE INFO FOR IMAGES, INCLUDING EXIF DATA BACKGROUND_PAGE CONTEXTMENUS TABS CHROME.CONTEXTMENUS.CREATE CHROME.TABS.GET CHROME.TABS.GETCURRENT CHROME.WINDOWS.CREATE CHROME.WINDOWS.UPDATE","ec97ec20ca2f095d081e39f1565fc12af09ef067":"MAPPY FINDS ADDRESSES IN THE WEB PAGE YOURE ON AND POPS UP A MAP WINDOW. BACKGROUND_PAGE PAGE_ACTION POPUP TABS CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.ONREQUEST CHROME.PAGEACTION.HIDE CHROME.PAGEACTION.SETTITLE CHROME.PAGEACTION.SHOW CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED CHROME.TABS.SENDREQUEST","b2f5f8a790e16f091a7e4e0a39b2d0a6d32e3a6d":"MERGE WINDOWS MERGES ALL OF THE BROWSERS WINDOWS INTO THE CURRENT WINDOW BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.MOVE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL CHROME.WINDOWS.GETCURRENT","51a83d2ba3a32e3ff1bdb624d4e18ccec4c4038e":"MESSAGE TIMER TIMES HOW LONG IT TAKES TO SEND A MESSAGE TO A CONTENT SCRIPT AND BACK. BROWSER_ACTION POPUP TABS CHROME.EXTENSION.ONCONNECT CHROME.EXTENSION.ONREQUEST CHROME.TABS.CONNECT CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.SENDREQUEST","4f6785ec4f937add6728615682dd37c9a42d9548":"MY BOOKMARKS A BROWSER ACTION WITH A POPUP DUMP OF ALL BOOKMARKS, INCLUDING SEARCH, ADD, EDIT AND DELETE. BOOKMARKS BROWSER_ACTION POPUP TABS CHROME.BOOKMARKS.CREATE CHROME.BOOKMARKS.GET CHROME.BOOKMARKS.GETTREE CHROME.BOOKMARKS.REMOVE CHROME.BOOKMARKS.UPDATE CHROME.TABS.CREATE","597015d3bcce3da693b02314afd607bec4f55291":"NEWS READER DISPLAYS THE FIRST 5 ITEMS FROM THE GOOGLE NEWS - TOP NEWS RSS FEED IN A POPUP. BROWSER_ACTION POPUP TABS CHROME.TABS.CREATE","6444e5c8ae112a6a433909c5e770669cd16e2e5f":"NEWS READER DISPLAYS THE FIRST 5 ITEMS FROM THE GOOGLE NEWS - TOP NEWS RSS FEED IN A POPUP. BROWSER_ACTION POPUP TABS CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE","3aea027164cb9b732ba4a8c51cb93708891726ef":"NEWS READER (BY GOOGLE) DISPLAYS THE LATEST STORIES FROM GOOGLE NEWS IN A POPUP. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE POPUP TABS CHROME.EXTENSION.GETURL CHROME.I18N.GETMESSAGE CHROME.TABS.CREATE","f799e26ceef2367cf836f24bcb47df4398b0df58":"NOTIFICATION DEMO SHOWS OFF DESKTOP NOTIFICATIONS, WHICH ARE TOAST WINDOWS THAT POP UP ON THE DESKTOP. BACKGROUND_PAGE NOTIFICATIONS OPTIONS_PAGE TABS CHROME.TABS.CREATE","e787b322bddbc6289bb31b7d7550b1bf6456a80b":"OMNIBOX EXAMPLE TO USE, TYPE OMNIX PLUS A SEARCH TERM INTO THE OMNIBOX. BACKGROUND_PAGE CHROME.OMNIBOX.ONINPUTCHANGED CHROME.OMNIBOX.ONINPUTENTERED","8d0a50b57c26bb498be592e871001ffed91541b4":"PAGE ACTION BY CONTENT SHOWS A PAGE ACTION FOR HTML PAGES CONTAINING THE WORD SANDWICH BACKGROUND_PAGE PAGE_ACTION CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.PAGEACTION.SHOW","80b86ccc6e8520660fa591caa565826f0ed1b12c":"PAGE ACTION BY URL SHOWS A PAGE ACTION FOR URLS WHICH HAVE THE LETTER G IN THEM. BACKGROUND_PAGE PAGE_ACTION TABS CHROME.PAGEACTION.SHOW CHROME.TABS.ONUPDATED","d74c3c18a1c1dd18b035149105a306f837c8823e":"PAGE BENCHMARKER CHROMIUM PAGE BENCHMARKER. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.CONNECT CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETEXTENSIONTABS CHROME.EXTENSION.GETURL CHROME.EXTENSION.ONCONNECT CHROME.TABS.CREATE CHROME.TABS.EXECUTESCRIPT CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.GETSELECTED CHROME.TABS.REMOVE CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETCURRENT","e6ae17ab4ccfd7e059c8c01f25760ca5d894c7fd":"PRINT THIS PAGE ADDS A PRINT BUTTON TO THE BROWSER. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.TABS.UPDATE","beff6ecd9677dea0a7c648c5042165b48bb66f09":"PROCESS MONITOR ADDS A BROWSER ACTION THAT MONITORS RESOURCE USAGE OF ALL BROWSER PROCESSES. BROWSER_ACTION EXPERIMENTAL POPUP TABS CHROME.EXPERIMENTAL.PROCESSES.ONUPDATED","3e8e226d87e431296bb110b4f6eb7eec2ca7a826":"PROXY SETTINGS SET CHROME-SPECIFIC PROXIES; A DEMONSTRATION OF CHROMES PROXY API TTS BACKGROUND_PAGE BROWSER_ACTION EXPERIMENTAL POPUP PROXY CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXPERIMENTAL.PROXY.ONPROXYERROR CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.I18N.GETMESSAGE","56a8d2ac24ca7bba78fd88ad57f43fc13c784497":"SAMPLE - OAUTH CONTACTS USES OAUTH TO CONNECT TO GOOGLES CONTACTS SERVICE AND DISPLAY A LIST OF YOUR CONTACTS. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETICON CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE","38f6e1e17756ede38b1364c7114a738ca717dcbb":"SANDWICHBAR SHOWS AN INFOBAR ON PAGES WHICH CONTAIN THE WORD SANDWICH BACKGROUND_PAGE EXPERIMENTAL CHROME.EXPERIMENTAL.INFOBARS.SHOW CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST","fc89b35755483af30b66cd72cefa34a43a3e8312":"SHOW TABS IN PROCESS ADDS A BROWSER ACTION SHOWING WHICH TABS SHARE THE CURRENT TABS PROCESS. BROWSER_ACTION EXPERIMENTAL POPUP TABS CHROME.EXPERIMENTAL.PROCESSES.GETPROCESSIDFORTAB CHROME.TABS.GET CHROME.TABS.GETSELECTED CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL CHROME.WINDOWS.GETCURRENT CHROME.WINDOWS.UPDATE","230463f2d5c3d4d0ca13c230e1f00f2aae0a8a64":"TAB INSPECTOR UTILITY FOR WORKING WITH THE EXTENSION TABS API BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.GETURL CHROME.TABS.CREATE CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.GETSELECTED CHROME.TABS.MOVE CHROME.TABS.ONATTACHED CHROME.TABS.ONCREATED CHROME.TABS.ONDETACHED CHROME.TABS.ONMOVED CHROME.TABS.ONREMOVED CHROME.TABS.ONSELECTIONCHANGED CHROME.TABS.ONUPDATED CHROME.TABS.REMOVE CHROME.TABS.UPDATE CHROME.WINDOWS.CREATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETALL CHROME.WINDOWS.GETCURRENT CHROME.WINDOWS.GETLASTFOCUSED CHROME.WINDOWS.ONCREATED CHROME.WINDOWS.ONFOCUSCHANGED CHROME.WINDOWS.ONREMOVED CHROME.WINDOWS.REMOVE CHROME.WINDOWS.UPDATE","e1697cacebad05218798bf3e8a0f724517f0e8c3":"TEST SCREENSHOT EXTENSION DEMONSTRATE SCREENSHOT FUNCTIONALITY IN THE CHROME.TABS API. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.EXTENSION.GETURL CHROME.EXTENSION.GETVIEWS CHROME.TABS.CAPTUREVISIBLETAB CHROME.TABS.CREATE CHROME.TABS.ONUPDATED","b3de91ab04b7d7a2670ca7ee9d740eb42cead0b6":"TYPED URL HISTORY READS YOUR HISTORY, AND SHOWS THE TOP TEN PAGES YOU GO TO BY TYPING THE URL. BROWSER_ACTION HISTORY TABS CHROME.HISTORY.GETVISITS CHROME.HISTORY.SEARCH CHROME.TABS.CREATE"}</script>
<script src="js/sample_search.js"></script>
@@ -2441,6 +2441,82 @@
<a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/api/processes/process_monitor/">Browse source</a>
- <a href="examples/api/processes/process_monitor.zip">Download source</a>
</div>
+</div><div class="sample" id="3e8e226d87e431296bb110b4f6eb7eec2ca7a826">
+ <img class="icon" style="display: none; ">
+ <img class="icon" src="images/sample-default-icon.png">
+ <h2 class="name">
+ <a href="#3e8e226d87e431296bb110b4f6eb7eec2ca7a826">Proxy Settings</a>
+ </h2>
+ <p class="metadata features">Uses
+ <span>
+ <strong>TTS</strong><span>, </span>
+ <span style="display: none; "> and</span>
+ </span><span>
+ <strong>background_page</strong><span>, </span>
+ <span style="display: none; "> and</span>
+ </span><span>
+ <strong>browser_action</strong><span>, </span>
+ <span style="display: none; "> and</span>
+ </span><span>
+ <strong>experimental</strong><span>, </span>
+ <span style="display: none; "> and</span>
+ </span><span>
+ <strong>popup</strong><span style="display: none; ">, </span>
+ <span> and</span>
+ </span><span>
+ <strong>proxy</strong><span style="display: none; ">, </span>
+ <span style="display: none; "> and</span>
+ </span>
+ </p>
+ <p>Set Chrome-specific proxies; a demonstration of Chrome's Proxy API</p>
+ <div class="apicalls"><strong>Calls:</strong>
+ <ul>
+ <li>
+ <code><a href="browserAction.html#method-setBadgeBackgroundColor">chrome.browserAction.setBadgeBackgroundColor</a></code>
+ </li><li>
+ <code><a href="browserAction.html#method-setBadgeText">chrome.browserAction.setBadgeText</a></code>
+ </li><li>
+ <code><a href="browserAction.html#method-setTitle">chrome.browserAction.setTitle</a></code>
+ </li><li>
+ <code><a href="experimental.proxy.html#event-onProxyError">chrome.experimental.proxy.onProxyError</a></code>
+ </li><li>
+ <code><a href="extension.html#event-onRequest">chrome.extension.onRequest</a></code>
+ </li><li>
+ <code><a href="extension.html#method-sendRequest">chrome.extension.sendRequest</a></code>
+ </li><li>
+ <code><a href="i18n.html#method-getMessage">chrome.i18n.getMessage</a></code>
+ </li>
+ </ul>
+ </div>
+ <div class="sourcefiles"><strong>Source files:</strong>
+ <ul>
+ <li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/_locales/en/messages.json?content-type=text/plain">_locales/en/messages.json</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/background.html?content-type=text/plain">background.html</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/manifest.json?content-type=text/plain">manifest.json</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/popup.html?content-type=text/plain">popup.html</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_error_handler.js?content-type=text/plain">proxy_error_handler.js</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/proxy_form_controller.js?content-type=text/plain">proxy_form_controller.js</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/jsunittest.js?content-type=text/plain">test/jsunittest.js</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.html?content-type=text/plain">test/proxy_form_controller_test.html</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/proxy_form_controller_test.js?content-type=text/plain">test/proxy_form_controller_test.js</a></code>
+ </li><li>
+ <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/test/unittest.css?content-type=text/plain">test/unittest.css</a></code>
+ </li>
+ </ul>
+ </div>
+ <div>
+ <a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/proxy_configuration/">Browse source</a>
+ - <a href="examples/extensions/proxy_configuration.zip">Download source</a>
+ </div>
</div><div class="sample" id="56a8d2ac24ca7bba78fd88ad57f43fc13c784497">
<img class="icon" src="examples/extensions/oauth_contacts/img/icon-128.png">
<img class="icon" src="images/sample-default-icon.png" style="display: none; ">
diff --git a/chrome/common/extensions/docs/samples.json b/chrome/common/extensions/docs/samples.json
index 2aad896..e293a2e 100644
--- a/chrome/common/extensions/docs/samples.json
+++ b/chrome/common/extensions/docs/samples.json
@@ -1378,6 +1378,46 @@
},
{
"api_calls": [
+ "chrome.browserAction.setBadgeBackgroundColor",
+ "chrome.browserAction.setBadgeText",
+ "chrome.browserAction.setTitle",
+ "chrome.experimental.proxy.onProxyError",
+ "chrome.extension.onRequest",
+ "chrome.extension.sendRequest",
+ "chrome.i18n.getMessage"
+ ],
+ "description": "Set Chrome-specific proxies; a demonstration of Chrome's Proxy API",
+ "features": [
+ "TTS",
+ "background_page",
+ "browser_action",
+ "experimental",
+ "popup",
+ "proxy"
+ ],
+ "icon": null,
+ "id": "3e8e226d87e431296bb110b4f6eb7eec2ca7a826",
+ "name": "Proxy Settings",
+ "path": "examples/extensions/proxy_configuration/",
+ "protocols": [],
+ "search_string": "PROXY SETTINGS SET CHROME-SPECIFIC PROXIES; A DEMONSTRATION OF CHROMES PROXY API TTS BACKGROUND_PAGE BROWSER_ACTION EXPERIMENTAL POPUP PROXY CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXPERIMENTAL.PROXY.ONPROXYERROR CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.I18N.GETMESSAGE",
+ "source_files": [
+ "_locales/en/messages.json",
+ "background.html",
+ "manifest.json",
+ "popup.html",
+ "proxy_error_handler.js",
+ "proxy_form_controller.js",
+ "test/jsunittest.js",
+ "test/proxy_form_controller_test.html",
+ "test/proxy_form_controller_test.js",
+ "test/unittest.css"
+ ],
+ "source_hash": "866078e5f0611065ef9ef26225a7c38abaa32509",
+ "zip_path": "examples/extensions/proxy_configuration.zip"
+ },
+ {
+ "api_calls": [
"chrome.browserAction.onClicked",
"chrome.browserAction.setIcon",
"chrome.extension.getBackgroundPage",