From 173e06ea77b0f88350abdcecb75a0bd5a286d7a9 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 14 Jun 2026 22:53:44 -0400 Subject: [PATCH] fix(18): enable first-run timezone save when not explicitly set (WR-01) - Derive isExplicit from timezoneQuery.data?.isExplicitlySet - Apply the input===stored no-op guard only when isExplicit is true - Keep pending and empty-input guards unconditional - Add unit tests (AdminPage.timezone.test.ts) verifying first-run Save is enabled when isExplicitlySet:false and input matches stored fallback value --- .../pwa/src/routes/AdminPage.timezone.test.ts | 124 ++++++++++++++++++ apps/pwa/src/routes/AdminPage.tsx | 14 +- 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 apps/pwa/src/routes/AdminPage.timezone.test.ts diff --git a/apps/pwa/src/routes/AdminPage.timezone.test.ts b/apps/pwa/src/routes/AdminPage.timezone.test.ts new file mode 100644 index 0000000..b92aada --- /dev/null +++ b/apps/pwa/src/routes/AdminPage.timezone.test.ts @@ -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); + }); +}); diff --git a/apps/pwa/src/routes/AdminPage.tsx b/apps/pwa/src/routes/AdminPage.tsx index ccacc83..efc2630 100644 --- a/apps/pwa/src/routes/AdminPage.tsx +++ b/apps/pwa/src/routes/AdminPage.tsx @@ -111,11 +111,21 @@ export function AdminPage() { const storedTimezone = timezoneQuery.data?.timezone ?? ''; 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 = timezoneMutation.isPending || effectiveTimezoneInput === '' || - effectiveTimezoneInput === storedTimezone; + (isExplicit && effectiveTimezoneInput === storedTimezone); // IANA zones list for the datalist (Intl.supportedValuesOf may not be present in all runtimes) const ianaZones: string[] =