+export function Statistic({value, label, color, className, style}: StatisticProps) {
+ return
{value}
@@ -89,6 +91,7 @@ export function StatisticGroup({children}: {children: React.ReactNode}) {
}}>
{items.map((child, index) =>
{
expect(unmapped.map(c => `${c.text}: ${c.icon}`)).toEqual([]);
});
+ it('maps every name the mimetype and file-suffix helpers can return', () => {
+ // These two build icon names from arbitrary files in the user's library, so an
+ // unmapped one is not hypothetical — it is a console error and a fallback glyph
+ // the moment somebody opens a folder containing that file type.
+ const common = fs.readFileSync(path.join(__dirname, '..', 'Common.js'), 'utf8');
+ const section = common.slice(
+ common.indexOf('export function mimetypeIconName'),
+ common.indexOf('export function FileIcon'));
+ const returned = [...new Set([...section.matchAll(/return '([^']+)'/g)].map(m => m[1]))];
+
+ expect(returned.length).toBeGreaterThan(10);
+ expect(returned.filter(name => !resolveIconName(name))).toEqual([]);
+ });
+
+ it('points every mapped name at a Tabler component that exists', () => {
+ // A typo in the map resolves to undefined and renders the fallback glyph, which
+ // looks like a design choice rather than a bug.
+ const source = fs.readFileSync(path.join(__dirname, 'Icon.tsx'), 'utf8');
+ const names = [...source.matchAll(/^ {4}'([^']+)': '([^']+)',$/gm)].map(m => m[1]);
+
+ expect(names.length).toBeGreaterThan(100);
+ expect(names.filter(name => !resolveIconName(name))).toEqual([]);
+ });
+
it('reports an unknown name instead of failing silently', () => {
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
@@ -116,6 +142,16 @@ describe('Button', () => {
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
});
+ it('translates Semantic size names, which Mantine would otherwise ignore', () => {
+ // Unmigrated call sites still pass size='tiny'. Mantine drops a size it does not
+ // recognise without warning, so the button quietly renders at the default size.
+ renderUI(<>Old New >);
+
+ expect(screen.getByRole('button', {name: 'Old'})).toHaveAttribute('data-size', 'xs');
+ // A Mantine size still passes through untouched.
+ expect(screen.getByRole('button', {name: 'New'})).toHaveAttribute('data-size', 'sm');
+ });
+
it('gives icon-only buttons an accessible name', () => {
renderUI( );
diff --git a/app/src/hooks/customHooks.test.js b/app/src/hooks/customHooks.test.js
index c762081b..5d925cc3 100644
--- a/app/src/hooks/customHooks.test.js
+++ b/app/src/hooks/customHooks.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import {act, renderHook} from '@testing-library/react';
-import {render, screen} from '@testing-library/react';
+import {render, screen} from '../test-utils';
import {usePages, useDriveTemperature, useDriveHealth} from './customHooks';
import {QueryContext, StatusContext} from '../contexts/contexts';
import {Paginator} from '../components/Common';
diff --git a/app/src/hooks/useForm.test.js b/app/src/hooks/useForm.test.js
index 8433ed1a..601f4bee 100644
--- a/app/src/hooks/useForm.test.js
+++ b/app/src/hooks/useForm.test.js
@@ -1044,7 +1044,7 @@ describe('Form Components', () => {
/>
);
- expect(screen.getByRole('checkbox')).toBeInTheDocument();
+ expect(screen.getByRole('switch')).toBeInTheDocument();
});
});
diff --git a/app/src/hooks/useKeyboardShortcuts.test.js b/app/src/hooks/useKeyboardShortcuts.test.js
index 51c1c263..d358e0ec 100644
--- a/app/src/hooks/useKeyboardShortcuts.test.js
+++ b/app/src/hooks/useKeyboardShortcuts.test.js
@@ -1,6 +1,5 @@
import React from 'react';
-import {render, screen, fireEvent, act} from '@testing-library/react';
-import {MemoryRouter} from 'react-router';
+import {render, screen, fireEvent, act} from '../test-utils';
import {KeyboardShortcutsContext} from '../contexts/KeyboardShortcutsContext';
import {KeyboardShortcutsProvider, SHORTCUTS} from '../components/KeyboardShortcutsProvider';
import {ThemeContext} from '../contexts/contexts';
@@ -43,17 +42,15 @@ function TestWrapper({children}) {
};
return (
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+ {children}
+
+
+
+
);
}
diff --git a/app/src/semantic-baseline.json b/app/src/semantic-baseline.json
index 5c858fca..fb5ae591 100644
--- a/app/src/semantic-baseline.json
+++ b/app/src/semantic-baseline.json
@@ -4,14 +4,9 @@
"src/Events.js",
"src/Tags.js",
"src/api.js",
- "src/components/AddToPlaylist.js",
- "src/components/Apps.js",
"src/components/Archive.js",
- "src/components/ArchivePreview.js",
"src/components/BulkTagModal.js",
- "src/components/CbzViewer.js",
"src/components/Channels.js",
- "src/components/Common.js",
"src/components/DatesSelector.js",
"src/components/Docs.js",
"src/components/Download.js",
@@ -24,10 +19,8 @@
"src/components/MapSearchView.js",
"src/components/MapViewer.js",
"src/components/Nav.js",
- "src/components/Placeholder.js",
"src/components/Playlists.js",
"src/components/Search.js",
- "src/components/TaggedDeleteConfirmModal.js",
"src/components/Theme.tsx",
"src/components/Upload.js",
"src/components/VideoPlayer.js",
@@ -46,19 +39,13 @@
"src/components/calculators/RationCalculator.js",
"src/components/calculators/WaterCalculator.js",
"src/components/collections/BatchReorganizeModal.js",
- "src/components/collections/CollectionEditForm.js",
"src/components/collections/CollectionReorganizeModal.js",
"src/components/collections/CollectionTable.js",
- "src/components/collections/CollectionTagModal.js",
"src/components/collections/ConflictResolutionModal.js",
- "src/components/inventory/CatalogEditor.js",
"src/components/inventory/FieldCell.js",
"src/components/inventory/FieldSchemaEditor.js",
- "src/components/inventory/InventoryExportPanel.js",
"src/components/inventory/InventoryImportModal.js",
- "src/components/inventory/InventoryItemsMobile.js",
"src/components/inventory/InventoryRoute.js",
- "src/components/inventory/InventorySummary.js",
"src/components/inventory/InventoryTable.js",
"src/hooks/customHooks.js",
"src/hooks/useForm.js"
diff --git a/app/src/semantic-baseline.test.js b/app/src/semantic-baseline.test.js
index 5d9fd5cc..7d21c93a 100644
--- a/app/src/semantic-baseline.test.js
+++ b/app/src/semantic-baseline.test.js
@@ -48,6 +48,23 @@ describe('Semantic UI removal', () => {
expect(stale).toEqual([]);
});
+ it('declares every migrated file that still leans on Semantic CSS classes', () => {
+ /*
+ * A file can stop importing semantic-ui-react and still depend on its stylesheet
+ * by writing `className="ui stackable grid"`. The import ratchet above cannot see
+ * that, so when Phase 4 deletes semantic.min.css the layout would silently
+ * collapse. Anything on this list is a deliberate bridge that must be revisited
+ * then; anything NOT on it is an accident.
+ */
+ const allowed = ['src/components/collections/CollectionEditForm.js'];
+
+ const bridges = sourceFiles()
+ .filter(file => !baseline.includes(file))
+ .filter(file => /className=["']ui /.test(fs.readFileSync(path.join(SRC, '..', file), 'utf8')));
+
+ expect(bridges.sort()).toEqual(allowed.sort());
+ });
+
it('reports how much of the migration is left', () => {
// Not an assertion so much as a progress readout in the test output.
const remaining = importers().length;
diff --git a/app/src/setupTests.js b/app/src/setupTests.js
index 24a37e2a..5f2ae4c6 100644
--- a/app/src/setupTests.js
+++ b/app/src/setupTests.js
@@ -52,6 +52,21 @@ global.IntersectionObserver = class IntersectionObserver {
}
};
+// Mock ResizeObserver (used by Mantine's ScrollArea, e.g. inside Select/Combobox dropdowns).
+global.ResizeObserver = class ResizeObserver {
+ constructor() {
+ }
+
+ disconnect() {
+ }
+
+ observe() {
+ }
+
+ unobserve() {
+ }
+};
+
// Suppress console errors during tests (optional - uncomment if needed)
// const originalError = console.error;
// beforeAll(() => {