feat(quick-260921-iwr): Rundenliste der Stoppuhr per Test belegt, MergeTab-Entfernen abgesichert
- Wirkungslose eslint-disable-Zeile in stopwatch-widget.tsx ersetzt durch Sachhinweis: Zeilen halten keinen Zustand, Rundennummer wird aus Laenge und Position berechnet. Keine neue Unterdrueckung, ARRAYKEY bleibt bei 19. - Neuer Testfall in stopwatch-widget.test.tsx: zwei Runden nacheinander, neuere Runde steht oben, Rundennummern 2/1 stimmen zu ihrer eigenen Zeit. - Neue MergeTab.test.tsx: Entfernen der mittleren Datei laesst genau erste und dritte Datei mit eigenem Namen und eigenem Entfernen-Knopf uebrig. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock next-intl — passthrough t(key) => key (same pattern as stopwatch-widget.test.tsx)
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
// Mock ../actions — MergeTab only needs mergeCertsAction and downloadBase64 to exist;
|
||||
// this test never triggers the merge action itself, only file-list management.
|
||||
vi.mock('../actions', () => ({
|
||||
mergeCertsAction: vi.fn(),
|
||||
downloadBase64: vi.fn(),
|
||||
}));
|
||||
|
||||
import { MergeTab } from './MergeTab';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('MergeTab — Dateiliste', () => {
|
||||
it('quick-260921-iwr: Entfernen der mittleren Datei laesst genau die erste und dritte Datei uebrig, in dieser Reihenfolge, mit ihren eigenen Namen (belegt den Positionsschluessel bei einer schrumpfenden Liste)', async () => {
|
||||
render(<MergeTab password="" />);
|
||||
|
||||
const fileInput = screen.getByTestId('merge-file-input');
|
||||
const file1 = new File(['cert1'], 'erste.pem', { type: 'application/x-pem-file' });
|
||||
const file2 = new File(['cert2'], 'mittlere.pem', { type: 'application/x-pem-file' });
|
||||
const file3 = new File(['cert3'], 'dritte.pem', { type: 'application/x-pem-file' });
|
||||
|
||||
fireEvent.change(fileInput, { target: { files: [file1, file2, file3] } });
|
||||
|
||||
// Alle drei Dateien sind zunaechst gelistet
|
||||
expect(screen.getByText('erste.pem')).toBeInTheDocument();
|
||||
expect(screen.getByText('mittlere.pem')).toBeInTheDocument();
|
||||
expect(screen.getByText('dritte.pem')).toBeInTheDocument();
|
||||
|
||||
// Die mittlere Datei ueber ihren Entfernen-Knopf loeschen (aria-label enthaelt den Dateinamen)
|
||||
const removeMiddleBtn = screen.getByLabelText('Remove mittlere.pem');
|
||||
fireEvent.click(removeMiddleBtn);
|
||||
|
||||
// "mittlere.pem" ist verschwunden
|
||||
expect(screen.queryByText('mittlere.pem')).not.toBeInTheDocument();
|
||||
|
||||
// Die erste und dritte Datei stehen weiterhin mit ihren eigenen Namen in der Liste,
|
||||
// in dieser Reihenfolge — kein Verwechseln durch den Positionsschluessel.
|
||||
const remainingNames = screen
|
||||
.getAllByText(/\.pem$/)
|
||||
.map((el) => el.textContent);
|
||||
expect(remainingNames).toEqual(['erste.pem', 'dritte.pem']);
|
||||
|
||||
// Die dritte Datei behaelt ihren eigenen Entfernen-Knopf (eigener Name im aria-label,
|
||||
// nicht der der geloeschten mittleren Datei).
|
||||
expect(screen.getByLabelText('Remove erste.pem')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Remove dritte.pem')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Remove mittlere.pem')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('quick-260921-iwr: Entfernen der ersten Datei laesst die zweite an ihrer eigenen Stelle mit eigenem Namen zurueck', async () => {
|
||||
render(<MergeTab password="" />);
|
||||
|
||||
const fileInput = screen.getByTestId('merge-file-input');
|
||||
const file1 = new File(['cert1'], 'a.pem', { type: 'application/x-pem-file' });
|
||||
const file2 = new File(['cert2'], 'b.pem', { type: 'application/x-pem-file' });
|
||||
|
||||
fireEvent.change(fileInput, { target: { files: [file1, file2] } });
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove a.pem'));
|
||||
|
||||
expect(screen.queryByText('a.pem')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('b.pem')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Remove b.pem')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -158,6 +158,43 @@ describe('StopwatchWidget', () => {
|
||||
expect(lapItems.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('quick-260921-iwr: zwei Runden nacheinander — neuere Runde steht oben, Rundennummern 2 und 1 stimmen zu ihrer eigenen Zeit (belegt den Positionsschluessel bei einer von vorn wachsenden Liste)', async () => {
|
||||
render(<StopwatchWidget instanceId="sw-laps" config={{}} isEditMode={false} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /stopwatch\.start/i }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /stopwatch\.lap/i }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /stopwatch\.lap/i }));
|
||||
});
|
||||
|
||||
const lapItems = screen.getAllByRole('listitem');
|
||||
expect(lapItems).toHaveLength(2);
|
||||
|
||||
// Oben steht die neuere Runde (Nummer 2), unten die aeltere (Nummer 1) —
|
||||
// die Liste waechst von vorn, newest-first.
|
||||
expect(lapItems[0]?.textContent).toContain('Runde 2');
|
||||
expect(lapItems[1]?.textContent).toContain('Runde 1');
|
||||
|
||||
// Runde 2 wurde spaeter aufgezeichnet und zeigt daher die laengere Zeit,
|
||||
// Runde 1 die kuerzere — jede Zeit steht bei ihrer eigenen Nummer.
|
||||
expect(lapItems[0]?.textContent).toContain('00:02');
|
||||
expect(lapItems[1]?.textContent).toContain('00:01');
|
||||
});
|
||||
|
||||
it('reload reconstruction: renders ~7000ms elapsed from config with startedAt 5s ago', async () => {
|
||||
// Fix system time so Date.now() is deterministic
|
||||
const fixedNow = new Date('2026-07-01T12:00:00.000Z');
|
||||
|
||||
@@ -271,8 +271,10 @@ export function StopwatchWidget({ instanceId, config, isEditMode: _isEditMode }:
|
||||
<div className="mt-1 max-h-32 overflow-y-auto border-t border-border pt-2">
|
||||
<ul className="space-y-0.5">
|
||||
{sw.laps.map((lapMs, idx) => (
|
||||
// Positionsschluessel ist hier vertretbar: die Zeilen halten keinen
|
||||
// eigenen Zustand, und die angezeigte Rundennummer wird ohnehin aus
|
||||
// Laenge und Position berechnet (quick-260921-iwr, D-01/D-06).
|
||||
<li
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={idx}
|
||||
className="flex justify-between px-1 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user