CI / changes (pull_request) Successful in 2s
CI / fast-checks (pull_request) Successful in 1m28s
CI / api (pull_request) Successful in 1m6s
CI / harness (pull_request) Successful in 4m22s
CI / security (pull_request) Successful in 41s
CI / gate (pull_request) Successful in 1s
The timezone spec mutates the single household_timezone row, but e2e global-setup resets it only once per run. Running on all three device profiles (iphone/pixel/ desktop) let one profile's "Save persists" write leak into another profile's first-run assertions, failing the harness job in CI (workers=1, serial). The admin timezone UI is desktop-focused, so skip the spec on non-desktop profiles — matching the layout.spec.ts desktop-only pattern. Full harness: 107 passed, 19 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
6.2 KiB
TypeScript
131 lines
6.2 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 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();
|
|
// 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();
|
|
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!);
|
|
});
|
|
});
|