|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const Cu = Components.utils; |
|
6 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
|
7 let TargetFactory = devtools.TargetFactory; |
|
8 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); |
|
9 let promise = devtools.require("devtools/toolkit/deprecated-sync-thenables"); |
|
10 let {getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor"); |
|
11 |
|
12 // All test are asynchronous |
|
13 waitForExplicitFinish(); |
|
14 |
|
15 //Services.prefs.setBoolPref("devtools.dump.emit", true); |
|
16 |
|
17 // Set the testing flag on gDevTools and reset it when the test ends |
|
18 gDevTools.testing = true; |
|
19 registerCleanupFunction(() => gDevTools.testing = false); |
|
20 |
|
21 // Clear preferences that may be set during the course of tests. |
|
22 registerCleanupFunction(() => { |
|
23 Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen"); |
|
24 Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); |
|
25 Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); |
|
26 Services.prefs.clearUserPref("devtools.dump.emit"); |
|
27 Services.prefs.clearUserPref("devtools.markup.pagesize"); |
|
28 }); |
|
29 |
|
30 // Auto close the toolbox and close the test tabs when the test ends |
|
31 registerCleanupFunction(() => { |
|
32 try { |
|
33 let target = TargetFactory.forTab(gBrowser.selectedTab); |
|
34 gDevTools.closeToolbox(target); |
|
35 } catch (ex) { |
|
36 dump(ex); |
|
37 } |
|
38 while (gBrowser.tabs.length > 1) { |
|
39 gBrowser.removeCurrentTab(); |
|
40 } |
|
41 }); |
|
42 |
|
43 const TEST_URL_ROOT = "http://mochi.test:8888/browser/browser/devtools/markupview/test/"; |
|
44 |
|
45 /** |
|
46 * Define an async test based on a generator function |
|
47 */ |
|
48 function asyncTest(generator) { |
|
49 return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish); |
|
50 } |
|
51 |
|
52 /** |
|
53 * Add a new test tab in the browser and load the given url. |
|
54 * @param {String} url The url to be loaded in the new tab |
|
55 * @return a promise that resolves to the tab object when the url is loaded |
|
56 */ |
|
57 function addTab(url) { |
|
58 info("Adding a new tab with URL: '" + url + "'"); |
|
59 let def = promise.defer(); |
|
60 |
|
61 let tab = gBrowser.selectedTab = gBrowser.addTab(); |
|
62 gBrowser.selectedBrowser.addEventListener("load", function onload() { |
|
63 gBrowser.selectedBrowser.removeEventListener("load", onload, true); |
|
64 info("URL '" + url + "' loading complete"); |
|
65 waitForFocus(() => { |
|
66 def.resolve(tab); |
|
67 }, content); |
|
68 }, true); |
|
69 content.location = url; |
|
70 |
|
71 return def.promise; |
|
72 } |
|
73 |
|
74 /** |
|
75 * Some tests may need to import one or more of the test helper scripts. |
|
76 * A test helper script is simply a js file that contains common test code that |
|
77 * is either not common-enough to be in head.js, or that is located in a separate |
|
78 * directory. |
|
79 * The script will be loaded synchronously and in the test's scope. |
|
80 * @param {String} filePath The file path, relative to the current directory. |
|
81 * Examples: |
|
82 * - "helper_attributes_test_runner.js" |
|
83 * - "../../../commandline/test/helpers.js" |
|
84 */ |
|
85 function loadHelperScript(filePath) { |
|
86 let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); |
|
87 Services.scriptloader.loadSubScript(testDir + "/" + filePath, this); |
|
88 } |
|
89 |
|
90 /** |
|
91 * Reload the current page |
|
92 * @return a promise that resolves when the inspector has emitted the event |
|
93 * new-root |
|
94 */ |
|
95 function reloadPage(inspector) { |
|
96 info("Reloading the page"); |
|
97 let newRoot = inspector.once("new-root"); |
|
98 content.location.reload(); |
|
99 return newRoot; |
|
100 } |
|
101 |
|
102 /** |
|
103 * Open the toolbox, with the inspector tool visible. |
|
104 * @return a promise that resolves when the inspector is ready |
|
105 */ |
|
106 function openInspector() { |
|
107 info("Opening the inspector panel"); |
|
108 let def = promise.defer(); |
|
109 |
|
110 let target = TargetFactory.forTab(gBrowser.selectedTab); |
|
111 gDevTools.showToolbox(target, "inspector").then(function(toolbox) { |
|
112 info("The toolbox is open"); |
|
113 let inspector = toolbox.getCurrentPanel(); |
|
114 inspector.once("inspector-updated", () => { |
|
115 info("The inspector panel is active and ready"); |
|
116 def.resolve({toolbox: toolbox, inspector: inspector}); |
|
117 }); |
|
118 }).then(null, console.error); |
|
119 |
|
120 return def.promise; |
|
121 } |
|
122 |
|
123 /** |
|
124 * Simple DOM node accesor function that takes either a node or a string css |
|
125 * selector as argument and returns the corresponding node |
|
126 * @param {String|DOMNode} nodeOrSelector |
|
127 * @return {DOMNode} |
|
128 */ |
|
129 function getNode(nodeOrSelector) { |
|
130 info("Getting the node for '" + nodeOrSelector + "'"); |
|
131 return typeof nodeOrSelector === "string" ? |
|
132 content.document.querySelector(nodeOrSelector) : |
|
133 nodeOrSelector; |
|
134 } |
|
135 |
|
136 /** |
|
137 * Set the inspector's current selection to a node or to the first match of the |
|
138 * given css selector |
|
139 * @param {String|DOMNode} nodeOrSelector |
|
140 * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox |
|
141 * @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection |
|
142 * @return a promise that resolves when the inspector is updated with the new |
|
143 * node |
|
144 */ |
|
145 function selectNode(nodeOrSelector, inspector, reason="test") { |
|
146 info("Selecting the node for '" + nodeOrSelector + "'"); |
|
147 let node = getNode(nodeOrSelector); |
|
148 let updated = inspector.once("inspector-updated"); |
|
149 inspector.selection.setNode(node, reason); |
|
150 return updated; |
|
151 } |
|
152 |
|
153 /** |
|
154 * Get the MarkupContainer object instance that corresponds to the given |
|
155 * HTML node |
|
156 * @param {DOMNode|String} nodeOrSelector The DOM node for which the |
|
157 * container is required |
|
158 * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
|
159 * loaded in the toolbox |
|
160 * @return {MarkupContainer} |
|
161 */ |
|
162 function getContainerForRawNode(nodeOrSelector, {markup}) { |
|
163 let front = markup.walker.frontForRawNode(getNode(nodeOrSelector)); |
|
164 let container = markup.getContainer(front); |
|
165 info("Markup-container object for " + nodeOrSelector + " " + container); |
|
166 return container; |
|
167 } |
|
168 |
|
169 /** |
|
170 * Using the markupview's _waitForChildren function, wait for all queued |
|
171 * children updates to be handled. |
|
172 * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
|
173 * loaded in the toolbox |
|
174 * @return a promise that resolves when all queued children updates have been |
|
175 * handled |
|
176 */ |
|
177 function waitForChildrenUpdated({markup}) { |
|
178 info("Waiting for queued children updates to be handled"); |
|
179 let def = promise.defer(); |
|
180 markup._waitForChildren().then(() => { |
|
181 executeSoon(def.resolve); |
|
182 }); |
|
183 return def.promise; |
|
184 } |
|
185 |
|
186 /** |
|
187 * Simulate a mouse-over on the markup-container (a line in the markup-view) |
|
188 * that corresponds to the node or selector passed. |
|
189 * @param {String|DOMNode} nodeOrSelector |
|
190 * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox |
|
191 * @return a promise that resolves when the container is hovered and the higlighter |
|
192 * is shown on the corresponding node |
|
193 */ |
|
194 function hoverContainer(nodeOrSelector, inspector) { |
|
195 info("Hovering over the markup-container for node " + nodeOrSelector); |
|
196 let highlit = inspector.toolbox.once("node-highlight"); |
|
197 let container = getContainerForRawNode(getNode(nodeOrSelector), inspector); |
|
198 EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"}, |
|
199 inspector.markup.doc.defaultView); |
|
200 return highlit; |
|
201 } |
|
202 |
|
203 /** |
|
204 * Simulate a click on the markup-container (a line in the markup-view) |
|
205 * that corresponds to the node or selector passed. |
|
206 * @param {String|DOMNode} nodeOrSelector |
|
207 * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox |
|
208 * @return a promise that resolves when the node has been selected. |
|
209 */ |
|
210 function clickContainer(nodeOrSelector, inspector) { |
|
211 info("Clicking on the markup-container for node " + nodeOrSelector); |
|
212 let updated = inspector.once("inspector-updated"); |
|
213 let container = getContainerForRawNode(getNode(nodeOrSelector), inspector); |
|
214 EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"}, |
|
215 inspector.markup.doc.defaultView); |
|
216 EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"}, |
|
217 inspector.markup.doc.defaultView); |
|
218 return updated; |
|
219 } |
|
220 |
|
221 /** |
|
222 * Checks if the highlighter is visible currently |
|
223 * @return {Boolean} |
|
224 */ |
|
225 function isHighlighterVisible() { |
|
226 let highlighter = gBrowser.selectedBrowser.parentNode |
|
227 .querySelector(".highlighter-container .box-model-root"); |
|
228 return highlighter && !highlighter.hasAttribute("hidden"); |
|
229 } |
|
230 |
|
231 /** |
|
232 * Simulate the mouse leaving the markup-view area |
|
233 * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox |
|
234 * @return a promise when done |
|
235 */ |
|
236 function mouseLeaveMarkupView(inspector) { |
|
237 info("Leaving the markup-view area"); |
|
238 let def = promise.defer(); |
|
239 |
|
240 // Find another element to mouseover over in order to leave the markup-view |
|
241 let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button"); |
|
242 |
|
243 EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, |
|
244 inspector.toolbox.doc.defaultView); |
|
245 executeSoon(def.resolve); |
|
246 |
|
247 return def.promise; |
|
248 } |
|
249 |
|
250 /** |
|
251 * Focus a given editable element, enter edit mode, set value, and commit |
|
252 * @param {DOMNode} field The element that gets editable after receiving focus |
|
253 * and <ENTER> keypress |
|
254 * @param {String} value The string value to be set into the edited field |
|
255 * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
|
256 * loaded in the toolbox |
|
257 */ |
|
258 function setEditableFieldValue(field, value, inspector) { |
|
259 field.focus(); |
|
260 EventUtils.sendKey("return", inspector.panelWin); |
|
261 let input = inplaceEditor(field).input; |
|
262 ok(input, "Found editable field for setting value: " + value); |
|
263 input.value = value; |
|
264 EventUtils.sendKey("return", inspector.panelWin); |
|
265 } |
|
266 |
|
267 /** |
|
268 * Focus the new-attribute inplace-editor field of the nodeOrSelector's markup |
|
269 * container, and enters the given text, then wait for it to be applied and the |
|
270 * for the node to mutates (when new attribute(s) is(are) created) |
|
271 * @param {DOMNode|String} nodeOrSelector The node or node selector to edit. |
|
272 * @param {String} text The new attribute text to be entered (e.g. "id='test'") |
|
273 * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
|
274 * loaded in the toolbox |
|
275 * @return a promise that resolves when the node has mutated |
|
276 */ |
|
277 function addNewAttributes(nodeOrSelector, text, inspector) { |
|
278 info("Entering text '" + text + "' in node '" + nodeOrSelector + "''s new attribute field"); |
|
279 |
|
280 let container = getContainerForRawNode(nodeOrSelector, inspector); |
|
281 ok(container, "The container for '" + nodeOrSelector + "' was found"); |
|
282 |
|
283 info("Listening for the markupmutation event"); |
|
284 let nodeMutated = inspector.once("markupmutation"); |
|
285 setEditableFieldValue(container.editor.newAttr, text, inspector); |
|
286 return nodeMutated; |
|
287 } |
|
288 |
|
289 /** |
|
290 * Checks that a node has the given attributes |
|
291 * |
|
292 * @param {DOMNode|String} nodeOrSelector The node or node selector to check. |
|
293 * @param {Object} attrs An object containing the attributes to check. |
|
294 * e.g. {id: "id1", class: "someclass"} |
|
295 * |
|
296 * Note that node.getAttribute() returns attribute values provided by the HTML |
|
297 * parser. The parser only provides unescaped entities so & will return &. |
|
298 */ |
|
299 function assertAttributes(nodeOrSelector, attrs) { |
|
300 let node = getNode(nodeOrSelector); |
|
301 |
|
302 is(node.attributes.length, Object.keys(attrs).length, |
|
303 "Node has the correct number of attributes."); |
|
304 for (let attr in attrs) { |
|
305 is(node.getAttribute(attr), attrs[attr], |
|
306 "Node has the correct " + attr + " attribute."); |
|
307 } |
|
308 } |
|
309 |
|
310 /** |
|
311 * Undo the last markup-view action and wait for the corresponding mutation to |
|
312 * occur |
|
313 * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
|
314 * loaded in the toolbox |
|
315 * @return a promise that resolves when the markup-mutation has been treated or |
|
316 * rejects if no undo action is possible |
|
317 */ |
|
318 function undoChange(inspector) { |
|
319 let canUndo = inspector.markup.undo.canUndo(); |
|
320 ok(canUndo, "The last change in the markup-view can be undone"); |
|
321 if (!canUndo) { |
|
322 return promise.reject(); |
|
323 } |
|
324 |
|
325 let mutated = inspector.once("markupmutation"); |
|
326 inspector.markup.undo.undo(); |
|
327 return mutated; |
|
328 } |
|
329 |
|
330 /** |
|
331 * Redo the last markup-view action and wait for the corresponding mutation to |
|
332 * occur |
|
333 * @param {InspectorPanel} inspector The instance of InspectorPanel currently |
|
334 * loaded in the toolbox |
|
335 * @return a promise that resolves when the markup-mutation has been treated or |
|
336 * rejects if no redo action is possible |
|
337 */ |
|
338 function redoChange(inspector) { |
|
339 let canRedo = inspector.markup.undo.canRedo(); |
|
340 ok(canRedo, "The last change in the markup-view can be redone"); |
|
341 if (!canRedo) { |
|
342 return promise.reject(); |
|
343 } |
|
344 |
|
345 let mutated = inspector.once("markupmutation"); |
|
346 inspector.markup.undo.redo(); |
|
347 return mutated; |
|
348 } |
|
349 |
|
350 /** |
|
351 * Get the selector-search input box from the inspector panel |
|
352 * @return {DOMNode} |
|
353 */ |
|
354 function getSelectorSearchBox(inspector) { |
|
355 return inspector.panelWin.document.getElementById("inspector-searchbox"); |
|
356 } |
|
357 |
|
358 /** |
|
359 * Using the inspector panel's selector search box, search for a given selector. |
|
360 * The selector input string will be entered in the input field and the <ENTER> |
|
361 * keypress will be simulated. |
|
362 * This function won't wait for any events and is not async. It's up to callers |
|
363 * to subscribe to events and react accordingly. |
|
364 */ |
|
365 function searchUsingSelectorSearch(selector, inspector) { |
|
366 info("Entering \"" + selector + "\" into the selector-search input field"); |
|
367 let field = getSelectorSearchBox(inspector); |
|
368 field.focus(); |
|
369 field.value = selector; |
|
370 EventUtils.sendKey("return", inspector.panelWin); |
|
371 } |
|
372 |
|
373 /** |
|
374 * This shouldn't be used in the tests, but is useful when writing new tests or |
|
375 * debugging existing tests in order to introduce delays in the test steps |
|
376 * @param {Number} ms The time to wait |
|
377 * @return A promise that resolves when the time is passed |
|
378 */ |
|
379 function wait(ms) { |
|
380 let def = promise.defer(); |
|
381 content.setTimeout(def.resolve, ms); |
|
382 return def.promise; |
|
383 } |