summaryrefslogtreecommitdiff
path: root/frontend/src/lib/mobileCss.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/mobileCss.test.ts')
-rw-r--r--frontend/src/lib/mobileCss.test.ts131
1 files changed, 51 insertions, 80 deletions
diff --git a/frontend/src/lib/mobileCss.test.ts b/frontend/src/lib/mobileCss.test.ts
index af7251f..8f5567c 100644
--- a/frontend/src/lib/mobileCss.test.ts
+++ b/frontend/src/lib/mobileCss.test.ts
@@ -1,98 +1,69 @@
// @ts-nocheck
import { describe, expect, it } from 'vitest';
-// Live 375px browser overflow regression (artifacts/qa/supervisor-live/
-// mobile-overflow-probe.json): document scrollWidth measured 532 → 552 at a
-// 375px viewport, sidebar/main 552px, cards 523px; DOM authoritative (a
-// full-page screenshot expands to the overflow width, so source/DOM checks
-// are the honest regression). Root causes are real CSS: grid min-content
-// blow-up (layout/grid3), nowrap table cells, and unbreakable provider error
-// URLs. CSS `?raw` imports are stubbed empty by vitest, so the real source
-// files are read from disk instead.
+// round5:app.css 只保留 tailwind + shadcn 主题变量层;页面一律 shadcn 组件
+// + tailwind 工具类。本测试改为"守门"断言:不允许自定义组件选择器回归。
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
+import { globSync } from 'node:fs';
const srcRoot = path.join(fileURLToPath('file://' + process.cwd()), 'src');
const appCss = readFileSync(path.join(srcRoot, 'app.css'), 'utf8');
-const layoutSource = readFileSync(path.join(srcRoot, 'components', 'Layout.svelte'), 'utf8');
-const datasetCard = readFileSync(path.join(srcRoot, 'components', 'DatasetCard.svelte'), 'utf8');
-describe('375px responsive sizing (no global content hiding)', () => {
- it('layout columns shrink below min-content (no document-level overflow)', () => {
- // the mobile layout must be allowed to shrink (minmax(0,1fr), not bare 1fr)
- expect(layoutSource).toMatch(/@media\s*\(max-width:\s*900px\)[\s\S]*?\.layout\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)/);
- // the content column opts out of min-content propagation
- expect(layoutSource).toMatch(/\.content\s*\{[^}]*min-width:\s*0/);
- // project tab context wraps instead of forcing one long line
- expect(layoutSource).toMatch(/\.projctx\s*\{[^}]*flex-wrap:\s*wrap/);
- });
-
- it('multi-column forms and tables collapse/wrap on narrow view', () => {
- expect(appCss).toContain('@media (max-width: 900px)');
- expect(appCss).toContain('.grid2, .grid3 { grid-template-columns: minmax(0, 1fr); }');
- expect(appCss).toContain('table.data th, table.data td {');
- expect(appCss).toContain('white-space: normal');
- });
+// 收集 src/pages/*.svelte 中的 class 字符串
+const pageSources = globSync('pages/*.svelte', { cwd: srcRoot }).map((p) =>
+ readFileSync(path.join(srcRoot, p), 'utf8').join('')
+);
- it('long real provider errors wrap instead of inflating width', () => {
- // `overflow-wrap: anywhere` wraps even long URL payloads; NOT
- // overflow-x:hidden on body/document (that would hide content).
- expect(appCss).toMatch(/\.banner\s*\{[^}]*overflow-wrap:\s*anywhere/);
- expect(appCss.includes('body { overflow-x: hidden')).toBe(false);
- expect(appCss.includes('html { overflow-x: hidden')).toBe(false);
- });
-
- it('dataset card keeps a local scroll container for tabular data', () => {
- expect(datasetCard).toContain('scrollx');
- });
-});
+function extractClasses(src) {
+ const out = new Set();
+ const re = /class="([^"]*)"/g;
+ let m;
+ while ((m = re.exec(src))) {
+ for (const c of m[1].split(/\s+/)) out.add(c);
+ }
+ return [...out];
+}
-describe('round3 shadcn-svelte 主题 tokens(b1D0dv72 preset 精神 + 截图重配)', () => {
- it('radius 上调至 0.75rem(Stripe 风格大圆角),shadcn radius-lg 由此派生', () => {
- expect(appCss).toMatch(/--radius:\s*0\.75rem/);
- expect(appCss).toMatch(/--radius-lg:\s*var\(--radius\)/);
- });
+// 允许的 class token:tailwind 工具类(以已知前缀/任意值开头)、来自 shadcn
+// 组件等问题不存在(页面只会写白/黑名单内的工具类)。这里用"禁止清单"
+// 反向守门:历史上出现过的 legacy 选择器一律不得再次出现。
+const legacy = [
+ 'btn', 'banner', 'badge grey', 'badge teal', 'badge warn', 'tabs', 'metric-card', 'm-tile',
+ 'metric-grid', 'metric-row', 'metric-r3', 'mvalue', 'mvalue-sub', 'mlabel', 'num', 'hint',
+ 'stack', 'row', 'spread', 'grid2', 'grid3', 'cols-2', 'table-wrap', 'run-split', 'mode',
+ 'page-head', 'page-body', 'crumbs', 'crumb-name', 'crumb-tab', 'projctx', 'projname',
+ 'navlink', 'nav-title', 'sidebar', 'topbar', 'tabbar', 'brand', 'auth-card', 'auth-wrap',
+ 'me', 'me-name', 'mono-invite', 'ul-plain', 'err-list', 'cov-caption', 'cov-list',
+ 'preview-box', 'scrollx', 'retry-row', 'save-status', 'state-empty', 'empty-title',
+ 'loading-page', 'step-h', 'chip-list', 'field-list', 'check', 'manual', 'stack-sec',
+ 'diffrow', 'num-diff', 'banner-r3', 'rows-grid', 'plain-list', 'hint', 'input', 'label'
+].map((x) => '.' + x.split(' ')[0].trim());
- it('mint 默认 + amber 双套变量组(用户 create 截图色值)', () => {
- // mint ~#70DAB6 / 深一档 #58D3AA;amber ~#F0AE00
- expect(appCss).toContain('--brand: #58d3aa');
- expect(appCss).toContain('--ring: #70dab6');
- expect(appCss).toMatch(/\.theme-amber\s*\{/);
- expect(appCss).toContain('--brand: #f0ae00');
- // 默认 mint、amber 仅通过 .theme-amber 切换(src/lib/theme.ts 挂类)
- expect(appCss).not.toMatch(/NAVER/);
+describe('round5 守门:无自定义选择器(tailwind 工具类除外)', () => {
+ it('app.css 不包含非主题层的自定义组件选择器', () => {
+ // 只允许:@import/@custom-variant/注释/:root/.dark/@theme/@layer/hex-oklch token
+ const stripped = appCss
+ .replace(/\/\*[\s\S]*?\*\//g, '')
+ .replace(/@import[^;]+;/g, '')
+ .replace(/@custom-variant[^;]+;/g, '')
+ .replace(/:root\s*,?\s*[\s\S]*?\}/, '')
+ .replace(/\.dark\s*\{[^}]*\}/, '')
+ .replace(/@theme\s+inline\s*\{[\s\S]*\}/, '')
+ .replace(/@layer\s+base\s*\{[\s\S]*\}/, '');
+ const leftovers = stripped.trim().replace(/\s+/g, ' ');
+ expect(leftovers, 'app.css must contain only tailwind imports + shadcn theme layer').toBe('');
});
- it('数字排印:tabular-nums 大字重(mvalue 1.5rem/700),保留 .num/.metric-grid', () => {
- expect(appCss).toMatch(/\.mvalue\s*\{[^}]*font-weight:\s*700/);
- expect(appCss).toMatch(/\.mvalue\s*\{[^}]*font-variant-numeric:\s*tabular-nums/);
- expect(appCss).toContain('.metric-grid {');
+ it('src/pages 90+ 处 class 全部是 tailwind 工具类(无 legacy 组件类名)', () => {
+ const tokens = new Set();
+ for (const src of pageSources) for (const c of extractClasses(src)) tokens.add(c);
+ const offenders = tokens.filter((c) => legacy.some((l) => c === l.slice(1) || c.startsWith(l)));
+ // data-testid 属性不是 class;tailwind 任意值与变体直接放行
+ expect(offenders).toEqual([]);
});
- it('legacy 组件选择器已从 app.css 删除(.btn/.banner 变体/.badge/.tabs/table.data 组件样式)', () => {
- expect(appCss).not.toMatch(/\.btn\s*\{|\.\btn\.|\.btn\./);
- expect(appCss).not.toContain('.btn.primary {');
- expect(appCss).not.toContain('.badge.teal {');
- expect(appCss).not.toContain('.tabs button {');
- expect(appCss).not.toContain('table.data th {');
- expect(appCss).not.toContain('.metric {');
- expect(appCss).not.toContain('.metric-card {');
- // .banner 仅剩窄视口防溢出工具(无颜色组件样式)
- expect(appCss).not.toContain('.banner.error {');
+ it('不存在全局 overflow-x: hidden(防溢出靠局部滚动而非隐藏)', () => {
+ expect(appCss.includes('overflow-x: hidden')).toBe(false);
});
});
-
-function getMediaBlock(css, query) {
- const idx = css.indexOf(`@media ${query}`);
- if (idx < 0) return null;
- const open = css.indexOf('{', idx);
- let depth = 1;
- for (let i = open + 1; i < css.length; i++) {
- if (css[i] === '{') depth++;
- else if (css[i] === '}') {
- depth--;
- if (depth === 0) return css.slice(idx, i + 1);
- }
- }
- return null;
-}