/* 首页筛选栏的两份聚合(`src/catalog-bars.ts`):键、回退、标签页内复用、口径变了重取、刷新强制重取、续页。 */ import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { dropBars, facetsKey, fetchBars, fetchTopsPage, loadTops, topsKey, topsPageKey, } from '../src/catalog-bars'; import { queryClient } from '../src/query'; const EMPTY = { performers: [], studios: [] }; const FULL = { performers: [{ k: '某人' }], studios: [] }; const FACETS = { tags: [], locations: [] }; /** 按地址回包的 fetch;`hold` 为真时先不回,等 `release()`。 */ function serve(answer: (url: string) => unknown, hold = false) { const waiting: (() => void)[] = []; const fetcher = vi.fn((url: string) => new Promise((resolve) => { const reply = () => resolve({ ok: true, status: 200, json: async () => answer(url) }); if (hold) waiting.push(reply); else reply(); })); vi.stubGlobal('fetch', fetcher); return { fetcher, urls: () => fetcher.mock.calls.map(([url]) => url), release: () => waiting.splice(0).forEach((run) => run()) }; } const answer = (tops: unknown) => (url: string) => (url.startsWith('/api/facets') ? FACETS : tops); beforeEach(() => { vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(0) }); afterEach(() => { queryClient.clear(); vi.useRealTimers(); vi.unstubAllGlobals() }); it('两份各存在自己的键下:facets 按口径,tops 按请求方传的参数与口径', async () => { serve(answer(FULL)); await fetchBars('state=flagged', 'n=60&seed=s&state=flagged'); expect(queryClient.getQueryData(facetsKey('state=flagged'))).toEqual(FACETS); expect(queryClient.getQueryData(topsKey('n=60&seed=s&state=flagged', 'state=flagged'))).toEqual(FULL); expect(facetsKey('a').slice(0, 2)).toEqual(['facets', 'a']); expect(topsKey('p', 'a').slice(0, 3)).toEqual(['tops', 'p', 'a']); }); it('状态页两排都空时退回全库口径再取一次,键仍按请求方传的那一份', async () => { const net = serve((url) => (url.includes('state=') ? EMPTY : FULL)); const [, tops] = await fetchBars('state=flagged', 'n=60&seed=s&state=flagged'); expect(net.urls()).toEqual(['/api/facets?state=flagged', '/api/tops?n=60&seed=s&state=flagged', '/api/tops?n=60&seed=s']); expect(tops).toEqual(FULL); expect(queryClient.getQueryData(topsKey('n=60&seed=s&state=flagged', 'state=flagged'))).toEqual(FULL); }); it('没有状态页参数、或者收窄后仍有人时不回退', async () => { const empty = serve(() => EMPTY); expect(await loadTops('n=60&seed=s')).toEqual(EMPTY); expect(empty.urls()).toEqual(['/api/tops?n=60&seed=s']); const full = serve(() => FULL); expect(await loadTops(new URLSearchParams({ n: '60', state: 'flagged' }))).toEqual(FULL); expect(full.urls()).toEqual(['/api/tops?n=60&state=flagged']); }); it('同一份口径跨长时间往返复用,主动刷新后重取', async () => { const net = serve(answer(FULL)); await fetchBars('', 'n=60&seed=s'); vi.setSystemTime(24 * 60 * 60 * 1000); await fetchBars('', 'n=60&seed=s'); expect(net.urls()).toEqual(['/api/facets', '/api/tops?n=60&seed=s']); dropBars(); await fetchBars('', 'n=60&seed=s'); expect(net.fetcher).toHaveBeenCalledTimes(4); }); it('口径变了两份都重取:tops 的参数没变也跟着取', async () => { const net = serve(answer(FULL)); await fetchBars('', 'n=60&seed=s'); await fetchBars('tag=a', 'n=60&seed=s'); expect(net.urls().slice(2)).toEqual(['/api/facets?tag=a', '/api/tops?n=60&seed=s']); }); it('同时要同一份只发一次请求', async () => { const net = serve(answer(FULL), true); const both = Promise.all([fetchBars('', 'n=60'), fetchBars('', 'n=60')]); await vi.waitFor(() => expect(net.fetcher).toHaveBeenCalledTimes(2)); net.release(); const [first, second] = await both; expect(second).toEqual(first); expect(net.fetcher).toHaveBeenCalledTimes(2); }); it('dropBars 之后立即重取', async () => { const net = serve(answer(FULL)); await fetchBars('', 'n=60&seed=s'); dropBars(); await fetchBars('', 'n=60&seed=s'); expect(net.urls()).toEqual(['/api/facets', '/api/tops?n=60&seed=s', '/api/facets', '/api/tops?n=60&seed=s']); }); it('路上那一趟碰上 dropBars:等它的调用方照样拿到数据,下一次另发请求', async () => { const net = serve(answer(FULL), true); const before = fetchBars('', 'n=60'); await vi.waitFor(() => expect(net.fetcher).toHaveBeenCalledTimes(2)); dropBars(); const after = fetchBars('', 'n=60'); await vi.waitFor(() => expect(net.fetcher).toHaveBeenCalledTimes(4)); net.release(); await expect(before).resolves.toEqual([FACETS, FULL]); await expect(after).resolves.toEqual([FACETS, FULL]); }); it('两排同时翻到同一页只发一次请求,与第一页的键分开', async () => { const net = serve(() => FULL, true); const both = Promise.all([fetchTopsPage('n=60&page=1'), fetchTopsPage('n=60&page=1')]); await vi.waitFor(() => expect(net.fetcher).toHaveBeenCalledTimes(1)); net.release(); const [performers, studios] = await both; expect(studios).toBe(performers); expect(net.urls()).toEqual(['/api/tops?n=60&page=1']); expect(queryClient.getQueryData(topsPageKey('n=60&page=1'))).toEqual(FULL); expect(topsPageKey('p')).not.toEqual(topsKey('p', '')); }); it('续页复用,dropBars 之后重取,状态页空页照样退回全库口径', async () => { const net = serve((url) => (url.includes('state=') ? EMPTY : FULL)); await fetchTopsPage('n=60&page=2&state=flagged'); await fetchTopsPage('n=60&page=2&state=flagged'); expect(net.urls()).toEqual(['/api/tops?n=60&page=2&state=flagged', '/api/tops?n=60&page=2']); dropBars(); expect(await fetchTopsPage('n=60&page=2&state=flagged')).toEqual(FULL); expect(net.fetcher).toHaveBeenCalledTimes(4); });