160 lines
6.8 KiB
JavaScript
160 lines
6.8 KiB
JavaScript
/**
|
|
* E2E tests for the Configurations modal on mobile (BUG-071 + #114).
|
|
*
|
|
* Runs only under the `chromium-mobile` Playwright project (viewport ≤ 768px);
|
|
* skipped on the desktop project that the CI job executes — same convention
|
|
* as mobile-editor.spec.js.
|
|
*
|
|
* Covered:
|
|
* - the TOC hamburger (#config-hamburger) is visible and reveals #config-nav
|
|
* as a left slide-over drawer (fixed positioning), hidden by default;
|
|
* - the drawer has a close button (#config-toc-close) and a dimming backdrop
|
|
* (tap outside closes the drawer, not the whole modal);
|
|
* - picking a TOC entry scrolls to the section, marks the link active and
|
|
* collapses the drawer;
|
|
* - the modal is full-screen (100dvh) and interactive controls are ≥44px;
|
|
* - the modal content does not overflow horizontally at 393px.
|
|
*
|
|
* Run:
|
|
* npx playwright test tests/e2e/config-mobile.spec.js --project=chromium-mobile
|
|
* (ObsiGate listening on http://localhost:2029, auth disabled)
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
const MOBILE_MAX_WIDTH = 768;
|
|
const MIN_TOUCH_TARGET = 44;
|
|
|
|
async function boot(page) {
|
|
await page.goto('/');
|
|
await page.waitForSelector('#app:not(.hidden)', { timeout: 15000 });
|
|
await expect(page.locator('#header-menu-btn')).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async function openConfigModal(page) {
|
|
await page.locator('#header-menu-btn').click();
|
|
await page.locator('#config-open-btn').click();
|
|
await expect(page.locator('#config-modal.active')).toBeVisible();
|
|
}
|
|
|
|
test.describe('Configurations modal on mobile (BUG-071 + #114)', () => {
|
|
test('hamburger reveals the TOC as a fixed drawer', async ({ page, viewport }) => {
|
|
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
|
await boot(page);
|
|
await openConfigModal(page);
|
|
|
|
// TOC hidden by default on mobile, hamburger visible.
|
|
await expect(page.locator('#config-hamburger')).toBeVisible();
|
|
await expect(page.locator('#config-nav')).toBeHidden();
|
|
|
|
await page.locator('#config-hamburger').click();
|
|
await expect(page.locator('#config-nav')).toBeVisible();
|
|
|
|
// #114: the drawer is a fixed slide-over, not an inline top block.
|
|
const position = await page.locator('#config-nav').evaluate(
|
|
(el) => window.getComputedStyle(el).position,
|
|
);
|
|
expect(position).toBe('fixed');
|
|
|
|
// The modal carries the drawer-open class (backdrop contract).
|
|
await expect(page.locator('#config-modal')).toHaveClass(/config-toc-open/);
|
|
});
|
|
|
|
test('close button and backdrop dismiss the drawer, not the modal', async ({ page, viewport }) => {
|
|
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
|
await boot(page);
|
|
await openConfigModal(page);
|
|
|
|
// Close button inside the drawer header.
|
|
await page.locator('#config-hamburger').click();
|
|
await expect(page.locator('#config-toc-close')).toBeVisible();
|
|
await page.locator('#config-toc-close').click();
|
|
await expect(page.locator('#config-nav')).toBeHidden();
|
|
await expect(page.locator('#config-modal.active')).toBeVisible();
|
|
|
|
// Backdrop tap closes the drawer first; the modal stays open.
|
|
// Click near the right edge of the modal (outside the left drawer).
|
|
await page.locator('#config-hamburger').click();
|
|
await expect(page.locator('#config-nav')).toBeVisible();
|
|
await page.locator('#config-modal').click({ position: { x: (viewport?.width ?? 393) - 8, y: 200 } });
|
|
await expect(page.locator('#config-nav')).toBeHidden();
|
|
await expect(page.locator('#config-modal.active')).toBeVisible();
|
|
});
|
|
|
|
test('picking a section scrolls to it and collapses the drawer', async ({ page, viewport }) => {
|
|
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
|
await boot(page);
|
|
await openConfigModal(page);
|
|
|
|
await page.locator('#config-hamburger').click();
|
|
const link = page.locator('#config-nav a[href="#cfg-tokens"]');
|
|
await expect(link).toBeVisible();
|
|
await link.click();
|
|
|
|
// Drawer collapses on mobile after selection…
|
|
await expect(page.locator('#config-nav')).toBeHidden();
|
|
// …the link is marked active…
|
|
await expect(link).toHaveClass(/active/);
|
|
// …and the section scrolls into view inside the modal (smooth scroll:
|
|
// poll for the settled position instead of racing the animation).
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const box = await page.locator('#cfg-tokens').boundingBox();
|
|
const modalBox = await page.locator('#config-modal').boundingBox();
|
|
if (!box || !modalBox) return Number.POSITIVE_INFINITY;
|
|
return box.y - (modalBox.y + modalBox.height);
|
|
},
|
|
{ timeout: 8000 },
|
|
)
|
|
.toBeLessThanOrEqual(0);
|
|
});
|
|
|
|
test('modal is full-screen and key controls meet the 44px touch target', async ({ page, viewport }) => {
|
|
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
|
await boot(page);
|
|
await openConfigModal(page);
|
|
|
|
// Full-screen: the modal container fills the viewport.
|
|
const container = page.locator('#config-modal .editor-container');
|
|
const box = await container.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box.width).toBeGreaterThanOrEqual((viewport?.width ?? 0) - 2);
|
|
expect(box.height).toBeGreaterThanOrEqual((viewport?.height ?? 0) - 2);
|
|
|
|
// Header controls (hamburger + close) are ≥44px.
|
|
for (const sel of ['#config-hamburger', '#config-close']) {
|
|
const b = await page.locator(sel).boundingBox();
|
|
expect(b, `${sel} has no bounding box`).not.toBeNull();
|
|
expect(b.height, `${sel} height`).toBeGreaterThanOrEqual(MIN_TOUCH_TARGET);
|
|
expect(b.width, `${sel} width`).toBeGreaterThanOrEqual(MIN_TOUCH_TARGET);
|
|
}
|
|
|
|
// Backend Save row: buttons are ≥44px tall.
|
|
await page.locator('#config-hamburger').click();
|
|
await page.locator('#config-nav a[href="#cfg-backend-settings"]').click();
|
|
await expect(page.locator('#config-nav')).toBeHidden();
|
|
const saveBtn = page.locator('#cfg-save-backend');
|
|
await expect(saveBtn).toBeVisible();
|
|
const saveBox = await saveBtn.boundingBox();
|
|
expect(saveBox).not.toBeNull();
|
|
expect(saveBox.height).toBeGreaterThanOrEqual(MIN_TOUCH_TARGET);
|
|
});
|
|
|
|
test('no horizontal overflow at 393px', async ({ page, viewport }) => {
|
|
test.skip((viewport?.width ?? 0) > MOBILE_MAX_WIDTH, 'Mobile viewport required');
|
|
await boot(page);
|
|
await openConfigModal(page);
|
|
|
|
for (const section of ['#cfg-ai', '#cfg-tokens', '#cfg-webhooks', '#cfg-partages-publics']) {
|
|
await page.locator('#config-hamburger').click();
|
|
await page.locator(`#config-nav a[href="${section}"]`).click();
|
|
}
|
|
const overflow = await page.evaluate(() => {
|
|
const scroller = document.getElementById('config-scroll');
|
|
return scroller.scrollWidth - scroller.clientWidth;
|
|
});
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
});
|
|
});
|