194 lines
6.2 KiB
TypeScript
194 lines
6.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import uiPlugin from '@nuxt/ui/vue-plugin'
|
|
import router from '@/router'
|
|
import type { UserRole } from '@/composables/useApi'
|
|
import type { UpdateStatus } from '@/composables/useUpdate'
|
|
import { DISMISSED_VERSION_KEY, useUpdateStore } from '@/stores/update'
|
|
import { useSessionStore } from '@/stores/session'
|
|
import { DESKTOP_WIDTH, MOBILE_WIDTH, setViewportWidth } from '@/components/__tests__/viewport'
|
|
import HomeView from '../HomeView.vue'
|
|
import { NEW_TAG, newerAvailable, upToDate } from './updateTestKit'
|
|
|
|
function mockResponse(status: number, body: unknown = null) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
headers: { get: () => null },
|
|
json: async () => body,
|
|
} as unknown as Response
|
|
}
|
|
|
|
let wrapper: VueWrapper | null = null
|
|
|
|
interface MountOptions {
|
|
role: UserRole
|
|
status: UpdateStatus
|
|
width?: number
|
|
}
|
|
|
|
async function mountHome({ role, status, width = DESKTOP_WIDTH }: MountOptions) {
|
|
setViewportWidth(width)
|
|
setActivePinia(createPinia())
|
|
const session = useSessionStore()
|
|
session.session = { verified: true, username: 'marta', role }
|
|
session.status = 'ready'
|
|
global.fetch = vi.fn((url: string) => {
|
|
if (url === '/v1/article/?format=html') {
|
|
return Promise.resolve(mockResponse(200, []))
|
|
}
|
|
if (url === '/v1/system/update') {
|
|
return Promise.resolve(mockResponse(200, status))
|
|
}
|
|
throw new Error(`Unexpected fetch call: ${url}`)
|
|
}) as unknown as typeof fetch
|
|
await router.push('/home')
|
|
wrapper = mount(HomeView, {
|
|
attachTo: document.body,
|
|
global: { plugins: [router, uiPlugin] },
|
|
})
|
|
await flushPromises()
|
|
return wrapper
|
|
}
|
|
|
|
function banner() {
|
|
return document.body.querySelector('[aria-label="Update available"][role="region"]')
|
|
}
|
|
|
|
function settingsNavItem() {
|
|
return document.body.querySelector('nav[aria-label="Main"] a[href="/settings"]')!
|
|
}
|
|
|
|
function mobileDot() {
|
|
return document.body.querySelector('[role="img"][aria-label="Update available"]')
|
|
}
|
|
|
|
function buttonByLabel(label: string) {
|
|
return Array.from(document.body.querySelectorAll('button')).find(
|
|
(button) =>
|
|
button.textContent?.trim() === label || button.getAttribute('aria-label') === label,
|
|
)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
wrapper?.unmount()
|
|
wrapper = null
|
|
await router.push('/login')
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
describe('Home banner for the owner, update available', () => {
|
|
it('shows the info region above the content with copy and both buttons', async () => {
|
|
await mountHome({ role: 'owner', status: newerAvailable() })
|
|
|
|
const region = banner()!
|
|
expect(region).not.toBeNull()
|
|
expect(region.textContent).toContain('Version 2.3.0-beta is available')
|
|
expect(region.textContent).toContain(
|
|
'Update it from Settings. It takes about 2 minutes and the site stays online.',
|
|
)
|
|
expect(region.textContent).toContain('Go to Settings')
|
|
expect(region.querySelector('button[aria-label="Dismiss"]')).not.toBeNull()
|
|
const stats = document.body.querySelector('[data-testid="greeting"]')!
|
|
expect(
|
|
stats.compareDocumentPosition(region) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
).toBeTruthy()
|
|
})
|
|
|
|
it('goes to /settings from Go to Settings', async () => {
|
|
await mountHome({ role: 'owner', status: newerAvailable() })
|
|
|
|
buttonByLabel('Go to Settings')!.click()
|
|
|
|
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/settings'))
|
|
})
|
|
|
|
it('hides on dismiss, remembers the version, and stays hidden after a remount', async () => {
|
|
await mountHome({ role: 'owner', status: newerAvailable() })
|
|
|
|
buttonByLabel('Dismiss')!.click()
|
|
await flushPromises()
|
|
|
|
expect(banner()).toBeNull()
|
|
expect(localStorage.getItem(DISMISSED_VERSION_KEY)).toBe(NEW_TAG)
|
|
|
|
wrapper?.unmount()
|
|
document.body.innerHTML = ''
|
|
await mountHome({ role: 'owner', status: newerAvailable() })
|
|
expect(banner()).toBeNull()
|
|
})
|
|
|
|
it('shows again when latest changes to a newer version', async () => {
|
|
localStorage.setItem(DISMISSED_VERSION_KEY, NEW_TAG)
|
|
await mountHome({ role: 'owner', status: newerAvailable({ latest: 'v2.4.0-beta' }) })
|
|
|
|
expect(banner()).not.toBeNull()
|
|
expect(banner()!.textContent).toContain('Version 2.4.0-beta is available')
|
|
})
|
|
|
|
it('is absent for an admin and when nothing is newer', async () => {
|
|
await mountHome({ role: 'admin', status: newerAvailable() })
|
|
expect(banner()).toBeNull()
|
|
|
|
wrapper?.unmount()
|
|
document.body.innerHTML = ''
|
|
await mountHome({ role: 'owner', status: upToDate() })
|
|
expect(banner()).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Settings sidebar pill and mobile dot', () => {
|
|
it('shows the Update pill and the dot for the owner when available', async () => {
|
|
await mountHome({ role: 'owner', status: newerAvailable() })
|
|
|
|
expect(settingsNavItem().textContent).toContain('Update')
|
|
expect(mobileDot()).not.toBeNull()
|
|
})
|
|
|
|
it('keeps the pill and the dot after the banner is dismissed', async () => {
|
|
await mountHome({ role: 'owner', status: newerAvailable() })
|
|
|
|
buttonByLabel('Dismiss')!.click()
|
|
await flushPromises()
|
|
|
|
expect(banner()).toBeNull()
|
|
expect(settingsNavItem().textContent).toContain('Update')
|
|
expect(mobileDot()).not.toBeNull()
|
|
})
|
|
|
|
it('shows the dot on the mobile menu icon', async () => {
|
|
await mountHome({ role: 'owner', status: newerAvailable(), width: MOBILE_WIDTH })
|
|
|
|
expect(mobileDot()).not.toBeNull()
|
|
expect(banner()).not.toBeNull()
|
|
})
|
|
|
|
it.each([
|
|
{ name: 'an admin', role: 'admin' as const, status: newerAvailable() },
|
|
{ name: 'an owner who is up to date', role: 'owner' as const, status: upToDate() },
|
|
])('shows no pill and no dot for $name', async ({ role, status }) => {
|
|
await mountHome({ role, status })
|
|
|
|
expect(settingsNavItem().textContent).not.toContain('Update')
|
|
expect(mobileDot()).toBeNull()
|
|
expect(banner()).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Update store cue rules', () => {
|
|
it('does not request the status for a non-owner on any page', async () => {
|
|
await mountHome({ role: 'admin', status: newerAvailable() })
|
|
|
|
const requested = vi
|
|
.mocked(global.fetch)
|
|
.mock.calls.some(([url]) => url === '/v1/system/update')
|
|
expect(requested).toBe(false)
|
|
expect(useUpdateStore().status).toBeNull()
|
|
})
|
|
})
|