mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-09-01 15:33:10 +08:00
test(web): extend three-terminal gate and add breakpoint guardrail
This commit is contained in:
@@ -47,6 +47,29 @@ test.describe('three-terminal gate', () => {
|
||||
await expectNoHorizontalOverflow(page, 'settings');
|
||||
});
|
||||
|
||||
// L4 template sweep: one representative route per page template family
|
||||
// (monitor / list / detail / history), gated on every terminal viewport.
|
||||
const TEMPLATE_ROUTES: Array<{template: string; hash: string}> = [
|
||||
{template: 'monitor', hash: '/#/settings/alarm/overview'},
|
||||
{template: 'list', hash: '/#/device'},
|
||||
{template: 'list', hash: '/#/driver'},
|
||||
{template: 'list', hash: '/#/profile'},
|
||||
{template: 'list', hash: '/#/settings/label'},
|
||||
{template: 'detail', hash: '/#/point_value'},
|
||||
{template: 'detail', hash: '/#/settings/alarm/point'},
|
||||
{template: 'history', hash: '/#/settings/event/history'},
|
||||
{template: 'history', hash: '/#/settings/command/history'}
|
||||
];
|
||||
|
||||
for (const {template, hash} of TEMPLATE_ROUTES) {
|
||||
test(`template ${template} ${hash} keeps zero page-level overflow`, async ({page}) => {
|
||||
await login(page);
|
||||
await page.goto(hash, {waitUntil: 'domcontentloaded'});
|
||||
await waitForAppSettled(page);
|
||||
await expectNoHorizontalOverflow(page, `${template} ${hash}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('shell adapts to the viewport (menu strip vs drawer, aside vs drawer)', async ({page}) => {
|
||||
await login(page);
|
||||
const mobile = isMobileViewport(page);
|
||||
@@ -80,8 +103,7 @@ test.describe('three-terminal gate', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('a11y smoke: visible interactive controls expose accessible names', async ({page}) => {
|
||||
await login(page);
|
||||
test('a11y smoke: visible interactive controls expose accessible names', async ({page}) => { await login(page);
|
||||
await page.goto('/#/settings/user', {waitUntil: 'domcontentloaded'});
|
||||
await waitForAppSettled(page);
|
||||
|
||||
@@ -100,4 +122,26 @@ test.describe('three-terminal gate', () => {
|
||||
|
||||
expect(unlabeled, 'visible controls without an accessible name').toEqual([]);
|
||||
});
|
||||
|
||||
// A3 acceptance: dialogs go (near) full-screen below 768px, so a fixed
|
||||
// 640px dialog width never overflows a mobile viewport. Gated on mobile
|
||||
// only — desktop/tablet keep the centered width contract.
|
||||
test('dialogs fit the mobile viewport below 768px', async ({page}) => {
|
||||
test.skip(!isMobileViewport(page), 'mobile-only dialog shape contract');
|
||||
await login(page);
|
||||
|
||||
await page.goto('/#/settings/label', {waitUntil: 'domcontentloaded'});
|
||||
await waitForAppSettled(page);
|
||||
await page.getByRole('button', {name: /Add|新增/}).first().click();
|
||||
const dialog = page.locator('.el-dialog:visible').last();
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
const overflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth - document.documentElement.clientWidth
|
||||
);
|
||||
expect(overflow, 'open dialog causes page-level horizontal overflow').toBeLessThanOrEqual(0);
|
||||
const dialogWidth = await dialog.boundingBox();
|
||||
const viewportWidth = page.viewportSize()?.width ?? 393;
|
||||
expect(dialogWidth?.width, 'dialog wider than viewport').toBeLessThanOrEqual(viewportWidth);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2016-present the IoT DC3 original author or authors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Axiom A5 guardrail: every @media width breakpoint must reference the
|
||||
// $breakpoint-* token from src/styles/tokens.scss — the single breakpoint
|
||||
// contract of docs/design/frontend-three-terminal-ux.md. Hand-rolled px
|
||||
// thresholds reintroduce desktop-first drift one patch at a time.
|
||||
|
||||
import {readdirSync, readFileSync, statSync} from 'node:fs';
|
||||
import {join, relative} from 'node:path';
|
||||
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const root = process.cwd();
|
||||
const srcDir = join(root, 'src');
|
||||
|
||||
// Media features that are width thresholds (allowed: prefers-*,
|
||||
// prefers-reduced-motion, print, and any non-width query).
|
||||
const WIDTH_MEDIA = /@media[^{]*\((?:max|min)-width\s*:\s*([^)]+)\)/g;
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((entry) => {
|
||||
const path = join(dir, entry);
|
||||
if (statSync(path).isDirectory()) return walk(path);
|
||||
return /\.(vue|scss)$/.test(path) ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function findViolations(): string[] {
|
||||
const violations: string[] = [];
|
||||
for (const path of walk(srcDir)) {
|
||||
const content = readFileSync(path, 'utf8');
|
||||
for (const match of content.matchAll(WIDTH_MEDIA)) {
|
||||
const value = match[1].trim();
|
||||
if (!value.startsWith('$breakpoint-')) {
|
||||
const line = content.slice(0, match.index).split('\n').length;
|
||||
violations.push(`${relative(root, path)}:${line} — @media width "${value}" is not a $breakpoint-* token`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
describe('three-terminal breakpoint contract (A5)', () => {
|
||||
it('all @media width breakpoints use $breakpoint-* tokens', () => {
|
||||
const violations = findViolations();
|
||||
expect(violations, violations.join('\n')).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user