Newer
Older
navi-1 / webclient / tests / unit / components / settings / appPanel.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'

// The install state is the composable's business and is tested on its own
// (tests/unit/composables/usePwa.test.js); here it is a plain switch.
const pwa = { state: 'manual', isIos: false }
const install = vi.fn()
vi.mock('@/composables/usePwa.js', () => ({
  usePwa: () => ({ state: pwa.state, isIos: pwa.isIos, install }),
}))

import AppPanel from '@/components/settings/AppPanel.vue'

const APK = {
  versionCode: 1,
  versionName: '1.0',
  size: 2006296,
  sha256: '6c53720ed7c909ded03584cc948b2b56d086f43b09391a904893102285594f50',
  builtAt: '2026-10-09T19:56:24+03:00',
}

function mountPanel() {
  return mount(AppPanel, { attachTo: document.body })
}

describe('AppPanel', () => {
  beforeEach(() => {
    pwa.state = 'manual'
    pwa.isIos = false
    install.mockReset()
    vi.stubGlobal(
      'fetch',
      vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(APK) }))
    )
    Object.defineProperty(window, 'location', {
      value: { ...window.location, origin: 'https://navi.gnexus.space', assign: vi.fn() },
      configurable: true,
      writable: true,
    })
  })

  it('gives per-platform install steps when the browser offers no prompt', async () => {
    const wrapper = mountPanel()
    const text = wrapper.text()

    expect(text).toContain('Chrome or Edge')
    expect(text).toContain('Android')
    expect(text).not.toContain('Add to Home Screen')
  })

  it('sends an iOS reader to the share sheet instead', async () => {
    pwa.isIos = true
    const wrapper = mountPanel()

    expect(wrapper.text()).toContain('Add to Home Screen')
    expect(wrapper.text()).not.toContain('Chrome or Edge')
  })

  it('installs through the native prompt when one was captured', async () => {
    pwa.state = 'prompt'
    const wrapper = mountPanel()

    const button = wrapper.findAll('button').find(b => b.text().includes('Install'))
    expect(button).toBeTruthy()
    await button.trigger('click')
    expect(install).toHaveBeenCalledTimes(1)
  })

  it('says the app is already installed', () => {
    pwa.state = 'installed'
    expect(mountPanel().text()).toContain('running in its own window')
  })

  it('points an Android app user at the APK instead of the prompt', () => {
    pwa.state = 'android-app'
    expect(mountPanel().text()).toContain('already installed')
  })

  it('publishes the version, size and checksum of the APK it offers', async () => {
    const wrapper = mountPanel()
    await flushPromises()

    const text = wrapper.text()
    expect(text).toContain('1.0')
    expect(text).toContain('1.9 MB')
    expect(text).toContain('2026')
    expect(text).toContain(APK.sha256.slice(0, 16))
    expect(text).toContain('https://navi.gnexus.space')
  })

  it('downloads the APK in a way the Android app hands to the browser', async () => {
    const wrapper = mountPanel()
    await flushPromises()

    const button = wrapper.findAll('button').find(b => b.text().includes('Download APK'))
    await button.trigger('click')
    expect(window.location.assign).toHaveBeenCalledWith('/download/navi-android.apk?download=1')
  })

  it('explains itself when no APK was ever published', async () => {
    vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ ok: false })))
    const wrapper = mountPanel()
    await flushPromises()

    expect(wrapper.text()).toContain('not published on this server')
    expect(wrapper.findAll('button').some(b => b.text().includes('Download'))).toBe(false)
  })
})