Base.test.jsx (13625B)
1 import { 2 _Base as Base, 3 BaseContent, 4 WithDsAdmin, 5 } from "content-src/components/Base/Base"; 6 import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin"; 7 import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary"; 8 import React from "react"; 9 import { Search } from "content-src/components/Search/Search"; 10 import { shallow } from "enzyme"; 11 import { actionCreators as ac } from "common/Actions.mjs"; 12 13 describe("<Base>", () => { 14 let DEFAULT_PROPS = { 15 store: { getState: () => {} }, 16 App: { initialized: true }, 17 Prefs: { values: {} }, 18 Sections: [], 19 DiscoveryStream: { config: { enabled: false } }, 20 dispatch: () => {}, 21 adminContent: { 22 message: {}, 23 }, 24 document: { 25 visibilityState: "visible", 26 addEventListener: sinon.stub(), 27 removeEventListener: sinon.stub(), 28 }, 29 }; 30 31 it("should render Base component", () => { 32 const wrapper = shallow(<Base {...DEFAULT_PROPS} />); 33 assert.ok(wrapper.exists()); 34 }); 35 36 it("should render the BaseContent component, passing through all props", () => { 37 const wrapper = shallow(<Base {...DEFAULT_PROPS} />); 38 const props = wrapper.find(BaseContent).props(); 39 assert.deepEqual( 40 props, 41 DEFAULT_PROPS, 42 JSON.stringify([props, DEFAULT_PROPS], null, 3) 43 ); 44 }); 45 46 it("should render an ErrorBoundary with class base-content-fallback", () => { 47 const wrapper = shallow(<Base {...DEFAULT_PROPS} />); 48 49 assert.equal( 50 wrapper.find(ErrorBoundary).first().prop("className"), 51 "base-content-fallback" 52 ); 53 }); 54 55 it("should render an WithDsAdmin if the devtools pref is true", () => { 56 const wrapper = shallow( 57 <Base 58 {...DEFAULT_PROPS} 59 Prefs={{ values: { "asrouter.devtoolsEnabled": true } }} 60 /> 61 ); 62 assert.lengthOf(wrapper.find(WithDsAdmin), 1); 63 }); 64 65 it("should not render an WithDsAdmin if the devtools pref is false", () => { 66 const wrapper = shallow( 67 <Base 68 {...DEFAULT_PROPS} 69 Prefs={{ values: { "asrouter.devtoolsEnabled": false } }} 70 /> 71 ); 72 assert.lengthOf(wrapper.find(WithDsAdmin), 0); 73 }); 74 }); 75 76 describe("<BaseContent>", () => { 77 let DEFAULT_PROPS = { 78 store: { getState: () => {} }, 79 App: { initialized: true }, 80 Prefs: { values: {} }, 81 Sections: [], 82 DiscoveryStream: { config: { enabled: false }, spocs: {} }, 83 Weather: {}, 84 dispatch: () => {}, 85 document: { 86 visibilityState: "visible", 87 addEventListener: sinon.stub(), 88 removeEventListener: sinon.stub(), 89 }, 90 }; 91 92 it("should render an ErrorBoundary with a Search child", () => { 93 const searchEnabledProps = Object.assign({}, DEFAULT_PROPS, { 94 Prefs: { values: { showSearch: true } }, 95 }); 96 97 const wrapper = shallow(<BaseContent {...searchEnabledProps} />); 98 99 assert.isTrue(wrapper.find(Search).parent().is(ErrorBoundary)); 100 }); 101 102 it("should dispatch a user event when the customize menu is opened or closed", () => { 103 const dispatch = sinon.stub(); 104 const wrapper = shallow( 105 <BaseContent 106 {...DEFAULT_PROPS} 107 dispatch={dispatch} 108 App={{ customizeMenuVisible: true }} 109 /> 110 ); 111 wrapper.instance().openCustomizationMenu(); 112 assert.calledWith(dispatch, { type: "SHOW_PERSONALIZE" }); 113 assert.calledWith(dispatch, ac.UserEvent({ event: "SHOW_PERSONALIZE" })); 114 wrapper.instance().closeCustomizationMenu(); 115 assert.calledWith(dispatch, { type: "HIDE_PERSONALIZE" }); 116 assert.calledWith(dispatch, ac.UserEvent({ event: "HIDE_PERSONALIZE" })); 117 }); 118 119 it("should render only search if no Sections are enabled", () => { 120 const onlySearchProps = Object.assign({}, DEFAULT_PROPS, { 121 Sections: [{ id: "highlights", enabled: false }], 122 Prefs: { values: { showSearch: true } }, 123 }); 124 125 const wrapper = shallow(<BaseContent {...onlySearchProps} />); 126 assert.lengthOf(wrapper.find(".only-search"), 1); 127 }); 128 129 it("should update firstVisibleTimestamp if it is visible immediately with no event listener", () => { 130 const props = Object.assign({}, DEFAULT_PROPS, { 131 document: { 132 visibilityState: "visible", 133 addEventListener: sinon.spy(), 134 removeEventListener: sinon.spy(), 135 }, 136 }); 137 138 const wrapper = shallow(<BaseContent {...props} />); 139 assert.notCalled(props.document.addEventListener); 140 assert.isDefined(wrapper.state("firstVisibleTimestamp")); 141 }); 142 it("should attach an event listener for visibility change if it is not visible", () => { 143 const props = Object.assign({}, DEFAULT_PROPS, { 144 document: { 145 visibilityState: "hidden", 146 addEventListener: sinon.spy(), 147 removeEventListener: sinon.spy(), 148 }, 149 }); 150 151 const wrapper = shallow(<BaseContent {...props} />); 152 assert.calledWith(props.document.addEventListener, "visibilitychange"); 153 assert.notExists(wrapper.state("firstVisibleTimestamp")); 154 }); 155 it("should remove the event listener for visibility change when unmounted", () => { 156 const props = Object.assign({}, DEFAULT_PROPS, { 157 document: { 158 visibilityState: "hidden", 159 addEventListener: sinon.spy(), 160 removeEventListener: sinon.spy(), 161 }, 162 }); 163 164 const wrapper = shallow(<BaseContent {...props} />); 165 const [, listener] = props.document.addEventListener.firstCall.args; 166 167 wrapper.unmount(); 168 assert.calledWith( 169 props.document.removeEventListener, 170 "visibilitychange", 171 listener 172 ); 173 }); 174 it("should remove the event listener for visibility change after becoming visible", () => { 175 const listeners = new Set(); 176 const props = Object.assign({}, DEFAULT_PROPS, { 177 document: { 178 visibilityState: "hidden", 179 addEventListener: (ev, cb) => listeners.add(cb), 180 removeEventListener: (ev, cb) => listeners.delete(cb), 181 }, 182 }); 183 184 const wrapper = shallow(<BaseContent {...props} />); 185 assert.equal(listeners.size, 1); 186 assert.notExists(wrapper.state("firstVisibleTimestamp")); 187 188 // Simulate listeners getting called 189 props.document.visibilityState = "visible"; 190 listeners.forEach(l => l()); 191 192 assert.equal(listeners.size, 0); 193 assert.isDefined(wrapper.state("firstVisibleTimestamp")); 194 }); 195 }); 196 197 describe("WithDsAdmin", () => { 198 describe("rendering inner content", () => { 199 it("should not set devtoolsCollapsed state for about:newtab (no hash)", () => { 200 const wrapper = shallow(<WithDsAdmin hash="" />); 201 assert.isTrue( 202 wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed") 203 ); 204 assert.lengthOf(wrapper.find(BaseContent), 1); 205 }); 206 207 it("should set devtoolsCollapsed state for about:newtab#devtools", () => { 208 const wrapper = shallow(<WithDsAdmin hash="#devtools" />); 209 assert.isFalse( 210 wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed") 211 ); 212 assert.lengthOf(wrapper.find(BaseContent), 0); 213 }); 214 215 it("should set devtoolsCollapsed state for about:newtab#devtools subroutes", () => { 216 const wrapper = shallow(<WithDsAdmin hash="#devtools-foo" />); 217 assert.isFalse( 218 wrapper.find(DiscoveryStreamAdmin).prop("devtoolsCollapsed") 219 ); 220 assert.lengthOf(wrapper.find(BaseContent), 0); 221 }); 222 }); 223 224 describe("SPOC Placeholder Duration Tracking", () => { 225 let wrapper; 226 let instance; 227 let dispatch; 228 let clock; 229 let baseProps; 230 231 beforeEach(() => { 232 // Setup: Create a component with expired spocs (showing placeholders) 233 // - useFakeTimers allows us to control time for duration testing 234 // - lastUpdated is 120000ms (2 mins) ago, exceeding cacheUpdateTime of 60000ms (1 min) 235 // - In this setup, spocs are expired and placeholders should be visible 236 clock = sinon.useFakeTimers(); 237 dispatch = sinon.spy(); 238 baseProps = { 239 store: { getState: () => {} }, 240 App: { initialized: true }, 241 Prefs: { values: {} }, 242 Sections: [], 243 Weather: {}, 244 document: { 245 visibilityState: "visible", 246 addEventListener: sinon.stub(), 247 removeEventListener: sinon.stub(), 248 }, 249 }; 250 const props = { 251 ...baseProps, 252 dispatch, 253 DiscoveryStream: { 254 config: { enabled: true }, 255 spocs: { 256 onDemand: { enabled: true, loaded: false }, 257 lastUpdated: Date.now() - 120000, // Expired (120s ago) 258 cacheUpdateTime: 60000, // Cache expires after 60s 259 }, 260 }, 261 }; 262 wrapper = shallow(<BaseContent {...props} />); 263 instance = wrapper.instance(); 264 instance.setState({ visible: true }); 265 }); 266 267 afterEach(() => { 268 clock.restore(); 269 }); 270 271 it("should start tracking when placeholders become visible", () => { 272 const prevProps = { 273 ...baseProps, 274 DiscoveryStream: { 275 config: { enabled: true }, 276 spocs: { 277 onDemand: { enabled: true, loaded: false }, 278 lastUpdated: Date.now() - 30000, 279 cacheUpdateTime: 60000, 280 }, 281 }, 282 }; 283 284 clock.tick(1000); 285 instance.trackSpocPlaceholderDuration(prevProps); 286 287 assert.isNotNull(instance.spocPlaceholderStartTime); 288 }); 289 290 it("should record duration when placeholders are replaced", () => { 291 // Create a fresh wrapper with expired spocs 292 const freshDispatch = sinon.spy(); 293 const expiredTime = Date.now() - 120000; 294 const freshWrapper = shallow( 295 <BaseContent 296 {...baseProps} 297 dispatch={freshDispatch} 298 DiscoveryStream={{ 299 config: { enabled: true }, 300 spocs: { 301 onDemand: { enabled: true, loaded: false }, 302 lastUpdated: expiredTime, 303 cacheUpdateTime: 60000, 304 }, 305 }} 306 /> 307 ); 308 const freshInstance = freshWrapper.instance(); 309 freshInstance.setState({ visible: true }); 310 311 // Advance clock a bit first so startTime is not 0 (which is falsy) 312 clock.tick(100); 313 314 // Set start time and advance clock 315 const startTime = Date.now(); 316 freshInstance.spocPlaceholderStartTime = startTime; 317 clock.tick(150); 318 319 // Update to fresh spocs - this triggers componentDidUpdate 320 // which automatically calls trackSpocPlaceholderDuration 321 freshWrapper.setProps({ 322 ...baseProps, 323 dispatch: freshDispatch, 324 DiscoveryStream: { 325 config: { enabled: true }, 326 spocs: { 327 onDemand: { enabled: true, loaded: false }, 328 lastUpdated: Date.now(), 329 cacheUpdateTime: 60000, 330 }, 331 }, 332 }); 333 334 // componentDidUpdate should have dispatched the placeholder duration action 335 const placeholderCall = freshDispatch 336 .getCalls() 337 .find( 338 call => 339 call.args[0].type === "DISCOVERY_STREAM_SPOC_PLACEHOLDER_DURATION" 340 ); 341 342 assert.isNotNull( 343 placeholderCall, 344 "Placeholder duration action should be dispatched" 345 ); 346 const [action] = placeholderCall.args; 347 assert.equal(action.data.duration, 150); 348 assert.deepEqual(action.meta, { 349 from: "ActivityStream:Content", 350 to: "ActivityStream:Main", 351 skipLocal: true, 352 }); 353 354 assert.isNull(freshInstance.spocPlaceholderStartTime); 355 }); 356 357 it("should start tracking on onVisible if placeholders already expired", () => { 358 wrapper.setProps({ 359 DiscoveryStream: { 360 config: { enabled: true }, 361 spocs: { 362 onDemand: { enabled: true, loaded: false }, 363 lastUpdated: Date.now() - 120000, 364 cacheUpdateTime: 60000, 365 }, 366 }, 367 }); 368 369 instance.setState({ visible: false }); 370 instance.spocPlaceholderStartTime = null; 371 372 instance.onVisible(); 373 374 assert.isNotNull(instance.spocPlaceholderStartTime); 375 }); 376 377 it("should not start tracking if tab is not visible", () => { 378 instance.setState({ visible: false }); 379 instance.spocPlaceholderStartTime = null; 380 381 const prevProps = { 382 ...baseProps, 383 DiscoveryStream: { 384 config: { enabled: true }, 385 spocs: { 386 onDemand: { enabled: true, loaded: false }, 387 lastUpdated: Date.now() - 30000, 388 cacheUpdateTime: 60000, 389 }, 390 }, 391 }; 392 393 instance.trackSpocPlaceholderDuration(prevProps); 394 395 assert.isNull(instance.spocPlaceholderStartTime); 396 }); 397 398 it("should not start tracking if onDemand is disabled", () => { 399 // Reset instance to have onDemand disabled from the start 400 const props = { 401 ...baseProps, 402 dispatch, 403 DiscoveryStream: { 404 config: { enabled: true }, 405 spocs: { 406 onDemand: { enabled: false, loaded: false }, 407 lastUpdated: Date.now() - 120000, 408 cacheUpdateTime: 60000, 409 }, 410 }, 411 }; 412 wrapper = shallow(<BaseContent {...props} />); 413 instance = wrapper.instance(); 414 instance.setState({ visible: true }); 415 instance.spocPlaceholderStartTime = null; 416 417 const prevProps = { 418 ...baseProps, 419 DiscoveryStream: { 420 config: { enabled: true }, 421 spocs: { 422 onDemand: { enabled: false, loaded: false }, 423 lastUpdated: Date.now() - 120000, 424 cacheUpdateTime: 60000, 425 }, 426 }, 427 }; 428 429 instance.trackSpocPlaceholderDuration(prevProps); 430 431 assert.isNull(instance.spocPlaceholderStartTime); 432 }); 433 }); 434 });