On /lists/[id], /tasks/[id], /notes/[id], /lists/[id]/session the global
MobileTopBar (hamburger + collective + avatar) is hidden on mobile. The
route's own contextual header (back + title + actions) becomes the only
top chrome and is now sticky with glassmorphism, matching the look of
the MobileTopBar it replaces. BottomTabBar stays visible so the user can
still hop between sections.
Detection: a regex over the path — /^\/(lists\/[uuid](\/session)?|
tasks\/[uuid]|notes\/[uuid])\/?$/ — in (app)/+layout.svelte.
Headers touched:
- apps/web/src/routes/(app)/lists/[id]/+page.svelte (px-4 on mobile,
px-8 on md+, transparent/no-border on md+ since the masthead
provides its own spacing)
- apps/web/src/routes/(app)/tasks/[id]/+page.svelte
- apps/web/src/routes/(app)/notes/[id]/+page.svelte
Tests
tests/e2e/mobile-shell.test.ts gains M-08: on /lists/bbbb..., the
mobile-hamburger element has count 0 (MobileTopBar removed) while the
detail's back link + BottomTabBar remain visible. All 5 shell tests
green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
69 lines
3.1 KiB
TypeScript
69 lines
3.1 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
|
||
test('M-08: on a detail route the MobileTopBar is hidden but BottomTabBar stays', async ({ page }) => {
|
||
await page.setViewportSize(MOBILE);
|
||
await loginAs(page, USERS.borja);
|
||
await page.goto('/lists/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
|
||
// The global hamburger lives inside MobileTopBar — absence == hidden.
|
||
await expect(page.getByTestId('mobile-hamburger')).toHaveCount(0, { timeout: 5_000 });
|
||
// The detail view still shows a back link + bottom tab bar.
|
||
await expect(page.getByRole('link', { name: /^back$/i }).first()).toBeVisible();
|
||
await expect(page.getByTestId('bottom-tab-bar')).toBeVisible();
|
||
});
|
||
});
|