diff --git a/src/components/dialog/__tests__/SiteAddEditDialog.spec.ts b/src/components/dialog/__tests__/SiteAddEditDialog.spec.ts index bc8d12fe..907b95fc 100644 --- a/src/components/dialog/__tests__/SiteAddEditDialog.spec.ts +++ b/src/components/dialog/__tests__/SiteAddEditDialog.spec.ts @@ -1,5 +1,6 @@ import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue' import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue' +import { getActiveRequestsCount } from '@/utils/requestOptimizer' import { fireEvent, screen, waitFor } from '@testing-library/vue' import userEvent from '@testing-library/user-event' import { createSite, createSiteDownloader } from '@tests/support/factories/site' @@ -11,7 +12,7 @@ import { } from '@tests/support/msw/handlers/site' import { server } from '@tests/support/msw/server' import { renderWithProviders } from '@tests/support/render' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ done: vi.fn(), @@ -30,8 +31,10 @@ vi.mock('@/api/nprogress', () => ({ startNProgress: mocks.start, })) -async function renderDialog(oper: 'add' | 'edit', siteid?: number) { +async function renderDialog(oper: 'add' | 'edit', siteid?: number, downloaders = [createSiteDownloader()]) { const events = { close: vi.fn(), save: vi.fn() } + const downloaderRequested = vi.fn() + server.use(siteDownloadersHandler(downloaders, 200, downloaderRequested)) const result = await renderWithProviders(SiteAddEditDialog, { props: { modelValue: true, @@ -42,22 +45,17 @@ async function renderDialog(oper: 'add' | 'edit', siteid?: number) { }, global: { components: { VDialogCloseBtn: DialogCloseBtn } }, }) + await waitFor(() => expect(downloaderRequested).toHaveBeenCalledOnce()) + await waitFor(() => expect(getActiveRequestsCount()).toBe(0)) return { ...result, events } } describe('SiteAddEditDialog', () => { - beforeEach(() => { - server.use(siteDownloadersHandler()) - }) - it('creates a site with the entered form values and loaded downloader', async () => { const saved = vi.fn() - server.use( - siteDownloadersHandler([createSiteDownloader({ name: '下载器 A' })]), - addSiteHandler({ success: true }, 200, saved), - ) + server.use(addSiteHandler({ success: true }, 200, saved)) const user = userEvent.setup() - const { events } = await renderDialog('add') + const { events } = await renderDialog('add', undefined, [createSiteDownloader({ name: '下载器 A' })]) await user.type(screen.getByLabelText('站点地址'), 'https://new.example.com/') await fireEvent.update(screen.getByLabelText('RSS地址'), 'https://new.example.com/rss') @@ -125,6 +123,7 @@ describe('SiteAddEditDialog', () => { }) it('restores progress and keeps the dialog open after an HTTP creation failure', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) server.use(addSiteHandler({ message: '服务异常', success: false }, 500)) const user = userEvent.setup() const { events } = await renderDialog('add') @@ -135,6 +134,7 @@ describe('SiteAddEditDialog', () => { await waitFor(() => expect(mocks.done).toHaveBeenCalledOnce()) expect(events.save).not.toHaveBeenCalled() expect(mocks.toastSuccess).not.toHaveBeenCalled() + expect(consoleError).toHaveBeenCalledOnce() }) it('restores numeric flags and API mode when editing a rate-limited site', async () => { @@ -148,8 +148,8 @@ describe('SiteAddEditDialog', () => { render: 0, token: 'token', }) - server.use(siteDetailsHandler(site.id, site), siteDownloadersHandler([createSiteDownloader({ name: '下载器 A' })])) - await renderDialog('edit', site.id) + server.use(siteDetailsHandler(site.id, site)) + await renderDialog('edit', site.id, [createSiteDownloader({ name: '下载器 A' })]) expect(await screen.findByText(site.name)).toBeInTheDocument() expect(screen.getByLabelText('使用代理访问')).toBeChecked() @@ -178,6 +178,7 @@ describe('SiteAddEditDialog', () => { ['business', 200, { message: '不允许更新', success: false }, '限流站点 更新失败:不允许更新'], ['HTTP', 500, { message: '服务异常', success: false }, '限流站点 更新失败!'], ])('keeps the dialog open on %s update failure', async (_case, status, response, message) => { + const consoleError = status === 500 ? vi.spyOn(console, 'error').mockImplementation(() => {}) : undefined const site = createSite({ name: '限流站点' }) server.use(siteDetailsHandler(site.id, site), updateSiteHandler(response, status)) const { events } = await renderDialog('edit', site.id) @@ -187,6 +188,7 @@ describe('SiteAddEditDialog', () => { await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(message)) expect(events.save).not.toHaveBeenCalled() expect(mocks.done).toHaveBeenCalledOnce() + if (status === 500) expect(consoleError).toHaveBeenCalledOnce() }) it('emits close from the dialog close button', async () => { diff --git a/src/components/dialog/__tests__/SiteCookieUpdateDialog.spec.ts b/src/components/dialog/__tests__/SiteCookieUpdateDialog.spec.ts index 771b87c5..faa69779 100644 --- a/src/components/dialog/__tests__/SiteCookieUpdateDialog.spec.ts +++ b/src/components/dialog/__tests__/SiteCookieUpdateDialog.spec.ts @@ -90,6 +90,7 @@ describe('SiteCookieUpdateDialog', () => { ['detail', { detail: '站点不存在' }, '站点不存在'], ['message', { message: '认证服务异常' }, '认证服务异常'], ])('shows an HTTP %s response and restores pending state', async (_case, body, message) => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) server.use(http.post('http://localhost/api/v1/site/cookie/701', () => HttpResponse.json(body, { status: 500 }))) const user = userEvent.setup() await renderDialog() @@ -100,6 +101,7 @@ describe('SiteCookieUpdateDialog', () => { await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(`Cookie 站点 更新失败:${message}`)) expect(screen.getByRole('button', { name: '开始更新' })).toBeEnabled() expect(screen.queryByRole('status')).not.toBeInTheDocument() + expect(consoleError).toHaveBeenCalledOnce() }) it('disables the update action while the request is pending and restores it afterward', async () => { diff --git a/src/components/dialog/__tests__/SiteImportDialog.spec.ts b/src/components/dialog/__tests__/SiteImportDialog.spec.ts index ca9e9eaa..525a4b25 100644 --- a/src/components/dialog/__tests__/SiteImportDialog.spec.ts +++ b/src/components/dialog/__tests__/SiteImportDialog.spec.ts @@ -83,15 +83,23 @@ describe('SiteImportDialog', () => { expect(screen.getByLabelText('选择文件')).toBeInTheDocument() }) - it.each([ - ['invalid JSON', brokenJsonFile(), '文件解析失败,请检查文件格式'], - ['non-array JSON', jsonFile({ name: 'single' }), '文件格式无效,请检查文件内容'], - ])('rejects %s', async (_case, file, message) => { + it('rejects invalid JSON', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) await renderDialog() - await chooseFile(file) + await chooseFile(brokenJsonFile()) - await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(message)) + await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('文件解析失败,请检查文件格式')) + expect(screen.getByLabelText('选择文件')).toBeInTheDocument() + expect(consoleError).toHaveBeenCalledWith('Parse file error:', expect.any(SyntaxError)) + }) + + it('rejects non-array JSON', async () => { + await renderDialog() + + await chooseFile(jsonFile({ name: 'single' })) + + await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('文件格式无效,请检查文件内容')) expect(screen.getByLabelText('选择文件')).toBeInTheDocument() }) @@ -169,6 +177,7 @@ describe('SiteImportDialog', () => { }) it('reports partial success and preserves the HTTP error message', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const sites = [createSite({ name: '成功站点' }), createSite({ name: '失败站点' })] let requestIndex = 0 server.use( @@ -188,6 +197,7 @@ describe('SiteImportDialog', () => { expect(mocks.toastError).toHaveBeenCalledWith('导入完成,成功 1 个,失败 1 个') await fireEvent.click(screen.getByText('失败站点 - 错误详情')) expect(await screen.findByText('Request failed with status code 500')).toBeInTheDocument() + expect(consoleError).toHaveBeenCalledWith('Import site 失败站点 failed:', expect.any(Error)) }) it('does not start a request when every record is invalid', async () => { diff --git a/src/components/dialog/__tests__/SubscribeSeasonDialog.spec.ts b/src/components/dialog/__tests__/SubscribeSeasonDialog.spec.ts index 11615289..165ef77c 100644 --- a/src/components/dialog/__tests__/SubscribeSeasonDialog.spec.ts +++ b/src/components/dialog/__tests__/SubscribeSeasonDialog.spec.ts @@ -2,6 +2,7 @@ import type { MediaInfo, MediaSeason, NotExistMediaInfo } from '@/api/types' import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue' import SubscribeSeasonDialog from '@/components/dialog/SubscribeSeasonDialog.vue' import type { SubscribeMode } from '@/composables/useMediaSubscribe' +import { getActiveRequestsCount } from '@/utils/requestOptimizer' import { fireEvent, screen, waitFor, within } from '@testing-library/vue' import userEvent from '@testing-library/user-event' import { createMediaInfo, createMediaSeason, createNotExistMediaInfo } from '@tests/support/factories/media' @@ -14,7 +15,7 @@ import { import { server } from '@tests/support/msw/server' import { renderWithProviders } from '@tests/support/render' import { flushPromises } from '@vue/test-utils' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' /** 季订阅弹窗测试属性。 */ interface SeasonDialogProps { @@ -102,9 +103,8 @@ async function settleRequests() { } describe('SubscribeSeasonDialog', () => { - beforeEach(() => { - vi.spyOn(console, 'error').mockImplementation(() => {}) - vi.spyOn(console, 'warn').mockImplementation(() => {}) + afterEach(async () => { + await waitFor(() => expect(getActiveRequestsCount()).toBe(0)) }) it('loads the default TMDB seasons once with exact requests and renders season states', async () => { @@ -250,7 +250,9 @@ describe('SubscribeSeasonDialog', () => { }, 'tmdb:source-7306', ], - ] as const)('uses the %s media identifier without requesting TMDB groups', async (_label, overrides, mediaId) => { + ] as const)('uses the %s media identifier without requesting TMDB groups', async (label, overrides, mediaId) => { + const consoleWarn = + label === 'source-only TMDB' ? vi.spyOn(console, 'warn').mockImplementation(() => {}) : undefined const media = createTvMedia(overrides) const requested = vi.fn() server.use( @@ -264,6 +266,7 @@ describe('SubscribeSeasonDialog', () => { await settleRequests() expect(requested).toHaveBeenCalledOnce() expect(requested.mock.calls[0][0].searchParams.get('mediaid')).toBe(mediaId) + if (label === 'source-only TMDB') expect(consoleWarn).toHaveBeenCalledWith('tmdbid is not set or is empty') }) it.each([ @@ -417,6 +420,7 @@ describe('SubscribeSeasonDialog', () => { }) it('exits Loading after the default season request fails', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const media = createTvMedia({ tmdb_id: 7310 }) const requested = vi.fn() server.use( @@ -431,9 +435,11 @@ describe('SubscribeSeasonDialog', () => { expect(document.querySelector('.initial-loading-container')).not.toBeInTheDocument() expect(screen.getByText(`${media.title} 未查询到季集信息`)).toBeInTheDocument() + expect(consoleError).toHaveBeenCalledOnce() }) it('keeps season selection usable when the missing-state request fails', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const media = createTvMedia({ tmdb_id: 7313 }) const missingRequested = vi.fn() server.use( @@ -452,9 +458,11 @@ describe('SubscribeSeasonDialog', () => { expect(events.subscribe).toHaveBeenCalledOnce() expect(events.subscribe.mock.calls[0][1]).toEqual({}) + expect(consoleError).toHaveBeenCalledOnce() }) it('keeps season selection usable when optional episode groups fail to load', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) const media = createTvMedia({ tmdb_id: 7314 }) const groupsRequested = vi.fn() server.use( @@ -473,6 +481,7 @@ describe('SubscribeSeasonDialog', () => { expect(events.subscribe).toHaveBeenCalledOnce() expect(screen.getByRole('button', { name: /^默认/ })).toBeInTheDocument() + expect(consoleError).toHaveBeenCalledOnce() }) it('renders the successful empty state and emits close without submitting', async () => { diff --git a/src/composables/__tests__/useGlassOpticalRenderer.spec.ts b/src/composables/__tests__/useGlassOpticalRenderer.spec.ts index 39f86c60..076f5153 100644 --- a/src/composables/__tests__/useGlassOpticalRenderer.spec.ts +++ b/src/composables/__tests__/useGlassOpticalRenderer.spec.ts @@ -1420,6 +1420,7 @@ describe('glass optical surface discovery', () => { }) it('publishes only the current pending preparation failure identity', async () => { + const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const three = await import('three') const pendingWallpaperUrl = ref('') const pendingWallpaperRevision = ref(0) @@ -1445,6 +1446,10 @@ describe('glass optical surface discovery', () => { pendingWallpaperUrl.value = 'https://example.com/wallpaper-next.jpg' await vi.waitFor(() => expect(renderer?.failedWallpaperRevision.value).toBe(51)) + expect(consoleWarn).toHaveBeenCalledWith( + '玻璃光学壁纸预备失败,继续使用当前纹理:', + expect.objectContaining({ message: 'pending source failed' }), + ) expect(renderer?.failedWallpaperUrl.value).toBe('https://example.com/wallpaper-next.jpg') expect(renderer?.failedWallpaperPreparationKey.value).toContain('frosted:balanced:') diff --git a/src/composables/__tests__/useMediaSubscribe.spec.ts b/src/composables/__tests__/useMediaSubscribe.spec.ts index d91a768c..f8d5bf36 100644 --- a/src/composables/__tests__/useMediaSubscribe.spec.ts +++ b/src/composables/__tests__/useMediaSubscribe.spec.ts @@ -7,6 +7,7 @@ import { type SubscribeMode, useMediaSubscribe, } from '@/composables/useMediaSubscribe' +import { getActiveRequestsCount } from '@/utils/requestOptimizer' import { fireEvent, screen, waitFor } from '@testing-library/vue' import { createSubscribe, createSubscribeMovie, createSubscribeTv } from '@tests/support/factories/subscribe' import { @@ -18,8 +19,9 @@ import { } from '@tests/support/msw/handlers/subscribe' import { server } from '@tests/support/msw/server' import { renderWithProviders } from '@tests/support/render' +import { flushPromises } from '@vue/test-utils' import { defineComponent, ref } from 'vue' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ cacheStatus: vi.fn(), @@ -223,6 +225,11 @@ describe('useMediaSubscribe entry flows', () => { mocks.openSharedDialog.mockReturnValue({ close: vi.fn(), id: 1, updateProps: vi.fn() }) }) + afterEach(async () => { + await flushPromises() + await waitFor(() => expect(getActiveRequestsCount()).toBe(0)) + }) + it('creates a normal movie subscription and synchronizes public state', async () => { const media = createSubscribeMovie({ title: '普通电影', tmdb_id: 101, year: '2025' }) const created = vi.fn() @@ -302,7 +309,7 @@ describe('useMediaSubscribe entry flows', () => { expect(mocks.toastSuccess).toHaveBeenCalledOnce() expect(mocks.toastError).not.toHaveBeenCalled() expect(mocks.openSharedDialog).not.toHaveBeenCalled() - consoleLog.mockRestore() + expect(consoleLog).toHaveBeenCalledOnce() }) it('opens the mode chooser for an existing movie and creates the selected mode', async () => { @@ -544,7 +551,7 @@ describe('useMediaSubscribe entry flows', () => { ['business failure', 200, { message: 'duplicate', success: false }], ['HTTP failure', 500, { message: 'server down', success: false }], ])('keeps state unchanged when create returns a %s', async (_case, status, response) => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const consoleError = status === 500 ? vi.spyOn(console, 'error').mockImplementation(() => {}) : undefined server.use(createSubscribeHandler(response, status)) await renderSubscribeHarness({ media: createSubscribeMovie({ tmdb_id: 107 }) }) @@ -555,14 +562,14 @@ describe('useMediaSubscribe entry flows', () => { expect(mocks.cacheStatus).not.toHaveBeenCalled() expect(mocks.openSharedDialog).not.toHaveBeenCalled() expect(mocks.doneProgress).toHaveBeenCalledOnce() - consoleError.mockRestore() + if (status === 500) expect(consoleError).toHaveBeenCalledOnce() }) it.each([ ['business failure', 200, { message: 'delete rejected', success: false }], ['HTTP failure', 500, { message: 'server down', success: false }], ])('keeps subscription state when removal returns a %s', async (_case, status, response) => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const consoleError = status === 500 ? vi.spyOn(console, 'error').mockImplementation(() => {}) : undefined const deleted = vi.fn() server.use(deleteSubscribeByMediaHandler('tmdb:108', response, status, url => deleted(url))) await renderSubscribeHarness({ @@ -586,14 +593,14 @@ describe('useMediaSubscribe entry flows', () => { expect(screen.getByTestId('modes')).toHaveTextContent('"2":"best_version"') expect(mocks.cacheStatus).not.toHaveBeenCalled() expect(mocks.doneProgress).toHaveBeenCalledOnce() - consoleError.mockRestore() + if (status === 500) expect(consoleError).toHaveBeenCalledOnce() }) it.each([ ['business failure', 200, { message: 'update rejected', success: false }], ['HTTP failure', 500, { message: 'server down', success: false }], ])('keeps the subscribed mode when an update returns a %s', async (_case, status, response) => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const consoleError = status === 500 ? vi.spyOn(console, 'error').mockImplementation(() => {}) : undefined const media = createSubscribeTv({ title: '模式更新失败剧集', tmdb_id: 110 }) const updated = vi.fn() server.use( @@ -621,7 +628,7 @@ describe('useMediaSubscribe entry flows', () => { expect(screen.getByTestId('modes')).toHaveTextContent('"2":"normal"') expect(mocks.cacheStatus).not.toHaveBeenCalled() expect(mocks.doneProgress).toHaveBeenCalledOnce() - consoleError.mockRestore() + if (status === 500) expect(consoleError).toHaveBeenCalledOnce() }) it('maps a 404 query to missing and propagates other HTTP errors', async () => { diff --git a/src/composables/__tests__/useVersionChecker.spec.ts b/src/composables/__tests__/useVersionChecker.spec.ts index 3c3c6134..d42e14c6 100644 --- a/src/composables/__tests__/useVersionChecker.spec.ts +++ b/src/composables/__tests__/useVersionChecker.spec.ts @@ -15,6 +15,7 @@ describe('useVersionChecker', () => { }) it('没有可用 Service Worker 时保留版本不一致的清缓存兜底', async () => { + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) const { checkVersion } = useVersionChecker() await checkVersion('version-that-never-matches-the-build') @@ -35,5 +36,9 @@ describe('useVersionChecker', () => { timeout: false, }), ) + expect(consoleLog.mock.calls).toEqual([ + [expect.stringMatching(/^\[VersionChecker\] 检测到版本不一致:/)], + ['[VersionChecker] 无 Service Worker, 直接显示通知'], + ]) }) }) diff --git a/src/utils/requestOptimizer.ts b/src/utils/requestOptimizer.ts index de68fcae..0571712c 100644 --- a/src/utils/requestOptimizer.ts +++ b/src/utils/requestOptimizer.ts @@ -83,8 +83,6 @@ export function initializeRequestOptimizer(axiosInstance: any) { return Promise.reject(error) }, ) - - console.log('Request optimizer initialized - all requests will be auto-managed') } // 获取当前活跃请求数量(调试用) diff --git a/src/views/dashboard/__tests__/MediaServerCards.spec.ts b/src/views/dashboard/__tests__/MediaServerCards.spec.ts index 58042b6c..2095bcc5 100644 --- a/src/views/dashboard/__tests__/MediaServerCards.spec.ts +++ b/src/views/dashboard/__tests__/MediaServerCards.spec.ts @@ -152,6 +152,7 @@ describe('dashboard media server cards', () => { [MediaServerPlaying, 'mediaserver/playing', '暂无继续观看记录'], [MediaServerLibrary, 'mediaserver/library', '暂无媒体库数据'], ])('keeps the successful empty snapshot when %s later fails', async (component, endpoint, emptyText) => { + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) let endpointReads = 0 mocks.apiGet.mockImplementation((url: string) => { if (url === 'system/setting/MediaServers') return { data: { value: [{ enabled: true, name: 'home' }] } } @@ -172,6 +173,7 @@ describe('dashboard media server cards', () => { expect(screen.getByText(emptyText)).toBeInTheDocument() expect(screen.queryByText('媒体服务器数据加载失败')).not.toBeInTheDocument() expect(await screen.findByRole('button', { name: '刷新失败,当前显示上次数据' })).toBeInTheDocument() + expect(consoleLog).toHaveBeenCalledOnce() }) it.each([ @@ -179,6 +181,7 @@ describe('dashboard media server cards', () => { [MediaServerPlaying, 'mediaserver/playing', '恢复的继续观看'], [MediaServerLibrary, 'mediaserver/library', '恢复的媒体库'], ])('shows a retry state when %s fails without a snapshot', async (component, endpoint, recoveredText) => { + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) let endpointReads = 0 let shouldFail = true mocks.apiGet.mockImplementation((url: string) => { @@ -202,6 +205,7 @@ describe('dashboard media server cards', () => { await fireEvent.click(screen.getByRole('button', { name: '媒体服务器数据加载失败' })) expect(await screen.findByText(recoveredText)).toBeInTheDocument() expect(endpointReads).toBe(failedReads + 1) + expect(consoleLog).toHaveBeenCalledOnce() }) it('restores the last successful library snapshot before F5 revalidation completes', async () => { @@ -256,6 +260,7 @@ describe('dashboard media server cards', () => { }) it('keeps continue-watching content when a warm refresh fails', async () => { + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) const refresh = deferred>() let playingReads = 0 mocks.apiGet.mockImplementation((url: string) => { @@ -281,9 +286,11 @@ describe('dashboard media server cards', () => { await new Promise(resolve => setTimeout(resolve, 0)) expect(screen.getByText('旧继续观看')).toBeInTheDocument() expect(screen.getByRole('button', { name: '刷新失败,当前显示上次数据' })).toBeInTheDocument() + expect(consoleLog).toHaveBeenCalledOnce() }) it('keeps recent-library content when a warm refresh fails', async () => { + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) const refresh = deferred>() let latestReads = 0 mocks.apiGet.mockImplementation((url: string) => { @@ -307,9 +314,11 @@ describe('dashboard media server cards', () => { await new Promise(resolve => setTimeout(resolve, 0)) expect(screen.getByText('旧最近入库')).toBeInTheDocument() expect(screen.getByRole('button', { name: '刷新失败,当前显示上次数据' })).toBeInTheDocument() + expect(consoleLog).toHaveBeenCalledOnce() }) it('keeps media-library content when a warm refresh fails', async () => { + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}) const refresh = deferred>() let libraryReads = 0 mocks.apiGet.mockImplementation((url: string) => { @@ -330,6 +339,7 @@ describe('dashboard media server cards', () => { await new Promise(resolve => setTimeout(resolve, 0)) expect(screen.getByText('旧媒体库')).toBeInTheDocument() expect(screen.getByRole('button', { name: '刷新失败,当前显示上次数据' })).toBeInTheDocument() + expect(consoleLog).toHaveBeenCalledOnce() }) it('replaces the media-library snapshot atomically after a warm refresh', async () => {