summaryrefslogtreecommitdiff
path: root/frontend/src/lib/mobileCss.test.ts
blob: 33e064cf94a1ed717b73f6108f9f95618e8a121d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// @ts-nocheck
import { describe, expect, it } from 'vitest';
// 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');

// 收集 src/pages/*.svelte 中的 class 字符串
const pageSources = globSync('pages/*.svelte', { cwd: srcRoot }).map((p) =>
  readFileSync(path.join(srcRoot, p), 'utf8')
);

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];
}

// 允许的 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());

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('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('不存在全局 overflow-x: hidden(防溢出靠局部滚动而非隐藏)', () => {
    expect(appCss.includes('overflow-x: hidden')).toBe(false);
  });
});