MultiSelect.test.jsx (13669B)
1 import React from "react"; 2 import { mount } from "enzyme"; 3 import { MultiSelect } from "content-src/components/MultiSelect"; 4 5 describe("MultiSelect component", () => { 6 let sandbox; 7 let MULTISELECT_SCREEN_PROPS; 8 let setScreenMultiSelects; 9 let setActiveMultiSelect; 10 beforeEach(() => { 11 sandbox = sinon.createSandbox(); 12 setScreenMultiSelects = sandbox.stub(); 13 setActiveMultiSelect = sandbox.stub(); 14 MULTISELECT_SCREEN_PROPS = { 15 id: "multiselect-screen", 16 content: { 17 position: "split", 18 split_narrow_bkg_position: "-60px", 19 image_alt_text: { 20 string_id: "mr2022-onboarding-default-image-alt", 21 }, 22 background: 23 "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", 24 progress_bar: true, 25 logo: {}, 26 title: "Test Title", 27 tiles: { 28 type: "multiselect", 29 label: "Test Subtitle", 30 data: [ 31 { 32 id: "checkbox-1", 33 defaultValue: true, 34 label: { 35 string_id: "mr2022-onboarding-set-default-primary-button-label", 36 }, 37 action: { 38 type: "SET_DEFAULT_BROWSER", 39 }, 40 }, 41 { 42 id: "checkbox-2", 43 defaultValue: true, 44 label: "Test Checkbox 2", 45 action: { 46 type: "SHOW_MIGRATION_WIZARD", 47 data: {}, 48 }, 49 }, 50 { 51 id: "checkbox-3", 52 defaultValue: false, 53 label: "Test Checkbox 3", 54 action: { 55 type: "SHOW_MIGRATION_WIZARD", 56 data: {}, 57 }, 58 }, 59 ], 60 }, 61 primary_button: { 62 label: "Save and Continue", 63 action: { 64 type: "MULTI_ACTION", 65 collectSelect: true, 66 navigate: true, 67 data: { actions: [] }, 68 }, 69 }, 70 secondary_button: { 71 label: "Skip", 72 action: { 73 navigate: true, 74 }, 75 has_arrow_icon: true, 76 }, 77 }, 78 setScreenMultiSelects, 79 setActiveMultiSelect, 80 }; 81 }); 82 afterEach(() => { 83 sandbox.restore(); 84 }); 85 86 it("should call setScreenMultiSelects with all ids of checkboxes", () => { 87 mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 88 89 assert.calledOnce(setScreenMultiSelects); 90 assert.calledWith(setScreenMultiSelects, [ 91 "checkbox-1", 92 "checkbox-2", 93 "checkbox-3", 94 ]); 95 }); 96 97 it("should not call setScreenMultiSelects if it's already set", () => { 98 let map = sandbox 99 .stub() 100 .returns(MULTISELECT_SCREEN_PROPS.content.tiles.data); 101 102 mount( 103 <MultiSelect screenMultiSelects={{ map }} {...MULTISELECT_SCREEN_PROPS} /> 104 ); 105 106 assert.notCalled(setScreenMultiSelects); 107 assert.calledOnce(map); 108 assert.calledWith(map, sinon.match.func); 109 }); 110 111 it("should call setActiveMultiSelect with ids of checkboxes with defaultValue true", () => { 112 const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 113 114 wrapper.setProps({ activeMultiSelect: null }); 115 assert.calledOnce(setActiveMultiSelect); 116 assert.calledWith(setActiveMultiSelect, ["checkbox-1", "checkbox-2"]); 117 }); 118 119 it("should use activeMultiSelect ids to set checked state for respective checkbox", () => { 120 const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 121 122 wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); 123 const checkBoxes = wrapper.find(".checkbox-container input"); 124 assert.strictEqual(checkBoxes.length, 3); 125 126 assert.strictEqual(checkBoxes.first().props().checked, true); 127 assert.strictEqual(checkBoxes.at(1).props().checked, true); 128 assert.strictEqual(checkBoxes.last().props().checked, false); 129 }); 130 131 it("cover the randomize property", async () => { 132 MULTISELECT_SCREEN_PROPS.content.tiles.data.forEach( 133 item => (item.randomize = true) 134 ); 135 136 const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 137 138 const checkBoxes = wrapper.find(".checkbox-container input"); 139 assert.strictEqual(checkBoxes.length, 3); 140 141 // We don't want to actually test the randomization, just that it doesn't 142 // throw. We _could_ render the component until we get a different order, 143 // and that should work the vast majority of the time, but it's 144 // theoretically possible that we get the same order over and over again 145 // until we hit the 2 second timeout. That would be an extremely low failure 146 // rate, but we already know Math.random() works, so we don't really need to 147 // test it anyway. It's not worth the added risk of false failures. 148 }); 149 150 it("should filter out id when checkbox is unchecked", () => { 151 const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 152 wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); 153 154 const ckbx1 = wrapper.find(".checkbox-container input").at(0); 155 assert.strictEqual(ckbx1.prop("value"), "checkbox-1"); 156 ckbx1.getDOMNode().checked = false; 157 ckbx1.simulate("change"); 158 assert.calledWith(setActiveMultiSelect, ["checkbox-2"]); 159 }); 160 161 it("should add id when checkbox is checked", () => { 162 const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 163 wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); 164 165 const ckbx3 = wrapper.find(".checkbox-container input").at(2); 166 assert.strictEqual(ckbx3.prop("value"), "checkbox-3"); 167 ckbx3.getDOMNode().checked = true; 168 ckbx3.simulate("change"); 169 assert.calledWith(setActiveMultiSelect, [ 170 "checkbox-1", 171 "checkbox-2", 172 "checkbox-3", 173 ]); 174 }); 175 176 it("should render radios and checkboxes with correct styles", async () => { 177 const SCREEN_PROPS = { ...MULTISELECT_SCREEN_PROPS }; 178 SCREEN_PROPS.content.tiles.style = { flexDirection: "row", gap: "24px" }; 179 SCREEN_PROPS.content.tiles.data = [ 180 { 181 id: "checkbox-1", 182 defaultValue: true, 183 label: { raw: "Test1" }, 184 action: { type: "OPEN_PROTECTION_REPORT" }, 185 style: { color: "red" }, 186 icon: { style: { color: "blue" } }, 187 }, 188 { 189 id: "radio-1", 190 type: "radio", 191 group: "radios", 192 defaultValue: true, 193 label: { raw: "Test3" }, 194 action: { type: "OPEN_PROTECTION_REPORT" }, 195 style: { color: "purple" }, 196 icon: { style: { color: "yellow" } }, 197 }, 198 ]; 199 const wrapper = mount(<MultiSelect {...SCREEN_PROPS} />); 200 201 // wait for effect hook 202 await new Promise(resolve => queueMicrotask(resolve)); 203 // activeMultiSelect was called on effect hook with default values 204 assert.calledWith(setActiveMultiSelect, ["checkbox-1", "radio-1"]); 205 206 const container = wrapper.find(".multi-select-container"); 207 assert.strictEqual(container.prop("style").flexDirection, "row"); 208 assert.strictEqual(container.prop("style").gap, "24px"); 209 210 // checkboxes/radios are rendered with correct styles 211 const checkBoxes = wrapper.find(".checkbox-container"); 212 assert.strictEqual(checkBoxes.length, 2); 213 assert.strictEqual(checkBoxes.first().prop("style").color, "red"); 214 assert.strictEqual(checkBoxes.at(1).prop("style").color, "purple"); 215 216 const checks = wrapper.find(".checkbox-container input"); 217 assert.strictEqual(checks.length, 2); 218 assert.strictEqual(checks.first().prop("style").color, "blue"); 219 assert.strictEqual(checks.at(1).prop("style").color, "yellow"); 220 }); 221 222 it("should render picker elements when multiSelectItemDesign is 'picker'", () => { 223 const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; 224 PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; 225 PICKER_PROPS.content.tiles.data = [ 226 { 227 id: "picker-option-1", 228 defaultValue: true, 229 label: "Picker Option 1", 230 pickerEmoji: "🙃", 231 pickerEmojiBackgroundColor: "#c3e0ff", 232 }, 233 { 234 id: "picker-option-2", 235 defaultValue: false, 236 label: "Picker Option 2", 237 pickerEmoji: "✨", 238 pickerEmojiBackgroundColor: "#ffebcc", 239 }, 240 ]; 241 242 const wrapper = mount(<MultiSelect {...PICKER_PROPS} />); 243 wrapper.setProps({ activeMultiSelect: ["picker-option-1"] }); 244 245 // Container should have picker class 246 const container = wrapper.find(".multi-select-container"); 247 assert.strictEqual(container.hasClass("picker"), true); 248 249 const pickerIcons = wrapper.find(".picker-icon"); 250 assert.strictEqual(pickerIcons.length, 2); 251 252 // First icon should be checked (no emoji, no background color) 253 const firstIcon = pickerIcons.at(0); 254 assert.strictEqual(firstIcon.hasClass("picker-checked"), true); 255 assert.strictEqual(firstIcon.text(), ""); 256 assert.strictEqual(firstIcon.prop("style").backgroundColor, undefined); 257 258 // Second icon should not be checked (should have emoji and background color) 259 const secondIcon = pickerIcons.at(1); 260 assert.strictEqual(secondIcon.hasClass("picker-checked"), false); 261 assert.strictEqual(secondIcon.text(), "✨"); 262 assert.strictEqual(secondIcon.prop("style").backgroundColor, "#ffebcc"); 263 }); 264 265 // The picker design adds functionality for checkbox to be checked 266 // even when click events occur on the container itself, instead of just 267 // the label or input 268 it("should handle click events for picker design", () => { 269 const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; 270 PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; 271 PICKER_PROPS.content.tiles.data = [ 272 { 273 id: "picker-option-1", 274 defaultValue: true, 275 label: "Picker Option 1", 276 pickerEmoji: "🙃", 277 }, 278 { 279 id: "picker-option-2", 280 defaultValue: false, 281 label: "Picker Option 2", 282 pickerEmoji: "✨", 283 }, 284 ]; 285 286 const wrapper = mount(<MultiSelect {...PICKER_PROPS} />); 287 wrapper.setProps({ activeMultiSelect: ["picker-option-1"] }); 288 289 // check the container of the second item 290 const checkboxContainers = wrapper.find(".checkbox-container"); 291 const secondContainer = checkboxContainers.at(1); 292 secondContainer.simulate("click"); 293 294 // setActiveMultiSelect should be called with both ids 295 assert.calledWith(setActiveMultiSelect, [ 296 "picker-option-1", 297 "picker-option-2", 298 ]); 299 300 // uncheck the first item 301 const firstContainer = checkboxContainers.at(0); 302 firstContainer.simulate("click"); 303 304 // setActiveMultiSelect should be called with just the second id 305 assert.calledWith(setActiveMultiSelect, ["picker-option-2"]); 306 }); 307 308 it("should handle keyboard events for picker design", () => { 309 const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; 310 PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; 311 PICKER_PROPS.content.tiles.data = [ 312 { 313 id: "picker-option-1", 314 defaultValue: false, 315 label: "Picker Option 1", 316 }, 317 ]; 318 319 const wrapper = mount(<MultiSelect {...PICKER_PROPS} />); 320 wrapper.setProps({ activeMultiSelect: [] }); 321 322 const checkboxContainer = wrapper.find(".checkbox-container").first(); 323 324 // Test spacebar press 325 checkboxContainer.simulate("keydown", { 326 key: " ", 327 }); 328 assert.calledWith(setActiveMultiSelect, ["picker-option-1"]); 329 330 // Test Enter press 331 checkboxContainer.simulate("keydown", { 332 key: "Enter", 333 }); 334 assert.calledWith(setActiveMultiSelect, []); 335 336 // Test other key press 337 setActiveMultiSelect.reset(); 338 checkboxContainer.simulate("keydown", { 339 key: "Tab", 340 }); 341 assert.notCalled(setActiveMultiSelect); 342 }); 343 344 it("should not use handleCheckboxContainerInteraction when multiSelectItemDesign is not 'picker'", () => { 345 const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); 346 wrapper.setProps({ activeMultiSelect: ["checkbox-1"] }); 347 348 const checkboxContainer = wrapper.find(".checkbox-container").first(); 349 350 assert.strictEqual(checkboxContainer.prop("tabIndex"), null); 351 assert.strictEqual(checkboxContainer.prop("onClick"), null); 352 assert.strictEqual(checkboxContainer.prop("onKeyDown"), null); 353 // Likewise, the extra accessibility attributes should not be present on the container 354 assert.strictEqual(checkboxContainer.prop("role"), null); 355 assert.strictEqual(checkboxContainer.prop("aria-checked"), null); 356 }); 357 358 it("should set proper accessibility attributes for picker design when multiSelectItemDesign is 'picker' ", () => { 359 const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; 360 PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; 361 PICKER_PROPS.content.tiles.data = [ 362 { 363 id: "picker-option-1", 364 defaultValue: true, 365 label: "Picker Option 1", 366 }, 367 ]; 368 369 const wrapper = mount(<MultiSelect {...PICKER_PROPS} />); 370 wrapper.setProps({ activeMultiSelect: ["picker-option-1"] }); 371 372 const checkboxContainer = wrapper.find(".checkbox-container").first(); 373 374 // the checkbox-container should have appropriate accessibility attributes 375 assert.strictEqual(checkboxContainer.prop("tabIndex"), "0"); 376 assert.strictEqual(checkboxContainer.prop("role"), "checkbox"); 377 assert.strictEqual(checkboxContainer.prop("aria-checked"), true); 378 379 // the actual (hidden) checkbox should have tabIndex="-1" (to avoid double focus) 380 const checkbox = wrapper.find("input[type='checkbox']").first(); 381 assert.strictEqual(checkbox.prop("tabIndex"), "-1"); 382 }); 383 });