feat: Implement initial Process Scheduling Simulator application with core logic, algorithms, UI components, and project setup.
This commit is contained in:
commit
083fc0027e
54 files changed
+8815
No files matched your search
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ControlBar from '../ControlBar/ControlBar';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import { useSimulationStore } from '../../store/simulationStore';
|
||||
import { useUiStore } from '../../store/uiStore';
|
||||
import type { Process } from '@core/types';
|
||||
|
||||
const MOCK_PROCESS: Process = {
|
||||
id: 'p1',
|
||||
name: 'P1',
|
||||
arrivalTime: 0,
|
||||
priority: 5,
|
||||
color: '#60a5fa',
|
||||
bursts: [{ type: 'cpu', duration: 4 }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useProcessStore.setState({ processes: [], config: useProcessStore.getState().config });
|
||||
useSimulationStore.setState({ status: 'idle', ticks: [], engine: null, _processes: [], _config: null });
|
||||
useUiStore.setState({ speed: 1 });
|
||||
});
|
||||
|
||||
describe('ControlBar', () => {
|
||||
it('renders the toolbar with all main controls', () => {
|
||||
render(<ControlBar />);
|
||||
expect(screen.getByRole('toolbar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('btn-play')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('btn-step')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('btn-reset')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('tick-display')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('speed-select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays tick 0000 on initial render', () => {
|
||||
render(<ControlBar />);
|
||||
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
|
||||
});
|
||||
|
||||
it('shows "Pronto" status when idle', () => {
|
||||
render(<ControlBar />);
|
||||
expect(screen.getByText('Pronto')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Play button is disabled when no processes are loaded', () => {
|
||||
render(<ControlBar />);
|
||||
expect(screen.getByTestId('btn-play')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Step button initialises simulation and advances tick', async () => {
|
||||
const user = userEvent.setup();
|
||||
useProcessStore.setState({ processes: [MOCK_PROCESS] });
|
||||
render(<ControlBar />);
|
||||
const stepBtn = screen.getByTestId('btn-step');
|
||||
await user.click(stepBtn);
|
||||
expect(screen.getByTestId('tick-display')).toHaveTextContent('0001');
|
||||
});
|
||||
|
||||
it('Play button triggers init and sets status to running', async () => {
|
||||
const user = userEvent.setup();
|
||||
useProcessStore.setState({ processes: [MOCK_PROCESS] });
|
||||
render(<ControlBar />);
|
||||
await user.click(screen.getByTestId('btn-play'));
|
||||
expect(useSimulationStore.getState().status).toBe('running');
|
||||
});
|
||||
|
||||
it('Pause button is shown (and plays) when simulation is running', async () => {
|
||||
const user = userEvent.setup();
|
||||
useProcessStore.setState({ processes: [MOCK_PROCESS] });
|
||||
render(<ControlBar />);
|
||||
await user.click(screen.getByTestId('btn-play'));
|
||||
expect(screen.getByTestId('btn-pause')).toBeInTheDocument();
|
||||
await user.click(screen.getByTestId('btn-pause'));
|
||||
expect(useSimulationStore.getState().status).toBe('paused');
|
||||
});
|
||||
|
||||
it('changing speed updates uiStore', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlBar />);
|
||||
const speedSelect = screen.getByTestId('speed-select');
|
||||
await user.selectOptions(speedSelect, '2');
|
||||
expect(useUiStore.getState().speed).toBe(2);
|
||||
});
|
||||
|
||||
it('Reset is disabled when status is idle', () => {
|
||||
render(<ControlBar />);
|
||||
expect(screen.getByTestId('btn-reset')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Reset becomes enabled after simulation starts and returns tick to 0', async () => {
|
||||
const user = userEvent.setup();
|
||||
useProcessStore.setState({ processes: [MOCK_PROCESS] });
|
||||
render(<ControlBar />);
|
||||
await user.click(screen.getByTestId('btn-step'));
|
||||
expect(screen.getByTestId('tick-display')).toHaveTextContent('0001');
|
||||
await user.click(screen.getByTestId('btn-reset'));
|
||||
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ProcessForm from '../ProcessPanel/ProcessForm';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
|
||||
// Reset Zustand processStore between tests
|
||||
beforeEach(() => {
|
||||
useProcessStore.setState({ processes: [], config: useProcessStore.getState().config });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('ProcessForm', () => {
|
||||
it('renders the "Novo Processo" form with default fields', () => {
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
expect(screen.getByTestId('process-form')).toBeInTheDocument();
|
||||
expect(screen.getByText('Novo Processo')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Nome do processo')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Tempo de chegada')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Prioridade do processo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Editar Processo" title when editingProcess is provided', () => {
|
||||
const proc = {
|
||||
id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff',
|
||||
bursts: [{ type: 'cpu' as const, duration: 4 }],
|
||||
};
|
||||
render(<ProcessForm editingProcess={proc} onClose={() => {}} />);
|
||||
expect(screen.getByText('Editar Processo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows validation error when name is empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
await user.clear(nameInput);
|
||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
||||
await user.click(submitBtn);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows validation error when arrival time is negative', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
await user.type(nameInput, 'P1');
|
||||
const arrivalInput = screen.getByLabelText('Tempo de chegada');
|
||||
// fireEvent.change used to reliably set a negative number bypassing HTML min attribute
|
||||
fireEvent.change(arrivalInput, { target: { value: '-1' } });
|
||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
||||
await user.click(submitBtn);
|
||||
expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows validation error for name longer than 8 chars', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
// Bypass the HTML maxLength attribute via fireEvent to test the JS validation
|
||||
fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } });
|
||||
await user.click(screen.getByRole('button', { name: /Criar/i }));
|
||||
expect(screen.getByText(/Nome deve ter no máximo 8 caracteres/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds a new burst with alternating type when clicking + button', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
const addBurstBtn = screen.getByLabelText('Adicionar burst');
|
||||
await user.click(addBurstBtn);
|
||||
// After one CPU burst at start, clicking + adds IO burst
|
||||
// getAllByText with exact matches for the type labels
|
||||
const cpuLabels = screen.getAllByText((_, el) => el?.tagName === 'SPAN' && el.textContent?.trim().toUpperCase() === 'CPU');
|
||||
const ioLabels = screen.getAllByText((_, el) => el?.tagName === 'SPAN' && el.textContent?.trim().toUpperCase() === 'IO');
|
||||
expect(cpuLabels.length).toBeGreaterThanOrEqual(1);
|
||||
expect(ioLabels.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('calls onClose after successful form submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = vi.fn();
|
||||
render(<ProcessForm onClose={onClose} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
await user.type(nameInput, 'P1');
|
||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
||||
await user.click(submitBtn);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('adds the process to the store after successful submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
await user.type(nameInput, 'Test1');
|
||||
await user.click(screen.getByRole('button', { name: /Criar/i }));
|
||||
const processes = useProcessStore.getState().processes;
|
||||
expect(processes).toHaveLength(1);
|
||||
expect(processes[0]?.name).toBe('Test1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SchedulerConfig from '../SchedulerPanel/SchedulerConfig';
|
||||
import { useProcessStore } from '../../store/processStore';
|
||||
import type { SchedulingAlgorithm } from '@core/types';
|
||||
|
||||
const setAlgorithm = (algo: SchedulingAlgorithm) => {
|
||||
useProcessStore.setState(s => ({ config: { ...s.config, algorithm: algo } }));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useProcessStore.setState(s => ({
|
||||
config: {
|
||||
...s.config,
|
||||
algorithm: 'FCFS',
|
||||
quantum: 2,
|
||||
contextSwitchTime: 0,
|
||||
isPreemptive: false,
|
||||
agingEnabled: false,
|
||||
agingInterval: 5,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
describe('SchedulerConfig', () => {
|
||||
it('always shows Context Switch field regardless of algorithm', () => {
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('always shows read-only preemptive badge', () => {
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('badge shows "Não-preemptivo" for FCFS', () => {
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
|
||||
});
|
||||
|
||||
it('badge shows "Preemptivo" for SJF_P (SRTF)', () => {
|
||||
setAlgorithm('SJF_P');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo');
|
||||
});
|
||||
|
||||
it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => {
|
||||
setAlgorithm('SJF_NP');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
|
||||
// Confirm the old interactive preemptive switch is gone
|
||||
expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT show Quantum field for FCFS', () => {
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.queryByTestId('quantum-field')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Quantum field when algorithm is RR', () => {
|
||||
setAlgorithm('RR');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Quantum field when algorithm is MULTILEVEL', () => {
|
||||
setAlgorithm('MULTILEVEL');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT show Quantum field for SJF_NP', () => {
|
||||
setAlgorithm('SJF_NP');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.queryByTestId('quantum-field')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Aging toggle for PRIORITY_NP', () => {
|
||||
setAlgorithm('PRIORITY_NP');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByRole('switch', { name: /Aging/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Aging toggle for PRIORITY_P', () => {
|
||||
setAlgorithm('PRIORITY_P');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByRole('switch', { name: /Aging/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Aging interval field only when aging is enabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
setAlgorithm('PRIORITY_NP');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.queryByTestId('aging-interval-field')).not.toBeInTheDocument();
|
||||
const agingToggle = screen.getByRole('switch', { name: /Aging/i });
|
||||
await user.click(agingToggle);
|
||||
expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Intervalo de aging')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates the store when Quantum field is changed', () => {
|
||||
setAlgorithm('RR');
|
||||
render(<SchedulerConfig />);
|
||||
const quantumInput = screen.getByLabelText('Quantum do Round Robin');
|
||||
fireEvent.change(quantumInput, { target: { value: '5' } });
|
||||
expect(useProcessStore.getState().config.quantum).toBe(5);
|
||||
});
|
||||
|
||||
it('does NOT show Aging toggle for FCFS', () => {
|
||||
setAlgorithm('FCFS');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.queryByRole('switch', { name: /Aging/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT show Aging toggle for HRRN', () => {
|
||||
setAlgorithm('HRRN');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.queryByRole('switch', { name: /Aging/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user