diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-18 15:49:44 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-18 15:49:44 +0800 |
| commit | b7108468742fb0169c23153222f6f5bb96d20cbe (patch) | |
| tree | accc17e7a9c5e9ecf2fa294b0acf16e0ab6e1a72 | |
| parent | a7aab01f051487327e5ce52b6a8905208c546e8c (diff) | |
| download | strategy-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.md | 64 | ||||
| -rw-r--r-- | frontend/scripts/ui-auth-shots.mjs | 30 | ||||
| -rw-r--r-- | frontend/src/lib/metricRow.test.ts | 2 | ||||
| -rw-r--r-- | frontend/src/lib/round2DashboardIntegration.test.ts | 2 |
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('已完成'); |
