Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
michael@0 | 2 | /* Any copyright is dedicated to the Public Domain. |
michael@0 | 3 | http://creativecommons.org/publicdomain/zero/1.0/ */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | const Cu = Components.utils; |
michael@0 | 8 | let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); |
michael@0 | 9 | let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
michael@0 | 10 | let TargetFactory = devtools.TargetFactory; |
michael@0 | 11 | let {CssHtmlTree} = devtools.require("devtools/styleinspector/computed-view"); |
michael@0 | 12 | let {CssRuleView, _ElementStyle} = devtools.require("devtools/styleinspector/rule-view"); |
michael@0 | 13 | let {CssLogic, CssSelector} = devtools.require("devtools/styleinspector/css-logic"); |
michael@0 | 14 | let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); |
michael@0 | 15 | let {editableField, getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor"); |
michael@0 | 16 | let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}); |
michael@0 | 17 | |
michael@0 | 18 | // All test are asynchronous |
michael@0 | 19 | waitForExplicitFinish(); |
michael@0 | 20 | |
michael@0 | 21 | const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/styleinspector/test/"; |
michael@0 | 22 | const TEST_URL_ROOT_SSL = "https://example.com/browser/browser/devtools/styleinspector/test/"; |
michael@0 | 23 | |
michael@0 | 24 | // Auto clean-up when a test ends |
michael@0 | 25 | registerCleanupFunction(() => { |
michael@0 | 26 | try { |
michael@0 | 27 | let target = TargetFactory.forTab(gBrowser.selectedTab); |
michael@0 | 28 | gDevTools.closeToolbox(target); |
michael@0 | 29 | } catch (ex) { |
michael@0 | 30 | dump(ex); |
michael@0 | 31 | } |
michael@0 | 32 | while (gBrowser.tabs.length > 1) { |
michael@0 | 33 | gBrowser.removeCurrentTab(); |
michael@0 | 34 | } |
michael@0 | 35 | }); |
michael@0 | 36 | |
michael@0 | 37 | // Uncomment to log events |
michael@0 | 38 | // Services.prefs.setBoolPref("devtools.dump.emit", true); |
michael@0 | 39 | |
michael@0 | 40 | // Clean-up all prefs that might have been changed during a test run |
michael@0 | 41 | // (safer here because if the test fails, then the pref is never reverted) |
michael@0 | 42 | registerCleanupFunction(() => { |
michael@0 | 43 | Services.prefs.clearUserPref("devtools.dump.emit"); |
michael@0 | 44 | Services.prefs.clearUserPref("devtools.defaultColorUnit"); |
michael@0 | 45 | }); |
michael@0 | 46 | |
michael@0 | 47 | /** |
michael@0 | 48 | * The functions found below are here to ease test development and maintenance. |
michael@0 | 49 | * Most of these functions are stateless and will require some form of context |
michael@0 | 50 | * (the instance of the current toolbox, or inspector panel for instance). |
michael@0 | 51 | * |
michael@0 | 52 | * Most of these functions are async too and return promises. |
michael@0 | 53 | * |
michael@0 | 54 | * All tests should follow the following pattern: |
michael@0 | 55 | * |
michael@0 | 56 | * let test = asyncTest(function*() { |
michael@0 | 57 | * yield addTab(TEST_URI); |
michael@0 | 58 | * let {toolbox, inspector, view} = yield openComputedView(); |
michael@0 | 59 | * |
michael@0 | 60 | * yield selectNode("#test", inspector); |
michael@0 | 61 | * yield someAsyncTestFunction(view); |
michael@0 | 62 | * }); |
michael@0 | 63 | * |
michael@0 | 64 | * asyncTest is the way to define the testcase in the test file. It accepts |
michael@0 | 65 | * a single generator-function argument. |
michael@0 | 66 | * The generator function should yield any async call. |
michael@0 | 67 | * |
michael@0 | 68 | * There is no need to clean tabs up at the end of a test as this is done |
michael@0 | 69 | * automatically. |
michael@0 | 70 | * |
michael@0 | 71 | * It is advised not to store any references on the global scope. There shouldn't |
michael@0 | 72 | * be a need to anyway. Thanks to asyncTest, test steps, even though asynchronous, |
michael@0 | 73 | * can be described in a nice flat way, and if/for/while/... control flow can be |
michael@0 | 74 | * used as in sync code, making it possible to write the outline of the test case |
michael@0 | 75 | * all in asyncTest, and delegate actual processing and assertions to other |
michael@0 | 76 | * functions. |
michael@0 | 77 | */ |
michael@0 | 78 | |
michael@0 | 79 | /* ********************************************* |
michael@0 | 80 | * UTILS |
michael@0 | 81 | * ********************************************* |
michael@0 | 82 | * General test utilities. |
michael@0 | 83 | * Define the test case, add new tabs, open the toolbox and switch to the |
michael@0 | 84 | * various panels, select nodes, get node references, ... |
michael@0 | 85 | */ |
michael@0 | 86 | |
michael@0 | 87 | /** |
michael@0 | 88 | * Define an async test based on a generator function |
michael@0 | 89 | */ |
michael@0 | 90 | function asyncTest(generator) { |
michael@0 | 91 | return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish); |
michael@0 | 92 | } |
michael@0 | 93 | |
michael@0 | 94 | /** |
michael@0 | 95 | * Add a new test tab in the browser and load the given url. |
michael@0 | 96 | * @param {String} url The url to be loaded in the new tab |
michael@0 | 97 | * @return a promise that resolves to the tab object when the url is loaded |
michael@0 | 98 | */ |
michael@0 | 99 | function addTab(url) { |
michael@0 | 100 | let def = promise.defer(); |
michael@0 | 101 | |
michael@0 | 102 | let tab = gBrowser.selectedTab = gBrowser.addTab(); |
michael@0 | 103 | gBrowser.selectedBrowser.addEventListener("load", function onload() { |
michael@0 | 104 | gBrowser.selectedBrowser.removeEventListener("load", onload, true); |
michael@0 | 105 | info("URL " + url + " loading complete into new test tab"); |
michael@0 | 106 | waitForFocus(() => { |
michael@0 | 107 | def.resolve(tab); |
michael@0 | 108 | }, content); |
michael@0 | 109 | }, true); |
michael@0 | 110 | content.location = url; |
michael@0 | 111 | |
michael@0 | 112 | return def.promise; |
michael@0 | 113 | } |
michael@0 | 114 | |
michael@0 | 115 | /** |
michael@0 | 116 | * Simple DOM node accesor function that takes either a node or a string css |
michael@0 | 117 | * selector as argument and returns the corresponding node |
michael@0 | 118 | * @param {String|DOMNode} nodeOrSelector |
michael@0 | 119 | * @return {DOMNode} |
michael@0 | 120 | */ |
michael@0 | 121 | function getNode(nodeOrSelector) { |
michael@0 | 122 | return typeof nodeOrSelector === "string" ? |
michael@0 | 123 | content.document.querySelector(nodeOrSelector) : |
michael@0 | 124 | nodeOrSelector; |
michael@0 | 125 | } |
michael@0 | 126 | |
michael@0 | 127 | /** |
michael@0 | 128 | * Set the inspector's current selection to a node or to the first match of the |
michael@0 | 129 | * given css selector |
michael@0 | 130 | * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
michael@0 | 131 | * loaded in the toolbox |
michael@0 | 132 | * @param {String} reason Defaults to "test" which instructs the inspector not |
michael@0 | 133 | * to highlight the node upon selection |
michael@0 | 134 | * @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection |
michael@0 | 135 | * @return a promise that resolves when the inspector is updated with the new |
michael@0 | 136 | * node |
michael@0 | 137 | */ |
michael@0 | 138 | function selectNode(nodeOrSelector, inspector, reason="test") { |
michael@0 | 139 | info("Selecting the node " + nodeOrSelector); |
michael@0 | 140 | let node = getNode(nodeOrSelector); |
michael@0 | 141 | let updated = inspector.once("inspector-updated"); |
michael@0 | 142 | inspector.selection.setNode(node, reason); |
michael@0 | 143 | return updated; |
michael@0 | 144 | } |
michael@0 | 145 | |
michael@0 | 146 | /** |
michael@0 | 147 | * Set the inspector's current selection to null so that no node is selected |
michael@0 | 148 | * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
michael@0 | 149 | * loaded in the toolbox |
michael@0 | 150 | * @return a promise that resolves when the inspector is updated |
michael@0 | 151 | */ |
michael@0 | 152 | function clearCurrentNodeSelection(inspector) { |
michael@0 | 153 | info("Clearing the current selection"); |
michael@0 | 154 | let updated = inspector.once("inspector-updated"); |
michael@0 | 155 | inspector.selection.setNode(null); |
michael@0 | 156 | return updated; |
michael@0 | 157 | } |
michael@0 | 158 | |
michael@0 | 159 | /** |
michael@0 | 160 | * Open the toolbox, with the inspector tool visible. |
michael@0 | 161 | * @return a promise that resolves when the inspector is ready |
michael@0 | 162 | */ |
michael@0 | 163 | let openInspector = Task.async(function*() { |
michael@0 | 164 | info("Opening the inspector"); |
michael@0 | 165 | let target = TargetFactory.forTab(gBrowser.selectedTab); |
michael@0 | 166 | |
michael@0 | 167 | let inspector, toolbox; |
michael@0 | 168 | |
michael@0 | 169 | // Checking if the toolbox and the inspector are already loaded |
michael@0 | 170 | // The inspector-updated event should only be waited for if the inspector |
michael@0 | 171 | // isn't loaded yet |
michael@0 | 172 | toolbox = gDevTools.getToolbox(target); |
michael@0 | 173 | if (toolbox) { |
michael@0 | 174 | inspector = toolbox.getPanel("inspector"); |
michael@0 | 175 | if (inspector) { |
michael@0 | 176 | info("Toolbox and inspector already open"); |
michael@0 | 177 | return { |
michael@0 | 178 | toolbox: toolbox, |
michael@0 | 179 | inspector: inspector |
michael@0 | 180 | }; |
michael@0 | 181 | } |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | info("Opening the toolbox"); |
michael@0 | 185 | toolbox = yield gDevTools.showToolbox(target, "inspector"); |
michael@0 | 186 | yield waitForToolboxFrameFocus(toolbox); |
michael@0 | 187 | inspector = toolbox.getPanel("inspector"); |
michael@0 | 188 | |
michael@0 | 189 | info("Waiting for the inspector to update"); |
michael@0 | 190 | yield inspector.once("inspector-updated"); |
michael@0 | 191 | |
michael@0 | 192 | return { |
michael@0 | 193 | toolbox: toolbox, |
michael@0 | 194 | inspector: inspector |
michael@0 | 195 | }; |
michael@0 | 196 | }); |
michael@0 | 197 | |
michael@0 | 198 | /** |
michael@0 | 199 | * Wait for the toolbox frame to receive focus after it loads |
michael@0 | 200 | * @param {Toolbox} toolbox |
michael@0 | 201 | * @return a promise that resolves when focus has been received |
michael@0 | 202 | */ |
michael@0 | 203 | function waitForToolboxFrameFocus(toolbox) { |
michael@0 | 204 | info("Making sure that the toolbox's frame is focused"); |
michael@0 | 205 | let def = promise.defer(); |
michael@0 | 206 | let win = toolbox.frame.contentWindow; |
michael@0 | 207 | waitForFocus(def.resolve, win); |
michael@0 | 208 | return def.promise; |
michael@0 | 209 | } |
michael@0 | 210 | |
michael@0 | 211 | /** |
michael@0 | 212 | * Open the toolbox, with the inspector tool visible, and the sidebar that |
michael@0 | 213 | * corresponds to the given id selected |
michael@0 | 214 | * @return a promise that resolves when the inspector is ready and the sidebar |
michael@0 | 215 | * view is visible and ready |
michael@0 | 216 | */ |
michael@0 | 217 | let openInspectorSideBar = Task.async(function*(id) { |
michael@0 | 218 | let {toolbox, inspector} = yield openInspector(); |
michael@0 | 219 | |
michael@0 | 220 | if (!hasSideBarTab(inspector, id)) { |
michael@0 | 221 | info("Waiting for the " + id + " sidebar to be ready"); |
michael@0 | 222 | yield inspector.sidebar.once(id + "-ready"); |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | info("Selecting the " + id + " sidebar"); |
michael@0 | 226 | inspector.sidebar.select(id); |
michael@0 | 227 | |
michael@0 | 228 | return { |
michael@0 | 229 | toolbox: toolbox, |
michael@0 | 230 | inspector: inspector, |
michael@0 | 231 | view: inspector.sidebar.getWindowForTab(id)[id].view |
michael@0 | 232 | }; |
michael@0 | 233 | }); |
michael@0 | 234 | |
michael@0 | 235 | /** |
michael@0 | 236 | * Open the toolbox, with the inspector tool visible, and the computed-view |
michael@0 | 237 | * sidebar tab selected. |
michael@0 | 238 | * @return a promise that resolves when the inspector is ready and the computed |
michael@0 | 239 | * view is visible and ready |
michael@0 | 240 | */ |
michael@0 | 241 | function openComputedView() { |
michael@0 | 242 | return openInspectorSideBar("computedview"); |
michael@0 | 243 | } |
michael@0 | 244 | |
michael@0 | 245 | /** |
michael@0 | 246 | * Open the toolbox, with the inspector tool visible, and the rule-view |
michael@0 | 247 | * sidebar tab selected. |
michael@0 | 248 | * @return a promise that resolves when the inspector is ready and the rule |
michael@0 | 249 | * view is visible and ready |
michael@0 | 250 | */ |
michael@0 | 251 | function openRuleView() { |
michael@0 | 252 | return openInspectorSideBar("ruleview"); |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | /** |
michael@0 | 256 | * Wait for eventName on target. |
michael@0 | 257 | * @param {Object} target An observable object that either supports on/off or |
michael@0 | 258 | * addEventListener/removeEventListener |
michael@0 | 259 | * @param {String} eventName |
michael@0 | 260 | * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener |
michael@0 | 261 | * @return A promise that resolves when the event has been handled |
michael@0 | 262 | */ |
michael@0 | 263 | function once(target, eventName, useCapture=false) { |
michael@0 | 264 | info("Waiting for event: '" + eventName + "' on " + target + "."); |
michael@0 | 265 | |
michael@0 | 266 | let deferred = promise.defer(); |
michael@0 | 267 | |
michael@0 | 268 | for (let [add, remove] of [ |
michael@0 | 269 | ["addEventListener", "removeEventListener"], |
michael@0 | 270 | ["addListener", "removeListener"], |
michael@0 | 271 | ["on", "off"] |
michael@0 | 272 | ]) { |
michael@0 | 273 | if ((add in target) && (remove in target)) { |
michael@0 | 274 | target[add](eventName, function onEvent(...aArgs) { |
michael@0 | 275 | target[remove](eventName, onEvent, useCapture); |
michael@0 | 276 | deferred.resolve.apply(deferred, aArgs); |
michael@0 | 277 | }, useCapture); |
michael@0 | 278 | break; |
michael@0 | 279 | } |
michael@0 | 280 | } |
michael@0 | 281 | |
michael@0 | 282 | return deferred.promise; |
michael@0 | 283 | } |
michael@0 | 284 | |
michael@0 | 285 | /** |
michael@0 | 286 | * This shouldn't be used in the tests, but is useful when writing new tests or |
michael@0 | 287 | * debugging existing tests in order to introduce delays in the test steps |
michael@0 | 288 | * @param {Number} ms The time to wait |
michael@0 | 289 | * @return A promise that resolves when the time is passed |
michael@0 | 290 | */ |
michael@0 | 291 | function wait(ms) { |
michael@0 | 292 | let def = promise.defer(); |
michael@0 | 293 | content.setTimeout(def.resolve, ms); |
michael@0 | 294 | return def.promise; |
michael@0 | 295 | } |
michael@0 | 296 | |
michael@0 | 297 | /** |
michael@0 | 298 | * Given an inplace editable element, click to switch it to edit mode, wait for |
michael@0 | 299 | * focus |
michael@0 | 300 | * @return a promise that resolves to the inplace-editor element when ready |
michael@0 | 301 | */ |
michael@0 | 302 | let focusEditableField = Task.async(function*(editable, xOffset=1, yOffset=1, options={}) { |
michael@0 | 303 | let onFocus = once(editable.parentNode, "focus", true); |
michael@0 | 304 | |
michael@0 | 305 | info("Clicking on editable field to turn to edit mode"); |
michael@0 | 306 | EventUtils.synthesizeMouse(editable, xOffset, yOffset, options, |
michael@0 | 307 | editable.ownerDocument.defaultView); |
michael@0 | 308 | let event = yield onFocus; |
michael@0 | 309 | |
michael@0 | 310 | info("Editable field gained focus, returning the input field now"); |
michael@0 | 311 | return inplaceEditor(editable.ownerDocument.activeElement); |
michael@0 | 312 | }); |
michael@0 | 313 | |
michael@0 | 314 | /** |
michael@0 | 315 | * Given a tooltip object instance (see Tooltip.js), checks if it is set to |
michael@0 | 316 | * toggle and hover and if so, checks if the given target is a valid hover target. |
michael@0 | 317 | * This won't actually show the tooltip (the less we interact with XUL panels |
michael@0 | 318 | * during test runs, the better). |
michael@0 | 319 | * @return a promise that resolves when the answer is known |
michael@0 | 320 | */ |
michael@0 | 321 | function isHoverTooltipTarget(tooltip, target) { |
michael@0 | 322 | if (!tooltip._basedNode || !tooltip.panel) { |
michael@0 | 323 | return promise.reject(new Error( |
michael@0 | 324 | "The tooltip passed isn't set to toggle on hover or is not a tooltip")); |
michael@0 | 325 | } |
michael@0 | 326 | return tooltip.isValidHoverTarget(target); |
michael@0 | 327 | } |
michael@0 | 328 | |
michael@0 | 329 | /** |
michael@0 | 330 | * Same as isHoverTooltipTarget except that it will fail the test if there is no |
michael@0 | 331 | * tooltip defined on hover of the given element |
michael@0 | 332 | * @return a promise |
michael@0 | 333 | */ |
michael@0 | 334 | function assertHoverTooltipOn(tooltip, element) { |
michael@0 | 335 | return isHoverTooltipTarget(tooltip, element).then(() => { |
michael@0 | 336 | ok(true, "A tooltip is defined on hover of the given element"); |
michael@0 | 337 | }, () => { |
michael@0 | 338 | ok(false, "No tooltip is defined on hover of the given element"); |
michael@0 | 339 | }); |
michael@0 | 340 | } |
michael@0 | 341 | |
michael@0 | 342 | /** |
michael@0 | 343 | * Same as assertHoverTooltipOn but fails the test if there is a tooltip defined |
michael@0 | 344 | * on hover of the given element |
michael@0 | 345 | * @return a promise |
michael@0 | 346 | */ |
michael@0 | 347 | function assertNoHoverTooltipOn(tooltip, element) { |
michael@0 | 348 | return isHoverTooltipTarget(tooltip, element).then(() => { |
michael@0 | 349 | ok(false, "A tooltip is defined on hover of the given element"); |
michael@0 | 350 | }, () => { |
michael@0 | 351 | ok(true, "No tooltip is defined on hover of the given element"); |
michael@0 | 352 | }); |
michael@0 | 353 | } |
michael@0 | 354 | |
michael@0 | 355 | /** |
michael@0 | 356 | * Listen for a new window to open and return a promise that resolves when one |
michael@0 | 357 | * does and completes its load. |
michael@0 | 358 | * Only resolves when the new window topic isn't domwindowopened. |
michael@0 | 359 | * @return a promise that resolves to the window object |
michael@0 | 360 | */ |
michael@0 | 361 | function waitForWindow() { |
michael@0 | 362 | let def = promise.defer(); |
michael@0 | 363 | |
michael@0 | 364 | info("Waiting for a window to open"); |
michael@0 | 365 | Services.ww.registerNotification(function onWindow(subject, topic) { |
michael@0 | 366 | if (topic != "domwindowopened") { |
michael@0 | 367 | return; |
michael@0 | 368 | } |
michael@0 | 369 | info("A window has been opened"); |
michael@0 | 370 | let win = subject.QueryInterface(Ci.nsIDOMWindow); |
michael@0 | 371 | once(win, "load").then(() => { |
michael@0 | 372 | info("The window load completed"); |
michael@0 | 373 | Services.ww.unregisterNotification(onWindow); |
michael@0 | 374 | def.resolve(win); |
michael@0 | 375 | }); |
michael@0 | 376 | }); |
michael@0 | 377 | |
michael@0 | 378 | return def.promise; |
michael@0 | 379 | } |
michael@0 | 380 | |
michael@0 | 381 | /** |
michael@0 | 382 | * @see SimpleTest.waitForClipboard |
michael@0 | 383 | * @param {Function} setup Function to execute before checking for the |
michael@0 | 384 | * clipboard content |
michael@0 | 385 | * @param {String|Boolean} expected An expected string or validator function |
michael@0 | 386 | * @return a promise that resolves when the expected string has been found or |
michael@0 | 387 | * the validator function has returned true, rejects otherwise. |
michael@0 | 388 | */ |
michael@0 | 389 | function waitForClipboard(setup, expected) { |
michael@0 | 390 | let def = promise.defer(); |
michael@0 | 391 | SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject); |
michael@0 | 392 | return def.promise; |
michael@0 | 393 | } |
michael@0 | 394 | |
michael@0 | 395 | /** |
michael@0 | 396 | * Dispatch the copy event on the given element |
michael@0 | 397 | */ |
michael@0 | 398 | function fireCopyEvent(element) { |
michael@0 | 399 | let evt = element.ownerDocument.createEvent("Event"); |
michael@0 | 400 | evt.initEvent("copy", true, true); |
michael@0 | 401 | element.dispatchEvent(evt); |
michael@0 | 402 | } |
michael@0 | 403 | |
michael@0 | 404 | /** |
michael@0 | 405 | * Polls a given function waiting for it to return true. |
michael@0 | 406 | * |
michael@0 | 407 | * @param {Function} validatorFn A validator function that returns a boolean. |
michael@0 | 408 | * This is called every few milliseconds to check if the result is true. When |
michael@0 | 409 | * it is true, the promise resolves. If validatorFn never returns true, then |
michael@0 | 410 | * polling timeouts after several tries and the promise rejects. |
michael@0 | 411 | * @param {String} name Optional name of the test. This is used to generate |
michael@0 | 412 | * the success and failure messages. |
michael@0 | 413 | * @param {Number} timeout Optional timeout for the validator function, in |
michael@0 | 414 | * milliseconds. Default is 5000. |
michael@0 | 415 | * @return a promise that resolves when the function returned true or rejects |
michael@0 | 416 | * if the timeout is reached |
michael@0 | 417 | */ |
michael@0 | 418 | function waitForSuccess(validatorFn, name="untitled", timeout=5000) { |
michael@0 | 419 | let def = promise.defer(); |
michael@0 | 420 | let start = Date.now(); |
michael@0 | 421 | |
michael@0 | 422 | function wait(validatorFn) { |
michael@0 | 423 | if ((Date.now() - start) > timeout) { |
michael@0 | 424 | ok(false, "Validator function " + name + " timed out"); |
michael@0 | 425 | return def.reject(); |
michael@0 | 426 | } |
michael@0 | 427 | if (validatorFn()) { |
michael@0 | 428 | ok(true, "Validator function " + name + " returned true"); |
michael@0 | 429 | def.resolve(); |
michael@0 | 430 | } else { |
michael@0 | 431 | setTimeout(() => wait(validatorFn), 100); |
michael@0 | 432 | } |
michael@0 | 433 | } |
michael@0 | 434 | wait(validatorFn); |
michael@0 | 435 | |
michael@0 | 436 | return def.promise; |
michael@0 | 437 | } |
michael@0 | 438 | |
michael@0 | 439 | /** |
michael@0 | 440 | * Create a new style tag containing the given style text and append it to the |
michael@0 | 441 | * document's head node |
michael@0 | 442 | * @param {Document} doc |
michael@0 | 443 | * @param {String} style |
michael@0 | 444 | * @return {DOMNode} The newly created style node |
michael@0 | 445 | */ |
michael@0 | 446 | function addStyle(doc, style) { |
michael@0 | 447 | info("Adding a new style tag to the document with style content: " + |
michael@0 | 448 | style.substring(0, 50)); |
michael@0 | 449 | let node = doc.createElement('style'); |
michael@0 | 450 | node.setAttribute("type", "text/css"); |
michael@0 | 451 | node.textContent = style; |
michael@0 | 452 | doc.getElementsByTagName("head")[0].appendChild(node); |
michael@0 | 453 | return node; |
michael@0 | 454 | } |
michael@0 | 455 | |
michael@0 | 456 | /** |
michael@0 | 457 | * Checks whether the inspector's sidebar corresponding to the given id already |
michael@0 | 458 | * exists |
michael@0 | 459 | * @param {InspectorPanel} |
michael@0 | 460 | * @param {String} |
michael@0 | 461 | * @return {Boolean} |
michael@0 | 462 | */ |
michael@0 | 463 | function hasSideBarTab(inspector, id) { |
michael@0 | 464 | return !!inspector.sidebar.getWindowForTab(id); |
michael@0 | 465 | } |
michael@0 | 466 | |
michael@0 | 467 | /* ********************************************* |
michael@0 | 468 | * RULE-VIEW |
michael@0 | 469 | * ********************************************* |
michael@0 | 470 | * Rule-view related test utility functions |
michael@0 | 471 | * This object contains functions to get rules, get properties, ... |
michael@0 | 472 | */ |
michael@0 | 473 | |
michael@0 | 474 | /** |
michael@0 | 475 | * Get the DOMNode for a css rule in the rule-view that corresponds to the given |
michael@0 | 476 | * selector |
michael@0 | 477 | * @param {CssRuleView} view The instance of the rule-view panel |
michael@0 | 478 | * @param {String} selectorText The selector in the rule-view for which the rule |
michael@0 | 479 | * object is wanted |
michael@0 | 480 | * @return {DOMNode} |
michael@0 | 481 | */ |
michael@0 | 482 | function getRuleViewRule(view, selectorText) { |
michael@0 | 483 | let rule; |
michael@0 | 484 | for (let r of view.doc.querySelectorAll(".ruleview-rule")) { |
michael@0 | 485 | let selector = r.querySelector(".ruleview-selector, .ruleview-selector-matched"); |
michael@0 | 486 | if (selector && selector.textContent === selectorText) { |
michael@0 | 487 | rule = r; |
michael@0 | 488 | break; |
michael@0 | 489 | } |
michael@0 | 490 | } |
michael@0 | 491 | |
michael@0 | 492 | return rule; |
michael@0 | 493 | } |
michael@0 | 494 | |
michael@0 | 495 | /** |
michael@0 | 496 | * Get references to the name and value span nodes corresponding to a given |
michael@0 | 497 | * selector and property name in the rule-view |
michael@0 | 498 | * @param {CssRuleView} view The instance of the rule-view panel |
michael@0 | 499 | * @param {String} selectorText The selector in the rule-view to look for the |
michael@0 | 500 | * property in |
michael@0 | 501 | * @param {String} propertyName The name of the property |
michael@0 | 502 | * @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode} |
michael@0 | 503 | */ |
michael@0 | 504 | function getRuleViewProperty(view, selectorText, propertyName) { |
michael@0 | 505 | let prop; |
michael@0 | 506 | |
michael@0 | 507 | let rule = getRuleViewRule(view, selectorText); |
michael@0 | 508 | if (rule) { |
michael@0 | 509 | // Look for the propertyName in that rule element |
michael@0 | 510 | for (let p of rule.querySelectorAll(".ruleview-property")) { |
michael@0 | 511 | let nameSpan = p.querySelector(".ruleview-propertyname"); |
michael@0 | 512 | let valueSpan = p.querySelector(".ruleview-propertyvalue"); |
michael@0 | 513 | |
michael@0 | 514 | if (nameSpan.textContent === propertyName) { |
michael@0 | 515 | prop = {nameSpan: nameSpan, valueSpan: valueSpan}; |
michael@0 | 516 | break; |
michael@0 | 517 | } |
michael@0 | 518 | } |
michael@0 | 519 | } |
michael@0 | 520 | return prop; |
michael@0 | 521 | } |
michael@0 | 522 | |
michael@0 | 523 | /** |
michael@0 | 524 | * Get the text value of the property corresponding to a given selector and name |
michael@0 | 525 | * in the rule-view |
michael@0 | 526 | * @param {CssRuleView} view The instance of the rule-view panel |
michael@0 | 527 | * @param {String} selectorText The selector in the rule-view to look for the |
michael@0 | 528 | * property in |
michael@0 | 529 | * @param {String} propertyName The name of the property |
michael@0 | 530 | * @return {String} The property value |
michael@0 | 531 | */ |
michael@0 | 532 | function getRuleViewPropertyValue(view, selectorText, propertyName) { |
michael@0 | 533 | return getRuleViewProperty(view, selectorText, propertyName) |
michael@0 | 534 | .valueSpan.textContent; |
michael@0 | 535 | } |
michael@0 | 536 | |
michael@0 | 537 | /** |
michael@0 | 538 | * Simulate a color change in a given color picker tooltip, and optionally wait |
michael@0 | 539 | * for a given element in the page to have its style changed as a result |
michael@0 | 540 | * @param {SwatchColorPickerTooltip} colorPicker |
michael@0 | 541 | * @param {Array} newRgba The new color to be set [r, g, b, a] |
michael@0 | 542 | * @param {Object} expectedChange Optional object that needs the following props: |
michael@0 | 543 | * - {DOMNode} element The element in the page that will have its |
michael@0 | 544 | * style changed. |
michael@0 | 545 | * - {String} name The style name that will be changed |
michael@0 | 546 | * - {String} value The expected style value |
michael@0 | 547 | * The style will be checked like so: getComputedStyle(element)[name] === value |
michael@0 | 548 | */ |
michael@0 | 549 | let simulateColorPickerChange = Task.async(function*(colorPicker, newRgba, expectedChange) { |
michael@0 | 550 | info("Getting the spectrum colorpicker object"); |
michael@0 | 551 | let spectrum = yield colorPicker.spectrum; |
michael@0 | 552 | info("Setting the new color"); |
michael@0 | 553 | spectrum.rgb = newRgba; |
michael@0 | 554 | info("Applying the change"); |
michael@0 | 555 | spectrum.updateUI(); |
michael@0 | 556 | spectrum.onChange(); |
michael@0 | 557 | |
michael@0 | 558 | if (expectedChange) { |
michael@0 | 559 | info("Waiting for the style to be applied on the page"); |
michael@0 | 560 | yield waitForSuccess(() => { |
michael@0 | 561 | let {element, name, value} = expectedChange; |
michael@0 | 562 | return content.getComputedStyle(element)[name] === value; |
michael@0 | 563 | }, "Color picker change applied on the page"); |
michael@0 | 564 | } |
michael@0 | 565 | }); |
michael@0 | 566 | |
michael@0 | 567 | /** |
michael@0 | 568 | * Get a rule-link from the rule-view given its index |
michael@0 | 569 | * @param {CssRuleView} view The instance of the rule-view panel |
michael@0 | 570 | * @param {Number} index The index of the link to get |
michael@0 | 571 | * @return {DOMNode} The link if any at this index |
michael@0 | 572 | */ |
michael@0 | 573 | function getRuleViewLinkByIndex(view, index) { |
michael@0 | 574 | let links = view.doc.querySelectorAll(".ruleview-rule-source"); |
michael@0 | 575 | return links[index]; |
michael@0 | 576 | } |
michael@0 | 577 | |
michael@0 | 578 | /** |
michael@0 | 579 | * Click on a rule-view's close brace to focus a new property name editor |
michael@0 | 580 | * @param {RuleEditor} ruleEditor An instance of RuleEditor that will receive |
michael@0 | 581 | * the new property |
michael@0 | 582 | * @return a promise that resolves to the newly created editor when ready and |
michael@0 | 583 | * focused |
michael@0 | 584 | */ |
michael@0 | 585 | let focusNewRuleViewProperty = Task.async(function*(ruleEditor) { |
michael@0 | 586 | info("Clicking on a close ruleEditor brace to start editing a new property"); |
michael@0 | 587 | ruleEditor.closeBrace.scrollIntoView(); |
michael@0 | 588 | let editor = yield focusEditableField(ruleEditor.closeBrace); |
michael@0 | 589 | |
michael@0 | 590 | is(inplaceEditor(ruleEditor.newPropSpan), editor, "Focused editor is the new property editor."); |
michael@0 | 591 | is(ruleEditor.rule.textProps.length, 0, "Starting with one new text property."); |
michael@0 | 592 | is(ruleEditor.propertyList.children.length, 1, "Starting with two property editors."); |
michael@0 | 593 | |
michael@0 | 594 | return editor; |
michael@0 | 595 | }); |
michael@0 | 596 | |
michael@0 | 597 | /** |
michael@0 | 598 | * Create a new property name in the rule-view, focusing a new property editor |
michael@0 | 599 | * by clicking on the close brace, and then entering the given text. |
michael@0 | 600 | * Keep in mind that the rule-view knows how to handle strings with multiple |
michael@0 | 601 | * properties, so the input text may be like: "p1:v1;p2:v2;p3:v3". |
michael@0 | 602 | * @param {RuleEditor} ruleEditor The instance of RuleEditor that will receive |
michael@0 | 603 | * the new property(ies) |
michael@0 | 604 | * @param {String} inputValue The text to be entered in the new property name |
michael@0 | 605 | * field |
michael@0 | 606 | * @return a promise that resolves when the new property name has been entered |
michael@0 | 607 | * and once the value field is focused |
michael@0 | 608 | */ |
michael@0 | 609 | let createNewRuleViewProperty = Task.async(function*(ruleEditor, inputValue) { |
michael@0 | 610 | info("Creating a new property editor"); |
michael@0 | 611 | let editor = yield focusNewRuleViewProperty(ruleEditor); |
michael@0 | 612 | |
michael@0 | 613 | info("Entering the value " + inputValue); |
michael@0 | 614 | editor.input.value = inputValue; |
michael@0 | 615 | |
michael@0 | 616 | info("Submitting the new value and waiting for value field focus"); |
michael@0 | 617 | let onFocus = once(ruleEditor.element, "focus", true); |
michael@0 | 618 | EventUtils.synthesizeKey("VK_RETURN", {}, |
michael@0 | 619 | ruleEditor.element.ownerDocument.defaultView); |
michael@0 | 620 | yield onFocus; |
michael@0 | 621 | }); |
michael@0 | 622 | |
michael@0 | 623 | // TO BE UNCOMMENTED WHEN THE EYEDROPPER FINALLY LANDS |
michael@0 | 624 | // /** |
michael@0 | 625 | // * Given a color swatch in the ruleview, click on it to open the color picker |
michael@0 | 626 | // * and then click on the eyedropper button to start the eyedropper tool |
michael@0 | 627 | // * @param {CssRuleView} view The instance of the rule-view panel |
michael@0 | 628 | // * @param {DOMNode} swatch The color swatch to be clicked on |
michael@0 | 629 | // * @return A promise that resolves when the dropper is opened |
michael@0 | 630 | // */ |
michael@0 | 631 | // let openRuleViewEyeDropper = Task.async(function*(view, swatch) { |
michael@0 | 632 | // info("Opening the colorpicker tooltip on a colorswatch"); |
michael@0 | 633 | // let tooltip = view.colorPicker.tooltip; |
michael@0 | 634 | // let onTooltipShown = tooltip.once("shown"); |
michael@0 | 635 | // swatch.click(); |
michael@0 | 636 | // yield onTooltipShown; |
michael@0 | 637 | |
michael@0 | 638 | // info("Finding the eyedropper icon in the colorpicker document"); |
michael@0 | 639 | // let tooltipDoc = tooltip.content.contentDocument; |
michael@0 | 640 | // let dropperButton = tooltipDoc.querySelector("#eyedropper-button"); |
michael@0 | 641 | // ok(dropperButton, "Found the eyedropper icon"); |
michael@0 | 642 | |
michael@0 | 643 | // info("Opening the eyedropper"); |
michael@0 | 644 | // let onOpen = tooltip.once("eyedropper-opened"); |
michael@0 | 645 | // dropperButton.click(); |
michael@0 | 646 | // return yield onOpen; |
michael@0 | 647 | // }); |
michael@0 | 648 | |
michael@0 | 649 | /* ********************************************* |
michael@0 | 650 | * COMPUTED-VIEW |
michael@0 | 651 | * ********************************************* |
michael@0 | 652 | * Computed-view related utility functions. |
michael@0 | 653 | * Allows to get properties, links, expand properties, ... |
michael@0 | 654 | */ |
michael@0 | 655 | |
michael@0 | 656 | /** |
michael@0 | 657 | * Get references to the name and value span nodes corresponding to a given |
michael@0 | 658 | * property name in the computed-view |
michael@0 | 659 | * @param {CssHtmlTree} view The instance of the computed view panel |
michael@0 | 660 | * @param {String} name The name of the property to retrieve |
michael@0 | 661 | * @return an object {nameSpan, valueSpan} |
michael@0 | 662 | */ |
michael@0 | 663 | function getComputedViewProperty(view, name) { |
michael@0 | 664 | let prop; |
michael@0 | 665 | for (let property of view.styleDocument.querySelectorAll(".property-view")) { |
michael@0 | 666 | let nameSpan = property.querySelector(".property-name"); |
michael@0 | 667 | let valueSpan = property.querySelector(".property-value"); |
michael@0 | 668 | |
michael@0 | 669 | if (nameSpan.textContent === name) { |
michael@0 | 670 | prop = {nameSpan: nameSpan, valueSpan: valueSpan}; |
michael@0 | 671 | break; |
michael@0 | 672 | } |
michael@0 | 673 | } |
michael@0 | 674 | return prop; |
michael@0 | 675 | } |
michael@0 | 676 | |
michael@0 | 677 | /** |
michael@0 | 678 | * Get the text value of the property corresponding to a given name in the |
michael@0 | 679 | * computed-view |
michael@0 | 680 | * @param {CssHtmlTree} view The instance of the computed view panel |
michael@0 | 681 | * @param {String} name The name of the property to retrieve |
michael@0 | 682 | * @return {String} The property value |
michael@0 | 683 | */ |
michael@0 | 684 | function getComputedViewPropertyValue(view, selectorText, propertyName) { |
michael@0 | 685 | return getComputedViewProperty(view, selectorText, propertyName) |
michael@0 | 686 | .valueSpan.textContent; |
michael@0 | 687 | } |
michael@0 | 688 | |
michael@0 | 689 | /** |
michael@0 | 690 | * Expand a given property, given its index in the current property list of |
michael@0 | 691 | * the computed view |
michael@0 | 692 | * @param {CssHtmlTree} view The instance of the computed view panel |
michael@0 | 693 | * @param {InspectorPanel} inspector The instance of the inspector panel |
michael@0 | 694 | * @param {Number} index The index of the property to be expanded |
michael@0 | 695 | * @return a promise that resolves when the property has been expanded, or |
michael@0 | 696 | * rejects if the property was not found |
michael@0 | 697 | */ |
michael@0 | 698 | function expandComputedViewPropertyByIndex(view, inspector, index) { |
michael@0 | 699 | info("Expanding property " + index + " in the computed view"); |
michael@0 | 700 | let expandos = view.styleDocument.querySelectorAll(".expandable"); |
michael@0 | 701 | if (!expandos.length || !expandos[index]) { |
michael@0 | 702 | return promise.reject(); |
michael@0 | 703 | } |
michael@0 | 704 | |
michael@0 | 705 | let onExpand = inspector.once("computed-view-property-expanded"); |
michael@0 | 706 | expandos[index].click(); |
michael@0 | 707 | return onExpand; |
michael@0 | 708 | } |
michael@0 | 709 | |
michael@0 | 710 | /** |
michael@0 | 711 | * Get a rule-link from the computed-view given its index |
michael@0 | 712 | * @param {CssHtmlTree} view The instance of the computed view panel |
michael@0 | 713 | * @param {Number} index The index of the link to be retrieved |
michael@0 | 714 | * @return {DOMNode} The link at the given index, if one exists, null otherwise |
michael@0 | 715 | */ |
michael@0 | 716 | function getComputedViewLinkByIndex(view, index) { |
michael@0 | 717 | let links = view.styleDocument.querySelectorAll(".rule-link .link"); |
michael@0 | 718 | return links[index]; |
michael@0 | 719 | } |
michael@0 | 720 | |
michael@0 | 721 | /* ********************************************* |
michael@0 | 722 | * STYLE-EDITOR |
michael@0 | 723 | * ********************************************* |
michael@0 | 724 | * Style-editor related utility functions. |
michael@0 | 725 | */ |
michael@0 | 726 | |
michael@0 | 727 | /** |
michael@0 | 728 | * Wait for the toolbox to emit the styleeditor-selected event and when done |
michael@0 | 729 | * wait for the stylesheet identified by href to be loaded in the stylesheet |
michael@0 | 730 | * editor |
michael@0 | 731 | * @param {Toolbox} toolbox |
michael@0 | 732 | * @param {String} href Optional, if not provided, wait for the first editor |
michael@0 | 733 | * to be ready |
michael@0 | 734 | * @return a promise that resolves to the editor when the stylesheet editor is |
michael@0 | 735 | * ready |
michael@0 | 736 | */ |
michael@0 | 737 | function waitForStyleEditor(toolbox, href) { |
michael@0 | 738 | let def = promise.defer(); |
michael@0 | 739 | |
michael@0 | 740 | info("Waiting for the toolbox to switch to the styleeditor"); |
michael@0 | 741 | toolbox.once("styleeditor-ready").then(() => { |
michael@0 | 742 | let panel = toolbox.getCurrentPanel(); |
michael@0 | 743 | ok(panel && panel.UI, "Styleeditor panel switched to front"); |
michael@0 | 744 | |
michael@0 | 745 | panel.UI.on("editor-selected", function onEditorSelected(event, editor) { |
michael@0 | 746 | let currentHref = editor.styleSheet.href; |
michael@0 | 747 | if (!href || (href && currentHref.endsWith(href))) { |
michael@0 | 748 | info("Stylesheet editor selected"); |
michael@0 | 749 | panel.UI.off("editor-selected", onEditorSelected); |
michael@0 | 750 | editor.getSourceEditor().then(editor => { |
michael@0 | 751 | info("Stylesheet editor fully loaded"); |
michael@0 | 752 | def.resolve(editor); |
michael@0 | 753 | }); |
michael@0 | 754 | } |
michael@0 | 755 | }); |
michael@0 | 756 | }); |
michael@0 | 757 | |
michael@0 | 758 | return def.promise; |
michael@0 | 759 | } |