// Copyright (c) 2025-2026, RTE (https://www.rte-france.com) // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 // This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent, within } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; import SidebarSummary from './SidebarSummary'; import { DEFAULT_ACTION_OVERVIEW_FILTERS } from '../utils/actionTypes'; import type { ActionOverviewFilters } from '../types'; describe('SidebarSummary', () => { const baseProps = { selectedContingency: [] as string[], n1LinesOverloaded: undefined as string[] | undefined, n1LinesOverloadedRho: undefined as number[] | undefined, selectedOverloads: undefined as Set | null | undefined, displayName: (id: string) => id, onContingencyZoom: vi.fn(), onOverloadClick: vi.fn(), }; it('renders nothing when there is no contingency, no overloads and no filters', () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); it('renders the contingency row with a zoom-to button', () => { const onContingencyZoom = vi.fn(); render( , ); const strip = screen.getByTestId('sticky-feed-summary'); expect(strip).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'LINE_A' })); expect(onContingencyZoom).toHaveBeenCalledWith('LINE_A'); }); it('uses the ⚡ lightning pictogram for the Contingency label (replaces the old 🎯)', () => { // Visual contract: the Contingency-as-fault metaphor is // pinned on the ⚡ glyph across the sidebar (status line + // picker card title). The legacy 🎯 must not regress here. render( , ); const strip = screen.getByTestId('sticky-feed-summary'); expect(strip.textContent).toContain('⚡ Contingency'); expect(strip.textContent).not.toContain('🎯'); }); it('renders the overloads row with per-line jump buttons', () => { const onOverloadClick = vi.fn(); render( , ); expect(screen.getByText(/Overloads:/i)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'LINE_X' })); expect(onOverloadClick).toHaveBeenCalledWith('', 'LINE_X', 'contingency'); }); it('renders the ActionFilterRings when hasActions + filters + onChange are all wired', () => { render( , ); expect(screen.getByTestId('sidebar-action-filters')).toBeInTheDocument(); }); it('hides the filter rings while the feed has no action to filter (hasActions=false)', () => { render( , ); expect(screen.queryByTestId('sidebar-action-filters')).not.toBeInTheDocument(); }); it('hides the filter rings when no overviewFilters object is supplied', () => { render( , ); expect(screen.queryByTestId('sidebar-action-filters')).not.toBeInTheDocument(); }); it('renders the strip for the filter rings alone, below the overloads row', () => { render( , ); const overloads = screen.getByText(/Overloads:/i); const filters = screen.getByTestId('sidebar-action-filters'); // Filter rings come AFTER the overloads row in document order. expect(overloads.compareDocumentPosition(filters) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); it('renders the strip even when ONLY the filter rings are present', () => { render( , ); expect(screen.getByTestId('sticky-feed-summary')).toBeInTheDocument(); expect(screen.getByTestId('sidebar-action-filters')).toBeInTheDocument(); }); // ===== readability-feed PR: Clear button ===== describe('Clear contingency shortcut', () => { it('renders the Clear button next to the contingency label when a handler is wired', () => { render( , ); const clearBtn = screen.getByTestId('sidebar-summary-clear'); expect(clearBtn).toBeInTheDocument(); expect(clearBtn.textContent).toMatch(/Clear/); }); it('fires onClearContingency when the Clear button is clicked', () => { const onClearContingency = vi.fn(); render( , ); fireEvent.click(screen.getByTestId('sidebar-summary-clear')); expect(onClearContingency).toHaveBeenCalledTimes(1); }); it('omits the Clear button when no handler is provided', () => { render( , ); expect(screen.queryByTestId('sidebar-summary-clear')).not.toBeInTheDocument(); }); it('omits the Clear button when there is no contingency to clear', () => { render( , ); expect(screen.queryByTestId('sidebar-summary-clear')).not.toBeInTheDocument(); }); }); // ===== readability-feed PR: double-click overload toggle ===== describe('Overload double-click toggle', () => { it('toggles the overload via onToggleOverload on double-click', () => { const onToggleOverload = vi.fn(); render( , ); fireEvent.doubleClick(screen.getByRole('button', { name: 'LINE_X' })); expect(onToggleOverload).toHaveBeenCalledWith('LINE_X'); }); it('single-click still routes to onOverloadClick (zoom shortcut) regardless of toggle wiring', () => { const onOverloadClick = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: 'LINE_X' })); expect(onOverloadClick).toHaveBeenCalledWith('', 'LINE_X', 'contingency'); }); it('does nothing on double-click when no toggle handler is wired', () => { // The double-click branch is gated on `onToggleOverload` — // without it the dbl-click is a no-op (the existing single- // click zoom semantics are preserved by the click handler). const onToggleOverload = vi.fn(); render( , ); fireEvent.doubleClick(screen.getByRole('button', { name: 'LINE_X' })); expect(onToggleOverload).not.toHaveBeenCalled(); }); it('shows the deselected style when an overload is not in selectedOverloads', () => { const selectedOverloads = new Set(); const { container } = render( , ); const link = screen.getByRole('button', { name: 'LINE_X' }); // Deselected links carry weight 400 and no underline (vs 600 // + underline-dotted for selected) — matches the OverloadPanel // contract preserved by the readability-feed PR. const style = link.getAttribute('style') || ''; expect(style).toMatch(/font-weight:\s*400/); expect(style).not.toMatch(/underline/); expect(container).toBeInTheDocument(); }); }); // ===== readability-feed PR: overload info bubble ===== describe('Overload info bubble', () => { const overloadProps = { ...baseProps, selectedContingency: ['LINE_A'], n1LinesOverloaded: ['LINE_X', 'LINE_Y'], n1LinesOverloadedRho: [1.05, 1.02], }; it('renders the bubble icon when overloads are present', () => { render(); expect(screen.getByTestId('sidebar-summary-overloads-bubble')).toBeInTheDocument(); }); it('opens the popover on click and shows N-overloads + N-1 toggles + monitor-deselected', () => { const onToggleOverload = vi.fn(); const onToggleMonitorDeselected = vi.fn(); render( , ); fireEvent.click(screen.getByTestId('sidebar-summary-overloads-bubble')); const popover = screen.getByTestId('overload-info-popover'); expect(popover).toBeInTheDocument(); expect(popover.textContent).toMatch(/100\/200 lines monitored/); expect(within(popover).getByText('LINE_N_PRE')).toBeInTheDocument(); expect(within(popover).getByTestId('overload-info-toggle-LINE_X')).toBeInTheDocument(); expect(within(popover).getByTestId('overload-info-toggle-LINE_Y')).toBeInTheDocument(); expect(within(popover).getByTestId('overload-info-monitor-deselected')).toBeInTheDocument(); }); it('toggles the overload from the popover checkbox', () => { const onToggleOverload = vi.fn(); render( , ); fireEvent.click(screen.getByTestId('sidebar-summary-overloads-bubble')); const popover = screen.getByTestId('overload-info-popover'); const checkbox = within(popover).getByTestId('overload-info-toggle-LINE_X') .querySelector('input[type="checkbox"]') as HTMLInputElement; fireEvent.click(checkbox); expect(onToggleOverload).toHaveBeenCalledWith('LINE_X'); }); it('hides the monitor-deselected toggle when every overload is selected', () => { render( , ); fireEvent.click(screen.getByTestId('sidebar-summary-overloads-bubble')); expect(screen.queryByTestId('overload-info-monitor-deselected')).not.toBeInTheDocument(); }); it('falls back to "None" for N-overloads when the list is empty', () => { render(); fireEvent.click(screen.getByTestId('sidebar-summary-overloads-bubble')); const popover = screen.getByTestId('overload-info-popover'); expect(popover.textContent).toMatch(/N Overloads:\s*None/); }); }); });