Co-Study4Grid / frontend /src /hooks /useManualSimulation.test.ts
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
5.78 kB
// 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
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useManualSimulation, type ManualSimulationParams } from './useManualSimulation';
import type { DiagramsState } from './useDiagrams';
import type { AnalysisResult, VlOverlay } from '../types';
const mockSimStream = vi.fn();
const mockGetSldTopologyPreview = vi.fn();
vi.mock('../api', () => ({
api: {
simulateAndVariantDiagramStream: (...args: unknown[]) => mockSimStream(...args),
getSldTopologyPreview: (...args: unknown[]) => mockGetSldTopologyPreview(...args),
simulateManualAction: vi.fn(),
},
}));
/** Build a streamed NDJSON Response-like from event objects. */
function ndjson(events: object[]) {
const enc = new TextEncoder();
return {
ok: true,
body: new ReadableStream<Uint8Array>({
start(controller) {
for (const e of events) controller.enqueue(enc.encode(JSON.stringify(e) + '\n'));
controller.close();
},
}),
};
}
function makeDiagrams(overrides: Partial<DiagramsState> = {}): DiagramsState {
return {
vlOverlay: null,
primeActionDiagram: vi.fn(),
handleVlDoubleClick: vi.fn(),
...overrides,
} as unknown as DiagramsState;
}
function makeParams(overrides: Partial<ManualSimulationParams> = {}): ManualSimulationParams {
return {
diagrams: makeDiagrams(),
selectedContingency: ['LINE_X'],
result: null,
voltageLevels: ['VL1'],
wrappedManualActionAdded: vi.fn(),
setError: vi.fn(),
...overrides,
};
}
function overlay(partial: Partial<VlOverlay>): VlOverlay {
return {
vlName: 'VL', actionId: null, svg: null, sldMetadata: null,
loading: false, error: null, tab: 'n-1',
...partial,
} as VlOverlay;
}
describe('useManualSimulation', () => {
beforeEach(() => { vi.clearAllMocks(); });
describe('handleSimulateUnsimulatedAction', () => {
it('errors (no API call) when no contingency is selected', async () => {
const setError = vi.fn();
const { result } = renderHook(() =>
useManualSimulation(makeParams({ selectedContingency: [], setError })));
await act(async () => { await result.current.handleSimulateUnsimulatedAction('ACT'); });
expect(setError).toHaveBeenCalledWith('Select a contingency first.');
expect(mockSimStream).not.toHaveBeenCalled();
});
it('streams metrics+diagram, primes the diagram, and registers the action with model provenance', async () => {
const diagrams = makeDiagrams();
const wrappedManualActionAdded = vi.fn();
mockSimStream.mockResolvedValue(ndjson([
{ type: 'metrics', action_id: 'ACT', description_unitaire: 'd', rho_before: [1], rho_after: [0.9], max_rho: 0.9, max_rho_line: 'LINE_1', is_rho_reduction: true, lines_overloaded: ['LINE_1'] },
{ type: 'diagram', svg: '<svg/>', metadata: '{}', action_id: 'ACT' },
]));
const { result } = renderHook(() => useManualSimulation(makeParams({
diagrams,
wrappedManualActionAdded,
result: { active_model: 'random' } as unknown as AnalysisResult,
})));
await act(async () => { await result.current.handleSimulateUnsimulatedAction('ACT'); });
expect(diagrams.primeActionDiagram).toHaveBeenCalledWith('ACT', expect.objectContaining({ svg: '<svg/>' }), 1);
// Provenance = the model that scored the pin, not "user".
expect(wrappedManualActionAdded).toHaveBeenCalledWith(
'ACT', expect.objectContaining({ max_rho_line: 'LINE_1' }), ['LINE_1'], 'random',
);
});
it('surfaces a stream error event via setError', async () => {
const setError = vi.fn();
mockSimStream.mockResolvedValue(ndjson([{ type: 'error', message: 'boom' }]));
const { result } = renderHook(() => useManualSimulation(makeParams({ setError })));
await act(async () => { await result.current.handleSimulateUnsimulatedAction('ACT'); });
expect(setError).toHaveBeenCalledWith('boom');
});
});
describe('handleSimulateSldEdit', () => {
it('is a no-op when nothing is staged', async () => {
const { result } = renderHook(() =>
useManualSimulation(makeParams({ diagrams: makeDiagrams({ vlOverlay: overlay({ tab: 'action' }) }) })));
await act(async () => { await result.current.handleSimulateSldEdit(); });
expect(mockSimStream).not.toHaveBeenCalled();
});
});
describe('sldEditBaseActionId', () => {
it('is null off the action tab and the action id on it', () => {
const { result, rerender } = renderHook(
({ ov }: { ov: VlOverlay }) => useManualSimulation(makeParams({ diagrams: makeDiagrams({ vlOverlay: ov }) })),
{ initialProps: { ov: overlay({ tab: 'n-1', actionId: 'X' }) } },
);
expect(result.current.sldEditBaseActionId).toBeNull();
rerender({ ov: overlay({ tab: 'action', actionId: 'ACT_1' }) });
expect(result.current.sldEditBaseActionId).toBe('ACT_1');
});
});
});