chore: final pre-submission polish for JOSS
- Add CITATION.cff for GitHub citation support and Zenodo/JOSS integration - Fix ossim reference URL in references.bib (was pointing to unrelated repo) - Remove broken favicon reference from index.html (no public/ directory exists) - Add meta description to index.html - Fix sidebar title inconsistency: "Process Simulator" → "Process Scheduler Simulator" - Rebuild dist.tar.gz with production assets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa009102eb
commit
22925578b1
34 files changed
+304
-255
No files matched your search
@@ -15,10 +15,10 @@ const SPEED_LABELS: Record<number, string> = {
|
||||
};
|
||||
|
||||
const statusLabel: Record<SimStatus, string> = {
|
||||
idle: 'Pronto',
|
||||
paused: 'Pausado',
|
||||
running: 'Executando',
|
||||
finished: 'Concluído',
|
||||
idle: 'Ready',
|
||||
paused: 'Paused',
|
||||
running: 'Running',
|
||||
finished: 'Finished',
|
||||
};
|
||||
|
||||
const statusDotClass: Record<SimStatus, string> = {
|
||||
@@ -68,7 +68,7 @@ const ControlBar = () => {
|
||||
<div
|
||||
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
|
||||
role="toolbar"
|
||||
aria-label="Controles da simulação"
|
||||
aria-label="Simulation controls"
|
||||
>
|
||||
{/* Left group: simulation controls */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -85,7 +85,7 @@ const ControlBar = () => {
|
||||
|
||||
{/* Step */}
|
||||
<IconButton
|
||||
label="Avançar 1 tick (Step)"
|
||||
label="Advance 1 tick (Step)"
|
||||
variant="default"
|
||||
onClick={handleStep}
|
||||
disabled={isFinished || processes.length === 0 || isRunning}
|
||||
@@ -97,7 +97,7 @@ const ControlBar = () => {
|
||||
{/* Play / Pause */}
|
||||
{isRunning ? (
|
||||
<IconButton
|
||||
label="Pausar simulação"
|
||||
label="Pause simulation"
|
||||
variant="primary"
|
||||
onClick={pause}
|
||||
data-testid="btn-pause"
|
||||
@@ -106,7 +106,7 @@ const ControlBar = () => {
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
label="Iniciar / Retomar simulação"
|
||||
label="Start / Resume simulation"
|
||||
variant="primary"
|
||||
onClick={handlePlay}
|
||||
disabled={!canPlay && !isRunning}
|
||||
@@ -124,7 +124,7 @@ const ControlBar = () => {
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700"
|
||||
aria-live="polite"
|
||||
aria-label={`Tick atual: ${currentTick}`}
|
||||
aria-label={`Current tick: ${currentTick}`}
|
||||
>
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span>
|
||||
<span
|
||||
@@ -145,14 +145,14 @@ const ControlBar = () => {
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<SkipBack size={14} className="text-slate-500" aria-hidden="true" />
|
||||
<label htmlFor="speed-select" className="text-xs text-slate-400 sr-only">
|
||||
Velocidade
|
||||
Speed
|
||||
</label>
|
||||
<select
|
||||
id="speed-select"
|
||||
value={speed}
|
||||
onChange={e => setSpeed(Number(e.target.value))}
|
||||
className="rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||
aria-label="Velocidade de simulação"
|
||||
aria-label="Simulation speed"
|
||||
data-testid="speed-select"
|
||||
>
|
||||
{SPEED_OPTIONS.map(s => (
|
||||
|
||||
@@ -27,7 +27,7 @@ const GanttChart = () => {
|
||||
return (
|
||||
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
|
||||
style={{ minHeight: 80 }}>
|
||||
Inicie a simulação para ver o diagrama de Gantt
|
||||
Start the simulation to see the Gantt chart
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ const AppHeader = () => {
|
||||
<button
|
||||
onClick={() => setPresetsOpen(o => !o)}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
|
||||
aria-label="Carregar cenário pré-definido"
|
||||
aria-label="Load preset scenario"
|
||||
>
|
||||
<BookOpen size={13} />
|
||||
Presets
|
||||
@@ -83,7 +83,7 @@ const AppHeader = () => {
|
||||
{/* Dropdown */}
|
||||
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden flex flex-col max-h-[min(32rem,calc(100vh-4rem))]">
|
||||
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium shrink-0">
|
||||
Cenários pré-definidos
|
||||
Preset Scenarios
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
{presets.map((preset, i) => (
|
||||
@@ -112,16 +112,16 @@ const AppHeader = () => {
|
||||
{/* Import JSON */}
|
||||
<label
|
||||
htmlFor="import-json"
|
||||
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'}
|
||||
title={isSimulationActive ? 'Reset the simulation before importing' : 'Import JSON configuration'}
|
||||
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
|
||||
isSimulationActive
|
||||
? 'text-slate-600 cursor-not-allowed'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-700'
|
||||
}`}
|
||||
aria-label="Importar configuração JSON"
|
||||
aria-label="Import JSON configuration"
|
||||
>
|
||||
<Upload size={13} />
|
||||
Importar
|
||||
Import
|
||||
</label>
|
||||
<input
|
||||
id="import-json"
|
||||
@@ -131,19 +131,19 @@ const AppHeader = () => {
|
||||
className="sr-only"
|
||||
onChange={handleImportJSON}
|
||||
disabled={isSimulationActive}
|
||||
aria-label="Selecionar arquivo JSON para importar"
|
||||
aria-label="Select JSON file to import"
|
||||
/>
|
||||
|
||||
{/* Export JSON */}
|
||||
<button
|
||||
onClick={handleExportJSON}
|
||||
disabled={processes.length === 0}
|
||||
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'}
|
||||
title={processes.length === 0 ? 'Add processes before exporting' : 'Export configuration as JSON'}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Exportar configuração JSON"
|
||||
aria-label="Export JSON configuration"
|
||||
>
|
||||
<Download size={13} />
|
||||
Exportar
|
||||
Export
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@ const Sidebar = () => {
|
||||
return (
|
||||
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
|
||||
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
|
||||
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
|
||||
Process Simulator
|
||||
<h1 className="text-xl font-bold bg-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
|
||||
Process Scheduler Simulator
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ const Sidebar = () => {
|
||||
</div>
|
||||
|
||||
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
|
||||
PSS v1.0
|
||||
PSS v1.2.0
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
<MetricCard
|
||||
label="Avg Response Time"
|
||||
value={avgResponseTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
sub="time units"
|
||||
accentColor="#f59e0b"
|
||||
accentClass="text-amber-300"
|
||||
icon={<Clock size={13} />}
|
||||
@@ -79,7 +79,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
<MetricCard
|
||||
label="Avg Turnaround Time"
|
||||
value={avgTurnaroundTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
sub="time units"
|
||||
accentColor="#60a5fa"
|
||||
accentClass="text-blue-300"
|
||||
icon={<Timer size={13} />}
|
||||
@@ -88,7 +88,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
<MetricCard
|
||||
label="Avg Waiting Time"
|
||||
value={avgWaitingTime.toFixed(2)}
|
||||
sub="unidades de tempo"
|
||||
sub="time units"
|
||||
accentColor="#fb923c"
|
||||
accentClass="text-orange-300"
|
||||
icon={<Hourglass size={13} />}
|
||||
@@ -97,7 +97,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
<MetricCard
|
||||
label="CPU Throughput"
|
||||
value={cpuThroughput.toFixed(3)}
|
||||
sub="processos / u.t."
|
||||
sub="processes / t.u."
|
||||
accentColor="#34d399"
|
||||
accentClass="text-emerald-300"
|
||||
icon={<Zap size={13} />}
|
||||
@@ -120,7 +120,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
|
||||
<span className="text-2xl font-bold font-mono leading-none text-violet-300">
|
||||
{utilizationPct.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">do tempo total em CPU</span>
|
||||
<span className="text-[10px] text-slate-500">of total CPU time</span>
|
||||
<div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
|
||||
@@ -32,7 +32,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
|
||||
Nenhum dado disponível.
|
||||
No data available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,35 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
const getName = (id: string) => processMap[id]?.name ?? id;
|
||||
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
|
||||
|
||||
// Reverse map: process name → process (for radar tooltip lookup)
|
||||
const nameToProcess = Object.fromEntries(
|
||||
Object.values(processMap).map(p => [p.name, p])
|
||||
);
|
||||
|
||||
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
|
||||
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
|
||||
{label}
|
||||
</p>
|
||||
{payload.map((entry: any) => {
|
||||
const proc = nameToProcess[entry.name];
|
||||
const displayName = proc?.pid != null ? `#${proc.pid} ${proc.name}` : entry.name;
|
||||
return (
|
||||
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
|
||||
<span style={{ color: '#cbd5e1' }}>{displayName}</span>
|
||||
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
|
||||
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Stacked bar data ──────────────────────────────────────────
|
||||
const barData = metrics.map((m) => ({
|
||||
name: getName(m.processId),
|
||||
@@ -87,9 +116,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
{showRadar && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Perfil Comparativo (normalizado)
|
||||
Comparative Profile (normalized)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
|
||||
<PolarGrid stroke="#334155" />
|
||||
<PolarAngleAxis
|
||||
@@ -108,10 +137,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
dot={{ r: 3, fill: getColor(m.processId) }}
|
||||
/>
|
||||
))}
|
||||
<Tooltip
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
formatter={(value) => [typeof value === 'number' ? `${value.toFixed(1)}%` : '-', '']}
|
||||
/>
|
||||
<Tooltip content={radarTooltipContent} />
|
||||
<Legend
|
||||
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
|
||||
/>
|
||||
@@ -123,9 +149,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
|
||||
{/* ── Stacked bar chart ─────────────────────────────────── */}
|
||||
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
|
||||
Breakdown por Processo (ticks)
|
||||
Breakdown per Process (ticks)
|
||||
</p>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis
|
||||
|
||||
@@ -8,12 +8,12 @@ import MetricsTable from './MetricsTable';
|
||||
import GlobalMetrics from './GlobalMetrics';
|
||||
import MetricsChart from './MetricsChart';
|
||||
|
||||
type Tab = 'table' | 'global' | 'chart';
|
||||
type Tab = 'table' | 'global' | 'charts';
|
||||
|
||||
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: 'table', label: 'Por Processo', icon: <Table size={13} /> },
|
||||
{ id: 'table', label: 'Per Process', icon: <Table size={13} /> },
|
||||
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
|
||||
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> },
|
||||
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
|
||||
];
|
||||
|
||||
const MetricsPanel = () => {
|
||||
@@ -31,7 +31,7 @@ const MetricsPanel = () => {
|
||||
if (!hasData) {
|
||||
return (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
|
||||
Execute a simulação para ver as métricas.
|
||||
Run the simulation to see metrics.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,7 +46,7 @@ const MetricsPanel = () => {
|
||||
const canExportCSV = status === 'finished' && processMetrics.length > 0;
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '340px' }}>
|
||||
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '600px' }}>
|
||||
{/* Panel header with tabs + export button */}
|
||||
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
|
||||
{TAB_CONFIG.map(tab => (
|
||||
@@ -69,9 +69,9 @@ const MetricsPanel = () => {
|
||||
<button
|
||||
onClick={() => exportCSV(processMetrics, processMap)}
|
||||
disabled={!canExportCSV}
|
||||
title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como CSV'}
|
||||
title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as CSV'}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Exportar métricas CSV"
|
||||
aria-label="Export metrics as CSV"
|
||||
>
|
||||
<Download size={12} />
|
||||
CSV
|
||||
@@ -87,7 +87,7 @@ const MetricsPanel = () => {
|
||||
{activeTab === 'global' && (
|
||||
<GlobalMetrics metrics={globalMetrics} />
|
||||
)}
|
||||
{activeTab === 'chart' && (
|
||||
{activeTab === 'charts' && (
|
||||
<MetricsChart metrics={processMetrics} processMap={processMap} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ interface MiniBarProps {
|
||||
const MiniBar = ({ value, max, color }: MiniBarProps) => {
|
||||
const pct = max > 0 ? (value / max) * 100 : 0;
|
||||
return (
|
||||
<div className="relative flex items-center justify-end gap-1.5 min-w-[52px]">
|
||||
<div className="relative flex items-center justify-end gap-1.5 min-w-13">
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
|
||||
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||
@@ -36,7 +36,7 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
|
||||
Nenhum processo concluído ainda.
|
||||
No completed processes yet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,10 +50,10 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-[780px]">
|
||||
<table className="w-full text-xs text-slate-300 border-collapse min-w-195">
|
||||
<thead>
|
||||
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
|
||||
<th className="text-left px-3 py-2 font-medium">Processo</th>
|
||||
<th className="text-left px-3 py-2 font-medium">Process</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Arrival</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Start</th>
|
||||
<th className="text-right px-3 py-2 font-medium">Finish</th>
|
||||
|
||||
@@ -56,7 +56,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label={`Editar ${process.name}`}
|
||||
label={`Edit ${process.name}`}
|
||||
onClick={() => onEdit(process)}
|
||||
>
|
||||
<Pencil size={13} />
|
||||
@@ -64,7 +64,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label={`Remover ${process.name}`}
|
||||
label={`Remove ${process.name}`}
|
||||
onClick={() => onRemove(process.id)}
|
||||
className="hover:text-red-400 hover:bg-red-900/20"
|
||||
>
|
||||
|
||||
@@ -69,16 +69,16 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errs: string[] = [];
|
||||
if (!name.trim()) errs.push('Nome é obrigatório');
|
||||
if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres');
|
||||
if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0');
|
||||
if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10');
|
||||
if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU');
|
||||
if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU');
|
||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
|
||||
if (!name.trim()) errs.push('Name is required');
|
||||
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
|
||||
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
|
||||
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
|
||||
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
|
||||
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
|
||||
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
|
||||
bursts.forEach((b, i) => {
|
||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
|
||||
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
|
||||
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} and ${i + 1} cannot be the same type (alternate CPU/IO)`);
|
||||
});
|
||||
setErrors(errs);
|
||||
return errs.length === 0;
|
||||
@@ -108,9 +108,9 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
<div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-slate-200">
|
||||
{isEditing ? 'Editar Processo' : 'Novo Processo'}
|
||||
{isEditing ? 'Edit Process' : 'New Process'}
|
||||
</h3>
|
||||
<IconButton size="sm" variant="ghost" label="Fechar" onClick={onClose}>
|
||||
<IconButton size="sm" variant="ghost" label="Close" onClick={onClose}>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
<div className="col-span-2 flex gap-3">
|
||||
{/* Name */}
|
||||
<div className="flex-1">
|
||||
<label className={labelCls} htmlFor="pf-name">Nome</label>
|
||||
<label className={labelCls} htmlFor="pf-name">Name</label>
|
||||
<input
|
||||
id="pf-name"
|
||||
type="text"
|
||||
@@ -129,19 +129,19 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="P1"
|
||||
className={inputCls}
|
||||
aria-label="Nome do processo"
|
||||
aria-label="Process name"
|
||||
/>
|
||||
</div>
|
||||
{/* Color */}
|
||||
<div>
|
||||
<label className={labelCls} htmlFor="pf-color">Cor</label>
|
||||
<label className={labelCls} htmlFor="pf-color">Color</label>
|
||||
<input
|
||||
id="pf-color"
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={e => setColor(e.target.value)}
|
||||
className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
|
||||
aria-label="Cor do processo"
|
||||
aria-label="Process color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +156,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
value={arrivalTime}
|
||||
onChange={e => setArrivalTime(Number(e.target.value))}
|
||||
className={inputCls}
|
||||
aria-label="Tempo de chegada"
|
||||
aria-label="Arrival time"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
value={priority}
|
||||
onChange={e => setPriority(Number(e.target.value))}
|
||||
className={inputCls}
|
||||
aria-label="Prioridade do processo"
|
||||
aria-label="Process priority"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +180,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
|
||||
<IconButton size="sm" variant="ghost" label="Adicionar burst" onClick={addBurst}>
|
||||
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
|
||||
<Plus size={13} />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -203,13 +203,13 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
value={burst.duration}
|
||||
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
|
||||
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Duração do burst ${idx + 1}`}
|
||||
aria-label={`Burst ${idx + 1} duration`}
|
||||
/>
|
||||
<span className="text-[10px] text-slate-500">ticks</span>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label={`Remover burst ${idx + 1}`}
|
||||
label={`Remove burst ${idx + 1}`}
|
||||
onClick={() => removeBurst(idx)}
|
||||
disabled={bursts.length <= 1}
|
||||
className="hover:text-red-400"
|
||||
@@ -235,17 +235,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors"
|
||||
aria-label={isEditing ? 'Salvar processo' : 'Criar processo'}
|
||||
aria-label={isEditing ? 'Save process' : 'Create process'}
|
||||
>
|
||||
<Check size={15} />
|
||||
{isEditing ? 'Salvar' : 'Criar'}
|
||||
{isEditing ? 'Save' : 'Create'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
|
||||
aria-label="Cancelar"
|
||||
aria-label="Cancel"
|
||||
>
|
||||
Cancelar
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,13 +47,13 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
||||
{/* List header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-slate-400">
|
||||
{processes.length}/{MAX_PROCESSES} processos
|
||||
{processes.length}/{MAX_PROCESSES} processes
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label="Gerar processo aleatório"
|
||||
label="Generate random process"
|
||||
onClick={handleRandomize}
|
||||
disabled={!canAdd}
|
||||
>
|
||||
@@ -62,7 +62,7 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
label="Limpar todos os processos"
|
||||
label="Clear all processes"
|
||||
onClick={clearProcesses}
|
||||
disabled={processes.length === 0}
|
||||
className="hover:text-red-400 hover:bg-red-900/20"
|
||||
@@ -76,8 +76,8 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
|
||||
{processes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg">
|
||||
<span>Nenhum processo adicionado</span>
|
||||
<span className="text-xs">Clique em + para criar</span>
|
||||
<span>No processes added</span>
|
||||
<span className="text-xs">Click + to create</span>
|
||||
</div>
|
||||
) : (
|
||||
processes.map(p => (
|
||||
@@ -95,11 +95,11 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
|
||||
{canAdd && (
|
||||
<button
|
||||
onClick={onAddNew}
|
||||
aria-label="Adicionar novo processo"
|
||||
aria-label="Add new process"
|
||||
className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Adicionar Processo
|
||||
Add Process
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,10 +24,10 @@ const ProcessPanel = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-label="Painel de Processos">
|
||||
<section aria-label="Processes Panel">
|
||||
<div className="flex items-center gap-2 mb-3 text-slate-300">
|
||||
<ListTree size={18} className="text-blue-400" />
|
||||
<h2 className="font-semibold uppercase text-xs tracking-wider">Processos</h2>
|
||||
<h2 className="font-semibold uppercase text-xs tracking-wider">Processes</h2>
|
||||
</div>
|
||||
|
||||
{showForm ? (
|
||||
|
||||
@@ -6,9 +6,9 @@ const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?:
|
||||
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
|
||||
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
|
||||
{ value: 'RR', label: 'RR — Round Robin' },
|
||||
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
|
||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
|
||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
|
||||
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
|
||||
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
|
||||
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
|
||||
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
|
||||
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
|
||||
];
|
||||
@@ -23,14 +23,14 @@ const AlgorithmSelector = () => {
|
||||
htmlFor="algorithm-select"
|
||||
className="block text-xs text-slate-400 mb-1"
|
||||
>
|
||||
Algoritmo
|
||||
Algorithm
|
||||
</label>
|
||||
<select
|
||||
id="algorithm-select"
|
||||
value={algorithm}
|
||||
onChange={e => setConfig({ algorithm: e.target.value as SchedulingAlgorithm })}
|
||||
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||
aria-label="Selecionar algoritmo de escalonamento"
|
||||
aria-label="Select scheduling algorithm"
|
||||
>
|
||||
{ALGORITHM_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
||||
|
||||
@@ -71,14 +71,14 @@ const MlqQueuesConfig = ({
|
||||
return (
|
||||
<div className="space-y-2 pt-1" data-testid="mlq-queues-config">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium">Filas MLQ (prioridade decrescente)</span>
|
||||
<span className="text-xs text-slate-400 font-medium">MLQ Queues (decreasing priority)</span>
|
||||
<button
|
||||
onClick={addQueue}
|
||||
disabled={queues.length >= 5}
|
||||
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label="Adicionar fila"
|
||||
aria-label="Add queue"
|
||||
>
|
||||
<Plus size={11} /> Fila
|
||||
<Plus size={11} /> Queue
|
||||
</button>
|
||||
</div>
|
||||
{queues.map((q, idx) => (
|
||||
@@ -89,46 +89,46 @@ const MlqQueuesConfig = ({
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
|
||||
Fila {idx} {idx === 0 ? '(mais alta)' : idx === queues.length - 1 ? '(mais baixa)' : ''}
|
||||
Queue {idx} {idx === 0 ? '(highest)' : idx === queues.length - 1 ? '(lowest)' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeQueue(idx)}
|
||||
disabled={queues.length <= 1}
|
||||
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
|
||||
aria-label={`Remover fila ${idx}`}
|
||||
aria-label={`Remove queue ${idx}`}
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. mín</label>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Min. Prio</label>
|
||||
<input
|
||||
type="number" min={1} max={10}
|
||||
value={q.priorityMin}
|
||||
onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Fila ${idx}: prioridade mínima`}
|
||||
aria-label={`Queue ${idx}: minimum priority`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. máx</label>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Max. Prio</label>
|
||||
<input
|
||||
type="number" min={1} max={10}
|
||||
value={q.priorityMax}
|
||||
onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label={`Fila ${idx}: prioridade máxima`}
|
||||
aria-label={`Queue ${idx}: maximum priority`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Algoritmo</label>
|
||||
<label className="block text-[10px] text-slate-500 mb-0.5">Algorithm</label>
|
||||
<select
|
||||
value={q.algorithm}
|
||||
onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })}
|
||||
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
|
||||
aria-label={`Fila ${idx}: algoritmo`}
|
||||
aria-label={`Queue ${idx}: algorithm`}
|
||||
>
|
||||
{MLQ_ALGO_OPTIONS.map(a => (
|
||||
<option key={a} value={a}>{a}</option>
|
||||
@@ -167,7 +167,7 @@ const SchedulerConfig = () => {
|
||||
|
||||
{/* Read-only preemptive indicator */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400">Tipo de escalonamento</span>
|
||||
<span className="text-xs text-slate-400">Scheduling type</span>
|
||||
<span
|
||||
className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
|
||||
isPreemptive
|
||||
@@ -176,7 +176,7 @@ const SchedulerConfig = () => {
|
||||
}`}
|
||||
data-testid="preemptive-badge"
|
||||
>
|
||||
{isPreemptive ? 'Preemptivo' : 'Não-preemptivo'}
|
||||
{isPreemptive ? 'Preemptive' : 'Non-preemptive'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +193,7 @@ const SchedulerConfig = () => {
|
||||
value={config.contextSwitchTime}
|
||||
onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })}
|
||||
className={inputCls}
|
||||
aria-label="Tempo de context switch"
|
||||
aria-label="Context switch time"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -210,7 +210,7 @@ const SchedulerConfig = () => {
|
||||
value={config.quantum}
|
||||
onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })}
|
||||
className={inputCls}
|
||||
aria-label="Quantum do Round Robin"
|
||||
aria-label="Round Robin quantum"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -227,7 +227,7 @@ const SchedulerConfig = () => {
|
||||
{config.agingEnabled && (
|
||||
<div data-testid="aging-interval-field">
|
||||
<label htmlFor="aging-interval" className={labelCls}>
|
||||
Intervalo de Aging (ticks)
|
||||
Aging Interval (ticks)
|
||||
</label>
|
||||
<input
|
||||
id="aging-interval"
|
||||
@@ -236,7 +236,7 @@ const SchedulerConfig = () => {
|
||||
value={config.agingInterval}
|
||||
onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })}
|
||||
className={inputCls}
|
||||
aria-label="Intervalo de aging"
|
||||
aria-label="Aging interval"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,10 +4,10 @@ import SchedulerConfig from './SchedulerConfig';
|
||||
|
||||
const SchedulerPanel = () => {
|
||||
return (
|
||||
<section aria-label="Painel do Escalonador" className="space-y-3">
|
||||
<section aria-label="Scheduler Panel" className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-slate-300">
|
||||
<Settings size={18} className="text-emerald-400" />
|
||||
<h2 className="font-semibold uppercase text-xs tracking-wider">Escalonador</h2>
|
||||
<h2 className="font-semibold uppercase text-xs tracking-wider">Scheduler</h2>
|
||||
</div>
|
||||
|
||||
<AlgorithmSelector />
|
||||
|
||||
@@ -38,9 +38,9 @@ describe('ControlBar', () => {
|
||||
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
|
||||
});
|
||||
|
||||
it('shows "Pronto" status when idle', () => {
|
||||
it('shows "Ready" status when idle', () => {
|
||||
render(<ControlBar />);
|
||||
expect(screen.getByText('Pronto')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ready')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Play button is disabled when no processes are loaded', () => {
|
||||
|
||||
@@ -11,62 +11,62 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe('ProcessForm', () => {
|
||||
it('renders the "Novo Processo" form with default fields', () => {
|
||||
it('renders the "New Process" 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();
|
||||
expect(screen.getByText('New Process')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Process name')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Arrival time')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Process priority')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Editar Processo" title when editingProcess is provided', () => {
|
||||
it('shows "Edit Process" 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();
|
||||
expect(screen.getByText('Edit Process')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows validation error when name is empty', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProcessForm onClose={() => {}} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
const nameInput = screen.getByLabelText('Process name');
|
||||
await user.clear(nameInput);
|
||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
||||
const submitBtn = screen.getByRole('button', { name: /Create/i });
|
||||
await user.click(submitBtn);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Name is required/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');
|
||||
const nameInput = screen.getByLabelText('Process name');
|
||||
await user.type(nameInput, 'P1');
|
||||
const arrivalInput = screen.getByLabelText('Tempo de chegada');
|
||||
const arrivalInput = screen.getByLabelText('Arrival time');
|
||||
// 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 });
|
||||
const submitBtn = screen.getByRole('button', { name: /Create/i });
|
||||
await user.click(submitBtn);
|
||||
expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Arrival Time must be ≥ 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');
|
||||
const nameInput = screen.getByLabelText('Process name');
|
||||
// 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();
|
||||
await user.click(screen.getByRole('button', { name: /Create/i }));
|
||||
expect(screen.getByText(/Name must be at most 8 characters/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');
|
||||
const addBurstBtn = screen.getByLabelText('Add burst');
|
||||
await user.click(addBurstBtn);
|
||||
// After one CPU burst at start, clicking + adds IO burst
|
||||
// getAllByText with exact matches for the type labels
|
||||
@@ -80,9 +80,9 @@ describe('ProcessForm', () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = vi.fn();
|
||||
render(<ProcessForm onClose={onClose} />);
|
||||
const nameInput = screen.getByLabelText('Nome do processo');
|
||||
const nameInput = screen.getByLabelText('Process name');
|
||||
await user.type(nameInput, 'P1');
|
||||
const submitBtn = screen.getByRole('button', { name: /Criar/i });
|
||||
const submitBtn = screen.getByRole('button', { name: /Create/i });
|
||||
await user.click(submitBtn);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -90,9 +90,9 @@ describe('ProcessForm', () => {
|
||||
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');
|
||||
const nameInput = screen.getByLabelText('Process name');
|
||||
await user.type(nameInput, 'Test1');
|
||||
await user.click(screen.getByRole('button', { name: /Criar/i }));
|
||||
await user.click(screen.getByRole('button', { name: /Create/i }));
|
||||
const processes = useProcessStore.getState().processes;
|
||||
expect(processes).toHaveLength(1);
|
||||
expect(processes[0]?.name).toBe('Test1');
|
||||
|
||||
@@ -26,7 +26,7 @@ beforeEach(() => {
|
||||
describe('SchedulerConfig', () => {
|
||||
it('always shows Context Switch field regardless of algorithm', () => {
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Context switch time')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('always shows read-only preemptive badge', () => {
|
||||
@@ -34,23 +34,23 @@ describe('SchedulerConfig', () => {
|
||||
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('badge shows "Não-preemptivo" for FCFS', () => {
|
||||
it('badge shows "Non-preemptive" for FCFS', () => {
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
|
||||
});
|
||||
|
||||
it('badge shows "Preemptivo" for SJF_P (SRTF)', () => {
|
||||
it('badge shows "Preemptive" for SJF_P (SRTF)', () => {
|
||||
setAlgorithm('SJF_P');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo');
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptive');
|
||||
});
|
||||
|
||||
it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => {
|
||||
it('badge shows "Non-preemptive" for SJF_NP (preemptive toggle no longer exists)', () => {
|
||||
setAlgorithm('SJF_NP');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
|
||||
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
|
||||
// Confirm the old interactive preemptive switch is gone
|
||||
expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('switch', { name: /Preemptive/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT show Quantum field for FCFS', () => {
|
||||
@@ -62,7 +62,7 @@ describe('SchedulerConfig', () => {
|
||||
setAlgorithm('RR');
|
||||
render(<SchedulerConfig />);
|
||||
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Round Robin quantum')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => {
|
||||
@@ -97,13 +97,13 @@ describe('SchedulerConfig', () => {
|
||||
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();
|
||||
expect(screen.getByLabelText('Aging interval')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates the store when Quantum field is changed', () => {
|
||||
setAlgorithm('RR');
|
||||
render(<SchedulerConfig />);
|
||||
const quantumInput = screen.getByLabelText('Quantum do Round Robin');
|
||||
const quantumInput = screen.getByLabelText('Round Robin quantum');
|
||||
fireEvent.change(quantumInput, { target: { value: '5' } });
|
||||
expect(useProcessStore.getState().config.quantum).toBe(5);
|
||||
});
|
||||
|
||||
@@ -236,11 +236,11 @@ describe("SimulationEngine — ciclo de vida", () => {
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SimulationEngine — chegada de processos
|
||||
// SimulationEngine — process arrival
|
||||
// Tests: late arrivals, idle ticks while waiting for process
|
||||
// ============================================================
|
||||
|
||||
describe("SimulationEngine — chegada de processos", () => {
|
||||
describe("SimulationEngine — process arrival", () => {
|
||||
it("process arriving after simulation starts is correctly scheduled", () => {
|
||||
const engine = new SimulationEngine(
|
||||
[
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ export const presets: Preset[] = [
|
||||
},
|
||||
{
|
||||
name: "context_switch_demo",
|
||||
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch",
|
||||
description: "3 FCFS processes with 2 ticks of overhead — see the red Context Switch cells",
|
||||
processes: [
|
||||
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
|
||||
|
||||
@@ -18,7 +18,9 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
|
||||
export function useExport() {
|
||||
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
|
||||
const headers = [
|
||||
"Processo",
|
||||
"PID",
|
||||
"Process",
|
||||
"Priority",
|
||||
"Arrival",
|
||||
"Start",
|
||||
"Finish",
|
||||
@@ -32,9 +34,14 @@ export function useExport() {
|
||||
];
|
||||
|
||||
const rows = metrics.map((m) => {
|
||||
const name = processMap[m.processId]?.name ?? m.processId;
|
||||
const proc = processMap[m.processId];
|
||||
const pid = proc?.pid ?? "";
|
||||
const name = proc?.name ?? m.processId;
|
||||
const priority = proc?.priority ?? "";
|
||||
return [
|
||||
pid,
|
||||
name,
|
||||
priority,
|
||||
m.arrivalTime,
|
||||
m.startTick,
|
||||
m.finishTick,
|
||||
|
||||
Reference in new issue
Block a user