summaryrefslogtreecommitdiff
path: root/frontend/src/lib/mobileTabbar.test.ts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 09:50:57 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 09:50:57 +0800
commit4390ca3472b8a4a201835e358ad0ad2065e8b7c6 (patch)
tree6fcd197f97e75128c056b7c97b6208c3592d556b /frontend/src/lib/mobileTabbar.test.ts
parente6ff0f697160ce152b363be97ecbeee13792cf6f (diff)
downloadstrategy-lab-4390ca3472b8a4a201835e358ad0ad2065e8b7c6.tar.gz
feat(frontend): 375px 底部 Tab Bar、窄标题栏与项目面包屑
[变更性质] 新增移动端导航形态,前端重设计第二轮之二。 [新增功能] ≤767px 呈现底部 Tab Bar(projects/datasets/runs/account 徽标+标签+aria-current)与窄标题栏(品牌/面包屑/账户),≥768px 原桌面布局不变;ProjectPage 顶部新增"我的项目/项目名/功能页"面包屑。 [实现方案] matchMedia('(max-width: 767px)') 反应式驱动的 (Safari <14 兼容addListener 回退),tests 以 matchMedia stub 驱动真实渲染断言;保留 900px 既有断点结构以兼容 mobileCss.test.ts 正则。 [影响范围] Layout.svelte、ProjectPage.svelte;新增 mobileTabbar.test.ts。
Diffstat (limited to 'frontend/src/lib/mobileTabbar.test.ts')
-rw-r--r--frontend/src/lib/mobileTabbar.test.ts69
1 files changed, 69 insertions, 0 deletions
diff --git a/frontend/src/lib/mobileTabbar.test.ts b/frontend/src/lib/mobileTabbar.test.ts
new file mode 100644
index 0000000..fce7863
--- /dev/null
+++ b/frontend/src/lib/mobileTabbar.test.ts
@@ -0,0 +1,69 @@
+// @ts-nocheck
+import { describe, expect, it, vi, afterEach } from 'vitest';
+import { mount, unmount } from 'svelte';
+import Layout from '../components/Layout.svelte';
+import { session } from '../lib/session.svelte';
+
+// Leader round2: 375 宽度下改为底部 Tab Bar(我的项目/数据集/实验记录/我的),
+// 选中态青绿强调色;侧栏只在 ≥768px 显示。
+// 真·渲染断言(不得跳过):
+// - phone 模式(matchMedia max-width:767px matches:true)Tab Bar 渲染 4 个 tab
+// - 非 phone 模式(matches:false)Tab Bar 不渲染
+// 匹配由 matchMedia stub 驱动,jsdom 无布局。
+
+const USER = { id: 'u1', name: '测试用户', role: 'user', session_id: 's1' };
+
+function mq(phones) {
+ return (q) => ({ matches: /max-width:\s*767px/.test(q) ? phones : false, media: q });
+}
+
+function mountLayout(routeName, phoneMode) {
+ vi.stubGlobal('matchMedia', vi.fn((q) => ({ matches: /max-width:\s*767px/.test(q) ? phoneMode : false, media: q, addEventListener: () => {}, removeEventListener: () => {} })));
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const inst = mount(Layout, {
+ target: host,
+ props: { route: { name: routeName, params: {}, query: new URLSearchParams() } }
+ });
+ return { host, inst };
+}
+
+afterEach(() => vi.unstubAllGlobals());
+
+describe('Layout bottom Tab Bar (mobile navigation redo)', () => {
+ it('phone mode (≤767px): renders a bottom tab bar with 4 tabs and appends nav labels', async () => {
+ const { host, inst } = mountLayout('projects', true);
+ await new Promise((r) => setTimeout(r, 50));
+ const bar = host.querySelector('[data-testid="mobile-tabbar"]');
+ expect(bar, 'bottom tab bar must really render in phone mode').toBeTruthy();
+ const tabs = [...bar.querySelectorAll('[data-tab]')].map(a => a.dataset.tab);
+ expect(tabs).toEqual(['projects', 'datasets', 'runs', 'account']);
+ // 图标 + 文字:每个 tab 有 svg 图标与文字标签
+ for (const tab of bar.querySelectorAll('[data-tab]')) {
+ expect(tab.querySelector('svg'), `${tab.dataset.tab} needs an icon svg`).toBeTruthy();
+ expect((tab.textContent || '').trim().length).toBeGreaterThan(0);
+ }
+ // 选中态标记由 aria-current 承载(青绿强调色为其样式责任)
+ expect(bar.querySelector('[data-tab="projects"][aria-current="page"]')).toBeTruthy();
+ unmount(inst);
+ host.remove();
+ });
+
+ it('selects the tab matching the current route (runs → runs)', async () => {
+ const { host, inst } = mountLayout('runs', true);
+ await new Promise((r) => setTimeout(r, 50));
+ const bar = host.querySelector('[data-testid="mobile-tabbar"]');
+ expect(bar.querySelector('[data-tab="runs"][aria-current="page"]')).toBeTruthy();
+ expect(bar.querySelector('[data-tab="projects"][aria-current="page"]')).toBeNull();
+ unmount(inst);
+ host.remove();
+ });
+
+ it('non-phone mode: tab bar does not render', async () => {
+ const { host, inst } = mountLayout('projects', false);
+ await new Promise((r) => setTimeout(r, 50));
+ expect(host.querySelector('[data-testid="mobile-tabbar"]')).toBeNull();
+ unmount(inst);
+ host.remove();
+ });
+});