1<?xml version="1.0"?>
2<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
3<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
4
5<window title="Popup Anchor Tests"
6  xmlns:html="http://www.w3.org/1999/xhtml"
7  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
8
9  <panel id="testPanel"
10         type="arrow"
11         animate="false"
12         noautohide="true">
13  </panel>
14
15  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
16
17<script>
18<![CDATA[
19var anchor, panel, arrow;
20
21function is_close(got, exp, msg) {
22  // on some platforms we see differences of a fraction of a pixel - so
23  // allow any difference of < 1 pixels as being OK.
24  ok(Math.abs(got - exp) < 1, msg + ": " + got + " should be equal(-ish) to " + exp);
25}
26
27function isArrowPositionedOn(side, offset) {
28  var arrowRect = arrow.getBoundingClientRect();
29  var arrowMidX = (arrowRect.left + arrowRect.right) / 2;
30  var arrowMidY = (arrowRect.top + arrowRect.bottom) / 2;
31  var panelRect = panel.getBoundingClientRect();
32  var panelMidX = (panelRect.left + panelRect.right) / 2;
33  var panelMidY = (panelRect.top + panelRect.bottom) / 2;
34  // First check the "flip" of the panel is correct.  If we are expecting the
35  // arrow to be pointing to the left side of the anchor, the arrow must
36  // also be on the left side of the panel (and vice-versa)
37  // XXX - on OSX, the arrow seems to always be exactly in the center, hence
38  // the 'equals' sign in the "<=" and ">=" comparisons.  NFI why though...
39  switch (side) {
40    case "left":
41      ok(arrowMidX <= panelMidX, "arrow should be on the left of the panel");
42      break;
43    case "right":
44      ok(arrowMidX >= panelMidX, "arrow should be on the right of the panel");
45      break;
46    case "top":
47      ok(arrowMidY <= panelMidY, "arrow should be on the top of the panel");
48      break;
49    case "bottom":
50      ok(arrowMidY >= panelMidY, "arrow should be on the bottom of the panel");
51      break;
52    default:
53      ok(false, "invalid position " + where);
54      break;
55  }
56  // Now check the arrow really is pointing where we expect.  The middle of
57  // the arrow should be pointing exactly to the left (or right) side of the
58  // anchor rect, +- any offsets.
59  if (offset === null) // special case - explicit 'null' means 'don't check offset'
60    return;
61  offset = offset || 0; // no param means no offset expected.
62  var anchorRect = anchor.getBoundingClientRect();
63  var anchorPos = anchorRect[side];
64  switch (side) {
65    case "left":
66    case "right":
67      is_close(arrowMidX - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor");
68      is_close(panelRect.top, anchorRect.bottom, "top of panel should be at bottom of anchor");
69      break;
70    case "top":
71    case "bottom":
72      is_close(arrowMidY - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor");
73      is_close(panelRect.right, anchorRect.left, "right of panel should be left of anchor");
74      break;
75    default:
76      ok(false, "unknown side " + side);
77      break;
78  }
79}
80
81function openSlidingPopup(position, callback) {
82  panel.setAttribute("flip", "slide");
83  _openPopup(position, callback);
84}
85
86function openPopup(position, callback) {
87  panel.setAttribute("flip", "both");
88  _openPopup(position, callback);
89}
90
91function waitForPopupPositioned(actionFn, callback)
92{
93  panel.addEventListener("popuppositioned", function listener() {
94    panel.removeEventListener("popuppositioned", listener, false);
95    callback();
96  }, false);
97  actionFn();
98}
99
100function _openPopup(position, callback) {
101  // this is very ugly: the panel CSS sets the arrow's list-style-image based
102  // on the 'side' attribute.  If the setting of the 'side' attribute causes
103  // the image to change, we may get the popupshown event before the new
104  // image has loaded - which causes the size of the arrow to be incorrect
105  // for a brief moment - right when we are measuring it!
106  // So we work around this in 2 steps:
107  // * Force the 'side' attribute to a value which causes the CSS to not
108  //   specify an image - then when the popup gets shown, the correct image
109  //   is set, causing a load() event on the image element.
110  // * Listen to *both* popupshown and the image load event.  When both have
111  //   fired (the order is indeterminate) we start the test.
112  panel.setAttribute("side", "noside");
113  var numEvents = 0;
114  function onEvent() {
115    if (++numEvents == 2) // after both panel 'popupshown' and image 'load'
116      callback();
117  };
118  panel.addEventListener("popupshown", function popupshown() {
119    panel.removeEventListener("popupshown", popupshown);
120    onEvent();
121  });
122  arrow.addEventListener("load", function imageload() {
123    arrow.removeEventListener("load", imageload);
124    onEvent();
125  });
126  panel.openPopup(anchor, position);
127}
128
129var tests = [
130  // A panel with the anchor after_end - the anchor should not move on resize
131  ['simpleResizeHorizontal', 'middle', function(next) {
132    openPopup("after_end", function() {
133      isArrowPositionedOn("right");
134      var origPanelRect = panel.getBoundingClientRect();
135      panel.sizeTo(100, 100);
136      isArrowPositionedOn("right"); // should not have flipped, so still "right"
137      panel.sizeTo(origPanelRect.width, origPanelRect.height);
138      isArrowPositionedOn("right"); // should not have flipped, so still "right"
139      next();
140    });
141  }],
142
143  ['simpleResizeVertical', 'middle', function(next) {
144    openPopup("start_after", function() {
145      isArrowPositionedOn("bottom");
146      var origPanelRect = panel.getBoundingClientRect();
147      panel.sizeTo(100, 100);
148      isArrowPositionedOn("bottom"); // should not have flipped
149      panel.sizeTo(origPanelRect.width, origPanelRect.height);
150      isArrowPositionedOn("bottom"); // should not have flipped
151      next();
152    });
153  }],
154
155  ['flippingResizeHorizontal', 'middle', function(next) {
156    openPopup("after_end", function() {
157      isArrowPositionedOn("right");
158      panel.sizeTo(anchor.getBoundingClientRect().left + 50, 50);
159      isArrowPositionedOn("left"); // check it flipped and has zero offset.
160      next();
161    });
162  }],
163
164  ['flippingResizeVertical', 'middle', function(next) {
165    openPopup("start_after", function() {
166      isArrowPositionedOn("bottom");
167      panel.sizeTo(50, anchor.getBoundingClientRect().top + 50);
168      isArrowPositionedOn("top"); // check it flipped and has zero offset.
169      next();
170    });
171  }],
172
173  ['simpleMoveToAnchorHorizontal', 'middle', function(next) {
174    openPopup("after_end", function() {
175      isArrowPositionedOn("right");
176      panel.moveToAnchor(anchor, "after_end", 20, 0);
177      // the anchor and the panel should have moved 20px right without flipping.
178      isArrowPositionedOn("right", 20);
179      panel.moveToAnchor(anchor, "after_end", -20, 0);
180      // the anchor and the panel should have moved 20px left without flipping.
181      isArrowPositionedOn("right", -20);
182      next();
183    });
184  }],
185
186  ['simpleMoveToAnchorVertical', 'middle', function(next) {
187    openPopup("start_after", function() {
188      isArrowPositionedOn("bottom");
189      panel.moveToAnchor(anchor, "start_after", 0, 20);
190      // the anchor and the panel should have moved 20px down without flipping.
191      isArrowPositionedOn("bottom", 20);
192      panel.moveToAnchor(anchor, "start_after", 0, -20);
193      // the anchor and the panel should have moved 20px up without flipping.
194      isArrowPositionedOn("bottom", -20);
195      next();
196    });
197  }],
198
199  // Do a moveToAnchor that causes the panel to flip horizontally
200  ['flippingMoveToAnchorHorizontal', 'middle', function(next) {
201    var anchorRight = anchor.getBoundingClientRect().right;
202    // Size the panel such that it only just fits from the left-hand side of
203    // the window to the right of the anchor - thus, it will fit when
204    // anchored to the right-hand side of the anchor.
205    panel.sizeTo(anchorRight - 10, 100);
206    openPopup("after_end", function() {
207      isArrowPositionedOn("right");
208      // Ask for it to be anchored 1/2 way between the left edge of the window
209      // and the anchor right - it can't fit with the panel on the left/arrow
210      // on the right, so it must flip (arrow on the left, panel on the right)
211      var offset = Math.floor(-anchorRight / 2);
212
213      waitForPopupPositioned(
214        () => panel.moveToAnchor(anchor, "after_end", offset, 0),
215        () => {
216          isArrowPositionedOn("left", offset); // should have flipped and have the offset.
217          // resize back to original and move to a zero offset - it should flip back.
218
219          panel.sizeTo(anchorRight - 10, 100);
220          waitForPopupPositioned(
221            () => panel.moveToAnchor(anchor, "after_end", 0, 0),
222            () => {
223              isArrowPositionedOn("right"); // should have flipped back and no offset
224              next();
225            });
226        });
227    });
228  }],
229
230  // Do a moveToAnchor that causes the panel to flip vertically
231  ['flippingMoveToAnchorVertical', 'middle', function(next) {
232    var anchorBottom = anchor.getBoundingClientRect().bottom;
233    // See comments above in flippingMoveToAnchorHorizontal, but read
234    // "top/bottom" instead of "left/right"
235    panel.sizeTo(100, anchorBottom - 10);
236    openPopup("start_after", function() {
237      isArrowPositionedOn("bottom");
238      var offset = Math.floor(-anchorBottom / 2);
239
240      waitForPopupPositioned(
241        () => panel.moveToAnchor(anchor, "start_after", 0, offset),
242        () => {
243          isArrowPositionedOn("top", offset);
244          panel.sizeTo(100, anchorBottom - 10);
245
246          waitForPopupPositioned(
247            () => panel.moveToAnchor(anchor, "start_after", 0, 0),
248            () => {
249              isArrowPositionedOn("bottom");
250              next();
251            });
252        });
253    });
254  }],
255
256  ['veryWidePanel-after_end', 'middle', function(next) {
257    openSlidingPopup("after_end", function() {
258      var origArrowRect = arrow.getBoundingClientRect();
259      // Now move it such that the arrow can't be at either end of the panel but
260      // instead somewhere in the middle as that is the only way things fit,
261      // meaning the arrow should "slide" down the panel.
262      panel.sizeTo(window.innerWidth - 10, 60);
263      is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested.")
264      // the arrow should not have moved.
265      var curArrowRect = arrow.getBoundingClientRect();
266      is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved");
267      is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down");
268      next();
269    });
270  }],
271
272  ['veryWidePanel-before_start', 'middle', function(next) {
273    openSlidingPopup("before_start", function() {
274      var origArrowRect = arrow.getBoundingClientRect();
275      // Now size it such that the arrow can't be at either end of the panel but
276      // instead somewhere in the middle as that is the only way things fit.
277      panel.sizeTo(window.innerWidth - 10, 60);
278      is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested")
279      // the arrow should not have moved.
280      var curArrowRect = arrow.getBoundingClientRect();
281      is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved");
282      is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down");
283      next();
284    });
285  }],
286
287  ['veryTallPanel-start_after', 'middle', function(next) {
288    openSlidingPopup("start_after", function() {
289      var origArrowRect = arrow.getBoundingClientRect();
290      // Now move it such that the arrow can't be at either end of the panel but
291      // instead somewhere in the middle as that is the only way things fit,
292      // meaning the arrow should "slide" down the panel.
293      panel.sizeTo(100, window.innerHeight - 10);
294      is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested.")
295      // the arrow should not have moved.
296      var curArrowRect = arrow.getBoundingClientRect();
297      is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved");
298      is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down");
299      next();
300    });
301  }],
302
303  ['veryTallPanel-start_before', 'middle', function(next) {
304    openSlidingPopup("start_before", function() {
305      var origArrowRect = arrow.getBoundingClientRect();
306      // Now size it such that the arrow can't be at either end of the panel but
307      // instead somewhere in the middle as that is the only way things fit.
308      panel.sizeTo(100, window.innerHeight - 10);
309      is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested")
310      // the arrow should not have moved.
311      var curArrowRect = arrow.getBoundingClientRect();
312      is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved");
313      is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down");
314      next();
315    });
316  }],
317
318  // Tests against the anchor at the right-hand side of the window
319  ['afterend', 'right', function(next) {
320    openPopup("after_end", function() {
321      // when we request too far to the right/bottom, the panel gets shrunk
322      // and moved.  The amount it is shrunk by is how far it is moved.
323      var panelRect = panel.getBoundingClientRect();
324      // panel was requested 100px wide - calc offset based on actual width.
325      var offset = panelRect.width - 100;
326      isArrowPositionedOn("right", offset);
327      next();
328    });
329  }],
330
331  ['after_start', 'right', function(next) {
332    openPopup("after_start", function() {
333      // See above - we are still too far to the right, but the anchor is
334      // on the other side.
335      var panelRect = panel.getBoundingClientRect();
336      var offset = panelRect.width - 100;
337      isArrowPositionedOn("right", offset);
338      next();
339    });
340  }],
341
342  // Tests against the anchor at the left-hand side of the window
343  ['after_start', 'left', function(next) {
344    openPopup("after_start", function() {
345      var panelRect = panel.getBoundingClientRect();
346      is(panelRect.left, 0, "panel remains within the screen");
347      // not sure how to determine the offset here, so given we have checked
348      // the panel is as left as possible while still being inside the window,
349      // we just don't check the offset.
350      isArrowPositionedOn("left", null);
351      next();
352    });
353  }],
354]
355
356function runTests() {
357  function runNextTest() {
358    let result = tests.shift();
359    if (!result) {
360      // out of tests
361      panel.hidePopup();
362      SimpleTest.finish();
363      return;
364    }
365    let [name, anchorPos, test] = result;
366    SimpleTest.info("sub-test " + anchorPos + "." + name + " starting");
367    // first arrange for the anchor to be where the test requires it.
368    panel.hidePopup();
369    panel.sizeTo(100, 50);
370    // hide all the anchors here, then later we make one of them visible.
371    document.getElementById("anchor-left-wrapper").style.display = "none";
372    document.getElementById("anchor-middle-wrapper").style.display = "none";
373    document.getElementById("anchor-right-wrapper").style.display = "none";
374    switch(anchorPos) {
375      case 'middle':
376        anchor = document.getElementById("anchor-middle");
377        document.getElementById("anchor-middle-wrapper").style.display = "block";
378        break;
379      case 'left':
380        anchor = document.getElementById("anchor-left");
381        document.getElementById("anchor-left-wrapper").style.display = "block";
382        break;
383      case 'right':
384        anchor = document.getElementById("anchor-right");
385        document.getElementById("anchor-right-wrapper").style.display = "block";
386        break;
387      default:
388        SimpleTest.ok(false, "Bad anchorPos: " + anchorPos);
389        runNextTest();
390        return;
391    }
392    try {
393      test(runNextTest);
394    } catch (ex) {
395      SimpleTest.ok(false, "sub-test " + anchorPos + "." + name + " failed: " + ex.toString() + "\n" + ex.stack);
396      runNextTest();
397    }
398  }
399  runNextTest();
400}
401
402SimpleTest.waitForExplicitFinish();
403
404addEventListener("load", function() {
405  // anchor is set by the test runner above
406  panel = document.getElementById("testPanel");
407
408  arrow = SpecialPowers.wrap(document).getAnonymousElementByAttribute(panel, "anonid", "arrow");
409  runTests();
410});
411
412]]>
413</script>
414
415<body xmlns="http://www.w3.org/1999/xhtml">
416<!-- Our tests assume at least 100px around the anchor on all sides, else the
417     panel may flip when we don't expect it to
418-->
419<div id="anchor-middle-wrapper" style="margin: 100px 100px 100px 100px;">
420  <p>The anchor --&gt; <span id="anchor-middle">v</span> &lt;--</p>
421</div>
422<div id="anchor-left-wrapper" style="text-align: left; display: none;">
423  <p><span id="anchor-left">v</span> &lt;-- The anchor;</p>
424</div>
425<div id="anchor-right-wrapper" style="text-align: right; display: none;">
426  <p>The anchor --&gt; <span id="anchor-right">v</span></p>
427</div>
428</body>
429
430</window>
431