From 98b19b9e32458dbab39038252aa65ad6ced56ab2 Mon Sep 17 00:00:00 2001 From: Bikram Choudhury Date: Thu, 25 Dec 2025 02:34:07 +0530 Subject: [PATCH] Refactor: Extract root components architecture with comprehensive tests --- .../ContainerCopy/ContainerCopyPanel.tsx | 2 + src/Main.tsx | 189 +---------- src/Metrics/MetricScenarioProvider.tsx | 2 +- src/RootComponents/App.test.tsx | 317 ++++++++++++++++++ src/RootComponents/App.tsx | 73 ++++ src/RootComponents/ExplorerContainer.test.tsx | 183 ++++++++++ src/RootComponents/ExplorerContainer.tsx | 71 ++++ src/RootComponents/LoadingExplorer.test.tsx | 71 ++++ src/RootComponents/LoadingExplorer.tsx | 36 ++ src/RootComponents/Root.test.tsx | 107 ++++++ src/RootComponents/Root.tsx | 28 ++ 11 files changed, 893 insertions(+), 186 deletions(-) create mode 100644 src/RootComponents/App.test.tsx create mode 100644 src/RootComponents/App.tsx create mode 100644 src/RootComponents/ExplorerContainer.test.tsx create mode 100644 src/RootComponents/ExplorerContainer.tsx create mode 100644 src/RootComponents/LoadingExplorer.test.tsx create mode 100644 src/RootComponents/LoadingExplorer.tsx create mode 100644 src/RootComponents/Root.test.tsx create mode 100644 src/RootComponents/Root.tsx diff --git a/src/Explorer/ContainerCopy/ContainerCopyPanel.tsx b/src/Explorer/ContainerCopy/ContainerCopyPanel.tsx index 1c82ad4a6..569ef88e2 100644 --- a/src/Explorer/ContainerCopy/ContainerCopyPanel.tsx +++ b/src/Explorer/ContainerCopy/ContainerCopyPanel.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import { SidePanel } from "../../Explorer/Panes/PanelContainerComponent"; import CopyJobCommandBar from "./CommandBar/CopyJobCommandBar"; import "./containerCopyStyles.less"; import { MonitorCopyJobsRefState } from "./MonitorCopyJobs/MonitorCopyJobRefState"; @@ -16,6 +17,7 @@ const ContainerCopyPanel: React.FC = ({ explorer }) => {
+
); }; diff --git a/src/Main.tsx b/src/Main.tsx index ddb6a22fe..8b9c8d45c 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -2,18 +2,9 @@ import "./ReactDevTools"; // CSS Dependencies -import { initializeIcons, loadTheme, useTheme } from "@fluentui/react"; -import { FluentProvider, makeStyles, webDarkTheme, webLightTheme } from "@fluentui/react-components"; -import { Platform } from "ConfigContext"; -import ContainerCopyPanel from "Explorer/ContainerCopy/ContainerCopyPanel"; -import Explorer from "Explorer/Explorer"; -import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; -import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial"; -import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial"; -import { userContext } from "UserContext"; +import { initializeIcons } from "@fluentui/react"; import "allotment/dist/style.css"; import "bootstrap/dist/css/bootstrap.css"; -import { useCarousel } from "hooks/useCarousel"; import React from "react"; import ReactDOM from "react-dom"; import "../externals/jquery-ui.min.css"; @@ -24,13 +15,8 @@ import "../externals/jquery.dataTables.min.css"; import "../externals/jquery.typeahead.min.css"; import "../externals/jquery.typeahead.min.js"; // Image Dependencies -import { SidePanel } from "Explorer/Panes/PanelContainerComponent"; -import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; -import { SidebarContainer } from "Explorer/Sidebar"; -import { KeyboardShortcutRoot } from "KeyboardShortcuts"; import "allotment/dist/style.css"; import "../images/CosmosDB_rgb_ui_lighttheme.ico"; -import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import "../images/favicon.ico"; import "../less/TableStyles/CustomizeColumns.less"; import "../less/TableStyles/EntityEditor.less"; @@ -42,175 +28,29 @@ import "../less/infobox.less"; import "../less/menus.less"; import "../less/messagebox.less"; import "../less/resourceTree.less"; -import * as StyleConstants from "./Common/StyleConstants"; import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; -import { Dialog } from "./Explorer/Controls/Dialog"; import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; import "./Explorer/Controls/TreeComponent/treeComponent.less"; -import { ErrorBoundary } from "./Explorer/ErrorBoundary"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; -import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; -import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import "./Explorer/Panes/PanelComponent.less"; import "./Explorer/SplashScreen/SplashScreen.less"; import "./Libs/jquery"; -import MetricScenario from "./Metrics/MetricEvents"; -import { MetricScenarioProvider, useMetricScenario } from "./Metrics/MetricScenarioProvider"; -import { ApplicationMetricPhase } from "./Metrics/ScenarioConfig"; -import { useInteractive } from "./Metrics/useMetricPhases"; -import { appThemeFabric } from "./Platform/Fabric/FabricTheme"; +import { MetricScenarioProvider } from "./Metrics/MetricScenarioProvider"; +import Root from "./RootComponents/Root"; import "./Shared/appInsights"; -import { useConfig } from "./hooks/useConfig"; -import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; -import { useThemeStore } from "./hooks/useTheme"; import "./less/DarkModeMenus.less"; import "./less/ThemeSystem.less"; + // Initialize icons before React is loaded initializeIcons(undefined, { disableWarnings: true }); -const useStyles = makeStyles({ - root: { - height: "100vh", - width: "100vw", - backgroundColor: "var(--colorNeutralBackground1)", - color: "var(--colorNeutralForeground1)", - }, -}); - -const App = (): JSX.Element => { - const config = useConfig(); - const styles = useStyles(); - // theme is used for application-wide styling - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const theme = useTheme(); - - // Load Fabric theme and styles only once when platform is Fabric - React.useEffect(() => { - if (config?.platform === Platform.Fabric) { - loadTheme(appThemeFabric); - import("../less/documentDBFabric.less"); - } - StyleConstants.updateStyles(); - }, [config?.platform]); - - const explorer = useKnockoutExplorer(config?.platform); - - // Scenario-based health tracking: start ApplicationLoad and complete phases. - const { startScenario, completePhase } = useMetricScenario(); - React.useEffect(() => { - startScenario(MetricScenario.ApplicationLoad); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - React.useEffect(() => { - if (explorer) { - completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [explorer]); - - if (!explorer) { - return ; - } - - return ( -
- -
- {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( - - ) : ( - - )} -
-
-
- ); -}; - -const DivExplorer: React.FC<{ explorer: Explorer }> = ({ explorer }) => { - const isCarouselOpen = useCarousel((state) => state.shouldOpen); - const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel); - useInteractive(MetricScenario.ApplicationLoad); - - return ( -
-
-
- - - -
- - - {} - {} - {} - {} -
- ); -}; - -const Root: React.FC = () => { - // Use React state to track isDarkMode and subscribe to changes - const [isDarkMode, setIsDarkMode] = React.useState(useThemeStore.getState().isDarkMode); - const currentTheme = isDarkMode ? webDarkTheme : webLightTheme; - - // Subscribe to theme changes - React.useEffect(() => { - return useThemeStore.subscribe((state) => { - setIsDarkMode(state.isDarkMode); - }); - }, []); - - return ( - - - - - - ); -}; - const mainElement = document.getElementById("Main"); if (mainElement) { ReactDOM.render( @@ -220,24 +60,3 @@ if (mainElement) { mainElement, ); } - -function LoadingExplorer(): JSX.Element { - const styles = useStyles(); - return ( -
-
-
-

- Azure Cosmos DB -

-

- Welcome to Azure Cosmos DB -

- -
-
-
- ); -} diff --git a/src/Metrics/MetricScenarioProvider.tsx b/src/Metrics/MetricScenarioProvider.tsx index c01815458..90c9c4f75 100644 --- a/src/Metrics/MetricScenarioProvider.tsx +++ b/src/Metrics/MetricScenarioProvider.tsx @@ -3,7 +3,7 @@ import MetricScenario from "./MetricEvents"; import { MetricPhase } from "./ScenarioConfig"; import { scenarioMonitor } from "./ScenarioMonitor"; -interface MetricScenarioContextValue { +export interface MetricScenarioContextValue { startScenario: (scenario: MetricScenario) => void; startPhase: (scenario: MetricScenario, phase: MetricPhase) => void; completePhase: (scenario: MetricScenario, phase: MetricPhase) => void; diff --git a/src/RootComponents/App.test.tsx b/src/RootComponents/App.test.tsx new file mode 100644 index 000000000..aff750dc4 --- /dev/null +++ b/src/RootComponents/App.test.tsx @@ -0,0 +1,317 @@ +import { loadTheme } from "@fluentui/react"; +import "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import React from "react"; +import { updateStyles } from "../Common/StyleConstants"; +import { Platform } from "../ConfigContext"; +import { useConfig } from "../hooks/useConfig"; +import { useKnockoutExplorer } from "../hooks/useKnockoutExplorer"; +import { MetricScenarioContextValue, useMetricScenario } from "../Metrics/MetricScenarioProvider"; +import App from "./App"; + +const mockUserContext = { + features: { enableContainerCopy: false }, + apiType: "SQL", +}; + +jest.mock("@fluentui/react", () => ({ + loadTheme: jest.fn(), + makeStyles: jest.fn(() => () => ({ + root: "mock-app-root-class", + })), + MessageBarType: { + error: "error", + warning: "warning", + info: "info", + success: "success", + }, + SpinnerSize: { + xSmall: "xSmall", + small: "small", + medium: "medium", + large: "large", + }, +})); + +jest.mock("../Common/StyleConstants", () => ({ + StyleConstants: { + BaseMedium: "#000000", + AccentMediumHigh: "#0078d4", + AccentMedium: "#106ebe", + AccentLight: "#deecf9", + AccentAccentExtra: "#0078d4", + FabricAccentMediumHigh: "#0078d4", + FabricAccentMedium: "#106ebe", + FabricAccentLight: "#deecf9", + PortalAccentMediumHigh: "#0078d4", + PortalAccentMedium: "#106ebe", + PortalAccentLight: "#deecf9", + }, + updateStyles: jest.fn(), +})); + +jest.mock("./LoadingExplorer", () => { + const MockLoadingExplorer = () => { + return
Loading Explorer
; + }; + MockLoadingExplorer.displayName = "MockLoadingExplorer"; + return MockLoadingExplorer; +}); + +jest.mock("./ExplorerContainer", () => { + const MockExplorerContainer = ({ explorer }: { explorer: unknown }) => { + return ( +
Explorer Container - {explorer ? "with explorer" : "no explorer"}
+ ); + }; + MockExplorerContainer.displayName = "MockExplorerContainer"; + return MockExplorerContainer; +}); + +jest.mock("../Explorer/ContainerCopy/ContainerCopyPanel", () => { + const MockContainerCopyPanel = ({ explorer }: { explorer: unknown }) => { + return ( +
+ Container Copy Panel - {explorer ? "with explorer" : "no explorer"} +
+ ); + }; + MockContainerCopyPanel.displayName = "MockContainerCopyPanel"; + return MockContainerCopyPanel; +}); + +jest.mock("../KeyboardShortcuts", () => ({ + KeyboardShortcutRoot: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +jest.mock("../UserContext", () => ({ + get userContext() { + return mockUserContext; + }, +})); + +const mockConfig = { + platform: Platform.Portal, +}; + +const mockExplorer = { + id: "test-explorer", + name: "Test Explorer", +}; + +jest.mock("../hooks/useConfig", () => ({ + useConfig: jest.fn(() => mockConfig), +})); + +jest.mock("../hooks/useKnockoutExplorer", () => ({ + useKnockoutExplorer: jest.fn(), +})); + +jest.mock("../Metrics/MetricScenarioProvider", () => ({ + useMetricScenario: jest.fn(() => ({ + startScenario: jest.fn(), + completePhase: jest.fn(), + })), +})); + +jest.mock("../Metrics/MetricEvents", () => ({ + __esModule: true, + default: { + ApplicationLoad: "ApplicationLoad", + }, +})); + +jest.mock("../Metrics/ScenarioConfig", () => ({ + ApplicationMetricPhase: { + ExplorerInitialized: "ExplorerInitialized", + }, + CommonMetricPhase: { + Interactive: "Interactive", + }, +})); + +jest.mock("../Platform/Fabric/FabricTheme", () => ({ + appThemeFabric: { name: "fabric-theme" }, +})); + +describe("App", () => { + + afterEach(() => { + jest.clearAllMocks(); + mockUserContext.features = { enableContainerCopy: false }; + mockUserContext.apiType = "SQL"; + }); + let mockStartScenario: jest.Mock; + let mockCompletePhase: jest.Mock; + let mockUseKnockoutExplorer: jest.Mock; + let mockUseConfig: jest.Mock; + let mockLoadTheme: jest.Mock; + let mockUpdateStyles: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + + mockStartScenario = jest.fn(); + mockCompletePhase = jest.fn(); + + mockUseKnockoutExplorer = jest.mocked(useKnockoutExplorer); + mockUseConfig = jest.mocked(useConfig); + mockLoadTheme = jest.mocked(loadTheme); + mockUpdateStyles = jest.mocked(updateStyles); + + const mockUseMetricScenario = jest.mocked(useMetricScenario); + mockUseMetricScenario.mockReturnValue({ + startScenario: mockStartScenario, + completePhase: mockCompletePhase + } as unknown as MetricScenarioContextValue); + + mockUseConfig.mockReturnValue(mockConfig); + mockUseKnockoutExplorer.mockReturnValue(null); + }); + + test("should render loading explorer when explorer is not ready", () => { + mockUseKnockoutExplorer.mockReturnValue(null); + + render(); + + expect(screen.getByTestId("mock-loading-explorer")).toBeInTheDocument(); + expect(screen.queryByTestId("mock-explorer-container")).not.toBeInTheDocument(); + }); + + test("should render explorer container when explorer is ready", () => { + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + render(); + + expect(screen.getByTestId("mock-explorer-container")).toBeInTheDocument(); + expect(screen.queryByTestId("mock-loading-explorer")).not.toBeInTheDocument(); + }); + + test("should start metric scenario on mount", () => { + render(); + + expect(mockStartScenario).toHaveBeenCalledWith("ApplicationLoad"); + expect(mockStartScenario).toHaveBeenCalledTimes(1); + }); + + test("should complete metric phase when explorer is initialized", async () => { + const { rerender } = render(); + + expect(mockCompletePhase).not.toHaveBeenCalled(); + + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + rerender(); + + await waitFor(() => { + expect(mockCompletePhase).toHaveBeenCalledWith("ApplicationLoad", "ExplorerInitialized"); + }); + }); + + test("should load fabric theme when platform is Fabric", () => { + const fabricConfig = { platform: Platform.Fabric }; + mockUseConfig.mockReturnValue(fabricConfig); + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + render(); + + expect(mockLoadTheme).toHaveBeenCalledWith({ name: "fabric-theme" }); + }); + + test("should not load fabric theme when platform is not Fabric", () => { + const portalConfig = { platform: Platform.Portal }; + mockUseConfig.mockReturnValue(portalConfig); + + render(); + + expect(mockLoadTheme).not.toHaveBeenCalled(); + }); + + test("should always call updateStyles", () => { + render(); + + expect(mockUpdateStyles).toHaveBeenCalled(); + }); + + test("should render container copy panel when container copy is enabled and API is SQL", () => { + mockUserContext.features = { enableContainerCopy: true }; + mockUserContext.apiType = "SQL"; + + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + render(); + + expect(screen.getByTestId("mock-container-copy-panel")).toBeInTheDocument(); + expect(screen.queryByTestId("mock-explorer-container")).not.toBeInTheDocument(); + }); + + test("should render explorer container when container copy is disabled", () => { + mockUserContext.features = { enableContainerCopy: false }; + mockUserContext.apiType = "SQL"; + + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + render(); + + expect(screen.getByTestId("mock-explorer-container")).toBeInTheDocument(); + expect(screen.queryByTestId("mock-container-copy-panel")).not.toBeInTheDocument(); + }); + + test("should render explorer container when API is not SQL", () => { + mockUserContext.features = { enableContainerCopy: true }; + mockUserContext.apiType = "MongoDB"; + + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + render(); + + expect(screen.getByTestId("mock-explorer-container")).toBeInTheDocument(); + expect(screen.queryByTestId("mock-container-copy-panel")).not.toBeInTheDocument(); + }); + + test("should have correct DOM structure", () => { + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + const { container } = render(); + + const mainDiv = container.querySelector("#Main"); + expect(mainDiv).toBeInTheDocument(); + expect(mainDiv).toHaveClass("mock-app-root-class"); + + expect(screen.getByTestId("mock-keyboard-shortcut-root")).toBeInTheDocument(); + + const flexContainer = container.querySelector(".flexContainer"); + expect(flexContainer).toBeInTheDocument(); + expect(flexContainer).toHaveAttribute("aria-hidden", "false"); + }); + + test("should handle config changes for Fabric platform", () => { + const { rerender } = render(); + + const fabricConfig = { platform: Platform.Fabric }; + mockUseConfig.mockReturnValue(fabricConfig); + + rerender(); + + expect(mockLoadTheme).toHaveBeenCalledWith({ name: "fabric-theme" }); + }); + + test("should pass explorer to child components", () => { + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + render(); + + expect(screen.getByText("Explorer Container - with explorer")).toBeInTheDocument(); + }); + + test("should handle null config gracefully", () => { + mockUseConfig.mockReturnValue(null); + mockUseKnockoutExplorer.mockReturnValue(mockExplorer); + + expect(() => render()).not.toThrow(); + + expect(mockLoadTheme).not.toHaveBeenCalled(); + expect(mockUpdateStyles).toHaveBeenCalled(); + }); +}); diff --git a/src/RootComponents/App.tsx b/src/RootComponents/App.tsx new file mode 100644 index 000000000..71489cdf1 --- /dev/null +++ b/src/RootComponents/App.tsx @@ -0,0 +1,73 @@ +import { loadTheme, makeStyles } from "@fluentui/react"; +import React from "react"; +import * as StyleConstants from "../Common/StyleConstants"; +import { Platform } from "../ConfigContext"; +import ContainerCopyPanel from "../Explorer/ContainerCopy/ContainerCopyPanel"; +import { useConfig } from "../hooks/useConfig"; +import { useKnockoutExplorer } from "../hooks/useKnockoutExplorer"; +import { KeyboardShortcutRoot } from "../KeyboardShortcuts"; +import MetricScenario from "../Metrics/MetricEvents"; +import { useMetricScenario } from "../Metrics/MetricScenarioProvider"; +import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig"; +import { appThemeFabric } from "../Platform/Fabric/FabricTheme"; +import { userContext } from "../UserContext"; +import ExplorerContainer from "./ExplorerContainer"; +import LoadingExplorer from "./LoadingExplorer"; + +const useStyles = makeStyles({ + root: { + height: "100vh", + width: "100vw", + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", + }, +}); + +const App = (): JSX.Element => { + const config = useConfig(); + const styles = useStyles(); + // Load Fabric theme and styles only once when platform is Fabric + React.useEffect(() => { + if (config?.platform === Platform.Fabric) { + loadTheme(appThemeFabric); + import("../../less/documentDBFabric.less"); + } + StyleConstants.updateStyles(); + }, [config?.platform]); + + const explorer = useKnockoutExplorer(config?.platform); + + // Scenario-based health tracking: start ApplicationLoad and complete phases. + const { startScenario, completePhase } = useMetricScenario(); + React.useEffect(() => { + startScenario(MetricScenario.ApplicationLoad); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + if (explorer) { + completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [explorer]); + + if (!explorer) { + return ; + } + + return ( +
+ +
+ {userContext.features.enableContainerCopy && userContext.apiType === "SQL" ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default App; diff --git a/src/RootComponents/ExplorerContainer.test.tsx b/src/RootComponents/ExplorerContainer.test.tsx new file mode 100644 index 000000000..855496274 --- /dev/null +++ b/src/RootComponents/ExplorerContainer.test.tsx @@ -0,0 +1,183 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import React from "react"; +import Explorer from "../Explorer/Explorer"; +import { useCarousel } from "../hooks/useCarousel"; +import { useInteractive } from "../Metrics/useMetricPhases"; +import ExplorerContainer from "./ExplorerContainer"; + +jest.mock("../Explorer/Controls/Dialog", () => ({ + Dialog: () =>
Dialog
, +})); + +jest.mock("../Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({ + CommandBar: ({ container }: { container: Explorer }) => ( +
CommandBar - {container ? "with explorer" : "no explorer"}
+ ), +})); + +jest.mock("../Explorer/Menus/NotificationConsole/NotificationConsoleComponent", () => ({ + NotificationConsole: () =>
NotificationConsole
, +})); + +jest.mock("../Explorer/Panes/PanelContainerComponent", () => ({ + SidePanel: () =>
SidePanel
, +})); + +jest.mock("../Explorer/QueryCopilot/CopilotCarousel", () => ({ + QueryCopilotCarousel: ({ isOpen, explorer }: { isOpen: boolean; explorer: Explorer }) => ( +
+ CopilotCarousel - {isOpen ? "open" : "closed"} - {explorer ? "with explorer" : "no explorer"} +
+ ), +})); + +jest.mock("../Explorer/Quickstart/QuickstartCarousel", () => ({ + QuickstartCarousel: ({ isOpen }: { isOpen: boolean }) => ( +
QuickstartCarousel - {isOpen ? "open" : "closed"}
+ ), +})); + +jest.mock("../Explorer/Quickstart/Tutorials/MongoQuickstartTutorial", () => ({ + MongoQuickstartTutorial: () =>
MongoQuickstartTutorial
, +})); + +jest.mock("../Explorer/Quickstart/Tutorials/SQLQuickstartTutorial", () => ({ + SQLQuickstartTutorial: () =>
SQLQuickstartTutorial
, +})); + +jest.mock("../Explorer/Sidebar", () => ({ + SidebarContainer: ({ explorer }: { explorer: Explorer }) => ( +
SidebarContainer - {explorer ? "with explorer" : "no explorer"}
+ ), +})); + +jest.mock("../hooks/useCarousel", () => ({ + useCarousel: jest.fn((selector) => { + if (selector.toString().includes("shouldOpen")) { + return true; + } + if (selector.toString().includes("showCopilotCarousel")) { + return false; + } + return false; + }), +})); + +jest.mock("../Metrics/useMetricPhases", () => ({ + useInteractive: jest.fn(), +})); + +jest.mock("../Metrics/MetricEvents", () => ({ + __esModule: true, + default: { + ApplicationLoad: "ApplicationLoad", + }, +})); + +describe("ExplorerContainer", () => { + let mockExplorer: Explorer; + + beforeEach(() => { + mockExplorer = { + id: "test-explorer", + name: "Test Explorer", + } as unknown as Explorer; + + jest.clearAllMocks(); + }); + + test("should render explorer container with all components", () => { + const { container } = render(); + + const mainContainer = container.querySelector('[data-test="DataExplorerRoot"]'); + expect(mainContainer).toBeInTheDocument(); + expect(mainContainer).toHaveClass("flexContainer"); + + expect(screen.getByTestId("mock-command-bar")).toBeInTheDocument(); + expect(screen.getByTestId("mock-sidebar-container")).toBeInTheDocument(); + expect(screen.getByTestId("mock-notification-console")).toBeInTheDocument(); + expect(screen.getByTestId("mock-side-panel")).toBeInTheDocument(); + expect(screen.getByTestId("mock-dialog")).toBeInTheDocument(); + expect(screen.getByTestId("mock-quickstart-carousel")).toBeInTheDocument(); + expect(screen.getByTestId("mock-sql-tutorial")).toBeInTheDocument(); + expect(screen.getByTestId("mock-mongo-tutorial")).toBeInTheDocument(); + expect(screen.getByTestId("mock-copilot-carousel")).toBeInTheDocument(); + }); + + test("should pass explorer to components that need it", () => { + render(); + + expect(screen.getByText("CommandBar - with explorer")).toBeInTheDocument(); + expect(screen.getByText("SidebarContainer - with explorer")).toBeInTheDocument(); + expect(screen.getByText("CopilotCarousel - closed - with explorer")).toBeInTheDocument(); + }); + + test("should have correct DOM structure", () => { + const { container } = render(); + + const mainContainer = container.querySelector('[data-test="DataExplorerRoot"]'); + expect(mainContainer).toBeInTheDocument(); + expect(mainContainer).toHaveAttribute("aria-hidden", "false"); + + const divExplorer = container.querySelector("#divExplorer"); + expect(divExplorer).toBeInTheDocument(); + expect(divExplorer).toHaveClass("flexContainer", "hideOverflows"); + + const freeTierBubble = container.querySelector("#freeTierTeachingBubble"); + expect(freeTierBubble).toBeInTheDocument(); + + const notificationContainer = container.querySelector("#explorerNotificationConsole"); + expect(notificationContainer).toBeInTheDocument(); + expect(notificationContainer).toHaveClass("dataExplorerErrorConsoleContainer"); + expect(notificationContainer).toHaveAttribute("role", "contentinfo"); + expect(notificationContainer).toHaveAttribute("aria-label", "Notification console"); + }); + + test("should apply correct inline styles", () => { + const { container } = render(); + + const mainContainer = container.querySelector('[data-test="DataExplorerRoot"]'); + expect(mainContainer).toHaveStyle({ + flex: "1", + display: "flex", + flexDirection: "column", + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", + }); + + const divExplorer = container.querySelector("#divExplorer"); + expect(divExplorer).toHaveStyle({ + flex: "1", + display: "flex", + flexDirection: "column", + }); + }); + + test("should handle carousel states correctly", () => { + const mockUseCarousel = jest.mocked(useCarousel); + + mockUseCarousel.mockImplementation((selector: { toString: () => string | string[] }) => { + if (selector.toString().includes("shouldOpen")) { + return false; + } + if (selector.toString().includes("showCopilotCarousel")) { + return true; + } + return false; + }); + + render(); + + expect(screen.getByText("QuickstartCarousel - closed")).toBeInTheDocument(); + expect(screen.getByText("CopilotCarousel - open - with explorer")).toBeInTheDocument(); + }); + + test("should call useInteractive hook with correct metric", () => { + const mockUseInteractive = jest.mocked(useInteractive); + + render(); + + expect(mockUseInteractive).toHaveBeenCalledWith("ApplicationLoad"); + }); +}); diff --git a/src/RootComponents/ExplorerContainer.tsx b/src/RootComponents/ExplorerContainer.tsx new file mode 100644 index 000000000..1bdbf679f --- /dev/null +++ b/src/RootComponents/ExplorerContainer.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Dialog } from "../Explorer/Controls/Dialog"; +import Explorer from "../Explorer/Explorer"; +import { CommandBar } from "../Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { NotificationConsole } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; +import { SidePanel } from "../Explorer/Panes/PanelContainerComponent"; +import { QueryCopilotCarousel } from "../Explorer/QueryCopilot/CopilotCarousel"; +import { QuickstartCarousel } from "../Explorer/Quickstart/QuickstartCarousel"; +import { MongoQuickstartTutorial } from "../Explorer/Quickstart/Tutorials/MongoQuickstartTutorial"; +import { SQLQuickstartTutorial } from "../Explorer/Quickstart/Tutorials/SQLQuickstartTutorial"; +import { SidebarContainer } from "../Explorer/Sidebar"; +import { useCarousel } from "../hooks/useCarousel"; +import MetricScenario from "../Metrics/MetricEvents"; +import { useInteractive } from "../Metrics/useMetricPhases"; + +const ExplorerContainer: React.FC<{ explorer: Explorer }> = ({ explorer }) => { + const isCarouselOpen = useCarousel((state) => state.shouldOpen); + const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel); + useInteractive(MetricScenario.ApplicationLoad); + + return ( +
+
+
+ + + +
+ + + {} + {} + {} + {} +
+ ); +}; + +export default ExplorerContainer; diff --git a/src/RootComponents/LoadingExplorer.test.tsx b/src/RootComponents/LoadingExplorer.test.tsx new file mode 100644 index 000000000..a8163d102 --- /dev/null +++ b/src/RootComponents/LoadingExplorer.test.tsx @@ -0,0 +1,71 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import React from "react"; +import LoadingExplorer from "./LoadingExplorer"; + +jest.mock("../../images/HdeConnectCosmosDB.svg", () => "test-hde-connect-image.svg"); + +jest.mock("@fluentui/react-components", () => ({ + makeStyles: jest.fn(() => () => ({ + root: "mock-root-class", + })), +})); + +describe("LoadingExplorer", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("should render loading explorer component", () => { + render(); + + const container = screen.getByRole("alert"); + expect(container).toBeInTheDocument(); + expect(container).toHaveTextContent("Connecting..."); + }); + + test("should display welcome title", () => { + render(); + + const title = screen.getByText("Welcome to Azure Cosmos DB"); + expect(title).toBeInTheDocument(); + expect(title).toHaveAttribute("id", "explorerLoadingStatusTitle"); + }); + + test("should display connecting status text", () => { + render(); + + const statusText = screen.getByText("Connecting..."); + expect(statusText).toBeInTheDocument(); + expect(statusText).toHaveAttribute("id", "explorerLoadingStatusText"); + expect(statusText).toHaveAttribute("role", "alert"); + }); + + test("should render Azure Cosmos DB image", () => { + render(); + + const image = screen.getByAltText("Azure Cosmos DB"); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute("src", "test-hde-connect-image.svg"); + }); + + test("should have correct class structure", () => { + render(); + + const splashContainer = document.querySelector(".splashLoaderContainer"); + expect(splashContainer).toBeInTheDocument(); + + const contentContainer = document.querySelector(".splashLoaderContentContainer"); + expect(contentContainer).toBeInTheDocument(); + + const connectContent = document.querySelector(".connectExplorerContent"); + expect(connectContent).toBeInTheDocument(); + }); + + test("should apply CSS classes correctly", () => { + const { container } = render(); + + const rootDiv = container.firstChild as HTMLElement; + expect(rootDiv).toHaveClass("mock-root-class"); + }); +}); diff --git a/src/RootComponents/LoadingExplorer.tsx b/src/RootComponents/LoadingExplorer.tsx new file mode 100644 index 000000000..7a8360922 --- /dev/null +++ b/src/RootComponents/LoadingExplorer.tsx @@ -0,0 +1,36 @@ +import { makeStyles } from "@fluentui/react-components"; +import React from "react"; +import hdeConnectImage from "../../images/HdeConnectCosmosDB.svg"; + +const useStyles = makeStyles({ + root: { + height: "100vh", + width: "100vw", + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", + }, +}); + +function LoadingExplorer(): JSX.Element { + const styles = useStyles(); + + return ( +
+
+
+

+ Azure Cosmos DB +

+

+ Welcome to Azure Cosmos DB +

+ +
+
+
+ ); +} + +export default LoadingExplorer; diff --git a/src/RootComponents/Root.test.tsx b/src/RootComponents/Root.test.tsx new file mode 100644 index 000000000..a5861a5b4 --- /dev/null +++ b/src/RootComponents/Root.test.tsx @@ -0,0 +1,107 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import React from "react"; +import Root from "./Root"; + +jest.mock("../Explorer/ErrorBoundary", () => ({ + ErrorBoundary: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +jest.mock("@fluentui/react-components", () => ({ + FluentProvider: ({ children, theme }: { children: React.ReactNode; theme: { colorNeutralBackground1: string } }) => ( +
+ {children} +
+ ), + webLightTheme: { colorNeutralBackground1: "light" }, + webDarkTheme: { colorNeutralBackground1: "dark" }, +})); + +jest.mock("./App", () => ({ + __esModule: true, + default: () =>
App
, +})); + +const createMockStore = (isDarkMode: boolean = false) => ({ + getState: jest.fn(() => ({ isDarkMode })), + subscribe: jest.fn(() => jest.fn()), +}); + +const mockThemeStore = createMockStore(false); + +jest.mock("../hooks/useTheme", () => ({ + get useThemeStore() { + return mockThemeStore; + }, +})); + +describe("Root", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("should render Root component with all child components", () => { + render(); + + expect(screen.getByTestId("mock-error-boundary")).toBeInTheDocument(); + expect(screen.getByTestId("mock-fluent-provider")).toBeInTheDocument(); + expect(screen.getByTestId("mock-app")).toBeInTheDocument(); + }); + + test("should have correct component hierarchy", () => { + render(); + + const errorBoundary = screen.getByTestId("mock-error-boundary"); + const fluentProvider = screen.getByTestId("mock-fluent-provider"); + const app = screen.getByTestId("mock-app"); + + expect(errorBoundary).toContainElement(fluentProvider); + expect(fluentProvider).toContainElement(app); + }); + + test("should subscribe to theme changes on mount", () => { + render(); + + expect(mockThemeStore.subscribe).toHaveBeenCalled(); + expect(mockThemeStore.subscribe).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("should get initial theme state", () => { + render(); + + expect(mockThemeStore.getState).toHaveBeenCalled(); + }); + + test("should handle component unmounting", () => { + const mockUnsubscribe = jest.fn(); + mockThemeStore.subscribe.mockReturnValue(mockUnsubscribe); + + const { unmount } = render(); + + unmount(); + + expect(mockUnsubscribe).toHaveBeenCalled(); + }); + + test("should call getState to initialize theme", () => { + render(); + + expect(mockThemeStore.getState).toHaveBeenCalledTimes(1); + }); + + test("should handle theme subscription properly", () => { + render(); + + expect(mockThemeStore.subscribe).toHaveBeenCalledTimes(1); + expect(mockThemeStore.getState).toHaveBeenCalled(); + }); + + test("should render without errors", () => { + expect(() => render()).not.toThrow(); + }); +}); diff --git a/src/RootComponents/Root.tsx b/src/RootComponents/Root.tsx new file mode 100644 index 000000000..31479c25a --- /dev/null +++ b/src/RootComponents/Root.tsx @@ -0,0 +1,28 @@ +import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; +import React from "react"; +import { ErrorBoundary } from "../Explorer/ErrorBoundary"; +import { useThemeStore } from "../hooks/useTheme"; +import App from "./App"; + +const Root: React.FC = () => { + // Use React state to track isDarkMode and subscribe to changes + const [isDarkMode, setIsDarkMode] = React.useState(useThemeStore.getState().isDarkMode); + const currentTheme = isDarkMode ? webDarkTheme : webLightTheme; + + // Subscribe to theme changes + React.useEffect(() => { + return useThemeStore.subscribe((state) => { + setIsDarkMode(state.isDarkMode); + }); + }, []); + + return ( + + + + + + ); +}; + +export default Root;