feat(SPA): schema list and the sidebar Content group

This commit is contained in:
Florian Sylvain committed 2026-10-19 19:34:45 +02:00
1 parent 73a75d5cb9
commit 3c67cd2314
34 files changed
+2181 -18

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
import { vi } from 'vitest'
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { defineComponent, h } from 'vue'
import { RouterView } from 'vue-router'
import uiPlugin from '@nuxt/ui/vue-plugin'
import UApp from '@nuxt/ui/components/App.vue'
import DashboardShell from '@/components/DashboardShell.vue'
import router from '@/router'
import { ApiError, NetworkError, useApi } from '@/composables/useApi'
import { useContentTypesStore } from '@/stores/contentTypes'
import { useSessionStore } from '@/stores/session'
import { DESKTOP_WIDTH, setViewportWidth } from '@/components/__tests__/viewport'
export type Responder = () => unknown
export const customTypes = [
{
id: 1,
key: 'events',
name: 'Events',
icon: 'files',
fields: new Array(7),
updated_at: '2026-10-15T09:30:00Z',
},
{
id: 2,
key: 'recipes',
name: 'Recipes',
icon: null,
fields: new Array(4),
updated_at: '2026-10-02T09:30:00Z',
},
{
id: 3,
key: 'press-mentions',
name: 'Press mentions',
icon: null,
fields: new Array(5),
updated_at: '2026-09-18T09:30:00Z',
},
].map((type) => ({ ...type, created_at: '2026-09-01T09:30:00Z' }))
export const builtinTypes = [
['articles', 'Articles', 5],
['authors', 'Authors', 5],
['sites', 'Sites', 8],
['media', 'Media', 6],
['users', 'Users', 6],
].map(([key, name, count]) => ({ key, name, fields: new Array(count) }))
export function manyTypes(count: number) {
return Array.from({ length: count }, (_, index) => ({
...customTypes[0]!,
id: index + 1,
key: `type-${index + 1}`,
name: `Type ${index + 1}`,
icon: null,
}))
}
export function failWith(status: number): Responder {
return () => {
throw new ApiError(status)
}
}
export const networkFailure: Responder = () => {
throw new NetworkError()
}
export interface SchemaMount {
role?: string
width?: number
content?: Responder
builtin?: Responder
verified?: boolean
actions?: string[]
}
let wrapper: VueWrapper | undefined
export const calls: string[] = []
export async function mountAt(path: string, options: SchemaMount = {}, shellOnly = false) {
calls.length = 0
localStorage.clear()
setViewportWidth(options.width ?? DESKTOP_WIDTH)
const pinia = createPinia()
setActivePinia(pinia)
if (options.actions) {
const actions = options.actions
useContentTypesStore().$onAction(({ name }) => {
actions.push(name)
})
}
const session = useSessionStore()
session.session = {
verified: options.verified ?? true,
username: 'marta',
id: 1,
email: 'marta@renew.example',
displayName: 'Marta Alves',
role: (options.role ?? 'owner') as never,
}
session.status = 'ready'
const request = vi.fn(async (url: string) => {
calls.push(url)
if (url === '/v1/content-type') {
return (options.content ?? (() => customTypes))()
}
if (url === '/v1/builtin-type') {
return (options.builtin ?? (() => builtinTypes))()
}
return undefined
})
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn().mockResolvedValue({ verified: true, username: 'marta' }),
request,
} as unknown as ReturnType<typeof useApi>)
await router.push(path)
wrapper = mount(
defineComponent({
render: () =>
h(UApp, null, {
default: () => (shellOnly ? h(DashboardShell) : h(RouterView)),
}),
}),
{
attachTo: document.body,
global: { plugins: [router, uiPlugin] },
},
)
await flushPromises()
return wrapper
}
export async function unmountAll() {
wrapper?.unmount()
wrapper = undefined
vi.useRealTimers()
await router.push('/login')
document.body.innerHTML = ''
}
export function pending() {
let resolve!: (value: unknown) => void
const promise = new Promise((done) => (resolve = done))
return { promise, resolve }
}
export function text(element: Element | null | undefined) {
if (!element) {
return ''
}
const parts: string[] = []
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT)
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
parts.push(node.textContent ?? '')
}
return parts.join(' ').replace(/\s+/g, ' ').trim()
}
export function button(label: string) {
return [...document.body.querySelectorAll('button')].find(
(candidate) => text(candidate) === label,
)
}