Phase 18: Auto timezone detection and ability to change timezone #21

Merged
luckberg merged 38 commits from gsd/phase-18-auto-timezone-detection-and-ability-to-change-timezone into main 2026-06-15 09:55:53 -04:00
2 changed files with 136 additions and 2 deletions
Showing only changes of commit 173e06ea77 - Show all commits
@@ -0,0 +1,124 @@
/**
* AdminPage timezone Save-button disabled logic — unit tests (WR-01 fix verification)
*
* Tests the `timezoneSaveDisabled` derivation inline:
*
* const timezoneSaveDisabled =
* timezoneMutation.isPending ||
* effectiveTimezoneInput === '' ||
* (isExplicit && effectiveTimezoneInput === storedTimezone);
*
* The pre-fix bug: Save was always disabled when input === stored value,
* regardless of isExplicitlySet. On first run (isExplicitlySet: false) this meant
* the admin could never confirm/save the displayed system-default zone.
*/
import { describe, it, expect } from 'vitest';
/**
* Pure function extracted from AdminPage that computes whether Save should be disabled.
* This mirrors the logic that must be present post-fix in AdminPage.tsx.
*/
function computeTimezoneSaveDisabled({
isPending,
effectiveTimezoneInput,
storedTimezone,
isExplicit,
}: {
isPending: boolean;
effectiveTimezoneInput: string;
storedTimezone: string;
isExplicit: boolean;
}): boolean {
return (
isPending ||
effectiveTimezoneInput === '' ||
(isExplicit && effectiveTimezoneInput === storedTimezone)
);
}
// ---------------------------------------------------------------------------
// Pre-fix behavior: the bug that WR-01 identified
// ---------------------------------------------------------------------------
describe('timezoneSaveDisabled — WR-01 first-run (isExplicitlySet: false)', () => {
it('Save is ENABLED when not explicitly set, even if input matches stored value (WR-01)', () => {
// First run: GET returns { timezone: 'UTC', isExplicitlySet: false }
// Input pre-fills to 'UTC'. This must enable Save so admin can confirm.
const disabled = computeTimezoneSaveDisabled({
isPending: false,
effectiveTimezoneInput: 'UTC',
storedTimezone: 'UTC',
isExplicit: false,
});
expect(disabled).toBe(false); // WR-01: was `true` before fix (bug)
});
it('Save is ENABLED when not explicitly set with a real zone matching stored', () => {
const disabled = computeTimezoneSaveDisabled({
isPending: false,
effectiveTimezoneInput: 'America/Chicago',
storedTimezone: 'America/Chicago',
isExplicit: false,
});
expect(disabled).toBe(false);
});
it('Save is DISABLED when pending (regardless of isExplicit)', () => {
const disabled = computeTimezoneSaveDisabled({
isPending: true,
effectiveTimezoneInput: 'UTC',
storedTimezone: 'UTC',
isExplicit: false,
});
expect(disabled).toBe(true);
});
it('Save is DISABLED when input is empty', () => {
const disabled = computeTimezoneSaveDisabled({
isPending: false,
effectiveTimezoneInput: '',
storedTimezone: 'UTC',
isExplicit: false,
});
expect(disabled).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Already-explicit cases: no-op re-saves should stay disabled
// ---------------------------------------------------------------------------
describe('timezoneSaveDisabled — already explicitly set (isExplicitlySet: true)', () => {
it('Save is DISABLED when input matches stored and timezone is explicit (no-op guard)', () => {
// Admin previously saved 'America/Chicago'. Input still shows 'America/Chicago'.
// No change → Save stays disabled (two-tap UX).
const disabled = computeTimezoneSaveDisabled({
isPending: false,
effectiveTimezoneInput: 'America/Chicago',
storedTimezone: 'America/Chicago',
isExplicit: true,
});
expect(disabled).toBe(true);
});
it('Save is ENABLED when input differs from stored (user changed value)', () => {
const disabled = computeTimezoneSaveDisabled({
isPending: false,
effectiveTimezoneInput: 'Europe/Paris',
storedTimezone: 'America/Chicago',
isExplicit: true,
});
expect(disabled).toBe(false);
});
it('Save is DISABLED when pending even if value differs', () => {
const disabled = computeTimezoneSaveDisabled({
isPending: true,
effectiveTimezoneInput: 'Europe/Paris',
storedTimezone: 'America/Chicago',
isExplicit: true,
});
expect(disabled).toBe(true);
});
});
+12 -2
View File
@@ -111,11 +111,21 @@ export function AdminPage() {
const storedTimezone = timezoneQuery.data?.timezone ?? ''; const storedTimezone = timezoneQuery.data?.timezone ?? '';
const effectiveTimezoneInput = timezoneInput ?? storedTimezone; const effectiveTimezoneInput = timezoneInput ?? storedTimezone;
// Save is disabled when pending, or when input matches what's stored // WR-01: derive isExplicit so we only apply the no-op guard when the timezone
// has ALREADY been explicitly saved. On first run (isExplicitlySet: false) the
// admin must be able to confirm/save the displayed system-default — even if the
// input value already matches the fallback string. Keep pending and empty-input
// guards unconditional.
const isExplicit = timezoneQuery.data?.isExplicitlySet ?? false;
// Save is disabled when:
// - mutation is in-flight (pending), OR
// - input is empty, OR
// - the timezone IS already explicitly set AND the input is unchanged (no-op)
const timezoneSaveDisabled = const timezoneSaveDisabled =
timezoneMutation.isPending || timezoneMutation.isPending ||
effectiveTimezoneInput === '' || effectiveTimezoneInput === '' ||
effectiveTimezoneInput === storedTimezone; (isExplicit && effectiveTimezoneInput === storedTimezone);
// IANA zones list for the datalist (Intl.supportedValuesOf may not be present in all runtimes) // IANA zones list for the datalist (Intl.supportedValuesOf may not be present in all runtimes)
const ianaZones: string[] = const ianaZones: string[] =