Co-Study4Grid / frontend /src /hooks /useSettings.test.ts
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
9.83 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
// 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, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useSettings } from './useSettings';
import { interactionLogger } from '../utils/interactionLogger';
// Mock the api module
vi.mock('../api', () => ({
api: {
pickPath: vi.fn(),
getUserConfig: vi.fn().mockResolvedValue({}),
getConfigFilePath: vi.fn().mockResolvedValue(''),
saveUserConfig: vi.fn().mockResolvedValue({}),
setConfigFilePath: vi.fn().mockResolvedValue({ config_file_path: '', config: {} }),
},
}));
describe('useSettings — interaction logging', () => {
beforeEach(() => {
interactionLogger.clear();
vi.clearAllMocks();
});
it('logs settings_opened with tab when handleOpenSettings is called', () => {
const { result } = renderHook(() => useSettings());
act(() => {
result.current.handleOpenSettings('recommender');
});
const log = interactionLogger.getLog();
expect(log).toHaveLength(1);
expect(log[0].type).toBe('settings_opened');
expect(log[0].details).toEqual({ tab: 'recommender' });
});
it('logs settings_opened with default tab "paths"', () => {
const { result } = renderHook(() => useSettings());
act(() => {
result.current.handleOpenSettings();
});
const log = interactionLogger.getLog();
expect(log[0].details).toEqual({ tab: 'paths' });
});
it('logs settings_cancelled when handleCloseSettings is called', () => {
const { result } = renderHook(() => useSettings());
// Open first to set backup
act(() => { result.current.handleOpenSettings(); });
interactionLogger.clear();
act(() => { result.current.handleCloseSettings(); });
const log = interactionLogger.getLog();
expect(log).toHaveLength(1);
expect(log[0].type).toBe('settings_cancelled');
});
it('logs path_picked when pickSettingsPath succeeds', async () => {
const { api } = await import('../api');
(api.pickPath as ReturnType<typeof vi.fn>).mockResolvedValue('/data/network.xiidm');
const { result } = renderHook(() => useSettings());
const setter = vi.fn();
await act(async () => {
await result.current.pickSettingsPath('file', setter);
});
const log = interactionLogger.getLog();
expect(log).toHaveLength(1);
expect(log[0].type).toBe('path_picked');
expect(log[0].details).toEqual({ type: 'file', path: '/data/network.xiidm' });
expect(setter).toHaveBeenCalledWith('/data/network.xiidm');
});
it('does not log path_picked when pickSettingsPath returns empty', async () => {
const { api } = await import('../api');
(api.pickPath as ReturnType<typeof vi.fn>).mockResolvedValue('');
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('dir', vi.fn());
});
expect(interactionLogger.getLog()).toHaveLength(0);
});
it('does not log path_picked when pickSettingsPath throws', async () => {
const { api } = await import('../api');
(api.pickPath as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('No picker'));
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('file', vi.fn());
});
expect(interactionLogger.getLog()).toHaveLength(0);
});
it('logs settings_opened for each tab type', () => {
const { result } = renderHook(() => useSettings());
const tabs = ['paths', 'recommender', 'configurations'] as const;
for (const tab of tabs) {
act(() => { result.current.handleOpenSettings(tab); });
}
const log = interactionLogger.getLog();
expect(log).toHaveLength(3);
expect(log.map(e => e.details.tab)).toEqual(['paths', 'recommender', 'configurations']);
});
// Bug 4: the file-picker buttons used to silently fail when the native
// dialog couldn't open (no display, tkinter missing). They now surface
// the backend error via window.alert so the user understands *why*
// nothing is happening, while still leaving the setter untouched.
describe('pickSettingsPath error surfacing (Bug 4)', () => {
it('calls window.alert with the backend error message when the picker fails', async () => {
const { api } = await import('../api');
(api.pickPath as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('tkinter: no display'),
);
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
const setter = vi.fn();
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('file', setter);
});
expect(alertSpy).toHaveBeenCalledTimes(1);
const message = alertSpy.mock.calls[0][0] as string;
expect(message).toContain('tkinter: no display');
expect(setter).not.toHaveBeenCalled();
alertSpy.mockRestore();
});
it('does not alert when the picker succeeds', async () => {
const { api } = await import('../api');
(api.pickPath as ReturnType<typeof vi.fn>).mockResolvedValue('/tmp/x.xiidm');
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
const setter = vi.fn();
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('file', setter);
});
expect(alertSpy).not.toHaveBeenCalled();
expect(setter).toHaveBeenCalledWith('/tmp/x.xiidm');
alertSpy.mockRestore();
});
it('hints at backend version mismatch when the request 404s', async () => {
// axios error shape with response.status — what the user
// hits when the picker endpoint isn't on the running
// backend (stale instance, mismatched code).
const { api } = await import('../api');
const axiosLike = Object.assign(new Error('Request failed with status code 404'), {
response: { status: 404 },
});
(api.pickPath as ReturnType<typeof vi.fn>).mockRejectedValue(axiosLike);
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('file', vi.fn());
});
expect(alertSpy).toHaveBeenCalledTimes(1);
const message = alertSpy.mock.calls[0][0] as string;
expect(message).toContain('Request failed with status code 404');
expect(message).toContain('http://127.0.0.1:8000');
expect(message).toMatch(/restart it after pulling the latest code/);
alertSpy.mockRestore();
});
it('hints at backend not running when the request fails with a network error', async () => {
const { api } = await import('../api');
const axiosLike = Object.assign(new Error('Network Error'), {
code: 'ERR_NETWORK',
});
(api.pickPath as ReturnType<typeof vi.fn>).mockRejectedValue(axiosLike);
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('file', vi.fn());
});
expect(alertSpy).toHaveBeenCalledTimes(1);
const message = alertSpy.mock.calls[0][0] as string;
expect(message).toContain('http://127.0.0.1:8000');
alertSpy.mockRestore();
});
it('does not add the backend hint for backend-reported errors (e.g. tkinter missing)', async () => {
// When the backend itself returns 200 with an `error` field
// (the picker subprocess died, tkinter missing, …) the
// problem isn't transport — surfacing the backend hint
// would be misleading. api.pickPath rethrows the error
// string as a plain Error, no `response` attached.
const { api } = await import('../api');
(api.pickPath as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('No module named tkinter'),
);
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { });
const { result } = renderHook(() => useSettings());
await act(async () => {
await result.current.pickSettingsPath('file', vi.fn());
});
const message = alertSpy.mock.calls[0][0] as string;
expect(message).toContain('No module named tkinter');
expect(message).not.toContain('http://127.0.0.1:8000');
alertSpy.mockRestore();
});
});
});