summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 15:49:44 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 15:49:44 +0800
commitb7108468742fb0169c23153222f6f5bb96d20cbe (patch)
treeaccc17e7a9c5e9ecf2fa294b0acf16e0ab6e1a72
parenta7aab01f051487327e5ce52b6a8905208c546e8c (diff)
downloadstrategy-lab-b7108468742fb0169c23153222f6f5bb96d20cbe.tar.gz
[变更性质] test(frontend)
[新增功能] probe/测试同步 round5:metric-value/capital-reporting 选择锚点、18/18 三档零溢出、docs 迁移矩阵 [实现方案] resultsAccounting/metricRow/round2Dashboard 选择器迁移;probe 断言 radiusOk/capital、shadcn 挂载点 [影响范围] scripts/ui-auth-shots.mjs、src/lib/*.test.ts、docs/ui-redesign-round5.md、artifacts/ui-shadcn-round5
-rw-r--r--docs/ui-redesign-round5.md64
-rw-r--r--frontend/scripts/ui-auth-shots.mjs30
-rw-r--r--frontend/src/lib/metricRow.test.ts2
-rw-r--r--frontend/src/lib/round2DashboardIntegration.test.ts2
4 files changed, 85 insertions, 13 deletions
diff --git a/docs/ui-redesign-round5.md b/docs/ui-redesign-round5.md
new file mode 100644
index 0000000..9522f1d
--- /dev/null
+++ b/docs/ui-redesign-round5.md
@@ -0,0 +1,64 @@
+# UI 迁移 round5 — 全量 shadcn-svelte(preset b6t6C6CTw,零自定义样式)
+
+round4 残留自定义样式被判不合格,round5 全量重来:
+
+1. `src/app.css` 重置为 **仅 tailwind 导入 + shadcn 主题变量层**
+ (preset `b6t6C6CTw`,baseColor mist,style mira,字体 Inter Variable;
+ `:root/.dark/@theme inline/@layer base` 之外不留任何自定义选择器)。
+ round3 的自定义 token 层(mint/amber/brand-*/legacy alias/布局原语/.num/.mvalue/.hint 等)全部删除。
+2. `pnpm dlx shadcn-svelte init --preset b6t6C6CTw` 重新初始化;组件按需全量 add:
+ alert badge button card chart input label select separator tabs table(+此前已有的 9 个)。
+3. 12 个页面(src/pages/*)所有 UI 元素改用 shadcn 组件;卡片=Card、按钮=Button、
+ 表格=Table、Tab=Tabs、表单=Select/Input/Label、状态=Badge、图表=Chart(layerchart,
+ round1 契约保持);手机 375 档底部 Tab Bar 改用 **Tabs 组件**(Tabs.Root/List/Trigger
+ + data-testid="mobile-tabbar",aria-current + route 驱动 activeTab,与 Layout 主导航同源)。
+4. 投入口径 reporting 保留:`data-testid="capital-reporting"` 三张卡
+ (累计投入 / 最终持仓估值 / 投入回报率,锚点 run 26e9c9ab:12029.09 / 10846.00 / -9.84%),
+ 数值节流通过 `[data-testid="metric-value"]` 选择器断言。
+
+## 迁移矩阵(旧类 → 新组件/tailwind)
+
+| 旧类(round3 残留) | round5 迁移 |
+| --- | --- |
+| `.layout/.sidebar/.content`(css grid 全局) | Layout 布局改 tailwind:`grid md:grid-cols-[208px_1fr]`、`bg-sidebar`、`min-w-0`、断点 `max-md` 面包屑 |
+| `.tabbar .item`(底部导航) | `Tabs.Root/List/Trigger`(variant line + tailwind flex),data-tab 保留 |
+| `.topbar/.brand/.navlink/.nav-title/.projctx/.divider/.me/.me-name` | tailwind: `sticky top-0 z-30 bg-sidebar/90 backdrop-blur`、`rounded-md px-2 py-1.5 text-sm`、`bg-primary text-primary-foreground`、`Separator` |
+| `Page.svelte .page-head/.page-body(.wide)` | tailwind:`max-w-3xl` / 无宽度限制(wide) |
+| `.metric-row/.metric-r3/.mvalue/.mlabel/.run-split`(ProjectMetricRow) | `Card.Root` + `grid grid-cols-2 md:grid-cols-4 gap-2.5` + `text-xs text-muted-foreground` / `text-2xl font-bold tabular-nums`(`[data-testid=metric-value]`) |
+| `.m-tile + .metric-grid`(结果页 metric tiles ×9,含 capital-reporting ×3) | `Card.Root` + `grid md:grid-cols-3 gap-2.5`,tile 共享 `const tile/tileLabel/tileValue` tailwind 组合 |
+| `.hint`(全部页面) | `text-sm text-muted-foreground` |
+| `.num` | `tabular-nums` |
+| `.stack/.row/.spread/.wrap/.grid2/.grid3/.cols-2/.wrap-3/.rows-grid/.run-split` | `flex flex-col gap-3` / `flex items-center gap-2 flex-wrap` / `flex flex-wrap justify-between` / `grid md:grid-cols-[1.4fr_1fr]` 等 tailwind 原语 |
+| `form.card/.manual/.check/.field-list/.step-h`(DatasetWizard) | `Card.Root` + `Grid/Label/Input/Select`(market/type/adjustment 均为 Select 组件)+ foundation checkbox 输入保持原生 + label tailwind |
+| 登录/注册/恢复 `auth-wrap/auth-card/auth-brand/.mark/.sub` | `Card.Root w-[min(400px,100%)] p-8` 居中段 + `bg-primary size-8 rounded-lg` 品牌块 |
+| `<table class="data">`(版本历史/搜索结果) | `Table.Root/…` 组件全替 |
+| `.banner error/warn/info`(DatasetCard/DatasetWizard/Status) | `Alert`(variant 蒋 destructive/default) |
+| `.diff/.legend/.sw/.body/.ln/.code`(DiffView) | tailwind: `bg-emerald-50 text-emerald-900` / `bg-red-50 text-red-900`、`font-mono text-xs break-all`、legend `size-2.5 rounded-sm` |
+| Modal `.overlay/.dialog/.content` | `fixed inset-0 bg-black/40 grid place-items-center` + `Card.Root w-[min(760px,100%)]` |
+| `.loading-page/.spinner` | tailwind `animate-spin`(无双主题依赖) |
+| `.cm-host/.editor-label`(CodeEditor) | 宿主编排 tailwind:`rounded-md border border-border bg-card`;cm theme 变量改 var(--card/--secondary/--foreground/--muted-foreground) |
+| `lib/theme.ts`(mint/amber 切换) | 删除(依赖的自定义 token 层已不存在;shadcn preset 按 CLI 原样输出) |
+| Radius 断言(0.75rem) | 改为 preset 自带 `--radius: 0.625rem`,probe 断言改为"组件圆角 = --radius token 等值" |
+
+## 测试同步
+
+- `mobileCss.test.ts` 重写为守门测试:app.css 不含主题层外任何选择器;
+ src/pages class 全为 tailwind 工具类(无 Legacy 清单);无 body/html overflow-x hidden。
+- `resultsAccounting.test.ts`:tile 选择器改 `[data-slot="card"].gap-1`×9、
+ `td` 文本读取、`[data-testid="metric-value"]`。
+- `metricRow.test.ts / round2DashboardIntegration.test.ts`:`.metric-row .mvalue`
+ → `[data-testid="metric-value"]`。
+
+## 验证
+
+- `svelte-check`:0 error 0 warning
+- `vitest`:80/80(守门 3 条 + 全量回归)
+- `npm run build`:成功(index 1.2MB code-split 提示与 round4 相同)
+- probe `artifacts/ui-shadcn-round5/`:三档 **18/18**、0 overflow、0 pageError、
+ tabbar 375 attached/visible、面包屑齐全、metricCards=4、sparks=2、
+ capital-reporting 三档均检测到、radiusOk=true。
+
+## 已知边界
+
+- CodeMirror 编辑器内部主题(cm-editor theme)为编辑器自身必需的最小样式,非组件 facade。
+- probe 新增锚点:`data-testid="project-link"`(项目行链接)、`nav[aria-label="面包屑"]`。
diff --git a/frontend/scripts/ui-auth-shots.mjs b/frontend/scripts/ui-auth-shots.mjs
index 2db1f1d..abd1f3a 100644
--- a/frontend/scripts/ui-auth-shots.mjs
+++ b/frontend/scripts/ui-auth-shots.mjs
@@ -91,7 +91,7 @@ await authPage.goto('https://fin.somhairle.bid/#/login', { waitUntil: 'domconten
await authPage.fill('#le', cred.email);
await authPage.fill('#lp', cred.password);
await authPage.click('form button:has-text("登录")');
-await authPage.waitForSelector('nav.tabbar, .sidebar', { timeout: 30000 });
+await authPage.waitForSelector('[data-testid="mobile-tabbar"], [aria-label="全局导航"]', { timeout: 30000 });
const cookies = await authContext.cookies('https://fin.somhairle.bid');
const session = cookies.find(function (c) { return c.name === 'sl_session'; });
if (!session) {
@@ -145,14 +145,14 @@ for (const w of widths) {
const read = async function (name) {
const dom = await page.evaluate(function () {
const el = document.querySelector('[data-testid="mobile-tabbar"]');
- const crumb = document.querySelector('nav.crumbs');
+ const crumb = document.querySelector('nav[aria-label="面包屑"]');
return {
hash: location.hash,
overflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
tabbarAttached: !!el,
tabbarVisible: !!el && getComputedStyle(el).display !== 'none',
topbarAttached: !!document.querySelector('[data-testid="mobile-topbar"]'),
- breadcrumbText: document.querySelector('nav.crumbs')?.textContent.replace(/\s+/g, ' ').trim() || null,
+ breadcrumbText: document.querySelector('nav[aria-label="面包屑"]')?.textContent.replace(/\s+/g, ' ').trim() || null,
sparklineCells: document.querySelectorAll('[data-testid="sparkline-cell"]').length,
metricCards: document.querySelectorAll('[data-testid="metric-card"]').length,
cardRadius: (function () {
@@ -161,6 +161,8 @@ for (const w of widths) {
const target = String(getComputedStyle(document.documentElement).getPropertyValue('--radius')).trim();
return { computed: getComputedStyle(r).borderRadius, token: getComputedStyle(document.documentElement).getPropertyValue('--radius') || target };
})(),
+ capitalReporting: !!document.querySelector('[data-testid="capital-reporting"]'),
+ capitalTiles: document.querySelectorAll('[data-testid="capital-reporting"] [data-testid="metric-value"]').length,
cardRadiusOk: (function () {
const r = document.querySelector('[data-slot="card"]');
if (!r) return null;
@@ -169,7 +171,7 @@ for (const w of widths) {
const rootFs = parseFloat(root.fontSize);
const expected = tok * rootFs;
const n = parseFloat(getComputedStyle(r).borderRadius);
- return n >= expected - 0.5 && tok >= 0.749;
+ return n >= expected - 0.5 && tok > 0;
})(),
projects: (window.__probeProjects !== undefined) ? window.__probeProjects : null,
runs: null,
@@ -187,6 +189,7 @@ for (const w of widths) {
' tabbarAttached=' + dom.tabbarAttached + ' tabbarVisible=' + dom.tabbarVisible +
' metricCards=' + dom.metricCards + ' sparks=' + dom.sparklineCells +
' radiusOk=' + dom.cardRadiusOk +
+ ' capital=' + dom.capitalReporting +
(dom.breadcrumbText ? ' crumbs="' + dom.breadcrumbText + '"' : '')
);
if (dom.overflow !== 0 || pageErrors.length !== 0) bad++;
@@ -195,7 +198,7 @@ for (const w of widths) {
// precondition fetch (same-origin, session cookie): counts for truthy assertions
await page.goto(site + '/#/projects', { waitUntil: 'domcontentloaded' });
// wait for app mount (tabbar/side header appear), not a fixed timer
- await page.waitForSelector('[data-testid="mobile-tabbar"], .sidebar', { state: 'attached', timeout: 20000 });
+ await page.waitForSelector('[data-testid="mobile-tabbar"], [aria-label="全局导航"]', { state: 'attached', timeout: 20000 });
const pre = await page.evaluate(async function () {
const get = function (u) { return fetch(u, { credentials: 'same-origin' }).then(function (r) { return r.status === 200 ? r.json() : null; }); };
const me = await get('/api/auth/me').catch(function () { return null; });
@@ -221,24 +224,29 @@ for (const w of widths) {
for (const tab of ['runs', 'datasets']) {
await page.goto(site + '/#/' + tab, { waitUntil: 'domcontentloaded' });
- await page.waitForSelector('[data-testid="mobile-tabbar"], .sidebar', { state: 'attached', timeout: 20000 });
+ await page.waitForSelector('[data-testid="mobile-tabbar"], [aria-label="全局导航"]', { state: 'attached', timeout: 20000 });
await read(tab);
}
// project deep pages: click-through the projects list; requires >=1 project
await page.goto(site + '/#/projects', { waitUntil: 'domcontentloaded' });
- await page.waitForSelector('a.p-name', { timeout: 20000 }).catch(function () {});
- const first = page.locator('a.p-name').first();
+ await page.waitForSelector('a[data-testid="project-link"], a.p-name', { timeout: 20000 }).catch(function () {});
+ const first = page.locator('a[data-testid="project-link"], a.p-name').first();
if ((await first.count()) > 0) {
await first.click();
- await page.waitForSelector('nav.crumbs', { state: 'attached', timeout: 20000 });
+ await page.waitForSelector('nav[aria-label="面包屑"]', { state: 'attached', timeout: 20000 });
const projUrl = new URL(page.url());
const projHash = projUrl.hash;
for (const tab of ['data', 'strategy', 'results']) {
await page.evaluate(function (h) { location.hash = h; }, projHash.replace(/\/(data|strategy|results|backtest|runs)(\?.*)?$/, '/' + tab));
// wait for layout render on the new tab (breadcrumb re-renders)
- await page.waitForSelector('[data-testid="mobile-tabbar"], .sidebar', { state: 'attached', timeout: 20000 });
- await page.waitForSelector('nav.crumbs, .projctx, main', { state: 'attached', timeout: 15000 });
+ await page.waitForSelector('[data-testid="mobile-tabbar"], [aria-label="全局导航"]', { state: 'attached', timeout: 20000 });
+ await page.waitForSelector('nav[aria-label="面包屑"], .projctx, main', { state: 'attached', timeout: 15000 });
+ if (tab === 'results') {
+ await page.waitForSelector('[data-testid="capital-reporting"]', { state: 'attached', timeout: 20000 }).catch(function () {});
+ if (!(await page.locator('[data-testid="capital-reporting"]').count()))
+ await page.getByText('还没有可展示的回测').first().waitFor({ timeout: 10000 }).catch(function () {});
+ }
await read('proj-' + tab);
}
} else {
diff --git a/frontend/src/lib/metricRow.test.ts b/frontend/src/lib/metricRow.test.ts
index a8149bd..ff0dde0 100644
--- a/frontend/src/lib/metricRow.test.ts
+++ b/frontend/src/lib/metricRow.test.ts
@@ -35,7 +35,7 @@ describe('ProjectMetricRow (real render, 4 metric cards)', () => {
expect(screen.getByText('回测')).toBeTruthy();
expect(screen.getByText('AI 用量')).toBeTruthy();
// 数字直接可见:项目卡/数据集卡与回测拆分值
- const values = [...container.querySelectorAll('.metric-row .mvalue')].map((n) => n.textContent!.replace(/\s+/g, ' ').trim());
+ const values = [...container.querySelectorAll('[data-testid="metric-value"]')].map((n) => n.textContent!.replace(/\s+/g, ' ').trim());
expect(values[0]).toBe('3');
expect(values[1]).toBe('2');
expect(values[2]).toContain('运行中');
diff --git a/frontend/src/lib/round2DashboardIntegration.test.ts b/frontend/src/lib/round2DashboardIntegration.test.ts
index 363e1da..c345ca2 100644
--- a/frontend/src/lib/round2DashboardIntegration.test.ts
+++ b/frontend/src/lib/round2DashboardIntegration.test.ts
@@ -38,7 +38,7 @@ describe('ProjectsPage dashboard round2 (metric row + row sparkline, real render
await waitFor(() => expect(vi.mocked(getAIUsage)).toHaveBeenCalled());
const cards = document.querySelectorAll('[data-testid="metric-card"]');
await waitFor(() => expect(cards.length).toBe(4));
- const values = [...document.querySelectorAll('.metric-row .mvalue')].map((n) => n.textContent!.replace(/\s+/g, ' ').trim());
+ const values = [...document.querySelectorAll('[data-testid="metric-value"]')].map((n) => n.textContent!.replace(/\s+/g, ' ').trim());
expect(values[0]).toBe('2');
expect(values[1]).toBe('3');
expect(values[2]).toContain('已完成');