browser/devtools/netmonitor/test/head.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* Any copyright is dedicated to the Public Domain.
michael@0 2 http://creativecommons.org/publicdomain/zero/1.0/ */
michael@0 3 "use strict";
michael@0 4
michael@0 5 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
michael@0 6
michael@0 7 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
michael@0 8 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
michael@0 9 let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 10 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
michael@0 11 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
michael@0 12 let { CurlUtils } = Cu.import("resource:///modules/devtools/Curl.jsm", {});
michael@0 13 let TargetFactory = devtools.TargetFactory;
michael@0 14 let Toolbox = devtools.Toolbox;
michael@0 15
michael@0 16 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/netmonitor/test/";
michael@0 17
michael@0 18 const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
michael@0 19 const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
michael@0 20 const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
michael@0 21 const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html";
michael@0 22 const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html";
michael@0 23 const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
michael@0 24 const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
michael@0 25 const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html";
michael@0 26 const POST_RAW_WITH_HEADERS_URL = EXAMPLE_URL + "html_post-raw-with-headers-test-page.html";
michael@0 27 const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html";
michael@0 28 const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
michael@0 29 const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
michael@0 30 const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html";
michael@0 31 const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html";
michael@0 32 const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html";
michael@0 33 const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html";
michael@0 34 const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
michael@0 35 const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
michael@0 36 const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
michael@0 37 const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html";
michael@0 38 const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
michael@0 39 const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
michael@0 40 const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
michael@0 41
michael@0 42 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
michael@0 43 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
michael@0 44 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
michael@0 45 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
michael@0 46
michael@0 47 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
michael@0 48 const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
michael@0 49
michael@0 50 gDevTools.testing = true;
michael@0 51 SimpleTest.registerCleanupFunction(() => {
michael@0 52 gDevTools.testing = false;
michael@0 53 });
michael@0 54
michael@0 55 // All tests are asynchronous.
michael@0 56 waitForExplicitFinish();
michael@0 57
michael@0 58 const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
michael@0 59 // To enable logging for try runs, just set the pref to true.
michael@0 60 Services.prefs.setBoolPref("devtools.debugger.log", false);
michael@0 61
michael@0 62 // Always reset some prefs to their original values after the test finishes.
michael@0 63 const gDefaultFilters = Services.prefs.getCharPref("devtools.netmonitor.filters");
michael@0 64
michael@0 65 registerCleanupFunction(() => {
michael@0 66 info("finish() was called, cleaning up...");
michael@0 67
michael@0 68 Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
michael@0 69 Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters);
michael@0 70 });
michael@0 71
michael@0 72 function addTab(aUrl, aWindow) {
michael@0 73 info("Adding tab: " + aUrl);
michael@0 74
michael@0 75 let deferred = promise.defer();
michael@0 76 let targetWindow = aWindow || window;
michael@0 77 let targetBrowser = targetWindow.gBrowser;
michael@0 78
michael@0 79 targetWindow.focus();
michael@0 80 let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
michael@0 81 let browser = tab.linkedBrowser;
michael@0 82
michael@0 83 browser.addEventListener("load", function onLoad() {
michael@0 84 browser.removeEventListener("load", onLoad, true);
michael@0 85 deferred.resolve(tab);
michael@0 86 }, true);
michael@0 87
michael@0 88 return deferred.promise;
michael@0 89 }
michael@0 90
michael@0 91 function removeTab(aTab, aWindow) {
michael@0 92 info("Removing tab.");
michael@0 93
michael@0 94 let targetWindow = aWindow || window;
michael@0 95 let targetBrowser = targetWindow.gBrowser;
michael@0 96
michael@0 97 targetBrowser.removeTab(aTab);
michael@0 98 }
michael@0 99
michael@0 100 function initNetMonitor(aUrl, aWindow) {
michael@0 101 info("Initializing a network monitor pane.");
michael@0 102
michael@0 103 return addTab(aUrl).then((aTab) => {
michael@0 104 info("Net tab added successfully: " + aUrl);
michael@0 105
michael@0 106 let deferred = promise.defer();
michael@0 107 let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject;
michael@0 108 let target = TargetFactory.forTab(aTab);
michael@0 109
michael@0 110 gDevTools.showToolbox(target, "netmonitor").then((aToolbox) => {
michael@0 111 info("Netork monitor pane shown successfully.");
michael@0 112
michael@0 113 let monitor = aToolbox.getCurrentPanel();
michael@0 114 deferred.resolve([aTab, debuggee, monitor]);
michael@0 115 });
michael@0 116
michael@0 117 return deferred.promise;
michael@0 118 });
michael@0 119 }
michael@0 120
michael@0 121 function restartNetMonitor(aMonitor, aNewUrl) {
michael@0 122 info("Restarting the specified network monitor.");
michael@0 123
michael@0 124 let deferred = promise.defer();
michael@0 125 let tab = aMonitor.target.tab;
michael@0 126 let url = aNewUrl || tab.linkedBrowser.contentWindow.wrappedJSObject.location.href;
michael@0 127
michael@0 128 aMonitor.once("destroyed", () => initNetMonitor(url).then(deferred.resolve));
michael@0 129 removeTab(tab);
michael@0 130
michael@0 131 return deferred.promise;
michael@0 132 }
michael@0 133
michael@0 134 function teardown(aMonitor) {
michael@0 135 info("Destroying the specified network monitor.");
michael@0 136
michael@0 137 let deferred = promise.defer();
michael@0 138 let tab = aMonitor.target.tab;
michael@0 139
michael@0 140 aMonitor.once("destroyed", () => executeSoon(deferred.resolve));
michael@0 141 removeTab(tab);
michael@0 142
michael@0 143 return deferred.promise;
michael@0 144 }
michael@0 145
michael@0 146 function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
michael@0 147 let deferred = promise.defer();
michael@0 148
michael@0 149 let panel = aMonitor.panelWin;
michael@0 150 let genericEvents = 0;
michael@0 151 let postEvents = 0;
michael@0 152
michael@0 153 function onGenericEvent() {
michael@0 154 genericEvents++;
michael@0 155 maybeResolve();
michael@0 156 }
michael@0 157
michael@0 158 function onPostEvent() {
michael@0 159 postEvents++;
michael@0 160 maybeResolve();
michael@0 161 }
michael@0 162
michael@0 163 function maybeResolve() {
michael@0 164 info("> Network events progress: " +
michael@0 165 genericEvents + "/" + ((aGetRequests + aPostRequests) * 13) + ", " +
michael@0 166 postEvents + "/" + (aPostRequests * 2));
michael@0 167
michael@0 168 // There are 15 updates which need to be fired for a request to be
michael@0 169 // considered finished. RequestPostData isn't fired for non-POST requests.
michael@0 170 if (genericEvents == (aGetRequests + aPostRequests) * 13 &&
michael@0 171 postEvents == aPostRequests * 2) {
michael@0 172
michael@0 173 panel.off(panel.EVENTS.UPDATING_REQUEST_HEADERS, onGenericEvent);
michael@0 174 panel.off(panel.EVENTS.RECEIVED_REQUEST_HEADERS, onGenericEvent);
michael@0 175 panel.off(panel.EVENTS.UPDATING_REQUEST_COOKIES, onGenericEvent);
michael@0 176 panel.off(panel.EVENTS.RECEIVED_REQUEST_COOKIES, onGenericEvent);
michael@0 177 panel.off(panel.EVENTS.UPDATING_REQUEST_POST_DATA, onPostEvent);
michael@0 178 panel.off(panel.EVENTS.RECEIVED_REQUEST_POST_DATA, onPostEvent);
michael@0 179 panel.off(panel.EVENTS.UPDATING_RESPONSE_HEADERS, onGenericEvent);
michael@0 180 panel.off(panel.EVENTS.RECEIVED_RESPONSE_HEADERS, onGenericEvent);
michael@0 181 panel.off(panel.EVENTS.UPDATING_RESPONSE_COOKIES, onGenericEvent);
michael@0 182 panel.off(panel.EVENTS.RECEIVED_RESPONSE_COOKIES, onGenericEvent);
michael@0 183 panel.off(panel.EVENTS.STARTED_RECEIVING_RESPONSE, onGenericEvent);
michael@0 184 panel.off(panel.EVENTS.UPDATING_RESPONSE_CONTENT, onGenericEvent);
michael@0 185 panel.off(panel.EVENTS.RECEIVED_RESPONSE_CONTENT, onGenericEvent);
michael@0 186 panel.off(panel.EVENTS.UPDATING_EVENT_TIMINGS, onGenericEvent);
michael@0 187 panel.off(panel.EVENTS.RECEIVED_EVENT_TIMINGS, onGenericEvent);
michael@0 188
michael@0 189 executeSoon(deferred.resolve);
michael@0 190 }
michael@0 191 }
michael@0 192
michael@0 193 panel.on(panel.EVENTS.UPDATING_REQUEST_HEADERS, onGenericEvent);
michael@0 194 panel.on(panel.EVENTS.RECEIVED_REQUEST_HEADERS, onGenericEvent);
michael@0 195 panel.on(panel.EVENTS.UPDATING_REQUEST_COOKIES, onGenericEvent);
michael@0 196 panel.on(panel.EVENTS.RECEIVED_REQUEST_COOKIES, onGenericEvent);
michael@0 197 panel.on(panel.EVENTS.UPDATING_REQUEST_POST_DATA, onPostEvent);
michael@0 198 panel.on(panel.EVENTS.RECEIVED_REQUEST_POST_DATA, onPostEvent);
michael@0 199 panel.on(panel.EVENTS.UPDATING_RESPONSE_HEADERS, onGenericEvent);
michael@0 200 panel.on(panel.EVENTS.RECEIVED_RESPONSE_HEADERS, onGenericEvent);
michael@0 201 panel.on(panel.EVENTS.UPDATING_RESPONSE_COOKIES, onGenericEvent);
michael@0 202 panel.on(panel.EVENTS.RECEIVED_RESPONSE_COOKIES, onGenericEvent);
michael@0 203 panel.on(panel.EVENTS.STARTED_RECEIVING_RESPONSE, onGenericEvent);
michael@0 204 panel.on(panel.EVENTS.UPDATING_RESPONSE_CONTENT, onGenericEvent);
michael@0 205 panel.on(panel.EVENTS.RECEIVED_RESPONSE_CONTENT, onGenericEvent);
michael@0 206 panel.on(panel.EVENTS.UPDATING_EVENT_TIMINGS, onGenericEvent);
michael@0 207 panel.on(panel.EVENTS.RECEIVED_EVENT_TIMINGS, onGenericEvent);
michael@0 208
michael@0 209 return deferred.promise;
michael@0 210 }
michael@0 211
michael@0 212 function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
michael@0 213 info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
michael@0 214 // This bloats log sizes significantly in automation (bug 992485)
michael@0 215 //info("> Request: " + aRequestItem.attachment.toSource());
michael@0 216
michael@0 217 let requestsMenu = aRequestItem.ownerView;
michael@0 218 let widgetIndex = requestsMenu.indexOfItem(aRequestItem);
michael@0 219 let visibleIndex = requestsMenu.visibleItems.indexOf(aRequestItem);
michael@0 220
michael@0 221 info("Widget index of item: " + widgetIndex);
michael@0 222 info("Visible index of item: " + visibleIndex);
michael@0 223
michael@0 224 let { fuzzyUrl, status, statusText, type, fullMimeType, size, time } = aData;
michael@0 225 let { attachment, target } = aRequestItem
michael@0 226
michael@0 227 let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
michael@0 228 let name = uri.fileName || "/";
michael@0 229 let query = uri.query;
michael@0 230 let hostPort = uri.hostPort;
michael@0 231
michael@0 232 if (fuzzyUrl) {
michael@0 233 ok(attachment.method.startsWith(aMethod), "The attached method is incorrect.");
michael@0 234 ok(attachment.url.startsWith(aUrl), "The attached url is incorrect.");
michael@0 235 } else {
michael@0 236 is(attachment.method, aMethod, "The attached method is incorrect.");
michael@0 237 is(attachment.url, aUrl, "The attached url is incorrect.");
michael@0 238 }
michael@0 239
michael@0 240 is(target.querySelector(".requests-menu-method").getAttribute("value"),
michael@0 241 aMethod, "The displayed method is incorrect.");
michael@0 242
michael@0 243 if (fuzzyUrl) {
michael@0 244 ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
michael@0 245 name + (query ? "?" + query : "")), "The displayed file is incorrect.");
michael@0 246 ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(
michael@0 247 name + (query ? "?" + query : "")), "The tooltip file is incorrect.");
michael@0 248 } else {
michael@0 249 is(target.querySelector(".requests-menu-file").getAttribute("value"),
michael@0 250 name + (query ? "?" + query : ""), "The displayed file is incorrect.");
michael@0 251 is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
michael@0 252 name + (query ? "?" + query : ""), "The tooltip file is incorrect.");
michael@0 253 }
michael@0 254
michael@0 255 is(target.querySelector(".requests-menu-domain").getAttribute("value"),
michael@0 256 hostPort, "The displayed domain is incorrect.");
michael@0 257 is(target.querySelector(".requests-menu-domain").getAttribute("tooltiptext"),
michael@0 258 hostPort, "The tooltip domain is incorrect.");
michael@0 259
michael@0 260 if (status !== undefined) {
michael@0 261 let value = target.querySelector(".requests-menu-status").getAttribute("code");
michael@0 262 let codeValue = target.querySelector(".requests-menu-status-code").getAttribute("value");
michael@0 263 let tooltip = target.querySelector(".requests-menu-status-and-method").getAttribute("tooltiptext");
michael@0 264 info("Displayed status: " + value);
michael@0 265 info("Displayed code: " + codeValue);
michael@0 266 info("Tooltip status: " + tooltip);
michael@0 267 is(value, status, "The displayed status is incorrect.");
michael@0 268 is(codeValue, status, "The displayed status code is incorrect.");
michael@0 269 is(tooltip, status + " " + statusText, "The tooltip status is incorrect.");
michael@0 270 }
michael@0 271 if (type !== undefined) {
michael@0 272 let value = target.querySelector(".requests-menu-type").getAttribute("value");
michael@0 273 let tooltip = target.querySelector(".requests-menu-type").getAttribute("tooltiptext");
michael@0 274 info("Displayed type: " + value);
michael@0 275 info("Tooltip type: " + tooltip);
michael@0 276 is(value, type, "The displayed type is incorrect.");
michael@0 277 is(tooltip, fullMimeType, "The tooltip type is incorrect.");
michael@0 278 }
michael@0 279 if (size !== undefined) {
michael@0 280 let value = target.querySelector(".requests-menu-size").getAttribute("value");
michael@0 281 let tooltip = target.querySelector(".requests-menu-size").getAttribute("tooltiptext");
michael@0 282 info("Displayed size: " + value);
michael@0 283 info("Tooltip size: " + tooltip);
michael@0 284 is(value, size, "The displayed size is incorrect.");
michael@0 285 is(tooltip, size, "The tooltip size is incorrect.");
michael@0 286 }
michael@0 287 if (time !== undefined) {
michael@0 288 let value = target.querySelector(".requests-menu-timings-total").getAttribute("value");
michael@0 289 let tooltip = target.querySelector(".requests-menu-timings-total").getAttribute("tooltiptext");
michael@0 290 info("Displayed time: " + value);
michael@0 291 info("Tooltip time: " + tooltip);
michael@0 292 ok(~~(value.match(/[0-9]+/)) >= 0, "The displayed time is incorrect.");
michael@0 293 ok(~~(tooltip.match(/[0-9]+/)) >= 0, "The tooltip time is incorrect.");
michael@0 294 }
michael@0 295
michael@0 296 if (visibleIndex != -1) {
michael@0 297 if (visibleIndex % 2 == 0) {
michael@0 298 ok(aRequestItem.target.hasAttribute("even"),
michael@0 299 "Unexpected 'even' attribute for " + aRequestItem.value);
michael@0 300 ok(!aRequestItem.target.hasAttribute("odd"),
michael@0 301 "Unexpected 'odd' attribute for " + aRequestItem.value);
michael@0 302 } else {
michael@0 303 ok(!aRequestItem.target.hasAttribute("even"),
michael@0 304 "Unexpected 'even' attribute for " + aRequestItem.value);
michael@0 305 ok(aRequestItem.target.hasAttribute("odd"),
michael@0 306 "Unexpected 'odd' attribute for " + aRequestItem.value);
michael@0 307 }
michael@0 308 }
michael@0 309 }
michael@0 310
michael@0 311 /**
michael@0 312 * Helper function for waiting for an event to fire before resolving a promise.
michael@0 313 * Example: waitFor(aMonitor.panelWin, aMonitor.panelWin.EVENTS.TAB_UPDATED);
michael@0 314 *
michael@0 315 * @param object subject
michael@0 316 * The event emitter object that is being listened to.
michael@0 317 * @param string eventName
michael@0 318 * The name of the event to listen to.
michael@0 319 * @return object
michael@0 320 * Returns a promise that resolves upon firing of the event.
michael@0 321 */
michael@0 322 function waitFor (subject, eventName) {
michael@0 323 let deferred = promise.defer();
michael@0 324 subject.once(eventName, deferred.resolve);
michael@0 325 return deferred.promise;
michael@0 326 }
michael@0 327
michael@0 328 /**
michael@0 329 * Tests if a button for a filter of given type is the only one checked.
michael@0 330 *
michael@0 331 * @param string aFilterType
michael@0 332 * The type of the filter that should be the only one checked.
michael@0 333 */
michael@0 334 function testFilterButtons(aMonitor, aFilterType) {
michael@0 335 let doc = aMonitor.panelWin.document;
michael@0 336 let target = doc.querySelector("#requests-menu-filter-" + aFilterType + "-button");
michael@0 337 let buttons = doc.querySelectorAll(".requests-menu-footer-button");
michael@0 338
michael@0 339 // Only target should be checked.
michael@0 340 let checkStatus = [(button == target) ? 1 : 0 for (button of buttons)]
michael@0 341 testFilterButtonsCustom(aMonitor, checkStatus);
michael@0 342 }
michael@0 343
michael@0 344 /**
michael@0 345 * Tests if filter buttons have 'checked' attributes set correctly.
michael@0 346 *
michael@0 347 * @param array aIsChecked
michael@0 348 * An array specifying if a button at given index should have a
michael@0 349 * 'checked' attribute. For example, if the third item of the array
michael@0 350 * evaluates to true, the third button should be checked.
michael@0 351 */
michael@0 352 function testFilterButtonsCustom(aMonitor, aIsChecked) {
michael@0 353 let doc = aMonitor.panelWin.document;
michael@0 354 let buttons = doc.querySelectorAll(".requests-menu-footer-button");
michael@0 355 for (let i = 0; i < aIsChecked.length; i++) {
michael@0 356 let button = buttons[i];
michael@0 357 if (aIsChecked[i]) {
michael@0 358 is(button.hasAttribute("checked"), true,
michael@0 359 "The " + button.id + " button should have a 'checked' attribute.");
michael@0 360 } else {
michael@0 361 is(button.hasAttribute("checked"), false,
michael@0 362 "The " + button.id + " button should not have a 'checked' attribute.");
michael@0 363 }
michael@0 364 }
michael@0 365 }

mercurial