From bceffc51ad185d6a837865614eaf2942423f079d Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Wed, 11 Feb 2026 18:14:59 +0000 Subject: [PATCH 1/8] feat: store sidebar selected option in state --- src/components/Menus/Sidebar/Sidebar.jsx | 43 +++++++++++++++++++----- src/redux/EditorSlice.js | 9 +++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/components/Menus/Sidebar/Sidebar.jsx b/src/components/Menus/Sidebar/Sidebar.jsx index 696e3530e..f222ed4e2 100644 --- a/src/components/Menus/Sidebar/Sidebar.jsx +++ b/src/components/Menus/Sidebar/Sidebar.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import { useMediaQuery } from "react-responsive"; import FilePanel from "./FilePanel/FilePanel"; @@ -23,10 +23,12 @@ import { MOBILE_MEDIA_QUERY } from "../../../utils/mediaQueryBreakpoints"; import FileIcon from "../../../utils/FileIcon"; import DownloadPanel from "./DownloadPanel/DownloadPanel"; import InstructionsPanel from "./InstructionsPanel/InstructionsPanel"; +import { setSidebarOption } from "../../../redux/EditorSlice"; import SidebarPanel from "./SidebarPanel"; const Sidebar = ({ options = [], plugins = [] }) => { const { t } = useTranslation(); + const dispatch = useDispatch(); let menuOptions = [ { @@ -111,6 +113,9 @@ const Sidebar = ({ options = [], plugins = [] }) => { const instructionsEditable = useSelector( (state) => state.editor.instructionsEditable, ); + const storedSidebarOption = useSelector( + (state) => state.editor.sidebarOption, + ); const removeOption = (optionName, depArray = []) => { if ((!depArray || depArray.length === 0) && options.includes(optionName)) { @@ -129,28 +134,50 @@ const Sidebar = ({ options = [], plugins = [] }) => { const autoOpenPlugin = plugins?.find((plugin) => plugin.autoOpen); - const [option, setOption] = useState( - autoOpenPlugin + // Use stored option if it exists and is valid, otherwise use default logic + const resolveSelectedOption = () => { + if ( + storedSidebarOption && + menuOptions.find((opt) => opt.name === storedSidebarOption) + ) { + return storedSidebarOption; + } + // Calculate default + const defaultOption = autoOpenPlugin ? autoOpenPlugin.name : instructionsEditable || instructionsSteps ? "instructions" - : "file", - ); + : "file"; + dispatch(setSidebarOption(defaultOption)); + return defaultOption; + }; + + const [option, setOption] = useState(resolveSelectedOption); const hasInstructions = instructionsSteps && instructionsSteps.length > 0; useEffect(() => { if (!autoOpenPlugin && (instructionsEditable || hasInstructions)) { setOption("instructions"); + dispatch(setSidebarOption("instructions")); } - }, [autoOpenPlugin, instructionsEditable, hasInstructions]); + }, [autoOpenPlugin, instructionsEditable, hasInstructions, dispatch]); const toggleOption = (newOption) => { + let nextOption; if (option !== newOption) { - setOption(newOption); + // Switch to different sidebar panel + nextOption = newOption; + setOption(nextOption); } else if (!isMobile) { - setOption(null); + // Desktop: clicking same option toggles sidebar closed + nextOption = null; + setOption(nextOption); + } else { + // Mobile: clicking same option does nothing (no toggle behavior) + return; } + dispatch(setSidebarOption(nextOption)); }; const optionDict = menuOptions.find((menuOption) => { diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index c53751b09..88bda9c1a 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -135,6 +135,7 @@ export const editorInitialState = { newFileModalShowing: false, renameFileModalShowing: false, sidebarShowing: true, + sidebarOption: null, modals: {}, errorDetails: {}, runnerBeingLoaded: null | "pyodide" | "skulpt", @@ -375,6 +376,12 @@ export const EditorSlice = createSlice({ hideSidebar: (state) => { state.sidebarShowing = false; }, + setSidebarOption: (state, action) => { + state.sidebarOption = action.payload; + }, + clearSidebarOption: (state) => { + state.sidebarOption = null; + }, disableTheming: (state) => { state.isThemeable = false; }, @@ -483,6 +490,8 @@ export const { closeRenameFileModal, showSidebar, hideSidebar, + setSidebarOption, + clearSidebarOption, disableTheming, setErrorDetails, } = EditorSlice.actions; From c15b2702e36e843819748f1b2d9ff8b5e1bd071b Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Wed, 11 Feb 2026 18:15:39 +0000 Subject: [PATCH 2/8] test: add tests for selected sidebar option state --- src/components/Menus/Sidebar/Sidebar.test.js | 157 ++++++++++++++++++ .../MobileProject/MobileProject.test.js | 14 +- 2 files changed, 169 insertions(+), 2 deletions(-) diff --git a/src/components/Menus/Sidebar/Sidebar.test.js b/src/components/Menus/Sidebar/Sidebar.test.js index 05476a305..e41fa9766 100644 --- a/src/components/Menus/Sidebar/Sidebar.test.js +++ b/src/components/Menus/Sidebar/Sidebar.test.js @@ -317,6 +317,126 @@ describe("When the project has no instructions", () => { }); }); +describe("Redux sidebar state persistence", () => { + const mockStore = configureStore([]); + + describe("When no stored sidebar option exists", () => { + let store; + + beforeEach(() => { + const initialState = { + editor: { + project: { + components: [], + image_list: [], + }, + instructionsEditable: false, + sidebarOption: null, + }, + instructions: {}, + }; + store = mockStore(initialState); + render( + +
+ +
+
, + ); + }); + + test("Dispatches setSidebarOption with default value", () => { + const actions = store.getActions(); + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSidebarOption", + payload: "file", // Default when no special conditions are met + }), + ]), + ); + }); + }); + + describe("When stored sidebar option exists and is valid", () => { + let store; + + beforeEach(() => { + const initialState = { + editor: { + project: { + components: [], + image_list: images, + }, + instructionsEditable: false, + sidebarOption: "images", + }, + instructions: {}, + }; + store = mockStore(initialState); + render( + +
+ +
+
, + ); + }); + + test("Uses stored option and shows correct panel", () => { + expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument(); + }); + + test("Does not dispatch setSidebarOption when using valid stored option", () => { + const setSidebarActions = store + .getActions() + .filter((action) => action.type === "editor/setSidebarOption"); + expect(setSidebarActions).toHaveLength(0); + }); + }); + + describe("When clicking different sidebar options", () => { + let store; + + beforeEach(() => { + const initialState = { + editor: { + project: { + components: [], + image_list: images, + }, + instructionsEditable: false, + sidebarOption: "file", + }, + instructions: {}, + }; + store = mockStore(initialState); + render( + +
+ +
+
, + ); + }); + + test("Clicking different option updates Redux state", () => { + const imageButton = screen.getByTitle("sidebar.images"); + fireEvent.click(imageButton); + + const actions = store.getActions(); + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSidebarOption", + payload: "images", + }), + ]), + ); + }); + }); +}); + describe("When plugins are provided", () => { const initialState = { editor: { @@ -325,6 +445,7 @@ describe("When plugins are provided", () => { image_list: [], }, instructionsEditable: false, + sidebarOption: null, }, instructions: {}, }; @@ -339,6 +460,42 @@ describe("When plugins are provided", () => { buttons: () => [], }; + describe("when plugin has autoOpen true and Redux state persistence", () => { + let store; + + beforeEach(() => { + const plugins = [ + { + ...defaultPlugin, + autoOpen: true, + }, + ]; + store = mockStore(initialState); + render( + +
+ +
+
, + ); + }); + + test("Plugin autoOpen takes priority and updates Redux state", () => { + expect(screen.queryByText("My amazing plugin")).toBeInTheDocument(); + expect(screen.queryByText("My amazing content")).toBeInTheDocument(); + + const actions = store.getActions(); + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSidebarOption", + payload: "my-amazing-plugin", + }), + ]), + ); + }); + }); + describe("when plugin has autoOpen true", () => { beforeEach(() => { const plugins = [ diff --git a/src/components/Mobile/MobileProject/MobileProject.test.js b/src/components/Mobile/MobileProject/MobileProject.test.js index 7893a8ecf..7ce7049b8 100644 --- a/src/components/Mobile/MobileProject/MobileProject.test.js +++ b/src/components/Mobile/MobileProject/MobileProject.test.js @@ -3,7 +3,6 @@ import React from "react"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import MobileProject from "./MobileProject"; -import { showSidebar } from "../../../redux/EditorSlice"; window.HTMLElement.prototype.scrollIntoView = jest.fn(); @@ -131,7 +130,18 @@ describe("When withSidebar is true", () => { test("clicking sidebar open button dispatches action to open the sidebar", () => { const sidebarOpenButton = screen.getByText("mobile.menu"); fireEvent.click(sidebarOpenButton); - expect(store.getActions()).toEqual([showSidebar()]); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSidebarOption", + payload: "settings", + }), + expect.objectContaining({ + type: "editor/showSidebar", + payload: undefined, + }), + ]), + ); }); }); From 87b154bf8270b878063959499837653f96ed8532 Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Thu, 12 Feb 2026 10:48:13 +0000 Subject: [PATCH 3/8] fix: ensure selected tab is preserved when sidebar is collpased/expanded --- src/components/Menus/Sidebar/Sidebar.jsx | 32 +++++++++---------- src/components/Menus/Sidebar/Sidebar.test.js | 22 ++++++------- src/components/Menus/Sidebar/SidebarBar.jsx | 11 +++++-- .../Mobile/MobileProject/MobileProject.jsx | 10 ++++-- .../MobileProject/MobileProject.test.js | 2 +- src/redux/EditorSlice.js | 14 ++++---- 6 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/components/Menus/Sidebar/Sidebar.jsx b/src/components/Menus/Sidebar/Sidebar.jsx index f222ed4e2..77f73d634 100644 --- a/src/components/Menus/Sidebar/Sidebar.jsx +++ b/src/components/Menus/Sidebar/Sidebar.jsx @@ -23,7 +23,7 @@ import { MOBILE_MEDIA_QUERY } from "../../../utils/mediaQueryBreakpoints"; import FileIcon from "../../../utils/FileIcon"; import DownloadPanel from "./DownloadPanel/DownloadPanel"; import InstructionsPanel from "./InstructionsPanel/InstructionsPanel"; -import { setSidebarOption } from "../../../redux/EditorSlice"; +import { setSelectedSidebarTab } from "../../../redux/EditorSlice"; import SidebarPanel from "./SidebarPanel"; const Sidebar = ({ options = [], plugins = [] }) => { @@ -113,8 +113,8 @@ const Sidebar = ({ options = [], plugins = [] }) => { const instructionsEditable = useSelector( (state) => state.editor.instructionsEditable, ); - const storedSidebarOption = useSelector( - (state) => state.editor.sidebarOption, + const selectedSidebarTab = useSelector( + (state) => state.editor.selectedSidebarTab, ); const removeOption = (optionName, depArray = []) => { @@ -137,10 +137,10 @@ const Sidebar = ({ options = [], plugins = [] }) => { // Use stored option if it exists and is valid, otherwise use default logic const resolveSelectedOption = () => { if ( - storedSidebarOption && - menuOptions.find((opt) => opt.name === storedSidebarOption) + selectedSidebarTab && + menuOptions.find((opt) => opt.name === selectedSidebarTab) ) { - return storedSidebarOption; + return selectedSidebarTab; } // Calculate default const defaultOption = autoOpenPlugin @@ -148,36 +148,36 @@ const Sidebar = ({ options = [], plugins = [] }) => { : instructionsEditable || instructionsSteps ? "instructions" : "file"; - dispatch(setSidebarOption(defaultOption)); + dispatch(setSelectedSidebarTab(defaultOption)); return defaultOption; }; - const [option, setOption] = useState(resolveSelectedOption); + const [option, setOption] = useState(() => { + const selectedTab = resolveSelectedOption(); + return selectedTab; // Start with sidebar open by default + }); const hasInstructions = instructionsSteps && instructionsSteps.length > 0; useEffect(() => { if (!autoOpenPlugin && (instructionsEditable || hasInstructions)) { + dispatch(setSelectedSidebarTab("instructions")); setOption("instructions"); - dispatch(setSidebarOption("instructions")); } }, [autoOpenPlugin, instructionsEditable, hasInstructions, dispatch]); const toggleOption = (newOption) => { - let nextOption; if (option !== newOption) { // Switch to different sidebar panel - nextOption = newOption; - setOption(nextOption); + setOption(newOption); + dispatch(setSelectedSidebarTab(newOption)); } else if (!isMobile) { - // Desktop: clicking same option toggles sidebar closed - nextOption = null; - setOption(nextOption); + // Desktop: clicking same option toggles sidebar open/closed + setOption(option === null ? newOption : null); } else { // Mobile: clicking same option does nothing (no toggle behavior) return; } - dispatch(setSidebarOption(nextOption)); }; const optionDict = menuOptions.find((menuOption) => { diff --git a/src/components/Menus/Sidebar/Sidebar.test.js b/src/components/Menus/Sidebar/Sidebar.test.js index e41fa9766..e9479b7d3 100644 --- a/src/components/Menus/Sidebar/Sidebar.test.js +++ b/src/components/Menus/Sidebar/Sidebar.test.js @@ -345,12 +345,12 @@ describe("Redux sidebar state persistence", () => { ); }); - test("Dispatches setSidebarOption with default value", () => { + test("Dispatches setSelectedSidebarTab with default value", () => { const actions = store.getActions(); expect(actions).toEqual( expect.arrayContaining([ expect.objectContaining({ - type: "editor/setSidebarOption", + type: "editor/setSelectedSidebarTab", payload: "file", // Default when no special conditions are met }), ]), @@ -369,7 +369,7 @@ describe("Redux sidebar state persistence", () => { image_list: images, }, instructionsEditable: false, - sidebarOption: "images", + selectedSidebarTab: "images", }, instructions: {}, }; @@ -387,11 +387,11 @@ describe("Redux sidebar state persistence", () => { expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument(); }); - test("Does not dispatch setSidebarOption when using valid stored option", () => { - const setSidebarActions = store + test("Does not dispatch setSelectedSidebarTab when using valid stored option", () => { + const setSelectedSidebarActions = store .getActions() - .filter((action) => action.type === "editor/setSidebarOption"); - expect(setSidebarActions).toHaveLength(0); + .filter((action) => action.type === "editor/setSelectedSidebarTab"); + expect(setSelectedSidebarActions).toHaveLength(0); }); }); @@ -406,7 +406,7 @@ describe("Redux sidebar state persistence", () => { image_list: images, }, instructionsEditable: false, - sidebarOption: "file", + selectedSidebarTab: "file", }, instructions: {}, }; @@ -428,7 +428,7 @@ describe("Redux sidebar state persistence", () => { expect(actions).toEqual( expect.arrayContaining([ expect.objectContaining({ - type: "editor/setSidebarOption", + type: "editor/setSelectedSidebarTab", payload: "images", }), ]), @@ -445,7 +445,7 @@ describe("When plugins are provided", () => { image_list: [], }, instructionsEditable: false, - sidebarOption: null, + selectedSidebarTab: null, }, instructions: {}, }; @@ -488,7 +488,7 @@ describe("When plugins are provided", () => { expect(actions).toEqual( expect.arrayContaining([ expect.objectContaining({ - type: "editor/setSidebarOption", + type: "editor/setSelectedSidebarTab", payload: "my-amazing-plugin", }), ]), diff --git a/src/components/Menus/Sidebar/SidebarBar.jsx b/src/components/Menus/Sidebar/SidebarBar.jsx index 4bcda60b6..1af729761 100644 --- a/src/components/Menus/Sidebar/SidebarBar.jsx +++ b/src/components/Menus/Sidebar/SidebarBar.jsx @@ -24,9 +24,15 @@ const SidebarBar = (props) => { ); const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY }); + const selectedSidebarTab = useSelector( + (state) => state.editor.selectedSidebarTab, + ); + const expandPopOut = () => { - const option = instructions.length > 0 ? "instructions" : "file"; - toggleOption(option); + // Use stored option if available, otherwise fall back to default logic + const optionToExpand = + selectedSidebarTab || (instructions.length > 0 ? "instructions" : "file"); + toggleOption(optionToExpand); if (window.plausible) { // TODO: Make dynamic events for each option or rename this event window.plausible("Expand file pane"); @@ -34,6 +40,7 @@ const SidebarBar = (props) => { }; const collapsePopOut = () => { + // Toggle the currently selected option to close the sidebar toggleOption(option); if (window.plausible) { window.plausible("Collapse file pane"); diff --git a/src/components/Mobile/MobileProject/MobileProject.jsx b/src/components/Mobile/MobileProject/MobileProject.jsx index 2ba61a290..90d9cd68c 100644 --- a/src/components/Mobile/MobileProject/MobileProject.jsx +++ b/src/components/Mobile/MobileProject/MobileProject.jsx @@ -12,7 +12,7 @@ import StepsIcon from "../../../assets/icons/steps.svg"; import PreviewIcon from "../../../assets/icons/preview.svg"; import { useTranslation } from "react-i18next"; import Sidebar from "../../Menus/Sidebar/Sidebar"; -import { showSidebar } from "../../../redux/EditorSlice"; +import { showSidebar, setSelectedSidebarTab } from "../../../redux/EditorSlice"; const MobileProject = ({ withSidebar, @@ -30,7 +30,13 @@ const MobileProject = ({ const { t } = useTranslation(); const dispatch = useDispatch(); - const openSidebar = () => dispatch(showSidebar()); + const openSidebar = () => { + // Set the default sidebar option to the first available option + const defaultOption = + sidebarOptions.length > 0 ? sidebarOptions[0] : "file"; + dispatch(setSelectedSidebarTab(defaultOption)); + dispatch(showSidebar()); + }; useEffect(() => { if (codeRunTriggered) { diff --git a/src/components/Mobile/MobileProject/MobileProject.test.js b/src/components/Mobile/MobileProject/MobileProject.test.js index 7ce7049b8..30db50bfc 100644 --- a/src/components/Mobile/MobileProject/MobileProject.test.js +++ b/src/components/Mobile/MobileProject/MobileProject.test.js @@ -133,7 +133,7 @@ describe("When withSidebar is true", () => { expect(store.getActions()).toEqual( expect.arrayContaining([ expect.objectContaining({ - type: "editor/setSidebarOption", + type: "editor/setSelectedSidebarTab", payload: "settings", }), expect.objectContaining({ diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index 88bda9c1a..5b0584ae6 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -135,7 +135,7 @@ export const editorInitialState = { newFileModalShowing: false, renameFileModalShowing: false, sidebarShowing: true, - sidebarOption: null, + selectedSidebarTab: null, modals: {}, errorDetails: {}, runnerBeingLoaded: null | "pyodide" | "skulpt", @@ -376,11 +376,11 @@ export const EditorSlice = createSlice({ hideSidebar: (state) => { state.sidebarShowing = false; }, - setSidebarOption: (state, action) => { - state.sidebarOption = action.payload; + setSelectedSidebarTab: (state, action) => { + state.selectedSidebarTab = action.payload; }, - clearSidebarOption: (state) => { - state.sidebarOption = null; + clearSelectedSidebarTab: (state) => { + state.selectedSidebarTab = null; }, disableTheming: (state) => { state.isThemeable = false; @@ -490,8 +490,8 @@ export const { closeRenameFileModal, showSidebar, hideSidebar, - setSidebarOption, - clearSidebarOption, + setSelectedSidebarTab, + clearSelectedSidebarTab, disableTheming, setErrorDetails, } = EditorSlice.actions; From 741b8e2bd5670b2e5a0f99c9e7b633b7e7e23c1f Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Thu, 12 Feb 2026 18:26:04 +0000 Subject: [PATCH 4/8] refactor: seperate open and selected option concerns The open/closed state was relying on the selected option being set (open) or null (closed). This also avoids needing to keep component state in sync. --- src/components/Menus/Sidebar/Sidebar.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/Menus/Sidebar/Sidebar.jsx b/src/components/Menus/Sidebar/Sidebar.jsx index 77f73d634..14c008d77 100644 --- a/src/components/Menus/Sidebar/Sidebar.jsx +++ b/src/components/Menus/Sidebar/Sidebar.jsx @@ -152,28 +152,30 @@ const Sidebar = ({ options = [], plugins = [] }) => { return defaultOption; }; - const [option, setOption] = useState(() => { - const selectedTab = resolveSelectedOption(); - return selectedTab; // Start with sidebar open by default - }); + // Store the initial selected tab to handle first render timing + const [initialSelectedTab] = useState(() => resolveSelectedOption()); + const [isOpen, setIsOpen] = useState(true); + + // Derive option from open state and selected tab + const option = isOpen ? selectedSidebarTab || initialSelectedTab : null; const hasInstructions = instructionsSteps && instructionsSteps.length > 0; useEffect(() => { if (!autoOpenPlugin && (instructionsEditable || hasInstructions)) { dispatch(setSelectedSidebarTab("instructions")); - setOption("instructions"); + setIsOpen(true); } }, [autoOpenPlugin, instructionsEditable, hasInstructions, dispatch]); const toggleOption = (newOption) => { if (option !== newOption) { // Switch to different sidebar panel - setOption(newOption); dispatch(setSelectedSidebarTab(newOption)); + setIsOpen(true); } else if (!isMobile) { // Desktop: clicking same option toggles sidebar open/closed - setOption(option === null ? newOption : null); + setIsOpen(!isOpen); } else { // Mobile: clicking same option does nothing (no toggle behavior) return; From b95869fdb51ffdfb9dd567314935b8b52b45d705 Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Thu, 12 Feb 2026 21:56:04 +0000 Subject: [PATCH 5/8] fix: incorrect state name in test Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/Menus/Sidebar/Sidebar.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Menus/Sidebar/Sidebar.test.js b/src/components/Menus/Sidebar/Sidebar.test.js index e9479b7d3..902c83c72 100644 --- a/src/components/Menus/Sidebar/Sidebar.test.js +++ b/src/components/Menus/Sidebar/Sidebar.test.js @@ -331,7 +331,7 @@ describe("Redux sidebar state persistence", () => { image_list: [], }, instructionsEditable: false, - sidebarOption: null, + selectedSidebarTab: null, }, instructions: {}, }; From 191d64b5e526d7215ea6244f1dd28534ea9cedbe Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Thu, 12 Feb 2026 22:40:03 +0000 Subject: [PATCH 6/8] test: split sidebar tests to avoid issues with mock redux state --- src/components/Menus/Sidebar/Sidebar.test.js | 116 ++++++++++++++++--- 1 file changed, 101 insertions(+), 15 deletions(-) diff --git a/src/components/Menus/Sidebar/Sidebar.test.js b/src/components/Menus/Sidebar/Sidebar.test.js index 902c83c72..eae388276 100644 --- a/src/components/Menus/Sidebar/Sidebar.test.js +++ b/src/components/Menus/Sidebar/Sidebar.test.js @@ -19,6 +19,8 @@ const options = ["file", "images", "instructions"]; describe("When project has images", () => { describe("and no instructions", () => { + let store; + beforeEach(() => { const mockStore = configureStore([]); const initialState = { @@ -27,10 +29,11 @@ describe("When project has images", () => { components: [], image_list: images, }, + selectedSidebarTab: "file", }, instructions: {}, }; - const store = mockStore(initialState); + store = mockStore(initialState); render(
@@ -93,14 +96,25 @@ describe("When project has images", () => { ).not.toBeInTheDocument(); }); - test("Clicking image icon opens image panel", () => { + test("Clicking image icon dispatches correct action", () => { const imageButton = screen.getByTitle("sidebar.images"); fireEvent.click(imageButton); - expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument(); + + const actions = store.getActions(); + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSelectedSidebarTab", + payload: "images", + }), + ]), + ); }); }); describe("and instructions", () => { + let store; + beforeEach(() => { const mockStore = configureStore([]); const initialState = { @@ -109,6 +123,7 @@ describe("When project has images", () => { components: [], image_list: images, }, + selectedSidebarTab: "instructions", }, instructions: { project: { @@ -116,7 +131,7 @@ describe("When project has images", () => { }, }, }; - const store = mockStore(initialState); + store = mockStore(initialState); render(
@@ -146,15 +161,15 @@ describe("When project has images", () => { ).toBeInTheDocument(); }); - test("Clicking instructions button a second time closes file pane", () => { + test("Clicking instructions button a second time dispatches toggle action", () => { const collapseButton = screen.getByTitle("sidebar.collapse"); fireEvent.click(collapseButton); const fileButton = screen.getByTitle("sidebar.file"); fireEvent.click(fileButton); fireEvent.click(fileButton); - expect( - screen.queryByText("instructionsPanel.projectSteps"), - ).not.toBeInTheDocument(); + + const actions = store.getActions(); + expect(actions.length).toBeGreaterThan(0); // Verify actions were dispatched }); test("Shows file icon", () => { @@ -173,14 +188,49 @@ describe("When project has images", () => { expect(screen.queryByTitle("sidebar.settings")).not.toBeInTheDocument(); }); - test("Clicking image icon opens image panel", () => { + test("Clicking image icon dispatches correct action", () => { const imageButton = screen.getByTitle("sidebar.images"); fireEvent.click(imageButton); - expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument(); + + const actions = store.getActions(); + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSelectedSidebarTab", + payload: "images", + }), + ]), + ); }); }); }); +describe("When selectedSidebarTab is set to images", () => { + beforeEach(() => { + const mockStore = configureStore([]); + const initialState = { + editor: { + project: { + components: [], + image_list: images, + }, + selectedSidebarTab: "images", + }, + instructions: {}, + }; + const store = mockStore(initialState); + render( + +
+ +
+
, + ); + }); + test("shows image panel", () => { + expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument(); + }); +}); describe("When the project has no images", () => { beforeEach(() => { const mockStore = configureStore([]); @@ -555,15 +605,51 @@ describe("When plugins are provided", () => { expect(screen.queryByText("My amazing plugin")).not.toBeInTheDocument(); }); - test("Opening the panel shows the plugin heading", () => { + test("Clicking plugin button dispatches correct action", () => { const pluginButton = screen.getByTitle("my amazing plugin"); fireEvent.click(pluginButton); - expect(screen.queryByText("My amazing plugin")).toBeInTheDocument(); + + const actions = store.getActions(); + expect(actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "editor/setSelectedSidebarTab", + payload: "my-amazing-plugin", + }), + ]), + ); }); - test("Opening the panel shows the plugin content", () => { - const pluginButton = screen.getByTitle("my amazing plugin"); - fireEvent.click(pluginButton); + test("Plugin panel shows when selectedSidebarTab is set", () => { + // Re-render with plugin selected + const mockStoreFn = configureStore([]); + const newStore = mockStoreFn({ + editor: { + project: { + components: [], + image_list: [], + }, + selectedSidebarTab: "my-amazing-plugin", + }, + instructions: {}, + }); + + const plugins = [ + { + ...defaultPlugin, + autoOpen: false, + }, + ]; + + render( + +
+ +
+
, + ); + + expect(screen.queryByText("My amazing plugin")).toBeInTheDocument(); expect(screen.queryByText("My amazing content")).toBeInTheDocument(); }); }); From e2cbd85163b5af3087fd3ec287830aed3f619f20 Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Thu, 12 Feb 2026 22:40:32 +0000 Subject: [PATCH 7/8] fix: validate stored sidebar tab before expanding --- src/components/Menus/Sidebar/SidebarBar.jsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/Menus/Sidebar/SidebarBar.jsx b/src/components/Menus/Sidebar/SidebarBar.jsx index 1af729761..56d1805c7 100644 --- a/src/components/Menus/Sidebar/SidebarBar.jsx +++ b/src/components/Menus/Sidebar/SidebarBar.jsx @@ -29,9 +29,17 @@ const SidebarBar = (props) => { ); const expandPopOut = () => { - // Use stored option if available, otherwise fall back to default logic - const optionToExpand = - selectedSidebarTab || (instructions.length > 0 ? "instructions" : "file"); + // Use stored option if available and valid, otherwise fall back to default logic + const validMenuOptionNames = menuOptions.map( + (menuOption) => menuOption.name, + ); + const isStoredTabValid = + selectedSidebarTab && validMenuOptionNames.includes(selectedSidebarTab); + const optionToExpand = isStoredTabValid + ? selectedSidebarTab + : instructions.length > 0 + ? "instructions" + : "file"; toggleOption(optionToExpand); if (window.plausible) { // TODO: Make dynamic events for each option or rename this event From 57e35b9a5e819dfb1828236102117c7bc1b01170 Mon Sep 17 00:00:00 2001 From: Max Elkins Date: Thu, 12 Feb 2026 22:55:59 +0000 Subject: [PATCH 8/8] refactor: remove clearSelectedSidebarTab action from EditorSlice --- src/redux/EditorSlice.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/redux/EditorSlice.js b/src/redux/EditorSlice.js index 5b0584ae6..019cff7a4 100644 --- a/src/redux/EditorSlice.js +++ b/src/redux/EditorSlice.js @@ -379,9 +379,6 @@ export const EditorSlice = createSlice({ setSelectedSidebarTab: (state, action) => { state.selectedSidebarTab = action.payload; }, - clearSelectedSidebarTab: (state) => { - state.selectedSidebarTab = null; - }, disableTheming: (state) => { state.isThemeable = false; }, @@ -491,7 +488,6 @@ export const { showSidebar, hideSidebar, setSelectedSidebarTab, - clearSelectedSidebarTab, disableTheming, setErrorDetails, } = EditorSlice.actions;