fune/browser/components/customizableui/test/browser_PanelMultiView.js
Paolo Amadini 91212fa6b1 Bug 1428839 - Part 4 - Avoid re-entrancy in PanelMultiView navigation functions. r=Gijs
We now use the "active" property of views to track whether navigation is possible. This has the advantage of being already handled correctly when views are moved to a different panel, and is in line with the purpose of the "active" state. The note about using the "popupshown" event for navigation has been updated accordingly.

Keyboard navigation is also linked to the "active" property now, so there is no need to track the state of the "_transitioning" property anymore.

Since the goBack and showSubView methods can only be called when the view is active, we don't need to check for attempts to start a transition while the panel is closed anymore.

MozReview-Commit-ID: 3KT3A5EwGFy

--HG--
extra : rebase_source : c338362923a7574210c7329bb84316d549898fdc
2018-02-27 15:33:33 +00:00

513 lines
15 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Unit tests for the PanelMultiView module.
*/
ChromeUtils.import("resource:///modules/PanelMultiView.jsm");
const PANELS_COUNT = 2;
let gPanelAnchors = [];
let gPanels = [];
let gPanelMultiViews = [];
const PANELVIEWS_COUNT = 4;
let gPanelViews = [];
let gPanelViewLabels = [];
const EVENT_TYPES = ["popupshown", "popuphidden", "PanelMultiViewHidden",
"ViewShowing", "ViewShown", "ViewHiding"];
/**
* Checks that the element is displayed, including the state of the popup where
* the element is located. This can trigger a synchronous reflow if necessary,
* because even though the code under test is designed to avoid synchronous
* reflows, it can raise completion events while a layout flush is still needed.
*
* In production code, event handlers for ViewShown have to wait for a flush if
* they need to read style or layout information, like other code normally does.
*/
function is_visible(element) {
var style = element.ownerGlobal.getComputedStyle(element);
if (style.display == "none")
return false;
if (style.visibility != "visible")
return false;
if (style.display == "-moz-popup" && element.state != "open")
return false;
// Hiding a parent element will hide all its children
if (element.parentNode != element.ownerDocument)
return is_visible(element.parentNode);
return true;
}
/**
* Checks whether the label in the specified view is visible.
*/
function assertLabelVisible(viewIndex, expectedVisible) {
Assert.equal(is_visible(gPanelViewLabels[viewIndex]), expectedVisible,
`Visibility of label in view ${viewIndex}`);
}
/**
* Opens the specified view as the main view in the specified panel.
*/
async function openPopup(panelIndex, viewIndex) {
gPanelMultiViews[panelIndex].setAttribute("mainViewId",
gPanelViews[viewIndex].id);
let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[viewIndex],
"ViewShown");
PanelMultiView.openPopup(gPanels[panelIndex], gPanelAnchors[panelIndex],
"bottomcenter topright");
await promiseShown;
Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active);
assertLabelVisible(viewIndex, true);
}
/**
* Closes the specified panel.
*/
async function hidePopup(panelIndex) {
gPanelMultiViews[panelIndex].setAttribute("mainViewId",
gPanelViews[panelIndex].id);
let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[panelIndex],
"popuphidden");
PanelMultiView.hidePopup(gPanels[panelIndex]);
await promiseHidden;
}
/**
* Opens the specified subview in the specified panel.
*/
async function showSubView(panelIndex, viewIndex) {
let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[viewIndex],
"ViewShown");
gPanelMultiViews[panelIndex].showSubView(gPanelViews[viewIndex]);
await promiseShown;
Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active);
assertLabelVisible(viewIndex, true);
}
/**
* Navigates backwards to the specified view, which is displayed as a result.
*/
async function goBack(panelIndex, viewIndex) {
let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[viewIndex],
"ViewShown");
gPanelMultiViews[panelIndex].goBack();
await promiseShown;
Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active);
assertLabelVisible(viewIndex, true);
}
/**
* Records the specified events on an element into the specified array. An
* optional callback can be used to respond to events and trigger nested events.
*/
function recordEvents(element, eventTypes, recordArray,
eventCallback = () => {}) {
let nestedEvents = [];
element.recorders = eventTypes.map(eventType => {
let recorder = {
eventType,
listener(event) {
let eventString = nestedEvents.join("") +
`${event.originalTarget.id}: ${event.type}`;
info(`Event on ${eventString}`);
recordArray.push(eventString);
// Any synchronous event triggered from within the given callback will
// include information about the current event.
nestedEvents.unshift(`${eventString} > `);
eventCallback(event);
nestedEvents.shift();
},
};
element.addEventListener(recorder.eventType, recorder.listener);
return recorder;
});
}
/**
* Stops recording events on an element.
*/
function stopRecordingEvents(element) {
for (let recorder of element.recorders) {
element.removeEventListener(recorder.eventType, recorder.listener);
}
delete element.recorders;
}
/**
* Sets up the elements in the browser window that will be used by all the other
* regression tests. Since the panel and view elements can live anywhere in the
* document, they are simply added to the same toolbar as the panel anchors.
*
* <toolbar id="nav-bar">
* <toolbarbutton/> -> gPanelAnchors[panelIndex]
* <panel> -> gPanels[panelIndex]
* <panelmultiview/> -> gPanelMultiViews[panelIndex]
* </panel>
* <panelview> -> gPanelViews[viewIndex]
* <label/> -> gPanelViewLabels[viewIndex]
* </panelview>
* </toolbar>
*/
add_task(async function test_setup() {
let navBar = document.getElementById("nav-bar");
for (let i = 0; i < PANELS_COUNT; i++) {
gPanelAnchors[i] = document.createElement("toolbarbutton");
gPanelAnchors[i].classList.add("toolbarbutton-1",
"chromeclass-toolbar-additional");
navBar.appendChild(gPanelAnchors[i]);
gPanels[i] = document.createElement("panel");
gPanels[i].id = "panel-" + i;
gPanels[i].setAttribute("type", "arrow");
gPanels[i].setAttribute("photon", true);
navBar.appendChild(gPanels[i]);
gPanelMultiViews[i] = document.createElement("panelmultiview");
gPanelMultiViews[i].id = "panelmultiview-" + i;
gPanels[i].appendChild(gPanelMultiViews[i]);
}
for (let i = 0; i < PANELVIEWS_COUNT; i++) {
gPanelViews[i] = document.createElement("panelview");
gPanelViews[i].id = "panelview-" + i;
navBar.appendChild(gPanelViews[i]);
gPanelViewLabels[i] = document.createElement("label");
gPanelViewLabels[i].setAttribute("value", "PanelView " + i);
gPanelViews[i].appendChild(gPanelViewLabels[i]);
}
registerCleanupFunction(() => {
[...gPanelAnchors, ...gPanels, ...gPanelViews].forEach(e => e.remove());
});
});
/**
* Shows and hides all views in a panel with this static structure:
*
* - Panel 0
* - View 0
* - View 1
* - View 3
* - View 2
*/
add_task(async function test_simple() {
// Show main view 0.
await openPopup(0, 0);
// Show and hide subview 1.
await showSubView(0, 1);
assertLabelVisible(0, false);
await goBack(0, 0);
assertLabelVisible(1, false);
// Show subview 3.
await showSubView(0, 3);
assertLabelVisible(0, false);
// Show and hide subview 2.
await showSubView(0, 2);
assertLabelVisible(3, false);
await goBack(0, 3);
assertLabelVisible(2, false);
// Hide subview 3.
await goBack(0, 0);
assertLabelVisible(3, false);
// Hide main view 0.
await hidePopup(0);
assertLabelVisible(0, false);
});
/**
* Tests the event sequence in a panel with this static structure:
*
* - Panel 0
* - View 0
* - View 1
* - View 3
* - View 2
*/
add_task(async function test_simple_event_sequence() {
let recordArray = [];
recordEvents(gPanels[0], EVENT_TYPES, recordArray);
await openPopup(0, 0);
await showSubView(0, 1);
await goBack(0, 0);
await showSubView(0, 3);
await showSubView(0, 2);
await goBack(0, 3);
await goBack(0, 0);
await hidePopup(0);
stopRecordingEvents(gPanels[0]);
Assert.deepEqual(recordArray, [
"panelview-0: ViewShowing",
"panelview-0: ViewShown",
"panel-0: popupshown",
"panelview-1: ViewShowing",
"panelview-1: ViewShown",
"panelview-1: ViewHiding",
"panelview-0: ViewShown",
"panelview-3: ViewShowing",
"panelview-3: ViewShown",
"panelview-2: ViewShowing",
"panelview-2: ViewShown",
"panelview-2: ViewHiding",
"panelview-3: ViewShown",
"panelview-3: ViewHiding",
"panelview-0: ViewShown",
"panelview-0: ViewHiding",
"panelmultiview-0: PanelMultiViewHidden",
"panel-0: popuphidden",
]);
});
/**
* Tests that further navigation is suppressed until the new view is shown.
*/
add_task(async function test_navigation_suppression() {
await openPopup(0, 0);
// Test re-entering the "showSubView" method.
let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[1], "ViewShown");
gPanelMultiViews[0].showSubView(gPanelViews[1]);
Assert.ok(!PanelView.forNode(gPanelViews[0]).active,
"The previous view should become inactive synchronously.");
// The following call will have no effect.
gPanelMultiViews[0].showSubView(gPanelViews[2]);
await promiseShown;
// Test re-entering the "goBack" method.
promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[0], "ViewShown");
gPanelMultiViews[0].goBack();
Assert.ok(!PanelView.forNode(gPanelViews[1]).active,
"The previous view should become inactive synchronously.");
// The following call will have no effect.
gPanelMultiViews[0].goBack();
await promiseShown;
// Main view 0 should be displayed.
assertLabelVisible(0, true);
await hidePopup(0);
});
/**
* Tests reusing views that are already open in another panel. In this test, the
* structure of the first panel will change dynamically:
*
* - Panel 0
* - View 0
* - View 1
* - Panel 1
* - View 1
* - View 2
* - Panel 0
* - View 1
* - View 0
*/
add_task(async function test_switch_event_sequence() {
let recordArray = [];
recordEvents(gPanels[0], EVENT_TYPES, recordArray);
recordEvents(gPanels[1], EVENT_TYPES, recordArray);
// Show panel 0.
await openPopup(0, 0);
await showSubView(0, 1);
// Show panel 1 with the view that is already open and visible in panel 0.
// This will close panel 0 automatically.
await openPopup(1, 1);
await showSubView(1, 2);
// Show panel 0 with a view that is already open but invisible in panel 1.
// This will close panel 1 automatically.
await openPopup(0, 1);
await showSubView(0, 0);
// Hide panel 0.
await hidePopup(0);
stopRecordingEvents(gPanels[0]);
stopRecordingEvents(gPanels[1]);
Assert.deepEqual(recordArray, [
"panelview-0: ViewShowing",
"panelview-0: ViewShown",
"panel-0: popupshown",
"panelview-1: ViewShowing",
"panelview-1: ViewShown",
"panelview-1: ViewHiding",
"panelview-0: ViewHiding",
"panelmultiview-0: PanelMultiViewHidden",
"panel-0: popuphidden",
"panelview-1: ViewShowing",
"panel-1: popupshown",
"panelview-1: ViewShown",
"panelview-2: ViewShowing",
"panelview-2: ViewShown",
"panel-1: popuphidden",
"panelview-2: ViewHiding",
"panelview-1: ViewHiding",
"panelmultiview-1: PanelMultiViewHidden",
"panelview-1: ViewShowing",
"panelview-1: ViewShown",
"panel-0: popupshown",
"panelview-0: ViewShowing",
"panelview-0: ViewShown",
"panelview-0: ViewHiding",
"panelview-1: ViewHiding",
"panelmultiview-0: PanelMultiViewHidden",
"panel-0: popuphidden",
]);
});
/**
* Tests the event sequence when opening the main view is canceled.
*/
add_task(async function test_cancel_mainview_event_sequence() {
let recordArray = [];
recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => {
if (event.type == "ViewShowing") {
event.preventDefault();
}
});
gPanelMultiViews[0].setAttribute("mainViewId", gPanelViews[0].id);
let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[0], "popuphidden");
PanelMultiView.openPopup(gPanels[0], gPanelAnchors[0],
"bottomcenter topright");
await promiseHidden;
stopRecordingEvents(gPanels[0]);
Assert.deepEqual(recordArray, [
"panelview-0: ViewShowing",
"panelview-0: ViewHiding",
"panelmultiview-0: popuphidden",
]);
});
/**
* Tests the event sequence when opening a subview is canceled.
*/
add_task(async function test_cancel_subview_event_sequence() {
let recordArray = [];
recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => {
if (event.type == "ViewShowing" &&
event.originalTarget.id == gPanelViews[1].id) {
event.preventDefault();
}
});
await openPopup(0, 0);
let promiseHiding = BrowserTestUtils.waitForEvent(gPanelViews[1],
"ViewHiding");
gPanelMultiViews[0].showSubView(gPanelViews[1]);
await promiseHiding;
// Only the subview should have received the hidden event at this point.
Assert.deepEqual(recordArray, [
"panelview-0: ViewShowing",
"panelview-0: ViewShown",
"panel-0: popupshown",
"panelview-1: ViewShowing",
"panelview-1: ViewHiding",
]);
recordArray.length = 0;
await hidePopup(0);
stopRecordingEvents(gPanels[0]);
Assert.deepEqual(recordArray, [
"panelview-0: ViewHiding",
"panelmultiview-0: PanelMultiViewHidden",
"panel-0: popuphidden",
]);
});
/**
* Tests the event sequence when closing the panel while opening the main view.
*/
add_task(async function test_close_while_showing_mainview_event_sequence() {
let recordArray = [];
recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => {
if (event.type == "ViewShowing") {
PanelMultiView.hidePopup(gPanels[0]);
}
});
gPanelMultiViews[0].setAttribute("mainViewId", gPanelViews[0].id);
let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[0], "popuphidden");
let promiseHiding = BrowserTestUtils.waitForEvent(gPanelViews[0],
"ViewHiding");
PanelMultiView.openPopup(gPanels[0], gPanelAnchors[0],
"bottomcenter topright");
await promiseHiding;
await promiseHidden;
stopRecordingEvents(gPanels[0]);
Assert.deepEqual(recordArray, [
"panelview-0: ViewShowing",
"panelview-0: ViewShowing > panelmultiview-0: popuphidden",
"panelview-0: ViewShowing > panelview-0: ViewHiding",
]);
});
/**
* Tests the event sequence when closing the panel while opening a subview.
*/
add_task(async function test_close_while_showing_subview_event_sequence() {
let recordArray = [];
recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => {
if (event.type == "ViewShowing" &&
event.originalTarget.id == gPanelViews[1].id) {
PanelMultiView.hidePopup(gPanels[0]);
}
});
await openPopup(0, 0);
let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[0], "popuphidden");
gPanelMultiViews[0].showSubView(gPanelViews[1]);
await promiseHidden;
stopRecordingEvents(gPanels[0]);
Assert.deepEqual(recordArray, [
"panelview-0: ViewShowing",
"panelview-0: ViewShown",
"panel-0: popupshown",
"panelview-1: ViewShowing",
"panelview-1: ViewShowing > panelview-1: ViewHiding",
"panelview-1: ViewShowing > panelview-0: ViewHiding",
"panelview-1: ViewShowing > panelmultiview-0: PanelMultiViewHidden",
"panelview-1: ViewShowing > panel-0: popuphidden",
]);
});