import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// The module keeps its state at module scope on purpose: the
// beforeinstallprompt listener has to be armed before any panel exists, so
// each test re-imports it from a clean registry.
async function freshUsePwa({ standalone = false, bridge = false, ua = 'Mozilla/5.0 (X11; Linux x86_64) Chrome/140' } = {}) {
vi.resetModules()
vi.stubGlobal('matchMedia', () => ({ matches: standalone, addEventListener: () => {} }))
Object.defineProperty(window.navigator, 'userAgent', { value: ua, configurable: true })
if (bridge) window.NaviAndroid = { notify: () => {}, enableBackgroundMode: () => {} }
else delete window.NaviAndroid
return import('@/composables/usePwa.js')
}
function installPrompt(outcome) {
const event = new Event('beforeinstallprompt')
event.prompt = vi.fn()
event.userChoice = Promise.resolve({ outcome })
window.dispatchEvent(event)
return event
}
describe('usePwa', () => {
beforeEach(() => {
vi.unstubAllGlobals()
})
afterEach(() => {
delete window.NaviAndroid
})
it('asks for manual instructions when the browser has not offered anything', async () => {
const { usePwa } = await freshUsePwa()
expect(usePwa().state.value).toBe('manual')
})
it('switches to the prompt as soon as the browser offers one', async () => {
const { usePwa } = await freshUsePwa()
installPrompt('accepted')
expect(usePwa().state.value).toBe('prompt')
})
it('replays the prompt and marks the app installed when accepted', async () => {
const { usePwa } = await freshUsePwa()
const event = installPrompt('accepted')
const { state, install } = usePwa()
await expect(install()).resolves.toBe('accepted')
expect(event.prompt).toHaveBeenCalledTimes(1)
expect(state.value).toBe('installed')
})
it('drops a dismissed prompt instead of offering it twice', async () => {
const { usePwa } = await freshUsePwa()
installPrompt('dismissed')
const { state, install } = usePwa()
await expect(install()).resolves.toBe('dismissed')
// The event is spent either way, so the page falls back to instructions.
expect(state.value).toBe('manual')
await expect(install()).resolves.toBeNull()
})
it('reports the iOS share-sheet route only on iOS', async () => {
const ios = await freshUsePwa({ ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari/604.1' })
expect(ios.isIos).toBe(true)
const desktop = await freshUsePwa()
expect(desktop.isIos).toBe(false)
})
it('calls it installed when already standing alone', async () => {
const { usePwa } = await freshUsePwa({ standalone: true })
expect(usePwa().state.value).toBe('installed')
})
it('says nothing to install inside the Android app', async () => {
const { usePwa } = await freshUsePwa({ bridge: true })
expect(usePwa().state.value).toBe('android-app')
})
})