feat: PrimeVue replaced by Nuxt UI using token files from web/src/theme, with AuthShell, DashboardShell, AppBoot and the route guards from routes.md

This commit is contained in:
fsylvain committed 2026-09-25 17:09:34 +02:00
1 parent 24f95a8fd7
commit eed661bb24
38 files changed
+2365 -108

No files matched your search

@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import uiPlugin from '@nuxt/ui/vue-plugin'
import AuthShell from '../AuthShell.vue'
function mountShell(props: Record<string, unknown>, slots: Record<string, unknown> = {}) {
return mount(AuthShell, {
props,
slots,
global: {
plugins: [uiPlugin],
},
})
}
describe('AuthShell renders its title as the page heading and sets the document title', () => {
it('renders an h1 containing the title and sets document.title', () => {
const wrapper = mountShell({ title: 'Log in' })
expect(wrapper.find('h1').text()).toBe('Log in')
expect(document.title).toBe('Log in · RenewCMS')
})
})
describe('AuthShell renders its notice and footer slots', () => {
it('renders the notice slot in an alert above the title and the footer slot outside the card', () => {
const wrapper = mountShell(
{ title: 'Log in' },
{
notice: '<span>Your session expired.</span>',
footer: '<a href="/register/reset/request">Forgot password?</a>',
},
)
const alertDescription = wrapper.find('[data-slot="description"]')
expect(alertDescription.text()).toBe('Your session expired.')
const html = wrapper.html()
expect(html.indexOf('Your session expired.')).toBeLessThan(html.indexOf('<h1>'))
const footerLink = wrapper.find('a[href="/register/reset/request"]')
expect(footerLink.exists()).toBe(true)
expect(footerLink.text()).toBe('Forgot password?')
})
})
@@ -0,0 +1,68 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import uiPlugin from '@nuxt/ui/vue-plugin'
import router from '@/router'
import { useApi } from '@/composables/useApi'
import DashboardShell from '../DashboardShell.vue'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return {
...actual,
useApi: vi.fn(),
}
})
async function mountShell(path: string) {
setActivePinia(createPinia())
vi.mocked(useApi).mockReturnValue({ getMe: vi.fn().mockResolvedValue({ verified: true }) })
await router.push(path)
const wrapper = mount(DashboardShell, {
global: {
plugins: [router, uiPlugin],
},
})
await flushPromises()
return wrapper
}
afterEach(async () => {
await router.push('/login')
})
describe('DashboardShell renders the three navigation destinations', () => {
it('lists Home, Articles and Integration, with only Articles carrying aria-current', async () => {
const wrapper = await mountShell('/article')
const home = wrapper.find('a[href="/home"]')
const articles = wrapper.find('a[href="/article"]')
const integration = wrapper.find('a[href="/integration"]')
expect(home.exists()).toBe(true)
expect(articles.exists()).toBe(true)
expect(integration.exists()).toBe(true)
expect(articles.attributes('aria-current')).toBe('page')
expect(home.attributes('aria-current')).toBeUndefined()
expect(integration.attributes('aria-current')).toBeUndefined()
})
})
describe('DashboardShell shows the offline banner when the browser reports offline', () => {
it('shows the banner while offline and hides it once back online', async () => {
const onLineSpy = vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(false)
const wrapper = await mountShell('/article')
expect(wrapper.text()).toContain("You're offline")
onLineSpy.mockReturnValue(true)
window.dispatchEvent(new Event('online'))
await flushPromises()
expect(wrapper.text()).not.toContain("You're offline")
onLineSpy.mockRestore()
})
})
@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import ErrorView from '../ErrorView.vue'
function makeRouter() {
return createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: { template: '<div />' } },
{ path: '/home', component: { template: '<div />' } },
{ path: '/article', component: { template: '<div />' } },
],
})
}
async function mountNotFound(breadcrumb?: string) {
const router = makeRouter()
await router.push('/')
return mount(ErrorView, {
props: breadcrumb ? { kind: 'not-found', breadcrumb } : { kind: 'not-found' },
global: {
plugins: [router],
},
})
}
describe('ErrorView not-found kind', () => {
it('renders the not-found copy with a breadcrumb, primary and secondary actions', async () => {
const wrapper = await mountNotFound('Not found')
expect(wrapper.find('nav[aria-label="Breadcrumb"]').text()).toBe('Not found')
expect(wrapper.find('h1').text()).toBe('Page not found')
expect(wrapper.text()).toContain('The link may be mistyped, or the page may have moved.')
const buttons = wrapper.findAll('button')
expect(buttons.some((button) => button.text() === 'Go to Home')).toBe(true)
expect(buttons.some((button) => button.text() === 'All articles')).toBe(true)
})
it('routes the primary action to /home and the secondary action to /article', async () => {
const wrapper = await mountNotFound()
const router = wrapper.vm.$router
const primary = wrapper.findAll('button').find((button) => button.text() === 'Go to Home')
await primary?.trigger('click')
await flushPromises()
expect(router.currentRoute.value.path).toBe('/home')
const secondary = wrapper
.findAll('button')
.find((button) => button.text() === 'All articles')
await secondary?.trigger('click')
await flushPromises()
expect(router.currentRoute.value.path).toBe('/article')
})
})