Newer
Older
navi-1 / webclient / tests / unit / composables / usePwa.test.js
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')
  })
})