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:
HMS MediaEngine Agent
2026-09-11 08:41:44 +02:00
parent f7f52f909d
commit 739a81bcf1
8 changed files with 1031 additions and 3 deletions
@@ -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();
});
});
+50
View File
@@ -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');
});
});
+8
View File
@@ -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();
});