feat(frontend): SimulationFormPage 3-tab layout (Red Team / SOC / Tâche C2)

- Tabs: disabled per-item (aria-disabled, native disabled, tab-underline-disabled,
  arrow-key skip); aria-controls wired
- useHashTab: optional validIds fallback to defaultId
- SimulationFormPage: 3-tab refactor; SOC tab gated on review_required|done;
  C2 tab disabled in create mode; contextual sticky bar hidden on C2 tab;
  safeTab guard against stale hash; SOC-blocked banner removed
- fr.json: simulation.form.tab.{redTeam,soc,c2} keys
- DESIGN.md: disabled tab variant + aria-controls documented
- Tests: 253 passing (5 new Tabs disabled tests, SimulationFormPage rewritten
  for tab layout)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-22 10:48:18 +02:00
parent 9b6b682acc
commit 0378792dc4
9 changed files with 548 additions and 375 deletions

View File

@@ -82,3 +82,44 @@ describe('Tabs', () => {
}
});
});
describe('Tabs — disabled state', () => {
const ITEMS_WITH_DISABLED = [
{ id: 'a', label: 'Alpha' },
{ id: 'b', label: 'Beta', disabled: true },
{ id: 'c', label: 'Gamma' },
];
it('click on disabled tab does not fire onChange', () => {
const onChange = vi.fn();
render(<Tabs items={ITEMS_WITH_DISABLED} activeId="a" onChange={onChange} />);
fireEvent.click(screen.getByRole('tab', { name: /Beta/i }));
expect(onChange).not.toHaveBeenCalled();
});
it('has aria-disabled="true" on disabled tab', () => {
render(<Tabs items={ITEMS_WITH_DISABLED} activeId="a" onChange={vi.fn()} />);
const btn = screen.getByRole('tab', { name: /Beta/i });
expect(btn).toHaveAttribute('aria-disabled', 'true');
});
it('applies tab-underline-disabled class on disabled tab', () => {
render(<Tabs items={ITEMS_WITH_DISABLED} activeId="a" onChange={vi.fn()} />);
const btn = screen.getByRole('tab', { name: /Beta/i });
expect(btn).toHaveClass('tab-underline-disabled');
});
it('ArrowRight skips disabled tab (a → c, skipping b)', () => {
const onChange = vi.fn();
render(<Tabs items={ITEMS_WITH_DISABLED} activeId="a" onChange={onChange} />);
fireEvent.keyDown(screen.getByRole('tab', { name: /Alpha/i }), { key: 'ArrowRight' });
expect(onChange).toHaveBeenCalledWith('c');
});
it('ArrowLeft skips disabled tab (c → a, skipping b)', () => {
const onChange = vi.fn();
render(<Tabs items={ITEMS_WITH_DISABLED} activeId="c" onChange={onChange} />);
fireEvent.keyDown(screen.getByRole('tab', { name: /Gamma/i }), { key: 'ArrowLeft' });
expect(onChange).toHaveBeenCalledWith('a');
});
});