Files
collective-lists/apps/web/tests/e2e/mobile-shell.test.ts
Oier Bravo Urtasun 2174d2c2c0 feat(fase-5): mobile UX shell + masthead + swipe-delete undo (224 tests green)
5.0 Tests primero
  Playwright: mobile-shell.test.ts (4 — sidebar hidden on mobile, bottom tab
    bar visible with aria-current, hamburger opens drawer, desktop shows
    sidebar), mobile-masthead.test.ts (4 — every top-level route pairs
    masthead-label + masthead-title), mobile-swipe-delete.test.ts (M-06/M-07
    describe.skip — Chromium touch emulation flaky; needs `playwright install
    webkit`)
  Vitest: undoQueue.test.ts (5 — schedule+commit, undo+restore, TTL expiry,
    double-undo no-op, custom ttlMs)

5.1 Responsive shell
  DesktopSidebar.svelte (hidden md:flex)
  MobileTopBar.svelte (md:hidden sticky backdrop-blur-xl)
  BottomTabBar.svelte (md:hidden glassmorphism, safe-area-inset-bottom)
  MobileDrawer.svelte (backdrop + collective switcher + settings + logout)
  (app)/+layout.svelte — CRITICAL FIX: flex flex-col md:flex-row. Was
    horizontal-only, which pushed masthead off-screen on mobile. UndoToast
    mounted globally here.

5.2 Masthead + responsive padding
  ScreenMasthead.svelte — label (13px uppercase) + title (26px/32px).
  Applied to /lists, /tasks, /notes, /search. px-8 → px-4 md:px-6 everywhere.
  messages/{en,es}.json: masthead_{lists,tasks,notes,search}_label +
    masthead_search_title, undo, undo_deleted_item, list_item_delete_aria.

5.4 Red-zone swipe-delete on /lists/[id]
  SWIPE_MAX=96 (red zone rest width), SWIPE_THRESHOLD=48 (latch-open),
  SWIPE_COMMIT_THRESHOLD=200 (full-swipe commit). Red zone button exposes
  aria-label="Delete item"/"Eliminar producto". handleDelete uses
  scheduleUndoable — optimistic local removal, 4-s TTL, commit to Supabase
  on timeout or restore from snapshot on Undo.

5.5 UndoQueue + UndoToast
  src/lib/sync/undoQueue.ts — writable store + Map-backed actions with TTL.
    __flushUndoQueueForTests + __pendingUndoCount for Vitest.
  src/lib/components/UndoToast.svelte — renders latest undoable, offset
    above bottom tab bar (bottom: calc(env(safe-area-inset-bottom) + 4.5rem)),
    Undo button.

5.6 Scrollable frequency chips
  ItemSuggestions.svelte — mobile: flex overflow-x-auto with
    [scrollbar-width:none] [&::-webkit-scrollbar]:hidden; desktop: wraps.
    Chips shrink-0 so they don't compress.

5.7 Shell polish on tasks/notes/search
  Search group headers now carry "{n} MATCH / MATCHES" badges.
  Grid in /lists goes 2-col → 3-col at md+.
  Sticky create input offset for the bottom tab bar on mobile.

5.Z Verification
  just test-all → 224 verdes, 4 skipped
    34 pgTAP (unchanged)
    140 Vitest integration + 2 skipped presence (unchanged)
    11 Vitest unit (was 6, +5 undoQueue)
    39 Playwright + 2 skipped swipe-touch (was 31, +8 new mobile tests)

Deferred (explicitly):
  - Presence avatars + item counts on list cards (needs per-list count RPC)
  - Integrate undoQueue in task delete + note trash (trivial when needed)
  - WebKit install to unskip M-06/M-07 swipe gesture tests
  - max-w-2xl reading-width constraint on notes/search
  - Manual visual QA in DevTools iPhone/Pixel/Desktop viewports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:09:17 +02:00

58 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* M-series (mobile shell) — Fase 5.1
*
* The app shell must switch between desktop (sidebar) and mobile (top bar +
* bottom tab bar + drawer) based on viewport. All four tests log in as Borja
* (member of the seed collective) so the collective-aware chrome is hydrated.
*/
import { test, expect, devices } from '@playwright/test';
import { USERS } from '../fixtures/users.js';
import { loginAs } from '../fixtures/login.js';
const MOBILE = devices['iPhone 13'].viewport; // 390 × 844
const DESKTOP = { width: 1280, height: 800 };
test.describe('Mobile shell — layout switches at md breakpoint', () => {
test('M-01: at mobile viewport the desktop sidebar is not visible', async ({ page }) => {
await page.setViewportSize(MOBILE);
await loginAs(page, USERS.borja);
await page.goto('/lists');
await expect(page.getByTestId('desktop-sidebar')).toBeHidden({ timeout: 10_000 });
});
test('M-02: at mobile viewport the bottom tab bar is visible with 4 nav items', async ({ page }) => {
await page.setViewportSize(MOBILE);
await loginAs(page, USERS.borja);
await page.goto('/lists');
const tabBar = page.getByTestId('bottom-tab-bar');
await expect(tabBar).toBeVisible({ timeout: 10_000 });
await expect(tabBar.getByRole('link', { name: /lists|listas/i })).toBeVisible();
await expect(tabBar.getByRole('link', { name: /tasks|tareas/i })).toBeVisible();
await expect(tabBar.getByRole('link', { name: /notes|notas/i })).toBeVisible();
await expect(tabBar.getByRole('link', { name: /search|buscar/i })).toBeVisible();
// The active route (/lists) must have aria-current="page"
await expect(
tabBar.getByRole('link', { name: /lists|listas/i })
).toHaveAttribute('aria-current', 'page');
});
test('M-03: tapping the hamburger opens the drawer with the collective switcher', async ({ page }) => {
await page.setViewportSize(MOBILE);
await loginAs(page, USERS.borja);
await page.goto('/lists');
await page.getByTestId('mobile-hamburger').click();
const drawer = page.getByTestId('mobile-drawer');
await expect(drawer).toBeVisible({ timeout: 5_000 });
await expect(drawer.getByText(/Casa García-López/)).toBeVisible();
});
test('M-04: at desktop viewport the sidebar is visible and the bottom bar is hidden', async ({ page }) => {
await page.setViewportSize(DESKTOP);
await loginAs(page, USERS.borja);
await page.goto('/lists');
await expect(page.getByTestId('desktop-sidebar')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('bottom-tab-bar')).toBeHidden();
});
});