@pyreon/head 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,32 @@
1
- import { h } from "@pyreon/core"
2
- import { signal } from "@pyreon/reactivity"
3
- import { mount } from "@pyreon/runtime-dom"
4
- import type { HeadContextValue } from "../index"
5
- import { createHeadContext, HeadProvider, useHead } from "../index"
6
- import { renderWithHead } from "../ssr"
1
+ import { h } from '@pyreon/core'
2
+ import { signal } from '@pyreon/reactivity'
3
+ import { mount } from '@pyreon/runtime-dom'
4
+ import type { HeadContextValue } from '../index'
5
+ import { createHeadContext, HeadProvider, useHead } from '../index'
6
+ import { renderWithHead } from '../ssr'
7
7
 
8
8
  // ─── SSR tests ────────────────────────────────────────────────────────────────
9
9
 
10
- describe("renderWithHead — SSR", () => {
11
- test("extracts <title> from useHead", async () => {
10
+ describe('renderWithHead — SSR', () => {
11
+ test('extracts <title> from useHead', async () => {
12
12
  function Page() {
13
- useHead({ title: "Hello Pyreon" })
14
- return h("main", null, "content")
13
+ useHead({ title: 'Hello Pyreon' })
14
+ return h('main', null, 'content')
15
15
  }
16
16
  const { html, head } = await renderWithHead(h(Page, null))
17
- expect(html).toContain("<main>")
18
- expect(head).toContain("<title>Hello Pyreon</title>")
17
+ expect(html).toContain('<main>')
18
+ expect(head).toContain('<title>Hello Pyreon</title>')
19
19
  })
20
20
 
21
- test("extracts <meta> tags from useHead", async () => {
21
+ test('extracts <meta> tags from useHead', async () => {
22
22
  function Page() {
23
23
  useHead({
24
24
  meta: [
25
- { name: "description", content: "A great page" },
26
- { property: "og:title", content: "Hello" },
25
+ { name: 'description', content: 'A great page' },
26
+ { property: 'og:title', content: 'Hello' },
27
27
  ],
28
28
  })
29
- return h("div", null)
29
+ return h('div', null)
30
30
  }
31
31
  const { head } = await renderWithHead(h(Page, null))
32
32
  expect(head).toContain('name="description"')
@@ -34,74 +34,74 @@ describe("renderWithHead — SSR", () => {
34
34
  expect(head).toContain('property="og:title"')
35
35
  })
36
36
 
37
- test("extracts <link> tags from useHead", async () => {
37
+ test('extracts <link> tags from useHead', async () => {
38
38
  function Page() {
39
- useHead({ link: [{ rel: "canonical", href: "https://example.com/page" }] })
40
- return h("div", null)
39
+ useHead({ link: [{ rel: 'canonical', href: 'https://example.com/page' }] })
40
+ return h('div', null)
41
41
  }
42
42
  const { head } = await renderWithHead(h(Page, null))
43
43
  expect(head).toContain('rel="canonical"')
44
44
  expect(head).toContain('href="https://example.com/page"')
45
45
  })
46
46
 
47
- test("deduplication: innermost title wins", async () => {
47
+ test('deduplication: innermost title wins', async () => {
48
48
  function Inner() {
49
- useHead({ title: "Inner Title" })
50
- return h("span", null)
49
+ useHead({ title: 'Inner Title' })
50
+ return h('span', null)
51
51
  }
52
52
  function Outer() {
53
- useHead({ title: "Outer Title" })
54
- return h("div", null, h(Inner, null))
53
+ useHead({ title: 'Outer Title' })
54
+ return h('div', null, h(Inner, null))
55
55
  }
56
56
  const { head } = await renderWithHead(h(Outer, null))
57
57
  // Only one <title> — Inner wins (last-added wins per key)
58
58
  const titleMatches = head.match(/<title>/g)
59
59
  expect(titleMatches).toHaveLength(1)
60
- expect(head).toContain("<title>Inner Title</title>")
60
+ expect(head).toContain('<title>Inner Title</title>')
61
61
  })
62
62
 
63
- test("escapes HTML entities in title", async () => {
63
+ test('escapes HTML entities in title', async () => {
64
64
  function Page() {
65
- useHead({ title: "A & B <script>" })
66
- return h("div", null)
65
+ useHead({ title: 'A & B <script>' })
66
+ return h('div', null)
67
67
  }
68
68
  const { head } = await renderWithHead(h(Page, null))
69
- expect(head).toContain("A &amp; B &lt;script&gt;")
70
- expect(head).not.toContain("<script>")
69
+ expect(head).toContain('A &amp; B &lt;script&gt;')
70
+ expect(head).not.toContain('<script>')
71
71
  })
72
72
 
73
- test("works with async component", async () => {
73
+ test('works with async component', async () => {
74
74
  async function AsyncPage() {
75
75
  await new Promise((r) => setTimeout(r, 1))
76
- useHead({ title: "Async Page" })
77
- return h("div", null)
76
+ useHead({ title: 'Async Page' })
77
+ return h('div', null)
78
78
  }
79
79
  const { head } = await renderWithHead(h(AsyncPage as never, null))
80
- expect(head).toContain("<title>Async Page</title>")
80
+ expect(head).toContain('<title>Async Page</title>')
81
81
  })
82
82
 
83
- test("renders <style> tags", async () => {
83
+ test('renders <style> tags', async () => {
84
84
  function Page() {
85
- useHead({ style: [{ children: "body { color: red }" }] })
86
- return h("div", null)
85
+ useHead({ style: [{ children: 'body { color: red }' }] })
86
+ return h('div', null)
87
87
  }
88
88
  const { head } = await renderWithHead(h(Page, null))
89
- expect(head).toContain("<style>body { color: red }</style>")
89
+ expect(head).toContain('<style>body { color: red }</style>')
90
90
  })
91
91
 
92
- test("renders <noscript> tags", async () => {
92
+ test('renders <noscript> tags', async () => {
93
93
  function Page() {
94
- useHead({ noscript: [{ children: "<p>Please enable JavaScript</p>" }] })
95
- return h("div", null)
94
+ useHead({ noscript: [{ children: '<p>Please enable JavaScript</p>' }] })
95
+ return h('div', null)
96
96
  }
97
97
  const { head } = await renderWithHead(h(Page, null))
98
- expect(head).toContain("<noscript><p>Please enable JavaScript</p></noscript>")
98
+ expect(head).toContain('<noscript><p>Please enable JavaScript</p></noscript>')
99
99
  })
100
100
 
101
- test("renders JSON-LD script tag", async () => {
101
+ test('renders JSON-LD script tag', async () => {
102
102
  function Page() {
103
- useHead({ jsonLd: { "@type": "WebPage", name: "Test" } })
104
- return h("div", null)
103
+ useHead({ jsonLd: { '@type': 'WebPage', name: 'Test' } })
104
+ return h('div', null)
105
105
  }
106
106
  const { head } = await renderWithHead(h(Page, null))
107
107
  expect(head).toContain('type="application/ld+json"')
@@ -109,53 +109,53 @@ describe("renderWithHead — SSR", () => {
109
109
  expect(head).toContain('"name":"Test"')
110
110
  })
111
111
 
112
- test("script content is not HTML-escaped", async () => {
112
+ test('script content is not HTML-escaped', async () => {
113
113
  function Page() {
114
- useHead({ script: [{ children: "var x = 1 < 2 && 3 > 1" }] })
115
- return h("div", null)
114
+ useHead({ script: [{ children: 'var x = 1 < 2 && 3 > 1' }] })
115
+ return h('div', null)
116
116
  }
117
117
  const { head } = await renderWithHead(h(Page, null))
118
- expect(head).toContain("var x = 1 < 2 && 3 > 1")
119
- expect(head).not.toContain("&lt;")
118
+ expect(head).toContain('var x = 1 < 2 && 3 > 1')
119
+ expect(head).not.toContain('&lt;')
120
120
  })
121
121
 
122
- test("titleTemplate with %s placeholder", async () => {
122
+ test('titleTemplate with %s placeholder', async () => {
123
123
  function Layout() {
124
- useHead({ titleTemplate: "%s | My App" })
125
- return h("div", null, h(Page, null))
124
+ useHead({ titleTemplate: '%s | My App' })
125
+ return h('div', null, h(Page, null))
126
126
  }
127
127
  function Page() {
128
- useHead({ title: "About" })
129
- return h("span", null)
128
+ useHead({ title: 'About' })
129
+ return h('span', null)
130
130
  }
131
131
  const { head } = await renderWithHead(h(Layout, null))
132
- expect(head).toContain("<title>About | My App</title>")
132
+ expect(head).toContain('<title>About | My App</title>')
133
133
  })
134
134
 
135
- test("titleTemplate with function", async () => {
135
+ test('titleTemplate with function', async () => {
136
136
  function Layout() {
137
- useHead({ titleTemplate: (t: string) => (t ? `${t} — Site` : "Site") })
138
- return h("div", null, h(Page, null))
137
+ useHead({ titleTemplate: (t: string) => (t ? `${t} — Site` : 'Site') })
138
+ return h('div', null, h(Page, null))
139
139
  }
140
140
  function Page() {
141
- useHead({ title: "Home" })
142
- return h("span", null)
141
+ useHead({ title: 'Home' })
142
+ return h('span', null)
143
143
  }
144
144
  const { head } = await renderWithHead(h(Layout, null))
145
- expect(head).toContain("<title>Home — Site</title>")
145
+ expect(head).toContain('<title>Home — Site</title>')
146
146
  })
147
147
 
148
- test("returns htmlAttrs and bodyAttrs", async () => {
148
+ test('returns htmlAttrs and bodyAttrs', async () => {
149
149
  function Page() {
150
- useHead({ htmlAttrs: { lang: "en", dir: "ltr" }, bodyAttrs: { class: "dark" } })
151
- return h("div", null)
150
+ useHead({ htmlAttrs: { lang: 'en', dir: 'ltr' }, bodyAttrs: { class: 'dark' } })
151
+ return h('div', null)
152
152
  }
153
153
  const result = await renderWithHead(h(Page, null))
154
- expect(result.htmlAttrs).toEqual({ lang: "en", dir: "ltr" })
155
- expect(result.bodyAttrs).toEqual({ class: "dark" })
154
+ expect(result.htmlAttrs).toEqual({ lang: 'en', dir: 'ltr' })
155
+ expect(result.bodyAttrs).toEqual({ class: 'dark' })
156
156
  })
157
157
 
158
- test("ssr.ts: serializeTag for non-void, non-raw tag with children (line 76)", async () => {
158
+ test('ssr.ts: serializeTag for non-void, non-raw tag with children (line 76)', async () => {
159
159
  // A <noscript> is raw, but let's use a tag that goes through esc() path
160
160
  // Actually noscript IS raw. We need a non-void, non-raw tag with children.
161
161
  // The only non-void non-raw tags possible: title (handled separately).
@@ -176,87 +176,87 @@ describe("renderWithHead — SSR", () => {
176
176
  // Line 60: tag.children ?? "" when children is undefined
177
177
  function _Page() {
178
178
  // title with undefined children — manually add to context
179
- return h("div", null)
179
+ return h('div', null)
180
180
  }
181
181
  // Test title with no children set (tag.children is undefined → fallback to "")
182
182
  const ctx2 = createHeadContext()
183
- ctx2.add(Symbol(), { tags: [{ tag: "title", key: "title" }] }) // no children prop
183
+ ctx2.add(Symbol(), { tags: [{ tag: 'title', key: 'title' }] }) // no children prop
184
184
  const tags = ctx2.resolve()
185
185
  expect(tags[0]?.children).toBeUndefined()
186
186
  // Now test through renderWithHead — the title tag should render with empty content
187
187
  function PageNoTitle() {
188
- useHead({ title: "" })
189
- return h("div", null)
188
+ useHead({ title: '' })
189
+ return h('div', null)
190
190
  }
191
191
  const result = await renderWithHead(h(PageNoTitle, null))
192
- expect(result.head).toContain("<title></title>")
192
+ expect(result.head).toContain('<title></title>')
193
193
  })
194
194
 
195
- test("ssr.ts: serializeTag renders noscript with closing-tag escaping (line 76)", async () => {
195
+ test('ssr.ts: serializeTag renders noscript with closing-tag escaping (line 76)', async () => {
196
196
  function Page() {
197
197
  useHead({
198
- noscript: [{ children: "test </noscript><script>alert(1)</script>" }],
198
+ noscript: [{ children: 'test </noscript><script>alert(1)</script>' }],
199
199
  })
200
- return h("div", null)
200
+ return h('div', null)
201
201
  }
202
202
  const { head } = await renderWithHead(h(Page, null))
203
203
  // The raw path should escape </noscript> to <\/noscript>
204
- expect(head).toContain("<\\/noscript>")
204
+ expect(head).toContain('<\\/noscript>')
205
205
  })
206
206
 
207
- test("ssr.ts: serializeTag for title with undefined children (line 60)", async () => {
207
+ test('ssr.ts: serializeTag for title with undefined children (line 60)', async () => {
208
208
  // Directly test by adding a title tag with no children to context
209
209
  const ctx2 = createHeadContext()
210
- ctx2.add(Symbol(), { tags: [{ tag: "title", key: "title" }] })
210
+ ctx2.add(Symbol(), { tags: [{ tag: 'title', key: 'title' }] })
211
211
  // Use renderWithHead with a component that adds title tag without children
212
212
  function _Page() {
213
- return h("div", null)
213
+ return h('div', null)
214
214
  }
215
215
  // We need to test serializeTag with title where children is undefined
216
216
  // Since we can't call serializeTag directly, let's use renderWithHead
217
217
  // with a titleTemplate and no title to exercise both template branches
218
218
  function PageWithTemplate() {
219
- useHead({ titleTemplate: "%s | Site" })
220
- return h("div", null, h(Inner, null))
219
+ useHead({ titleTemplate: '%s | Site' })
220
+ return h('div', null, h(Inner, null))
221
221
  }
222
222
  function Inner() {
223
- useHead({ title: "" }) // empty string title with template
224
- return h("span", null)
223
+ useHead({ title: '' }) // empty string title with template
224
+ return h('span', null)
225
225
  }
226
226
  const { head } = await renderWithHead(h(PageWithTemplate, null))
227
- expect(head).toContain("<title> | Site</title>")
227
+ expect(head).toContain('<title> | Site</title>')
228
228
  })
229
229
 
230
- test("ssr.ts: script tag with closing tag escaping (line 76)", async () => {
230
+ test('ssr.ts: script tag with closing tag escaping (line 76)', async () => {
231
231
  function Page() {
232
232
  useHead({
233
233
  script: [{ children: "var x = '</script><img onerror=alert(1)>'" }],
234
234
  })
235
- return h("div", null)
235
+ return h('div', null)
236
236
  }
237
237
  const { head } = await renderWithHead(h(Page, null))
238
- expect(head).toContain("<\\/script>")
238
+ expect(head).toContain('<\\/script>')
239
239
  })
240
240
 
241
- test("use-head.ts: reactive function input on SSR (line 88)", async () => {
241
+ test('use-head.ts: reactive function input on SSR (line 88)', async () => {
242
242
  // The SSR path for function input evaluates once synchronously
243
243
  function Page() {
244
- useHead(() => ({ title: "SSR Reactive" }))
245
- return h("div", null)
244
+ useHead(() => ({ title: 'SSR Reactive' }))
245
+ return h('div', null)
246
246
  }
247
247
  const { head } = await renderWithHead(h(Page, null))
248
- expect(head).toContain("<title>SSR Reactive</title>")
248
+ expect(head).toContain('<title>SSR Reactive</title>')
249
249
  })
250
250
 
251
- test("multiple link tags with same rel but different href are kept", async () => {
251
+ test('multiple link tags with same rel but different href are kept', async () => {
252
252
  function Page() {
253
253
  useHead({
254
254
  link: [
255
- { rel: "stylesheet", href: "/a.css" },
256
- { rel: "stylesheet", href: "/b.css" },
255
+ { rel: 'stylesheet', href: '/a.css' },
256
+ { rel: 'stylesheet', href: '/b.css' },
257
257
  ],
258
258
  })
259
- return h("div", null)
259
+ return h('div', null)
260
260
  }
261
261
  const { head } = await renderWithHead(h(Page, null))
262
262
  expect(head).toContain('href="/a.css"')
@@ -266,43 +266,43 @@ describe("renderWithHead — SSR", () => {
266
266
 
267
267
  // ─── CSR tests ────────────────────────────────────────────────────────────────
268
268
 
269
- describe("useHead — CSR", () => {
269
+ describe('useHead — CSR', () => {
270
270
  let container: HTMLElement
271
271
  let ctx: HeadContextValue
272
272
 
273
273
  beforeEach(() => {
274
- container = document.createElement("div")
274
+ container = document.createElement('div')
275
275
  document.body.appendChild(container)
276
276
  ctx = createHeadContext()
277
277
  // Clean up any pyreon-injected head tags from prior tests
278
- for (const el of document.head.querySelectorAll("[data-pyreon-head]")) el.remove()
279
- document.title = ""
278
+ for (const el of document.head.querySelectorAll('[data-pyreon-head]')) el.remove()
279
+ document.title = ''
280
280
  })
281
281
 
282
- test("syncs document.title on mount", () => {
282
+ test('syncs document.title on mount', () => {
283
283
  function Page() {
284
- useHead({ title: "CSR Title" })
285
- return h("div", null)
284
+ useHead({ title: 'CSR Title' })
285
+ return h('div', null)
286
286
  }
287
287
  // When using h() directly (not JSX), component children must be in props.children
288
288
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
289
- expect(document.title).toBe("CSR Title")
289
+ expect(document.title).toBe('CSR Title')
290
290
  })
291
291
 
292
- test("syncs <meta> tags on mount", () => {
292
+ test('syncs <meta> tags on mount', () => {
293
293
  function Page() {
294
- useHead({ meta: [{ name: "description", content: "CSR desc" }] })
295
- return h("div", null)
294
+ useHead({ meta: [{ name: 'description', content: 'CSR desc' }] })
295
+ return h('div', null)
296
296
  }
297
297
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
298
298
  const meta = document.head.querySelector('meta[name="description"]')
299
- expect(meta?.getAttribute("content")).toBe("CSR desc")
299
+ expect(meta?.getAttribute('content')).toBe('CSR desc')
300
300
  })
301
301
 
302
- test("removes meta tags on unmount", () => {
302
+ test('removes meta tags on unmount', () => {
303
303
  function Page() {
304
- useHead({ meta: [{ name: "keywords", content: "pyreon" }] })
305
- return h("div", null)
304
+ useHead({ meta: [{ name: 'keywords', content: 'pyreon' }] })
305
+ return h('div', null)
306
306
  }
307
307
  const cleanup = mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
308
308
  expect(document.head.querySelector('meta[name="keywords"]')).not.toBeNull()
@@ -310,33 +310,33 @@ describe("useHead — CSR", () => {
310
310
  expect(document.head.querySelector('meta[name="keywords"]')).toBeNull()
311
311
  })
312
312
 
313
- test("reactive useHead updates title when signal changes", () => {
314
- const title = signal("Initial")
313
+ test('reactive useHead updates title when signal changes', () => {
314
+ const title = signal('Initial')
315
315
  function Page() {
316
316
  useHead(() => ({ title: title() }))
317
- return h("div", null)
317
+ return h('div', null)
318
318
  }
319
319
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
320
- expect(document.title).toBe("Initial")
321
- title.set("Updated")
322
- expect(document.title).toBe("Updated")
320
+ expect(document.title).toBe('Initial')
321
+ title.set('Updated')
322
+ expect(document.title).toBe('Updated')
323
323
  })
324
324
 
325
- test("syncs <style> tags on mount", () => {
325
+ test('syncs <style> tags on mount', () => {
326
326
  function Page() {
327
- useHead({ style: [{ children: "body { margin: 0 }" }] })
328
- return h("div", null)
327
+ useHead({ style: [{ children: 'body { margin: 0 }' }] })
328
+ return h('div', null)
329
329
  }
330
330
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
331
- const style = document.head.querySelector("style[data-pyreon-head]")
331
+ const style = document.head.querySelector('style[data-pyreon-head]')
332
332
  expect(style).not.toBeNull()
333
- expect(style?.textContent).toBe("body { margin: 0 }")
333
+ expect(style?.textContent).toBe('body { margin: 0 }')
334
334
  })
335
335
 
336
- test("syncs JSON-LD script on mount", () => {
336
+ test('syncs JSON-LD script on mount', () => {
337
337
  function Page() {
338
- useHead({ jsonLd: { "@type": "WebPage", name: "Test" } })
339
- return h("div", null)
338
+ useHead({ jsonLd: { '@type': 'WebPage', name: 'Test' } })
339
+ return h('div', null)
340
340
  }
341
341
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
342
342
  const script = document.head.querySelector('script[type="application/ld+json"]')
@@ -344,75 +344,75 @@ describe("useHead — CSR", () => {
344
344
  expect(script?.textContent).toContain('"@type":"WebPage"')
345
345
  })
346
346
 
347
- test("titleTemplate applies to document.title", () => {
347
+ test('titleTemplate applies to document.title', () => {
348
348
  function Layout() {
349
- useHead({ titleTemplate: "%s | My App" })
350
- return h("div", null, h(Page, null))
349
+ useHead({ titleTemplate: '%s | My App' })
350
+ return h('div', null, h(Page, null))
351
351
  }
352
352
  function Page() {
353
- useHead({ title: "Dashboard" })
354
- return h("span", null)
353
+ useHead({ title: 'Dashboard' })
354
+ return h('span', null)
355
355
  }
356
356
  mount(h(HeadProvider, { context: ctx, children: h(Layout, null) }), container)
357
- expect(document.title).toBe("Dashboard | My App")
357
+ expect(document.title).toBe('Dashboard | My App')
358
358
  })
359
359
 
360
- test("htmlAttrs sets attributes on <html>", () => {
360
+ test('htmlAttrs sets attributes on <html>', () => {
361
361
  function Page() {
362
- useHead({ htmlAttrs: { lang: "fr" } })
363
- return h("div", null)
362
+ useHead({ htmlAttrs: { lang: 'fr' } })
363
+ return h('div', null)
364
364
  }
365
365
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
366
- expect(document.documentElement.getAttribute("lang")).toBe("fr")
366
+ expect(document.documentElement.getAttribute('lang')).toBe('fr')
367
367
  })
368
368
 
369
- test("bodyAttrs sets attributes on <body>", () => {
369
+ test('bodyAttrs sets attributes on <body>', () => {
370
370
  function Page() {
371
- useHead({ bodyAttrs: { class: "dark-mode" } })
372
- return h("div", null)
371
+ useHead({ bodyAttrs: { class: 'dark-mode' } })
372
+ return h('div', null)
373
373
  }
374
374
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
375
- expect(document.body.getAttribute("class")).toBe("dark-mode")
375
+ expect(document.body.getAttribute('class')).toBe('dark-mode')
376
376
  })
377
377
 
378
- test("incremental sync updates attributes in place", () => {
379
- const desc = signal("initial")
378
+ test('incremental sync updates attributes in place', () => {
379
+ const desc = signal('initial')
380
380
  function Page() {
381
- useHead(() => ({ meta: [{ name: "description", content: desc() }] }))
382
- return h("div", null)
381
+ useHead(() => ({ meta: [{ name: 'description', content: desc() }] }))
382
+ return h('div', null)
383
383
  }
384
384
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
385
385
  const meta1 = document.head.querySelector('meta[name="description"]')
386
- expect(meta1?.getAttribute("content")).toBe("initial")
387
- desc.set("updated")
386
+ expect(meta1?.getAttribute('content')).toBe('initial')
387
+ desc.set('updated')
388
388
  const meta2 = document.head.querySelector('meta[name="description"]')
389
389
  // Same element should be reused (incremental sync)
390
390
  expect(meta2).toBe(meta1)
391
- expect(meta2?.getAttribute("content")).toBe("updated")
391
+ expect(meta2?.getAttribute('content')).toBe('updated')
392
392
  })
393
393
 
394
- test("titleTemplate function applies to document.title in CSR", () => {
394
+ test('titleTemplate function applies to document.title in CSR', () => {
395
395
  function Layout() {
396
- useHead({ titleTemplate: (t: string) => (t ? `${t} - App` : "App") })
397
- return h("div", null, h(Page, null))
396
+ useHead({ titleTemplate: (t: string) => (t ? `${t} - App` : 'App') })
397
+ return h('div', null, h(Page, null))
398
398
  }
399
399
  function Page() {
400
- useHead({ title: "CSR Page" })
401
- return h("span", null)
400
+ useHead({ title: 'CSR Page' })
401
+ return h('span', null)
402
402
  }
403
403
  mount(h(HeadProvider, { context: ctx, children: h(Layout, null) }), container)
404
- expect(document.title).toBe("CSR Page - App")
404
+ expect(document.title).toBe('CSR Page - App')
405
405
  })
406
406
 
407
- test("removes stale elements when tags change", () => {
407
+ test('removes stale elements when tags change', () => {
408
408
  const show = signal(true)
409
409
  function Page() {
410
410
  useHead(() => {
411
411
  const tags: { name: string; content: string }[] = []
412
- if (show()) tags.push({ name: "keywords", content: "pyreon" })
412
+ if (show()) tags.push({ name: 'keywords', content: 'pyreon' })
413
413
  return { meta: tags }
414
414
  })
415
- return h("div", null)
415
+ return h('div', null)
416
416
  }
417
417
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
418
418
  expect(document.head.querySelector('meta[name="keywords"]')).not.toBeNull()
@@ -420,368 +420,368 @@ describe("useHead — CSR", () => {
420
420
  expect(document.head.querySelector('meta[name="keywords"]')).toBeNull()
421
421
  })
422
422
 
423
- test("patchAttrs removes old attributes no longer in props", () => {
424
- const attrs = signal<Record<string, string>>({ name: "test", content: "value" })
423
+ test('patchAttrs removes old attributes no longer in props', () => {
424
+ const attrs = signal<Record<string, string>>({ name: 'test', content: 'value' })
425
425
  function Page() {
426
426
  useHead(() => ({ meta: [attrs()] }))
427
- return h("div", null)
427
+ return h('div', null)
428
428
  }
429
429
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
430
430
  const el = document.head.querySelector('meta[name="test"]')
431
- expect(el?.getAttribute("content")).toBe("value")
431
+ expect(el?.getAttribute('content')).toBe('value')
432
432
  // Change attrs to remove 'content'
433
- attrs.set({ name: "test" })
433
+ attrs.set({ name: 'test' })
434
434
  const el2 = document.head.querySelector('meta[name="test"]')
435
- expect(el2?.getAttribute("content")).toBeNull()
435
+ expect(el2?.getAttribute('content')).toBeNull()
436
436
  })
437
437
 
438
- test("syncElementAttrs removes previously managed attrs", () => {
438
+ test('syncElementAttrs removes previously managed attrs', () => {
439
439
  const show = signal(true)
440
440
  function Page() {
441
- useHead(() => (show() ? { htmlAttrs: { lang: "en", dir: "ltr" } } : { htmlAttrs: {} }))
442
- return h("div", null)
441
+ useHead(() => (show() ? { htmlAttrs: { lang: 'en', dir: 'ltr' } } : { htmlAttrs: {} }))
442
+ return h('div', null)
443
443
  }
444
444
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
445
- expect(document.documentElement.getAttribute("lang")).toBe("en")
446
- expect(document.documentElement.getAttribute("dir")).toBe("ltr")
445
+ expect(document.documentElement.getAttribute('lang')).toBe('en')
446
+ expect(document.documentElement.getAttribute('dir')).toBe('ltr')
447
447
  show.set(false)
448
448
  // Previously managed attrs should be removed
449
- expect(document.documentElement.getAttribute("lang")).toBeNull()
450
- expect(document.documentElement.getAttribute("dir")).toBeNull()
449
+ expect(document.documentElement.getAttribute('lang')).toBeNull()
450
+ expect(document.documentElement.getAttribute('dir')).toBeNull()
451
451
  })
452
452
 
453
- test("link tag key deduplication by rel when no href", async () => {
453
+ test('link tag key deduplication by rel when no href', async () => {
454
454
  function Page() {
455
455
  useHead({
456
- link: [{ rel: "icon" }],
456
+ link: [{ rel: 'icon' }],
457
457
  })
458
- return h("div", null)
458
+ return h('div', null)
459
459
  }
460
460
  const { head } = await renderWithHead(h(Page, null))
461
- expect(head).toContain("rel=")
461
+ expect(head).toContain('rel=')
462
462
  })
463
463
 
464
- test("link tag key uses index when no href or rel", async () => {
464
+ test('link tag key uses index when no href or rel', async () => {
465
465
  function Page() {
466
466
  useHead({
467
- link: [{ crossorigin: "anonymous" }],
467
+ link: [{ crossorigin: 'anonymous' }],
468
468
  })
469
- return h("div", null)
469
+ return h('div', null)
470
470
  }
471
471
  const { head } = await renderWithHead(h(Page, null))
472
- expect(head).toContain("<link")
472
+ expect(head).toContain('<link')
473
473
  })
474
474
 
475
- test("meta tag key uses property when name is absent", async () => {
475
+ test('meta tag key uses property when name is absent', async () => {
476
476
  function Page() {
477
477
  useHead({
478
- meta: [{ property: "og:title", content: "OG" }],
478
+ meta: [{ property: 'og:title', content: 'OG' }],
479
479
  })
480
- return h("div", null)
480
+ return h('div', null)
481
481
  }
482
482
  const { head } = await renderWithHead(h(Page, null))
483
483
  expect(head).toContain('property="og:title"')
484
484
  })
485
485
 
486
- test("meta tag key falls back to index when no name or property", async () => {
486
+ test('meta tag key falls back to index when no name or property', async () => {
487
487
  function Page() {
488
488
  useHead({
489
- meta: [{ charset: "utf-8" }],
489
+ meta: [{ charset: 'utf-8' }],
490
490
  })
491
- return h("div", null)
491
+ return h('div', null)
492
492
  }
493
493
  const { head } = await renderWithHead(h(Page, null))
494
494
  expect(head).toContain('charset="utf-8"')
495
495
  })
496
496
 
497
- test("script tag with src uses src as key", async () => {
497
+ test('script tag with src uses src as key', async () => {
498
498
  function Page() {
499
499
  useHead({
500
- script: [{ src: "/app.js" }],
500
+ script: [{ src: '/app.js' }],
501
501
  })
502
- return h("div", null)
502
+ return h('div', null)
503
503
  }
504
504
  const { head } = await renderWithHead(h(Page, null))
505
505
  expect(head).toContain('src="/app.js"')
506
506
  })
507
507
 
508
- test("script tag without src uses index as key", async () => {
508
+ test('script tag without src uses index as key', async () => {
509
509
  function Page() {
510
510
  useHead({
511
511
  script: [{ children: "console.log('hi')" }],
512
512
  })
513
- return h("div", null)
513
+ return h('div', null)
514
514
  }
515
515
  const { head } = await renderWithHead(h(Page, null))
516
516
  expect(head).toContain("console.log('hi')")
517
517
  })
518
518
 
519
- test("base tag renders in SSR", async () => {
519
+ test('base tag renders in SSR', async () => {
520
520
  function Page() {
521
- useHead({ base: { href: "/" } })
522
- return h("div", null)
521
+ useHead({ base: { href: '/' } })
522
+ return h('div', null)
523
523
  }
524
524
  const { head } = await renderWithHead(h(Page, null))
525
- expect(head).toContain("<base")
525
+ expect(head).toContain('<base')
526
526
  expect(head).toContain('href="/"')
527
527
  })
528
528
 
529
- test("noscript raw content escaping in SSR", async () => {
529
+ test('noscript raw content escaping in SSR', async () => {
530
530
  function Page() {
531
531
  useHead({
532
- noscript: [{ children: "<p>Enable JS</p>" }],
532
+ noscript: [{ children: '<p>Enable JS</p>' }],
533
533
  })
534
- return h("div", null)
534
+ return h('div', null)
535
535
  }
536
536
  const { head } = await renderWithHead(h(Page, null))
537
537
  // noscript is a raw tag, content preserved
538
- expect(head).toContain("<p>Enable JS</p>")
538
+ expect(head).toContain('<p>Enable JS</p>')
539
539
  })
540
540
 
541
- test("style content escaping in SSR prevents tag injection", async () => {
541
+ test('style content escaping in SSR prevents tag injection', async () => {
542
542
  function Page() {
543
543
  useHead({
544
- style: [{ children: "body { color: red } </style><script>" }],
544
+ style: [{ children: 'body { color: red } </style><script>' }],
545
545
  })
546
- return h("div", null)
546
+ return h('div', null)
547
547
  }
548
548
  const { head } = await renderWithHead(h(Page, null))
549
549
  // Closing tag should be escaped
550
- expect(head).toContain("<\\/style>")
550
+ expect(head).toContain('<\\/style>')
551
551
  })
552
552
 
553
- test("unkeyed tags are all preserved in resolve", () => {
553
+ test('unkeyed tags are all preserved in resolve', () => {
554
554
  const id1 = Symbol()
555
555
  const id2 = Symbol()
556
- ctx.add(id1, { tags: [{ tag: "meta", props: { name: "a", content: "1" } }] })
557
- ctx.add(id2, { tags: [{ tag: "meta", props: { name: "b", content: "2" } }] })
556
+ ctx.add(id1, { tags: [{ tag: 'meta', props: { name: 'a', content: '1' } }] })
557
+ ctx.add(id2, { tags: [{ tag: 'meta', props: { name: 'b', content: '2' } }] })
558
558
  const tags = ctx.resolve()
559
559
  expect(tags).toHaveLength(2)
560
560
  ctx.remove(id1)
561
561
  ctx.remove(id2)
562
562
  })
563
563
 
564
- test("title tag without children renders empty", async () => {
564
+ test('title tag without children renders empty', async () => {
565
565
  function Page() {
566
- useHead({ title: "" })
567
- return h("div", null)
566
+ useHead({ title: '' })
567
+ return h('div', null)
568
568
  }
569
569
  const { head } = await renderWithHead(h(Page, null))
570
- expect(head).toContain("<title></title>")
570
+ expect(head).toContain('<title></title>')
571
571
  })
572
572
 
573
- test("useHead with no context is a no-op", () => {
573
+ test('useHead with no context is a no-op', () => {
574
574
  // Calling useHead outside of any HeadProvider should not throw
575
575
  expect(() => {
576
- useHead({ title: "No Provider" })
576
+ useHead({ title: 'No Provider' })
577
577
  }).not.toThrow()
578
578
  })
579
579
 
580
- test("CSR sync creates new elements for unkeyed tags", () => {
580
+ test('CSR sync creates new elements for unkeyed tags', () => {
581
581
  function Page() {
582
- useHead({ meta: [{ name: "viewport", content: "width=device-width" }] })
583
- return h("div", null)
582
+ useHead({ meta: [{ name: 'viewport', content: 'width=device-width' }] })
583
+ return h('div', null)
584
584
  }
585
585
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
586
586
  const meta = document.head.querySelector('meta[name="viewport"]')
587
587
  expect(meta).not.toBeNull()
588
588
  })
589
589
 
590
- test("CSR patchAttrs sets new attribute values", () => {
591
- const val = signal("initial")
590
+ test('CSR patchAttrs sets new attribute values', () => {
591
+ const val = signal('initial')
592
592
  function Page() {
593
593
  useHead(() => ({
594
- meta: [{ name: "test-patch", content: val() }],
594
+ meta: [{ name: 'test-patch', content: val() }],
595
595
  }))
596
- return h("div", null)
596
+ return h('div', null)
597
597
  }
598
598
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
599
599
  const el = document.head.querySelector('meta[name="test-patch"]')
600
- expect(el?.getAttribute("content")).toBe("initial")
601
- val.set("changed")
602
- expect(el?.getAttribute("content")).toBe("changed")
600
+ expect(el?.getAttribute('content')).toBe('initial')
601
+ val.set('changed')
602
+ expect(el?.getAttribute('content')).toBe('changed')
603
603
  })
604
604
 
605
- test("dom.ts: syncDom creates new meta element when none exists", () => {
605
+ test('dom.ts: syncDom creates new meta element when none exists', () => {
606
606
  function Page() {
607
- useHead({ meta: [{ name: "keywords", content: "test,coverage" }] })
608
- return h("div", null)
607
+ useHead({ meta: [{ name: 'keywords', content: 'test,coverage' }] })
608
+ return h('div', null)
609
609
  }
610
610
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
611
611
  const meta = document.head.querySelector('meta[name="keywords"]')
612
612
  expect(meta).not.toBeNull()
613
- expect(meta?.getAttribute("content")).toBe("test,coverage")
613
+ expect(meta?.getAttribute('content')).toBe('test,coverage')
614
614
  })
615
615
 
616
- test("dom.ts: syncDom replaces element when tag name differs from found (line 41-49)", () => {
616
+ test('dom.ts: syncDom replaces element when tag name differs from found (line 41-49)', () => {
617
617
  // Pre-create a keyed element with a different tag name
618
- const existing = document.createElement("link")
619
- existing.setAttribute("data-pyreon-head", "style-0")
618
+ const existing = document.createElement('link')
619
+ existing.setAttribute('data-pyreon-head', 'style-0')
620
620
  document.head.appendChild(existing)
621
621
 
622
622
  function Page() {
623
- useHead({ style: [{ children: ".x { color: red }" }] })
624
- return h("div", null)
623
+ useHead({ style: [{ children: '.x { color: red }' }] })
624
+ return h('div', null)
625
625
  }
626
626
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
627
627
  // The old link should be removed (stale), new style element created
628
- const style = document.head.querySelector("style[data-pyreon-head]")
628
+ const style = document.head.querySelector('style[data-pyreon-head]')
629
629
  expect(style).not.toBeNull()
630
- expect(style?.textContent).toBe(".x { color: red }")
630
+ expect(style?.textContent).toBe('.x { color: red }')
631
631
  })
632
632
 
633
- test("dom.ts: syncDom handles tag with empty key (line 34)", () => {
633
+ test('dom.ts: syncDom handles tag with empty key (line 34)', () => {
634
634
  // A tag with no key should not use byKey lookup (empty key → undefined)
635
635
  function Page() {
636
636
  useHead({
637
- meta: [{ charset: "utf-8" }], // no name or property → key is "meta-0"
637
+ meta: [{ charset: 'utf-8' }], // no name or property → key is "meta-0"
638
638
  })
639
- return h("div", null)
639
+ return h('div', null)
640
640
  }
641
641
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
642
642
  const meta = document.head.querySelector('meta[charset="utf-8"]')
643
643
  expect(meta).not.toBeNull()
644
644
  })
645
645
 
646
- test("dom.ts: syncDom patches textContent when content changes (line 40)", () => {
647
- const content = signal("initial content")
646
+ test('dom.ts: syncDom patches textContent when content changes (line 40)', () => {
647
+ const content = signal('initial content')
648
648
  function Page() {
649
649
  useHead(() => ({
650
650
  style: [{ children: content() }],
651
651
  }))
652
- return h("div", null)
652
+ return h('div', null)
653
653
  }
654
654
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
655
- const style = document.head.querySelector("style[data-pyreon-head]")
656
- expect(style?.textContent).toBe("initial content")
657
- content.set("updated content")
658
- expect(style?.textContent).toBe("updated content")
655
+ const style = document.head.querySelector('style[data-pyreon-head]')
656
+ expect(style?.textContent).toBe('initial content')
657
+ content.set('updated content')
658
+ expect(style?.textContent).toBe('updated content')
659
659
  })
660
660
 
661
- test("dom.ts: syncElementAttrs removes managed-attrs tracker when all attrs removed (line 99-100)", () => {
661
+ test('dom.ts: syncElementAttrs removes managed-attrs tracker when all attrs removed (line 99-100)', () => {
662
662
  const show = signal(true)
663
663
  function Page() {
664
- useHead(() => (show() ? { bodyAttrs: { "data-theme": "dark" } } : { bodyAttrs: {} }))
665
- return h("div", null)
664
+ useHead(() => (show() ? { bodyAttrs: { 'data-theme': 'dark' } } : { bodyAttrs: {} }))
665
+ return h('div', null)
666
666
  }
667
667
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
668
- expect(document.body.getAttribute("data-theme")).toBe("dark")
669
- expect(document.body.getAttribute("data-pyreon-head-attrs")).toBe("data-theme")
668
+ expect(document.body.getAttribute('data-theme')).toBe('dark')
669
+ expect(document.body.getAttribute('data-pyreon-head-attrs')).toBe('data-theme')
670
670
  show.set(false)
671
- expect(document.body.getAttribute("data-theme")).toBeNull()
671
+ expect(document.body.getAttribute('data-theme')).toBeNull()
672
672
  // The managed-attrs tracker should also be removed
673
- expect(document.body.getAttribute("data-pyreon-head-attrs")).toBeNull()
673
+ expect(document.body.getAttribute('data-pyreon-head-attrs')).toBeNull()
674
674
  })
675
675
 
676
- test("dom.ts: syncElementAttrs updates managed attrs tracking (lines 92-98)", () => {
677
- const val = signal("en")
676
+ test('dom.ts: syncElementAttrs updates managed attrs tracking (lines 92-98)', () => {
677
+ const val = signal('en')
678
678
  function Page() {
679
- useHead(() => ({ htmlAttrs: { lang: val(), "data-x": "y" } }))
680
- return h("div", null)
679
+ useHead(() => ({ htmlAttrs: { lang: val(), 'data-x': 'y' } }))
680
+ return h('div', null)
681
681
  }
682
682
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
683
- const managed = document.documentElement.getAttribute("data-pyreon-head-attrs")
684
- expect(managed).toContain("lang")
685
- expect(managed).toContain("data-x")
683
+ const managed = document.documentElement.getAttribute('data-pyreon-head-attrs')
684
+ expect(managed).toContain('lang')
685
+ expect(managed).toContain('data-x')
686
686
  // Update to only one attr to verify partial removal
687
- val.set("fr")
688
- expect(document.documentElement.getAttribute("lang")).toBe("fr")
687
+ val.set('fr')
688
+ expect(document.documentElement.getAttribute('lang')).toBe('fr')
689
689
  })
690
690
 
691
- test("provider.tsx: HeadProvider handles function children (line 32)", () => {
691
+ test('provider.tsx: HeadProvider handles function children (line 32)', () => {
692
692
  function Page() {
693
- useHead({ title: "Func Children" })
694
- return h("div", null, "hello")
693
+ useHead({ title: 'Func Children' })
694
+ return h('div', null, 'hello')
695
695
  }
696
696
  // Pass children as a function (thunk)
697
697
  const childFn = () => h(Page, null)
698
698
  mount(h(HeadProvider, { context: ctx, children: childFn }), container)
699
- expect(document.title).toBe("Func Children")
699
+ expect(document.title).toBe('Func Children')
700
700
  })
701
701
 
702
- test("dom.ts: patchAttrs removes old attrs not in new props (line 67)", () => {
702
+ test('dom.ts: patchAttrs removes old attrs not in new props (line 67)', () => {
703
703
  const val = signal<Record<string, string>>({
704
- name: "desc",
705
- content: "old",
706
- "data-extra": "yes",
704
+ name: 'desc',
705
+ content: 'old',
706
+ 'data-extra': 'yes',
707
707
  })
708
708
  function Page() {
709
709
  useHead(() => ({ meta: [val()] }))
710
- return h("div", null)
710
+ return h('div', null)
711
711
  }
712
712
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
713
713
  const el = document.head.querySelector('meta[name="desc"]')
714
- expect(el?.getAttribute("data-extra")).toBe("yes")
714
+ expect(el?.getAttribute('data-extra')).toBe('yes')
715
715
  // Update to remove data-extra attr
716
- val.set({ name: "desc", content: "new" })
717
- expect(el?.getAttribute("data-extra")).toBeNull()
718
- expect(el?.getAttribute("content")).toBe("new")
716
+ val.set({ name: 'desc', content: 'new' })
717
+ expect(el?.getAttribute('data-extra')).toBeNull()
718
+ expect(el?.getAttribute('content')).toBe('new')
719
719
  })
720
720
 
721
- test("dom.ts: syncDom skips textContent update when content already matches (line 40 false)", () => {
721
+ test('dom.ts: syncDom skips textContent update when content already matches (line 40 false)', () => {
722
722
  const trigger = signal(0)
723
723
  function Page() {
724
724
  useHead(() => {
725
725
  trigger() // subscribe so effect re-runs
726
- return { style: [{ children: "unchanged" }] }
726
+ return { style: [{ children: 'unchanged' }] }
727
727
  })
728
- return h("div", null)
728
+ return h('div', null)
729
729
  }
730
730
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
731
- const style = document.head.querySelector("style[data-pyreon-head]")
732
- expect(style?.textContent).toBe("unchanged")
731
+ const style = document.head.querySelector('style[data-pyreon-head]')
732
+ expect(style?.textContent).toBe('unchanged')
733
733
  // Re-trigger syncDom with same content — exercises the "content matches" branch
734
734
  trigger.set(1)
735
- expect(style?.textContent).toBe("unchanged")
735
+ expect(style?.textContent).toBe('unchanged')
736
736
  })
737
737
 
738
- test("dom.ts: syncDom handles tag.children when content changes (line 39-40)", () => {
739
- const s = signal("body1")
738
+ test('dom.ts: syncDom handles tag.children when content changes (line 39-40)', () => {
739
+ const s = signal('body1')
740
740
  function Page() {
741
741
  useHead(() => ({ style: [{ children: s() }] }))
742
- return h("div", null)
742
+ return h('div', null)
743
743
  }
744
744
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
745
- const style = document.head.querySelector("style[data-pyreon-head]")
746
- expect(style?.textContent).toBe("body1")
747
- s.set("body2")
748
- expect(style?.textContent).toBe("body2")
745
+ const style = document.head.querySelector('style[data-pyreon-head]')
746
+ expect(style?.textContent).toBe('body1')
747
+ s.set('body2')
748
+ expect(style?.textContent).toBe('body2')
749
749
  })
750
750
  })
751
751
 
752
752
  // ─── createHeadContext — context stacking & caching ──────────────────────────
753
753
 
754
- describe("createHeadContext", () => {
755
- test("resolve returns empty array when no entries", () => {
754
+ describe('createHeadContext', () => {
755
+ test('resolve returns empty array when no entries', () => {
756
756
  const ctx = createHeadContext()
757
757
  expect(ctx.resolve()).toEqual([])
758
758
  })
759
759
 
760
- test("resolve caches result until dirty", () => {
760
+ test('resolve caches result until dirty', () => {
761
761
  const ctx = createHeadContext()
762
762
  const id = Symbol()
763
- ctx.add(id, { tags: [{ tag: "meta", key: "a", props: { name: "a" } }] })
763
+ ctx.add(id, { tags: [{ tag: 'meta', key: 'a', props: { name: 'a' } }] })
764
764
  const first = ctx.resolve()
765
765
  const second = ctx.resolve()
766
766
  expect(first).toBe(second) // same array reference — cached
767
767
  })
768
768
 
769
- test("resolve invalidates cache after add", () => {
769
+ test('resolve invalidates cache after add', () => {
770
770
  const ctx = createHeadContext()
771
771
  const id1 = Symbol()
772
- ctx.add(id1, { tags: [{ tag: "meta", key: "a", props: { name: "a" } }] })
772
+ ctx.add(id1, { tags: [{ tag: 'meta', key: 'a', props: { name: 'a' } }] })
773
773
  const first = ctx.resolve()
774
774
  const id2 = Symbol()
775
- ctx.add(id2, { tags: [{ tag: "meta", key: "b", props: { name: "b" } }] })
775
+ ctx.add(id2, { tags: [{ tag: 'meta', key: 'b', props: { name: 'b' } }] })
776
776
  const second = ctx.resolve()
777
777
  expect(first).not.toBe(second) // different reference — rebuilt
778
778
  expect(second).toHaveLength(2)
779
779
  })
780
780
 
781
- test("resolve invalidates cache after remove", () => {
781
+ test('resolve invalidates cache after remove', () => {
782
782
  const ctx = createHeadContext()
783
783
  const id = Symbol()
784
- ctx.add(id, { tags: [{ tag: "meta", key: "a", props: { name: "a" } }] })
784
+ ctx.add(id, { tags: [{ tag: 'meta', key: 'a', props: { name: 'a' } }] })
785
785
  const first = ctx.resolve()
786
786
  ctx.remove(id)
787
787
  const second = ctx.resolve()
@@ -789,59 +789,59 @@ describe("createHeadContext", () => {
789
789
  expect(first).not.toBe(second)
790
790
  })
791
791
 
792
- test("keyed tags deduplicate — last added wins", () => {
792
+ test('keyed tags deduplicate — last added wins', () => {
793
793
  const ctx = createHeadContext()
794
794
  const id1 = Symbol()
795
795
  const id2 = Symbol()
796
- ctx.add(id1, { tags: [{ tag: "title", key: "title", children: "First" }] })
797
- ctx.add(id2, { tags: [{ tag: "title", key: "title", children: "Second" }] })
796
+ ctx.add(id1, { tags: [{ tag: 'title', key: 'title', children: 'First' }] })
797
+ ctx.add(id2, { tags: [{ tag: 'title', key: 'title', children: 'Second' }] })
798
798
  const tags = ctx.resolve()
799
799
  expect(tags).toHaveLength(1)
800
- expect(tags[0]?.children).toBe("Second")
800
+ expect(tags[0]?.children).toBe('Second')
801
801
  })
802
802
 
803
- test("resolveTitleTemplate returns undefined when none set", () => {
803
+ test('resolveTitleTemplate returns undefined when none set', () => {
804
804
  const ctx = createHeadContext()
805
805
  expect(ctx.resolveTitleTemplate()).toBeUndefined()
806
806
  })
807
807
 
808
- test("resolveTitleTemplate returns last added template", () => {
808
+ test('resolveTitleTemplate returns last added template', () => {
809
809
  const ctx = createHeadContext()
810
810
  const id1 = Symbol()
811
811
  const id2 = Symbol()
812
- ctx.add(id1, { tags: [], titleTemplate: "%s | Site A" })
813
- ctx.add(id2, { tags: [], titleTemplate: "%s | Site B" })
814
- expect(ctx.resolveTitleTemplate()).toBe("%s | Site B")
812
+ ctx.add(id1, { tags: [], titleTemplate: '%s | Site A' })
813
+ ctx.add(id2, { tags: [], titleTemplate: '%s | Site B' })
814
+ expect(ctx.resolveTitleTemplate()).toBe('%s | Site B')
815
815
  })
816
816
 
817
- test("resolveHtmlAttrs merges from multiple entries", () => {
817
+ test('resolveHtmlAttrs merges from multiple entries', () => {
818
818
  const ctx = createHeadContext()
819
819
  const id1 = Symbol()
820
820
  const id2 = Symbol()
821
- ctx.add(id1, { tags: [], htmlAttrs: { lang: "en" } })
822
- ctx.add(id2, { tags: [], htmlAttrs: { dir: "ltr" } })
823
- expect(ctx.resolveHtmlAttrs()).toEqual({ lang: "en", dir: "ltr" })
821
+ ctx.add(id1, { tags: [], htmlAttrs: { lang: 'en' } })
822
+ ctx.add(id2, { tags: [], htmlAttrs: { dir: 'ltr' } })
823
+ expect(ctx.resolveHtmlAttrs()).toEqual({ lang: 'en', dir: 'ltr' })
824
824
  })
825
825
 
826
- test("resolveHtmlAttrs later entries override earlier", () => {
826
+ test('resolveHtmlAttrs later entries override earlier', () => {
827
827
  const ctx = createHeadContext()
828
828
  const id1 = Symbol()
829
829
  const id2 = Symbol()
830
- ctx.add(id1, { tags: [], htmlAttrs: { lang: "en" } })
831
- ctx.add(id2, { tags: [], htmlAttrs: { lang: "fr" } })
832
- expect(ctx.resolveHtmlAttrs()).toEqual({ lang: "fr" })
830
+ ctx.add(id1, { tags: [], htmlAttrs: { lang: 'en' } })
831
+ ctx.add(id2, { tags: [], htmlAttrs: { lang: 'fr' } })
832
+ expect(ctx.resolveHtmlAttrs()).toEqual({ lang: 'fr' })
833
833
  })
834
834
 
835
- test("resolveBodyAttrs merges from multiple entries", () => {
835
+ test('resolveBodyAttrs merges from multiple entries', () => {
836
836
  const ctx = createHeadContext()
837
837
  const id1 = Symbol()
838
838
  const id2 = Symbol()
839
- ctx.add(id1, { tags: [], bodyAttrs: { class: "dark" } })
840
- ctx.add(id2, { tags: [], bodyAttrs: { "data-page": "home" } })
841
- expect(ctx.resolveBodyAttrs()).toEqual({ class: "dark", "data-page": "home" })
839
+ ctx.add(id1, { tags: [], bodyAttrs: { class: 'dark' } })
840
+ ctx.add(id2, { tags: [], bodyAttrs: { 'data-page': 'home' } })
841
+ expect(ctx.resolveBodyAttrs()).toEqual({ class: 'dark', 'data-page': 'home' })
842
842
  })
843
843
 
844
- test("remove non-existent id does not throw", () => {
844
+ test('remove non-existent id does not throw', () => {
845
845
  const ctx = createHeadContext()
846
846
  expect(() => ctx.remove(Symbol())).not.toThrow()
847
847
  })
@@ -849,316 +849,316 @@ describe("createHeadContext", () => {
849
849
 
850
850
  // ─── HeadProvider — context stacking ─────────────────────────────────────────
851
851
 
852
- describe("HeadProvider — context stacking", () => {
852
+ describe('HeadProvider — context stacking', () => {
853
853
  let container: HTMLElement
854
854
 
855
855
  beforeEach(() => {
856
- container = document.createElement("div")
856
+ container = document.createElement('div')
857
857
  document.body.appendChild(container)
858
- for (const el of document.head.querySelectorAll("[data-pyreon-head]")) el.remove()
859
- document.title = ""
858
+ for (const el of document.head.querySelectorAll('[data-pyreon-head]')) el.remove()
859
+ document.title = ''
860
860
  })
861
861
 
862
- test("auto-creates context when no context prop", () => {
862
+ test('auto-creates context when no context prop', () => {
863
863
  function Page() {
864
- useHead({ title: "Auto Context" })
865
- return h("div", null)
864
+ useHead({ title: 'Auto Context' })
865
+ return h('div', null)
866
866
  }
867
867
  // HeadProvider without context prop
868
868
  mount(h(HeadProvider, { children: h(Page, null) }), container)
869
- expect(document.title).toBe("Auto Context")
869
+ expect(document.title).toBe('Auto Context')
870
870
  })
871
871
 
872
- test("nested HeadProviders — inner context receives inner useHead calls", () => {
872
+ test('nested HeadProviders — inner context receives inner useHead calls', () => {
873
873
  const outerCtx = createHeadContext()
874
874
  const innerCtx = createHeadContext()
875
875
 
876
876
  function Outer() {
877
- useHead({ title: "Outer" })
878
- return h("div", null, h(HeadProvider, { context: innerCtx, children: h(Inner, null) }))
877
+ useHead({ title: 'Outer' })
878
+ return h('div', null, h(HeadProvider, { context: innerCtx, children: h(Inner, null) }))
879
879
  }
880
880
  function Inner() {
881
- useHead({ title: "Inner" })
882
- return h("span", null)
881
+ useHead({ title: 'Inner' })
882
+ return h('span', null)
883
883
  }
884
884
  mount(h(HeadProvider, { context: outerCtx, children: h(Outer, null) }), container)
885
885
  // Outer context has "Outer" title, inner context has "Inner" title
886
886
  // The outer syncDom runs and sets title to "Outer"
887
887
  // Both contexts sync independently
888
888
  const outerTags = outerCtx.resolve()
889
- expect(outerTags.some((t) => t.children === "Outer")).toBe(true)
889
+ expect(outerTags.some((t) => t.children === 'Outer')).toBe(true)
890
890
  // The inner context should have Inner's title registered
891
891
  const innerTags = innerCtx.resolve()
892
- expect(innerTags.some((t) => t.children === "Inner")).toBe(true)
892
+ expect(innerTags.some((t) => t.children === 'Inner')).toBe(true)
893
893
  })
894
894
  })
895
895
 
896
896
  // ─── useHead with reactive signals — CSR ────────────────────────────────────
897
897
 
898
- describe("useHead — reactive signal-driven values", () => {
898
+ describe('useHead — reactive signal-driven values', () => {
899
899
  let container: HTMLElement
900
900
  let ctx: HeadContextValue
901
901
 
902
902
  beforeEach(() => {
903
- container = document.createElement("div")
903
+ container = document.createElement('div')
904
904
  document.body.appendChild(container)
905
905
  ctx = createHeadContext()
906
- for (const el of document.head.querySelectorAll("[data-pyreon-head]")) el.remove()
907
- document.title = ""
906
+ for (const el of document.head.querySelectorAll('[data-pyreon-head]')) el.remove()
907
+ document.title = ''
908
908
  })
909
909
 
910
- test("reactive meta tags update when signal changes", () => {
911
- const description = signal("Initial description")
910
+ test('reactive meta tags update when signal changes', () => {
911
+ const description = signal('Initial description')
912
912
  function Page() {
913
913
  useHead(() => ({
914
- meta: [{ name: "description", content: description() }],
914
+ meta: [{ name: 'description', content: description() }],
915
915
  }))
916
- return h("div", null)
916
+ return h('div', null)
917
917
  }
918
918
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
919
- expect(document.head.querySelector('meta[name="description"]')?.getAttribute("content")).toBe(
920
- "Initial description",
919
+ expect(document.head.querySelector('meta[name="description"]')?.getAttribute('content')).toBe(
920
+ 'Initial description',
921
921
  )
922
- description.set("Updated description")
923
- expect(document.head.querySelector('meta[name="description"]')?.getAttribute("content")).toBe(
924
- "Updated description",
922
+ description.set('Updated description')
923
+ expect(document.head.querySelector('meta[name="description"]')?.getAttribute('content')).toBe(
924
+ 'Updated description',
925
925
  )
926
926
  })
927
927
 
928
- test("reactive link tags update when signal changes", () => {
929
- const href = signal("/page-v1")
928
+ test('reactive link tags update when signal changes', () => {
929
+ const href = signal('/page-v1')
930
930
  function Page() {
931
931
  useHead(() => ({
932
- link: [{ rel: "canonical", href: href() }],
932
+ link: [{ rel: 'canonical', href: href() }],
933
933
  }))
934
- return h("div", null)
934
+ return h('div', null)
935
935
  }
936
936
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
937
937
  const link = document.head.querySelector('link[rel="canonical"]')
938
- expect(link?.getAttribute("href")).toBe("/page-v1")
939
- href.set("/page-v2")
938
+ expect(link?.getAttribute('href')).toBe('/page-v1')
939
+ href.set('/page-v2')
940
940
  // A new link element is created because the key changes (includes href)
941
- const newLink = document.head.querySelector("link[data-pyreon-head]")
941
+ const newLink = document.head.querySelector('link[data-pyreon-head]')
942
942
  expect(newLink).not.toBeNull()
943
943
  })
944
944
 
945
- test("reactive bodyAttrs update when signal changes", () => {
946
- const theme = signal("light")
945
+ test('reactive bodyAttrs update when signal changes', () => {
946
+ const theme = signal('light')
947
947
  function Page() {
948
- useHead(() => ({ bodyAttrs: { "data-theme": theme() } }))
949
- return h("div", null)
948
+ useHead(() => ({ bodyAttrs: { 'data-theme': theme() } }))
949
+ return h('div', null)
950
950
  }
951
951
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
952
- expect(document.body.getAttribute("data-theme")).toBe("light")
953
- theme.set("dark")
954
- expect(document.body.getAttribute("data-theme")).toBe("dark")
952
+ expect(document.body.getAttribute('data-theme')).toBe('light')
953
+ theme.set('dark')
954
+ expect(document.body.getAttribute('data-theme')).toBe('dark')
955
955
  })
956
956
 
957
- test("reactive htmlAttrs update when signal changes", () => {
958
- const lang = signal("en")
957
+ test('reactive htmlAttrs update when signal changes', () => {
958
+ const lang = signal('en')
959
959
  function Page() {
960
960
  useHead(() => ({ htmlAttrs: { lang: lang() } }))
961
- return h("div", null)
961
+ return h('div', null)
962
962
  }
963
963
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
964
- expect(document.documentElement.getAttribute("lang")).toBe("en")
965
- lang.set("de")
966
- expect(document.documentElement.getAttribute("lang")).toBe("de")
964
+ expect(document.documentElement.getAttribute('lang')).toBe('en')
965
+ lang.set('de')
966
+ expect(document.documentElement.getAttribute('lang')).toBe('de')
967
967
  })
968
968
 
969
- test("reactive jsonLd updates when signal changes", () => {
970
- const pageName = signal("Home")
969
+ test('reactive jsonLd updates when signal changes', () => {
970
+ const pageName = signal('Home')
971
971
  function Page() {
972
- useHead(() => ({ jsonLd: { "@type": "WebPage", name: pageName() } }))
973
- return h("div", null)
972
+ useHead(() => ({ jsonLd: { '@type': 'WebPage', name: pageName() } }))
973
+ return h('div', null)
974
974
  }
975
975
  mount(h(HeadProvider, { context: ctx, children: h(Page, null) }), container)
976
976
  const script = document.head.querySelector('script[type="application/ld+json"]')
977
977
  expect(script?.textContent).toContain('"name":"Home"')
978
- pageName.set("About")
978
+ pageName.set('About')
979
979
  const updated = document.head.querySelector('script[type="application/ld+json"]')
980
980
  expect(updated?.textContent).toContain('"name":"About"')
981
981
  })
982
982
 
983
- test("reactive titleTemplate with signal-driven title", () => {
984
- const pageTitle = signal("Home")
983
+ test('reactive titleTemplate with signal-driven title', () => {
984
+ const pageTitle = signal('Home')
985
985
  function Layout() {
986
- useHead({ titleTemplate: "%s | MySite" })
987
- return h("div", null, h(Page, null))
986
+ useHead({ titleTemplate: '%s | MySite' })
987
+ return h('div', null, h(Page, null))
988
988
  }
989
989
  function Page() {
990
990
  useHead(() => ({ title: pageTitle() }))
991
- return h("span", null)
991
+ return h('span', null)
992
992
  }
993
993
  mount(h(HeadProvider, { context: ctx, children: h(Layout, null) }), container)
994
- expect(document.title).toBe("Home | MySite")
995
- pageTitle.set("About")
996
- expect(document.title).toBe("About | MySite")
994
+ expect(document.title).toBe('Home | MySite')
995
+ pageTitle.set('About')
996
+ expect(document.title).toBe('About | MySite')
997
997
  })
998
998
  })
999
999
 
1000
1000
  // ─── renderWithHead — SSR subpath import ─────────────────────────────────────
1001
1001
 
1002
- describe("renderWithHead — @pyreon/head/ssr subpath", () => {
1003
- test("renderWithHead is importable from ssr module", async () => {
1004
- const mod = await import("../ssr")
1005
- expect(typeof mod.renderWithHead).toBe("function")
1002
+ describe('renderWithHead — @pyreon/head/ssr subpath', () => {
1003
+ test('renderWithHead is importable from ssr module', async () => {
1004
+ const mod = await import('../ssr')
1005
+ expect(typeof mod.renderWithHead).toBe('function')
1006
1006
  })
1007
1007
 
1008
- test("renderWithHead with multiple useHead calls merges tags", async () => {
1008
+ test('renderWithHead with multiple useHead calls merges tags', async () => {
1009
1009
  function Layout() {
1010
- useHead({ titleTemplate: "%s | App", htmlAttrs: { lang: "en" } })
1011
- return h("div", null, h(Page, null))
1010
+ useHead({ titleTemplate: '%s | App', htmlAttrs: { lang: 'en' } })
1011
+ return h('div', null, h(Page, null))
1012
1012
  }
1013
1013
  function Page() {
1014
1014
  useHead({
1015
- title: "Dashboard",
1016
- meta: [{ name: "description", content: "Dashboard page" }],
1017
- bodyAttrs: { class: "dashboard" },
1015
+ title: 'Dashboard',
1016
+ meta: [{ name: 'description', content: 'Dashboard page' }],
1017
+ bodyAttrs: { class: 'dashboard' },
1018
1018
  })
1019
- return h("span", null)
1019
+ return h('span', null)
1020
1020
  }
1021
1021
  const result = await renderWithHead(h(Layout, null))
1022
- expect(result.head).toContain("<title>Dashboard | App</title>")
1022
+ expect(result.head).toContain('<title>Dashboard | App</title>')
1023
1023
  expect(result.head).toContain('name="description"')
1024
- expect(result.htmlAttrs).toEqual({ lang: "en" })
1025
- expect(result.bodyAttrs).toEqual({ class: "dashboard" })
1024
+ expect(result.htmlAttrs).toEqual({ lang: 'en' })
1025
+ expect(result.bodyAttrs).toEqual({ class: 'dashboard' })
1026
1026
  })
1027
1027
 
1028
- test("renderWithHead with empty head returns empty string", async () => {
1028
+ test('renderWithHead with empty head returns empty string', async () => {
1029
1029
  function Page() {
1030
- return h("div", null, "content")
1030
+ return h('div', null, 'content')
1031
1031
  }
1032
1032
  const result = await renderWithHead(h(Page, null))
1033
- expect(result.head).toBe("")
1034
- expect(result.html).toContain("content")
1033
+ expect(result.head).toBe('')
1034
+ expect(result.html).toContain('content')
1035
1035
  })
1036
1036
 
1037
- test("renderWithHead serializes HTML comment openers in script content", async () => {
1037
+ test('renderWithHead serializes HTML comment openers in script content', async () => {
1038
1038
  function Page() {
1039
- useHead({ script: [{ children: "if (x <!-- y) {}" }] })
1040
- return h("div", null)
1039
+ useHead({ script: [{ children: 'if (x <!-- y) {}' }] })
1040
+ return h('div', null)
1041
1041
  }
1042
1042
  const { head } = await renderWithHead(h(Page, null))
1043
- expect(head).toContain("<\\!--")
1044
- expect(head).not.toContain("<!--")
1043
+ expect(head).toContain('<\\!--')
1044
+ expect(head).not.toContain('<!--')
1045
1045
  })
1046
1046
  })
1047
1047
 
1048
1048
  // ─── SSR — additional branch coverage ────────────────────────────────────────
1049
1049
 
1050
- describe("renderWithHead — SSR additional branches", () => {
1051
- test("ssr.ts: title without children (line 60)", async () => {
1050
+ describe('renderWithHead — SSR additional branches', () => {
1051
+ test('ssr.ts: title without children (line 60)', async () => {
1052
1052
  function Page() {
1053
1053
  useHead({} as any) // no title field at all
1054
- return h("div", null)
1054
+ return h('div', null)
1055
1055
  }
1056
1056
  const { head } = await renderWithHead(h(Page, null))
1057
- expect(head).toBe("") // no tags
1057
+ expect(head).toBe('') // no tags
1058
1058
  })
1059
1059
 
1060
- test("ssr.ts: non-raw tag with children (line 76)", async () => {
1060
+ test('ssr.ts: non-raw tag with children (line 76)', async () => {
1061
1061
  function Page() {
1062
1062
  useHead({
1063
- noscript: [{ children: "<p>Enable JavaScript</p>" }],
1063
+ noscript: [{ children: '<p>Enable JavaScript</p>' }],
1064
1064
  })
1065
- return h("div", null)
1065
+ return h('div', null)
1066
1066
  }
1067
1067
  const { head } = await renderWithHead(h(Page, null))
1068
- expect(head).toContain("<noscript>")
1069
- expect(head).toContain("Enable JavaScript")
1068
+ expect(head).toContain('<noscript>')
1069
+ expect(head).toContain('Enable JavaScript')
1070
1070
  })
1071
1071
 
1072
- test("ssr.ts: function titleTemplate in SSR", async () => {
1072
+ test('ssr.ts: function titleTemplate in SSR', async () => {
1073
1073
  function Page() {
1074
1074
  useHead({
1075
- title: "Page Title",
1075
+ title: 'Page Title',
1076
1076
  titleTemplate: (t: string) => `${t} | MySite`,
1077
1077
  })
1078
- return h("div", null)
1078
+ return h('div', null)
1079
1079
  }
1080
1080
  const { head } = await renderWithHead(h(Page, null))
1081
- expect(head).toContain("Page Title | MySite")
1081
+ expect(head).toContain('Page Title | MySite')
1082
1082
  })
1083
1083
 
1084
- test("ssr.ts: string titleTemplate in SSR", async () => {
1084
+ test('ssr.ts: string titleTemplate in SSR', async () => {
1085
1085
  function Page() {
1086
1086
  useHead({
1087
- title: "Page",
1088
- titleTemplate: "%s - App",
1087
+ title: 'Page',
1088
+ titleTemplate: '%s - App',
1089
1089
  })
1090
- return h("div", null)
1090
+ return h('div', null)
1091
1091
  }
1092
1092
  const { head } = await renderWithHead(h(Page, null))
1093
- expect(head).toContain("Page - App")
1093
+ expect(head).toContain('Page - App')
1094
1094
  })
1095
1095
 
1096
- test("use-head.ts: reactive input in SSR evaluates once (line 86-88)", async () => {
1096
+ test('use-head.ts: reactive input in SSR evaluates once (line 86-88)', async () => {
1097
1097
  function Page() {
1098
1098
  useHead(() => ({
1099
- title: "SSR Reactive",
1100
- meta: [{ name: "desc", content: "from function" }],
1099
+ title: 'SSR Reactive',
1100
+ meta: [{ name: 'desc', content: 'from function' }],
1101
1101
  }))
1102
- return h("div", null)
1102
+ return h('div', null)
1103
1103
  }
1104
1104
  const { head } = await renderWithHead(h(Page, null))
1105
- expect(head).toContain("SSR Reactive")
1105
+ expect(head).toContain('SSR Reactive')
1106
1106
  })
1107
1107
 
1108
- test("use-head.ts: link key without href falls back to rel (line 20)", async () => {
1108
+ test('use-head.ts: link key without href falls back to rel (line 20)', async () => {
1109
1109
  function Page() {
1110
1110
  useHead({
1111
1111
  link: [
1112
- { rel: "preconnect" }, // no href → key uses rel
1113
- { rel: "dns-prefetch" }, // another no-href
1112
+ { rel: 'preconnect' }, // no href → key uses rel
1113
+ { rel: 'dns-prefetch' }, // another no-href
1114
1114
  ],
1115
1115
  })
1116
- return h("div", null)
1116
+ return h('div', null)
1117
1117
  }
1118
1118
  const { head } = await renderWithHead(h(Page, null))
1119
- expect(head).toContain("preconnect")
1120
- expect(head).toContain("dns-prefetch")
1119
+ expect(head).toContain('preconnect')
1120
+ expect(head).toContain('dns-prefetch')
1121
1121
  })
1122
1122
 
1123
- test("use-head.ts: link key without href or rel falls back to index (line 20)", async () => {
1123
+ test('use-head.ts: link key without href or rel falls back to index (line 20)', async () => {
1124
1124
  function Page() {
1125
1125
  useHead({
1126
- link: [{ type: "text/css" }], // no href, no rel → key is "link-0"
1126
+ link: [{ type: 'text/css' }], // no href, no rel → key is "link-0"
1127
1127
  })
1128
- return h("div", null)
1128
+ return h('div', null)
1129
1129
  }
1130
1130
  const { head } = await renderWithHead(h(Page, null))
1131
- expect(head).toContain("text/css")
1131
+ expect(head).toContain('text/css')
1132
1132
  })
1133
1133
 
1134
- test("use-head.ts: script with children in SSR (line 30)", async () => {
1134
+ test('use-head.ts: script with children in SSR (line 30)', async () => {
1135
1135
  function Page() {
1136
1136
  useHead({
1137
1137
  script: [{ children: "console.log('hi')" }],
1138
1138
  })
1139
- return h("div", null)
1139
+ return h('div', null)
1140
1140
  }
1141
1141
  const { head } = await renderWithHead(h(Page, null))
1142
- expect(head).toContain("console.log")
1142
+ expect(head).toContain('console.log')
1143
1143
  })
1144
1144
 
1145
- test("ssr.ts: htmlAttrs and bodyAttrs in result", async () => {
1145
+ test('ssr.ts: htmlAttrs and bodyAttrs in result', async () => {
1146
1146
  function Page() {
1147
1147
  useHead({
1148
- htmlAttrs: { lang: "en", dir: "ltr" },
1149
- bodyAttrs: { class: "dark" },
1148
+ htmlAttrs: { lang: 'en', dir: 'ltr' },
1149
+ bodyAttrs: { class: 'dark' },
1150
1150
  })
1151
- return h("div", null)
1151
+ return h('div', null)
1152
1152
  }
1153
1153
  const { htmlAttrs, bodyAttrs } = await renderWithHead(h(Page, null))
1154
- expect(htmlAttrs.lang).toBe("en")
1155
- expect(bodyAttrs.class).toBe("dark")
1154
+ expect(htmlAttrs.lang).toBe('en')
1155
+ expect(bodyAttrs.class).toBe('dark')
1156
1156
  })
1157
1157
  })
1158
1158
 
1159
1159
  // ─── SSR paths via document mocking ──────────────────────────────────────────
1160
1160
 
1161
- describe("useHead — SSR paths (document undefined)", () => {
1161
+ describe('useHead — SSR paths (document undefined)', () => {
1162
1162
  const origDoc = globalThis.document
1163
1163
 
1164
1164
  beforeEach(() => {
@@ -1169,19 +1169,21 @@ describe("useHead — SSR paths (document undefined)", () => {
1169
1169
  globalThis.document = origDoc
1170
1170
  })
1171
1171
 
1172
- test("syncDom is no-op when document is undefined (dom.ts line 13)", async () => {
1173
- const { syncDom } = await import("../dom")
1172
+ test('syncDom is no-op when document is undefined (dom.ts line 13)', async () => {
1173
+ const { syncDom } = await import('../dom')
1174
1174
  const ctx2 = createHeadContext()
1175
- ctx2.add(Symbol(), { tags: [{ tag: "meta", key: "test", props: { name: "x", content: "y" } }] })
1175
+ ctx2.add(Symbol(), {
1176
+ tags: [{ tag: 'meta', key: 'test', props: { name: 'x', content: 'y' } }],
1177
+ })
1176
1178
  // Should not throw — early return because document is undefined
1177
1179
  syncDom(ctx2)
1178
1180
  })
1179
1181
 
1180
- test("useHead static input registers synchronously in SSR (use-head.ts line 88-92)", async () => {
1182
+ test('useHead static input registers synchronously in SSR (use-head.ts line 88-92)', async () => {
1181
1183
  // In SSR (no document), static input doesn't trigger syncDom
1182
- const { useHead: _uh } = await import("../use-head")
1184
+ const { useHead: _uh } = await import('../use-head')
1183
1185
  // This just verifies the code path doesn't error when document is undefined
1184
1186
  // The actual registration happens via the context
1185
- expect(typeof document).toBe("undefined")
1187
+ expect(typeof document).toBe('undefined')
1186
1188
  })
1187
1189
  })