wrolpi/app/src/components/DomainEditPage.test.js
Roland e00f0439da
ui: the last of Semantic UI, in name and in dead CSS
The library has been uninstalled for some time, but the codebase still read as
though it were there: identifiers named after it, rules written against its
class names, and about 180 comments explaining a rule in terms of a thing that
is no longer installed.

Renamed, keeping the vocabulary the app actually uses: semanticUIColorMap ->
navColorHexMap, semanticColorNames -> paletteColorNames, semanticNames ->
iconNameAliases, semanticSizes -> sizeAliases, and SemanticUIColors ->
NavColors in wrolpi/schema.py, which is what validates nav_color on the way in.
ColorToSemanticHexColor had no callers at all and is gone.

The baseline ratchet has done its job -- the list reached zero, so the JSON is
deleted and the guard keeps only the checks with teeth: no package, no import,
no `className="ui "`, no toast library.  Its Theme.tsx check went with the
wrappers it was watching for.  It also asserts its own walk found files, so a
clean result cannot mean it scanned nothing.

Deleted from App.css: every rule requiring `.ui`, `.inverted`, `.segment` or
`.modal`, none of which any component has set since the stylesheet stopped
loading, plus the form skeletons, ui-alerts and progress-text rules that
nothing renders.  Two of them were doing harm rather than nothing: the drag
tints had a dark-theme variant under `.inverted` that stopped applying, leaving
15% of a fixed blue over a dark row, and now mix a token instead.

Two selectors naming markup that no longer exists were live bugs.  The file
browser's filter had `.ui.input.file-browser-filter`, so it rendered at the
browser default width on a bar laid out to give it 34em -- now qualified by its
container, because `.wrolpi-action-input` sets `width: 100%` at the same
specificity and is loaded later.  And SearchBox takes named props rather than a
rest spread, so the `id` DestinationForm hands it was dropped in silence and
the label it renders pointed at nothing; the download forms' Destination field
has had no associated label since the migration.

"Semantic roles" survives in four places, where it means what it says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:05:54 -06:00

474 lines
16 KiB
JavaScript

import React from 'react';
import {createMockDomain, createTestForm, render, renderInDarkMode, screen, waitFor} from '../test-utils';
import userEvent from '@testing-library/user-event';
import {DomainEditPage} from './Archive';
// Mock useParams to return domain ID
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
useParams: () => ({domainId: '1'}),
useNavigate: () => jest.fn(),
}));
// Mock the useDomain hook
const mockUseDomain = jest.fn();
jest.mock('../hooks/customHooks', () => ({
...jest.requireActual('../hooks/customHooks'),
useDomain: (...args) => mockUseDomain(...args),
}));
// Mock useTitle
jest.mock('./Common', () => ({
...jest.requireActual('./Common'),
useTitle: jest.fn(),
}));
// Mock CollectionEditForm
jest.mock('./collections/CollectionEditForm', () => ({
CollectionEditForm: ({form, fields, title, actionButtons}) => (
<div data-testid="collection-edit-form">
{title && <h1>{title}</h1>}
{form?.loading && <div data-testid="loading-indicator">Loading...</div>}
{form?.formData && <div data-testid="collection-data">Collection data loaded</div>}
{fields && <div data-testid="fields-present">Fields configured</div>}
{actionButtons && <div data-testid="action-buttons">{actionButtons}</div>}
</div>
),
}));
// Mock CollectionTagModal
jest.mock('./collections/CollectionTagModal', () => ({
CollectionTagModal: ({open, onClose, currentTagName, originalDirectory, getTagInfo, onSave, collectionName}) => {
if (!open) return null;
return (
<div data-testid="collection-tag-modal">
<div data-testid="modal-header">{currentTagName ? 'Modify Tag' : 'Add Tag'}</div>
<input type="text" data-testid="directory-input" defaultValue={originalDirectory || ''}/>
<button data-testid="cancel-button" onClick={onClose}>Cancel</button>
<button data-testid="save-button"
onClick={() => onSave && onSave(currentTagName, originalDirectory)}>Save
</button>
</div>
);
},
}));
// Mock CollectionReorganizeModal
jest.mock('./collections/CollectionReorganizeModal', () => ({
CollectionReorganizeModal: ({open, onClose, collectionId, collectionName, onComplete}) => {
if (!open) return null;
return (
<div data-testid="collection-reorganize-modal">
<div data-testid="modal-header">Reorganize Files: {collectionName}</div>
<button data-testid="close-button" onClick={onClose}>Close</button>
</div>
);
},
}));
// Mock API functions
const mockGetCollectionTagInfo = jest.fn();
jest.mock('../api', () => ({
...jest.requireActual('../api'),
getCollectionTagInfo: (...args) => mockGetCollectionTagInfo(...args),
tagDomain: jest.fn(),
}));
describe('DomainEditPage', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Loading States', () => {
it('handles loading state while fetching domain', async () => {
// Start with loading state (no domain yet)
const form = createTestForm({}, {
overrides: {ready: false, loading: false}
});
mockUseDomain.mockReturnValue({
domain: null,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// Should show the Loading component with text
expect(screen.getByText(/loading domain/i)).toBeInTheDocument();
// Form should not be visible during initial load
expect(screen.queryByTestId('collection-edit-form')).not.toBeInTheDocument();
});
it('shows form when domain is loaded', () => {
const mockDomain = createMockDomain({
domain: 'test.com',
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// Should NOT show loading message
expect(screen.queryByText(/loading domain/i)).not.toBeInTheDocument();
// Should show domain name (may appear multiple times in header and form)
expect(screen.getAllByText(/test\.com/i).length).toBeGreaterThan(0);
});
it('passes loading state to form during submission', () => {
const mockDomain = createMockDomain({
domain: 'example.com',
});
// Domain is loaded but form is submitting
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: true}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// Should show loading indicator in form (from mocked component)
// Multiple indicators due to Fresnel rendering for mobile and tablet+
expect(screen.getAllByTestId('loading-indicator').length).toBeGreaterThan(0);
// Should also show the domain name
expect(screen.getAllByText(/example\.com/i).length).toBeGreaterThan(0);
});
});
describe('Error States', () => {
it('shows error message when domain fetch fails', () => {
const form = createTestForm({}, {
overrides: {
ready: false,
loading: false,
error: 'Domain collection with ID 999 not found',
}
});
mockUseDomain.mockReturnValue({
domain: null,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
expect(screen.getByText(/domain not found/i)).toBeInTheDocument();
expect(screen.getByText(/Domain collection with ID 999 not found/i)).toBeInTheDocument();
expect(screen.queryByText(/loading domain/i)).not.toBeInTheDocument();
expect(screen.queryByTestId('collection-edit-form')).not.toBeInTheDocument();
});
it('shows loader when form is not ready and there is no error', () => {
const form = createTestForm({}, {
overrides: {ready: false, loading: false, error: null}
});
mockUseDomain.mockReturnValue({
domain: null,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
expect(screen.getByText(/loading domain/i)).toBeInTheDocument();
expect(screen.queryByTestId('collection-edit-form')).not.toBeInTheDocument();
});
it('shows form when ready even if there was a submission error', () => {
// Form is ready (domain loaded) but submission may have failed
const mockDomain = createMockDomain({
domain: 'example.com',
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false, error: 'Update failed'}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// Should show domain name (form is rendered)
// Multiple occurrences due to Fresnel rendering for mobile and tablet+
expect(screen.getAllByText(/example\.com/i).length).toBeGreaterThan(0);
// Should NOT show the initial loading screen
expect(screen.queryByText(/loading domain/i)).not.toBeInTheDocument();
});
});
describe('Page Title', () => {
it('sets page title with domain name', () => {
const {useTitle} = require('./Common');
const mockDomain = createMockDomain({
domain: 'example.com',
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// useTitle should be called with domain name
expect(useTitle).toHaveBeenCalledWith('Edit Domain: example.com');
});
it('sets page title with placeholder while loading', () => {
const {useTitle} = require('./Common');
const form = createTestForm({}, {
overrides: {ready: false, loading: false}
});
mockUseDomain.mockReturnValue({
domain: null,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// useTitle should be called with placeholder
expect(useTitle).toHaveBeenCalledWith('Edit Domain: ...');
});
});
describe('Theme Integration', () => {
it('passes theme context to CollectionEditForm in dark mode', () => {
const mockDomain = createMockDomain({
domain: 'example.com',
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
// Render in dark mode
renderInDarkMode(<DomainEditPage/>);
// CollectionEditForm should be rendered
expect(screen.getByTestId('collection-edit-form')).toBeInTheDocument();
// The title should include the domain name
expect(screen.getAllByText(/example\.com/i).length).toBeGreaterThan(0);
});
it('renders properly in light mode', () => {
const mockDomain = createMockDomain({
domain: 'test.com',
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
// Default render (light mode)
render(<DomainEditPage/>);
// CollectionEditForm should be rendered
expect(screen.getByTestId('collection-edit-form')).toBeInTheDocument();
// The title should include the domain name
expect(screen.getAllByText(/test\.com/i).length).toBeGreaterThan(0);
});
});
describe('Tag Modal and Directory Suggestions', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetCollectionTagInfo.mockClear();
});
it('suggests directory when tag is selected', async () => {
const mockDomain = createMockDomain({
domain: 'example.com',
id: 1,
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
// Mock successful tag info response
mockGetCollectionTagInfo.mockResolvedValue({
suggested_directory: 'archive/WROL/example.com',
conflict: false,
conflict_message: null,
});
render(<DomainEditPage/>);
// Click the Tag button to open modal
const tagButton = screen.getByText('Tag');
await userEvent.click(tagButton);
// Wait for modal to open
await waitFor(() => {
expect(screen.getByText(/Modify Tag|Add Tag/i)).toBeInTheDocument();
});
// Simulate selecting a tag (this would normally be done by TagsSelector)
// Since TagsSelector is a real component, we need to wait for the API call
// We'll verify the API was called when a tag would be selected
// This test validates the structure is in place
});
it('displays conflict warning in modal when directory conflict exists', async () => {
const mockDomain = createMockDomain({
domain: 'example.com',
id: 1,
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
// Mock tag info response with conflict
mockGetCollectionTagInfo.mockResolvedValue({
suggested_directory: 'archive/WROL/example.com',
conflict: true,
conflict_message: "A domain collection 'other.com' already uses this directory. Choose a different tag or directory.",
});
render(<DomainEditPage/>);
// Open the modal
const tagButton = screen.getByText('Tag');
await userEvent.click(tagButton);
// The modal should be present
await waitFor(() => {
expect(screen.getByTestId('collection-tag-modal')).toBeInTheDocument();
});
// Verify the modal structure includes the necessary inputs
expect(screen.getByTestId('directory-input')).toBeInTheDocument();
});
it('clears conflict message when tag is changed', async () => {
const mockDomain = createMockDomain({
domain: 'example.com',
id: 1,
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
render(<DomainEditPage/>);
// Open modal
const tagButton = screen.getByText('Tag');
await userEvent.click(tagButton);
await waitFor(() => {
expect(screen.getByText(/Modify Tag|Add Tag/i)).toBeInTheDocument();
});
// Modal should be open and ready for tag selection
// The actual tag selection and conflict clearing would be tested in integration tests
// This validates the structure is present
});
it('populates directory input with suggested directory', async () => {
const mockDomain = createMockDomain({
domain: 'example.com',
id: 1,
directory: 'archive/example.com',
});
const form = createTestForm(mockDomain, {
overrides: {ready: true, loading: false}
});
mockUseDomain.mockReturnValue({
domain: mockDomain,
form,
fetchDomain: jest.fn(),
});
mockGetCollectionTagInfo.mockResolvedValue({
suggested_directory: 'archive/WROL/example.com',
conflict: false,
conflict_message: null,
});
render(<DomainEditPage/>);
// Open modal
const tagButton = screen.getByText('Tag');
await userEvent.click(tagButton);
// Wait for modal to open
await waitFor(() => {
expect(screen.getByTestId('collection-tag-modal')).toBeInTheDocument();
});
// Verify directory input field exists with original directory value
const directoryInput = screen.getByTestId('directory-input');
expect(directoryInput).toBeInTheDocument();
expect(directoryInput).toHaveValue('archive/example.com');
});
});
});