Files
vue-faq-layer/tests/client.test.ts
Jakub Zych 2d62924b78 feat: FaqBlock layer for the SummerCMS FAQ API
- typed client and useFaq/useFaqApi (SSR, faq:<slug>:<locale> key,
  server and public bases, x-forwarded-for forwarded also on cached renders)
- FaqBlock, FaqList and FaqItem: native details, exclusive and openFirst,
  server-sanitized description and answers, nothing or #fallback on 404,
  error or no items
- FAQ tokens and base CSS with the .faq-html rules, README
- node client tests and a playground SSR smoke against a fixture API
2026-10-12 18:22:03 +02:00

92 lines
3.4 KiB
TypeScript

// The FAQ client against a fake fetcher: the URL built from base, slug and
// locale, the envelope unwrapped, malformed items dropped, 404 as null. No
// network.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createFaqClient, faqItems, faqPath, isNotFound } from '../app/utils/client.ts'
type Call = { path: string; query?: Record<string, string> }
const FAQ = {
data: {
name: 'Shipping',
slug: 'shipping',
locale: 'en',
description: '<p>About shipping.</p>',
content: [
{ question: 'How long?', answer: '<p>Two days.</p>' },
{ question: 'Where?', answer: '<p>Everywhere.</p>' },
],
},
}
function fakeFetcher(answer: () => unknown) {
const calls: Call[] = []
const fetcher = async (path: string, query?: Record<string, string>) => {
calls.push(query === undefined ? { path } : { path, query })
return answer()
}
return { fetcher, calls }
}
test('faqPath encodes the slug', () => {
assert.equal(faqPath('shipping'), '/faqs/shipping')
assert.equal(faqPath('a b/c'), '/faqs/a%20b%2Fc')
})
test('getFaq calls /faqs/<slug> and passes locale only when set', async () => {
const { fetcher, calls } = fakeFetcher(() => FAQ)
const client = createFaqClient(fetcher)
const faq = await client.getFaq('shipping')
assert.equal(faq?.name, 'Shipping')
assert.equal(faq?.content.length, 2)
await client.getFaq('shipping', 'pl')
assert.deepEqual(calls, [{ path: '/faqs/shipping' }, { path: '/faqs/shipping', query: { locale: 'pl' } }])
})
test('the fetcher base plus the path is the API URL', async () => {
const urls: string[] = []
const base = 'http://127.0.0.1:8096/_faq/api/v1'
const client = createFaqClient(async (path, query) => {
const u = new URL(base + path)
for (const [k, v] of Object.entries(query ?? {})) u.searchParams.set(k, v)
urls.push(u.toString())
return FAQ
})
await client.getFaq('what-mushrooms-can-give-you', 'pl')
assert.deepEqual(urls, ['http://127.0.0.1:8096/_faq/api/v1/faqs/what-mushrooms-can-give-you?locale=pl'])
})
test('a 404 resolves to null; other errors are thrown', async () => {
const notFound = createFaqClient(async () => {
throw Object.assign(new Error('Not Found'), { response: { status: 404 } })
})
assert.equal(await notFound.getFaq('nope'), null)
const failing = createFaqClient(async () => {
throw Object.assign(new Error('boom'), { statusCode: 502 })
})
await assert.rejects(failing.getFaq('x'), /boom/)
})
test('a body without data is null; a null description stays null', async () => {
assert.equal(await createFaqClient(async () => ({})).getFaq('x'), null)
const faq = await createFaqClient(async () => ({ data: { ...FAQ.data, description: null } })).getFaq('x')
assert.equal(faq?.description, null)
})
test('faqItems keeps only items with a string question and answer', () => {
assert.deepEqual(faqItems(null), [])
assert.deepEqual(faqItems({}), [])
assert.deepEqual(faqItems([{ question: 'Q', answer: 'A' }, { question: 'Q' }, null, 'x', { question: 1, answer: 'A' }]), [
{ question: 'Q', answer: 'A' },
])
})
test('isNotFound reads status, statusCode and response.status', () => {
assert.equal(isNotFound({ status: 404 }), true)
assert.equal(isNotFound({ statusCode: 404 }), true)
assert.equal(isNotFound({ response: { status: 404 } }), true)
assert.equal(isNotFound({ status: 500 }), false)
assert.equal(isNotFound(undefined), false)
})