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
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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[] =
|
||||||
|
|||||||
Reference in New Issue
Block a user