Files
familysync/apps/pwa/e2e/timezone-verify.spec.ts
T
Lucas BergerandClaude Opus 4.8 a8d6142566 fix(18): timezone picker shows full list on tap (native select)
The IANA picker was an <input list=datalist>, which filters the dropdown by
whatever text is already in the field — so with the stored zone pre-filled a
user only saw a single option and had to erase the value (undiscoverable) to
browse. datalist is also unreliable in iOS Safari.

Replace it with a native <select> grouped by region (<optgroup>): tapping
shows the whole list with no typing/erasing, and it renders as the native
wheel picker on iOS. The "Use detected" one-tap shortcut still covers the
common case. Option labels are shortened (region stripped, underscores → spaces)
while values remain full IANA ids. e2e updated from fill() to selectOption().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:19:42 -04:00

102 lines
4.6 KiB
TypeScript

/**
* timezone-verify.spec.ts — 18-04 round-trip verification
*
* Verifies the admin Timezone section with the real 18-02 API endpoints.
* Runs on desktop profile only (admin UI is desktop-focused).
*
* NOTE: the IANA picker is a native <select> grouped by region, which has the
* ARIA combobox role. Selecting a zone uses selectOption (not fill), and the
* option's value is the full IANA id even though its visible label is shortened.
*/
import { test, expect } from '@playwright/test';
test.describe('Admin Timezone section — 18-04 round-trip', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/admin');
await expect(page.getByRole('heading', { name: 'Admin Settings' })).toBeVisible();
// Wait for the Timezone section to load (requires the 18-02 GET endpoint)
const tzSection = page.getByRole('region', { name: 'Timezone' });
await expect(tzSection).toBeVisible({ timeout: 15_000 });
});
test('Timezone section is visible on /admin', async ({ page }) => {
await expect(page.getByRole('region', { name: 'Timezone' })).toBeVisible();
});
test('Timezone picker (combobox) is visible and pre-filled', async ({ page }) => {
// Native <select> has the combobox role
const input = page.getByRole('combobox', { name: 'Household timezone' });
await expect(input).toBeVisible();
const val = await input.inputValue();
expect(val.length, 'Picker should have a non-empty timezone').toBeGreaterThan(0);
});
test('Save is enabled on first run when timezone is not yet explicit (WR-01)', async ({ page }) => {
// On first run the GET returns isExplicitlySet:false with the detected zone
// pre-filled. Saving that value to make the choice explicit is a meaningful
// action, so Save must be ENABLED even though the input matches the displayed
// default. (The disabled-when-unchanged behaviour for an already-explicit
// value is covered by the "Save persists" test below, which re-disables Save
// after a successful save.)
const tzSection = page.getByRole('region', { name: 'Timezone' });
// Confirm we are in the first-run (system-default) state for this assertion.
await expect(tzSection.getByText('Using system default')).toBeVisible();
const saveBtn = tzSection.getByRole('button', { name: /Save|Saving/ });
await expect(saveBtn).toBeVisible();
await expect(saveBtn).toBeEnabled();
});
test('Changing the selection enables Save', async ({ page }) => {
const tzSection = page.getByRole('region', { name: 'Timezone' });
const input = page.getByRole('combobox', { name: 'Household timezone' });
await input.selectOption('America/Chicago');
const saveBtn = tzSection.getByRole('button', { name: /Save/ });
await expect(saveBtn).toBeEnabled();
});
test('Save persists timezone across reload', async ({ page }) => {
const input = page.getByRole('combobox', { name: 'Household timezone' });
const tzSection = page.getByRole('region', { name: 'Timezone' });
// Set to a known value
await input.selectOption('America/Chicago');
const saveBtn = tzSection.getByRole('button', { name: /^Save$/ });
await expect(saveBtn).toBeEnabled();
await saveBtn.click();
// Wait for save to complete (button re-disables when stored === input)
await expect(saveBtn).toBeDisabled({ timeout: 15_000 });
// Reload and verify persistence
await page.reload();
await expect(page.getByRole('heading', { name: 'Admin Settings' })).toBeVisible();
await expect(tzSection).toBeVisible({ timeout: 15_000 });
const inputAfterReload = page.getByRole('combobox', { name: 'Household timezone' });
await expect(inputAfterReload).toHaveValue('America/Chicago');
// system-default note should be gone after explicit set
await expect(tzSection.getByText('Using system default')).toHaveCount(0);
});
test('"Use detected" affordance sets input to browser zone', async ({ page }) => {
const tzSection = page.getByRole('region', { name: 'Timezone' });
const detectedBtn = tzSection.getByRole('button', { name: /Use detected:/ });
// The button is only visible when detected TZ != stored TZ
const count = await detectedBtn.count();
if (count === 0) {
test.skip();
return;
}
// Get the detected zone from the button label
const btnText = await detectedBtn.textContent();
const match = btnText?.match(/Use detected:\s*(.+)/);
const detectedZone = match?.[1]?.trim();
expect(detectedZone, 'Detected zone should be non-empty').toBeTruthy();
await detectedBtn.click();
const input = page.getByRole('combobox', { name: 'Household timezone' });
await expect(input).toHaveValue(detectedZone!);
});
});