/** * LTF-series (UI): Shopping list title flow — required title, default prefill, * auto-numbered `#N`, admin catalog CRUD. Fase 18. * * Live Keycloak login per test (no cached storageState). The admin specs run * as Ana; the read-only spec runs as Borja (member). All specs land on a * fresh modal each time, so we don't need to reset DB state between cases — * we tag created rows with a unique timestamp suffix. */ import { test, expect, type Page } from '@playwright/test'; import { USERS } from '../fixtures/users.js'; import { loginAs } from '../fixtures/login.js'; /** Click "New list" on the masthead and wait for the modal to mount. */ async function openCreateListModal(page: Page) { await page.getByRole('button', { name: /new list|nueva lista/i }).click(); await expect(page.getByTestId('create-list-modal')).toBeVisible({ timeout: 5_000 }); } /** * Set `collectives.default_list_title` by driving the in-page Supabase * client directly. Faster + more deterministic than driving the manage UI, * which lives in a separate spec (LTF-02 / LTF-04). */ async function setDefaultListTitle(page: Page, title: string) { // Land on any auth-gated route so __sb is exposed and the active // collective is loaded. await page.goto('/lists'); await page.waitForFunction( () => Boolean((window as unknown as { __sb?: unknown }).__sb), null, { timeout: 10_000 } ); await page.evaluate(async (value) => { const sb = ( window as unknown as { __sb: import('@supabase/supabase-js').SupabaseClient } ).__sb; await sb .from('collectives') .update({ default_list_title: value && value.length > 0 ? value : null }) .eq('id', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'); }, title); // Force a reload so the root layout's `loadUserCollectives` hydrates // `$currentCollective.default_list_title` with the updated value before // the modal opens. await page.reload(); await expect( page.getByRole('button', { name: /new list|nueva lista/i }) ).toBeVisible({ timeout: 15_000 }); } test.describe('Shopping list title flow — admin (Ana)', () => { test.beforeEach(async ({ page }) => { await loginAs(page, USERS.ana); }); test('LTF-01: default_list_title prefills the create modal and submits as-is', async ({ page }) => { const defaultTitle = `LTF01 ${Date.now()}`; await setDefaultListTitle(page, defaultTitle); // `setDefaultListTitle` already navigates to /lists after the reload. await openCreateListModal(page); // Input is prefilled with the configured default. const input = page.getByTestId('create-list-modal-name'); await expect(input).toHaveValue(defaultTitle); // Submit creates a list with the prefilled name — no `#1` suffix // because the default was never added to the catalog (auto-suffix is // catalog-gated). await page.getByTestId('create-list-modal-submit').click(); await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 10_000 }); // List name lives in an `` on the detail page (inline rename). const titleInput = page.locator('input[aria-label]').filter({ has: page.locator(':scope[aria-label*="list name" i], :scope[aria-label*="nombre" i]') }); // Fallback assertion that's robust to label changes: assert the input // value directly via locator.inputValue() once the input is mounted. await expect.poll(async () => { const inputs = await page.locator('input').all(); for (const inp of inputs) { const v = await inp.inputValue().catch(() => ''); if (v === defaultTitle) return v; } return ''; }, { timeout: 5_000 }).toBe(defaultTitle); // Silence the unused locator warning — we keep titleInput so the // intent is clear if the inputValue poll is ever rewritten. void titleInput; }); test('LTF-03: empty input keeps the submit button disabled', async ({ page }) => { // Make sure no default is set so the input opens blank. await setDefaultListTitle(page, ''); // `setDefaultListTitle` already navigates to /lists after the reload. await openCreateListModal(page); const input = page.getByTestId('create-list-modal-name'); const submit = page.getByTestId('create-list-modal-submit'); await expect(input).toHaveValue(''); await expect(submit).toBeDisabled(); // Typing then erasing should keep the disabled state when only spaces // remain (trim-then-check). await input.fill(' '); await expect(submit).toBeDisabled(); await input.fill('something'); await expect(submit).toBeEnabled(); }); });