mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Phase 2, part 3. Baseline 63 -> 50.
Common.js is the one that mattered: 51 files import it, so its Semantic usage
was propping up the rest of the migration. Everything in it now comes from the
library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the
toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer,
CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons.
Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist,
TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory
components.
New in the library, all in the gallery and tested:
SearchBox Semantic's `<Search category>` with its grouped results shape
kept, because four call sites already build it. A real
combobox rather than a div that listens for clicks.
Pagination Semantic's prop names; onPageChange gets the page number.
TabBar the strip only -- routing stays with the caller.
IconStack Semantic's IconGroup, named once for assistive technology.
Table.HeaderCell sorted/onSort -- a real button carrying aria-sort.
Button/IconButton translate Semantic's size names.
That last one is defensive: unmigrated callers still pass size='tiny', and
Mantine drops a size it does not recognise without a word, so the button
quietly renders at the wrong size. Mapping here keeps the mixed state honest.
Three real bugs found and fixed on the way, none of them cosmetic:
* `fileSuffixIconName` returned fourteen icon names the map did not have, so
opening a folder with a PDF, a Word document or an archive in it logged an
error and drew a fallback glyph. All fourteen mapped, with a test that
walks the mapper's own return values, and another asserting every mapped
name resolves to a Tabler component that exists.
* `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on
the dashboard the load averages ran together into "1 MIN. LOAD5 MIN.
LOAD15 MIN. LOAD". It carries its own spacing now.
* Semantic's inverted stylesheet colors every descendant of an inverted
segment, so Toggle labels inherited #dddddd -- white pixels in night mode,
on the Settings page, which is exactly where someone adjusts their WROLPi
in the dark. Restated from tokens.
The baseline ratchet gained a third assertion: a migrated file may still lean
on Semantic's CSS classes without importing the library, which the import check
cannot see and which would collapse silently when Phase 4 deletes the
stylesheet. There is one such bridge today and it is now declared.
290 lines
9.5 KiB
JavaScript
290 lines
9.5 KiB
JavaScript
import React from 'react';
|
|
import {render, screen, fireEvent, act} from '../test-utils';
|
|
import {KeyboardShortcutsContext} from '../contexts/KeyboardShortcutsContext';
|
|
import {KeyboardShortcutsProvider, SHORTCUTS} from '../components/KeyboardShortcutsProvider';
|
|
import {ThemeContext} from '../contexts/contexts';
|
|
import {TagsContext} from '../Tags';
|
|
import {QueryContext} from '../contexts/contexts';
|
|
|
|
// Mock useSearchSuggestions to avoid complex setup
|
|
jest.mock('../components/Search', () => ({
|
|
...jest.requireActual('../components/Search'),
|
|
useSearchSuggestions: () => ({
|
|
suggestionsResults: {},
|
|
handleResultSelect: jest.fn(),
|
|
resultRenderer: () => null,
|
|
loading: false,
|
|
searchStr: '',
|
|
setSearchStr: jest.fn(),
|
|
}),
|
|
}));
|
|
|
|
// Test wrapper with all required providers
|
|
function TestWrapper({children}) {
|
|
const mockThemeContext = {
|
|
i: {},
|
|
s: {},
|
|
t: {},
|
|
theme: 'light',
|
|
inverted: '',
|
|
};
|
|
|
|
const mockTagsContext = {
|
|
SingleTag: () => null,
|
|
fuzzyMatchTagsByName: () => [],
|
|
};
|
|
|
|
const mockQueryContext = {
|
|
searchParams: new URLSearchParams(),
|
|
setSearchParams: jest.fn(),
|
|
updateQuery: jest.fn(),
|
|
getLocationStr: jest.fn((params, base) => base || '/'),
|
|
};
|
|
|
|
return (
|
|
<ThemeContext.Provider value={mockThemeContext}>
|
|
<QueryContext.Provider value={mockQueryContext}>
|
|
<TagsContext.Provider value={mockTagsContext}>
|
|
<KeyboardShortcutsProvider>
|
|
{children}
|
|
</KeyboardShortcutsProvider>
|
|
</TagsContext.Provider>
|
|
</QueryContext.Provider>
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
// Simple component that uses the context
|
|
function TestComponent() {
|
|
const {
|
|
searchModalOpen,
|
|
helpModalOpen,
|
|
openSearchModal,
|
|
closeSearchModal,
|
|
openHelpModal,
|
|
closeHelpModal,
|
|
closeAllModals,
|
|
} = React.useContext(KeyboardShortcutsContext);
|
|
|
|
return (
|
|
<div>
|
|
<div data-testid="search-modal-status">{searchModalOpen ? 'open' : 'closed'}</div>
|
|
<div data-testid="help-modal-status">{helpModalOpen ? 'open' : 'closed'}</div>
|
|
<button data-testid="open-search" onClick={openSearchModal}>Open Search</button>
|
|
<button data-testid="close-search" onClick={closeSearchModal}>Close Search</button>
|
|
<button data-testid="open-help" onClick={openHelpModal}>Open Help</button>
|
|
<button data-testid="close-help" onClick={closeHelpModal}>Close Help</button>
|
|
<button data-testid="close-all" onClick={closeAllModals}>Close All</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe('KeyboardShortcutsContext', () => {
|
|
test('provides default values (modals closed)', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('closed');
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('openSearchModal opens search modal', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-search'));
|
|
});
|
|
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('open');
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('closeSearchModal closes search modal', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open first
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-search'));
|
|
});
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('open');
|
|
|
|
// Close
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('close-search'));
|
|
});
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('openHelpModal opens help modal', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-help'));
|
|
});
|
|
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('open');
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('closeHelpModal closes help modal', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open first
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-help'));
|
|
});
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('open');
|
|
|
|
// Close
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('close-help'));
|
|
});
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('closeAllModals closes both modals', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open search
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-search'));
|
|
});
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('open');
|
|
|
|
// Close all
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('close-all'));
|
|
});
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('closed');
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('opening search modal closes help modal', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open help first
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-help'));
|
|
});
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('open');
|
|
|
|
// Open search should close help
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-search'));
|
|
});
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('open');
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
|
|
test('opening help modal closes search modal', () => {
|
|
render(
|
|
<TestWrapper>
|
|
<TestComponent/>
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open search first
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-search'));
|
|
});
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('open');
|
|
|
|
// Open help should close search
|
|
act(() => {
|
|
fireEvent.click(screen.getByTestId('open-help'));
|
|
});
|
|
expect(screen.getByTestId('help-modal-status')).toHaveTextContent('open');
|
|
expect(screen.getByTestId('search-modal-status')).toHaveTextContent('closed');
|
|
});
|
|
});
|
|
|
|
describe('SHORTCUTS constant', () => {
|
|
test('is defined and is an array', () => {
|
|
expect(SHORTCUTS).toBeDefined();
|
|
expect(Array.isArray(SHORTCUTS)).toBe(true);
|
|
expect(SHORTCUTS.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('contains search shortcuts', () => {
|
|
const shortcutKeys = SHORTCUTS.map(s => s.keys);
|
|
expect(shortcutKeys).toContain('meta+k, ctrl+k');
|
|
expect(shortcutKeys).toContain('/');
|
|
});
|
|
|
|
test('contains help shortcut', () => {
|
|
const shortcutKeys = SHORTCUTS.map(s => s.keys);
|
|
expect(shortcutKeys).toContain('shift+/');
|
|
});
|
|
|
|
test('contains escape shortcut', () => {
|
|
const shortcutKeys = SHORTCUTS.map(s => s.keys);
|
|
expect(shortcutKeys).toContain('escape');
|
|
});
|
|
|
|
test('contains navigation shortcuts', () => {
|
|
const shortcutKeys = SHORTCUTS.map(s => s.keys);
|
|
expect(shortcutKeys).toContain('g h');
|
|
expect(shortcutKeys).toContain('g v');
|
|
expect(shortcutKeys).toContain('g a');
|
|
expect(shortcutKeys).toContain('g m');
|
|
expect(shortcutKeys).toContain('g z');
|
|
expect(shortcutKeys).toContain('g f');
|
|
expect(shortcutKeys).toContain('g s');
|
|
expect(shortcutKeys).toContain('g i');
|
|
});
|
|
|
|
test('all shortcuts have required properties', () => {
|
|
SHORTCUTS.forEach(shortcut => {
|
|
expect(shortcut).toHaveProperty('keys');
|
|
expect(shortcut).toHaveProperty('description');
|
|
expect(shortcut).toHaveProperty('category');
|
|
expect(typeof shortcut.keys).toBe('string');
|
|
expect(typeof shortcut.description).toBe('string');
|
|
expect(typeof shortcut.category).toBe('string');
|
|
});
|
|
});
|
|
|
|
test('navigation shortcuts have path property', () => {
|
|
const navShortcuts = SHORTCUTS.filter(s => s.category === 'Navigation');
|
|
expect(navShortcuts.length).toBeGreaterThan(0);
|
|
navShortcuts.forEach(shortcut => {
|
|
expect(shortcut).toHaveProperty('path');
|
|
expect(shortcut.path).toMatch(/^\//);
|
|
});
|
|
});
|
|
|
|
test('shortcuts are grouped by category', () => {
|
|
const categories = [...new Set(SHORTCUTS.map(s => s.category))];
|
|
expect(categories).toContain('Search');
|
|
expect(categories).toContain('Navigation');
|
|
expect(categories).toContain('General');
|
|
expect(categories).toContain('Help');
|
|
});
|
|
});
|