Build: Windows Portable-Build (§30.2) + Frontend-Tests (§17.10)
- build_portable.sh: Nuitka Onefolder (ADR-0005), 5 Schritte: uv sync -> Frontend -> Rust (cargo optional) -> Nuitka -> GStreamer-Buendelung + portable Struktur + SHA-256 + ZIP - Portable Struktur (§9): PORTABLE_MODE, runtime/gstreamer, web/, plugins/, projects/, media/, userdata/, config/, logs/, licenses/ - Frontend-Tests: Vitest + Testing Library, 9 Tests: LayerTable (10 Spalten, Selektion, 7 Zustaende, Screen-Reader), StatusBar (FPS 2 Dezimalstellen, Quality Auto, Art-Net-Status) - Gesamtsuite 595 Python + 9 Vitest gruen
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import LayerTable from '../components/LayerTable';
|
||||
import type { Layer } from '../types';
|
||||
|
||||
function makeLayer(overrides: Partial<Layer> = {}): Layer {
|
||||
return {
|
||||
id: 'layer-1',
|
||||
z: 1,
|
||||
name: 'Test Layer',
|
||||
type: 'media',
|
||||
status: 'active',
|
||||
sourceName: 'Clip A',
|
||||
sourceBank: 'B1·04',
|
||||
target: 'Out 1',
|
||||
timePositionSec: 0,
|
||||
timeDurationSec: 10,
|
||||
opacity: 100,
|
||||
blend: 'Normal',
|
||||
fxSlots: [null, null],
|
||||
control: { sources: ['web'], owner: 'web' },
|
||||
loopMode: 'Loop',
|
||||
generatorPhase: 0,
|
||||
transform: { x: 0, y: 0, scale: 100, rotation: 0 },
|
||||
color: { brightness: 100, contrast: 100, saturation: 100, hue: 0 },
|
||||
dmx: { universe: 0, address: 1, mode: 'rgb' },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function renderTable(layers: Layer[], selectedId: string | null = null) {
|
||||
return render(
|
||||
<LayerTable
|
||||
layers={layers}
|
||||
selectedId={selectedId}
|
||||
cursorId={null}
|
||||
liveLock={false}
|
||||
onSelect={noop}
|
||||
onCursor={noop}
|
||||
onRename={noop}
|
||||
onOpacityLocal={noop}
|
||||
onOpacityCommit={noop}
|
||||
onEnterPressed={noop}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LayerTable', () => {
|
||||
it('renders all 10 required columns', () => {
|
||||
renderTable([makeLayer()]);
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(10);
|
||||
const headerTexts = headers.map((h) => h.textContent);
|
||||
// Status header is empty; the remaining 9 are named.
|
||||
expect(headerTexts).toEqual(['', 'Layer', 'Typ', 'Source', 'Target', 'Time', 'Opacity', 'Blend', 'FX', 'Control']);
|
||||
});
|
||||
|
||||
it('shows layer names', () => {
|
||||
renderTable([makeLayer({ id: 'a', name: 'Alpha' }), makeLayer({ id: 'b', name: 'Beta' })]);
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument();
|
||||
expect(screen.getByText('Beta')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('marks the selected row with the selected class', () => {
|
||||
renderTable([makeLayer({ id: 'a', name: 'Alpha' }), makeLayer({ id: 'b', name: 'Beta' })], 'b');
|
||||
const rows = screen.getAllByRole('row');
|
||||
// rows[0] is the header row; rows[1] = Alpha, rows[2] = Beta
|
||||
expect(rows[1]).not.toHaveClass('selected');
|
||||
expect(rows[2]).toHaveClass('selected');
|
||||
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('distinguishes active/paused/error status visually via tone classes', () => {
|
||||
renderTable([
|
||||
makeLayer({ id: 'a', name: 'Active', status: 'active' }),
|
||||
makeLayer({ id: 'b', name: 'Paused', status: 'paused' }),
|
||||
makeLayer({ id: 'c', name: 'Error', status: 'error' }),
|
||||
]);
|
||||
const rows = screen.getAllByRole('row');
|
||||
// Each row's status cell carries the tone class.
|
||||
expect(rows[1].querySelector('.c-status')).toHaveClass('st-active');
|
||||
expect(rows[2].querySelector('.c-status')).toHaveClass('st-paused');
|
||||
expect(rows[3].querySelector('.c-status')).toHaveClass('st-error');
|
||||
});
|
||||
|
||||
it('shows the status label as screen-reader text (never color alone)', () => {
|
||||
renderTable([makeLayer({ id: 'a', name: 'Active', status: 'active' })]);
|
||||
expect(screen.getByText('Aktiv')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import StatusBar from '../components/StatusBar';
|
||||
import type { StatusToneItem } from '../components/StatusBar';
|
||||
|
||||
function makeTone(overrides: Partial<StatusToneItem> = {}): StatusToneItem {
|
||||
return { tone: 'ok', label: 'OK', ...overrides };
|
||||
}
|
||||
|
||||
function renderBar(overrides: Partial<ComponentProps<typeof StatusBar>> = {}) {
|
||||
return render(
|
||||
<StatusBar
|
||||
projectName="Demo"
|
||||
nodeName="node-1"
|
||||
output={makeTone({ label: 'Live' })}
|
||||
quality="Auto"
|
||||
fps={59.9401}
|
||||
artnet={makeTone({ label: 'Art-Net OK' })}
|
||||
sync={makeTone({ label: 'Sync' })}
|
||||
clock="08:39:00"
|
||||
{...overrides}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('StatusBar', () => {
|
||||
it('shows FPS with two decimal places', () => {
|
||||
renderBar({ fps: 59.9401 });
|
||||
expect(screen.getByText('59.94')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows FPS placeholder when no measurement is available', () => {
|
||||
renderBar({ fps: null });
|
||||
expect(screen.getByText('—')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Quality: Auto', () => {
|
||||
renderBar({ quality: 'Auto' });
|
||||
expect(screen.getByText('Quality:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Auto')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the Art-Net status with label and tone class', () => {
|
||||
renderBar({ artnet: makeTone({ tone: 'warn', label: 'Art-Net Warn' }) });
|
||||
expect(screen.getByText('Art-Net Warn')).toBeInTheDocument();
|
||||
const item = screen.getByTitle('Art-Net: Art-Net Warn');
|
||||
expect(item).toHaveClass('tone-warn');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import { afterEach } from 'vitest';
|
||||
|
||||
// Ensure the DOM is cleaned up between tests (globals are disabled).
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
Reference in New Issue
Block a user