Add one change & crime
This commit is contained in:
parent
8e4c56bb0d
commit
54fbcb1ea6
17 changed files with 306 additions and 154 deletions
|
|
@ -34,6 +34,7 @@ describe('useTravelTime', () => {
|
|||
timeRange: null,
|
||||
useBest: false,
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
},
|
||||
]);
|
||||
|
|
@ -143,6 +144,27 @@ describe('useTravelTime', () => {
|
|||
act(() => result.current.handleToggleNoChange(0));
|
||||
expect(result.current.entries[0]).toMatchObject({ noChange: false, noBuses: true });
|
||||
});
|
||||
|
||||
it('keeps noChange and oneChange mutually exclusive', () => {
|
||||
const { result } = renderHook(() => useTravelTime());
|
||||
act(() => result.current.handleAddEntry('transit'));
|
||||
act(() => result.current.handleSetDestination(0, 'bank', 'Bank'));
|
||||
|
||||
act(() => result.current.handleToggleNoChange(0));
|
||||
expect(result.current.entries[0]).toMatchObject({ noChange: true, oneChange: false });
|
||||
|
||||
// Enabling oneChange clears noChange...
|
||||
act(() => result.current.handleToggleOneChange(0));
|
||||
expect(result.current.entries[0]).toMatchObject({ noChange: false, oneChange: true });
|
||||
|
||||
// ...and vice versa.
|
||||
act(() => result.current.handleToggleNoChange(0));
|
||||
expect(result.current.entries[0]).toMatchObject({ noChange: true, oneChange: false });
|
||||
|
||||
// Both can be off (no restriction): toggling the active one off leaves neither set.
|
||||
act(() => result.current.handleToggleNoChange(0));
|
||||
expect(result.current.entries[0]).toMatchObject({ noChange: false, oneChange: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTransitVariant', () => {
|
||||
|
|
@ -163,10 +185,20 @@ describe('resolveTransitVariant', () => {
|
|||
it('maps transit toggle combinations to the right variant string', () => {
|
||||
expect(resolveTransitVariant(base)).toBe('transit');
|
||||
expect(resolveTransitVariant({ ...base, noChange: true })).toBe('transit-no-change');
|
||||
expect(resolveTransitVariant({ ...base, oneChange: true })).toBe('transit-one-change');
|
||||
expect(resolveTransitVariant({ ...base, noBuses: true })).toBe('transit-no-bus');
|
||||
expect(resolveTransitVariant({ ...base, noChange: true, noBuses: true })).toBe(
|
||||
'transit-no-change-no-bus'
|
||||
);
|
||||
expect(resolveTransitVariant({ ...base, oneChange: true, noBuses: true })).toBe(
|
||||
'transit-one-change-no-bus'
|
||||
);
|
||||
});
|
||||
|
||||
it('lets noChange win if both change flags are somehow set', () => {
|
||||
expect(resolveTransitVariant({ ...base, noChange: true, oneChange: true })).toBe(
|
||||
'transit-no-change'
|
||||
);
|
||||
});
|
||||
|
||||
it('treats undefined flags as false', () => {
|
||||
|
|
@ -177,47 +209,69 @@ describe('resolveTransitVariant', () => {
|
|||
});
|
||||
|
||||
describe('parseServerMode', () => {
|
||||
it('round-trips the four toggle-reachable variants', () => {
|
||||
it('round-trips the six toggle-reachable transit variants', () => {
|
||||
expect(parseServerMode('transit')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('transit-no-bus')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: true,
|
||||
});
|
||||
expect(parseServerMode('transit-no-change')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: true,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('transit-no-change-no-bus')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: true,
|
||||
oneChange: false,
|
||||
noBuses: true,
|
||||
});
|
||||
expect(parseServerMode('transit-one-change')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: false,
|
||||
oneChange: true,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('transit-one-change-no-bus')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: false,
|
||||
oneChange: true,
|
||||
noBuses: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses non-transit base modes', () => {
|
||||
expect(parseServerMode('car')).toEqual({ mode: 'car', noChange: false, noBuses: false });
|
||||
expect(parseServerMode('car')).toEqual({
|
||||
mode: 'car',
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('bicycle')).toEqual({
|
||||
mode: 'bicycle',
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('walking')).toEqual({
|
||||
mode: 'walking',
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for variants the UI cannot represent (no silent broadening)', () => {
|
||||
expect(parseServerMode('transit-one-change')).toBeNull();
|
||||
expect(parseServerMode('transit-one-change-no-bus')).toBeNull();
|
||||
expect(parseServerMode('unknown-mode')).toBeNull();
|
||||
expect(parseServerMode('transit-two-change')).toBeNull();
|
||||
});
|
||||
|
||||
it('travelFieldKey uses the resolved variant', () => {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ export interface TravelTimeEntry {
|
|||
useBest: boolean;
|
||||
/** Restrict transit to walk-transit-walk (0 changes). Optional; defaults to false. */
|
||||
noChange?: boolean;
|
||||
/** Restrict transit to at most one change. Optional; defaults to false.
|
||||
* Mutually exclusive with noChange (the UI never sets both). */
|
||||
oneChange?: boolean;
|
||||
/** Drop buses from the allowed transit modes. Optional; defaults to false. */
|
||||
noBuses?: boolean;
|
||||
}
|
||||
|
|
@ -77,42 +80,44 @@ export interface TravelTimeEntry {
|
|||
*
|
||||
* For non-transit modes the entry.mode passes through unchanged.
|
||||
*
|
||||
* Note: the transit-one-change* variants exist server-side but are not reachable
|
||||
* from the UI toggles (only no-change + no-buses are exposed). They're available
|
||||
* via direct API access for callers that want them.
|
||||
* noChange and oneChange are mutually exclusive (the UI never sets both); if
|
||||
* they ever were, noChange wins here.
|
||||
*/
|
||||
export function resolveTransitVariant(entry: TravelTimeEntry): string {
|
||||
if (entry.mode !== 'transit') return entry.mode;
|
||||
const nc = entry.noChange ?? false;
|
||||
const oc = entry.oneChange ?? false;
|
||||
const nb = entry.noBuses ?? false;
|
||||
if (nc && nb) return 'transit-no-change-no-bus';
|
||||
if (nc) return 'transit-no-change';
|
||||
if (nb) return 'transit-no-bus';
|
||||
return 'transit';
|
||||
if (nc) return nb ? 'transit-no-change-no-bus' : 'transit-no-change';
|
||||
if (oc) return nb ? 'transit-one-change-no-bus' : 'transit-one-change';
|
||||
return nb ? 'transit-no-bus' : 'transit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a server-side mode string (incl. transit variants) back into a base
|
||||
* TransportMode + UI toggle booleans. Returns null for mode strings the UI
|
||||
* cannot represent (currently: transit-one-change, transit-one-change-no-bus).
|
||||
* Callers should skip entries that parse to null rather than silently
|
||||
* normalising to a different variant.
|
||||
* cannot represent. Callers should skip entries that parse to null rather than
|
||||
* silently normalising to a different variant.
|
||||
*/
|
||||
export function parseServerMode(
|
||||
modeStr: string
|
||||
): { mode: TransportMode; noChange: boolean; noBuses: boolean } | null {
|
||||
): { mode: TransportMode; noChange: boolean; oneChange: boolean; noBuses: boolean } | null {
|
||||
if (modeStr === 'car' || modeStr === 'bicycle' || modeStr === 'walking') {
|
||||
return { mode: modeStr, noChange: false, noBuses: false };
|
||||
return { mode: modeStr, noChange: false, oneChange: false, noBuses: false };
|
||||
}
|
||||
switch (modeStr) {
|
||||
case 'transit':
|
||||
return { mode: 'transit', noChange: false, noBuses: false };
|
||||
return { mode: 'transit', noChange: false, oneChange: false, noBuses: false };
|
||||
case 'transit-no-bus':
|
||||
return { mode: 'transit', noChange: false, noBuses: true };
|
||||
return { mode: 'transit', noChange: false, oneChange: false, noBuses: true };
|
||||
case 'transit-no-change':
|
||||
return { mode: 'transit', noChange: true, noBuses: false };
|
||||
return { mode: 'transit', noChange: true, oneChange: false, noBuses: false };
|
||||
case 'transit-no-change-no-bus':
|
||||
return { mode: 'transit', noChange: true, noBuses: true };
|
||||
return { mode: 'transit', noChange: true, oneChange: false, noBuses: true };
|
||||
case 'transit-one-change':
|
||||
return { mode: 'transit', noChange: false, oneChange: true, noBuses: false };
|
||||
case 'transit-one-change-no-bus':
|
||||
return { mode: 'transit', noChange: false, oneChange: true, noBuses: true };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -145,6 +150,7 @@ export function useTravelTime(initial?: TravelTimeInitial) {
|
|||
timeRange: null,
|
||||
useBest: false,
|
||||
noChange: false,
|
||||
oneChange: false,
|
||||
noBuses: false,
|
||||
},
|
||||
]);
|
||||
|
|
@ -182,11 +188,27 @@ export function useTravelTime(initial?: TravelTimeInitial) {
|
|||
);
|
||||
}, []);
|
||||
|
||||
// noChange and oneChange are mutually exclusive: enabling one clears the
|
||||
// other so the resolved server variant is never ambiguous.
|
||||
const handleToggleNoChange = useCallback((index: number) => {
|
||||
setEntries((prev) =>
|
||||
dedupeTravelTimeEntries(
|
||||
prev.map((entry, i) =>
|
||||
i === index ? { ...entry, noChange: !(entry.noChange ?? false) } : entry
|
||||
i === index
|
||||
? { ...entry, noChange: !(entry.noChange ?? false), oneChange: false }
|
||||
: entry
|
||||
)
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleToggleOneChange = useCallback((index: number) => {
|
||||
setEntries((prev) =>
|
||||
dedupeTravelTimeEntries(
|
||||
prev.map((entry, i) =>
|
||||
i === index
|
||||
? { ...entry, oneChange: !(entry.oneChange ?? false), noChange: false }
|
||||
: entry
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
@ -219,6 +241,7 @@ export function useTravelTime(initial?: TravelTimeInitial) {
|
|||
handleTimeRangeChange,
|
||||
handleToggleBest,
|
||||
handleToggleNoChange,
|
||||
handleToggleOneChange,
|
||||
handleToggleNoBuses,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue