import { afterEach, 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 { useApi, type UserRole } from '@/composables/useApi' import type { UpdateStatus } from '@/composables/useUpdate' import { useSessionStore } from '@/stores/session' import SettingsView from '../SettingsView.vue' import { RUNNING_VERSION, STATUS_PATH, findButton, newerAvailable, stubClipboard, upToDate, visibleButtonLabels, DEFAULT_PURGE_SETTINGS, PURGE_SETTINGS_PATH, } from './updateTestKit' vi.mock('@/composables/useApi', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useApi: vi.fn(), } }) const CONTAINER_COMMAND = 'docker compose pull && docker compose up -d' let wrapper: VueWrapper | null = null async function mountSettings(role: UserRole, status: UpdateStatus) { setActivePinia(createPinia()) const session = useSessionStore() session.session = { verified: true, username: 'marta', id: 1, role } session.status = 'ready' const request = vi.fn((path: string) => { if (path === '/v1/settings/') { return Promise.resolve({ version: RUNNING_VERSION }) } if (path === STATUS_PATH) { return Promise.resolve(status) } if (path === PURGE_SETTINGS_PATH) { return Promise.resolve(DEFAULT_PURGE_SETTINGS) } throw new Error(`Unexpected request: ${path}`) }) vi.mocked(useApi).mockReturnValue({ getMe: vi.fn().mockResolvedValue({ verified: true, username: 'marta', id: 1, displayName: '', role, }), request, }) await router.push('/settings') wrapper = mount(SettingsView, { attachTo: document.body, global: { plugins: [router, uiPlugin] }, }) await flushPromises() } function updatesCard() { return document.body.querySelector('[data-testid="updates-card"]')! } function cardText() { return updatesCard().textContent ?? '' } afterEach(async () => { wrapper?.unmount() wrapper = null vi.useRealTimers() await router.push('/login') document.body.innerHTML = '' }) describe('Settings, owner, container, update available', () => { it('shows the pull-image guidance, the command, Copy and the docs link, with no other button', async () => { await mountSettings('owner', newerAvailable({ container: true })) expect(cardText()).toContain('Version 2.3.0-beta is available') expect(cardText()).toContain( 'You have v2.2.0-beta. This site runs in Docker, so it updates by pulling a new image. Run the commands below on the server.', ) expect(updatesCard().querySelector('code')?.textContent).toBe(CONTAINER_COMMAND) const link = Array.from(updatesCard().querySelectorAll('a')).find( (anchor) => anchor.textContent?.trim() === 'How to update', )! expect(link.getAttribute('href')).toBe( 'https://git.example/fsylvain/RenewCMS/src/branch/main/docs/docker.md', ) expect(visibleButtonLabels(updatesCard())).toEqual(["What's new in 2.3.0-beta", 'Copy']) expect(cardText()).not.toContain('renewcms enable-browser-updates') expect(cardText()).not.toContain('They run this once on the server') expect(cardText()).not.toContain("can't update itself from the browser") }) it('copies the container command and reads Copied for 2 s', async () => { const writeText = stubClipboard() vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) await mountSettings('owner', newerAvailable({ container: true })) findButton('Copy')!.click() await flushPromises() expect(writeText).toHaveBeenCalledWith(CONTAINER_COMMAND) expect(findButton('Copied')!.getAttribute('aria-live')).toBe('polite') await vi.advanceTimersByTimeAsync(2000) expect(findButton('Copy')?.textContent?.trim()).toBe('Copy') expect(findButton('Copied')).toBeUndefined() }) }) describe('Settings, owner, container, up to date', () => { it("shows You're up to date with only Check for updates and no container hint", async () => { await mountSettings('owner', upToDate({ container: true })) expect(cardText()).toContain("You're up to date") expect(visibleButtonLabels(updatesCard())).toEqual(['Check for updates']) expect(cardText()).not.toContain('Docker') expect(updatesCard().querySelector('code')).toBeNull() }) }) describe('Settings, admin, container', () => { it('keeps the admin view with no command block', async () => { await mountSettings('admin', newerAvailable({ container: true })) expect(cardText()).toContain('Only the owner can update RenewCMS.') expect(cardText()).not.toContain('Docker') expect(updatesCard().querySelector('code')).toBeNull() expect(visibleButtonLabels(updatesCard())).toEqual([]) }) }) describe('Settings, owner, not container, not prepared', () => { it('still shows the enable-browser-updates command', async () => { await mountSettings('owner', newerAvailable({ container: false })) expect(updatesCard().querySelector('code')?.textContent).toBe( 'renewcms enable-browser-updates', ) expect(cardText()).toContain("This site can't update itself from the browser yet.") expect(cardText()).not.toContain('Docker') }) })