import React from "react"; import { mount } from "enzyme"; import { MultiSelect } from "content-src/components/MultiSelect"; describe("MultiSelect component", () => { let sandbox; let MULTISELECT_SCREEN_PROPS; let setScreenMultiSelects; let setActiveMultiSelect; beforeEach(() => { sandbox = sinon.createSandbox(); setScreenMultiSelects = sandbox.stub(); setActiveMultiSelect = sandbox.stub(); MULTISELECT_SCREEN_PROPS = { id: "multiselect-screen", content: { position: "split", split_narrow_bkg_position: "-60px", image_alt_text: { string_id: "mr2022-onboarding-default-image-alt", }, background: "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", progress_bar: true, logo: {}, title: "Test Title", tiles: { type: "multiselect", label: "Test Subtitle", data: [ { id: "checkbox-1", defaultValue: true, label: { string_id: "mr2022-onboarding-set-default-primary-button-label", }, action: { type: "SET_DEFAULT_BROWSER", }, }, { id: "checkbox-2", defaultValue: true, label: "Test Checkbox 2", action: { type: "SHOW_MIGRATION_WIZARD", data: {}, }, }, { id: "checkbox-3", defaultValue: false, label: "Test Checkbox 3", action: { type: "SHOW_MIGRATION_WIZARD", data: {}, }, }, ], }, primary_button: { label: "Save and Continue", action: { type: "MULTI_ACTION", collectSelect: true, navigate: true, data: { actions: [] }, }, }, secondary_button: { label: "Skip", action: { navigate: true, }, has_arrow_icon: true, }, }, setScreenMultiSelects, setActiveMultiSelect, }; }); afterEach(() => { sandbox.restore(); }); it("should call setScreenMultiSelects with all ids of checkboxes", () => { mount(); assert.calledOnce(setScreenMultiSelects); assert.calledWith(setScreenMultiSelects, [ "checkbox-1", "checkbox-2", "checkbox-3", ]); }); it("should not call setScreenMultiSelects if it's already set", () => { let map = sandbox .stub() .returns(MULTISELECT_SCREEN_PROPS.content.tiles.data); mount( ); assert.notCalled(setScreenMultiSelects); assert.calledOnce(map); assert.calledWith(map, sinon.match.func); }); it("should call setActiveMultiSelect with ids of checkboxes with defaultValue true", () => { const wrapper = mount(); wrapper.setProps({ activeMultiSelect: null }); assert.calledOnce(setActiveMultiSelect); assert.calledWith(setActiveMultiSelect, ["checkbox-1", "checkbox-2"]); }); it("should use activeMultiSelect ids to set checked state for respective checkbox", () => { const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); const checkBoxes = wrapper.find(".checkbox-container input"); assert.strictEqual(checkBoxes.length, 3); assert.strictEqual(checkBoxes.first().props().checked, true); assert.strictEqual(checkBoxes.at(1).props().checked, true); assert.strictEqual(checkBoxes.last().props().checked, false); }); it("cover the randomize property", async () => { MULTISELECT_SCREEN_PROPS.content.tiles.data.forEach( item => (item.randomize = true) ); const wrapper = mount(); const checkBoxes = wrapper.find(".checkbox-container input"); assert.strictEqual(checkBoxes.length, 3); // We don't want to actually test the randomization, just that it doesn't // throw. We _could_ render the component until we get a different order, // and that should work the vast majority of the time, but it's // theoretically possible that we get the same order over and over again // until we hit the 2 second timeout. That would be an extremely low failure // rate, but we already know Math.random() works, so we don't really need to // test it anyway. It's not worth the added risk of false failures. }); it("should filter out id when checkbox is unchecked", () => { const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); const ckbx1 = wrapper.find(".checkbox-container input").at(0); assert.strictEqual(ckbx1.prop("value"), "checkbox-1"); ckbx1.getDOMNode().checked = false; ckbx1.simulate("change"); assert.calledWith(setActiveMultiSelect, ["checkbox-2"]); }); it("should add id when checkbox is checked", () => { const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); const ckbx3 = wrapper.find(".checkbox-container input").at(2); assert.strictEqual(ckbx3.prop("value"), "checkbox-3"); ckbx3.getDOMNode().checked = true; ckbx3.simulate("change"); assert.calledWith(setActiveMultiSelect, [ "checkbox-1", "checkbox-2", "checkbox-3", ]); }); it("should render radios and checkboxes with correct styles", async () => { const SCREEN_PROPS = { ...MULTISELECT_SCREEN_PROPS }; SCREEN_PROPS.content.tiles.style = { flexDirection: "row", gap: "24px" }; SCREEN_PROPS.content.tiles.data = [ { id: "checkbox-1", defaultValue: true, label: { raw: "Test1" }, action: { type: "OPEN_PROTECTION_REPORT" }, style: { color: "red" }, icon: { style: { color: "blue" } }, }, { id: "radio-1", type: "radio", group: "radios", defaultValue: true, label: { raw: "Test3" }, action: { type: "OPEN_PROTECTION_REPORT" }, style: { color: "purple" }, icon: { style: { color: "yellow" } }, }, ]; const wrapper = mount(); // wait for effect hook await new Promise(resolve => queueMicrotask(resolve)); // activeMultiSelect was called on effect hook with default values assert.calledWith(setActiveMultiSelect, ["checkbox-1", "radio-1"]); const container = wrapper.find(".multi-select-container"); assert.strictEqual(container.prop("style").flexDirection, "row"); assert.strictEqual(container.prop("style").gap, "24px"); // checkboxes/radios are rendered with correct styles const checkBoxes = wrapper.find(".checkbox-container"); assert.strictEqual(checkBoxes.length, 2); assert.strictEqual(checkBoxes.first().prop("style").color, "red"); assert.strictEqual(checkBoxes.at(1).prop("style").color, "purple"); const checks = wrapper.find(".checkbox-container input"); assert.strictEqual(checks.length, 2); assert.strictEqual(checks.first().prop("style").color, "blue"); assert.strictEqual(checks.at(1).prop("style").color, "yellow"); }); it("should render picker elements when multiSelectItemDesign is 'picker'", () => { const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; PICKER_PROPS.content.tiles.data = [ { id: "picker-option-1", defaultValue: true, label: "Picker Option 1", pickerEmoji: "🙃", pickerEmojiBackgroundColor: "#c3e0ff", }, { id: "picker-option-2", defaultValue: false, label: "Picker Option 2", pickerEmoji: "✨", pickerEmojiBackgroundColor: "#ffebcc", }, ]; const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["picker-option-1"] }); // Container should have picker class const container = wrapper.find(".multi-select-container"); assert.strictEqual(container.hasClass("picker"), true); const pickerIcons = wrapper.find(".picker-icon"); assert.strictEqual(pickerIcons.length, 2); // First icon should be checked (no emoji, no background color) const firstIcon = pickerIcons.at(0); assert.strictEqual(firstIcon.hasClass("picker-checked"), true); assert.strictEqual(firstIcon.text(), ""); assert.strictEqual(firstIcon.prop("style").backgroundColor, undefined); // Second icon should not be checked (should have emoji and background color) const secondIcon = pickerIcons.at(1); assert.strictEqual(secondIcon.hasClass("picker-checked"), false); assert.strictEqual(secondIcon.text(), "✨"); assert.strictEqual(secondIcon.prop("style").backgroundColor, "#ffebcc"); }); // The picker design adds functionality for checkbox to be checked // even when click events occur on the container itself, instead of just // the label or input it("should handle click events for picker design", () => { const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; PICKER_PROPS.content.tiles.data = [ { id: "picker-option-1", defaultValue: true, label: "Picker Option 1", pickerEmoji: "🙃", }, { id: "picker-option-2", defaultValue: false, label: "Picker Option 2", pickerEmoji: "✨", }, ]; const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["picker-option-1"] }); // check the container of the second item const checkboxContainers = wrapper.find(".checkbox-container"); const secondContainer = checkboxContainers.at(1); secondContainer.simulate("click"); // setActiveMultiSelect should be called with both ids assert.calledWith(setActiveMultiSelect, [ "picker-option-1", "picker-option-2", ]); // uncheck the first item const firstContainer = checkboxContainers.at(0); firstContainer.simulate("click"); // setActiveMultiSelect should be called with just the second id assert.calledWith(setActiveMultiSelect, ["picker-option-2"]); }); it("should handle keyboard events for picker design", () => { const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; PICKER_PROPS.content.tiles.data = [ { id: "picker-option-1", defaultValue: false, label: "Picker Option 1", }, ]; const wrapper = mount(); wrapper.setProps({ activeMultiSelect: [] }); const checkboxContainer = wrapper.find(".checkbox-container").first(); // Test spacebar press checkboxContainer.simulate("keydown", { key: " ", }); assert.calledWith(setActiveMultiSelect, ["picker-option-1"]); // Test Enter press checkboxContainer.simulate("keydown", { key: "Enter", }); assert.calledWith(setActiveMultiSelect, []); // Test other key press setActiveMultiSelect.reset(); checkboxContainer.simulate("keydown", { key: "Tab", }); assert.notCalled(setActiveMultiSelect); }); it("should not use handleCheckboxContainerInteraction when multiSelectItemDesign is not 'picker'", () => { const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["checkbox-1"] }); const checkboxContainer = wrapper.find(".checkbox-container").first(); assert.strictEqual(checkboxContainer.prop("tabIndex"), null); assert.strictEqual(checkboxContainer.prop("onClick"), null); assert.strictEqual(checkboxContainer.prop("onKeyDown"), null); // Likewise, the extra accessibility attributes should not be present on the container assert.strictEqual(checkboxContainer.prop("role"), null); assert.strictEqual(checkboxContainer.prop("aria-checked"), null); }); it("should set proper accessibility attributes for picker design when multiSelectItemDesign is 'picker' ", () => { const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS }; PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker"; PICKER_PROPS.content.tiles.data = [ { id: "picker-option-1", defaultValue: true, label: "Picker Option 1", }, ]; const wrapper = mount(); wrapper.setProps({ activeMultiSelect: ["picker-option-1"] }); const checkboxContainer = wrapper.find(".checkbox-container").first(); // the checkbox-container should have appropriate accessibility attributes assert.strictEqual(checkboxContainer.prop("tabIndex"), "0"); assert.strictEqual(checkboxContainer.prop("role"), "checkbox"); assert.strictEqual(checkboxContainer.prop("aria-checked"), true); // the actual (hidden) checkbox should have tabIndex="-1" (to avoid double focus) const checkbox = wrapper.find("input[type='checkbox']").first(); assert.strictEqual(checkbox.prop("tabIndex"), "-1"); }); });