// 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`), `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>/) })