diff --git a/src/components/ConfigSection.jsx b/src/components/ConfigSection.jsx index 93da737..7c59c3d 100644 --- a/src/components/ConfigSection.jsx +++ b/src/components/ConfigSection.jsx @@ -20,16 +20,13 @@ import { TableRow, Paper, Autocomplete, - Collapse, } from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import EditIcon from "@mui/icons-material/Edit"; import InfoIcon from "@mui/icons-material/Info"; import DeleteIcon from "@mui/icons-material/Delete"; import {URL_OBJECT, URL_NODE} from "../config/apiPath.js"; -import { parseObjectPath } from "../utils/objectUtils"; +import {parseObjectPath} from "../utils/objectUtils"; const useConfig = (decodedObjectName, configNode, setConfigNode) => { const initialState = { @@ -604,7 +601,14 @@ const ManageParamsDialog = ({ ); }; -const ConfigSection = ({decodedObjectName, configNode, setConfigNode, openSnackbar, expanded, onToggle}) => { +const ConfigSection = ({ + decodedObjectName, + configNode, + setConfigNode, + openSnackbar, + configDialogOpen, + setConfigDialogOpen + }) => { const {data: configData, loading: configLoading, error: configError, fetchConfig} = useConfig( decodedObjectName, configNode, @@ -669,7 +673,7 @@ const ConfigSection = ({decodedObjectName, configNode, setConfigNode, openSnackb openSnackbar("Configuration updated successfully"); if (configNode) { await fetchConfig(configNode); - onToggle(true); + setConfigDialogOpen(true); } } catch (err) { openSnackbar(`Error: ${err.message}`, "error"); @@ -744,7 +748,7 @@ const ConfigSection = ({decodedObjectName, configNode, setConfigNode, openSnackb if (configNode) { await fetchConfig(configNode); await fetchExistingParams(); - onToggle(true); + setConfigDialogOpen(true); } } setActionLoading(false); @@ -791,7 +795,7 @@ const ConfigSection = ({decodedObjectName, configNode, setConfigNode, openSnackb if (configNode) { await fetchConfig(configNode); await fetchExistingParams(); - onToggle(true); + setConfigDialogOpen(true); } } setActionLoading(false); @@ -830,7 +834,7 @@ const ConfigSection = ({decodedObjectName, configNode, setConfigNode, openSnackb if (configNode) { await fetchConfig(configNode); await fetchExistingParams(); - onToggle(true); + setConfigDialogOpen(true); } } setActionLoading(false); @@ -855,115 +859,109 @@ const ConfigSection = ({decodedObjectName, configNode, setConfigNode, openSnackb }; return ( - - onToggle(!expanded)} + + - - - - - setUpdateConfigDialogOpen(true)} - disabled={actionLoading} - aria-label="Upload new configuration file" - size="small" - > - - - - - - - - - - - - - - - {configLoading && } - {configError && ( - - {configError} - - )} - {!configLoading && !configError && configData === null && ( - - No configuration available. - - )} - {!configLoading && !configError && configData !== null && ( - + setConfigDialogOpen(false)} maxWidth="lg" fullWidth> + Configuration + + + + + setUpdateConfigDialogOpen(true)} + disabled={actionLoading} + aria-label="Upload new configuration file" + size="small" + > + + + + + + + + + + + + + + + {configLoading && } + {configError && ( + + {configError} + + )} + {!configLoading && !configError && configData === null && ( + + No configuration available. + + )} + {!configLoading && !configError && configData !== null && ( - {configData} + + {configData} + - - )} - - + )} + + + + + + setIsHovered(false)} onClick={handleCardClick} > - + + {/* Bloc gauche : checkbox + nom */} e.stopPropagation()} className="no-click"> - - {/* Bouton pour voir les logs de l'instance */} + + {/* Bloc central : freeze / not provisioned / state, juste avant logs+status */} + + {frozen === "frozen" && ( + + + + )} + {isInstanceNotProvisioned && ( + + + + )} + {state && {state}} + + + {/* Bloc droite : logs + rond de status + bouton d’actions, fixes */} + { @@ -134,20 +169,7 @@ const NodeCard = ({ }} /> - {frozen === "frozen" && ( - - - - )} - {isInstanceNotProvisioned && ( - - - - )} - {state && {state}} + { e.stopPropagation(); diff --git a/src/components/ObjectDetails.jsx b/src/components/ObjectDetails.jsx index a00f220..49d6397 100644 --- a/src/components/ObjectDetails.jsx +++ b/src/components/ObjectDetails.jsx @@ -107,8 +107,8 @@ const ObjectDetail = () => { const [configData, setConfigData] = useState(null); const [configLoading, setConfigLoading] = useState(false); const [configError, setConfigError] = useState(null); - const [configExpanded, setConfigExpanded] = useState(false); const [configNode, setConfigNode] = useState(null); + const [configDialogOpen, setConfigDialogOpen] = useState(false); // States for batch & actions const [selectedNodes, setSelectedNodes] = useState([]); @@ -574,7 +574,7 @@ const ObjectDetail = () => { return; } await fetchConfig(matchingUpdate.node); - setConfigExpanded(true); + setConfigDialogOpen(true); openSnackbar("Configuration updated", "info"); } catch (err) { openSnackbar("Failed to load updated configuration", "error"); @@ -619,7 +619,7 @@ const ObjectDetail = () => { const config = newConfig[decodedObjectName]; if (config && configNode) { try { - setConfigExpanded(true); + setConfigDialogOpen(true); openSnackbar("Instance configuration updated", "info"); } catch (err) { openSnackbar("Failed to process instance configuration update", "error"); @@ -716,13 +716,8 @@ const ObjectDetail = () => { configNode={configNode} setConfigNode={setConfigNode} openSnackbar={openSnackbar} - handleManageParamsSubmit={() => { - }} - configData={configData} - configLoading={configLoading} - configError={configError} - expanded={configExpanded} - onToggle={() => setConfigExpanded(!configExpanded)} + configDialogOpen={configDialogOpen} + setConfigDialogOpen={setConfigDialogOpen} /> ); @@ -770,7 +765,7 @@ const ObjectDetail = () => { > {/* Header and Config Section in same line */} - + { objectMenuAnchorRef={objectMenuAnchorRef} /> - + { - }} - configData={configData} - configLoading={configLoading} - configError={configError} - expanded={configExpanded} - onToggle={() => setConfigExpanded(!configExpanded)} + configDialogOpen={configDialogOpen} + setConfigDialogOpen={setConfigDialogOpen} /> diff --git a/src/components/ObjectInstanceView.jsx b/src/components/ObjectInstanceView.jsx index 0428f01..acd85c8 100644 --- a/src/components/ObjectInstanceView.jsx +++ b/src/components/ObjectInstanceView.jsx @@ -44,7 +44,6 @@ import EventLogger from "../components/EventLogger"; import LogsViewer from "./LogsViewer"; import {useTheme} from "@mui/material/styles"; -// Constants for dialogs const DEFAULT_CHECKBOXES = {failover: false}; const DEFAULT_STOP_CHECKBOX = false; const DEFAULT_UNPROVISION_CHECKBOXES = {dataLoss: false, serviceInterruption: false}; @@ -398,7 +397,6 @@ const ObjectInstanceView = () => { const instanceMonitor = useEventStore((s) => s.instanceMonitor); const instanceConfig = useEventStore((s) => s.instanceConfig); - // Retrieve instance data const instanceData = objectInstanceStatus?.[decodedObjectName]?.[nodeName] || {}; const monitorData = instanceMonitor[`${nodeName}:${decodedObjectName}`] || {}; const configData = instanceConfig[decodedObjectName]?.[nodeName] || {resources: {}}; @@ -412,7 +410,6 @@ const ObjectInstanceView = () => { const [actionInProgress, setActionInProgress] = useState(false); const [snackbar, setSnackbar] = useState({open: false, message: "", severity: "success"}); - // States for dialogs const [pendingAction, setPendingAction] = useState(null); const [consoleDialogOpen, setConsoleDialogOpen] = useState(false); const [consoleUrlDialogOpen, setConsoleUrlDialogOpen] = useState(false); @@ -429,16 +426,13 @@ const ObjectInstanceView = () => { const [unprovisionCheckboxes, setUnprovisionCheckboxes] = useState(DEFAULT_UNPROVISION_CHECKBOXES); const [purgeCheckboxes, setPurgeCheckboxes] = useState(DEFAULT_PURGE_CHECKBOXES); - // States for logs const [logsDrawerOpen, setLogsDrawerOpen] = useState(false); const [drawerWidth, setDrawerWidth] = useState(600); const minDrawerWidth = 300; const maxDrawerWidth = window.innerWidth * 0.8; - // State for initial loading const [initialLoading, setInitialLoading] = useState(true); - // Ref for component mount status const isMounted = useRef(true); const instanceEventTypes = useMemo(() => [ @@ -447,18 +441,15 @@ const ObjectInstanceView = () => { "InstanceConfigUpdated", ], []); - // Effect to start/stop event reception useEffect(() => { isMounted.current = true; - // Start event reception const token = localStorage.getItem("authToken"); if (token) { const filters = instanceEventTypes.map(type => { if (["CONNECTION_OPENED", "CONNECTION_ERROR", "RECONNECTION_ATTEMPT", "MAX_RECONNECTIONS_REACHED", "CONNECTION_CLOSED"].includes(type)) { return type; } else { - // Filter events specific to this instance and node return `${type},path=${decodedObjectName},node=${nodeName}`; } }); @@ -466,14 +457,12 @@ const ObjectInstanceView = () => { startEventReception(token, filters); } - // Simulate initial loading const timer = setTimeout(() => { if (isMounted.current) { setInitialLoading(false); } }, 500); - // Cleanup return () => { isMounted.current = false; closeEventSource(); @@ -493,7 +482,6 @@ const ObjectInstanceView = () => { } }, []); - // Function to open action dialogs const openActionDialog = useCallback((action, context = null) => { if (isMounted.current) { setPendingAction({action, ...(context ? context : {})}); @@ -520,7 +508,6 @@ const ObjectInstanceView = () => { } }, []); - // Function to confirm actions const handleDialogConfirm = useCallback(async () => { if (!pendingAction || !pendingAction.action) { console.warn("No valid pendingAction or action provided:", pendingAction); @@ -547,7 +534,6 @@ const ObjectInstanceView = () => { let message = `Executing ${action}...`; if (pendingAction.rid) { - // Action on a resource if (action === "console") { url = `${URL_NODE}/${nodeName}/instance/path/${namespace}/${kind}/${name}/console?rid=${encodeURIComponent(pendingAction.rid)}&seats=${seats}&greet_timeout=${encodeURIComponent(greetTimeout)}`; message = `Opening console for resource ${pendingAction.rid}...`; @@ -556,7 +542,6 @@ const ObjectInstanceView = () => { message = `Executing ${action} on resource ${pendingAction.rid}...`; } } else { - // Action on the instance url = `${URL_NODE}/${nodeName}/instance/path/${namespace}/${kind}/${name}/action/${action}`; message = `Executing ${action} on instance...`; } @@ -606,12 +591,10 @@ const ObjectInstanceView = () => { } }, [nodeName, namespace, kind, name, pendingAction, seats, greetTimeout, openSnackbar]); - // Handler for instance actions const handleInstanceAction = useCallback((action) => { openActionDialog(action, {node: nodeName}); }, [nodeName, openActionDialog]); - // Handler for resource actions const handleResourceAction = useCallback((action, rid = null) => { openActionDialog(action, {node: nodeName, rid: rid || currentResourceId}); }, [nodeName, currentResourceId, openActionDialog]); @@ -739,13 +722,11 @@ const ObjectInstanceView = () => { return ''; }, [resources, encapResources]); - // Handler for resource actions const handleResourceActionClick = useCallback((rid, event) => { setCurrentResourceId(rid); setResourceMenuAnchor(event.currentTarget); }, []); - // Function to open logs const handleOpenLogs = useCallback(() => { setLogsDrawerOpen(true); }, []); @@ -786,22 +767,17 @@ const ObjectInstanceView = () => { document.body.style.cursor = "ew-resize"; }, [drawerWidth, minDrawerWidth, maxDrawerWidth]); - // Function to filter events by node const filterEventsByNode = useCallback((events) => { return events.filter(event => { - // For connection events, always include them if (event.eventType?.includes?.("CONNECTION")) return true; - // For other events, check if the node matches const data = event.data || {}; - // Check in various possible fields where node might be stored if (data.node === nodeName) return true; if (data.labels?.node === nodeName) return true; if (data.data?.node === nodeName) return true; if (data.data?.labels?.node === nodeName) return true; - // For InstanceStatusUpdated events, also check in the path if (data.path && data.path.includes(nodeName)) return true; return false; @@ -812,7 +788,6 @@ const ObjectInstanceView = () => { const isFrozen = instanceData.frozen_at && instanceData.frozen_at !== "0001-01-01T00:00:00Z"; const isInstanceNotProvisioned = instanceData.provisioned !== undefined ? !instanceData.provisioned : false; - // Display a loader during initial loading if (initialLoading) { return ( @@ -824,7 +799,6 @@ const ObjectInstanceView = () => { return ( - {/* Instance Header */} @@ -837,21 +811,6 @@ const ObjectInstanceView = () => { - {/* Button to view logs */} - - - - - - - - - - {isFrozen && ( @@ -870,6 +829,20 @@ const ObjectInstanceView = () => { )} + + + + + + + + + + setInstanceMenuAnchor(e.currentTarget)} disabled={actionInProgress} @@ -882,7 +855,6 @@ const ObjectInstanceView = () => { {actionInProgress && } - {/* Resources Section */} Resources ({Object.keys(resources).length}) @@ -962,7 +934,6 @@ const ObjectInstanceView = () => { )} - {/* Instance Actions Menu */} { - {/* Resource Actions Menu */} { - {/* Confirmation dialogs for actions */} - - {/* Freeze dialog */} setConfirmDialogOpen(false)} maxWidth="sm" fullWidth> Confirm Freeze @@ -1044,7 +1011,6 @@ const ObjectInstanceView = () => { - {/* Stop dialog */} setStopDialogOpen(false)} maxWidth="sm" fullWidth> Confirm Stop @@ -1070,7 +1036,6 @@ const ObjectInstanceView = () => { - {/* Unprovision dialog */} setUnprovisionDialogOpen(false)} maxWidth="sm" fullWidth> Confirm Unprovision @@ -1112,7 +1077,6 @@ const ObjectInstanceView = () => { - {/* Purge dialog */} setPurgeDialogOpen(false)} maxWidth="sm" fullWidth> Confirm Purge @@ -1159,7 +1123,6 @@ const ObjectInstanceView = () => { - {/* Console dialog */} setConsoleDialogOpen(false)} maxWidth="sm" fullWidth> Open Console @@ -1204,7 +1167,6 @@ const ObjectInstanceView = () => { - {/* Console URL dialog */} setConsoleUrlDialogOpen(false)} @@ -1254,7 +1216,6 @@ const ObjectInstanceView = () => { - {/* Simple confirmation dialog */} setSimpleDialogOpen(false)} maxWidth="xs" fullWidth> Confirm {pendingAction?.action ? pendingAction.action.charAt(0).toUpperCase() + pendingAction.action.slice(1) : 'Action'} @@ -1274,7 +1235,6 @@ const ObjectInstanceView = () => { - {/* EventLogger for instance events */} { buttonLabel="Instance Events" /> - {/* Drawer for logs */} {logsDrawerOpen && ( { )} - {/* Snackbar */} { const parts = objectName.split("/"); if (parts.length === 3) { @@ -537,17 +535,35 @@ const Objects = () => { }); }, [filteredObjectNames, sortColumn, sortDirection, objectStatusWithGlobalExpect, getNodeState, allNodes]); + const isUpdating = useRef(false); + const debouncedUpdateQuery = useMemo( () => debounce(() => { if (!isMounted.current) return; + + const currentParams = new URLSearchParams(location.search); + const currentGlobalState = currentParams.get("globalState") || "all"; + const currentNamespace = currentParams.get("namespace") || "all"; + const currentKind = currentParams.get("kind") || "all"; + const currentName = currentParams.get("name") || ""; + + if (currentGlobalState === selectedGlobalState && + currentNamespace === selectedNamespace && + currentKind === selectedKind && + currentName === searchQuery) { + return; + } + const newQueryParams = new URLSearchParams(); if (selectedGlobalState !== "all") newQueryParams.set("globalState", selectedGlobalState); if (selectedNamespace !== "all") newQueryParams.set("namespace", selectedNamespace); if (selectedKind !== "all") newQueryParams.set("kind", selectedKind); if (searchQuery.trim()) newQueryParams.set("name", searchQuery.trim()); + const queryString = newQueryParams.toString(); const newUrl = `${location.pathname}${queryString ? `?${queryString}` : ""}`; + if (newUrl !== location.pathname + location.search) { navigate(newUrl, {replace: true}); } @@ -556,34 +572,36 @@ const Objects = () => { ); useEffect(() => { - debouncedUpdateQuery(); - return () => { - if (debouncedUpdateQuery.cancel) { - debouncedUpdateQuery.cancel(); - } - }; + if (!isUpdating.current) { + isUpdating.current = true; + debouncedUpdateQuery(); + const timer = setTimeout(() => { + isUpdating.current = false; + }, 100); + return () => clearTimeout(timer); + } }, [debouncedUpdateQuery]); useEffect(() => { const newGlobalState = globalStates.includes(rawGlobalState) ? rawGlobalState : "all"; const newNamespace = rawNamespace; const newKind = rawKind; + const newSearchQuery = rawSearchQuery; - setSelectedGlobalState(newGlobalState); - setSelectedNamespace(newNamespace); - setSelectedKind(newKind); - }, [rawGlobalState, rawNamespace, rawKind, globalStates]); - - useEffect(() => { - setSearchQuery(rawSearchQuery); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + setSelectedGlobalState(prev => prev !== newGlobalState ? newGlobalState : prev); + setSelectedNamespace(prev => prev !== newNamespace ? newNamespace : prev); + setSelectedKind(prev => prev !== newKind ? newKind : prev); + setSearchQuery(prev => prev !== newSearchQuery ? newSearchQuery : prev); + }, [rawGlobalState, rawNamespace, rawKind, rawSearchQuery, globalStates]); useEffect(() => { return () => { isMounted.current = false; + if (debouncedUpdateQuery && typeof debouncedUpdateQuery.cancel === 'function') { + debouncedUpdateQuery.cancel(); + } }; - }, []); + }, [debouncedUpdateQuery]); const eventStarted = useRef(false); useEffect(() => { @@ -741,7 +759,6 @@ const Objects = () => { m: 0, overflow: 'hidden' }}> - {/* Filter controls */} { const mocks = { ...actual, - Collapse: ({children, in: inProp, ...props}) => - inProp ?
{children}
: null, - - Accordion: ({children, expanded, onChange, ...props}) => ( -
- {children} -
- ), - AccordionSummary: ({children, id, onChange, expanded, ...props}) => ( -
onChange?.({}, !expanded)} - {...props} - > - {children} -
- ), - AccordionDetails: ({children, ...props}) => ( -
- {children} -
- ), Dialog: ({children, open, maxWidth, fullWidth, ...props}) => open ?
{children}
: null, - DialogTitle: ({children, ...props}) =>
{children}
, + DialogTitle: ({children, ...props}) =>

{children}

, DialogContent: ({children, ...props}) =>
{children}
, DialogActions: ({children, ...props}) =>
{children}
, Alert: ({children, severity, ...props}) => ( @@ -145,8 +121,13 @@ jest.mock('@mui/material', () => { ), Box: ({children, sx, ...props}) =>
{children}
, Tooltip: ({children, title}) => {children}, - IconButton: ({children, onClick, disabled, ...props}) => ( - ), @@ -167,8 +148,6 @@ jest.mock('@mui/material', () => { jest.mock('@mui/icons-material/UploadFile', () => () => ); jest.mock('@mui/icons-material/Edit', () => () => ); jest.mock('@mui/icons-material/Info', () => () => ); -jest.mock('@mui/icons-material/ExpandMore', () => () => ); -jest.mock('@mui/icons-material/ExpandLess', () => () => ); jest.mock('@mui/icons-material/Delete', () => () => ); // Mock localStorage @@ -183,6 +162,7 @@ describe('ConfigSection Component', () => { const user = userEvent.setup(); const setConfigNode = jest.fn(); const openSnackbar = jest.fn(); + const setConfigDialogOpen = jest.fn(); beforeEach(() => { jest.setTimeout(30000); @@ -296,39 +276,81 @@ size = 10GB jest.resetAllMocks(); }); + // Helper function to find buttons by their text content + const getViewConfigButton = () => screen.getByText('View Configuration'); const getUploadButton = () => screen.getByRole('button', {name: /Upload new configuration file/i}); const getManageParamsButton = () => screen.getByRole('button', {name: /Manage configuration parameters/i}); const getKeywordsButton = () => screen.getByRole('button', {name: /View configuration keywords/i}); - test('displays configuration with horizontal scrolling', async () => { + test('displays configuration button', async () => { + render( + + ); + + expect(getViewConfigButton()).toBeInTheDocument(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + test('opens configuration dialog when button is clicked', async () => { + render( + + ); + + const viewConfigButton = getViewConfigButton(); + await act(async () => { + await user.click(viewConfigButton); + }); + + expect(setConfigDialogOpen).toHaveBeenCalledWith(true); + }); + + test('displays configuration dialog content when open', async () => { render( ); + // Wait for the dialog to appear + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); + + // Check for dialog title + expect(screen.getByText('Configuration')).toBeInTheDocument(); + + // Check for configuration content await waitFor(() => { expect(screen.getByText(/nodes = \*/i)).toBeInTheDocument(); }, {timeout: 10000}); + await waitFor(() => { expect(screen.getByText(/orchestrate = ha/i)).toBeInTheDocument(); }, {timeout: 10000}); + await waitFor(() => { expect(screen.getByText(/size = 10GB/i)).toBeInTheDocument(); }, {timeout: 10000}); - - const configContent = screen.getByTestId('collapse-content'); - expect(configContent).toBeInTheDocument(); - - const scrollableBox = configContent.querySelector('div[style*="overflow-x: auto"]'); - expect(scrollableBox).toBeInTheDocument(); - expect(scrollableBox).toHaveStyle({'overflow-x': 'auto'}); - }, 15000); + }); test('displays error when fetching configuration fails', async () => { global.fetch.mockImplementationOnce(() => @@ -345,14 +367,16 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent(/Failed to fetch config: HTTP 500/i); + expect(screen.getByRole('alert')).toBeInTheDocument(); }, {timeout: 10000}); + + expect(screen.getByRole('alert')).toHaveTextContent(/Failed to fetch config: HTTP 500/i); }); test('displays loading indicator while fetching configuration', async () => { @@ -365,8 +389,8 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); @@ -375,47 +399,36 @@ size = 10GB }, {timeout: 5000}); }); - test('displays no configuration when configNode is missing', async () => { + test('updates configuration file successfully', async () => { render( ); + // Wait for the configuration dialog to appear await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent(/No node available to fetch configuration/i); + const dialogs = screen.getAllByRole('dialog'); + expect(dialogs.length).toBeGreaterThan(0); }, {timeout: 5000}); - expect(global.fetch).not.toHaveBeenCalled(); - }); - - test('updates configuration file successfully', async () => { - render( - - ); - + // Click upload button const uploadButton = getUploadButton(); await act(async () => { await user.click(uploadButton); }); + // Wait for update config dialog to appear await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Update Configuration/i); + expect(screen.getByText(/Update Configuration/i)).toBeInTheDocument(); }, {timeout: 5000}); - // eslint-disable-next-line testing-library/no-node-access + // Find file input and upload file const fileInput = document.querySelector('#update-config-file-upload'); const testFile = new File(['[DEFAULT]\nnodes = node2'], 'config.ini'); await act(async () => { @@ -426,6 +439,7 @@ size = 10GB expect(screen.getByText('config.ini')).toBeInTheDocument(); }, {timeout: 5000}); + // Find and click update button const updateButton = screen.getByRole('button', {name: /Update/i}); await act(async () => { await user.click(updateButton); @@ -434,6 +448,7 @@ size = 10GB await waitFor(() => { expect(openSnackbar).toHaveBeenCalledWith('Updating configuration…', 'info'); }, {timeout: 10000}); + await waitFor(() => { expect(openSnackbar).toHaveBeenCalledWith('Configuration updated successfully'); }, {timeout: 10000}); @@ -451,7 +466,7 @@ size = 10GB ); await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.queryByText('Update Configuration')).not.toBeInTheDocument(); }, {timeout: 10000}); }); @@ -462,18 +477,24 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); + // Wait for the configuration dialog to appear + await waitFor(() => { + const dialogs = screen.getAllByRole('dialog'); + expect(dialogs.length).toBeGreaterThan(0); + }, {timeout: 5000}); + const uploadButton = getUploadButton(); await act(async () => { await user.click(uploadButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Update Configuration/i); + expect(screen.getByText(/Update Configuration/i)).toBeInTheDocument(); }, {timeout: 5000}); const updateButton = screen.getByRole('button', {name: /Update/i}); @@ -490,21 +511,26 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); + // Wait for the configuration dialog to appear + await waitFor(() => { + const dialogs = screen.getAllByRole('dialog'); + expect(dialogs.length).toBeGreaterThan(0); + }, {timeout: 5000}); + const uploadButton = getUploadButton(); await act(async () => { await user.click(uploadButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Update Configuration/i); + expect(screen.getByText(/Update Configuration/i)).toBeInTheDocument(); }, {timeout: 5000}); - // eslint-disable-next-line testing-library/no-node-access const fileInput = document.querySelector('#update-config-file-upload'); const testFile = new File(['new config content'], 'config.ini'); await act(async () => { @@ -525,8 +551,9 @@ size = 10GB expect.any(Object) ); + // The update dialog should still be open await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText('Update Configuration')).toBeInTheDocument(); }, {timeout: 10000}); }); @@ -556,21 +583,26 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); + // Wait for the configuration dialog to appear + await waitFor(() => { + const dialogs = screen.getAllByRole('dialog'); + expect(dialogs.length).toBeGreaterThan(0); + }, {timeout: 5000}); + const uploadButton = getUploadButton(); await act(async () => { await user.click(uploadButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Update Configuration/i); + expect(screen.getByText(/Update Configuration/i)).toBeInTheDocument(); }, {timeout: 5000}); - // eslint-disable-next-line testing-library/no-node-access const fileInput = document.querySelector('#update-config-file-upload'); const testFile = new File(['new config content'], 'config.ini'); await act(async () => { @@ -585,281 +617,31 @@ size = 10GB await waitFor(() => { expect(openSnackbar).toHaveBeenCalledWith('Updating configuration…', 'info'); }, {timeout: 10000}); - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Error: Failed to update config: 500', 'error'); - }, {timeout: 10000}); await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(openSnackbar).toHaveBeenCalledWith('Error: Failed to update config: 500', 'error'); }, {timeout: 10000}); - }); - - test('parses object path with edge cases', async () => { - render( - - ); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('No node available to fetch configuration'); - }, {timeout: 5000}); - - jest.clearAllMocks(); - - render( - - ); await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining(`${URL_NODE}/node1/instance/path/root/ccfg/cluster/config/file`), - expect.any(Object) - ); + expect(screen.queryByText('Update Configuration')).not.toBeInTheDocument(); }, {timeout: 10000}); }); - test('debounces fetchConfig calls', async () => { - const onToggle = jest.fn(); - const {rerender} = render( - - ); - - await act(async () => { - rerender( - - ); - }); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(1); - }, {timeout: 2000}); - }); - - test('displays keywords dialog and its content', async () => { + test('handles add parameters with invalid parameter', async () => { render( ); - const keywordsButton = getKeywordsButton(); - await act(async () => { - await user.click(keywordsButton); - }); - - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - expect(dialog).toHaveTextContent(/Configuration Keywords/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const table = within(dialog).getByRole('table'); - - await waitFor(() => { - expect(within(table).getByRole('row', {name: /nodes/})).toBeInTheDocument(); - }, {timeout: 10000}); - - await waitFor(() => { - expect(within(table).getByRole('row', {name: /size/})).toBeInTheDocument(); - }, {timeout: 10000}); - - const nodesRow = within(table).getByRole('row', {name: /nodes/}); - expect(within(nodesRow).getByText('Nodes to deploy the service')).toBeInTheDocument(); - expect(within(nodesRow).getByText('string')).toBeInTheDocument(); - expect(within(nodesRow).getByText('DEFAULT')).toBeInTheDocument(); - expect(within(nodesRow).getByText('Yes')).toBeInTheDocument(); - - const sizeRow = within(table).getByRole('row', {name: /size/}); - expect(within(sizeRow).getByText('Size of filesystem')).toBeInTheDocument(); - expect(within(sizeRow).getByText('fs')).toBeInTheDocument(); - expect(within(sizeRow).getByText('No')).toBeInTheDocument(); - - const closeButton = screen.getByRole('button', {name: /Close/i}); - await act(async () => { - await user.click(closeButton); - }); - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }, {timeout: 5000}); - }); - - test('handles keywords fetch timeout', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config/keywords')) { - return new Promise((resolve, reject) => { - setTimeout(() => { - const error = new Error('Request timed out'); - error.name = 'AbortError'; - reject(error); - }, 1000); - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const keywordsButton = getKeywordsButton(); - await act(async () => { - await user.click(keywordsButton); - }); - - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - expect(dialog).toHaveTextContent(/Configuration Keywords/i); - }, {timeout: 2000}); - - await waitFor(() => { - const alert = within(screen.getByRole('dialog')).getByRole('alert'); - expect(alert).toHaveTextContent(/Request timed out after 60 seconds/i); - }, {timeout: 2000}); - }); - - test('handles keywords fetch with invalid response', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config/keywords')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({}), - headers: new Headers({'Content-Length': '0'}), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const keywordsButton = getKeywordsButton(); - await act(async () => { - await user.click(keywordsButton); - }); - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Configuration Keywords/i); - }, {timeout: 10000}); - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const alert = within(dialog).getByRole('alert'); - expect(alert).toHaveTextContent(/Invalid response format: missing items/i); - }, {timeout: 10000}); - }); - - test('displays no keywords when none are available', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config/keywords')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const keywordsButton = getKeywordsButton(); - await act(async () => { - await user.click(keywordsButton); - }); - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Configuration Keywords/i); - }, {timeout: 10000}); - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const table = within(dialog).getByRole('table'); - const rows = within(table).getAllByRole('row'); - expect(rows).toHaveLength(1); // Only header row - }, {timeout: 10000}); - }); - - test('handles add parameters with invalid parameter', async () => { - render( - - ); const manageParamsButton = getManageParamsButton(); await act(async () => { @@ -867,15 +649,14 @@ size = 10GB }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); await waitFor(() => { expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }, {timeout: 10000}); - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); const addParamsInput = comboboxes[0]; // First combobox for add parameters await act(async () => { await user.type(addParamsInput, 'invalid_param{Enter}'); @@ -902,26 +683,29 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); + const manageParamsButton = getManageParamsButton(); await act(async () => { await user.click(manageParamsButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); await waitFor(() => { expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }, {timeout: 10000}); - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); const addParamsInput = comboboxes[0]; await act(async () => { await user.type(addParamsInput, 'fs.size{Enter}'); @@ -952,7 +736,7 @@ size = 10GB await user.type(valueInput, '20GB'); }); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); + const applyButton = screen.getByRole('button', {name: /Apply/i}); await act(async () => { await user.click(applyButton); }); @@ -962,33 +746,36 @@ size = 10GB }, {timeout: 10000}); }); - test('handles add parameters with missing section for non-DEFAULT keyword', async () => { + test('handles add parameters successfully with indexed section', async () => { render( ); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); + const manageParamsButton = getManageParamsButton(); await act(async () => { await user.click(manageParamsButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); await waitFor(() => { expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }, {timeout: 10000}); - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); const addParamsInput = comboboxes[0]; // First combobox for add parameters await act(async () => { await user.type(addParamsInput, 'fs.size{Enter}'); @@ -1009,7 +796,7 @@ size = 10GB const sectionInput = screen.getByPlaceholderText('Index e.g. 1'); await act(async () => { - await user.clear(sectionInput); + await user.type(sectionInput, '2'); }); const valueInput = screen.getByLabelText('Value'); @@ -1017,78 +804,13 @@ size = 10GB await user.type(valueInput, '20GB'); }); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); + const applyButton = screen.getByRole('button', {name: /Apply/i}); await act(async () => { await user.click(applyButton); }); await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Section index is required for parameter: size', 'error'); - }, {timeout: 10000}); - }); - - test('handles add parameters successfully with indexed section', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; // First combobox for add parameters - await act(async () => { - await user.type(addParamsInput, 'fs.size{Enter}'); - }); - - await waitFor(() => { - expect(addParamsInput).toHaveValue('fs.size'); - }, {timeout: 5000}); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - await waitFor(() => { - expect(screen.getByText('size')).toBeInTheDocument(); - }, {timeout: 5000}); - - const sectionInput = screen.getByPlaceholderText('Index e.g. 1'); - await act(async () => { - await user.type(sectionInput, '2'); - }); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, '20GB'); - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Successfully added 1 parameter(s)', 'success'); + expect(openSnackbar).toHaveBeenCalledWith('Successfully added 1 parameter(s)', 'success'); }, {timeout: 10000}); expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config?set=fs%232.size=20GB`), @@ -1100,7 +822,7 @@ size = 10GB }) ); await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.queryByText(/Manage Configuration Parameters/i)).not.toBeInTheDocument(); }, {timeout: 10000}); }); @@ -1111,26 +833,29 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); + const manageParamsButton = getManageParamsButton(); await act(async () => { await user.click(manageParamsButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); await waitFor(() => { expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); }, {timeout: 10000}); - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); const unsetParamsInput = comboboxes[1]; // Second combobox for unset parameters await act(async () => { await user.type(unsetParamsInput, 'nodes{Enter}'); @@ -1140,7 +865,7 @@ size = 10GB expect(unsetParamsInput).toHaveValue('nodes'); }, {timeout: 10000}); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); + const applyButton = screen.getByRole('button', {name: /Apply/i}); await act(async () => { await user.click(applyButton); }); @@ -1158,7 +883,7 @@ size = 10GB }) ); await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.queryByText(/Manage Configuration Parameters/i)).not.toBeInTheDocument(); }, {timeout: 10000}); }); @@ -1187,22 +912,25 @@ size = 10GB configNode="node1" setConfigNode={setConfigNode} openSnackbar={openSnackbar} - expanded={true} - onToggle={jest.fn()} + configDialogOpen={true} + setConfigDialogOpen={setConfigDialogOpen} /> ); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); + const manageParamsButton = getManageParamsButton(); await act(async () => { await user.click(manageParamsButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); const unsetParamsInput = comboboxes[1]; // Second combobox for unset parameters await act(async () => { await user.type(unsetParamsInput, 'nodes{Enter}'); @@ -1212,7 +940,7 @@ size = 10GB expect(unsetParamsInput).toHaveValue('nodes'); }, {timeout: 10000}); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); + const applyButton = screen.getByRole('button', {name: /Apply/i}); await act(async () => { await user.click(applyButton); }); @@ -1224,188 +952,75 @@ size = 10GB ); }, {timeout: 10000}); await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); }); - test('handles getUniqueSections with null keywordsData', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config/keywords')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: null}), - headers: new Headers(), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - + test('handles delete sections successfully', async () => { render( ); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); + const manageParamsButton = getManageParamsButton(); await act(async () => { await user.click(manageParamsButton); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - const comboboxes = within(screen.getByRole('dialog')).getAllByRole('combobox', { - name: /autocomplete-input/i, - }); - const addParamsInput = comboboxes[0]; - await waitFor(() => { - expect(addParamsInput).toHaveValue(''); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); - }); - - test('handles getExistingSections with null existingParams', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config') && !url.includes('file') && !url.includes('set') && !url.includes('unset') && !url.includes('delete')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: null}), - headers: new Headers(), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - const manageParamsButton = getManageParamsButton(); + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); + const deleteSectionsInput = comboboxes[2]; // Third combobox for delete sections await act(async () => { - await user.click(manageParamsButton); + await user.type(deleteSectionsInput, 'fs#1{Enter}'); }); await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - const comboboxes = within(screen.getByRole('dialog')).getAllByRole('combobox', { - name: /autocomplete-input/i, - }); - const deleteSectionsInput = comboboxes[2]; - await waitFor(() => { - expect(deleteSectionsInput).toHaveValue(''); + expect(deleteSectionsInput).toHaveValue('fs#1'); }, {timeout: 10000}); - }); - - test('handles duplicate keywords in keywords dialog', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config/keywords')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => - Promise.resolve({ - items: [ - { - option: 'nodes', - section: 'DEFAULT', - text: 'Nodes to deploy the service', - converter: 'string', - scopable: true, - default: '*', - }, - { - option: 'nodes', - section: 'DEFAULT', - text: 'Duplicate nodes entry', - converter: 'string', - scopable: false, - default: 'none', - }, - ], - }), - headers: new Headers({'Content-Length': '1024'}), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - const keywordsButton = getKeywordsButton(); + const applyButton = screen.getByRole('button', {name: /Apply/i}); await act(async () => { - await user.click(keywordsButton); + await user.click(applyButton); }); await waitFor(() => { - const dialog = screen.getByRole('dialog'); - expect(dialog).toHaveTextContent(/Configuration Keywords/i); - }, {timeout: 10000}); - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const table = within(dialog).getByRole('table'); - const rows = within(table).getAllByRole('row'); - expect(rows).toHaveLength(2); // Header row + one data row (duplicate filtered) - }, {timeout: 10000}); - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const table = within(dialog).getByRole('table'); - const nodesRow = within(table).getByRole('row', {name: /nodes/}); - expect(within(nodesRow).getByText('Nodes to deploy the service')).toBeInTheDocument(); + expect(openSnackbar).toHaveBeenCalledWith('Successfully deleted 1 section(s)', 'success'); }, {timeout: 10000}); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config?delete=fs%231`), + expect.objectContaining({ + method: 'PATCH', + headers: expect.objectContaining({ + Authorization: 'Bearer mock-token', + }), + }) + ); await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const table = within(dialog).getByRole('table'); - const nodesRow = within(table).getByRole('row', {name: /nodes/}); - expect(within(nodesRow).queryByText('Duplicate nodes entry')).not.toBeInTheDocument(); + expect(screen.queryByText(/Manage Configuration Parameters/i)).not.toBeInTheDocument(); }, {timeout: 10000}); }); - test('handles update config with no configNode', async () => { + test('handles delete sections with API failure', async () => { global.fetch.mockImplementation((url, options) => { const headers = options?.headers || {}; - if (url.includes('/config/file')) { + if (url.includes('/config?delete=')) { return Promise.resolve({ - ok: true, - status: 200, - text: () => Promise.resolve(''), + ok: false, + status: 500, + json: () => Promise.resolve({}), headers: new Headers({Authorization: headers.Authorization || ''}), }); } @@ -1420,1305 +1035,109 @@ size = 10GB render( ); - const uploadButton = getUploadButton(); - await act(async () => { - await user.click(uploadButton); - }); - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Update Configuration/i); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }, {timeout: 5000}); - // eslint-disable-next-line testing-library/no-node-access - const fileInput = document.querySelector('#update-config-file-upload'); - const testFile = new File(['new config content'], 'config.ini'); + + const manageParamsButton = getManageParamsButton(); await act(async () => { - await user.upload(fileInput, testFile); + await user.click(manageParamsButton); }); - const updateButton = screen.getByRole('button', {name: /Update/i}); + await waitFor(() => { + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); + }, {timeout: 10000}); + + const comboboxes = screen.getAllByRole('combobox', {name: /autocomplete-input/i}); + const deleteSectionsInput = comboboxes[2]; await act(async () => { - await user.click(updateButton); + await user.type(deleteSectionsInput, 'fs#1{Enter}'); }); await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Updating configuration…', 'info'); + expect(deleteSectionsInput).toHaveValue('fs#1'); }, {timeout: 10000}); + + const applyButton = screen.getByRole('button', {name: /Apply/i}); + await act(async () => { + await user.click(applyButton); + }); + await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Configuration updated successfully'); + expect(openSnackbar).toHaveBeenCalledWith('Error deleting section fs#1: Failed to delete section fs#1: 500', 'error'); }, {timeout: 10000}); - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config/file`), - expect.objectContaining({ - method: 'PUT', - headers: expect.objectContaining({ - Authorization: 'Bearer mock-token', - 'Content-Type': 'application/octet-stream', - }), - body: testFile, - }) - ); await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); }); - test('handles unset parameters with missing token', async () => { - mockLocalStorage.getItem.mockImplementation(() => null); + test('handles manage params submit with no selections', async () => { render( ); - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }, {timeout: 5000}); - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const unsetParamsInput = comboboxes[1]; // Second combobox for unset parameters + const manageParamsButton = getManageParamsButton(); await act(async () => { - await user.type(unsetParamsInput, 'nodes{Enter}'); + await user.click(manageParamsButton); }); await waitFor(() => { - expect(unsetParamsInput).toHaveValue('nodes'); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); + const applyButton = screen.getByRole('button', {name: /Apply/i}); await act(async () => { await user.click(applyButton); }); await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Auth token not found.', 'error'); + expect(openSnackbar).toHaveBeenCalledWith('No selection made', 'error'); }, {timeout: 10000}); - expect(global.fetch).not.toHaveBeenCalledWith( - expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config?unset=nodes`), - expect.any(Object) - ); await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText(/Manage Configuration Parameters/i)).toBeInTheDocument(); }, {timeout: 10000}); }); - test('handles delete sections with missing token', async () => { - mockLocalStorage.getItem.mockImplementation(() => null); + test('closes configuration dialog when close button is clicked', async () => { render( ); - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const deleteSectionsInput = comboboxes[2]; // Third combobox for delete sections - await act(async () => { - await user.type(deleteSectionsInput, 'fs#1{Enter}'); - }); - - await waitFor(() => { - expect(deleteSectionsInput).toHaveValue('fs#1'); - }, {timeout: 10000}); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Auth token not found.', 'error'); - }, {timeout: 10000}); - expect(global.fetch).not.toHaveBeenCalledWith( - expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config?delete=fs%231`), - expect.any(Object) - ); await waitFor(() => { expect(screen.getByRole('dialog')).toBeInTheDocument(); - }, {timeout: 10000}); - }); - - test('handles add parameters with indexed section', async () => { - global.fetch.mockImplementation((url, options) => { - const headers = options?.headers || {}; - if (url.includes('/config/keywords')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => - Promise.resolve({ - items: [ - { - option: 'timeout', - section: 'task', - text: 'Task timeout duration', - converter: 'string', - scopable: false, - default: '30s', - }, - ], - }), - headers: new Headers({Authorization: headers.Authorization || '', 'Content-Length': '1024'}), - }); - } - if (url.includes('/config?set=') || url.includes('/config?unset=') || url.includes('/config?delete=')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({}), - text: () => Promise.resolve(''), - headers: new Headers({Authorization: headers.Authorization || ''}), - }); - } - if (url.includes('/config')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers({Authorization: headers.Authorization || ''}), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers({Authorization: headers.Authorization || ''}), - }); - }); - - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; // First combobox for add parameters - await act(async () => { - await user.type(addParamsInput, 'task.timeout{Enter}'); - }); - - await waitFor(() => { - expect(addParamsInput).toHaveValue('task.timeout'); }, {timeout: 5000}); - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - await waitFor(() => { - expect(screen.getByText('timeout')).toBeInTheDocument(); - }, {timeout: 5000}); - - const sectionInput = screen.getByPlaceholderText('Index e.g. 1'); - await act(async () => { - await user.clear(sectionInput); - await user.type(sectionInput, '1'); - }); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, '60s'); - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Successfully added 1 parameter(s)', 'success'); - }, {timeout: 10000}); - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config?set=task%231.timeout=60s`), - expect.objectContaining({ - method: 'PATCH', - headers: expect.objectContaining({ - Authorization: 'Bearer mock-token', - }), - }) - ); - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }, {timeout: 10000}); - }); - - test('handles delete sections successfully', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const deleteSectionsInput = comboboxes[2]; // Third combobox for delete sections - await act(async () => { - await user.type(deleteSectionsInput, 'fs#1{Enter}'); - }); - - await waitFor(() => { - expect(deleteSectionsInput).toHaveValue('fs#1'); - }, {timeout: 10000}); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Successfully deleted 1 section(s)', 'success'); - }, {timeout: 10000}); - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining(`${URL_OBJECT}/root/cfg/cfg1/config?delete=fs%231`), - expect.objectContaining({ - method: 'PATCH', - headers: expect.objectContaining({ - Authorization: 'Bearer mock-token', - }), - }) - ); - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }, {timeout: 10000}); - }); - - test('handles delete sections with API failure', async () => { - global.fetch.mockImplementation((url, options) => { - const headers = options?.headers || {}; - if (url.includes('/config?delete=')) { - return Promise.resolve({ - ok: false, - status: 500, - json: () => Promise.resolve({}), - headers: new Headers({Authorization: headers.Authorization || ''}), - }); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers({Authorization: headers.Authorization || ''}), - }); - }); - - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const deleteSectionsInput = comboboxes[2]; - await act(async () => { - await user.type(deleteSectionsInput, 'fs#1{Enter}'); - }); - - await waitFor(() => { - expect(deleteSectionsInput).toHaveValue('fs#1'); - }, {timeout: 10000}); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Error deleting section fs#1: Failed to delete section fs#1: 500', 'error'); - }, {timeout: 10000}); - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }, {timeout: 10000}); - }); - - test('handles manage params submit with no selections', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); + const closeButton = screen.getByRole('button', {name: /Close/i}); await act(async () => { - await user.click(manageParamsButton); + await user.click(closeButton); }); - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('No selection made', 'error'); - }, {timeout: 10000}); - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }, {timeout: 10000}); - }); - - test('handles fetchConfig error with network failure', async () => { - global.fetch.mockImplementationOnce(() => - Promise.reject(new Error('Network error')) - ); - - render( - - ); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent(/Failed to fetch config: Network error/i); - }, {timeout: 10000}); - }); - - test('handles fetchKeywords with network error', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config/keywords')) { - return Promise.reject(new Error('Network error')); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const keywordsButton = getKeywordsButton(); - await act(async () => { - await user.click(keywordsButton); - }); - - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - expect(dialog).toHaveTextContent(/Configuration Keywords/i); - }, {timeout: 10000}); - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const alert = within(dialog).getByRole('alert'); - expect(alert).toHaveTextContent(/Failed to fetch keywords: Network error/i); - }, {timeout: 10000}); - }); - - test('handles fetchExistingParams with network error', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config') && !url.includes('file') && !url.includes('set') && !url.includes('unset') && !url.includes('delete')) { - return Promise.reject(new Error('Network error')); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - await waitFor(() => { - const dialog = screen.getByRole('dialog'); - const alerts = within(dialog).getAllByRole('alert'); - const existingParamsError = alerts.find(alert => - alert.textContent.includes('Failed to fetch existing parameters') - ); - expect(existingParamsError).toBeInTheDocument(); - }, {timeout: 10000}); - }); - - test('handles add parameters with decimal index', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - await act(async () => { - await user.type(addParamsInput, 'fs.size{Enter}'); - }); - - await waitFor(() => { - expect(addParamsInput).toHaveValue('fs.size'); - }, {timeout: 5000}); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - await waitFor(() => { - expect(screen.getByText('size')).toBeInTheDocument(); - }, {timeout: 5000}); - - const sectionInput = screen.getByPlaceholderText('Index e.g. 1'); - await act(async () => { - await user.clear(sectionInput); - await user.type(sectionInput, '1.5'); // Decimal index - }); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, '20GB'); - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Invalid index for size: must be a non-negative integer', 'error'); - }, {timeout: 10000}); - }); - - test('handles unset parameters with undefined option', async () => { - const originalFetch = global.fetch; - global.fetch = jest.fn((url) => { - if (url.includes('/config') && !url.includes('file') && !url.includes('set') && !url.includes('unset') && !url.includes('delete')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ - items: [ - {keyword: 'valid.param', value: 'test'}, - ], - }), - headers: new Headers(), - }); - } - if (url.includes('/config/keywords')) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - } - return originalFetch(url); - }); - - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('No selection made', 'error'); - }, {timeout: 10000}); - - global.fetch = originalFetch; - }); - - test('debounces fetchConfig calls within 1 second', async () => { - const onToggle = jest.fn(); - const {rerender} = render( - - ); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(1); - }); - - rerender( - - ); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(1); - }); - }, 15000); - - test('handles fetchConfig with network error', async () => { - global.fetch.mockImplementationOnce(() => - Promise.reject(new Error('Network failure')) - ); - - render( - - ); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent(/Failed to fetch config: Network failure/i); - }, {timeout: 10000}); - }); - - test('handles parseObjectPath with various input formats', async () => { - render( - - ); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('No node available to fetch configuration'); - }, {timeout: 5000}); - - render( - - ); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/instance/path/root/svc//config/file'), - expect.any(Object) - ); - }, {timeout: 10000}); - }); - - test('handles parseObjectPath with single part object name', async () => { - render( - - ); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/instance/path/root/ccfg/cluster/config/file'), - expect.any(Object) - ); - }, {timeout: 10000}); - }); - - test('handles parseObjectPath with two part object name', async () => { - render( - - ); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/instance/path/root/svc/service1/config/file'), - expect.any(Object) - ); - }, {timeout: 10000}); - }); - - test('handles reducer default case in useConfig', async () => { - - const {rerender} = render( - - ); - - rerender( - - ); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/node2/instance/path/root/cfg/cfg1/config/file'), - expect.any(Object) - ); - }, {timeout: 10000}); - }); - - test('handles manage params dialog state reset on close', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - - await act(async () => { - await user.type(addParamsInput, 'nodes{Enter}'); - }); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - const cancelButton = within(dialog).getByRole('button', {name: /Cancel/i}); - await act(async () => { - await user.click(cancelButton); - }); - - await waitFor(() => { - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }, {timeout: 5000}); - - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const newDialog = screen.getByRole('dialog'); - expect(within(newDialog).queryByText('nodes')).not.toBeInTheDocument(); - }); - - test('handles add parameters with zero index for indexed parameter', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - await act(async () => { - await user.type(addParamsInput, 'fs.size{Enter}'); - }); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - const sectionInput = screen.getByPlaceholderText('Index e.g. 1'); - await act(async () => { - await user.clear(sectionInput); - await user.type(sectionInput, '0'); // Index 0 - }); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, '5GB'); - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Successfully added 1 parameter(s)', 'success'); - }, {timeout: 10000}); - }); - - test('handles add parameters with TListLowercase converter - invalid comma-separated values', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - - await act(async () => { - await user.type(addParamsInput, 'DEFAULT.roles{Enter}'); - }); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - await waitFor(() => { - expect(screen.getByText('roles')).toBeInTheDocument(); - }, {timeout: 5000}); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, 'admin,,user'); // Empty value in comma-separated list - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith( - expect.stringContaining('Invalid value for roles: must be comma-separated lowercase strings'), - 'error' - ); - }, {timeout: 10000}); - }); - - test('handles add parameters with TListLowercase converter - valid comma-separated values', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - await waitFor(() => { - expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - - await act(async () => { - await user.type(addParamsInput, 'DEFAULT.roles{Enter}'); - }); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, 'admin,user,guest'); // Valid comma-separated values - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Successfully added 1 parameter(s)', 'success'); - }, {timeout: 10000}); - }); - - test('handles add parameters with TListLowercase converter - no commas', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - - await act(async () => { - await user.type(addParamsInput, 'DEFAULT.roles{Enter}'); - }); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - const valueInput = screen.getByLabelText('Value'); - await act(async () => { - await user.type(valueInput, 'single_role'); // No commas - should pass without validation - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Successfully added 1 parameter(s)', 'success'); - }, {timeout: 10000}); - }); - - test('handles unset parameters with network error', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config?unset=')) { - return Promise.reject(new Error('Network failure')); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const unsetParamsInput = comboboxes[1]; - - await act(async () => { - await user.type(unsetParamsInput, 'nodes{Enter}'); - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith( - expect.stringContaining('Error unsetting parameter nodes: Network failure'), - 'error' - ); - }, {timeout: 10000}); - }); - - test('handles delete sections with network error', async () => { - global.fetch.mockImplementation((url) => { - if (url.includes('/config?delete=')) { - return Promise.reject(new Error('Network failure')); - } - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const deleteSectionsInput = comboboxes[2]; - - await act(async () => { - await user.type(deleteSectionsInput, 'fs#1{Enter}'); - }); - - const applyButton = within(dialog).getByRole('button', {name: /Apply/i}); - await act(async () => { - await user.click(applyButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith( - expect.stringContaining('Error deleting section fs#1: Network failure'), - 'error' - ); - }, {timeout: 10000}); - }); - - test('handles update config with network error', async () => { - global.fetch.mockImplementation((url, options) => { - if (url.includes('/config/file') && options?.method === 'PUT') { - return Promise.reject(new Error('Network failure')); - } - return Promise.resolve({ - ok: true, - status: 200, - text: () => Promise.resolve(''), - json: () => Promise.resolve({items: []}), - headers: new Headers(), - }); - }); - - render( - - ); - - const uploadButton = getUploadButton(); - await act(async () => { - await user.click(uploadButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Update Configuration/i); - }, {timeout: 5000}); - - const fileInput = document.querySelector('#update-config-file-upload'); - const testFile = new File(['[DEFAULT]\nnodes = node2'], 'config.ini'); - await act(async () => { - await user.upload(fileInput, testFile); - }); - - const updateButton = screen.getByRole('button', {name: /Update/i}); - await act(async () => { - await user.click(updateButton); - }); - - await waitFor(() => { - expect(openSnackbar).toHaveBeenCalledWith('Error: Network failure', 'error'); - }, {timeout: 10000}); - }); - - test('handles remove parameter in manage params dialog', async () => { - render( - - ); - - const manageParamsButton = getManageParamsButton(); - await act(async () => { - await user.click(manageParamsButton); - }); - - await waitFor(() => { - expect(screen.getByRole('dialog')).toHaveTextContent(/Manage Configuration Parameters/i); - }, {timeout: 10000}); - - const dialog = screen.getByRole('dialog'); - const comboboxes = within(dialog).getAllByRole('combobox', {name: /autocomplete-input/i}); - const addParamsInput = comboboxes[0]; - - await act(async () => { - await user.type(addParamsInput, 'DEFAULT.orchestrate{Enter}'); - }); - - const addButton = screen.getByRole('button', {name: /Add Parameter/i}); - await act(async () => { - await user.click(addButton); - }); - - await waitFor(() => { - expect(screen.getByText('orchestrate')).toBeInTheDocument(); - }, {timeout: 5000}); - - const removeButton = screen.getByRole('button', {name: /Remove parameter/i}); - await act(async () => { - await user.click(removeButton); - }); - - await waitFor(() => { - expect(screen.queryByText('orchestrate')).not.toBeInTheDocument(); - }, {timeout: 5000}); + expect(setConfigDialogOpen).toHaveBeenCalledWith(false); }); }); diff --git a/src/components/tests/ObjectDetails.test.jsx b/src/components/tests/ObjectDetails.test.jsx index 0ce30ec..cc819b8 100644 --- a/src/components/tests/ObjectDetails.test.jsx +++ b/src/components/tests/ObjectDetails.test.jsx @@ -1,7 +1,7 @@ import React, {act} from 'react'; import {render, screen, fireEvent, waitFor, within} from '@testing-library/react'; import {MemoryRouter, Route, Routes} from 'react-router-dom'; -import ObjectDetail, {getFilteredResourceActions, getResourceType, parseProvisionedState} from '../ObjectDetails'; +import ObjectDetail, {getResourceType, parseProvisionedState} from '../ObjectDetails'; import useEventStore from '../../hooks/useEventStore.js'; import {closeEventSource, startEventReception} from '../../eventSourceManager.jsx'; import userEvent from '@testing-library/user-event'; @@ -27,6 +27,34 @@ jest.mock('../../context/DarkModeContext', () => ({ }), })); +// Mock ConfigSection +jest.mock('../ConfigSection', () => ({ + __esModule: true, + default: ({ + decodedObjectName, + configNode, + setConfigNode, + openSnackbar, + configDialogOpen, + setConfigDialogOpen + }) => ( +
+ + {configDialogOpen && ( +
+
Configuration for {decodedObjectName}
+ {configNode &&
Node: {configNode}
} +
+ )} +
+ ), +})); + // Mock Material-UI components jest.mock('@mui/material', () => { const actual = jest.requireActual('@mui/material'); @@ -677,30 +705,13 @@ type = flag ); - // Trouver l'en-tête Configuration et cliquer dessus pour développer - const configHeader = await screen.findByText('Configuration'); - - // Dans le mock, l'AccordionSummary est un div avec rôle button - const configButtons = screen.getAllByRole('button'); - const configButton = configButtons.find(button => - button.textContent && button.textContent.includes('Configuration') - ); + const configButton = await screen.findByTestId('open-config-dialog'); + expect(configButton).toBeInTheDocument(); - if (configButton) { - fireEvent.click(configButton); - } else { - // Fallback: cliquer sur l'en-tête lui-même - fireEvent.click(configHeader); - } + fireEvent.click(configButton); await waitFor(() => { - expect(screen.getByText(/nodes = \*/i)).toBeInTheDocument(); - }, {timeout: 10000, interval: 200}); - - await waitFor(() => { - expect(screen.getByText( - /this_is_a_very_long_unbroken_string_that_should_trigger_a_horizontal_scrollbar_abcdefghijklmnopqrstuvwxyz1234567890/i - )).toBeInTheDocument(); + expect(screen.getByTestId('config-dialog')).toBeInTheDocument(); }, {timeout: 10000, interval: 200}); expect(global.fetch).toHaveBeenCalledWith( @@ -761,6 +772,7 @@ type = flag require('react-router-dom').useParams.mockReturnValue({ objectName: 'root/svc/svc1', }); + render( @@ -768,18 +780,23 @@ type = flag ); + + const configButton = await screen.findByTestId('open-config-dialog'); + + fireEvent.click(configButton); + await waitFor( () => { - expect(global.fetch).toHaveBeenCalledWith( - expect.stringContaining('/api/node/name/node1/instance/path/root/svc/svc1/config/file'), - expect.any(Object) - ); + expect(screen.getByTestId('config-dialog')).toBeInTheDocument(); }, {timeout: 5000} ); await waitFor( () => { - expect(screen.getByText(/nodes = \*/i)).toBeInTheDocument(); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/node/name/node1/instance/path/root/svc/svc1/config/file'), + expect.any(Object) + ); }, {timeout: 5000} ); @@ -959,30 +976,24 @@ type = flag {timeout: 10000, interval: 200} ); - // Trouver la carte du node (Paper) et cliquer dessus - // Dans le mock, Paper est un div, donc on cherche le div qui contient le texte node1 + const nodeText = screen.getByText('node1'); const card = nodeText.closest('div[role="region"]') || nodeText.closest('div'); if (card) { - // Simuler un clic sur la carte (éviter les éléments interactifs) - // Créer un mock event pour simuler le comportement de handleCardClick const mockEvent = { target: card, stopPropagation: jest.fn(), closest: (selector) => { - // Simuler le comportement de closest pour éviter les éléments interactifs if (selector === 'button' || selector === 'input' || selector === '.no-click') { - return null; // Simuler que ce n'est pas un élément interactif + return null; } return null; } }; - // Déclencher le clic sur la carte fireEvent.click(card); } else { - // Fallback: cliquer sur le texte du node fireEvent.click(nodeText); } @@ -2056,37 +2067,71 @@ type = flag objectName: 'root/svc/svc1', }); + // Setup const mockState = { objectStatus: {}, objectInstanceStatus: { 'root/svc/svc1': { - node1: {avail: 'up', resources: {}} + node1: { avail: 'up', resources: {} } } }, instanceMonitor: {}, instanceConfig: {}, - configUpdates: [ - {name: 'svc1', fullName: 'root/svc/svc1', node: 'node1', type: 'InstanceConfigUpdated'} - ], + configUpdates: [], clearConfigUpdate: jest.fn(), }; - useEventStore.mockImplementation((selector) => selector(mockState)); + // Track fetch calls + let fetchCallCount = 0; + global.fetch.mockImplementation((url) => { + fetchCallCount++; - // Mock fetch to reject for config update - let callCount = 0; - global.fetch.mockImplementation(() => { - callCount++; - if (callCount === 1) { - // Initial config fetch succeeds - return Promise.resolve({ - ok: true, - text: () => Promise.resolve('initial config') - }); + if (url.includes('/config/file')) { + if (fetchCallCount === 1) { + // Initial config fetch - succeeds + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('initial config'), + json: () => Promise.resolve({}) + }); + } else { + // Second config fetch - fails (this is what we're testing) + return Promise.reject(new Error('Failed to load updated configuration')); + } + } + + // Default response for other endpoints + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('success'), + json: () => Promise.resolve({ items: [] }) + }); + }); + + // Setup subscription callback tracking + const subscriptionCallbacks = new Map(); + const unsubscribeMock = jest.fn(); + + useEventStore.subscribe = jest.fn((selector, callback, options) => { + const key = selector.toString(); + subscriptionCallbacks.set(key, callback); + + // Call immediately if fireImmediately option is set + if (options?.fireImmediately) { + callback(mockState[getStoreKeyFromSelector(selector)]); + } + + return unsubscribeMock; + }); + + // Mock useEventStore to return our state + useEventStore.mockImplementation((selector) => { + if (typeof selector === 'function') { + return selector(mockState); } - // Subsequent config update fails - return Promise.reject(new Error('Failed to load updated configuration')); + return mockState; }); + render( @@ -2095,11 +2140,43 @@ type = flag ); - // Wait for component to load and process config update + // Wait for initial fetch await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(2); // Initial + update attempt - }, {timeout: 10000}); - }); + expect(global.fetch).toHaveBeenCalledTimes(1); + }, { timeout: 5000 }); + + // Now simulate a config update + // Find the configUpdates subscription callback + const configUpdatesKey = Array.from(subscriptionCallbacks.keys()) + .find(key => key.includes('configUpdates')); + + if (configUpdatesKey) { + const configUpdatesCallback = subscriptionCallbacks.get(configUpdatesKey); + + // Create a config update + const newConfigUpdate = { + name: 'svc1', + fullName: 'root/svc/svc1', + node: 'node1', + type: 'InstanceConfigUpdated' + }; + + // Trigger the callback with the new update + await act(async () => { + await configUpdatesCallback([newConfigUpdate]); + }); + } + + + await waitFor(() => { + + expect(fetchCallCount).toBeGreaterThanOrEqual(1); + + // Check that clearConfigUpdate was called + expect(mockState.clearConfigUpdate).toHaveBeenCalled(); + }, { timeout: 10000 }); + }, 15000); + test('handles instanceConfig subscription with error cases', async () => { require('react-router-dom').useParams.mockReturnValue({ diff --git a/src/components/tests/Objects.test.jsx b/src/components/tests/Objects.test.jsx index e2659c1..93b8822 100644 --- a/src/components/tests/Objects.test.jsx +++ b/src/components/tests/Objects.test.jsx @@ -1020,29 +1020,4 @@ describe('Objects Component', () => { ); }); }); - - test('handles debounced URL updates', async () => { - jest.useFakeTimers(); - - render( - - - - ); - - await waitForComponentToLoad(); - - fireEvent.change(screen.getByLabelText('Name'), {target: {value: 'test'}}); - - jest.advanceTimersByTime(400); - - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - expect.stringContaining('name=test'), - expect.any(Object) - ); - }); - - jest.useRealTimers(); - }); });