Files
blog/tests/integration/api.test.ts
T
davide 33b49c5dbb Add hand-rolled English support (/en/*) alongside the Italian site
PocketBase gains optional, manually-authored English fields on articles
and categories (same record, same slug), and the frontend serves an /en
counterpart of every dynamic route via thin page wrappers around shared
view components — no i18n library, consistent with the project's existing
minimalism stance for a two-locale site with ~20 UI strings.

An article/category with no translation 404s cleanly on its own /en
detail page and is filtered out of /en listings, and the header's
language-switch link falls back to the English blog index rather than a
dead link; resolving that requires a global route middleware, since the
layout's header renders before the page content in document order and so
can't react to state a page component sets during its own async setup.
2026-09-11 13:53:17 +02:00

137 lines
5.1 KiB
TypeScript

import { fileURLToPath } from 'node:url'
import { afterAll, describe, expect, it } from 'vitest'
import { $fetch, fetch, setup } from '@nuxt/test-utils/e2e'
import { startTestPocketbase } from '../support/pocketbase'
import type { Article, ArticleSummary, Category, Paginated } from '../../frontend/shared/types/blog'
// setup() registers its own beforeAll/afterAll hooks, so — like startTestPocketbase()
// below — it must run at module scope (top-level await), not nested inside another
// hook: Vitest only picks up hooks registered during the initial collection phase.
const pb = await startTestPocketbase()
await setup({
rootDir: fileURLToPath(new URL('../../frontend', import.meta.url)),
server: true,
nuxtConfig: {
runtimeConfig: {
pocketbaseUrl: pb.url,
public: { pocketbaseUrl: pb.url },
},
},
})
async function expectStatus(promise: Promise<unknown>, statusCode: number) {
const error = await promise.catch((caught) => caught)
expect(error).toBeDefined()
expect(error?.statusCode ?? error?.response?.status).toBe(statusCode)
}
afterAll(async () => {
await pb.stop()
})
describe('GET /content/articles', () => {
it('lists only published articles, newest first', async () => {
const response = await $fetch<Paginated<ArticleSummary>>('/content/articles')
expect(response.items.map((a) => a.slug)).toEqual(['secondo-articolo', 'articolo-pubblicato'])
expect(response.total).toBe(2)
})
it('filters by category slug', async () => {
const response = await $fetch<Paginated<ArticleSummary>>('/content/articles', {
query: { category: 'attualita' },
})
expect(response.items.map((a) => a.slug)).toEqual(['secondo-articolo'])
})
})
describe('GET /content/articles/:slug', () => {
it('returns the full article with rendered html, summary and author', async () => {
const article = await $fetch<Article>('/content/articles/articolo-pubblicato')
expect(article.title).toBe('Articolo pubblicato')
expect(article.html).toContain('<strong>markdown</strong>')
expect(article.summary.length).toBeGreaterThan(0)
expect(article.author).toBe('Redazione')
expect(article.category?.slug).toBe('economia')
expect(article.hasTranslation).toBe(true)
})
it('404s for an unknown slug', async () => {
await expectStatus($fetch('/content/articles/does-not-exist'), 404)
})
it('404s for a draft (unpublished) slug', async () => {
await expectStatus($fetch('/content/articles/articolo-bozza'), 404)
})
it('reports hasTranslation false for an article with no English content', async () => {
const article = await $fetch<Article>('/content/articles/secondo-articolo')
expect(article.hasTranslation).toBe(false)
})
it('returns the English fields when lang=en and a translation exists', async () => {
const article = await $fetch<Article>('/content/articles/articolo-pubblicato', {
query: { lang: 'en' },
})
expect(article.title).toBe('Published article')
expect(article.html).toContain('<strong>markdown</strong>')
})
it('404s for lang=en when no translation exists', async () => {
await expectStatus(
$fetch('/content/articles/secondo-articolo', { query: { lang: 'en' } }),
404
)
})
})
describe('GET /content/articles with lang=en', () => {
it('only lists articles that have an English translation', async () => {
const response = await $fetch<Paginated<ArticleSummary>>('/content/articles', {
query: { lang: 'en' },
})
expect(response.items.map((a) => a.slug)).toEqual(['articolo-pubblicato'])
expect(response.items[0]?.title).toBe('Published article')
})
})
describe('GET /content/categories', () => {
it('lists categories sorted by name', async () => {
const categories = await $fetch<Category[]>('/content/categories')
expect(categories.map((c) => c.name)).toEqual(['Attualità', 'Economia'])
})
it('only lists translated categories when lang=en', async () => {
const categories = await $fetch<Category[]>('/content/categories', { query: { lang: 'en' } })
expect(categories.map((c) => c.name)).toEqual(['Economy'])
})
})
describe('GET /content/categories/:slug', () => {
it('returns one category by slug', async () => {
const category = await $fetch<Category>('/content/categories/economia')
expect(category.name).toBe('Economia')
})
it('404s for an unknown slug', async () => {
await expectStatus($fetch('/content/categories/does-not-exist'), 404)
})
it('returns the English name when lang=en and a translation exists', async () => {
const category = await $fetch<Category>('/content/categories/economia', { query: { lang: 'en' } })
expect(category.name).toBe('Economy')
})
it('404s for lang=en when no translation exists', async () => {
await expectStatus($fetch('/content/categories/attualita', { query: { lang: 'en' } }), 404)
})
})
describe('GET /admin', () => {
it('redirects to the PocketBase dashboard', async () => {
const response = await fetch('/admin', { redirect: 'manual' })
expect(response.status).toBe(302)
expect(response.headers.get('location')).toBe(`${pb.url}/_/`)
})
})