summaryrefslogtreecommitdiff
path: root/frontend/src/lib/mobileCss.test.ts
blob: af7251f8c5175d96b72f44dad560eb9c5aa3f0e8 (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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// @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.
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

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');
  });

  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');
  });
});

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\)/);
  });

  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/);
  });

  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('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 {');
  });
});

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