feat(mobile): side-nav settings + scroll drawer + aide refondue settings-style (v7.59.0)
- Sidebar mobile: display:block du breakpoint ≤768px cassait flex:1 de .sidebar-scroll → molette/touch incapables de scroller le drawer. Flex restauré. Gate: wheel scroll scrollTop > 0. - Settings mobile: nav horizontale remplacée par une side-navigation gauche drawer (hamburger .settings-menu-btn + backdrop, fermeture auto au choix). Squelette panneau extrait vers static/css/settings.css (partagé). - /help refaite au complet: template help.html (13 sections couvrant toute l'app — éditeur, databases/vues, tâches, workspaces, collab, agent IA, API v2, PWA, SSO, raccourcis, tips) avec le même panneau 2 colonnes + drawer mobile; route réduite à un render; help.js (Alpine.data helpInit). E2E mobile_regression 6/6 + smoke 2/2 · pytest app+pwa 230/230
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
// Mobile regression: hamburger opens the sidebar drawer, settings page fits viewport.
|
||||
// Mobile regression: hamburger drawer, wheel scroll, settings side-nav, help page.
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080';
|
||||
const USER = process.env.FD_USER || '[email protected]';
|
||||
const PASS = process.env.FD_PASS || 'e2e-pass-123';
|
||||
|
||||
async function login(page) {
|
||||
// One login for the whole file (app rate limiter: 60 req/min per IP) — reused
|
||||
// as storageState by every test.
|
||||
let STATE = null;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
|
||||
await page.fill('#email', USER);
|
||||
await page.fill('#password', PASS);
|
||||
@@ -19,6 +24,12 @@ async function login(page) {
|
||||
await page.click('.btn-primary');
|
||||
await page.waitForURL('**/workspaces', { timeout: 10000 });
|
||||
}
|
||||
STATE = await ctx.storageState();
|
||||
await ctx.close();
|
||||
});
|
||||
async function login(page) {
|
||||
// Session reused from the single beforeAll login (rate limiter: 60 req/min/IP)
|
||||
await page.context().addCookies(STATE.cookies);
|
||||
}
|
||||
|
||||
test.describe('mobile', () => {
|
||||
@@ -49,6 +60,19 @@ test.describe('mobile', () => {
|
||||
await expect(sidebar).not.toHaveClass(/mobile-open/, { timeout: 5000 });
|
||||
});
|
||||
|
||||
test('mobile sidebar drawer scrolls with mouse wheel', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('.hamburger-btn').click();
|
||||
await page.waitForSelector('#sidebar.mobile-open');
|
||||
const sc = page.locator('.sidebar-scroll');
|
||||
await expect.poll(async () => sc.evaluate((el) => el.scrollHeight > el.clientHeight), { timeout: 3000 }).toBe(true);
|
||||
const box = await sc.boundingBox();
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + Math.min(box.height / 2, 400));
|
||||
await page.mouse.wheel(0, 300);
|
||||
await expect.poll(async () => sc.evaluate((el) => el.scrollTop), { timeout: 3000 }).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('settings page fits mobile viewport', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' });
|
||||
@@ -59,16 +83,81 @@ test.describe('mobile', () => {
|
||||
expect(box.width).toBeLessThanOrEqual(390);
|
||||
expect(box.height).toBeLessThanOrEqual(844);
|
||||
|
||||
// Nav becomes horizontal tabs: nav wider than tall
|
||||
const navBox = await page.locator('.settings-nav').boundingBox();
|
||||
expect(navBox.width).toBeGreaterThan(navBox.height);
|
||||
|
||||
// Tapping a nav item still switches sections
|
||||
await page.locator('.settings-nav-item', { hasText: 'Tags' }).click();
|
||||
await expect(page.locator('h2', { hasText: 'Tags' }).first()).toBeVisible();
|
||||
|
||||
// No horizontal page overflow
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
expect(overflow).toBeLessThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('settings mobile nav is a hamburger side drawer', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' });
|
||||
const burger = page.locator('.settings-menu-btn');
|
||||
await expect(burger).toBeVisible();
|
||||
const nav = page.locator('.settings-nav');
|
||||
const before = await nav.boundingBox();
|
||||
expect(before.x + before.width).toBeLessThanOrEqual(1);
|
||||
|
||||
await burger.click();
|
||||
await expect(nav).toHaveClass(/nav-open/);
|
||||
|
||||
// Nav item switches section AND closes the drawer
|
||||
await page.locator('.settings-nav-item', { hasText: 'Tags' }).click();
|
||||
await expect(nav).not.toHaveClass(/nav-open/);
|
||||
await expect(page.locator('h2', { hasText: 'Tags' }).first()).toBeVisible();
|
||||
|
||||
// Re-open and dismiss via backdrop
|
||||
await burger.click();
|
||||
await expect(nav).toHaveClass(/nav-open/);
|
||||
await page.locator('.settings-nav-backdrop').click({ position: { x: 375, y: 500 } });
|
||||
await expect(nav).not.toHaveClass(/nav-open/);
|
||||
});
|
||||
|
||||
test('help page uses settings-style side navigation', async ({ page }) => {
|
||||
const errs = [];
|
||||
page.on('pageerror', (e) => errs.push(e.message));
|
||||
await login(page);
|
||||
await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Same panel skeleton as settings
|
||||
await expect(page.locator('.settings-panel')).toBeVisible();
|
||||
await expect(page.locator('.settings-menu-btn')).toBeVisible();
|
||||
|
||||
// Mobile: nav drawer behind hamburger
|
||||
const nav = page.locator('.settings-nav');
|
||||
await expect(nav).not.toHaveClass(/nav-open/);
|
||||
await page.locator('.settings-menu-btn').click();
|
||||
await expect(nav).toHaveClass(/nav-open/);
|
||||
await page.locator('.settings-nav-item', { hasText: 'Databases' }).click();
|
||||
await expect(nav).not.toHaveClass(/nav-open/);
|
||||
await expect(page.locator('h2', { hasText: 'Databases & Views' })).toBeVisible();
|
||||
|
||||
// App-wide coverage: all sections exist in the nav
|
||||
const sections = ['Getting Started', 'Pages & Editor', 'Databases & Views', 'Tasks & Dependencies',
|
||||
'Workspaces & Forges', 'Collaboration', 'Library, Trash & Search', 'AI Agent & Automations',
|
||||
'Integrations & API', 'Offline & PWA', 'Accounts & SSO', 'Keyboard Shortcuts', 'Tips'];
|
||||
const labels = (await nav.locator('.settings-nav-item').allInnerTexts()).join(' ');
|
||||
for (const s of sections) expect(labels, s).toContain(s);
|
||||
|
||||
expect(errs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('desktop', () => {
|
||||
test.use({ viewport: { width: 1440, height: 900 } });
|
||||
|
||||
test('settings and help keep the 2-column layout on desktop', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.settings-menu-btn')).toBeHidden();
|
||||
const nav = page.locator('.settings-nav');
|
||||
await expect(nav).toBeVisible();
|
||||
const navBox = await nav.boundingBox();
|
||||
expect(navBox.height).toBeGreaterThan(navBox.width);
|
||||
|
||||
await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.settings-menu-btn')).toBeHidden();
|
||||
await page.locator('.settings-nav-item', { hasText: 'SSO' }).click();
|
||||
await expect(page.locator('h2', { hasText: 'Accounts & SSO' })).toBeVisible();
|
||||
await expect(page.locator('.settings-content')).toContainText('IndexedDB');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const { test } = require('@playwright/test');
|
||||
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
test('diagnose mobile sidebar wheel scroll', async ({ page }) => {
|
||||
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
|
||||
await page.fill('#email', process.env.FD_USER || '[email protected]');
|
||||
await page.fill('#password', process.env.FD_PASS || 'e2e-pass-123');
|
||||
await page.click('.btn-primary');
|
||||
await page.waitForURL('**/workspaces', { timeout: 10000 }).catch(() => {});
|
||||
await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => document.querySelector('.hamburger-btn'));
|
||||
await page.locator('.hamburger-btn').click();
|
||||
await page.waitForSelector('#sidebar.mobile-open');
|
||||
|
||||
const info = await page.evaluate(() => {
|
||||
const sb = document.getElementById('sidebar');
|
||||
const sc = document.querySelector('.sidebar-scroll');
|
||||
const cs = getComputedStyle(sb);
|
||||
const ccs = sc ? getComputedStyle(sc) : null;
|
||||
return {
|
||||
sidebar: { display: cs.display, height: cs.height, overflow: cs.overflow, sh: sb.scrollHeight, ch: sb.clientHeight },
|
||||
scroll: sc ? { top: sc.scrollTop, sh: sc.scrollHeight, ch: sc.clientHeight, oy: ccs.overflowY, flex: ccs.flex, rect: JSON.stringify(sc.getBoundingClientRect()) } : null,
|
||||
wheelTarget: (() => { const el = document.elementFromPoint(150, 400); return el ? el.className : 'none'; })(),
|
||||
};
|
||||
});
|
||||
console.log('DIAG:', JSON.stringify(info, null, 1));
|
||||
|
||||
await page.mouse.move(150, 400);
|
||||
await page.mouse.wheel(0, 300);
|
||||
await page.waitForTimeout(300);
|
||||
const after = await page.evaluate(() => {
|
||||
const sc = document.querySelector('.sidebar-scroll');
|
||||
return { scTop: sc.scrollTop, scSH: sc.scrollHeight, scCH: sc.clientHeight, bodyTop: document.scrollingElement.scrollTop };
|
||||
});
|
||||
console.log('AFTER WHEEL:', JSON.stringify(after));
|
||||
});
|
||||
Reference in New Issue
Block a user