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)
})
})