@setzkasten-cms/astro-admin 0.6.0
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.
- package/LICENSE +37 -0
- package/package.json +70 -0
- package/src/admin-page.astro +148 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +383 -0
- package/src/api-routes/__tests__/catalog-api.test.ts +115 -0
- package/src/api-routes/__tests__/deferred-operations.test.ts +232 -0
- package/src/api-routes/__tests__/deploy-hook.test.ts +134 -0
- package/src/api-routes/__tests__/patch-page-file.test.ts +193 -0
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +162 -0
- package/src/api-routes/__tests__/section-management.test.ts +284 -0
- package/src/api-routes/_storage-config.ts +54 -0
- package/src/api-routes/asset-proxy.ts +76 -0
- package/src/api-routes/auth-callback.ts +105 -0
- package/src/api-routes/auth-login.ts +87 -0
- package/src/api-routes/auth-logout.ts +9 -0
- package/src/api-routes/auth-session.ts +36 -0
- package/src/api-routes/catalog-add.ts +151 -0
- package/src/api-routes/catalog-export.ts +86 -0
- package/src/api-routes/catalog-helpers.ts +83 -0
- package/src/api-routes/catalog-list.ts +12 -0
- package/src/api-routes/config.ts +30 -0
- package/src/api-routes/deploy-hook.ts +69 -0
- package/src/api-routes/github-proxy.ts +111 -0
- package/src/api-routes/init-add-section.ts +511 -0
- package/src/api-routes/init-apply.ts +270 -0
- package/src/api-routes/init-migrate.ts +262 -0
- package/src/api-routes/init-scan-page.ts +336 -0
- package/src/api-routes/init-scan.ts +162 -0
- package/src/api-routes/pages.ts +17 -0
- package/src/api-routes/section-add.ts +189 -0
- package/src/api-routes/section-commit-pending.ts +147 -0
- package/src/api-routes/section-delete.ts +141 -0
- package/src/api-routes/section-duplicate.ts +144 -0
- package/src/api-routes/section-management.ts +95 -0
- package/src/api-routes/section-prepare-copy.ts +93 -0
- package/src/api-routes/section-prepare.ts +121 -0
- package/src/env.d.ts +7 -0
- package/src/init/__tests__/page-level.test.ts +1033 -0
- package/src/init/__tests__/page-list-coverage.test.ts +474 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +434 -0
- package/src/init/__tests__/patcher-page-mode.test.ts +272 -0
- package/src/init/__tests__/section-pipeline.test.ts +393 -0
- package/src/init/analyzer-types.ts +92 -0
- package/src/init/astro-config-patcher.ts +98 -0
- package/src/init/astro-detector.ts +207 -0
- package/src/init/astro-section-analyzer-v2.ts +1663 -0
- package/src/init/field-label-enricher.ts +72 -0
- package/src/init/template-patcher-v2.ts +1957 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: hardcoded <li> items in docs pages.
|
|
3
|
+
*
|
|
4
|
+
* Bug: When a docs page has <ul>/<li> elements with hardcoded text,
|
|
5
|
+
* those items are invisible to the CMS editor after adoption — the analyzer
|
|
6
|
+
* detected headings and paragraphs but missed static list item content.
|
|
7
|
+
*
|
|
8
|
+
* Covers:
|
|
9
|
+
* 1. Analyzer: detects <li> text content as array fields
|
|
10
|
+
* 2. Analyzer: total field count includes list items
|
|
11
|
+
* 3. Patcher: after patching, no <li> contains hardcoded text (only CMS bindings)
|
|
12
|
+
* 4. Patcher: list items are driven by .map() over CMS array
|
|
13
|
+
* 5. Regression: PageWithListItemsAdopted is already clean (all CMS-bound)
|
|
14
|
+
* 6. Regression: real docs pages have no hardcoded <li> text after fix
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { join, resolve } from 'node:path'
|
|
20
|
+
import { analyzeAstroSection } from '../astro-section-analyzer-v2'
|
|
21
|
+
import { patchTemplateForFields } from '../template-patcher-v2'
|
|
22
|
+
|
|
23
|
+
const SECTIONS_DIR = resolve(import.meta.dirname!, '..', '..', '..', '..', '..', 'test-sections')
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helper: find hardcoded <li> text — a <span> inside <li> with literal text,
|
|
27
|
+
// no set:html attribute, and not inside a {(...).map(...)} expression.
|
|
28
|
+
// After a correct adoption, this should return 0 matches.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function findHardcodedListItems(source: string): RegExpMatchArray[] {
|
|
31
|
+
// Match <span> that has direct text (no set:html, no {expr}), inside a <li>
|
|
32
|
+
// We look for: <span> followed by a letter (not { or <), then text, then </span>
|
|
33
|
+
const pattern = /<li[\s\S]{0,300}?<span(?![^>]*set:html)(?![^>]*data-sk-field)[^>]*>([A-Za-zÄÖÜäöüß][^<{]*)<\/span>/g
|
|
34
|
+
return [...source.matchAll(pattern)]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// 1. Fixture: PageWithHardcodedList (the bug scenario)
|
|
39
|
+
// The analyzer SHOULD find the list item texts as array fields.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
describe('PageWithHardcodedList — analyzer detects list items', () => {
|
|
42
|
+
let source: string
|
|
43
|
+
let section: Awaited<ReturnType<typeof analyzeAstroSection>>
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
source = readFileSync(join(SECTIONS_DIR, 'PageWithHardcodedList.astro'), 'utf-8')
|
|
47
|
+
section = await analyzeAstroSection(
|
|
48
|
+
source,
|
|
49
|
+
'_page_docs_admin',
|
|
50
|
+
'adminPage',
|
|
51
|
+
'src/pages/docs/admin.astro',
|
|
52
|
+
{ mode: 'page' },
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should detect fields (page has visible content)', () => {
|
|
57
|
+
expect(section.fields.length).toBeGreaterThan(0)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should detect the h2 headings as text fields', () => {
|
|
61
|
+
const keys = section.fields.map(f => f.key)
|
|
62
|
+
// The page has "Page Builder", "Section-Editor", "Collections" as <h2>
|
|
63
|
+
const hasHeadings = keys.some(k => k.includes('heading') || k.includes('pageBuild') || k.includes('section'))
|
|
64
|
+
expect(hasHeadings, 'Should detect h2 headings').toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should detect list item content as array fields', () => {
|
|
68
|
+
// List items should appear either as array fields or individual text fields
|
|
69
|
+
const arrayFields = section.fields.filter(f => f.type === 'array')
|
|
70
|
+
const allFields = section.fields
|
|
71
|
+
|
|
72
|
+
// Either the list items are grouped as arrays, or they appear as individual text fields
|
|
73
|
+
// In both cases, the TOTAL field count must be higher than just headings+descriptions
|
|
74
|
+
// (3 headings + 3 descriptions = 6; with list items: should be > 6)
|
|
75
|
+
expect(allFields.length).toBeGreaterThan(6)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should find list item default values', () => {
|
|
79
|
+
const allDefaultValues = section.fields
|
|
80
|
+
.flatMap(f => {
|
|
81
|
+
if (f.type === 'array' && Array.isArray(f.defaultValue)) return f.defaultValue as string[]
|
|
82
|
+
return typeof f.defaultValue === 'string' ? [f.defaultValue] : []
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const hasListContent = allDefaultValues.some(v =>
|
|
86
|
+
typeof v === 'string' && (
|
|
87
|
+
v.includes('adoptieren') ||
|
|
88
|
+
v.includes('Textfelder') ||
|
|
89
|
+
v.includes('Eintraege')
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
expect(hasListContent, 'List item text should appear in detected field default values').toBe(true)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 2. Patcher: after patching PageWithHardcodedList, no hardcoded <li> text remains
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
describe('PageWithHardcodedList — patcher eliminates hardcoded list items', () => {
|
|
100
|
+
let patched: string
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithHardcodedList.astro'), 'utf-8')
|
|
104
|
+
const section = await analyzeAstroSection(
|
|
105
|
+
source,
|
|
106
|
+
'_page_docs_admin',
|
|
107
|
+
'adminPage',
|
|
108
|
+
'src/pages/docs/admin.astro',
|
|
109
|
+
{ mode: 'page' },
|
|
110
|
+
)
|
|
111
|
+
const groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
112
|
+
patched = await patchTemplateForFields(
|
|
113
|
+
source,
|
|
114
|
+
'_page_docs_admin',
|
|
115
|
+
section.fields,
|
|
116
|
+
groups,
|
|
117
|
+
{ mode: 'page' },
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should not contain hardcoded <li> text after patching', () => {
|
|
122
|
+
const hardcoded = findHardcodedListItems(patched)
|
|
123
|
+
expect(
|
|
124
|
+
hardcoded,
|
|
125
|
+
`Hardcoded list items found after patching:\n${hardcoded.map(m => ` "${m[1]}"`).join('\n')}`,
|
|
126
|
+
).toHaveLength(0)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should contain data-sk-field on list items', () => {
|
|
130
|
+
expect(patched).toContain('data-sk-field')
|
|
131
|
+
// List items should be driven by .map() with indexed data-sk-field (template literal form)
|
|
132
|
+
expect(patched).toMatch(/data-sk-field=\{`[^`]*\.\$\{_i\}`\}|data-sk-field="[^"]*\.\d+"/)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should use skData array for list items', () => {
|
|
136
|
+
// Expect something like: (skData?.items ?? [...]).map(
|
|
137
|
+
expect(patched).toMatch(/skData\?\.items.*map|\.map.*skData\?\./)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should inject getSection in page mode', () => {
|
|
141
|
+
expect(patched).toContain("import { getSection } from 'setzkasten:content'")
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// 3b. Partially adopted page with static list: patcher fixes the remaining items
|
|
147
|
+
// This covers the real-world case of admin.astro / license.astro after a
|
|
148
|
+
// first adoption pass that bound headings+descriptions but missed <ul>/<li>.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
describe('PagePartiallyAdoptedWithList — patcher fixes remaining static list', () => {
|
|
151
|
+
let patched: string
|
|
152
|
+
|
|
153
|
+
beforeAll(async () => {
|
|
154
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PagePartiallyAdoptedWithList.astro'), 'utf-8')
|
|
155
|
+
const section = await analyzeAstroSection(
|
|
156
|
+
source,
|
|
157
|
+
'_page_docs_partial',
|
|
158
|
+
'partialPage',
|
|
159
|
+
'src/pages/docs/partial.astro',
|
|
160
|
+
{ mode: 'page' },
|
|
161
|
+
)
|
|
162
|
+
const groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
163
|
+
patched = await patchTemplateForFields(
|
|
164
|
+
source,
|
|
165
|
+
'_page_docs_partial',
|
|
166
|
+
section.fields,
|
|
167
|
+
groups,
|
|
168
|
+
{ mode: 'page' },
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should detect the static list as an array field', async () => {
|
|
173
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PagePartiallyAdoptedWithList.astro'), 'utf-8')
|
|
174
|
+
const section = await analyzeAstroSection(
|
|
175
|
+
source, '_page_docs_partial', 'partialPage', 'src/pages/docs/partial.astro', { mode: 'page' },
|
|
176
|
+
)
|
|
177
|
+
const arrayFields = section.fields.filter(f => f.type === 'array')
|
|
178
|
+
expect(arrayFields.length, 'Should detect at least one array field').toBeGreaterThanOrEqual(1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should have no hardcoded <li> text after patching', () => {
|
|
182
|
+
const hardcoded = findHardcodedListItems(patched)
|
|
183
|
+
expect(
|
|
184
|
+
hardcoded,
|
|
185
|
+
`Hardcoded list items remain after patching partially-adopted page:\n${hardcoded.map(m => ` "${m[1]}"`).join('\n')}`,
|
|
186
|
+
).toHaveLength(0)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should use .map() for the list after patching', () => {
|
|
190
|
+
expect(patched).toMatch(/\.map\(\s*\(item/)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should keep existing data-sk-field bindings intact', () => {
|
|
194
|
+
// Existing CMS bindings (headings, descriptions) must not be broken
|
|
195
|
+
expect(patched).toContain('data-sk-field="_page_docs_partial.heading"')
|
|
196
|
+
expect(patched).toContain('data-sk-field="_page_docs_partial.description"')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// 3. Regression: PageWithListItemsAdopted is already clean
|
|
202
|
+
// The "good" fixture should have zero hardcoded <li> items.
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
describe('PageWithListItemsAdopted — already clean (regression baseline)', () => {
|
|
205
|
+
it('should have no hardcoded <li> text', () => {
|
|
206
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithListItemsAdopted.astro'), 'utf-8')
|
|
207
|
+
const hardcoded = findHardcodedListItems(source)
|
|
208
|
+
expect(
|
|
209
|
+
hardcoded,
|
|
210
|
+
`Adopted fixture should have no hardcoded list items, found:\n${hardcoded.map(m => ` "${m[1]}"`).join('\n')}`,
|
|
211
|
+
).toHaveLength(0)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should have .map() driven lists', () => {
|
|
215
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithListItemsAdopted.astro'), 'utf-8')
|
|
216
|
+
// Each <ul> should be driven by a .map() over a CMS array
|
|
217
|
+
const mapCount = (source.match(/\.map\(\s*\(item/g) ?? []).length
|
|
218
|
+
expect(mapCount).toBeGreaterThanOrEqual(3) // three lists
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should have data-sk-field on every list item', () => {
|
|
222
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithListItemsAdopted.astro'), 'utf-8')
|
|
223
|
+
expect(source).toMatch(/data-sk-field=\{`_page_docs_admin\.items/)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// 4. PageWithFormattedList — <li> items with <strong> inside spans
|
|
229
|
+
// The analyzer must detect formatting, store innerHTML as default values,
|
|
230
|
+
// and set formatting: true on the array item type.
|
|
231
|
+
// The patcher must replace the content span with set:html={item}.
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
describe('PageWithFormattedList — analyzer detects formatting in list items', () => {
|
|
234
|
+
let source: string
|
|
235
|
+
let section: Awaited<ReturnType<typeof analyzeAstroSection>>
|
|
236
|
+
|
|
237
|
+
beforeAll(async () => {
|
|
238
|
+
source = readFileSync(join(SECTIONS_DIR, 'PageWithFormattedList.astro'), 'utf-8')
|
|
239
|
+
section = await analyzeAstroSection(
|
|
240
|
+
source,
|
|
241
|
+
'_page_formatted',
|
|
242
|
+
'formattedPage',
|
|
243
|
+
'src/pages/formatted.astro',
|
|
244
|
+
{ mode: 'page' },
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should detect the list as an array field', () => {
|
|
249
|
+
const arrayFields = section.fields.filter(f => f.type === 'array')
|
|
250
|
+
expect(arrayFields.length).toBeGreaterThanOrEqual(1)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should set formatting: true on the array item type', () => {
|
|
254
|
+
const arrayField = section.fields.find(f => f.type === 'array')
|
|
255
|
+
expect(arrayField).toBeDefined()
|
|
256
|
+
expect((arrayField?.options as any)?.arrayItem?.formatting).toBe(true)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should store innerHTML (with <strong>) as default values', () => {
|
|
260
|
+
const arrayField = section.fields.find(f => f.type === 'array')
|
|
261
|
+
const items = arrayField?.defaultValue as string[] | undefined
|
|
262
|
+
expect(Array.isArray(items)).toBe(true)
|
|
263
|
+
const hasStrong = (items ?? []).some(v => typeof v === 'string' && v.includes('<strong'))
|
|
264
|
+
expect(hasStrong, 'Default values should contain <strong> HTML').toBe(true)
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('PageWithFormattedList — patcher replaces content span with set:html', () => {
|
|
269
|
+
let patched: string
|
|
270
|
+
|
|
271
|
+
beforeAll(async () => {
|
|
272
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithFormattedList.astro'), 'utf-8')
|
|
273
|
+
const section = await analyzeAstroSection(
|
|
274
|
+
source,
|
|
275
|
+
'_page_formatted',
|
|
276
|
+
'formattedPage',
|
|
277
|
+
'src/pages/formatted.astro',
|
|
278
|
+
{ mode: 'page' },
|
|
279
|
+
)
|
|
280
|
+
const groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
281
|
+
patched = await patchTemplateForFields(
|
|
282
|
+
source,
|
|
283
|
+
'_page_formatted',
|
|
284
|
+
section.fields,
|
|
285
|
+
groups,
|
|
286
|
+
{ mode: 'page' },
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should inject set:html={item} on the content span', () => {
|
|
291
|
+
expect(patched).toContain('set:html={item}')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should not contain hardcoded <strong> text in the .map() template', () => {
|
|
295
|
+
// After patching, the <li> template inside .map() must not have hardcoded <strong>.
|
|
296
|
+
// The fallback array (between ?? [...]) may contain HTML — that's fine (it's data).
|
|
297
|
+
// Strip JS template literals and {expressions} before checking.
|
|
298
|
+
const withoutTemplateLiterals = patched.replace(/`[^`]*`/g, '""')
|
|
299
|
+
const withoutExpressions = withoutTemplateLiterals.replace(/\{[^}]*\}/g, '{}')
|
|
300
|
+
const strongInMarkup = withoutExpressions.match(/<strong[^>]*>[^<{]+<\/strong>/g) ?? []
|
|
301
|
+
expect(strongInMarkup, `Hardcoded <strong> in template: ${strongInMarkup.join(', ')}`).toHaveLength(0)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('should add data-sk-field to list items', () => {
|
|
305
|
+
expect(patched).toMatch(/data-sk-field=\{`_page_formatted\.\w+\.\$\{_i\}`\}/)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should use .map() for the list', () => {
|
|
309
|
+
expect(patched).toMatch(/\.map\(\s*\(item/)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// 6. PageWithCalloutDiv — <div> with mixed content (text + inline <a>)
|
|
315
|
+
// The analyzer must NOT skip the callout div just because it contains <a>.
|
|
316
|
+
// Mixed-text divs (direct text nodes + inline links) must be detected.
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
describe('PageWithCalloutDiv — callout div with inline link detected as description field', () => {
|
|
319
|
+
let section: Awaited<ReturnType<typeof analyzeAstroSection>>
|
|
320
|
+
|
|
321
|
+
beforeAll(async () => {
|
|
322
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithCalloutDiv.astro'), 'utf-8')
|
|
323
|
+
section = await analyzeAstroSection(
|
|
324
|
+
source,
|
|
325
|
+
'_page_callout_div',
|
|
326
|
+
'calloutDivPage',
|
|
327
|
+
'src/pages/callout-div.astro',
|
|
328
|
+
{ mode: 'page' },
|
|
329
|
+
)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('detects the callout div as a description field (not silently dropped)', () => {
|
|
333
|
+
// The fixture has 3 already-bound description fields (description, description2, description3).
|
|
334
|
+
// The callout div must get the next available key: description4.
|
|
335
|
+
// Bug: descCount started at 0, so key was 'description' → collision → skipped.
|
|
336
|
+
// Fix: descCount is initialized from the count of already-used description* keys.
|
|
337
|
+
const calloutField = section.fields.find(f => f.key === 'description4')
|
|
338
|
+
expect(calloutField, 'description4 field must be detected from callout div (not dropped due to key collision)').toBeDefined()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('sets formatting: true on the callout field (contains inline <a>)', () => {
|
|
342
|
+
const calloutField = section.fields.find(f => f.key === 'description4')
|
|
343
|
+
expect((calloutField?.options as any)?.formatting).toBe(true)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('extracts the text content as defaultValue', () => {
|
|
347
|
+
const calloutField = section.fields.find(f => f.key === 'description4')
|
|
348
|
+
expect(typeof calloutField?.defaultValue).toBe('string')
|
|
349
|
+
expect((calloutField?.defaultValue as string)).toMatch(/kommerzielle Lizenzen/)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('still detects heading from data-sk-field (Section 0)', () => {
|
|
353
|
+
const headingField = section.fields.find(f => f.key === 'heading')
|
|
354
|
+
expect(headingField, 'heading must be detected via Section 0').toBeDefined()
|
|
355
|
+
expect(headingField?.defaultValue).toBe('Lizenz')
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Note: regression coverage for real docs pages is intentionally omitted here.
|
|
360
|
+
// Real website pages can be in various adoption states (pre/post-adoption) and
|
|
361
|
+
// should not be coupled to unit tests. The PageWithListItemsAdopted fixture
|
|
362
|
+
// above serves as the stable regression baseline for the patcher pipeline.
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// 5. PageWithAdoptedArrays — already-adopted page with ?? [...] fallback arrays
|
|
366
|
+
// The analyzer must detect array fields (not text) from .map() expressions
|
|
367
|
+
// that carry ?? ['item1', 'item2'] fallbacks (script-patched pages).
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
describe('PageWithAdoptedArrays — analyzer detects arrays from ?? [...] fallbacks', () => {
|
|
370
|
+
let section: Awaited<ReturnType<typeof analyzeAstroSection>>
|
|
371
|
+
|
|
372
|
+
beforeAll(async () => {
|
|
373
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithAdoptedArrays.astro'), 'utf-8')
|
|
374
|
+
section = await analyzeAstroSection(
|
|
375
|
+
source,
|
|
376
|
+
'_page_adopted_arrays',
|
|
377
|
+
'adoptedArraysPage',
|
|
378
|
+
'src/pages/adopted-arrays.astro',
|
|
379
|
+
{ mode: 'page' },
|
|
380
|
+
)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('detects items as type array (not text)', () => {
|
|
384
|
+
const itemsField = section.fields.find(f => f.key === 'items')
|
|
385
|
+
expect(itemsField, 'items field must exist').toBeDefined()
|
|
386
|
+
expect(itemsField?.type).toBe('array')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('extracts fallback items as defaultValue', () => {
|
|
390
|
+
const itemsField = section.fields.find(f => f.key === 'items')
|
|
391
|
+
expect(Array.isArray(itemsField?.defaultValue)).toBe(true)
|
|
392
|
+
expect(itemsField?.defaultValue).toEqual(['Eintrag A', 'Eintrag B', 'Eintrag C'])
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('sets no formatting on plain-text array items', () => {
|
|
396
|
+
const itemsField = section.fields.find(f => f.key === 'items')
|
|
397
|
+
expect((itemsField?.options as any)?.arrayItem?.formatting).toBeFalsy()
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('detects items2 as type array', () => {
|
|
401
|
+
const items2Field = section.fields.find(f => f.key === 'items2')
|
|
402
|
+
expect(items2Field?.type).toBe('array')
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('sets formatting: true on items2 because items contain HTML', () => {
|
|
406
|
+
const items2Field = section.fields.find(f => f.key === 'items2')
|
|
407
|
+
expect((items2Field?.options as any)?.arrayItem?.formatting).toBe(true)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('extracts HTML items as defaultValue for items2', () => {
|
|
411
|
+
const items2Field = section.fields.find(f => f.key === 'items2')
|
|
412
|
+
const items = items2Field?.defaultValue as string[] | undefined
|
|
413
|
+
expect(Array.isArray(items)).toBe(true)
|
|
414
|
+
expect(items?.some(v => v.includes('<strong'))).toBe(true)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('detects heading with fallback value', () => {
|
|
418
|
+
const headingField = section.fields.find(f => f.key === 'heading')
|
|
419
|
+
expect(headingField?.type).toBe('text')
|
|
420
|
+
expect(headingField?.defaultValue).toBe('Lizenz')
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// 7. PageWithHtmlFallback — Section 0 walkers must set formatting: true
|
|
426
|
+
// when fallback values contain HTML tags (<code>, <strong>).
|
|
427
|
+
//
|
|
428
|
+
// Bug: attribute walker and expression walker both added fields without
|
|
429
|
+
// formatting:true even when the ?? fallback contained HTML. This caused:
|
|
430
|
+
// a) generated schema to have f.text({ label: '...' }) without formatting:true
|
|
431
|
+
// b) MiniRTE not active → raw HTML shown in plain text input
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
describe('PageWithHtmlFallback — Section 0 sets formatting:true for HTML fallbacks', () => {
|
|
434
|
+
let section: Awaited<ReturnType<typeof analyzeAstroSection>>
|
|
435
|
+
|
|
436
|
+
beforeAll(async () => {
|
|
437
|
+
const source = readFileSync(join(SECTIONS_DIR, 'PageWithHtmlFallback.astro'), 'utf-8')
|
|
438
|
+
section = await analyzeAstroSection(
|
|
439
|
+
source,
|
|
440
|
+
'_page_html_fallback',
|
|
441
|
+
'htmlFallbackPage',
|
|
442
|
+
'src/pages/html-fallback.astro',
|
|
443
|
+
{ mode: 'page' },
|
|
444
|
+
)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('detects heading without formatting (plain text fallback)', () => {
|
|
448
|
+
const f = section.fields.find(field => field.key === 'heading')
|
|
449
|
+
expect(f, 'heading must be detected').toBeDefined()
|
|
450
|
+
expect((f?.options as any)?.formatting).toBeFalsy()
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('detects description with formatting:true (set:html attr with <code> in fallback)', () => {
|
|
454
|
+
const f = section.fields.find(field => field.key === 'description')
|
|
455
|
+
expect(f, 'description must be detected').toBeDefined()
|
|
456
|
+
expect((f?.options as any)?.formatting).toBe(true)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('description defaultValue contains the HTML with <code>', () => {
|
|
460
|
+
const f = section.fields.find(field => field.key === 'description')
|
|
461
|
+
expect((f?.defaultValue as string)).toMatch(/<code>/)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('detects note with formatting:true (expression fallback with <strong>)', () => {
|
|
465
|
+
const f = section.fields.find(field => field.key === 'note')
|
|
466
|
+
expect(f, 'note must be detected').toBeDefined()
|
|
467
|
+
expect((f?.options as any)?.formatting).toBe(true)
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('note defaultValue contains the HTML with <strong>', () => {
|
|
471
|
+
const f = section.fields.find(field => field.key === 'note')
|
|
472
|
+
expect((f?.defaultValue as string)).toMatch(/<strong>/)
|
|
473
|
+
})
|
|
474
|
+
})
|