fix: prevent background timer registration after stop (#701)

This commit is contained in:
InfinityPacer
2026-08-23 19:22:50 +08:00
committed by GitHub
parent cec2169c13
commit f2ecc6af73
4 changed files with 716 additions and 3 deletions
@@ -0,0 +1,327 @@
import { useBackground } from '@/composables/useBackground'
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { defineComponent, h, nextTick, ref } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
addBackgroundTimer: vi.fn(),
removeBackgroundTimer: vi.fn(),
}))
vi.mock('@/utils/backgroundManager', () => ({
addBackgroundTimer: mocks.addBackgroundTimer,
removeBackgroundTimer: mocks.removeBackgroundTimer,
}))
const wrappers: VueWrapper[] = []
function deferred<T = void>() {
let resolve!: (value: T | PromiseLike<T>) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve
reject = promiseReject
})
return { promise, resolve, reject }
}
async function settle() {
await nextTick()
await flushPromises()
await nextTick()
}
function mountComposable<T>(createComposable: () => T) {
let result!: T
const Harness = defineComponent({
name: 'UseBackgroundRefreshHarness',
setup() {
result = createComposable()
return () => h('div')
},
})
const wrapper = mount(Harness)
wrappers.push(wrapper)
return { result, wrapper }
}
describe('useBackground useTimer', () => {
beforeEach(() => {
mocks.addBackgroundTimer.mockReset()
mocks.removeBackgroundTimer.mockReset()
})
afterEach(() => {
for (const wrapper of wrappers.splice(0)) wrapper.unmount()
})
it('在组件挂载后注册带完整选项的后台定时器', async () => {
const callback = vi.fn()
mountComposable(() =>
useBackground().useTimer('timer-a', callback, 1000, {
runInBackground: true,
skipInitialRun: true,
}),
)
await nextTick()
expect(mocks.addBackgroundTimer).toHaveBeenCalledOnce()
expect(mocks.addBackgroundTimer).toHaveBeenCalledWith('timer-a', callback, 1000, {
runInBackground: true,
skipInitialRun: true,
})
})
it('在组件卸载时移除后台定时器', async () => {
const { wrapper } = mountComposable(() => useBackground().useTimer('timer-b', vi.fn(), 2000))
await nextTick()
mocks.removeBackgroundTimer.mockClear()
wrapper.unmount()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledOnce()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledWith('timer-b')
})
it('暴露手动移除定时器的操作', async () => {
const { result } = mountComposable(() => useBackground().useTimer('timer-c', vi.fn(), 3000))
await nextTick()
mocks.removeBackgroundTimer.mockClear()
result.remove()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledOnce()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledWith('timer-c')
})
})
describe('useBackground useDataRefresh', () => {
beforeEach(() => {
mocks.addBackgroundTimer.mockReset()
mocks.removeBackgroundTimer.mockReset()
})
afterEach(() => {
for (const wrapper of wrappers.splice(0)) wrapper.unmount()
})
it('immediate 模式先完成首轮加载,再注册跳过初始执行的定时器', async () => {
const loadData = vi.fn().mockResolvedValue(undefined)
mountComposable(() => useBackground().useDataRefresh('data-a', loadData, 4000, true))
expect(loadData).toHaveBeenCalledOnce()
expect(mocks.addBackgroundTimer).not.toHaveBeenCalled()
await settle()
expect(mocks.addBackgroundTimer).toHaveBeenCalledWith('data-a', expect.any(Function), 4000, {
runInBackground: false,
skipInitialRun: true,
})
})
it('非 immediate 模式直接注册定时器且不执行首轮加载', async () => {
const loadData = vi.fn().mockResolvedValue(undefined)
mountComposable(() => useBackground().useDataRefresh('data-b', loadData, 5000, false))
await nextTick()
expect(loadData).not.toHaveBeenCalled()
expect(mocks.addBackgroundTimer).toHaveBeenCalledWith('data-b', expect.any(Function), 5000, {
runInBackground: false,
skipInitialRun: true,
})
})
it('并发 refresh 只允许一个加载执行,并在完成后释放 loading', async () => {
const pending = deferred()
const loadData = vi.fn(() => pending.promise)
const { result } = mountComposable(() => useBackground().useDataRefresh('data-c', loadData, 6000, false))
await nextTick()
const firstRefresh = result.refresh()
const secondRefresh = result.refresh()
expect(loadData).toHaveBeenCalledOnce()
expect(result.loading.value).toBe(true)
pending.resolve()
await Promise.all([firstRefresh, secondRefresh])
expect(result.loading.value).toBe(false)
})
it('收口加载错误并恢复 loading 状态', async () => {
const error = new Error('load failed')
const loadData = vi.fn().mockRejectedValue(error)
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const { result } = mountComposable(() => useBackground().useDataRefresh('data-d', loadData, 7000, false))
await nextTick()
await expect(result.refresh()).resolves.toBeUndefined()
expect(consoleError).toHaveBeenCalledWith('数据刷新失败 [data-d]:', error)
expect(result.loading.value).toBe(false)
})
it('暴露 stop 操作并在卸载时清理定时器', async () => {
const { result, wrapper } = mountComposable(() => useBackground().useDataRefresh('data-e', vi.fn(), 8000, false))
await nextTick()
mocks.removeBackgroundTimer.mockClear()
result.stop()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledWith('data-e')
mocks.removeBackgroundTimer.mockClear()
wrapper.unmount()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledOnce()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledWith('data-e')
})
it('首轮异步加载未完成时卸载,完成后不得重新注册定时器', async () => {
const initialLoad = deferred()
const loadData = vi.fn(() => initialLoad.promise)
const { wrapper } = mountComposable(() => useBackground().useDataRefresh('data-race', loadData, 9000, true))
expect(loadData).toHaveBeenCalledOnce()
wrapper.unmount()
initialLoad.resolve()
await settle()
expect(mocks.addBackgroundTimer).not.toHaveBeenCalled()
})
it('首轮异步加载未完成时 stop,完成后不得重新注册定时器', async () => {
const initialLoad = deferred()
const loadData = vi.fn(() => initialLoad.promise)
const { result } = mountComposable(() => useBackground().useDataRefresh('data-stop-race', loadData, 9000, true))
expect(loadData).toHaveBeenCalledOnce()
result.stop()
initialLoad.resolve()
await settle()
expect(mocks.addBackgroundTimer).not.toHaveBeenCalled()
})
})
describe('useBackground useConditionalDataRefresh', () => {
beforeEach(() => {
mocks.addBackgroundTimer.mockReset()
mocks.removeBackgroundTimer.mockReset()
})
afterEach(() => {
for (const wrapper of wrappers.splice(0)) wrapper.unmount()
})
it.each([
[true, true, true],
[true, false, true],
[false, true, false],
[false, false, false],
])('根据初始 condition=%s 决定是否注册定时器(immediate=%s', async (conditionValue, immediate, shouldStart) => {
const condition = ref(conditionValue)
const { result } = mountComposable(() =>
useBackground().useConditionalDataRefresh('conditional-initial', vi.fn(), condition, 1000, immediate),
)
await nextTick()
expect(mocks.addBackgroundTimer).toHaveBeenCalledTimes(shouldStart ? 1 : 0)
expect(result.isActive.value).toBe(shouldStart)
if (shouldStart) {
expect(mocks.addBackgroundTimer).toHaveBeenCalledWith('conditional-initial', expect.any(Function), 1000, {
runInBackground: false,
skipInitialRun: !immediate,
})
}
})
it('随 condition 切换启动和停止定时器', async () => {
const condition = ref(false)
const { result } = mountComposable(() =>
useBackground().useConditionalDataRefresh('conditional-toggle', vi.fn(), condition),
)
await nextTick()
condition.value = true
await nextTick()
expect(mocks.addBackgroundTimer).toHaveBeenCalledOnce()
expect(result.isActive.value).toBe(true)
condition.value = false
await nextTick()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledOnce()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledWith('conditional-toggle')
expect(result.isActive.value).toBe(false)
})
it('start 和 stop 具有幂等性', async () => {
const condition = ref(true)
const { result } = mountComposable(() =>
useBackground().useConditionalDataRefresh('conditional-idempotent', vi.fn(), condition),
)
await nextTick()
result.stop()
mocks.addBackgroundTimer.mockClear()
mocks.removeBackgroundTimer.mockClear()
result.start()
result.start()
expect(mocks.addBackgroundTimer).toHaveBeenCalledOnce()
result.stop()
result.stop()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledOnce()
})
it('仅在 condition 为 true 时允许 refresh,并保持单飞行为', async () => {
const condition = ref(false)
const pending = deferred()
const loadData = vi.fn(() => pending.promise)
const { result } = mountComposable(() =>
useBackground().useConditionalDataRefresh('conditional-refresh', loadData, condition, 1000, false),
)
await nextTick()
await result.refresh()
expect(loadData).not.toHaveBeenCalled()
condition.value = true
const firstRefresh = result.refresh()
const secondRefresh = result.refresh()
expect(loadData).toHaveBeenCalledOnce()
expect(result.loading.value).toBe(true)
pending.resolve()
await Promise.all([firstRefresh, secondRefresh])
expect(result.loading.value).toBe(false)
})
it('收口错误并在卸载时清理活动定时器', async () => {
const condition = ref(true)
const error = new Error('conditional load failed')
const loadData = vi.fn().mockRejectedValue(error)
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const { result, wrapper } = mountComposable(() =>
useBackground().useConditionalDataRefresh('conditional-error', loadData, condition),
)
await nextTick()
await expect(result.refresh()).resolves.toBeUndefined()
expect(consoleError).toHaveBeenCalledWith('条件数据刷新失败 [conditional-error]:', error)
expect(result.loading.value).toBe(false)
mocks.removeBackgroundTimer.mockClear()
wrapper.unmount()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledOnce()
expect(mocks.removeBackgroundTimer).toHaveBeenCalledWith('conditional-error')
})
})
@@ -0,0 +1,372 @@
import { mount, type VueWrapper } from '@vue/test-utils'
import { defineComponent, h, ref, type Ref } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useBackground } from '@/composables/useBackground'
type SSEStatus = 'idle' | 'connecting' | 'open' | 'error' | 'closed'
type StatusListener = (status: SSEStatus) => void
type MessageListener = (event: MessageEvent) => void
interface MockManager {
url: string
options?: Record<string, unknown>
readyState: number
addStatusListener: ReturnType<typeof vi.fn>
removeStatusListener: ReturnType<typeof vi.fn>
addMessageListener: ReturnType<typeof vi.fn>
removeMessageListener: ReturnType<typeof vi.fn>
forceReconnect: ReturnType<typeof vi.fn>
emitStatus: (status: SSEStatus) => void
closed: boolean
}
const { closeIndependentManager, getCurrentLocaleMock, getIndependentManager, managers } = vi.hoisted(() => {
const managers = new Map<string, MockManager>()
const createManager = (url: string, options?: Record<string, unknown>): MockManager => {
let status: SSEStatus = 'idle'
let statusListener: StatusListener | undefined
let messageListener: MessageListener | undefined
const manager = {
url,
options,
readyState: 0,
closed: false,
addStatusListener: vi.fn((_id: string, listener: StatusListener, emitCurrent = true) => {
statusListener = listener
if (emitCurrent) listener(status)
}),
removeStatusListener: vi.fn(() => {
statusListener = undefined
}),
addMessageListener: vi.fn((_id: string, listener: MessageListener) => {
messageListener = listener
}),
removeMessageListener: vi.fn(() => {
messageListener = undefined
}),
forceReconnect: vi.fn(),
emitStatus: (nextStatus: SSEStatus) => {
status = nextStatus
statusListener?.(nextStatus)
},
}
void messageListener
return manager
}
const getIndependentManager = vi.fn((url: string, listenerId: string, options?: Record<string, unknown>) => {
const key = `${url}::${listenerId}`
const existing = managers.get(key)
if (existing) return existing
const manager = createManager(url, options)
managers.set(key, manager)
return manager
})
const closeIndependentManager = vi.fn((url: string, listenerId: string) => {
const key = `${url}::${listenerId}`
const manager = managers.get(key)
if (manager) manager.closed = true
managers.delete(key)
})
return {
closeIndependentManager,
getCurrentLocaleMock: vi.fn(() => 'zh-CN'),
getIndependentManager,
managers,
}
})
vi.mock('@/utils/sseManager', () => ({
sseManagerSingleton: {
closeIndependentManager,
getIndependentManager,
},
}))
vi.mock('@/plugins/i18n', () => ({
getCurrentLocale: getCurrentLocaleMock,
}))
type Background = ReturnType<typeof useBackground>
type UseSSEResult = ReturnType<Background['useSSE']>
type UseDelayedSSEResult = ReturnType<Background['useDelayedSSE']>
type UseProgressSSEResult = ReturnType<Background['useProgressSSE']>
const wrappers: VueWrapper[] = []
function managerKey(url: string, listenerId: string) {
return `${url}::${listenerId}`
}
function mountSSE(url = '/api/v1/events', listenerId = 'sse-listener', options?: Parameters<Background['useSSE']>[3]) {
let result!: UseSSEResult
const messageHandler = vi.fn<(event: MessageEvent) => void>()
const Component = defineComponent({
setup() {
result = useBackground().useSSE(url, messageHandler, listenerId, options)
return () => h('div')
},
})
const wrapper = mount(Component)
wrappers.push(wrapper)
return { messageHandler, result, wrapper }
}
function mountDelayedSSE(url = '/api/v1/delayed-events', listenerId = 'delayed-listener', delay = 3_000) {
let result!: UseDelayedSSEResult
const messageHandler = vi.fn<(event: MessageEvent) => void>()
const Component = defineComponent({
setup() {
result = useBackground().useDelayedSSE(url, messageHandler, listenerId, delay)
return () => h('div')
},
})
const wrapper = mount(Component)
wrappers.push(wrapper)
return { messageHandler, result, wrapper }
}
function mountProgressSSE(active: Ref<boolean>, url = '/api/v1/progress', listenerId = 'progress-listener') {
let result!: UseProgressSSEResult
const messageHandler = vi.fn<(event: MessageEvent) => void>()
const Component = defineComponent({
setup() {
result = useBackground().useProgressSSE(url, messageHandler, listenerId, active)
return () => h('div')
},
})
const wrapper = mount(Component)
wrappers.push(wrapper)
return { messageHandler, result, wrapper }
}
function unmount(wrapper: VueWrapper) {
const index = wrappers.indexOf(wrapper)
if (index >= 0) wrappers.splice(index, 1)
wrapper.unmount()
}
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
managers.clear()
getCurrentLocaleMock.mockReturnValue('zh-CN')
})
afterEach(() => {
for (const wrapper of wrappers.splice(0)) wrapper.unmount()
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('useBackground SSE composables', () => {
describe('useSSE', () => {
it('延迟注册消息监听器并透传 manager options', async () => {
const options = { connectDelay: 25, reconnectDelay: 800 }
const { messageHandler, result } = mountSSE('/api/v1/events', 'events', options)
const manager = managers.get(managerKey('/api/v1/events', 'events'))!
expect(getIndependentManager).toHaveBeenCalledWith('/api/v1/events', 'events', options)
expect(manager.addMessageListener).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(24)
expect(manager.addMessageListener).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(manager.addMessageListener).toHaveBeenCalledWith('events', messageHandler)
unmount(wrappers[0])
expect(result.isConnected.value).toBe(false)
})
it('映射连接状态并透传 readyState 与 forceReconnect', () => {
const { result, wrapper } = mountSSE()
const manager = managers.get(managerKey('/api/v1/events', 'sse-listener'))!
manager.emitStatus('connecting')
expect(result.isConnected.value).toBe(false)
manager.emitStatus('open')
expect(result.isConnected.value).toBe(true)
manager.emitStatus('closed')
expect(result.isConnected.value).toBe(false)
manager.readyState = 1
expect(result.readyState()).toBe(1)
result.forceReconnect()
expect(manager.forceReconnect).toHaveBeenCalledOnce()
unmount(wrapper)
})
it('隔离延迟连接时的 manager 异常', async () => {
const { result, wrapper } = mountSSE('/api/v1/events', 'throwing-listener', { connectDelay: 1 })
const manager = managers.get(managerKey('/api/v1/events', 'throwing-listener'))!
const error = new Error('connect failed')
manager.addMessageListener.mockImplementationOnce(() => {
throw error
})
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
await vi.advanceTimersByTimeAsync(1)
expect(consoleError).toHaveBeenCalledWith('SSE连接建立失败:', error)
expect(result.isConnected.value).toBe(false)
unmount(wrapper)
})
it('close 是幂等的', () => {
const { result, wrapper } = mountSSE()
const manager = managers.get(managerKey('/api/v1/events', 'sse-listener'))!
result.close()
result.close()
expect(manager.removeStatusListener).toHaveBeenCalledOnce()
expect(manager.removeMessageListener).toHaveBeenCalledOnce()
expect(closeIndependentManager).toHaveBeenCalledOnce()
unmount(wrapper)
})
it('提前卸载会取消延迟连接并清理 manager', async () => {
const { wrapper } = mountSSE('/api/v1/events', 'early-unmount', { connectDelay: 100 })
const manager = managers.get(managerKey('/api/v1/events', 'early-unmount'))!
unmount(wrapper)
await vi.advanceTimersByTimeAsync(100)
expect(manager.addMessageListener).not.toHaveBeenCalled()
expect(manager.removeStatusListener).toHaveBeenCalledOnce()
expect(manager.removeMessageListener).toHaveBeenCalledOnce()
expect(closeIndependentManager).toHaveBeenCalledWith('/api/v1/events', 'early-unmount')
})
})
describe('useDelayedSSE', () => {
it('使用自定义 delay 注册消息监听器', async () => {
const { messageHandler, result, wrapper } = mountDelayedSSE('/api/v1/delayed', 'delayed', 40)
const manager = managers.get(managerKey('/api/v1/delayed', 'delayed'))!
await vi.advanceTimersByTimeAsync(39)
expect(manager.addMessageListener).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
expect(manager.addMessageListener).toHaveBeenCalledWith('delayed', messageHandler)
expect(result.isConnected.value).toBe(false)
unmount(wrapper)
})
it('映射状态并在卸载时清理监听器和延迟任务', async () => {
const { result, wrapper } = mountDelayedSSE('/api/v1/delayed', 'delayed-state', 100)
const manager = managers.get(managerKey('/api/v1/delayed', 'delayed-state'))!
manager.emitStatus('open')
expect(result.isConnected.value).toBe(true)
unmount(wrapper)
await vi.advanceTimersByTimeAsync(100)
expect(manager.addMessageListener).not.toHaveBeenCalled()
expect(manager.removeStatusListener).toHaveBeenCalledOnce()
expect(manager.removeMessageListener).toHaveBeenCalledOnce()
expect(closeIndependentManager).toHaveBeenCalledWith('/api/v1/delayed', 'delayed-state')
expect(result.isConnected.value).toBe(false)
})
})
describe('useProgressSSE', () => {
it('按活动状态惰性创建 manager,并避免重复 start', () => {
const active = ref(false)
const { result, wrapper } = mountProgressSSE(active)
result.start()
expect(getIndependentManager).not.toHaveBeenCalled()
active.value = true
result.start()
result.start()
const managerUrl = new URL('/api/v1/progress', window.location.origin)
managerUrl.searchParams.set('locale', 'zh-CN')
const manager = managers.get(managerKey(managerUrl.toString(), 'progress-listener'))!
expect(getIndependentManager).toHaveBeenCalledOnce()
expect(manager.addMessageListener).toHaveBeenCalledOnce()
expect(result.manager).toBe(manager)
unmount(wrapper)
})
it('为已有 query 的 URL 追加 locale,并传递进度连接选项', () => {
const active = ref(true)
const { result, wrapper } = mountProgressSSE(active, '/api/v1/progress?task=42', 'progress-query')
result.start()
const expectedUrl = new URL('/api/v1/progress?task=42', window.location.origin)
expectedUrl.searchParams.set('locale', 'zh-CN')
expect(getIndependentManager).toHaveBeenCalledWith(expectedUrl.toString(), 'progress-query', {
backgroundCloseDelay: 1_000,
reconnectDelay: 1_000,
maxReconnectAttempts: 5,
})
unmount(wrapper)
})
it('URL 无法解析时保留原始地址并使用 query fallback', () => {
const active = ref(true)
const malformedUrl = 'https://[invalid?task=42'
const { result, wrapper } = mountProgressSSE(active, malformedUrl, 'progress-fallback')
result.start()
expect(getIndependentManager).toHaveBeenCalledWith(
`${malformedUrl}&locale=zh-CN`,
'progress-fallback',
expect.any(Object),
)
unmount(wrapper)
})
it('stop(false) 保留 managerstop(true) 销毁 manager', () => {
const active = ref(true)
const { result, wrapper } = mountProgressSSE(active, '/api/v1/progress', 'progress-stop')
result.start()
const manager = result.manager!
const managerUrl = new URL('/api/v1/progress', window.location.origin)
managerUrl.searchParams.set('locale', 'zh-CN')
result.stop(false)
expect(manager.removeMessageListener).toHaveBeenCalledOnce()
expect(closeIndependentManager).not.toHaveBeenCalled()
expect(result.manager).toBe(manager)
result.start()
expect(manager.addMessageListener).toHaveBeenCalledTimes(2)
result.stop(true)
expect(closeIndependentManager).toHaveBeenCalledWith(managerUrl.toString(), 'progress-stop')
expect(result.manager).toBeNull()
unmount(wrapper)
})
it('setup scope 卸载时停止监听并销毁 manager', () => {
const active = ref(true)
const { result, wrapper } = mountProgressSSE(active, '/api/v1/progress', 'progress-unmount')
result.start()
const manager = result.manager!
unmount(wrapper)
expect(manager.removeMessageListener).toHaveBeenCalledOnce()
expect(closeIndependentManager).toHaveBeenCalledOnce()
expect(result.manager).toBeNull()
})
})
})
+10 -3
View File
@@ -263,6 +263,7 @@ export function useBackground() {
immediate: boolean = true,
) => {
const loading = ref(false)
let timerStopped = false
const wrappedLoadData = async () => {
if (loading.value) return
@@ -282,20 +283,26 @@ export function useBackground() {
await wrappedLoadData()
}
if (timerStopped) return
addBackgroundTimer(id, wrappedLoadData, interval, {
runInBackground: false, // 后台不刷新数据
skipInitialRun: true, // 已经手动执行过了
})
})
onUnmounted(() => {
const stopTimer = () => {
// stop 或卸载后的异步 continuation 不得重新注册全局 timer。
timerStopped = true
removeBackgroundTimer(id)
})
}
onUnmounted(stopTimer)
return {
loading,
refresh: wrappedLoadData,
stop: () => removeBackgroundTimer(id),
stop: stopTimer,
}
}
+7
View File
@@ -345,6 +345,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({
'src/composables/useMediaSubscribe.ts',
'src/composables/useLlmProviderDirectory.ts',
'src/composables/useDashboardMediaGridCapacity.ts',
'src/composables/useBackground.ts',
'src/composables/useKeepAliveRefresh.ts',
'src/composables/useOfflineStatus.ts',
'src/composables/useScheduleProgress.ts',
@@ -446,6 +447,12 @@ export default defineConfig(({ command, mode, isPreview }) => ({
lines: 90,
statements: 90,
},
'src/composables/useBackground.ts': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'src/composables/useKeepAliveRefresh.ts': {
branches: 85,
functions: 90,