summaryrefslogtreecommitdiff
path: root/frontend/src/lib/mobileTabbar.test.ts
blob: fce7863b98e7f9162802c414c99e8217e7c18462 (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, 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();
  });
});