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