import {
_Base as Base,
BaseContent,
WithDsAdmin,
} from "content-src/components/Base/Base";
import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin";
import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary";
import React from "react";
import { Search } from "content-src/components/Search/Search";
import { shallow } from "enzyme";
import { actionCreators as ac } from "common/Actions.mjs";
describe("", () => {
let DEFAULT_PROPS = {
store: { getState: () => {} },
App: { initialized: true },
Prefs: { values: {} },
Sections: [],
DiscoveryStream: { config: { enabled: false } },
dispatch: () => {},
adminContent: {
message: {},
},
document: {
visibilityState: "visible",
addEventListener: sinon.stub(),
removeEventListener: sinon.stub(),
},
};
it("should render Base component", () => {
const wrapper = shallow();
assert.ok(wrapper.exists());
});
it("should render the BaseContent component, passing through all props", () => {
const wrapper = shallow();
const props = wrapper.find(BaseContent).props();
assert.deepEqual(
props,
DEFAULT_PROPS,
JSON.stringify([props, DEFAULT_PROPS], null, 3)
);
});
it("should render an ErrorBoundary with class base-content-fallback", () => {
const wrapper = shallow();
assert.equal(
wrapper.find(ErrorBoundary).first().prop("className"),
"base-content-fallback"
);
});
it("should render an WithDsAdmin if the devtools pref is true", () => {
const wrapper = shallow(
);
assert.lengthOf(wrapper.find(WithDsAdmin), 1);
});
it("should not render an WithDsAdmin if the devtools pref is false", () => {
const wrapper = shallow(
);
assert.lengthOf(wrapper.find(WithDsAdmin), 0);
});
});
describe("", () => {
let DEFAULT_PROPS = {
store: { getState: () => {} },
App: { initialized: true },
Prefs: { values: {} },
Sections: [],
DiscoveryStream: { config: { enabled: false }, spocs: {} },
dispatch: () => {},
document: {
visibilityState: "visible",
addEventListener: sinon.stub(),
removeEventListener: sinon.stub(),
},
};
it("should render an ErrorBoundary with a Search child", () => {
const searchEnabledProps = Object.assign({}, DEFAULT_PROPS, {
Prefs: { values: { showSearch: true } },
});
const wrapper = shallow();
assert.isTrue(wrapper.find(Search).parent().is(ErrorBoundary));
});
it("should dispatch a user event when the customize menu is opened or closed", () => {
const dispatch = sinon.stub();
const wrapper = shallow(
);
wrapper.instance().openCustomizationMenu();
assert.calledWith(dispatch, { type: "SHOW_PERSONALIZE" });
assert.calledWith(dispatch, ac.UserEvent({ event: "SHOW_PERSONALIZE" }));
wrapper.instance().closeCustomizationMenu();
assert.calledWith(dispatch, { type: "HIDE_PERSONALIZE" });
assert.calledWith(dispatch, ac.UserEvent({ event: "HIDE_PERSONALIZE" }));
});
it("should render only search if no Sections are enabled", () => {
const onlySearchProps = Object.assign({}, DEFAULT_PROPS, {
Sections: [{ id: "highlights", enabled: false }],
Prefs: { values: { showSearch: true } },
});
const wrapper = shallow();
assert.lengthOf(wrapper.find(".only-search"), 1);
});
it("should update firstVisibleTimestamp if it is visible immediately with no event listener", () => {
const props = Object.assign({}, DEFAULT_PROPS, {
document: {
visibilityState: "visible",
addEventListener: sinon.spy(),
removeEventListener: sinon.spy(),
},
});
const wrapper = shallow();
assert.notCalled(props.document.addEventListener);
assert.isDefined(wrapper.state("firstVisibleTimestamp"));
});
it("should attach an event listener for visibility change if it is not visible", () => {
const props = Object.assign({}, DEFAULT_PROPS, {
document: {
visibilityState: "hidden",
addEventListener: sinon.spy(),
removeEventListener: sinon.spy(),
},
});
const wrapper = shallow();
assert.calledWith(props.document.addEventListener, "visibilitychange");
assert.notExists(wrapper.state("firstVisibleTimestamp"));
});
it("should remove the event listener for visibility change when unmounted", () => {
const props = Object.assign({}, DEFAULT_PROPS, {
document: {
visibilityState: "hidden",
addEventListener: sinon.spy(),
removeEventListener: sinon.spy(),
},
});
const wrapper = shallow();
const [, listener] = props.document.addEventListener.firstCall.args;
wrapper.unmount();
assert.calledWith(
props.document.removeEventListener,
"visibilitychange",
listener
);
});
it("should remove the event listener for visibility change after becoming visible", () => {
const listeners = new Set();
const props = Object.assign({}, DEFAULT_PROPS, {
document: {
visibilityState: "hidden",
addEventListener: (ev, cb) => listeners.add(cb),
removeEventListener: (ev, cb) => listeners.delete(cb),
},
});
const wrapper = shallow();
assert.equal(listeners.size, 1);
assert.notExists(wrapper.state("firstVisibleTimestamp"));
// Simulate listeners getting called
props.document.visibilityState = "visible";
listeners.forEach(l => l());
assert.equal(listeners.size, 0);
assert.isDefined(wrapper.state("firstVisibleTimestamp"));
});
});
describe("WithDsAdmin", () => {
describe("rendering inner content", () => {
it("should not set devtoolsCollapsed state for about:newtab (no hash)", () => {
const wrapper = shallow();
assert.isTrue(
wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed")
);
assert.lengthOf(wrapper.find(BaseContent), 1);
});
it("should set devtoolsCollapsed state for about:newtab#devtools", () => {
const wrapper = shallow();
assert.isFalse(
wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed")
);
assert.lengthOf(wrapper.find(BaseContent), 0);
});
it("should set devtoolsCollapsed state for about:newtab#devtools subroutes", () => {
const wrapper = shallow();
assert.isFalse(
wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed")
);
assert.lengthOf(wrapper.find(BaseContent), 0);
});
});
describe("SPOC Placeholder Duration Tracking", () => {
let wrapper;
let instance;
let dispatch;
let clock;
let baseProps;
beforeEach(() => {
// Setup: Create a component with expired spocs (showing placeholders)
// - useFakeTimers allows us to control time for duration testing
// - lastUpdated is 120000ms (2 mins) ago, exceeding cacheUpdateTime of 60000ms (1 min)
// - In this setup, spocs are expired and placeholders should be visible
clock = sinon.useFakeTimers();
dispatch = sinon.spy();
baseProps = {
store: { getState: () => {} },
App: { initialized: true },
Prefs: { values: {} },
Sections: [],
Weather: {},
document: {
visibilityState: "visible",
addEventListener: sinon.stub(),
removeEventListener: sinon.stub(),
},
};
const props = {
...baseProps,
dispatch,
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: true, loaded: false },
lastUpdated: Date.now() - 120000, // Expired (120s ago)
cacheUpdateTime: 60000, // Cache expires after 60s
},
},
};
wrapper = shallow();
instance = wrapper.instance();
instance.setState({ visible: true });
});
afterEach(() => {
clock.restore();
});
it("should start tracking when placeholders become visible", () => {
const prevProps = {
...baseProps,
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: true, loaded: false },
lastUpdated: Date.now() - 30000,
cacheUpdateTime: 60000,
},
},
};
clock.tick(1000);
instance.trackSpocPlaceholderDuration(prevProps);
assert.isNotNull(instance.spocPlaceholderStartTime);
});
it("should record duration when placeholders are replaced", () => {
// Create a fresh wrapper with expired spocs
const freshDispatch = sinon.spy();
const expiredTime = Date.now() - 120000;
const freshWrapper = shallow(
);
const freshInstance = freshWrapper.instance();
freshInstance.setState({ visible: true });
// Advance clock a bit first so startTime is not 0 (which is falsy)
clock.tick(100);
// Set start time and advance clock
const startTime = Date.now();
freshInstance.spocPlaceholderStartTime = startTime;
clock.tick(150);
// Update to fresh spocs - this triggers componentDidUpdate
// which automatically calls trackSpocPlaceholderDuration
freshWrapper.setProps({
...baseProps,
dispatch: freshDispatch,
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: true, loaded: false },
lastUpdated: Date.now(),
cacheUpdateTime: 60000,
},
},
});
// componentDidUpdate should have dispatched the placeholder duration action
const placeholderCall = freshDispatch
.getCalls()
.find(
call =>
call.args[0].type === "DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION"
);
assert.isNotNull(
placeholderCall,
"Placeholder duration action should be dispatched"
);
const [action] = placeholderCall.args;
assert.equal(action.data.duration, 150);
assert.deepEqual(action.meta, {
from: "ActivityStream:Content",
to: "ActivityStream:Main",
skipLocal: true,
});
assert.isNull(freshInstance.spocPlaceholderStartTime);
});
it("should start tracking on onVisible if placeholders already expired", () => {
wrapper.setProps({
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: true, loaded: false },
lastUpdated: Date.now() - 120000,
cacheUpdateTime: 60000,
},
},
});
instance.setState({ visible: false });
instance.spocPlaceholderStartTime = null;
instance.onVisible();
assert.isNotNull(instance.spocPlaceholderStartTime);
});
it("should not start tracking if tab is not visible", () => {
instance.setState({ visible: false });
instance.spocPlaceholderStartTime = null;
const prevProps = {
...baseProps,
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: true, loaded: false },
lastUpdated: Date.now() - 30000,
cacheUpdateTime: 60000,
},
},
};
instance.trackSpocPlaceholderDuration(prevProps);
assert.isNull(instance.spocPlaceholderStartTime);
});
it("should not start tracking if onDemand is disabled", () => {
// Reset instance to have onDemand disabled from the start
const props = {
...baseProps,
dispatch,
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: false, loaded: false },
lastUpdated: Date.now() - 120000,
cacheUpdateTime: 60000,
},
},
};
wrapper = shallow();
instance = wrapper.instance();
instance.setState({ visible: true });
instance.spocPlaceholderStartTime = null;
const prevProps = {
...baseProps,
DiscoveryStream: {
config: { enabled: true },
spocs: {
onDemand: { enabled: false, loaded: false },
lastUpdated: Date.now() - 120000,
cacheUpdateTime: 60000,
},
},
};
instance.trackSpocPlaceholderDuration(prevProps);
assert.isNull(instance.spocPlaceholderStartTime);
});
});
});