import React from 'react'; import {fireEvent, render, screen, waitFor, within} from '@testing-library/react'; import {BrowserRouter} from 'react-router'; import {MantineProvider} from '@mantine/core'; import {ThemeContext, SettingsContext} from '../../contexts/contexts'; import {cssVariablesResolver, mantineTheme} from '../../themes/mantine'; import {ControllerPage} from './ControllerPage'; // Mock the controller API jest.mock('../../api/controller', () => ({ getServices: jest.fn().mockResolvedValue([ { name: 'wrolpi-api', port: 8081, status: 'running', viewable: true, view_path: '/docs', enabled: true, kind: 'persistent', group: 'core', }, { name: 'wrolpi-app', port: 3000, status: 'running', viewable: true, view_path: '', enabled: true, kind: 'persistent', group: 'core', }, { name: 'smbd', port: 445, status: 'stopped', viewable: false, view_path: '', enabled: false, kind: 'persistent', group: 'optional', }, { name: 'wrolpi-repair', port: null, status: 'stopped', viewable: false, view_path: '', enabled: false, kind: 'task', group: 'optional', }, ]), startService: jest.fn().mockResolvedValue({success: true}), stopService: jest.fn().mockResolvedValue({success: true}), restartService: jest.fn().mockResolvedValue({success: true}), enableService: jest.fn().mockResolvedValue({success: true}), disableService: jest.fn().mockResolvedValue({success: true}), getServiceLogs: jest.fn().mockResolvedValue({logs: 'Sample log output'}), getDisks: jest.fn().mockResolvedValue([]), getMounts: jest.fn().mockResolvedValue([]), getFstabEntries: jest.fn().mockResolvedValue([]), getSmartStatus: jest.fn().mockResolvedValue({}), mountDisk: jest.fn().mockResolvedValue({success: true}), unmountDisk: jest.fn().mockResolvedValue( {success: true, stopped_services: ['wrolpi-api']}), addFstabEntry: jest.fn().mockResolvedValue({success: true}), removeFstabEntry: jest.fn().mockResolvedValue({success: true}), restartServices: jest.fn().mockResolvedValue({success: true}), getSambaStatus: jest.fn().mockResolvedValue({enabled: false, available: true, shares: []}), getHotspotSettings: jest.fn(), getHotspotDevices: jest.fn(), getHotspotProtocols: jest.fn(), updateHotspotSettings: jest.fn(), addSambaShare: jest.fn().mockResolvedValue({success: true}), removeSambaShare: jest.fn().mockResolvedValue({success: true}), })); // Mock hooks jest.mock('../../hooks/customHooks', () => ({ useDockerized: jest.fn().mockReturnValue(false), useMediaDirectory: jest.fn().mockReturnValue('/media/wrolpi'), useBluetooth: jest.fn().mockReturnValue({on: false, setBluetooth: jest.fn()}), useDesktop: jest.fn().mockReturnValue({on: true, setDesktop: jest.fn()}), useVnc: jest.fn().mockReturnValue({on: false, desktopRunning: true, setVnc: jest.fn()}), useHotspot: jest.fn().mockReturnValue({on: true, setHotspot: jest.fn()}), useThrottle: jest.fn().mockReturnValue({on: false, setThrottle: jest.fn()}), })); // Mock Common.js components that have complex dependencies jest.mock('../Common', () => { const actual = jest.requireActual('../Common'); return { ...actual, BluetoothToggle: () =>
BluetoothToggle
, DesktopToggle: () =>
DesktopToggle
, VncToggle: () =>
VncToggle
, HotspotToggle: () =>
HotspotToggle
, ThrottleToggle: () =>
ThrottleToggle
, Toggle: ({label, checked, onChange}) => ( ), APIButton: ({children, onClick, disabled}) => ( ), DirectorySearch: ({onSelect, value}) => ( onSelect && onSelect(e.target.value)}/> ), }; }); // Mock Settings.js components jest.mock('./Settings', () => ({ RestartButton: () => , ShutdownButton: () => , })); const defaultTheme = { t: {style: {}}, inverted: false, }; const defaultSettings = { settings: {}, saveSettings: jest.fn(), fetchSettings: jest.fn(), }; const renderControllerPage = () => { return render( ); }; describe('ControllerPage', () => { beforeEach(() => { jest.clearAllMocks(); // Reset useDockerized to false for regular tests const {useDockerized} = require('../../hooks/customHooks'); useDockerized.mockReturnValue(false); // resetMocks clears module-factory implementations, so configure hotspot mocks here. const controllerApi = require('../../api/controller'); controllerApi.getHotspotSettings.mockResolvedValue( {device: 'wlan0', ssid: 'WROLPi', password: 'wrolpi hotspot', protocol: 'wpa2'}); controllerApi.getHotspotDevices.mockResolvedValue({devices: ['wlan0']}); controllerApi.getHotspotProtocols.mockResolvedValue({device: 'wlan0', protocols: ['wpa2', 'wpa3']}); controllerApi.updateHotspotSettings.mockResolvedValue( {device: 'wlan0', ssid: 'WROLPi', password: 'wrolpi hotspot', protocol: 'wpa2'}); }); test('renders Services section', async () => { renderControllerPage(); await waitFor(() => { expect(screen.getByText('Services')).toBeInTheDocument(); }); }); test('renders Disk Management section', async () => { renderControllerPage(); await waitFor(() => { expect(screen.getByText('Disk Management')).toBeInTheDocument(); }); }); test('renders Hardware Controls section', async () => { renderControllerPage(); await waitFor(() => { expect(screen.getByText('Hardware Controls')).toBeInTheDocument(); }); expect(screen.getByTestId('hotspot-toggle')).toBeInTheDocument(); expect(screen.getByTestId('bluetooth-toggle')).toBeInTheDocument(); expect(screen.getByTestId('desktop-toggle')).toBeInTheDocument(); expect(screen.getByTestId('vnc-toggle')).toBeInTheDocument(); expect(screen.getByTestId('throttle-toggle')).toBeInTheDocument(); }); test('calls getServices on mount', async () => { const {getServices} = require('../../api/controller'); renderControllerPage(); // Verify getServices was called await waitFor(() => { expect(getServices).toHaveBeenCalled(); }); }); test('shows Restart and Shutdown buttons in non-Docker mode', async () => { renderControllerPage(); await waitFor(() => { expect(screen.getByTestId('restart-button')).toBeInTheDocument(); expect(screen.getByTestId('shutdown-button')).toBeInTheDocument(); }); }); test('hotspot protocol dropdown lists the protocols supported by the device', async () => { const {getHotspotProtocols} = require('../../api/controller'); renderControllerPage(); await waitFor(() => { expect(getHotspotProtocols).toHaveBeenCalledWith('wlan0'); }); expect(screen.getByText('Hotspot Protocol')).toBeInTheDocument(); // The saved protocol is selected; WPA3 is offered because the device supports it. // The select renders its chosen value as both the field's text and an option. await waitFor(() => { expect(screen.getAllByText('WPA2 (most compatible)').length).toBeGreaterThan(0); expect(screen.getAllByText('WPA3').length).toBeGreaterThan(0); }); }); test('a saved protocol the device does not support is reset to a supported one', async () => { const controllerApi = require('../../api/controller'); controllerApi.getHotspotSettings.mockResolvedValue( {device: 'wlan0', ssid: 'WROLPi', password: 'wrolpi hotspot', protocol: 'wpa3'}); controllerApi.getHotspotProtocols.mockResolvedValue({device: 'wlan0', protocols: ['wpa2']}); renderControllerPage(); // The stale WPA3 selection is replaced by the only protocol the device supports. await waitFor(() => { expect(screen.getAllByText('WPA2 (most compatible)').length).toBeGreaterThan(0); }); expect(screen.queryByText('WPA3')).not.toBeInTheDocument(); }); test('hotspot protocol dropdown only offers WPA2 on a Raspberry Pi', async () => { const {getHotspotProtocols} = require('../../api/controller'); getHotspotProtocols.mockResolvedValue({device: 'wlan0', protocols: ['wpa2']}); renderControllerPage(); await waitFor(() => { expect(getHotspotProtocols).toHaveBeenCalledWith('wlan0'); }); await waitFor(() => { expect(screen.getAllByText('WPA2 (most compatible)').length).toBeGreaterThan(0); }); expect(screen.queryByText('WPA3')).not.toBeInTheDocument(); }); }); describe('DiskSection primary unmount', () => { const controllerApi = require('../../api/controller'); beforeEach(() => { controllerApi.getDisks.mockResolvedValue([ { name: 'sda1', path: '/dev/sda1', size: '500G', fstype: 'ext4', label: 'wrolpi', mountpoint: '/media/wrolpi', uuid: 'abc-123', }, ]); controllerApi.unmountDisk.mockClear(); }); afterEach(() => { controllerApi.getDisks.mockResolvedValue([]); }); test('primary mount shows Unmount button; confirming warns and unmounts', async () => { renderControllerPage(); // The primary mount is no longer labeled "System"; it gets Unmount. const unmountButton = await screen.findByText('Unmount'); fireEvent.click(unmountButton); // The confirm modal warns that the API service will be stopped. await waitFor(() => { expect(screen.getByText(/API service will/)).toBeInTheDocument(); }); // The Confirm modal is portaled to the end of , so it renders // *before* the page content in DOM order -- scope the query to the // dialog rather than assuming which match comes last. const dialog = screen.getByRole('dialog'); fireEvent.click(within(dialog).getByText('Unmount')); await waitFor(() => { expect(controllerApi.unmountDisk).toHaveBeenCalledWith('/media/wrolpi'); }); }); }); describe('groupServices', () => { const {groupServices} = require('./ControllerPage'); test('splits core from optional by group field', () => { const {core, optional} = groupServices([ {name: 'wrolpi-api', status: 'running', group: 'core'}, {name: 'smbd', status: 'stopped', group: 'optional'}, {name: 'wrolpi-repair', status: 'stopped', group: 'optional'}, ]); expect(core.map(s => s.name)).toEqual(['wrolpi-api']); expect(optional.map(s => s.name)).toEqual(['smbd', 'wrolpi-repair']); }); test('treats a missing group (Docker containers) as core', () => { const {core, optional} = groupServices([ {name: 'api', status: 'running'}, {name: 'db', status: 'running'}, ]); expect(core).toHaveLength(2); expect(optional).toHaveLength(0); }); test('sorts problems first, then alphabetically, within a group', () => { const {core} = groupServices([ {name: 'caddy', status: 'running', group: 'core'}, {name: 'wrolpi-api', status: 'failed', group: 'core'}, {name: 'wrolpi-app', status: 'running', group: 'core'}, ]); expect(core.map(s => s.name)).toEqual(['wrolpi-api', 'caddy', 'wrolpi-app']); }); }); describe('ControllerPage in Docker mode', () => { beforeEach(() => { jest.clearAllMocks(); const {useDockerized} = require('../../hooks/customHooks'); useDockerized.mockReturnValue(true); }); test('hides reboot/shutdown buttons in Docker mode', async () => { renderControllerPage(); await waitFor(() => { expect(screen.getByText('Hardware Controls')).toBeInTheDocument(); }); // Shutdown and Restart buttons should not be visible expect(screen.queryByTestId('shutdown-button')).not.toBeInTheDocument(); expect(screen.queryByTestId('restart-button')).not.toBeInTheDocument(); }); test('shows disk management unavailable message in Docker mode', async () => { renderControllerPage(); await waitFor(() => { expect(screen.getByText('Disk management is not available in Docker environments.')).toBeInTheDocument(); }); }); });