mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
`semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are
gone from package.json, the lockfile and node_modules -- 19 MB of dependencies
that nothing had imported since the migration finished.
Four things still referenced them and would have broken on uninstall. None was
caught by a build or by jest:
Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for
components that no longer use it. A jest.mock of a module that does not exist
throws, so the packages could not have come out without this.
`src/components/Download.cy.js` and `cypress/support/component.js` both
imported semantic.min.css. The support file now loads what the app loads --
index.css, the font and token stylesheets, and Mantine's.
The ratchet changes job rather than being deleted. An import of a missing module
fails loudly now, but three quieter mistakes would not: a `className="ui button"`
that styles nothing, a file reaching back through Theme.tsx, and someone
reinstalling a package to "just use one Semantic component". It asserts all
three, plus that package.json names nothing matching /semantic/i.
Two pre-existing problems in the cypress component harness surfaced while
checking the uninstall had not broken it, both fixed because that file had to be
edited anyway:
It imported `react-router-dom`, which is not installed -- this app is on
react-router v7. Dates to 5621d473, so the component suite has been
un-runnable since well before this migration.
Its mount helper supplied no MantineProvider, so every library component threw
-- the same gap test-utils.js had for jest.
The component specs now load and four pass; eleven assertions are still written
against markup from before they stopped running. Repairing those is its own
piece of work, deliberately not bundled in here. The e2e suite is unaffected:
43/43.
461 lines
16 KiB
JavaScript
461 lines
16 KiB
JavaScript
import React from 'react';
|
|
import {screen, waitFor, fireEvent} from '@testing-library/react';
|
|
import {renderWithProviders} from '../../test-utils';
|
|
import {CollectionReorganizeModal} from './CollectionReorganizeModal';
|
|
import * as api from '../../api';
|
|
|
|
// Mock the API functions
|
|
jest.mock('../../api', () => ({
|
|
previewCollectionReorganization: jest.fn(),
|
|
executeCollectionReorganization: jest.fn(),
|
|
getReorganizationStatus: jest.fn(),
|
|
}));
|
|
|
|
|
|
// Mock the FileWorkerStatusContext - default mock returns inactive state
|
|
jest.mock('../../contexts/FileWorkerStatusContext', () => ({
|
|
useReorganizationStatus: () => ({
|
|
isReorganizing: false,
|
|
taskType: null,
|
|
collectionId: null,
|
|
collectionKind: null,
|
|
batchStatus: null,
|
|
workerStatus: null,
|
|
refresh: jest.fn(),
|
|
}),
|
|
useFileWorkerStatus: () => ({
|
|
status: null,
|
|
error: null,
|
|
refresh: jest.fn(),
|
|
setFastPolling: jest.fn(),
|
|
}),
|
|
}));
|
|
|
|
describe('CollectionReorganizeModal', () => {
|
|
const defaultProps = {
|
|
open: true,
|
|
onClose: jest.fn(),
|
|
collectionId: 1,
|
|
collectionName: 'Test Channel',
|
|
onComplete: jest.fn(),
|
|
needsReorganization: true,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('loads preview when opened', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 45,
|
|
total_files: 50,
|
|
current_file_format: '%(title)s.%(ext)s',
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [
|
|
{old_path: 'video.mp4', new_path: '2024-01-15/video.mp4'},
|
|
],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
// Should show loading initially
|
|
expect(screen.getByText(/Analyzing files/i)).toBeInTheDocument();
|
|
|
|
// Wait for preview to load
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/45/)).toBeInTheDocument();
|
|
expect(screen.getByText(/50/)).toBeInTheDocument();
|
|
});
|
|
|
|
expect(api.previewCollectionReorganization).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it('shows sample moves in preview', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
current_file_format: '%(title)s.%(ext)s',
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [
|
|
{old_path: 'old/video1.mp4', new_path: 'new/video1.mp4'},
|
|
{old_path: 'old/video2.mp4', new_path: 'new/video2.mp4'},
|
|
],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('old/video1.mp4')).toBeInTheDocument();
|
|
expect(screen.getByText('new/video1.mp4')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows "No Files Need Reorganization" message when no files need moving', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 0,
|
|
total_files: 50,
|
|
current_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/No Files Need Reorganization/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows warning when collection appears organized but has files to move', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 5,
|
|
total_files: 50,
|
|
current_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [{old_path: 'a.mp4', new_path: 'b.mp4'}],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
|
|
// needsReorganization is false, but there are files to move
|
|
renderWithProviders(
|
|
<CollectionReorganizeModal {...defaultProps} needsReorganization={false} />
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Collection Appears Organized/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('executes reorganization on button click', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-abc123',
|
|
message: 'Reorganization started',
|
|
});
|
|
api.getReorganizationStatus.mockResolvedValue({
|
|
status: 'complete',
|
|
total: 10,
|
|
completed: 10,
|
|
percent: 100,
|
|
});
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
// Wait for preview to load
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
// Click the reorganize button
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
await waitFor(() => {
|
|
expect(api.executeCollectionReorganization).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|
|
|
|
it('shows progress bar during execution', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-abc123',
|
|
message: 'Reorganization started',
|
|
});
|
|
api.getReorganizationStatus.mockResolvedValue({
|
|
status: 'running',
|
|
total: 10,
|
|
completed: 5,
|
|
percent: 50,
|
|
});
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/5 of 10 files processed/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls onComplete when reorganization finishes', async () => {
|
|
const mockOnComplete = jest.fn();
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-abc123',
|
|
message: 'Reorganization started',
|
|
});
|
|
api.getReorganizationStatus.mockResolvedValue({
|
|
status: 'complete',
|
|
total: 10,
|
|
completed: 10,
|
|
percent: 100,
|
|
});
|
|
|
|
renderWithProviders(
|
|
<CollectionReorganizeModal {...defaultProps} onComplete={mockOnComplete} />
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnComplete).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('displays error message on API failure', async () => {
|
|
api.previewCollectionReorganization.mockRejectedValue(new Error('Network error'));
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Network error/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls onClose when cancel button is clicked', async () => {
|
|
const mockOnClose = jest.fn();
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
|
|
renderWithProviders(
|
|
<CollectionReorganizeModal {...defaultProps} onClose={mockOnClose} />
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Cancel/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Cancel/i}));
|
|
|
|
expect(mockOnClose).toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows completion message when reorganization completes', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-abc123',
|
|
message: 'Reorganization started',
|
|
});
|
|
api.getReorganizationStatus.mockResolvedValue({
|
|
status: 'complete',
|
|
total: 10,
|
|
completed: 10,
|
|
percent: 100,
|
|
});
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Reorganization Complete/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('handles execution error gracefully', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockRejectedValue(new Error('Permission denied'));
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Permission denied/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('does not fetch preview when modal is closed', () => {
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} open={false} />);
|
|
|
|
expect(api.previewCollectionReorganization).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('changes button text based on state', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-abc123',
|
|
message: 'Reorganization started',
|
|
});
|
|
api.getReorganizationStatus.mockResolvedValue({
|
|
status: 'complete',
|
|
total: 10,
|
|
completed: 10,
|
|
percent: 100,
|
|
});
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
// Wait for preview to load and button to appear
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
// Should show "Cancel" initially (when not in progress)
|
|
expect(screen.getByRole('button', {name: /Cancel/i})).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
// Should show "Close" after completion
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Close/i})).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('polls for status with real job ID, not with "active"', async () => {
|
|
// This test verifies the bug fix: the component should poll using
|
|
// the real job_id returned from executeCollectionReorganization,
|
|
// not a hardcoded string like 'active'
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-real-job-id-123',
|
|
message: 'Reorganization started',
|
|
});
|
|
api.getReorganizationStatus.mockResolvedValue({
|
|
status: 'running',
|
|
total: 10,
|
|
completed: 3,
|
|
percent: 30,
|
|
});
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
// Wait for status polling to start
|
|
await waitFor(() => {
|
|
expect(api.getReorganizationStatus).toHaveBeenCalled();
|
|
});
|
|
|
|
// CRITICAL: Verify that getReorganizationStatus was called with the real job ID
|
|
// NOT with 'active' which was the bug
|
|
expect(api.getReorganizationStatus).toHaveBeenCalledWith(1, 'reorganize-real-job-id-123');
|
|
expect(api.getReorganizationStatus).not.toHaveBeenCalledWith(1, 'active');
|
|
});
|
|
|
|
it('handles status polling error and retries', async () => {
|
|
const mockPreview = {
|
|
files_needing_move: 10,
|
|
total_files: 20,
|
|
new_file_format: '%(upload_date)s/%(title)s.%(ext)s',
|
|
sample_moves: [],
|
|
};
|
|
|
|
api.previewCollectionReorganization.mockResolvedValue(mockPreview);
|
|
api.executeCollectionReorganization.mockResolvedValue({
|
|
job_id: 'reorganize-abc123',
|
|
message: 'Reorganization started',
|
|
});
|
|
|
|
// First call fails, second call succeeds
|
|
api.getReorganizationStatus
|
|
.mockRejectedValueOnce(new Error('Network error'))
|
|
.mockResolvedValue({
|
|
status: 'complete',
|
|
total: 10,
|
|
completed: 10,
|
|
percent: 100,
|
|
});
|
|
|
|
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
renderWithProviders(<CollectionReorganizeModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', {name: /Reorganize Files/i})).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', {name: /Reorganize Files/i}));
|
|
|
|
// Wait for retry and completion
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Reorganization Complete/i)).toBeInTheDocument();
|
|
}, {timeout: 3000});
|
|
|
|
// Should have retried after initial error
|
|
expect(api.getReorganizationStatus.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|