Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
PANEL_HIDDEN,
isRootPanelVisible,
isThemesPanelVisible,
notifyThemePickerShown,
notifyThemePickersOnTransition,
} from "content-src/lib/theme-picker-shown";
import { CUSTOMIZE_SUBPANELS } from "content-src/lib/constants";
const ROOT_VISIBLE = {
showing: true,
activeSubpanel: null,
};
const HIDDEN = { ...ROOT_VISIBLE, showing: false };
const THEMES_VISIBLE = {
...ROOT_VISIBLE,
activeSubpanel: CUSTOMIZE_SUBPANELS.THEMES,
};
const shownCalls = [];
// Shaped like the lit element: once defined, `layout` is a property, so React
// sets it as a property rather than an attribute.
class StubThemePicker extends HTMLElement {
get layout() {
return this._layout ?? this.getAttribute("layout");
}
set layout(value) {
this._layout = value;
}
shown() {
shownCalls.push(this);
}
}
// Lets any pending promise callbacks run, including a shown() call.
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
// A picker rendered before the element was defined: React set the attribute.
function makeAttributePicker(layout) {
const el = document.createElement("theme-picker");
el.setAttribute("layout", layout);
return el;
}
// A picker rendered after the element was defined: React set the property.
function makeUpgradedPicker(layout) {
const el = document.createElement("theme-picker");
el.layout = layout;
return el;
}
describe("theme-picker-shown", () => {
beforeEach(() => {
shownCalls.length = 0;
document.body.innerHTML = "";
});
describe("isRootPanelVisible", () => {
it("is false when the panel is not showing", () => {
expect(isRootPanelVisible(HIDDEN)).toBe(false);
});
it("is true when showing with no subpanel open", () => {
expect(isRootPanelVisible(ROOT_VISIBLE)).toBe(true);
});
it("is false when showing with a subpanel open", () => {
expect(
isRootPanelVisible({
...ROOT_VISIBLE,
activeSubpanel: CUSTOMIZE_SUBPANELS.THEMES,
})
).toBe(false);
});
it("reads PANEL_HIDDEN as hidden", () => {
expect(isRootPanelVisible(PANEL_HIDDEN)).toBe(false);
expect(isThemesPanelVisible(PANEL_HIDDEN)).toBe(false);
});
});
describe("isThemesPanelVisible", () => {
it("is false when the panel is not showing", () => {
expect(
isThemesPanelVisible({
...HIDDEN,
activeSubpanel: CUSTOMIZE_SUBPANELS.THEMES,
})
).toBe(false);
});
it("is false when showing without the themes subpanel", () => {
expect(isThemesPanelVisible(ROOT_VISIBLE)).toBe(false);
});
it("is true when showing with the themes subpanel open", () => {
expect(isThemesPanelVisible(THEMES_VISIBLE)).toBe(true);
});
});
describe("notifyThemePickerShown", () => {
it("does nothing for a missing element", async () => {
await expect(notifyThemePickerShown(null)).resolves.toBeUndefined();
});
// Custom elements stay registered, so this test must run before any that
// relies on theme-picker being defined.
it("waits for the definition, keeps only the latest request per element, and re-checks visibility afterwards", async () => {
const compact = makeAttributePicker("compact");
const full = makeAttributePicker("full");
document.body.append(compact, full);
const superseded = notifyThemePickerShown(compact);
const latest = notifyThemePickerShown(compact);
let fullVisible = true;
const hiddenByThen = notifyThemePickerShown(full, () => fullVisible);
await flush();
expect(shownCalls).toHaveLength(0);
fullVisible = false;
customElements.define("theme-picker", StubThemePicker);
await Promise.all([superseded, latest, hiddenByThen]);
expect(shownCalls).toEqual([compact]);
});
it("skips an element that is no longer in the document", async () => {
const el = makeUpgradedPicker("compact");
await notifyThemePickerShown(el);
expect(shownCalls).toHaveLength(0);
});
it("skips when the live check says the picker is no longer shown", async () => {
const el = makeUpgradedPicker("compact");
document.body.append(el);
await notifyThemePickerShown(el, () => false);
expect(shownCalls).toHaveLength(0);
});
it("skips an element without shown(), as on older hosts", async () => {
const el = makeUpgradedPicker("compact");
document.body.append(el);
el.shown = undefined;
await expect(notifyThemePickerShown(el)).resolves.toBeUndefined();
expect(shownCalls).toHaveLength(0);
});
});
describe("notifyThemePickersOnTransition", () => {
let container;
let compact;
let full;
beforeEach(() => {
container = document.createElement("div");
compact = makeUpgradedPicker("compact");
full = makeUpgradedPicker("full");
container.append(compact, full);
document.body.append(container);
});
const transition = (prevProps, props) =>
notifyThemePickersOnTransition(container, prevProps, () => props);
it("does nothing without a container", async () => {
notifyThemePickersOnTransition(null, HIDDEN, () => ROOT_VISIBLE);
await flush();
expect(shownCalls).toHaveLength(0);
});
it("notifies the compact picker when the panel opens on the root", async () => {
transition(HIDDEN, ROOT_VISIBLE);
await flush();
expect(shownCalls).toEqual([compact]);
});
it("finds a picker that still carries layout as an attribute", async () => {
compact.remove();
const attributeCompact = makeAttributePicker("compact");
container.append(attributeCompact);
transition(HIDDEN, ROOT_VISIBLE);
await flush();
expect(shownCalls).toEqual([attributeCompact]);
});
it("does not notify the compact picker when the panel opens into a subpanel", async () => {
transition(HIDDEN, {
...ROOT_VISIBLE,
activeSubpanel: CUSTOMIZE_SUBPANELS.WIDGETS,
});
await flush();
expect(shownCalls).toHaveLength(0);
});
it("notifies the compact picker when a subpanel closes back to the root", async () => {
transition(
{ ...ROOT_VISIBLE, activeSubpanel: CUSTOMIZE_SUBPANELS.WALLPAPERS },
ROOT_VISIBLE
);
await flush();
expect(shownCalls).toEqual([compact]);
});
it("notifies the compact picker on a mount already showing the root", async () => {
transition(PANEL_HIDDEN, ROOT_VISIBLE);
await flush();
expect(shownCalls).toEqual([compact]);
});
it("notifies the full picker when the themes subpanel opens", async () => {
transition(ROOT_VISIBLE, THEMES_VISIBLE);
await flush();
expect(shownCalls).toEqual([full]);
});
it("notifies the full picker when the panel reopens with the themes subpanel still open", async () => {
transition(
{
...HIDDEN,
activeSubpanel: CUSTOMIZE_SUBPANELS.THEMES,
},
THEMES_VISIBLE
);
await flush();
expect(shownCalls).toEqual([full]);
});
it("does not notify the full picker while the panel is hidden", async () => {
transition(HIDDEN, {
...HIDDEN,
activeSubpanel: CUSTOMIZE_SUBPANELS.THEMES,
});
await flush();
expect(shownCalls).toHaveLength(0);
});
it("does nothing on a re-render with no transition", async () => {
transition(ROOT_VISIBLE, ROOT_VISIBLE);
transition(THEMES_VISIBLE, THEMES_VISIBLE);
await flush();
expect(shownCalls).toHaveLength(0);
});
it("drops the compact call when the root is covered by the time the element is ready", async () => {
let current = ROOT_VISIBLE;
notifyThemePickersOnTransition(container, HIDDEN, () => current);
current = {
...ROOT_VISIBLE,
activeSubpanel: CUSTOMIZE_SUBPANELS.WALLPAPERS,
};
await flush();
expect(shownCalls).toHaveLength(0);
});
it("drops the full call when the themes subpanel closed by the time the element is ready", async () => {
let current = THEMES_VISIBLE;
notifyThemePickersOnTransition(container, ROOT_VISIBLE, () => current);
current = ROOT_VISIBLE;
await flush();
expect(shownCalls).toHaveLength(0);
});
});
});