browser/base/content/test/newtab/head.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:33cc9fa60e89
1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
3
4 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
5 const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directorySource";
6
7 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
8 // start with no directory links by default
9 Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json,{}");
10
11 let tmp = {};
12 Cu.import("resource://gre/modules/Promise.jsm", tmp);
13 Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
14 Cc["@mozilla.org/moz/jssubscript-loader;1"]
15 .getService(Ci.mozIJSSubScriptLoader)
16 .loadSubScript("chrome://browser/content/sanitize.js", tmp);
17 let {Promise, NewTabUtils, Sanitizer} = tmp;
18
19 let uri = Services.io.newURI("about:newtab", null, null);
20 let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
21
22 let isMac = ("nsILocalFileMac" in Ci);
23 let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc);
24 let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
25 let gWindow = window;
26
27 // The tests assume all three rows of sites are shown, but the window may be too
28 // short to actually show three rows. Resize it if necessary.
29 let requiredInnerHeight =
30 40 + 32 + // undo container + bottom margin
31 44 + 32 + // search bar + bottom margin
32 (3 * (150 + 32)) + // 3 rows * (tile height + title and bottom margin)
33 100; // breathing room
34
35 let oldInnerHeight = null;
36 if (gBrowser.contentWindow.innerHeight < requiredInnerHeight) {
37 oldInnerHeight = gBrowser.contentWindow.innerHeight;
38 info("Changing browser inner height from " + oldInnerHeight + " to " +
39 requiredInnerHeight);
40 gBrowser.contentWindow.innerHeight = requiredInnerHeight;
41 let screenHeight = {};
42 Cc["@mozilla.org/gfx/screenmanager;1"].
43 getService(Ci.nsIScreenManager).
44 primaryScreen.
45 GetAvailRectDisplayPix({}, {}, {}, screenHeight);
46 screenHeight = screenHeight.value;
47 if (screenHeight < gBrowser.contentWindow.outerHeight) {
48 info("Warning: Browser outer height is now " +
49 gBrowser.contentWindow.outerHeight + ", which is larger than the " +
50 "available screen height, " + screenHeight +
51 ". That may cause problems.");
52 }
53 }
54
55 registerCleanupFunction(function () {
56 while (gWindow.gBrowser.tabs.length > 1)
57 gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
58
59 if (oldInnerHeight)
60 gBrowser.contentWindow.innerHeight = oldInnerHeight;
61
62 Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
63 Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
64 });
65
66 /**
67 * Provide the default test function to start our test runner.
68 */
69 function test() {
70 TestRunner.run();
71 }
72
73 /**
74 * The test runner that controls the execution flow of our tests.
75 */
76 let TestRunner = {
77 /**
78 * Starts the test runner.
79 */
80 run: function () {
81 waitForExplicitFinish();
82
83 this._iter = runTests();
84 this.next();
85 },
86
87 /**
88 * Runs the next available test or finishes if there's no test left.
89 */
90 next: function () {
91 try {
92 TestRunner._iter.next();
93 } catch (e if e instanceof StopIteration) {
94 TestRunner.finish();
95 }
96 },
97
98 /**
99 * Finishes all tests and cleans up.
100 */
101 finish: function () {
102 function cleanupAndFinish() {
103 clearHistory(function () {
104 whenPagesUpdated(finish);
105 NewTabUtils.restore();
106 });
107 }
108
109 let callbacks = NewTabUtils.links._populateCallbacks;
110 let numCallbacks = callbacks.length;
111
112 if (numCallbacks)
113 callbacks.splice(0, numCallbacks, cleanupAndFinish);
114 else
115 cleanupAndFinish();
116 }
117 };
118
119 /**
120 * Returns the selected tab's content window.
121 * @return The content window.
122 */
123 function getContentWindow() {
124 return gWindow.gBrowser.selectedBrowser.contentWindow;
125 }
126
127 /**
128 * Returns the selected tab's content document.
129 * @return The content document.
130 */
131 function getContentDocument() {
132 return gWindow.gBrowser.selectedBrowser.contentDocument;
133 }
134
135 /**
136 * Returns the newtab grid of the selected tab.
137 * @return The newtab grid.
138 */
139 function getGrid() {
140 return getContentWindow().gGrid;
141 }
142
143 /**
144 * Returns the cell at the given index of the selected tab's newtab grid.
145 * @param aIndex The cell index.
146 * @return The newtab cell.
147 */
148 function getCell(aIndex) {
149 return getGrid().cells[aIndex];
150 }
151
152 /**
153 * Allows to provide a list of links that is used to construct the grid.
154 * @param aLinksPattern the pattern (see below)
155 *
156 * Example: setLinks("1,2,3")
157 * Result: [{url: "http://example.com/#1", title: "site#1"},
158 * {url: "http://example.com/#2", title: "site#2"}
159 * {url: "http://example.com/#3", title: "site#3"}]
160 */
161 function setLinks(aLinks) {
162 let links = aLinks;
163
164 if (typeof links == "string") {
165 links = aLinks.split(/\s*,\s*/).map(function (id) {
166 return {url: "http://example.com/#" + id, title: "site#" + id};
167 });
168 }
169
170 // Call populateCache() once to make sure that all link fetching that is
171 // currently in progress has ended. We clear the history, fill it with the
172 // given entries and call populateCache() now again to make sure the cache
173 // has the desired contents.
174 NewTabUtils.links.populateCache(function () {
175 clearHistory(function () {
176 fillHistory(links, function () {
177 NewTabUtils.links.populateCache(function () {
178 NewTabUtils.allPages.update();
179 TestRunner.next();
180 }, true);
181 });
182 });
183 });
184 }
185
186 function clearHistory(aCallback) {
187 Services.obs.addObserver(function observe(aSubject, aTopic, aData) {
188 Services.obs.removeObserver(observe, aTopic);
189 executeSoon(aCallback);
190 }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
191
192 PlacesUtils.history.removeAllPages();
193 }
194
195 function fillHistory(aLinks, aCallback) {
196 let numLinks = aLinks.length;
197 if (!numLinks) {
198 if (aCallback)
199 executeSoon(aCallback);
200 return;
201 }
202
203 let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK;
204
205 // Important: To avoid test failures due to clock jitter on Windows XP, call
206 // Date.now() once here, not each time through the loop.
207 let now = Date.now() * 1000;
208
209 for (let i = 0; i < aLinks.length; i++) {
210 let link = aLinks[i];
211 let place = {
212 uri: makeURI(link.url),
213 title: link.title,
214 // Links are secondarily sorted by visit date descending, so decrease the
215 // visit date as we progress through the array so that links appear in the
216 // grid in the order they're present in the array.
217 visits: [{visitDate: now - i, transitionType: transitionLink}]
218 };
219
220 PlacesUtils.asyncHistory.updatePlaces(place, {
221 handleError: function () ok(false, "couldn't add visit to history"),
222 handleResult: function () {},
223 handleCompletion: function () {
224 if (--numLinks == 0 && aCallback)
225 aCallback();
226 }
227 });
228 }
229 }
230
231 /**
232 * Allows to specify the list of pinned links (that have a fixed position in
233 * the grid.
234 * @param aLinksPattern the pattern (see below)
235 *
236 * Example: setPinnedLinks("3,,1")
237 * Result: 'http://example.com/#3' is pinned in the first cell. 'http://example.com/#1' is
238 * pinned in the third cell.
239 */
240 function setPinnedLinks(aLinks) {
241 let links = aLinks;
242
243 if (typeof links == "string") {
244 links = aLinks.split(/\s*,\s*/).map(function (id) {
245 if (id)
246 return {url: "http://example.com/#" + id, title: "site#" + id};
247 });
248 }
249
250 let string = Cc["@mozilla.org/supports-string;1"]
251 .createInstance(Ci.nsISupportsString);
252 string.data = JSON.stringify(links);
253 Services.prefs.setComplexValue("browser.newtabpage.pinned",
254 Ci.nsISupportsString, string);
255
256 NewTabUtils.pinnedLinks.resetCache();
257 NewTabUtils.allPages.update();
258 }
259
260 /**
261 * Restore the grid state.
262 */
263 function restore() {
264 whenPagesUpdated();
265 NewTabUtils.restore();
266 }
267
268 /**
269 * Creates a new tab containing 'about:newtab'.
270 */
271 function addNewTabPageTab() {
272 let tab = gWindow.gBrowser.selectedTab = gWindow.gBrowser.addTab("about:newtab");
273 let browser = tab.linkedBrowser;
274
275 function whenNewTabLoaded() {
276 if (NewTabUtils.allPages.enabled) {
277 // Continue when the link cache has been populated.
278 NewTabUtils.links.populateCache(function () {
279 executeSoon(TestRunner.next);
280 });
281 } else {
282 // It's important that we call next() asynchronously.
283 // 'yield addNewTabPageTab()' would fail if next() is called
284 // synchronously because the iterator is already executing.
285 executeSoon(TestRunner.next);
286 }
287 }
288
289 // The new tab page might have been preloaded in the background.
290 if (browser.contentDocument.readyState == "complete") {
291 whenNewTabLoaded();
292 return;
293 }
294
295 // Wait for the new tab page to be loaded.
296 browser.addEventListener("load", function onLoad() {
297 browser.removeEventListener("load", onLoad, true);
298 whenNewTabLoaded();
299 }, true);
300 }
301
302 /**
303 * Compares the current grid arrangement with the given pattern.
304 * @param the pattern (see below)
305 * @param the array of sites to compare with (optional)
306 *
307 * Example: checkGrid("3p,2,,1p")
308 * Result: We expect the first cell to contain the pinned site 'http://example.com/#3'.
309 * The second cell contains 'http://example.com/#2'. The third cell is empty.
310 * The fourth cell contains the pinned site 'http://example.com/#4'.
311 */
312 function checkGrid(aSitesPattern, aSites) {
313 let length = aSitesPattern.split(",").length;
314 let sites = (aSites || getGrid().sites).slice(0, length);
315 let current = sites.map(function (aSite) {
316 if (!aSite)
317 return "";
318
319 let pinned = aSite.isPinned();
320 let pinButton = aSite.node.querySelector(".newtab-control-pin");
321 let hasPinnedAttr = pinButton.hasAttribute("pinned");
322
323 if (pinned != hasPinnedAttr)
324 ok(false, "invalid state (site.isPinned() != site[pinned])");
325
326 return aSite.url.replace(/^http:\/\/example\.com\/#(\d+)$/, "$1") + (pinned ? "p" : "");
327 });
328
329 is(current, aSitesPattern, "grid status = " + aSitesPattern);
330 }
331
332 /**
333 * Blocks a site from the grid.
334 * @param aIndex The cell index.
335 */
336 function blockCell(aIndex) {
337 whenPagesUpdated();
338 getCell(aIndex).site.block();
339 }
340
341 /**
342 * Pins a site on a given position.
343 * @param aIndex The cell index.
344 * @param aPinIndex The index the defines where the site should be pinned.
345 */
346 function pinCell(aIndex, aPinIndex) {
347 getCell(aIndex).site.pin(aPinIndex);
348 }
349
350 /**
351 * Unpins the given cell's site.
352 * @param aIndex The cell index.
353 */
354 function unpinCell(aIndex) {
355 whenPagesUpdated();
356 getCell(aIndex).site.unpin();
357 }
358
359 /**
360 * Simulates a drag and drop operation.
361 * @param aSourceIndex The cell index containing the dragged site.
362 * @param aDestIndex The cell index of the drop target.
363 */
364 function simulateDrop(aSourceIndex, aDestIndex) {
365 let src = getCell(aSourceIndex).site.node;
366 let dest = getCell(aDestIndex).node;
367
368 // Drop 'src' onto 'dest' and continue testing when all newtab
369 // pages have been updated (i.e. the drop operation is completed).
370 startAndCompleteDragOperation(src, dest, whenPagesUpdated);
371 }
372
373 /**
374 * Simulates a drag and drop operation. Instead of rearranging a site that is
375 * is already contained in the newtab grid, this is used to simulate dragging
376 * an external link onto the grid e.g. the text from the URL bar.
377 * @param aDestIndex The cell index of the drop target.
378 */
379 function simulateExternalDrop(aDestIndex) {
380 let dest = getCell(aDestIndex).node;
381
382 // Create an iframe that contains the external link we'll drag.
383 createExternalDropIframe().then(iframe => {
384 let link = iframe.contentDocument.getElementById("link");
385
386 // Drop 'link' onto 'dest'.
387 startAndCompleteDragOperation(link, dest, () => {
388 // Wait until the drop operation is complete
389 // and all newtab pages have been updated.
390 whenPagesUpdated(() => {
391 // Clean up and remove the iframe.
392 iframe.remove();
393 // Continue testing.
394 TestRunner.next();
395 });
396 });
397 });
398 }
399
400 /**
401 * Starts and complete a drag-and-drop operation.
402 * @param aSource The node that is being dragged.
403 * @param aDest The node we're dragging aSource onto.
404 * @param aCallback The function that is called when we're done.
405 */
406 function startAndCompleteDragOperation(aSource, aDest, aCallback) {
407 // Start by pressing the left mouse button.
408 synthesizeNativeMouseLDown(aSource);
409
410 // Move the mouse in 5px steps until the drag operation starts.
411 let offset = 0;
412 let interval = setInterval(() => {
413 synthesizeNativeMouseDrag(aSource, offset += 5);
414 }, 10);
415
416 // When the drag operation has started we'll move
417 // the dragged element to its target position.
418 aSource.addEventListener("dragstart", function onDragStart() {
419 aSource.removeEventListener("dragstart", onDragStart);
420 clearInterval(interval);
421
422 // Place the cursor above the drag target.
423 synthesizeNativeMouseMove(aDest);
424 });
425
426 // As soon as the dragged element hovers the target, we'll drop it.
427 aDest.addEventListener("dragenter", function onDragEnter() {
428 aDest.removeEventListener("dragenter", onDragEnter);
429
430 // Finish the drop operation.
431 synthesizeNativeMouseLUp(aDest);
432 aCallback();
433 });
434 }
435
436 /**
437 * Helper function that creates a temporary iframe in the about:newtab
438 * document. This will contain a link we can drag to the test the dropping
439 * of links from external documents.
440 */
441 function createExternalDropIframe() {
442 const url = "data:text/html;charset=utf-8," +
443 "<a id='link' href='http://example.com/%2399'>link</a>";
444
445 let deferred = Promise.defer();
446 let doc = getContentDocument();
447 let iframe = doc.createElement("iframe");
448 iframe.setAttribute("src", url);
449 iframe.style.width = "50px";
450 iframe.style.height = "50px";
451
452 let margin = doc.getElementById("newtab-margin-top");
453 margin.appendChild(iframe);
454
455 iframe.addEventListener("load", function onLoad() {
456 iframe.removeEventListener("load", onLoad);
457 executeSoon(() => deferred.resolve(iframe));
458 });
459
460 return deferred.promise;
461 }
462
463 /**
464 * Fires a synthetic 'mousedown' event on the current about:newtab page.
465 * @param aElement The element used to determine the cursor position.
466 */
467 function synthesizeNativeMouseLDown(aElement) {
468 if (isLinux) {
469 let win = aElement.ownerDocument.defaultView;
470 EventUtils.synthesizeMouseAtCenter(aElement, {type: "mousedown"}, win);
471 } else {
472 let msg = isWindows ? 2 : 1;
473 synthesizeNativeMouseEvent(aElement, msg);
474 }
475 }
476
477 /**
478 * Fires a synthetic 'mouseup' event on the current about:newtab page.
479 * @param aElement The element used to determine the cursor position.
480 */
481 function synthesizeNativeMouseLUp(aElement) {
482 let msg = isWindows ? 4 : (isMac ? 2 : 7);
483 synthesizeNativeMouseEvent(aElement, msg);
484 }
485
486 /**
487 * Fires a synthetic mouse drag event on the current about:newtab page.
488 * @param aElement The element used to determine the cursor position.
489 * @param aOffsetX The left offset that is added to the position.
490 */
491 function synthesizeNativeMouseDrag(aElement, aOffsetX) {
492 let msg = isMac ? 6 : 1;
493 synthesizeNativeMouseEvent(aElement, msg, aOffsetX);
494 }
495
496 /**
497 * Fires a synthetic 'mousemove' event on the current about:newtab page.
498 * @param aElement The element used to determine the cursor position.
499 */
500 function synthesizeNativeMouseMove(aElement) {
501 let msg = isMac ? 5 : 1;
502 synthesizeNativeMouseEvent(aElement, msg);
503 }
504
505 /**
506 * Fires a synthetic mouse event on the current about:newtab page.
507 * @param aElement The element used to determine the cursor position.
508 * @param aOffsetX The left offset that is added to the position (optional).
509 * @param aOffsetY The top offset that is added to the position (optional).
510 */
511 function synthesizeNativeMouseEvent(aElement, aMsg, aOffsetX = 0, aOffsetY = 0) {
512 let rect = aElement.getBoundingClientRect();
513 let win = aElement.ownerDocument.defaultView;
514 let x = aOffsetX + win.mozInnerScreenX + rect.left + rect.width / 2;
515 let y = aOffsetY + win.mozInnerScreenY + rect.top + rect.height / 2;
516
517 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
518 .getInterface(Ci.nsIDOMWindowUtils);
519
520 let scale = utils.screenPixelsPerCSSPixel;
521 utils.sendNativeMouseEvent(x * scale, y * scale, aMsg, 0, null);
522 }
523
524 /**
525 * Sends a custom drag event to a given DOM element.
526 * @param aEventType The drag event's type.
527 * @param aTarget The DOM element that the event is dispatched to.
528 * @param aData The event's drag data (optional).
529 */
530 function sendDragEvent(aEventType, aTarget, aData) {
531 let event = createDragEvent(aEventType, aData);
532 let ifaceReq = getContentWindow().QueryInterface(Ci.nsIInterfaceRequestor);
533 let windowUtils = ifaceReq.getInterface(Ci.nsIDOMWindowUtils);
534 windowUtils.dispatchDOMEventViaPresShell(aTarget, event, true);
535 }
536
537 /**
538 * Creates a custom drag event.
539 * @param aEventType The drag event's type.
540 * @param aData The event's drag data (optional).
541 * @return The drag event.
542 */
543 function createDragEvent(aEventType, aData) {
544 let dataTransfer = new (getContentWindow()).DataTransfer("dragstart", false);
545 dataTransfer.mozSetDataAt("text/x-moz-url", aData, 0);
546 let event = getContentDocument().createEvent("DragEvents");
547 event.initDragEvent(aEventType, true, true, getContentWindow(), 0, 0, 0, 0, 0,
548 false, false, false, false, 0, null, dataTransfer);
549
550 return event;
551 }
552
553 /**
554 * Resumes testing when all pages have been updated.
555 * @param aCallback Called when done. If not specified, TestRunner.next is used.
556 * @param aOnlyIfHidden If true, this resumes testing only when an update that
557 * applies to pre-loaded, hidden pages is observed. If
558 * false, this resumes testing when any update is observed.
559 */
560 function whenPagesUpdated(aCallback, aOnlyIfHidden=false) {
561 let page = {
562 update: function (onlyIfHidden=false) {
563 if (onlyIfHidden == aOnlyIfHidden) {
564 NewTabUtils.allPages.unregister(this);
565 executeSoon(aCallback || TestRunner.next);
566 }
567 }
568 };
569
570 NewTabUtils.allPages.register(page);
571 registerCleanupFunction(function () {
572 NewTabUtils.allPages.unregister(page);
573 });
574 }
575
576 /**
577 * Waits a small amount of time for search events to stop occurring in the
578 * newtab page.
579 *
580 * newtab pages receive some search events around load time that are difficult
581 * to predict. There are two categories of such events: (1) "State" events
582 * triggered by engine notifications like engine-changed, due to the search
583 * service initializing itself on app startup. This can happen when a test is
584 * the first test to run. (2) "State" events triggered by the newtab page
585 * itself when gSearch first sets itself up. newtab preloading makes these a
586 * pain to predict.
587 */
588 function whenSearchInitDone() {
589 info("Waiting for initial search events...");
590 let numTicks = 0;
591 function reset(event) {
592 info("Got initial search event " + event.detail.type +
593 ", waiting for more...");
594 numTicks = 0;
595 }
596 let eventName = "ContentSearchService";
597 getContentWindow().addEventListener(eventName, reset);
598 let interval = window.setInterval(() => {
599 if (++numTicks >= 100) {
600 info("Done waiting for initial search events");
601 window.clearInterval(interval);
602 getContentWindow().removeEventListener(eventName, reset);
603 TestRunner.next();
604 }
605 }, 0);
606 }

mercurial