CardGrid.test.jsx (8305B)
1 import { 2 _CardGrid as CardGrid, 3 // eslint-disable-next-line no-shadow 4 IntersectionObserver, 5 } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid"; 6 import { combineReducers, createStore } from "redux"; 7 import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs"; 8 import { Provider } from "react-redux"; 9 import { DSCard } from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard"; 10 import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget"; 11 import React from "react"; 12 import { shallow, mount } from "enzyme"; 13 14 // Wrap this around any component that uses useSelector, 15 // or any mount that uses a child that uses redux. 16 function WrapWithProvider({ children, state = INITIAL_STATE }) { 17 let store = createStore(combineReducers(reducers), state); 18 return <Provider store={store}>{children}</Provider>; 19 } 20 21 describe("<CardGrid>", () => { 22 let wrapper; 23 24 beforeEach(() => { 25 wrapper = shallow( 26 <CardGrid 27 Prefs={INITIAL_STATE.Prefs} 28 DiscoveryStream={INITIAL_STATE.DiscoveryStream} 29 /> 30 ); 31 }); 32 33 it("should render an empty div", () => { 34 assert.ok(wrapper.exists()); 35 assert.lengthOf(wrapper.children(), 0); 36 }); 37 38 it("should render DSCards", () => { 39 wrapper.setProps({ items: 2, data: { recommendations: [{}, {}] } }); 40 41 assert.lengthOf(wrapper.find(".ds-card-grid").children(), 2); 42 assert.equal(wrapper.find(".ds-card-grid").children().at(0).type(), DSCard); 43 }); 44 45 it("should add 4 card classname to card grid", () => { 46 wrapper.setProps({ 47 fourCardLayout: true, 48 data: { recommendations: [{}, {}] }, 49 }); 50 51 assert.ok(wrapper.find(".ds-card-grid-four-card-variant").exists()); 52 }); 53 54 it("should add no description classname to card grid", () => { 55 wrapper.setProps({ 56 hideCardBackground: true, 57 data: { recommendations: [{}, {}] }, 58 }); 59 60 assert.ok(wrapper.find(".ds-card-grid-hide-background").exists()); 61 }); 62 63 it("should add/hide description classname to card grid", () => { 64 wrapper.setProps({ 65 data: { recommendations: [{}, {}] }, 66 }); 67 68 assert.ok(wrapper.find(".ds-card-grid-include-descriptions").exists()); 69 70 wrapper.setProps({ 71 hideDescriptions: true, 72 data: { recommendations: [{}, {}] }, 73 }); 74 75 assert.ok(!wrapper.find(".ds-card-grid-include-descriptions").exists()); 76 }); 77 78 it("should create a widget card", () => { 79 wrapper.setProps({ 80 widgets: { 81 positions: [{ index: 1 }], 82 data: [{ type: "TopicsWidget" }], 83 }, 84 data: { 85 recommendations: [{}, {}, {}], 86 }, 87 }); 88 89 assert.ok(wrapper.find(TopicsWidget).exists()); 90 }); 91 92 it("should render AdBanner if enabled", () => { 93 const commonProps = { 94 ...INITIAL_STATE, 95 items: 2, 96 data: { recommendations: [{}, {}] }, 97 Prefs: { 98 ...INITIAL_STATE.Prefs, 99 values: { 100 ...INITIAL_STATE.Prefs.values, 101 "newtabAdSize.leaderboard": true, 102 "newtabAdSize.billboard": true, 103 }, 104 }, 105 DiscoveryStream: { 106 ...INITIAL_STATE.DiscoveryStream, 107 spocs: { 108 ...INITIAL_STATE.DiscoveryStream.spocs, 109 data: { 110 newtab_spocs: { 111 items: [ 112 { 113 format: "leaderboard", 114 }, 115 ], 116 }, 117 }, 118 }, 119 }, 120 }; 121 122 wrapper = mount( 123 <WrapWithProvider> 124 <CardGrid {...commonProps} /> 125 </WrapWithProvider> 126 ); 127 128 assert.ok(wrapper.find(".ad-banner-wrapper").exists()); 129 }); 130 131 describe("Keyboard navigation", () => { 132 beforeEach(() => { 133 const commonProps = { 134 items: 3, 135 data: { 136 recommendations: [{}, {}, {}], 137 }, 138 Prefs: INITIAL_STATE.Prefs, 139 DiscoveryStream: INITIAL_STATE.DiscoveryStream, 140 }; 141 142 wrapper = mount( 143 <WrapWithProvider> 144 <CardGrid {...commonProps} /> 145 </WrapWithProvider> 146 ); 147 }); 148 149 afterEach(() => { 150 wrapper.unmount(); 151 }); 152 153 it("should pass tabIndex={0} to the first card and tabIndex={-1} to other cards", () => { 154 const firstCard = wrapper.find(DSCard).at(0); 155 const secondCard = wrapper.find(DSCard).at(1); 156 const thirdCard = wrapper.find(DSCard).at(2); 157 158 assert.equal(firstCard.prop("tabIndex"), 0); 159 assert.equal(secondCard.prop("tabIndex"), -1); 160 assert.equal(thirdCard.prop("tabIndex"), -1); 161 }); 162 163 it("should update focused index when onFocus is called", () => { 164 const secondCard = wrapper.find(DSCard).at(1); 165 const onFocus = secondCard.prop("onFocus"); 166 167 onFocus(); 168 wrapper.update(); 169 170 assert.equal(wrapper.find(DSCard).at(1).prop("tabIndex"), 0); 171 assert.equal(wrapper.find(DSCard).at(0).prop("tabIndex"), -1); 172 }); 173 174 describe("handleCardKeyDown", () => { 175 let sandbox; 176 let grid; 177 let mockLink; 178 let mockTargetCard; 179 let mockCurrentCard; 180 let mockEvent; 181 182 beforeEach(() => { 183 sandbox = sinon.createSandbox(); 184 grid = wrapper.find(".ds-card-grid"); 185 186 mockLink = { focus: sandbox.spy() }; 187 mockTargetCard = { 188 matches: sandbox.stub().returns(true), 189 querySelector: sandbox.stub().returns(mockLink), 190 }; 191 mockCurrentCard = {}; 192 mockEvent = { 193 preventDefault: sandbox.spy(), 194 target: { 195 closest: sandbox.stub().returns(mockCurrentCard), 196 }, 197 }; 198 }); 199 200 afterEach(() => { 201 sandbox.restore(); 202 }); 203 204 it("should navigate to next card with ArrowRight", () => { 205 mockEvent.key = "ArrowRight"; 206 mockCurrentCard.nextElementSibling = mockTargetCard; 207 208 grid.prop("onKeyDown")(mockEvent); 209 210 assert.calledOnce(mockEvent.preventDefault); 211 assert.calledOnce(mockTargetCard.querySelector); 212 assert.calledWith(mockTargetCard.querySelector, "a.ds-card-link"); 213 assert.calledOnce(mockLink.focus); 214 }); 215 216 it("should navigate to previous card with ArrowLeft", () => { 217 mockEvent.key = "ArrowLeft"; 218 mockCurrentCard.previousElementSibling = mockTargetCard; 219 220 grid.prop("onKeyDown")(mockEvent); 221 222 assert.calledOnce(mockEvent.preventDefault); 223 assert.calledOnce(mockTargetCard.querySelector); 224 assert.calledWith(mockTargetCard.querySelector, "a.ds-card-link"); 225 assert.calledOnce(mockLink.focus); 226 }); 227 228 it("should return early if no current card found", () => { 229 mockEvent.key = "ArrowRight"; 230 mockEvent.target.closest.returns(null); 231 232 grid.prop("onKeyDown")(mockEvent); 233 234 assert.calledOnce(mockEvent.preventDefault); 235 assert.notCalled(mockTargetCard.querySelector); 236 }); 237 238 it("should handle case where no matching sibling card is found", () => { 239 mockEvent.key = "ArrowRight"; 240 mockCurrentCard.nextElementSibling = { 241 matches: sandbox.stub().returns(false), 242 }; 243 244 grid.prop("onKeyDown")(mockEvent); 245 246 assert.calledOnce(mockEvent.preventDefault); 247 assert.notCalled(mockLink.focus); 248 }); 249 }); 250 }); 251 }); 252 253 // Build IntersectionObserver class with the arg `entries` for the intersect callback. 254 function buildIntersectionObserver(entries) { 255 return class { 256 constructor(callback) { 257 this.callback = callback; 258 } 259 260 observe() { 261 this.callback(entries); 262 } 263 264 unobserve() {} 265 266 disconnect() {} 267 }; 268 } 269 270 describe("<IntersectionObserver>", () => { 271 let wrapper; 272 let fakeWindow; 273 let intersectEntries; 274 275 beforeEach(() => { 276 intersectEntries = [{ isIntersecting: true }]; 277 fakeWindow = { 278 IntersectionObserver: buildIntersectionObserver(intersectEntries), 279 }; 280 wrapper = mount(<IntersectionObserver windowObj={fakeWindow} />); 281 }); 282 283 it("should render an empty div", () => { 284 assert.ok(wrapper.exists()); 285 assert.equal(wrapper.children().at(0).type(), "div"); 286 }); 287 288 it("should fire onIntersecting", () => { 289 const onIntersecting = sinon.stub(); 290 wrapper = mount( 291 <IntersectionObserver 292 windowObj={fakeWindow} 293 onIntersecting={onIntersecting} 294 /> 295 ); 296 assert.calledOnce(onIntersecting); 297 }); 298 });