951b5b3f1c
Verhindert Blank-Screens und Reload-Schleifen beim Wiederöffnen der PWA, indem Recovery nur bei bestätigter SW-Übernahme greift und Navigationen bevorzugt frisch aus dem Netzwerk geladen werden. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
forcePwaRecovery,
|
|
markReloadAttempt,
|
|
recentlyAttemptedReload,
|
|
reconcileServiceWorkerOnStartup,
|
|
reconcileVersionOnStartup
|
|
} from './pwaStartup.js'
|
|
|
|
const STALE_RECOVERY_COUNT_KEY = 'pwa_stale_recovery_count'
|
|
const STALE_RECOVERY_LAST_KEY = 'pwa_stale_recovery_last_ts'
|
|
|
|
describe('pwaStartup reload guards', () => {
|
|
beforeEach(() => {
|
|
sessionStorage.clear()
|
|
})
|
|
|
|
it('blocks repeated reload attempts within the debounce window', () => {
|
|
expect(recentlyAttemptedReload(10_000)).toBe(false)
|
|
markReloadAttempt(10_000)
|
|
expect(recentlyAttemptedReload(12_000)).toBe(true)
|
|
expect(recentlyAttemptedReload(15_000)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('forcePwaRecovery stale counter reset', () => {
|
|
beforeEach(() => {
|
|
sessionStorage.clear()
|
|
vi.unstubAllEnvs()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('clears stale recovery counter before hard recovery reload', async () => {
|
|
vi.stubEnv('DEV', false)
|
|
sessionStorage.setItem(STALE_RECOVERY_COUNT_KEY, '2')
|
|
sessionStorage.setItem(STALE_RECOVERY_LAST_KEY, String(Date.now()))
|
|
|
|
const reload = vi.fn()
|
|
vi.stubGlobal('location', { reload })
|
|
vi.stubGlobal('caches', {
|
|
keys: vi.fn().mockResolvedValue([]),
|
|
delete: vi.fn()
|
|
})
|
|
Object.defineProperty(navigator, 'serviceWorker', {
|
|
configurable: true,
|
|
value: {
|
|
getRegistrations: vi.fn().mockResolvedValue([])
|
|
}
|
|
})
|
|
|
|
await forcePwaRecovery()
|
|
|
|
expect(sessionStorage.getItem(STALE_RECOVERY_COUNT_KEY)).toBeNull()
|
|
expect(sessionStorage.getItem(STALE_RECOVERY_LAST_KEY)).toBeNull()
|
|
expect(reload).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('returns false when hard recovery was just attempted', async () => {
|
|
sessionStorage.setItem('pwa_hard_recovery_ts', String(Date.now()))
|
|
const result = await forcePwaRecovery()
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('reconcileServiceWorkerOnStartup', () => {
|
|
beforeEach(() => {
|
|
sessionStorage.clear()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('returns false in dev mode', async () => {
|
|
vi.stubEnv('DEV', true)
|
|
await expect(reconcileServiceWorkerOnStartup()).resolves.toBe(false)
|
|
})
|
|
|
|
it('returns false when no waiting worker exists', async () => {
|
|
vi.stubEnv('DEV', false)
|
|
Object.defineProperty(navigator, 'serviceWorker', {
|
|
configurable: true,
|
|
value: {
|
|
controller: {},
|
|
getRegistration: vi.fn().mockResolvedValue({
|
|
waiting: null,
|
|
installing: null,
|
|
update: vi.fn().mockResolvedValue(undefined),
|
|
addEventListener: vi.fn()
|
|
}),
|
|
addEventListener: vi.fn()
|
|
}
|
|
})
|
|
|
|
await expect(reconcileServiceWorkerOnStartup()).resolves.toBe(false)
|
|
})
|
|
|
|
it('returns false when waiting worker activation never takes over', async () => {
|
|
vi.useFakeTimers()
|
|
const postMessage = vi.fn()
|
|
const addEventListener = vi.fn()
|
|
vi.stubEnv('DEV', false)
|
|
|
|
Object.defineProperty(navigator, 'serviceWorker', {
|
|
configurable: true,
|
|
value: {
|
|
controller: { scriptURL: '/sw.js?v=1' },
|
|
getRegistration: vi.fn().mockResolvedValue({
|
|
waiting: { postMessage },
|
|
installing: null,
|
|
update: vi.fn().mockResolvedValue(undefined),
|
|
addEventListener: vi.fn()
|
|
}),
|
|
addEventListener
|
|
}
|
|
})
|
|
|
|
const reconcilePromise = reconcileServiceWorkerOnStartup()
|
|
await vi.advanceTimersByTimeAsync(4_000)
|
|
|
|
await expect(reconcilePromise).resolves.toBe(false)
|
|
expect(postMessage).toHaveBeenCalledWith({ type: 'SKIP_WAITING' })
|
|
vi.useRealTimers()
|
|
})
|
|
})
|
|
|
|
describe('reconcileVersionOnStartup', () => {
|
|
beforeEach(() => {
|
|
sessionStorage.clear()
|
|
vi.unstubAllEnvs()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('returns noop in dev mode', async () => {
|
|
vi.stubEnv('DEV', true)
|
|
await expect(reconcileVersionOnStartup()).resolves.toBe('noop')
|
|
})
|
|
|
|
it('returns noop when deployed version matches bundled version', async () => {
|
|
vi.stubEnv('DEV', false)
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ version: '0.1.0.57' })
|
|
}))
|
|
vi.stubGlobal('__APP_VERSION__', '0.1.0.57')
|
|
|
|
await expect(reconcileVersionOnStartup()).resolves.toBe('noop')
|
|
})
|
|
})
|