Remove the sticky bottom create input from /lists. The masthead now hosts
a primary "New list" button next to the Trash icon, styled identically to
the "New note" button on /notes. Click → createList with an empty name →
goto /lists/[id], where a prominent editable title input (above To buy /
Checked) autosaves on blur / input with a 500 ms debounce.
Changes
apps/web/src/routes/(app)/lists/+page.svelte
- remove `newListName`, `nameInput`, `handleKeydown`
- remove the absolute-positioned sticky bottom block
- handleCreate now creates with name = '' and navigates to the detail
- actions snippet gains the primary New list button; Trash button
demoted to ghost icon
apps/web/src/routes/(app)/lists/[id]/+page.svelte
- new listName / scheduleNameSave / flushNameSave state (mirrors the
/notes editor pattern)
- big editable title <input> at top of content, autosaves on blur
via renameList
- contextual header h1 trimmed to a secondary caption-size preview
on mobile (so the user still sees the list name at the top)
- flushNameSave on component destroy so unsaved renames commit
apps/web/messages/{en,es}.json: lists_new_list = "New list"/"Nueva lista"
Tests
apps/web/tests/e2e/lists.test.ts: createList helper now clicks the
button, fills the title input, blurs, waits for autosave, and goes
back to /lists to present the same "list-is-on-the-overview" state
that C-11 / C-12 expect. C-07 picks up the reload check.
apps/web/tests/e2e/session.test.ts: S-03 mirrors the new flow for its
fresh-list setup.
Verification
just test-e2e → 40 passed, 2 skipped (swipe-delete .skip remains).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
/**
|
|
* S-series (Modo Compra) — Fase 2b.3
|
|
*
|
|
* Full-screen shopping session at `/lists/[id]/session`. Large tap targets,
|
|
* flip animation moving items between TO BUY / CHECKED, and a confirmation
|
|
* modal for "Finish shopping" that marks the list completed and returns to
|
|
* `/lists`.
|
|
*/
|
|
import { test, expect } from '@playwright/test';
|
|
import { USERS } from '../fixtures/users.js';
|
|
import { loginAs } from '../fixtures/login.js';
|
|
|
|
const SEED_LIST_ID = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
|
const SEED_LIST_PATH = `/lists/${SEED_LIST_ID}`;
|
|
const SESSION_PATH = `${SEED_LIST_PATH}/session`;
|
|
|
|
test.describe('Shopping session — full-screen mode', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginAs(page, USERS.borja);
|
|
});
|
|
|
|
test('S-01: navigating to /lists/[id]/session shows the full-screen container', async ({
|
|
page
|
|
}) => {
|
|
await page.goto(SESSION_PATH);
|
|
await expect(page.getByTestId('shopping-session')).toBeVisible({ timeout: 15_000 });
|
|
// The finish CTA must be visible — it's the whole point of the view.
|
|
await expect(page.getByRole('button', { name: /finish shopping|terminar compra/i })).toBeVisible();
|
|
});
|
|
|
|
test('S-02: checking an item moves it into the CHECKED section', async ({ page }) => {
|
|
// Ensure at least one unchecked item exists by creating one from the
|
|
// regular list page (simpler than seeding items in the session view).
|
|
await page.goto(SEED_LIST_PATH);
|
|
await expect(page.getByPlaceholder(/add item|añadir producto/i)).toBeVisible({
|
|
timeout: 15_000
|
|
});
|
|
const itemName = `S-02-${Date.now()}`;
|
|
await page.getByPlaceholder(/add item|añadir producto/i).fill(itemName);
|
|
await page.getByPlaceholder(/add item|añadir producto/i).press('Enter');
|
|
await expect(page.getByText(itemName)).toBeVisible({ timeout: 5_000 });
|
|
|
|
// Now go to the session view and check the item
|
|
await page.goto(SESSION_PATH);
|
|
await expect(page.getByTestId('shopping-session')).toBeVisible({ timeout: 15_000 });
|
|
|
|
const row = page.locator('[role="listitem"]').filter({ hasText: itemName });
|
|
await row.getByRole('button', { name: /toggle item/i }).click();
|
|
|
|
// The row's toggle now has aria-label "Uncheck item" (in CHECKED section)
|
|
await expect(
|
|
page.locator('[role="listitem"]').filter({ hasText: itemName }).getByRole('button', {
|
|
name: /uncheck item/i
|
|
})
|
|
).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test('S-03: Finish shopping → confirm → list is completed and we return to /lists', async ({
|
|
page
|
|
}) => {
|
|
// Need a fresh list so completing it doesn't affect shared seed state.
|
|
// Use the new big-button create flow: masthead "New list" → lands on
|
|
// /lists/[id] → fill inline title → autosave → we're already on the
|
|
// detail view, no need to navigate back.
|
|
await page.goto('/lists');
|
|
await page
|
|
.getByRole('button', { name: /new list|nueva lista/i })
|
|
.click();
|
|
await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 10_000 });
|
|
const listName = `S-03-list-${Date.now()}`;
|
|
const titleInput = page.getByPlaceholder(/weekly shop|compra semanal/i);
|
|
await expect(titleInput).toBeVisible({ timeout: 10_000 });
|
|
await titleInput.fill(listName);
|
|
await titleInput.blur();
|
|
await page.waitForTimeout(900); // autosave debounce
|
|
|
|
// Open session mode
|
|
await page.getByTestId('start-session').click();
|
|
await expect(page.getByTestId('shopping-session')).toBeVisible({ timeout: 15_000 });
|
|
|
|
// Finish shopping → confirm
|
|
await page.getByRole('button', { name: /finish shopping|terminar compra/i }).click();
|
|
await page.getByRole('button', { name: /yes, finish|sí, terminar/i }).click();
|
|
|
|
// Redirected back to /lists, and the completed list is not in the
|
|
// active grid anymore (it moves to "Completed").
|
|
await expect(page).toHaveURL(/\/lists\/?$/, { timeout: 10_000 });
|
|
});
|
|
});
|