Ctrl K
ComunidadGuías…

frontend/test/board-skeleton.test.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect, it, vi } from 'vitest';
import { boardPageSkeleton, detailSkeletonBody } from '../src/board-skeleton';
import { wireBoardSegments } from '../src/board-controls';

vi.mock('@peach/legacy/core', async importOriginal => ({
  ...await importOriginal<object>(),
  icon: (name: string) => `<svg aria-hidden="true" data-icon="${name}"></svg>`,
}));

describe('Board 页面骨架', () => {
  it.each([
    ['subscriptions', '来源与订阅'], ['downloads', '下载渠道'], ['network', '网络与访问'], ['maintenance', '应用与维护'],
  ])('配置深链 %s 使用对应分区', (key, label) => {
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton(`/settings/${key}`);
    expect(root.querySelector('.ui-board-local-nav [aria-selected="true"]')?.textContent).toBe(label);
    expect(root.querySelector('[aria-label="媒体库与目录"]')).toBeNull();
  });
  it.each(['/stats', '/taste', '/subscriptions', '/settings/library', '/settings/diagnostics', '/tasks', '/duplicates', '/quality-goals', '/playlists', '/settings/maintenance/repairs'])('为 %s 提供单一等待语义', path => {
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton(path);
    expect(root.querySelectorAll('[role="status"]')).toHaveLength(1);
    expect(root.querySelector('[aria-hidden="true"]')).not.toBeNull();
    expect(root.querySelector('[aria-hidden="true"][inert]')).not.toBeNull();
    for (const control of root.querySelectorAll('button, input, a, summary')) {
      expect(control.closest('[inert]')).not.toBeNull();
    }
  });
  it('JAV 新作分区在列表模式下画新作自己的表格骨架,下排有卡片/列表开关、没有文件夹开关', () => {
    const before = location.pathname + location.search;
    history.replaceState(null, '', '/jav?tab=releases');
    localStorage.setItem('peach.catalog-view.v1', 'list');
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton('/jav');
    localStorage.removeItem('peach.catalog-view.v1');
    history.replaceState(null, '', before);
    expect([...root.querySelectorAll('[data-catalog-table] th')].map(node => node.textContent).filter(Boolean))
      .toEqual(['作品', '艺人', '厂牌', '标签', '发行时间', '时长', '来源评分', '操作']);
    expect(root.querySelector('[data-skeleton="jav-discover"]')).toBeNull();
    expect(root.querySelector('[aria-label="馆藏显示方式"] [aria-checked="true"]')?.getAttribute('aria-label')).toBe('列表');
    expect(root.querySelector('[data-folder-mode-toggle]')).toBeNull();
  });
  it('JAV 新作分区骨架按下新作,页头是名册页那种筛选框,下面是两排头像占位与新作自己的排序键', () => {
    const before = location.pathname + location.search;
    history.replaceState(null, '', '/jav?tab=releases&sort=duration&dir=asc');
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton('/jav');
    history.replaceState(null, '', before);
    expect([...root.querySelectorAll('span.whitespace-nowrap.rounded-md')].find(node => node.classList.contains('text-text-primary'))?.textContent).toBe('新作');
    expect(root.querySelector('[data-index-search] input')?.getAttribute('aria-label')).toBe('筛选新作');
    expect(root.querySelector('form')).toBeNull();
    expect(root.querySelectorAll('[data-catalog-tier] [data-catalog-placeholder]').length).toBeGreaterThan(0);
    expect(root.querySelector('[data-entity-tag="external"]')).toBeNull();
    expect([...root.querySelectorAll('[data-entity-sort]')].map(node => node.textContent)).toEqual(['发行时间', '来源评分', '时长', '番号', '口味']);
    expect(root.querySelector('[data-entity-sort][aria-pressed="true"]')?.getAttribute('data-entity-sort')).toBe('duration');
    expect(root.querySelector('[data-entity-batch]')?.getAttribute('aria-label')).toBe('换一批');
  });
  it('统计预留四张指标卡,未知页面交给自身骨架', () => {
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton('/stats');
    expect(root.querySelectorAll('[data-stats-metrics] > div')).toHaveLength(4);
    expect(boardPageSkeleton('/assets/5')).toBe('');
  });
  it.each([
    ['/stats', '[data-stats-chart]', 2],
    ['/tasks', '.activitysection', 3],
    ['/duplicates', '[data-duplicate-group] .duplicate-row', 4],
    ['/quality-goals', '.peach-react .card-grid-cover > li', 6],
    ['/subscriptions', '.peach-react .ui-follow-skeleton-surface .follow-skeleton-author', 3],
    ['/settings/library', '.peach-react .ui-configpage section[aria-label="媒体库与目录"]', 1],
    ['/playlists', '.ui-playlistcards > .playlistcard', 6],
  ] as const)('%s 复用最终内容容器', (path, selector, count) => {
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton(path);
    expect(root.querySelectorAll(selector)).toHaveLength(count);
    expect(root.querySelector('.skeleton-cards')).toBeNull();
  });
  it('详情保留媒体区和资料侧栏,不创建播放器', () => {
    const root = document.createElement('div');
    root.innerHTML = detailSkeletonBody();
    expect(root.querySelector('[data-stage-grid] > [data-stage-media]')).not.toBeNull();
    expect(root.querySelector('[data-stage-grid] > [data-stage-side] > [data-stage-side-content]')).not.toBeNull();
    expect(root.querySelectorAll('video, audio, iframe')).toHaveLength(0);
  });
  const follow = (options: Parameters<typeof boardPageSkeleton>[1] = {}) => {
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton('/subscriptions', options);
    return root;
  };
  it('关注骨架的分类使用 Pills,选中关注列表', () => {
    const root = follow();
    wireBoardSegments(root);
    expect(root.querySelector('.board-segment-thumb')).toBeNull();
    expect(root.querySelector('[data-section-items] [aria-selected=true]')?.textContent).toBe('关注列表');
    const track = root.querySelector('[data-section-items]')!;
    expect([...track.children].map((span) => span.textContent)).toEqual(['关注列表', '添加关注', '标签管理']);
  });
  it('关注骨架选中地址栏 `?tab=` 那一枚页签,列表以外的页签不画关注列表的工具行', () => {
    const tags = follow({ followTab: 'tags' });
    expect(tags.querySelector('[data-section-items] [aria-selected=true]')?.textContent).toBe('标签管理');
    expect(tags.querySelector('.ui-follow-skeleton-toolbar')).toBeNull();
    expect(tags.querySelector('.ui-follow-skeleton-surface')).not.toBeNull();
    expect(follow({ followTab: 'feeds' }).querySelector('[data-section-items] [aria-selected=true]')?.textContent).toBe('关注列表');
    expect(follow({ followTab: 'wants' }).querySelector('[data-section-items] [aria-selected=true]')?.textContent).toBe('关注列表');
    expect(follow({ followTab: 'nope' }).querySelector('[data-section-items] [aria-selected=true]')?.textContent).toBe('关注列表');
  });
  it('关注骨架的工具行按 Board UI 最终变体画:检查全部是主按钮,视图切换是按版式选中的按钮组', () => {
    const toolbar = follow().querySelector('.ui-follow-skeleton-toolbar')!;
    const check = toolbar.querySelector('[data-follow-toolbar-actions] > button')!;
    expect(check.textContent).toBe('检查全部');
    expect(check.classList.contains('bg-button-primary')).toBe(true);
    const group = [...toolbar.querySelectorAll('[data-button-group][role="group"] > button')];
    expect(group.map((button) => button.querySelector('use')?.getAttribute('href'))).toEqual(['#i-layout-grid', '#i-table']);
    expect(group.map((button) => button.getAttribute('aria-pressed'))).toEqual(['true', 'false']);
    expect(group.every((button) => button.classList.contains('bg-button-ghost-background'))).toBe(true);
    expect(toolbar.querySelector('button[aria-haspopup="listbox"]')?.textContent).toBe('排序检查时间');
    expect([...toolbar.querySelectorAll('button')].at(-1)?.textContent).toBe('全部收起');
    const tableGroup = follow({ followLayout: 'table' }).querySelectorAll('[data-button-group] > button');
    expect([...tableGroup].map((button) => button.getAttribute('aria-pressed'))).toEqual(['false', 'true']);
  });
  it('排序框与方向键跟着地址栏上的排序', () => {
    const toolbar = (options: Parameters<typeof boardPageSkeleton>[1]) =>
      follow(options).querySelector('.ui-follow-skeleton-toolbar')!;
    const arrow = (root: Element) => root.querySelector('button[aria-haspopup]')!.parentElement!
      .nextElementSibling!.querySelector('use')?.getAttribute('href');
    expect(arrow(toolbar({}))).toBe('#i-arrow-down');
    const byName = toolbar({ followSort: 'name' });
    expect(byName.querySelector('button[aria-haspopup]')?.textContent).toBe('排序创作者名称');
    expect(arrow(byName)).toBe('#i-arrow-up');
    expect(arrow(toolbar({ followSort: 'name', followDir: 'desc' }))).toBe('#i-arrow-down');
  });
  it('创作者卡与来源行的动作键是小号次级键,和最终行内操作同一形态', () => {
    const root = follow();
    expect(root.querySelectorAll('.follow-skeleton-source')).toHaveLength(10);
    for (const row of root.querySelectorAll('.follow-skeleton-source')) {
      const actions = [...row.lastElementChild!.lastElementChild!.children];
      expect(actions.map((button) => button.querySelector('use')?.getAttribute('href'))).toEqual(['#i-refresh-cw', '#i-trash']);
      for (const button of actions) expect(button.className).toContain('size-8 rounded-lg p-0');
    }
    const head = root.querySelector('.follow-skeleton-author [data-follow-author-header]')!;
    expect([...head.querySelectorAll('button')].map((button) => button.textContent)).toEqual(['', '全选', '收起']);
    expect(root.querySelector('.ui-follow-skeleton-surface > .group')?.textContent).toBe('全选本页');
    expect(root.querySelector('[data-selection-dock]')).toBeNull();
  });
  it('口味骨架画在 React 岛里,按浏览器记录那一页排', () => {
    const root = document.createElement('div');
    root.innerHTML = boardPageSkeleton('/taste');
    expect(root.querySelector('.peach-react')).not.toBeNull();
    expect(root.querySelector('[data-taste-segments] [data-selected]')?.textContent).toBe('浏览器记录');
    expect([...root.querySelectorAll('[data-taste-metrics] > div > span')].map((span) => span.textContent))
      .toEqual(['浏览记录', '口味维度', '浏览线索', '私有导出']);
    expect(root.querySelector('[data-taste-portrait]')?.textContent).toContain('浏览器画像');
    expect([...root.querySelectorAll('h3')].map((h3) => h3.textContent))
      .toEqual(['浏览活跃时间', '每日活跃', '创作者线索来源']);
  });
  it('关注表格视图是 Board UI 表格外框,当前排序那一列带方向', () => {
    const root = follow({ followLayout: 'table' });
    const table = root.querySelector('.follow-skeleton-table[data-board-data-table] table.bui-table')!;
    expect([...table.querySelectorAll('thead th')].map((th) => th.textContent)).toEqual(['', '创作者', '来源', '站点', '状态', '上次检查', '']);
    expect(table.querySelectorAll('tbody tr')).toHaveLength(20);
    expect([...table.querySelectorAll('[data-sort-indicator][data-direction]')].map((mark) => [mark.parentElement!.textContent, mark.getAttribute('data-direction')]))
      .toEqual([['上次检查', 'descending']]);
    expect(root.querySelector('.follow-skeleton-author')).toBeNull();
    expect(root.querySelector('.ui-follow-skeleton-toolbar')?.textContent).not.toContain('全部收起');
  });
  it('表格骨架的行数跟着每页条数走', () => {
    localStorage.setItem('peach.settings.v1', JSON.stringify({ followPageSize: 40 }));
    try {
      const root = follow({ followLayout: 'table' });
      expect(root.querySelectorAll('.follow-skeleton-table tbody tr')).toHaveLength(40);
      expect(root.querySelector('.ui-follow-skeleton-surface > :last-child button[aria-haspopup]')?.textContent).toBe('每页 40 条');
    } finally { localStorage.removeItem('peach.settings.v1') }
  });
});