mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix: the checks the layout and weight fixes broke: a quote in a test name, state set inside effects, the misused-promise baseline, and tests that render the converter before settings load
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"src": {
|
||||
"@typescript-eslint/no-non-null-assertion": 375,
|
||||
"@typescript-eslint/no-misused-promises": 845,
|
||||
"@typescript-eslint/no-misused-promises": 843,
|
||||
"no-console": 88,
|
||||
"complexity": 218,
|
||||
"@typescript-eslint/no-explicit-any": 144,
|
||||
|
||||
@@ -77,11 +77,13 @@ describe('CollectionsWidget', () => {
|
||||
expect(await screen.findByText('No saved places yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-COLWIDGET-008: nothing renders while the fetch is still in flight', async () => {
|
||||
render(<CollectionsWidget onOpen={() => {}} />);
|
||||
it('FE-COMP-COLWIDGET-008: only a placeholder shows while the fetch is still in flight', async () => {
|
||||
const { container } = render(<CollectionsWidget onOpen={() => {}} />);
|
||||
|
||||
expect(screen.queryByText('No saved places yet')).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByText('No saved places yet')).toBeInTheDocument());
|
||||
// Nothing remembered yet: the empty hint's shape, hidden from assistive tech, holds the place.
|
||||
expect(container.querySelector('.col-empty.trek-skeleton')).toHaveAttribute('aria-hidden', 'true');
|
||||
await waitFor(() => expect(container.querySelector('.trek-skeleton')).toBeNull());
|
||||
expect(screen.getByText('No saved places yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-COLWIDGET-009: #3009 holds the badges it showed last time while the lists load, then remembers the new count', async () => {
|
||||
|
||||
@@ -147,13 +147,8 @@ describe('useCurrencyConverter', () => {
|
||||
});
|
||||
|
||||
it('FE-COMP-FXHOOK-010: #3009 never prices a new pair with the previous base and says it is still loading', async () => {
|
||||
let resolveJpy: (body: unknown) => void = () => {};
|
||||
fetchMock.mockImplementation((url: string) =>
|
||||
url.endsWith('base=JPY')
|
||||
? new Promise((resolve) => {
|
||||
resolveJpy = (body) => resolve({ json: () => Promise.resolve(body) } as Response);
|
||||
})
|
||||
: respond([{ quote: 'USD', rate: 1.1 }])
|
||||
respond(url.endsWith('base=JPY') ? [{ quote: 'EUR', rate: 0.0056 }] : [{ quote: 'USD', rate: 1.1 }])
|
||||
);
|
||||
const { result } = renderHook(() => useCurrencyConverter());
|
||||
await waitFor(() => expect(result.current.rate).toBe(1.1));
|
||||
@@ -163,11 +158,6 @@ describe('useCurrencyConverter', () => {
|
||||
expect(result.current.rate).toBeNull();
|
||||
expect(result.current.ratePending).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
resolveJpy([{ quote: 'EUR', rate: 0.0056 }]);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
await waitFor(() => expect(result.current.rate).toBe(0.0056));
|
||||
expect(result.current.ratePending).toBe(false);
|
||||
});
|
||||
|
||||
@@ -27,14 +27,14 @@ export function useCurrencyConverter() {
|
||||
// the new base's rates arrive, and the old map priced the new pair wrongly
|
||||
// (EUR's self-rate of 1 showed as "1 JPY = 1.0000 EUR", #3009).
|
||||
const [rates, setRates] = useState<{ base: string; map: Record<string, number> } | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
// The base whose rates could not be had, so a failure belongs to its pair only.
|
||||
const [failedBase, setFailedBase] = useState<string | null>(null);
|
||||
const inFlight = useRef<AbortController | null>(null);
|
||||
|
||||
const fetchRates = useCallback(() => {
|
||||
// Not before the user's pair is known: the defaults' rates were fetched and
|
||||
// shown first, then replaced by the saved pair (#3009).
|
||||
if (!isLoaded) return;
|
||||
setFailed(false);
|
||||
inFlight.current?.abort();
|
||||
const controller = new AbortController();
|
||||
inFlight.current = controller;
|
||||
@@ -45,19 +45,20 @@ export function useCurrencyConverter() {
|
||||
if (signal.aborted) return;
|
||||
if (!Array.isArray(d)) {
|
||||
setRates(null);
|
||||
setFailed(true);
|
||||
setFailedBase(from);
|
||||
return;
|
||||
}
|
||||
// Frankfurter omits the base's own self-rate; seed it so `from` stays selectable.
|
||||
const map: Record<string, number> = { [from]: 1 };
|
||||
for (const r of d) map[r.quote] = r.rate;
|
||||
setFailedBase(null);
|
||||
setRates({ base: from, map });
|
||||
})
|
||||
.catch(() => {
|
||||
// An abort is not a failure: it means nobody is waiting for the answer.
|
||||
if (signal.aborted) return;
|
||||
setRates(null);
|
||||
setFailed(true);
|
||||
setFailedBase(from);
|
||||
});
|
||||
}, [from, isLoaded]);
|
||||
|
||||
@@ -95,7 +96,7 @@ export function useCurrencyConverter() {
|
||||
// The pair is not known yet, or its rates are still on their way: neither is
|
||||
// "rate unavailable", and neither may show a rate.
|
||||
const pairPending = !isLoaded;
|
||||
const ratePending = isLoaded && !current && !failed;
|
||||
const ratePending = isLoaded && !current && failedBase !== from;
|
||||
const converted = rate != null ? (Number.parseFloat(amount.replace(',', '.')) || 0) * rate : null;
|
||||
const swap = () => {
|
||||
setFrom(to);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, type CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
import { NumericInput } from '../shared/NumericInput';
|
||||
|
||||
interface Props {
|
||||
@@ -16,21 +16,24 @@ interface Props {
|
||||
*/
|
||||
export function WeightInput({ value, onSave, readOnly, width }: Props) {
|
||||
const stored = value == null ? '' : String(value);
|
||||
const [draft, setDraft] = useState(stored);
|
||||
const editing = useRef(false);
|
||||
// A change from elsewhere (another member, the server's answer) shows only
|
||||
// while the field is not being typed in.
|
||||
useEffect(() => {
|
||||
if (!editing.current) setDraft(stored);
|
||||
}, [stored]);
|
||||
// What is being typed, while the field has it; null shows the stored weight,
|
||||
// so a change from elsewhere (another member) shows whenever nobody types.
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
// A saved weight until the store has it, so the field does not fall back to
|
||||
// the old number for the length of the request.
|
||||
const [pending, setPending] = useState<string | null>(null);
|
||||
const shown = draft ?? pending ?? stored;
|
||||
|
||||
const commit = () => {
|
||||
editing.current = false;
|
||||
if (draft === null) return;
|
||||
const typed = draft;
|
||||
setDraft(null);
|
||||
if (readOnly) return;
|
||||
const grams = draft.trim() === '' ? null : Math.max(0, Number.parseInt(draft, 10) || 0);
|
||||
const grams = typed.trim() === '' ? null : Math.max(0, Number.parseInt(typed, 10) || 0);
|
||||
const next = grams == null ? '' : String(grams);
|
||||
setDraft(next);
|
||||
if (next !== stored) void onSave(grams);
|
||||
if (next === stored) return;
|
||||
setPending(next);
|
||||
void Promise.resolve(onSave(grams)).finally(() => setPending(null));
|
||||
};
|
||||
|
||||
const style: CSSProperties = {
|
||||
@@ -47,11 +50,9 @@ export function WeightInput({ value, onSave, readOnly, width }: Props) {
|
||||
|
||||
return (
|
||||
<NumericInput
|
||||
value={draft}
|
||||
value={shown}
|
||||
readOnly={readOnly}
|
||||
onFocus={() => {
|
||||
editing.current = true;
|
||||
}}
|
||||
onFocus={() => setDraft(shown)}
|
||||
onValueChange={setDraft}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('Skeleton', () => {
|
||||
})
|
||||
|
||||
describe('composed skeletons', () => {
|
||||
it('FE-W4SKEL-006: SpotlightSkeleton takes the hero's own box and renders the backdrop plus two bars', () => {
|
||||
it('FE-W4SKEL-006: SpotlightSkeleton takes the box of the hero and renders the backdrop plus two bars', () => {
|
||||
const { container } = render(<SpotlightSkeleton />)
|
||||
|
||||
expect(container.querySelectorAll('.trek-skeleton')).toHaveLength(3)
|
||||
|
||||
@@ -67,6 +67,8 @@ beforeEach(() => {
|
||||
vi.setSystemTime(new Date('2026-07-05T12:00:00Z'));
|
||||
installMatchMedia();
|
||||
resetAllStores();
|
||||
// The currency tool waits for the saved pair (#3009); the page renders with settings in place.
|
||||
useSettingsStore.setState({ isLoaded: true });
|
||||
seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() });
|
||||
usePluginStore.setState({ plugins: [], loaded: true });
|
||||
onlyTrips([TRIP]);
|
||||
|
||||
@@ -43,6 +43,8 @@ function enableCollectionsAddon(enabled: boolean) {
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
// The converter waits for the saved pair (#3009); these widgets render with settings in place.
|
||||
useSettingsStore.setState({ isLoaded: true });
|
||||
fxHandler();
|
||||
collectionsHandler([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user