diff --git a/tests/block.test.ts b/tests/block.test.ts
new file mode 100644
index 0000000..9978ddd
--- /dev/null
+++ b/tests/block.test.ts
@@ -0,0 +1,120 @@
+// 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>/)
+})