mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Specs had no shared vocabulary for the app's data or its contexts, so each one rebuilt both. A settings object was written out by hand wherever one was needed, twenty-odd keys at a time, and nothing held those copies to each other or to the API. Contexts were worse: to render a consumer a spec mocked the whole context module, which replaces every export including the providers, so each spec reinvented the pieces it still needed. One supplies a `Media` that always reports desktop -- quietly leaving that component's mobile branch untested -- and one hand-writes a context as a bare `_currentValue` with no provider, working only because useContext reads that React internal. test-fixtures.js is now the single definition of both, shared by jest, the Cypress component specs, and e2e intercept bodies, so the three cannot disagree about what the API returns. Every fixture is a function taking overrides; a shared mutable object lets one test change what the next one sees. The drift this prevents was already real. The theme fixture supplied `i`, `s` and `t` holding hardcoded Semantic greys for months after ThemeProvider stopped supplying them, and ThemeContext still declared all four. So the fixture agreed with the tests, and with nothing else: FileBrowser was reading `inverted` off the context and pasting the resulting `undefined` into a className, which is why the file browser's footer -- near-white #f9fafb, with a `.inverted` dark variant that nothing could select any more -- stayed light in dark, night and amber. A white bar pinned to the bottom of the screen in night mode costs the user exactly the dark adaptation the theme exists to protect. It takes tokens now. test-fixtures.test.js holds each fixture to its context's declared shape in both directions, so a property added to one and forgotten in the other fails a test. Two failed attempts are recorded in test-utils.js because both were instructive. Importing Tags.js and FileWorkerStatusContext.js so the harness could supply those providers broke eight suites: it put both into the module graph of every spec ahead of that spec's own mocks, and components bound to a second instance of a mocked module -- a different jest.fn() than the test had configured, so a hook mocked to return a value returned undefined, in a file no test mentions. Requiring them lazily fixed that and not its mirror image: useContext read a different TagsContext than the harness had required, so the provider was ignored and the context default won. A harness must not change what a spec imports, so it owns only contexts/contexts.js and a spec passes any other context itself. DomainTagging is converted off the `_currentValue` fake as proof, and now asserts the tag name a user sees rather than a test id its own mock invented. The five remaining specs are listed in a ratchet so the count can only fall. 974 jest, 15 browser component, 46 e2e, clean cache-busted build, tsc clean. Fixtures are confirmed absent from the shipped bundle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
73 lines
2.4 KiB
JavaScript
73 lines
2.4 KiB
JavaScript
import React, {createContext, useContext, useState, useEffect, useCallback, useRef} from 'react';
|
|
import {fetchFilesProgress} from '../api';
|
|
|
|
/*
|
|
* Exported so a test can wrap a component in the real provider holding a fixture value. While
|
|
* this was private the only way to render a consumer was `jest.mock` on this whole module,
|
|
* which replaces the provider and the hooks together and has to be rewritten in every spec
|
|
* that needs it.
|
|
*/
|
|
export const FileWorkerStatusContext = createContext(null);
|
|
|
|
export function FileWorkerStatusProvider({children, pollInterval = 3000}) {
|
|
const [status, setStatus] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const requestedInterval = useRef(pollInterval);
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const data = await fetchFilesProgress();
|
|
setStatus(data);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err);
|
|
}
|
|
}, []);
|
|
|
|
// Allow components to request faster polling temporarily
|
|
const setFastPolling = useCallback((fast) => {
|
|
requestedInterval.current = fast ? 500 : pollInterval;
|
|
}, [pollInterval]);
|
|
|
|
useEffect(() => {
|
|
let timeoutId;
|
|
const poll = async () => {
|
|
await refresh();
|
|
timeoutId = setTimeout(poll, requestedInterval.current);
|
|
};
|
|
poll();
|
|
return () => clearTimeout(timeoutId);
|
|
}, [refresh]);
|
|
|
|
return (
|
|
<FileWorkerStatusContext.Provider value={{status, error, refresh, setFastPolling}}>
|
|
{children}
|
|
</FileWorkerStatusContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useFileWorkerStatus() {
|
|
const context = useContext(FileWorkerStatusContext);
|
|
if (!context) {
|
|
throw new Error('useFileWorkerStatus must be used within FileWorkerStatusProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
// Convenience hook for reorganization-specific status
|
|
export function useReorganizationStatus() {
|
|
const {status: progress, refresh} = useFileWorkerStatus();
|
|
|
|
const isReorganizing = progress?.status === 'reorganizing' ||
|
|
progress?.status === 'batch_reorganizing';
|
|
|
|
return {
|
|
isReorganizing,
|
|
taskType: progress?.task_type,
|
|
collectionId: progress?.collection_id,
|
|
collectionKind: progress?.collection_kind,
|
|
batchStatus: progress?.batch_status,
|
|
workerStatus: progress,
|
|
refresh,
|
|
};
|
|
}
|