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:
1 parent
24f95a8fd7
commit
eed661bb24
38 files changed
+2365
-108
No files matched your search
@@ -0,0 +1,250 @@
|
||||
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 { defineComponent, h } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import router from '@/router'
|
||||
import { useApi, ApiError } from '@/composables/useApi'
|
||||
|
||||
vi.mock('@/composables/useApi', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
||||
return {
|
||||
...actual,
|
||||
useApi: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const RootComponent = defineComponent({
|
||||
setup() {
|
||||
return () => h(RouterView)
|
||||
},
|
||||
})
|
||||
|
||||
let wrapper: VueWrapper | null = null
|
||||
|
||||
function setUpSession() {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
}
|
||||
|
||||
function mountApp() {
|
||||
wrapper = mount(RootComponent, {
|
||||
global: {
|
||||
plugins: [router, uiPlugin],
|
||||
},
|
||||
})
|
||||
return wrapper
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useApi).mockReset()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
wrapper?.unmount()
|
||||
wrapper = null
|
||||
await router.push('/login')
|
||||
})
|
||||
|
||||
describe('Nuxt UI shell foundation and router guards', () => {
|
||||
it('A public route never triggers a session check', async () => {
|
||||
const getMe = vi.fn()
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/login')
|
||||
const app = mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(getMe).not.toHaveBeenCalled()
|
||||
expect(app.find('[aria-label="Loading"]').exists()).toBe(false)
|
||||
expect(app.find('h1').text()).toBe('Log in')
|
||||
})
|
||||
|
||||
it('The first navigation to a non-public route resolves the session and shows AppBoot meanwhile', async () => {
|
||||
let resolveMe: (value: { verified: boolean }) => void = () => {}
|
||||
const getMe = vi.fn(
|
||||
() => new Promise<{ verified: boolean }>((resolve) => (resolveMe = resolve)),
|
||||
)
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/home')
|
||||
const app = mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(app.find('[aria-label="Loading"]').exists()).toBe(true)
|
||||
expect(app.text()).not.toContain('Home placeholder')
|
||||
|
||||
resolveMe({ verified: true })
|
||||
await flushPromises()
|
||||
|
||||
expect(app.find('[aria-label="Loading"]').exists()).toBe(false)
|
||||
expect(app.text()).toContain('Home placeholder')
|
||||
expect(router.currentRoute.value.path).toBe('/home')
|
||||
})
|
||||
|
||||
it('A second navigation reuses the cached session instead of calling GET /v1/auth/me again', async () => {
|
||||
const getMe = vi.fn().mockResolvedValue({ verified: true })
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/home')
|
||||
const app = mountApp()
|
||||
await flushPromises()
|
||||
|
||||
await router.push('/article')
|
||||
await flushPromises()
|
||||
|
||||
expect(getMe).toHaveBeenCalledTimes(1)
|
||||
expect(app.find('[aria-label="Loading"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('A verified-only route admits a verified session', async () => {
|
||||
const getMe = vi.fn().mockResolvedValue({ verified: true })
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/article')
|
||||
mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/article')
|
||||
})
|
||||
|
||||
it('A verified-only route redirects an unverified session to the pending page', async () => {
|
||||
const getMe = vi.fn().mockResolvedValue({ verified: false })
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/article')
|
||||
mountApp()
|
||||
|
||||
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/register/pending'))
|
||||
})
|
||||
|
||||
it('A session-only route admits an unverified session', async () => {
|
||||
const getMe = vi.fn().mockResolvedValue({ verified: false })
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/register/pending')
|
||||
mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/register/pending')
|
||||
})
|
||||
|
||||
it('No session redirects to login with the original path and an expired reason', async () => {
|
||||
const getMe = vi.fn().mockRejectedValue(new ApiError(401))
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/article/12/edit')
|
||||
mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.fullPath).toBe(
|
||||
'/login?redirect=/article/12/edit&reason=expired',
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ verified: true, destination: '/home' },
|
||||
{ verified: false, destination: '/register/pending' },
|
||||
])(
|
||||
'The root path resolves depending on the session (verified $verified)',
|
||||
async ({ verified, destination }) => {
|
||||
const getMe = vi.fn().mockResolvedValue({ verified })
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/')
|
||||
mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe(destination)
|
||||
},
|
||||
)
|
||||
|
||||
it('The root path resolves to /login when there is no session', async () => {
|
||||
const getMe = vi.fn().mockRejectedValue(new ApiError(401))
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/')
|
||||
mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
|
||||
it('A session check that fails with a server error renders the Generic error view instead of redirecting', async () => {
|
||||
const getMe = vi.fn().mockRejectedValue(new ApiError(500))
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/')
|
||||
const app = mountApp()
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/')
|
||||
expect(app.find('h1').text()).toBe('Something went wrong')
|
||||
expect(app.text()).toContain(
|
||||
"This page couldn't load. Your saved work is safe. Try again, and if it keeps happening, restart the RenewCMS server.",
|
||||
)
|
||||
expect(app.text()).toContain('Home')
|
||||
|
||||
const primaryButton = app.findAll('button').find((button) => button.text() === 'Try again')
|
||||
await primaryButton?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(getMe).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('A session check that fails with a network error on a non-root verified route also renders the Generic error view', async () => {
|
||||
const getMe = vi.fn().mockRejectedValue(new TypeError('network error'))
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push('/article')
|
||||
const app = mountApp()
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(app.find('[role="alert"] h1').text()).toBe('Something went wrong'),
|
||||
)
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/article')
|
||||
|
||||
const primaryButton = app.findAll('button').find((button) => button.text() === 'Try again')
|
||||
await primaryButton?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(getMe).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ path: '/home', title: 'Home' },
|
||||
{ path: '/article', title: 'Articles' },
|
||||
{ path: '/integration', title: 'Integration' },
|
||||
])(
|
||||
'Placeholder verified route $path renders inside DashboardShell with title $title',
|
||||
async ({ path, title }) => {
|
||||
const getMe = vi.fn().mockResolvedValue({ verified: true })
|
||||
vi.mocked(useApi).mockReturnValue({ getMe })
|
||||
setUpSession()
|
||||
|
||||
await router.push(path)
|
||||
const app = mountApp()
|
||||
await flushPromises()
|
||||
|
||||
const headings = app.findAll('h1')
|
||||
expect(headings.some((heading) => heading.text() === title)).toBe(true)
|
||||
expect(app.find('nav[aria-label="breadcrumb"]').text()).toBe(title)
|
||||
|
||||
const activeLink = app.find(`a[href="${path}"][aria-current="page"]`)
|
||||
expect(activeLink.exists()).toBe(true)
|
||||
},
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user