sprint: Media Ops — shuttle status endpoint, graceful offline mode, 18 backend tests, shuttle detail in pipeline card, error states with retry, 13 frontend vitest tests
Deploy Dashboard (Main) / Backend Tests (push) Failing after 18s
Deploy Dashboard (Main) / Frontend Tests (push) Successful in 10s
Deploy Dashboard (Main) / Build Main Image (push) Failing after 7m26s
Deploy Dashboard (Main) / Deploy to Production (push) Has been skipped
Deploy Dashboard (Main) / Backend Tests (push) Failing after 18s
Deploy Dashboard (Main) / Frontend Tests (push) Successful in 10s
Deploy Dashboard (Main) / Build Main Image (push) Failing after 7m26s
Deploy Dashboard (Main) / Deploy to Production (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Component tests for MediaOps.svelte
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
|
||||
import MediaOps from '../../src/routes/media/MediaOps.svelte';
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('../../src/lib/api.js', () => ({
|
||||
getMediaHealth: vi.fn(),
|
||||
getMediaStorage: vi.fn(),
|
||||
getMediaShuttleStatus: vi.fn(),
|
||||
getMediaStatus: vi.fn(),
|
||||
getMediaLogs: vi.fn(),
|
||||
startMediaScan: vi.fn(),
|
||||
startMediaRequeue: vi.fn(),
|
||||
}));
|
||||
|
||||
import { getMediaHealth, getMediaStorage, getMediaShuttleStatus, getMediaStatus, getMediaLogs, startMediaScan, startMediaRequeue } from '../../src/lib/api.js';
|
||||
|
||||
describe('MediaOps Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default mock data — healthy NAS
|
||||
getMediaHealth.mockResolvedValue({ status: 'ok', exit_code: 0, stdout: 'All checks passed', stderr: '' });
|
||||
getMediaStorage.mockResolvedValue({
|
||||
volume: { path: '/volume1', total: '14T', used: '12T', available: '2.2T', used_pct: 85 },
|
||||
libraries: [
|
||||
{ name: 'DJI', total_size: '3.5T', total_files: 15000, images: 5000, videos: 10000, volume_pct: 25 },
|
||||
{ name: 'Photos', total_size: '6T', total_files: 80000, images: 70000, videos: 10000, volume_pct: 43 },
|
||||
],
|
||||
directories: {
|
||||
dji_001: { path: '/volume1/video/DJI_001', size: '2.1T' },
|
||||
immich_upload: { path: '/volume1/immich/upload', size: '4.5T' },
|
||||
},
|
||||
});
|
||||
getMediaShuttleStatus.mockResolvedValue({
|
||||
running: false, queue_depth: 0, last_transfer: null, recent_errors: [], current_stage: null,
|
||||
});
|
||||
getMediaStatus.mockResolvedValue({
|
||||
scan: { running: false, pid: null },
|
||||
requeue: { running: false, pid: null },
|
||||
shuttle: { running: false, pid: null },
|
||||
'phone-ingest': { running: false, pid: null },
|
||||
});
|
||||
getMediaLogs.mockResolvedValue({ content: 'No log file found.' });
|
||||
});
|
||||
|
||||
it('renders the Media Ops header', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Media Ops')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders storage data after loading', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/14T/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders library names from storage data', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('DJI')).toBeTruthy();
|
||||
expect(screen.getByText('Photos')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows ingestion task table with scan, requeue, shuttle, phone-ingest', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Library Scan')).toBeTruthy();
|
||||
expect(screen.getByText('Thumbnail Clinic')).toBeTruthy();
|
||||
expect(screen.getByText('SSD Shuttle')).toBeTruthy();
|
||||
expect(screen.getByText('Phone Sync')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows health check button', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
const btn = screen.getByText('Run Health Check');
|
||||
expect(btn).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows Refresh Data button', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
const btn = screen.getByText('Refresh Data');
|
||||
expect(btn).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows NAS unreachable banner when status returns unavailable', async () => {
|
||||
getMediaStatus.mockResolvedValue({ status: 'unavailable', error: 'NAS unreachable' });
|
||||
getMediaStorage.mockResolvedValue({ status: 'unavailable', error: 'NAS unreachable' });
|
||||
getMediaShuttleStatus.mockResolvedValue({ status: 'unavailable', error: 'NAS unreachable' });
|
||||
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('NAS Unreachable')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows Retry button in unreachable banner', async () => {
|
||||
getMediaStatus.mockResolvedValue({ status: 'unavailable', error: 'NAS unreachable' });
|
||||
getMediaStorage.mockResolvedValue({ status: 'unavailable', error: 'NAS unreachable' });
|
||||
getMediaShuttleStatus.mockResolvedValue({ status: 'unavailable', error: 'NAS unreachable' });
|
||||
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
const retryBtn = screen.getByText('Retry');
|
||||
expect(retryBtn).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows shuttle queue depth when data available', async () => {
|
||||
getMediaShuttleStatus.mockResolvedValue({
|
||||
running: true, queue_depth: 3, last_transfer: 'rsync: 1.2GB from SD_CARD', recent_errors: [], current_stage: 'Stage 2: verify',
|
||||
});
|
||||
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/3 items/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows shuttle error count when errors in log', async () => {
|
||||
getMediaShuttleStatus.mockResolvedValue({
|
||||
running: false, queue_depth: 0, last_transfer: null,
|
||||
recent_errors: ['❌ failed to transfer', '❌ connection reset'], current_stage: null,
|
||||
});
|
||||
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/2 errors/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows shuttle current stage when running', async () => {
|
||||
getMediaShuttleStatus.mockResolvedValue({
|
||||
running: true, queue_depth: 1, last_transfer: '1.2GB', recent_errors: [],
|
||||
current_stage: 'Stage 3: ship',
|
||||
});
|
||||
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Stage 3: ship/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders task output console section', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Task Output Console')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows Scan DJI and Scan Photos action buttons', async () => {
|
||||
render(MediaOps);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Scan DJI')).toBeTruthy();
|
||||
expect(screen.getByText('Scan Photos')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user