mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +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() {
|
function handleConnectionStatusChange() {
|
||||||
// 重启期间由重启进度弹窗承载反馈,避免离线提示与进度提示叠加。
|
|
||||||
if (isRestarting.value) return
|
|
||||||
|
|
||||||
if (connectionStatus.value === 'online') {
|
if (connectionStatus.value === 'online') {
|
||||||
shownConnectionPromptKeys.clear()
|
shownConnectionPromptKeys.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重启期间由重启进度弹窗承载反馈,避免离线提示与进度提示叠加。
|
||||||
|
if (isRestarting.value) return
|
||||||
|
|
||||||
const promptKey =
|
const promptKey =
|
||||||
connectionStatus.value === 'checking'
|
connectionStatus.value === 'checking'
|
||||||
? connectionStatus.value
|
? 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/views/subscribe/SubscribeShareView.vue',
|
||||||
'src/composables/useMediaSubscribe.ts',
|
'src/composables/useMediaSubscribe.ts',
|
||||||
'src/composables/useLlmProviderDirectory.ts',
|
'src/composables/useLlmProviderDirectory.ts',
|
||||||
|
'src/composables/useOfflineStatus.ts',
|
||||||
'src/composables/useTorrentFilter.ts',
|
'src/composables/useTorrentFilter.ts',
|
||||||
|
'src/layouts/default/components/OfflinePage.vue',
|
||||||
'src/components/cards/SubscribeCard.vue',
|
'src/components/cards/SubscribeCard.vue',
|
||||||
'src/components/cards/UserCard.vue',
|
'src/components/cards/UserCard.vue',
|
||||||
'src/components/filter/TorrentFilterBar.vue',
|
'src/components/filter/TorrentFilterBar.vue',
|
||||||
@@ -425,6 +427,18 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
|||||||
lines: 90,
|
lines: 90,
|
||||||
statements: 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': {
|
'src/components/cards/SubscribeCard.vue': {
|
||||||
branches: 75,
|
branches: 75,
|
||||||
functions: 80,
|
functions: 80,
|
||||||
|
|||||||
Reference in New Issue
Block a user