// FaqBlock (D-52, UI-SPEC L6) rendered on the server: the SFC is compiled
// with vue/compiler-sfc and rendered with vue/server-renderer, with the
// Nuxt composables it calls (useFaq, useSeoAnswer, useRequestURL,
// useSchemaNode) stubbed on globalThis and FaqList stubbed as a component.
// headingLevel picks the title's element (0 a
), the section stays
// labelled by the title, and the FAQPage node it hands to the page graph
// carries the answers as plain text.
import assert from 'node:assert/strict'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { after, test } from 'node:test'
import { pathToFileURL } from 'node:url'
import { compileScript, parse } from 'vue/compiler-sfc'
import { createSSRApp, h, ref, type Component } from 'vue'
import { renderToString } from 'vue/server-renderer'
const root = new URL('..', import.meta.url)
const dir = mkdtempSync(join(tmpdir(), 'faq-block-'))
after(() => rmSync(dir, { recursive: true, force: true }))
async function loadBlock(): Promise {
const source = readFileSync(new URL('app/components/Faq/Block.vue', root), 'utf8')
const { descriptor } = parse(source, { filename: 'Block.vue' })
const script = compileScript(descriptor, { id: 'faq-block', inlineTemplate: true, templateOptions: { ssr: true } })
const code = script.content
.replaceAll(`from 'vue'`, `from '${import.meta.resolve('vue')}'`)
.replaceAll(`from "vue"`, `from '${import.meta.resolve('vue')}'`)
.replaceAll(`from 'vue/server-renderer'`, `from '${import.meta.resolve('vue/server-renderer')}'`)
.replaceAll(`from "vue/server-renderer"`, `from '${import.meta.resolve('vue/server-renderer')}'`)
.replaceAll(`'../../utils/faqSchema.ts'`, `'${new URL('app/utils/faqSchema.ts', root).href}'`)
const file = join(dir, 'Block.ts')
writeFileSync(file, code)
return (await import(pathToFileURL(file).href)).default as Component
}
const faq = {
id: 1,
slug: 'grzyby',
name: 'Pytania o grzyby',
description: 'Najczęstsze pytania
',
content: [
{ question: ' Czy chaga jest bezpieczna? ', answer: 'Tak, w umiarkowanych dawkach.
' },
{ question: 'Jak parzyć?', answer: '' },
],
}
const g = globalThis as Record
let nodes: unknown[] = []
function stub(data: unknown, error: unknown = null) {
nodes = []
Object.assign(g, {
useFaq: async () => ({ data: ref(data), error: ref(error) }),
useSeoAnswer: () => ref({ action: 'ok', url: 'https://example.test/faq#top' }),
useRequestURL: () => new URL('https://fallback.test/page'),
useSchemaNode: (fn: () => unknown) => nodes.push(fn()),
})
}
const FaqList: Component = { props: ['items', 'exclusive'], setup: (p: { items: unknown[] }) => () => h('div', { class: 'faq-list' }, String(p.items.length)) }
const Block = await loadBlock()
async function render(props: Record): Promise {
const app = createSSRApp({ render: () => h(Block, { slug: 'grzyby', ...props }, { fallback: () => h('em', 'fallback') }) })
app.component('FaqList', FaqList)
return renderToString(app)
}
test('headingLevel 2 to 4 renders that heading, out-of-range levels are clamped', async () => {
stub(faq)
for (const [level, tag] of [[2, 'h2'], [3, 'h3'], [4, 'h4'], [6, 'h4'], [1, 'h2']] as const) {
const html = await render({ headingLevel: level })
assert.match(html, new RegExp(`<${tag} id="([^"]+)" class="faq-block__title">Pytania o grzyby${tag}>`), `level ${level}`)
}
})
test('headingLevel 0 renders the title as a that still labels the section', async () => {
stub(faq)
const html = await render({ headingLevel: 0 })
const title = html.match(/
Pytania o grzyby<\/p>/)
assert.ok(title, html)
assert.match(html, new RegExp(` {
stub(faq)
const html = await render({ showHeading: false })
assert.match(html, / {
stub(faq)
await render({})
assert.equal(nodes.length, 1)
assert.deepEqual(nodes[0], {
'@type': 'FAQPage',
'@id': 'https://example.test/faq#faq',
url: 'https://example.test/faq',
mainEntity: [
{ '@type': 'Question', name: 'Czy chaga jest bezpieczna?', acceptedAnswer: { '@type': 'Answer', text: 'Tak, w umiarkowanych dawkach.' } },
{ '@type': 'Question', name: 'Jak parzyć?', acceptedAnswer: { '@type': 'Answer', text: 'Woda 80°C & 5 min' } },
],
})
})
test('schema false, a missing FAQ or one without items: no node; the fallback slot instead of an empty block', async () => {
stub(faq)
await render({ schema: false })
assert.deepEqual(nodes, [null])
stub(null, new Error('404'))
const missing = await render({})
assert.match(missing, /fallback<\/em>/)
assert.doesNotMatch(missing, /faq-block/)
assert.deepEqual(nodes, [null])
stub({ ...faq, content: [] })
assert.match(await render({}), /fallback<\/em>/)
})