/** * 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 searchable combobox — a text input (role=combobox) * that opens a role=listbox of role=option items on focus. Selecting a zone means * focusing the input, typing to filter, then clicking the option (not selectOption). * Option accessible names are the full IANA id (e.g. "America/Chicago"). */ import { test, expect } from '@playwright/test'; test.describe('Admin Timezone section — 18-04 round-trip', () => { test.beforeEach(async ({ page }, testInfo) => { // Desktop-only: the admin timezone UI is desktop-focused, and these tests mutate // the single household_timezone row. global-setup resets that row once per run, so // running on multiple profiles lets one project's save leak into another project's // first-run assertions. Restricting to one project keeps the shared state coherent. test.skip(testInfo.project.name !== 'desktop', 'Admin timezone UI is desktop-only'); await page.goto('/admin'); await expect(page.getByRole('heading', { name: 'Admin Settings' })).toBeVisible(); // Phase 17 (D-10) reworked /admin into a two-tab layout; the Timezone section // now lives in the "Settings" tab panel, which is hidden until selected. Open // it before asserting the section. await page.getByRole('tab', { name: 'Settings' }).click(); // 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 }) => { // The searchable text input exposes role=combobox 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.click(); await input.fill('America/Chicago'); await page.getByRole('option', { name: 'America/Chicago' }).click(); await expect(input).toHaveValue('America/Chicago'); const saveBtn = tzSection.getByRole('button', { name: /Save/ }); await expect(saveBtn).toBeEnabled(); }); test('Typing filters the list (type-to-search)', async ({ page }) => { const input = page.getByRole('combobox', { name: 'Household timezone' }); const listbox = page.getByRole('listbox', { name: 'Timezones' }); // Focus opens the full list with no typing required. await input.click(); await expect(listbox).toBeVisible(); await expect(listbox.getByRole('option').first()).toBeVisible(); // Human-friendly partial query (case-insensitive, underscores ignored) filters. await input.fill('york'); await expect(page.getByRole('option', { name: 'America/New_York' })).toBeVisible(); await expect(page.getByRole('option', { name: 'Europe/Paris' })).toHaveCount(0); }); 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 via the searchable combobox await input.click(); await input.fill('America/Chicago'); await page.getByRole('option', { name: 'America/Chicago' }).click(); await expect(input).toHaveValue('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(); // Reload resets to the default "Members & Accounts" tab — re-open Settings. await page.getByRole('tab', { name: 'Settings' }).click(); 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!); }); });