mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
fix(offline): reset connection prompts after recovery (#681)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { useGlobalOfflineStatus, useOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
describe('useOfflineStatus', () => {
|
||||
const status = useGlobalOfflineStatus()
|
||||
|
||||
beforeEach(() => {
|
||||
status.markServerOnline()
|
||||
})
|
||||
|
||||
it('在所有消费者之间共享连接状态,并为组件暴露只读操作边界', () => {
|
||||
const anotherGlobalStatus = useGlobalOfflineStatus()
|
||||
const componentStatus = useOfflineStatus()
|
||||
|
||||
expect(anotherGlobalStatus.connectionStatus).toBe(status.connectionStatus)
|
||||
expect(anotherGlobalStatus.connectionReason).toBe(status.connectionReason)
|
||||
expect(componentStatus.connectionReason).toBe(status.connectionReason)
|
||||
expect(componentStatus).not.toHaveProperty('markServerOffline')
|
||||
|
||||
status.markServerOffline('timeout')
|
||||
expect(componentStatus.isOnline.value).toBe(false)
|
||||
expect(componentStatus.isChecking.value).toBe(false)
|
||||
expect(componentStatus.isOffline.value).toBe(true)
|
||||
expect(componentStatus.canPerformNetworkAction.value).toBe(false)
|
||||
|
||||
const initialCheckRequestId = status.connectionCheckRequestId.value
|
||||
componentStatus.requestConnectionCheck('network-error')
|
||||
expect(status.connectionStatus.value).toBe('checking')
|
||||
expect(status.connectionReason.value).toBe('network-error')
|
||||
expect(status.connectionCheckRequestId.value).toBe(initialCheckRequestId + 1)
|
||||
})
|
||||
|
||||
it('区分待确认与离线状态,并只在确认离线后阻断网络操作', () => {
|
||||
status.markConnectionChecking('timeout')
|
||||
|
||||
expect(status.connectionStatus.value).toBe('checking')
|
||||
expect(status.connectionReason.value).toBe('timeout')
|
||||
expect(status.isOnline.value).toBe(false)
|
||||
expect(status.isChecking.value).toBe(true)
|
||||
expect(status.isOffline.value).toBe(false)
|
||||
expect(status.canPerformNetworkAction.value).toBe(true)
|
||||
|
||||
status.markServerOffline()
|
||||
|
||||
expect(status.connectionStatus.value).toBe('offline')
|
||||
expect(status.connectionReason.value).toBe('server-unreachable')
|
||||
expect(status.isOffline.value).toBe(true)
|
||||
expect(status.canPerformNetworkAction.value).toBe(false)
|
||||
|
||||
status.markServerOnline()
|
||||
|
||||
expect(status.connectionStatus.value).toBe('online')
|
||||
expect(status.connectionReason.value).toBeNull()
|
||||
expect(status.isOnline.value).toBe(true)
|
||||
expect(status.canPerformNetworkAction.value).toBe(true)
|
||||
})
|
||||
|
||||
it('为网络错误和主动检查递增探测序列,并为成功响应递增恢复序列', () => {
|
||||
const initialCheckRequestId = status.connectionCheckRequestId.value
|
||||
const initialSuccessSequence = status.serverSuccessSequence.value
|
||||
|
||||
status.reportNetworkError()
|
||||
expect(status.connectionStatus.value).toBe('checking')
|
||||
expect(status.connectionReason.value).toBe('network-error')
|
||||
expect(status.connectionCheckRequestId.value).toBe(initialCheckRequestId + 1)
|
||||
|
||||
status.requestConnectionCheck('browser-offline')
|
||||
expect(status.connectionReason.value).toBe('browser-offline')
|
||||
expect(status.connectionCheckRequestId.value).toBe(initialCheckRequestId + 2)
|
||||
|
||||
status.requestConnectionCheck()
|
||||
expect(status.connectionReason.value).toBe('browser-offline')
|
||||
expect(status.connectionCheckRequestId.value).toBe(initialCheckRequestId + 3)
|
||||
|
||||
status.markServerOnline()
|
||||
expect(status.serverSuccessSequence.value).toBe(initialSuccessSequence + 1)
|
||||
})
|
||||
})
|
||||
@@ -40,14 +40,14 @@ function showConnectionPrompt() {
|
||||
|
||||
/** 在同一轮连接异常内按状态去重提示,并在恢复在线后允许下一轮提示重新出现。 */
|
||||
function handleConnectionStatusChange() {
|
||||
// 重启期间由重启进度弹窗承载反馈,避免离线提示与进度提示叠加。
|
||||
if (isRestarting.value) return
|
||||
|
||||
if (connectionStatus.value === 'online') {
|
||||
shownConnectionPromptKeys.clear()
|
||||
return
|
||||
}
|
||||
|
||||
// 重启期间由重启进度弹窗承载反馈,避免离线提示与进度提示叠加。
|
||||
if (isRestarting.value) return
|
||||
|
||||
const promptKey =
|
||||
connectionStatus.value === 'checking'
|
||||
? connectionStatus.value
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import OfflinePage from '@/layouts/default/components/OfflinePage.vue'
|
||||
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
error: vi.fn(),
|
||||
isRestarting: { value: false },
|
||||
warning: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useSystemRestart', () => ({
|
||||
useSystemRestartStatus: () => ({ isRestarting: mocks.isRestarting }),
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async importOriginal => ({
|
||||
...(await importOriginal<typeof import('vue-i18n')>()),
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.error, warning: mocks.warning }),
|
||||
}))
|
||||
|
||||
describe('OfflinePage', () => {
|
||||
const status = useGlobalOfflineStatus()
|
||||
let wrapper: VueWrapper | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.error.mockReset()
|
||||
mocks.warning.mockReset()
|
||||
mocks.isRestarting.value = false
|
||||
status.markServerOnline()
|
||||
wrapper = mount(OfflinePage)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
status.markServerOnline()
|
||||
})
|
||||
|
||||
it('同一轮 checking 状态只提示一次,并使用非阻断警告', async () => {
|
||||
status.markConnectionChecking('network-error')
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.warning).toHaveBeenCalledWith('app.connectionChecking:app.connectionCheckingMessage', {
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
status.markConnectionChecking('timeout')
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('按离线原因显示错误,并允许不同离线状态分别提示', async () => {
|
||||
status.markServerOffline('timeout')
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.error).toHaveBeenNthCalledWith(1, 'app.serviceUnavailable:app.serviceTimeoutMessage', {
|
||||
timeout: 7000,
|
||||
})
|
||||
|
||||
status.markServerOffline('browser-offline')
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.error).toHaveBeenNthCalledWith(2, 'app.serviceUnavailable:app.browserOfflineMessage', {
|
||||
timeout: 7000,
|
||||
})
|
||||
|
||||
status.markServerOffline('server-unreachable')
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.error).toHaveBeenNthCalledWith(3, 'app.serviceUnavailable:app.serviceUnavailableMessage', {
|
||||
timeout: 7000,
|
||||
})
|
||||
expect(mocks.warning).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('在线恢复后允许下一轮 checking 再次提示', async () => {
|
||||
status.markConnectionChecking()
|
||||
await nextTick()
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(1)
|
||||
|
||||
status.markServerOnline()
|
||||
await nextTick()
|
||||
status.markConnectionChecking()
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('重启期间不提示,但在线恢复仍清空上一轮去重状态', async () => {
|
||||
status.markConnectionChecking()
|
||||
await nextTick()
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(1)
|
||||
|
||||
mocks.isRestarting.value = true
|
||||
status.markServerOffline('timeout')
|
||||
await nextTick()
|
||||
expect(mocks.error).not.toHaveBeenCalled()
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(1)
|
||||
|
||||
status.markServerOnline()
|
||||
await nextTick()
|
||||
expect(mocks.error).not.toHaveBeenCalled()
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(1)
|
||||
|
||||
mocks.isRestarting.value = false
|
||||
status.markConnectionChecking()
|
||||
await nextTick()
|
||||
|
||||
expect(mocks.warning).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -336,7 +336,9 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
||||
'src/views/subscribe/SubscribeShareView.vue',
|
||||
'src/composables/useMediaSubscribe.ts',
|
||||
'src/composables/useLlmProviderDirectory.ts',
|
||||
'src/composables/useOfflineStatus.ts',
|
||||
'src/composables/useTorrentFilter.ts',
|
||||
'src/layouts/default/components/OfflinePage.vue',
|
||||
'src/components/cards/SubscribeCard.vue',
|
||||
'src/components/cards/UserCard.vue',
|
||||
'src/components/filter/TorrentFilterBar.vue',
|
||||
@@ -425,6 +427,18 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/composables/useOfflineStatus.ts': {
|
||||
branches: 85,
|
||||
functions: 90,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
'src/layouts/default/components/OfflinePage.vue': {
|
||||
branches: 75,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
'src/components/cards/SubscribeCard.vue': {
|
||||
branches: 75,
|
||||
functions: 80,
|
||||
|
||||
Reference in New Issue
Block a user