Ctrl K
ComunidadGuías…

frontend/e2e/settings-fixture.ts

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

Archivo original ↗Preguntar a KiBot
/* 设置面板(`settings-panel` 岛)的桩与入口:`settings-panel.test.ts` 与设计决定用例(`design-*.test.ts`)共用。
 *
 * 跟数据库走的 `/api/settings` 与两份机器状态(`/api/follow/schedule`、`/api/thumbnail-jobs`)各给一份
 * 内存桩:读写都由它接住,好看写进去的是什么,也让「刷新后保持」不依赖临时服务上的状态。 */
import type { Browser, Page, Request } from 'playwright-core';

import { settle, visit, VIEWPORTS, type Viewport, type Visit } from './harness.ts';

export const PANEL = '#settingsPanel';
export const ORDER = ['', 'follow', 'jav', 'performers', 'tags', 'studios', 'flagged', 'manage'];
const DESKTOP = VIEWPORTS.find((viewport) => !viewport.mobile)!;

export interface Server {
  settings: Record<string, unknown>;
  schedule: Record<string, unknown>;
  thumbnails: Record<string, unknown>;
  /** 三个写接口收到的请求体,按到达顺序。 */
  posts: { path: string; body: Record<string, unknown> }[];
}

const body = (request: Request) => JSON.parse(request.postData() || '{}') as Record<string, unknown>;

export async function stubServer(page: Page): Promise<Server> {
  const server: Server = {
    settings: {
      sidebarOrder: [...ORDER], metadataRefreshDays: 30, followInitialDays: 30, postSetupTutorialDone: true,
      organizeTemplates: {}, feedHideGroupCompilations: true, feedHideSoloCompilations: false, feedHideExcerpts: true,
      searchHistoryLimit: 10,
    },
    schedule: {
      available: true, enabled: true, interval_minutes: 180, running: false, last_error: null,
      last_finished_at: '2026-09-30T08:00:00Z', last_added: 3, next_run_at: null,
    },
    thumbnails: { mode: 'precise', status: 'idle' },
    posts: [],
  };
  const route = (path: string, merge: (sent: Record<string, unknown>) => Record<string, unknown>) =>
    page.route((url) => url.pathname === path, (handled) => {
      const request = handled.request();
      if (request.method() === 'GET') return handled.fulfill({ json: merge({}) });
      const sent = body(request);
      server.posts.push({ path, body: sent });
      return handled.fulfill({ json: merge(sent) });
    });
  await route('/api/settings', (sent) => {
    Object.assign(server.settings, sent);
    return { ok: true, ...server.settings };
  });
  await route('/api/follow/schedule', (sent) => Object.assign(server.schedule, sent));
  await route('/api/thumbnail-jobs', (sent) => Object.assign(server.thumbnails, sent));
  return server;
}

/** 齿轮打开面板并停在 `section` 那一格。窄屏下齿轮收在抽屉底部,直接派发点击,两种宽度走同一条入口。 */
export async function openPanel(page: Page, section = '外观与导航'): Promise<void> {
  await page.locator('#settingsBtn').evaluate((button: HTMLElement) => button.click());
  await page.locator(`${PANEL}:not([hidden])`).waitFor({ timeout: 10_000 });
  if (section !== '外观与导航') await page.locator(PANEL).getByRole('tab', { name: section, exact: true }).click();
  await page.locator(PANEL).getByRole('tab', { name: section, exact: true })
    .and(page.locator('[aria-selected="true"]')).waitFor({ timeout: 5_000 });
}

/** 首页挂上桩之后重载一次,桩从第一次取设置起就接住。 */
export async function openHome(browser: Browser, viewport: Viewport = DESKTOP): Promise<Visit & { server: Server }> {
  const opened = await visit(browser, '/', viewport);
  const server = await stubServer(opened.page);
  await opened.page.reload({ waitUntil: 'load' });
  await opened.page.locator('#settingsBtn').waitFor({ state: 'attached', timeout: 15_000 });
  await settle(opened.page);
  return { ...opened, server };
}