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 https://mozilla.org/MPL/2.0/. */
import {
DEFAULT_PAGE_LAYOUT_VARIANT,
PAGE_LAYOUT_VARIANTS,
SIDE_BY_SIDE_PAGE_LAYOUTS,
SPACES_PAGE_LAYOUTS,
isSideBySideActive,
isSideBySideAssigned,
isSpacesActive,
isSpacesAssigned,
resolvePageLayoutVariant,
resolvePopulatedSpaces,
sideBySideBandClasses,
spacesBandClasses,
} from "common/PageLayoutVariants.mjs";
// The gates isSideBySideActive checks besides the variant itself.
const STORIES_ON = {
"feeds.section.topstories": true,
"feeds.system.topstories": true,
};
// The container being on is not enough: something has to actually be in it.
const WIDGETS_ON = {
"widgets.system.enabled": true,
"widgets.enabled": true,
"widgets.system.lists.enabled": true,
"widgets.lists.enabled": true,
};
const sideBySide = {
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD,
...STORIES_ON,
...WIDGETS_ON,
};
describe("resolvePageLayoutVariant", () => {
it("defaults to the full-width layout when nothing is set", () => {
expect(resolvePageLayoutVariant({})).toBe(DEFAULT_PAGE_LAYOUT_VARIANT);
expect(DEFAULT_PAGE_LAYOUT_VARIANT).toBe("nova-full-width");
});
it("returns the pref value", () => {
expect(
resolvePageLayoutVariant({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD,
})
).toBe("side-by-side-widgets-lead");
});
it("lets trainhopConfig override a non-default pref value", () => {
expect(
resolvePageLayoutVariant({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD,
trainhopConfig: {
pageLayouts: {
variant: PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD,
},
},
})
).toBe("side-by-side-widgets-lead");
});
it("falls back to the pref for a missing or non-string trainhop value", () => {
const pref = {
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD,
};
expect(resolvePageLayoutVariant({ ...pref, trainhopConfig: {} })).toBe(
"side-by-side-content-lead"
);
expect(
resolvePageLayoutVariant({ ...pref, trainhopConfig: { pageLayouts: {} } })
).toBe("side-by-side-content-lead");
expect(
resolvePageLayoutVariant({
...pref,
trainhopConfig: { pageLayouts: { variant: true } },
})
).toBe("side-by-side-content-lead");
expect(
resolvePageLayoutVariant({
...pref,
trainhopConfig: { pageLayouts: { variant: "" } },
})
).toBe("side-by-side-content-lead");
});
});
describe("isSideBySideAssigned", () => {
it("is true for every side-by-side variant regardless of the sections", () => {
expect(SIDE_BY_SIDE_PAGE_LAYOUTS).toHaveLength(4);
for (const variant of SIDE_BY_SIDE_PAGE_LAYOUTS) {
expect(isSideBySideAssigned({ "pageLayouts.variant": variant })).toBe(
true
);
}
});
// This is the difference from isSideBySideActive: the panels follow the
// assignment, so a lone section is still framed.
it("stays true where isSideBySideActive is false", () => {
const noWidgets = { ...sideBySide, "widgets.lists.enabled": false };
expect(isSideBySideAssigned(noWidgets)).toBe(true);
expect(isSideBySideActive(noWidgets)).toBe(false);
});
it("is false for the default layout", () => {
expect(isSideBySideAssigned({})).toBe(false);
expect(
isSideBySideAssigned({ "pageLayouts.variant": "nova-full-width" })
).toBe(false);
});
it("follows a variant set through trainhopConfig", () => {
expect(
isSideBySideAssigned({
trainhopConfig: {
pageLayouts: {
variant: PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD,
},
},
})
).toBe(true);
});
});
describe("isSideBySideActive", () => {
it("is true for every side-by-side variant when stories and widgets are on", () => {
for (const variant of SIDE_BY_SIDE_PAGE_LAYOUTS) {
expect(
isSideBySideActive({ ...sideBySide, "pageLayouts.variant": variant })
).toBe(true);
}
});
it("is false for the default variant even with stories and widgets on", () => {
expect(
isSideBySideActive({ ...sideBySide, "pageLayouts.variant": undefined })
).toBe(false);
});
it("is false when either stories pref is off", () => {
expect(
isSideBySideActive({ ...sideBySide, "feeds.section.topstories": false })
).toBe(false);
expect(
isSideBySideActive({ ...sideBySide, "feeds.system.topstories": false })
).toBe(false);
});
it("is false when the widgets container is not visible", () => {
expect(
isSideBySideActive({ ...sideBySide, "widgets.system.enabled": false })
).toBe(false);
});
it("is false when the container is on but every widget is hidden", () => {
expect(
isSideBySideActive({ ...sideBySide, "widgets.enabled": false })
).toBe(false);
expect(
isSideBySideActive({ ...sideBySide, "widgets.lists.enabled": false })
).toBe(false);
});
// Small weather lives in the sidebar, not the band.
it("is false when the only enabled widget is weather in the sidebar", () => {
const weatherOnly = {
...sideBySide,
"widgets.lists.enabled": false,
"widgets.system.weather.enabled": true,
"widgets.weather.enabled": true,
"weather.display": "",
"widgets.weather.size": "small",
};
expect(isSideBySideActive(weatherOnly)).toBe(false);
// ...but a larger weather widget stays in the content area and does count.
expect(
isSideBySideActive({ ...weatherOnly, "widgets.weather.size": "medium" })
).toBe(true);
});
it("is true when the variant comes from trainhopConfig", () => {
expect(
isSideBySideActive({
...STORIES_ON,
...WIDGETS_ON,
trainhopConfig: {
pageLayouts: {
variant: PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD,
},
},
})
).toBe(true);
});
// Goes through isWidgetsContainerVisible, so the keys the hand-rolled copies in
// DiscoveryStreamBase.jsx and selectLayoutRender.mjs miss still count.
it("is true when widgets are enabled only via trainhopConfig.widgetsSettings", () => {
expect(
isSideBySideActive({
...sideBySide,
"widgets.system.enabled": false,
trainhopConfig: { widgetsSettings: { enabled: true } },
})
).toBe(true);
});
it("is true when widgets are enabled only via trainhopConfig.widgets", () => {
expect(
isSideBySideActive({
...sideBySide,
"widgets.system.enabled": false,
trainhopConfig: { widgets: { enabled: true } },
})
).toBe(true);
});
});
describe("sideBySideBandClasses", () => {
it("is empty outside the experiment", () => {
expect(sideBySideBandClasses({})).toEqual([]);
expect(
sideBySideBandClasses({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.NOVA_FULL_WIDTH,
})
).toEqual([]);
expect(
sideBySideBandClasses({ "pageLayouts.variant": "not-a-variant" })
).toEqual([]);
});
// The lead class alone, so nothing unlocks the fourth content card.
it("gives the four-column variants just their lead class", () => {
expect(
sideBySideBandClasses({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD,
})
).toEqual(["side-by-side-content-lead"]);
expect(
sideBySideBandClasses({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD,
})
).toEqual(["side-by-side-widgets-lead"]);
});
// The lead class is shared with the four-column variant, so every existing
// side-by-side rule keeps matching; side-by-side-five is the only difference.
it("adds side-by-side-five for the five-column variants", () => {
expect(
sideBySideBandClasses({
"pageLayouts.variant":
PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD_FIVE,
})
).toEqual(["side-by-side-content-lead", "side-by-side-five"]);
expect(
sideBySideBandClasses({
"pageLayouts.variant":
PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD_FIVE,
})
).toEqual(["side-by-side-widgets-lead", "side-by-side-five"]);
});
// CSS matches classes per token, so a "-five" variant name would not be
// matched by any .side-by-side-*-lead rule. It must never reach the class list.
it("never emits a -five variant name as a class", () => {
for (const variant of SIDE_BY_SIDE_PAGE_LAYOUTS.filter(v =>
v.endsWith("-five")
)) {
expect(
sideBySideBandClasses({ "pageLayouts.variant": variant })
).not.toContain(variant);
}
});
it("follows a variant set through trainhopConfig", () => {
expect(
sideBySideBandClasses({
trainhopConfig: {
pageLayouts: {
variant: PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD_FIVE,
},
},
})
).toEqual(["side-by-side-widgets-lead", "side-by-side-five"]);
});
});
const spaces = {
"nova.enabled": true,
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SPACES_BUTTONS_BOTTOM,
...STORIES_ON,
...WIDGETS_ON,
};
describe("spacesBandClasses", () => {
it("decomposes each variant into the family class plus a placement", () => {
expect(
spacesBandClasses({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SPACES_BUTTONS_TOP,
})
).toEqual(["spaces", "spaces-buttons-top"]);
expect(
spacesBandClasses({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SPACES_BUTTONS_BOTTOM,
})
).toEqual(["spaces", "spaces-buttons-bottom"]);
});
it("returns nothing for the other layouts", () => {
expect(spacesBandClasses({})).toEqual([]);
expect(
spacesBandClasses({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_CONTENT_LEAD,
})
).toEqual([]);
});
});
describe("isSpacesAssigned", () => {
it("is true for every spaces variant, whatever the content prefs", () => {
for (const variant of SPACES_PAGE_LAYOUTS) {
expect(isSpacesAssigned({ "pageLayouts.variant": variant })).toBe(true);
}
});
it("is false outside the spaces variants", () => {
expect(isSpacesAssigned({})).toBe(false);
expect(
isSpacesAssigned({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SIDE_BY_SIDE_WIDGETS_LEAD,
})
).toBe(false);
});
});
describe("resolvePopulatedSpaces", () => {
it("lists only the spaces whose content is enabled, in tablist order", () => {
expect(resolvePopulatedSpaces(spaces)).toEqual(["stories", "widgets"]);
expect(
resolvePopulatedSpaces({
...spaces,
"feeds.section.highlights": true,
})
).toEqual(["stories", "widgets", "activity"]);
});
it("does not turn anything on", () => {
expect(resolvePopulatedSpaces({ ...spaces, ...{} })).not.toContain(
"activity"
);
expect(resolvePopulatedSpaces({})).toEqual([]);
});
});
describe("the experiment override", () => {
// One shape for all three spaces: the experiment turns a space on unless its
// opt-out pref says the user turned it off while enrolled. The user's own
// pref is never written, so unenrolling restores their choice.
const enrolled = {
...spaces,
trainhopConfig: {
stories: { enabled: true },
widgets: { enabled: true },
highlights: { enabled: true },
},
};
it("overrides a space the profile enrolled with turned off", () => {
expect(
resolvePopulatedSpaces({
...enrolled,
"feeds.section.topstories": false,
"feeds.section.highlights": false,
})
).toEqual(["stories", "widgets", "activity"]);
});
it("respects a space turned off while enrolled, for every space", () => {
expect(
resolvePopulatedSpaces({
...enrolled,
"feeds.section.topstories": false,
"spaces.storiesOptOut": true,
"widgets.enabled": false,
"spaces.widgetsOptOut": true,
"feeds.section.highlights": false,
"spaces.activityOptOut": true,
})
).toEqual([]);
});
it("does not override outside the experiment", () => {
expect(
resolvePopulatedSpaces({
...enrolled,
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.NOVA_FULL_WIDTH,
"feeds.section.topstories": false,
})
).toEqual(["widgets"]);
});
it("does not conjure content that is not there", () => {
// Region and locale decide whether stories exist at all, and no override
// should produce an empty panel.
expect(
resolvePopulatedSpaces({
...enrolled,
"feeds.system.topstories": false,
})
).not.toContain("stories");
});
});
describe("the widgets space gate", () => {
it("is not a space when the user has switched widgets off", () => {
expect(
resolvePopulatedSpaces({ ...spaces, "widgets.enabled": false })
).not.toContain("widgets");
});
it("is not a space when the container is on but no widget is enabled", () => {
expect(
resolvePopulatedSpaces({
...spaces,
"widgets.lists.enabled": false,
})
).not.toContain("widgets");
});
it("is not a space when weather is the only widget", () => {
// Weather moves to the sidebar at its small size, so the row is empty.
expect(
resolvePopulatedSpaces({
...spaces,
"widgets.lists.enabled": false,
"widgets.system.weather.enabled": true,
"widgets.weather.enabled": true,
"widgets.weather.size": "small",
})
).not.toContain("widgets");
});
it("is a space when a widget is actually showing", () => {
expect(resolvePopulatedSpaces(spaces)).toContain("widgets");
});
});
describe("isSpacesActive", () => {
it("is true with a spaces variant and somewhere to navigate to", () => {
expect(isSpacesActive(spaces)).toBe(true);
});
it("is false with only one populated space", () => {
expect(
isSpacesActive({
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.SPACES_BUTTONS_BOTTOM,
...STORIES_ON,
})
).toBe(false);
});
it("is false outside the spaces variants", () => {
expect(
isSpacesActive({
...spaces,
"pageLayouts.variant": PAGE_LAYOUT_VARIANTS.NOVA_FULL_WIDTH,
})
).toBe(false);
});
it("is false without Nova, whose stylesheet it depends on", () => {
expect(isSpacesActive({ ...spaces, "nova.enabled": false })).toBe(false);
});
});