@setzkasten-cms/astro-admin 1.4.2 → 1.5.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/dist/api-routes/_auth-guard.d.ts +27 -3
- package/dist/api-routes/_auth-guard.js +5 -2
- package/dist/api-routes/_dev-session-secret.d.ts +8 -0
- package/dist/api-routes/_dev-session-secret.js +8 -0
- package/dist/api-routes/_github-token.js +1 -1
- package/dist/api-routes/_role-resolver.js +6 -3
- package/dist/api-routes/_session-secret.d.ts +19 -0
- package/dist/api-routes/_session-secret.js +7 -0
- package/dist/api-routes/_session-signing.d.ts +45 -0
- package/dist/api-routes/_session-signing.js +8 -0
- package/dist/api-routes/_webhook-dispatcher.js +4 -4
- package/dist/api-routes/asset-proxy.js +1 -1
- package/dist/api-routes/auth-callback.js +12 -5
- package/dist/api-routes/auth-logout.d.ts +4 -4
- package/dist/api-routes/auth-logout.js +8 -2
- package/dist/api-routes/auth-session.d.ts +6 -0
- package/dist/api-routes/auth-session.js +19 -19
- package/dist/api-routes/auth-setzkasten-login.js +14 -7
- package/dist/api-routes/catalog-add.js +59 -17
- package/dist/api-routes/catalog-export.js +14 -4
- package/dist/api-routes/config.d.ts +10 -3
- package/dist/api-routes/config.js +26 -4
- package/dist/api-routes/deploy-hook.js +8 -8
- package/dist/api-routes/editors.d.ts +1 -1
- package/dist/api-routes/editors.js +5 -2
- package/dist/api-routes/github-proxy.js +30 -8
- package/dist/api-routes/global-config.js +6 -3
- package/dist/api-routes/history-rollback.js +31 -14
- package/dist/api-routes/history-version.js +8 -6
- package/dist/api-routes/history.js +5 -2
- package/dist/api-routes/icons-local.js +1 -1
- package/dist/api-routes/init-add-section.js +150 -48
- package/dist/api-routes/init-apply.js +56 -42
- package/dist/api-routes/init-migrate.js +43 -36
- package/dist/api-routes/init-scan-page.d.ts +1 -1
- package/dist/api-routes/init-scan-page.js +59 -13
- package/dist/api-routes/init-scan.js +22 -7
- package/dist/api-routes/migrate-to-multi.js +5 -2
- package/dist/api-routes/pages.js +15 -4
- package/dist/api-routes/section-add.js +68 -16
- package/dist/api-routes/section-commit-pending.js +70 -22
- package/dist/api-routes/section-delete.js +49 -14
- package/dist/api-routes/section-duplicate.js +65 -16
- package/dist/api-routes/section-prepare-copy.js +15 -2
- package/dist/api-routes/section-prepare.js +25 -4
- package/dist/api-routes/setup-github-app-bounce.js +15 -1
- package/dist/api-routes/setup-github-app-branches.js +9 -6
- package/dist/api-routes/setup-github-app-callback.js +24 -1
- package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
- package/dist/api-routes/setup-github-app-credentials.js +43 -0
- package/dist/api-routes/setup-github-app-installed.js +22 -1
- package/dist/api-routes/setup-github-app-repos.js +5 -2
- package/dist/api-routes/setup-github-app.d.ts +4 -0
- package/dist/api-routes/setup-github-app.js +19 -2
- package/dist/api-routes/updater-register.js +7 -1
- package/dist/api-routes/webhooks-status.js +5 -2
- package/dist/api-routes/webhooks-test.js +9 -8
- package/dist/api-routes/webhooks.js +12 -14
- package/dist/api-routes/websites-add.js +5 -2
- package/dist/api-routes/websites-remove.js +5 -2
- package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
- package/dist/{chunk-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
- package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
- package/dist/chunk-KENFINT4.js +76 -0
- package/dist/chunk-ONP6BRZO.js +47 -0
- package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
- package/dist/chunk-QVCW6EF3.js +26 -0
- package/dist/{chunk-K22A4ZBS.js → chunk-UHI6323G.js} +293 -174
- package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
- package/package.json +12 -6
- package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
- package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +91 -97
- package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
- package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
- package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
- package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
- package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
- package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
- package/src/api-routes/__tests__/github-cache.test.ts +1 -1
- package/src/api-routes/__tests__/github-token.test.ts +1 -1
- package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
- package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
- package/src/api-routes/__tests__/history.test.ts +9 -6
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
- package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
- package/src/api-routes/__tests__/pages.test.ts +7 -2
- package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
- package/src/api-routes/__tests__/route-registry.test.ts +11 -18
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
- package/src/api-routes/__tests__/section-management.test.ts +28 -28
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
- package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
- package/src/api-routes/__tests__/updater-register.test.ts +230 -0
- package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
- package/src/api-routes/__tests__/webhooks.test.ts +19 -7
- package/src/api-routes/__tests__/websites-add.test.ts +2 -1
- package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
- package/src/api-routes/_auth-guard.ts +47 -15
- package/src/api-routes/_commit-trailers.ts +3 -2
- package/src/api-routes/_dev-session-secret.ts +79 -0
- package/src/api-routes/_github-token.ts +1 -1
- package/src/api-routes/_pages-meta-store.ts +2 -2
- package/src/api-routes/_role-resolver.ts +7 -5
- package/src/api-routes/_session-secret.ts +46 -0
- package/src/api-routes/_session-signing.ts +135 -0
- package/src/api-routes/_vercel-origin.ts +2 -6
- package/src/api-routes/_webhook-dispatcher.ts +12 -16
- package/src/api-routes/_website-resolver.ts +3 -10
- package/src/api-routes/auth-callback.ts +9 -5
- package/src/api-routes/auth-login.ts +5 -3
- package/src/api-routes/auth-logout.ts +18 -1
- package/src/api-routes/auth-session.ts +13 -21
- package/src/api-routes/auth-setzkasten-login.ts +12 -9
- package/src/api-routes/catalog-add.ts +89 -31
- package/src/api-routes/catalog-export.ts +30 -10
- package/src/api-routes/config.ts +39 -6
- package/src/api-routes/deploy-hook.ts +13 -11
- package/src/api-routes/editors.ts +33 -22
- package/src/api-routes/github-proxy.ts +25 -11
- package/src/api-routes/global-config.ts +103 -18
- package/src/api-routes/history-rollback.ts +41 -14
- package/src/api-routes/history-version.ts +5 -6
- package/src/api-routes/history.ts +3 -3
- package/src/api-routes/icons-local.ts +2 -2
- package/src/api-routes/init-add-section.ts +218 -88
- package/src/api-routes/init-apply.ts +71 -56
- package/src/api-routes/init-migrate.ts +54 -48
- package/src/api-routes/init-scan-page.ts +77 -30
- package/src/api-routes/init-scan.ts +19 -11
- package/src/api-routes/pages.ts +16 -11
- package/src/api-routes/section-add.ts +98 -27
- package/src/api-routes/section-commit-pending.ts +87 -34
- package/src/api-routes/section-delete.ts +76 -27
- package/src/api-routes/section-duplicate.ts +95 -28
- package/src/api-routes/section-management.ts +3 -7
- package/src/api-routes/section-prepare-copy.ts +29 -8
- package/src/api-routes/section-prepare.ts +38 -10
- package/src/api-routes/setup-github-app-bounce.ts +7 -1
- package/src/api-routes/setup-github-app-branches.ts +6 -7
- package/src/api-routes/setup-github-app-callback.ts +18 -1
- package/src/api-routes/setup-github-app-credentials.ts +55 -0
- package/src/api-routes/setup-github-app-installed.ts +12 -1
- package/src/api-routes/setup-github-app-repos.ts +2 -3
- package/src/api-routes/setup-github-app.ts +14 -5
- package/src/api-routes/updater-check.ts +6 -4
- package/src/api-routes/updater-register.ts +34 -20
- package/src/api-routes/updater-transfer.ts +8 -6
- package/src/api-routes/updater-unbind.ts +14 -10
- package/src/api-routes/webhooks-test.ts +9 -11
- package/src/api-routes/webhooks.ts +15 -19
- package/src/init/__tests__/page-level.test.ts +279 -105
- package/src/init/__tests__/page-list-coverage.test.ts +70 -70
- package/src/init/__tests__/patcher-child-component.test.ts +126 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
- package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
- package/src/init/__tests__/section-pipeline.test.ts +102 -16
- package/src/init/astro-config-patcher.ts +4 -18
- package/src/init/astro-detector.ts +2 -7
- package/src/init/astro-section-analyzer-v2.ts +475 -193
- package/src/init/field-label-enricher.ts +6 -6
- package/src/init/template-patcher-v2.ts +490 -56
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* 6. Regression: real docs pages have no hardcoded <li> text after fix
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { describe, it, expect, beforeAll } from 'vitest'
|
|
18
17
|
import { readFileSync } from 'node:fs'
|
|
19
18
|
import { join, resolve } from 'node:path'
|
|
19
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
20
20
|
import { analyzeAstroSection } from '../astro-section-analyzer-v2'
|
|
21
21
|
import { patchTemplateForFields } from '../template-patcher-v2'
|
|
22
22
|
|
|
@@ -30,7 +30,8 @@ const SECTIONS_DIR = resolve(import.meta.dirname!, '..', '..', '..', '..', '..',
|
|
|
30
30
|
function findHardcodedListItems(source: string): RegExpMatchArray[] {
|
|
31
31
|
// Match <span> that has direct text (no set:html, no {expr}), inside a <li>
|
|
32
32
|
// We look for: <span> followed by a letter (not { or <), then text, then </span>
|
|
33
|
-
const pattern =
|
|
33
|
+
const pattern =
|
|
34
|
+
/<li[\s\S]{0,300}?<span(?![^>]*set:html)(?![^>]*data-sk-field)[^>]*>([A-Za-zÄÖÜäöüß][^<{]*)<\/span>/g
|
|
34
35
|
return [...source.matchAll(pattern)]
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -58,15 +59,17 @@ describe('PageWithHardcodedList — analyzer detects list items', () => {
|
|
|
58
59
|
})
|
|
59
60
|
|
|
60
61
|
it('should detect the h2 headings as text fields', () => {
|
|
61
|
-
const keys = section.fields.map(f => f.key)
|
|
62
|
+
const keys = section.fields.map((f) => f.key)
|
|
62
63
|
// The page has "Page Builder", "Section-Editor", "Collections" as <h2>
|
|
63
|
-
const hasHeadings = keys.some(
|
|
64
|
+
const hasHeadings = keys.some(
|
|
65
|
+
(k) => k.includes('heading') || k.includes('pageBuild') || k.includes('section'),
|
|
66
|
+
)
|
|
64
67
|
expect(hasHeadings, 'Should detect h2 headings').toBe(true)
|
|
65
68
|
})
|
|
66
69
|
|
|
67
70
|
it('should detect list item content as array fields', () => {
|
|
68
71
|
// List items should appear either as array fields or individual text fields
|
|
69
|
-
const arrayFields = section.fields.filter(f => f.type === 'array')
|
|
72
|
+
const arrayFields = section.fields.filter((f) => f.type === 'array')
|
|
70
73
|
const allFields = section.fields
|
|
71
74
|
|
|
72
75
|
// Either the list items are grouped as arrays, or they appear as individual text fields
|
|
@@ -76,20 +79,19 @@ describe('PageWithHardcodedList — analyzer detects list items', () => {
|
|
|
76
79
|
})
|
|
77
80
|
|
|
78
81
|
it('should find list item default values', () => {
|
|
79
|
-
const allDefaultValues = section.fields
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
v.includes('adoptieren') ||
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
const allDefaultValues = section.fields.flatMap((f) => {
|
|
83
|
+
if (f.type === 'array' && Array.isArray(f.defaultValue)) return f.defaultValue as string[]
|
|
84
|
+
return typeof f.defaultValue === 'string' ? [f.defaultValue] : []
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const hasListContent = allDefaultValues.some(
|
|
88
|
+
(v) =>
|
|
89
|
+
typeof v === 'string' &&
|
|
90
|
+
(v.includes('adoptieren') || v.includes('Textfelder') || v.includes('Eintraege')),
|
|
91
|
+
)
|
|
92
|
+
expect(hasListContent, 'List item text should appear in detected field default values').toBe(
|
|
93
|
+
true,
|
|
91
94
|
)
|
|
92
|
-
expect(hasListContent, 'List item text should appear in detected field default values').toBe(true)
|
|
93
95
|
})
|
|
94
96
|
})
|
|
95
97
|
|
|
@@ -109,20 +111,16 @@ describe('PageWithHardcodedList — patcher eliminates hardcoded list items', ()
|
|
|
109
111
|
{ mode: 'page' },
|
|
110
112
|
)
|
|
111
113
|
const groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
112
|
-
patched = await patchTemplateForFields(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
section.fields,
|
|
116
|
-
groups,
|
|
117
|
-
{ mode: 'page' },
|
|
118
|
-
)
|
|
114
|
+
patched = await patchTemplateForFields(source, '_page_docs_admin', section.fields, groups, {
|
|
115
|
+
mode: 'page',
|
|
116
|
+
})
|
|
119
117
|
})
|
|
120
118
|
|
|
121
119
|
it('should not contain hardcoded <li> text after patching', () => {
|
|
122
120
|
const hardcoded = findHardcodedListItems(patched)
|
|
123
121
|
expect(
|
|
124
122
|
hardcoded,
|
|
125
|
-
`Hardcoded list items found after patching:\n${hardcoded.map(m => ` "${m[1]}"`).join('\n')}`,
|
|
123
|
+
`Hardcoded list items found after patching:\n${hardcoded.map((m) => ` "${m[1]}"`).join('\n')}`,
|
|
126
124
|
).toHaveLength(0)
|
|
127
125
|
})
|
|
128
126
|
|
|
@@ -160,21 +158,21 @@ describe('PagePartiallyAdoptedWithList — patcher fixes remaining static list',
|
|
|
160
158
|
{ mode: 'page' },
|
|
161
159
|
)
|
|
162
160
|
const groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
163
|
-
patched = await patchTemplateForFields(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
section.fields,
|
|
167
|
-
groups,
|
|
168
|
-
{ mode: 'page' },
|
|
169
|
-
)
|
|
161
|
+
patched = await patchTemplateForFields(source, '_page_docs_partial', section.fields, groups, {
|
|
162
|
+
mode: 'page',
|
|
163
|
+
})
|
|
170
164
|
})
|
|
171
165
|
|
|
172
166
|
it('should detect the static list as an array field', async () => {
|
|
173
167
|
const source = readFileSync(join(SECTIONS_DIR, 'PagePartiallyAdoptedWithList.astro'), 'utf-8')
|
|
174
168
|
const section = await analyzeAstroSection(
|
|
175
|
-
source,
|
|
169
|
+
source,
|
|
170
|
+
'_page_docs_partial',
|
|
171
|
+
'partialPage',
|
|
172
|
+
'src/pages/docs/partial.astro',
|
|
173
|
+
{ mode: 'page' },
|
|
176
174
|
)
|
|
177
|
-
const arrayFields = section.fields.filter(f => f.type === 'array')
|
|
175
|
+
const arrayFields = section.fields.filter((f) => f.type === 'array')
|
|
178
176
|
expect(arrayFields.length, 'Should detect at least one array field').toBeGreaterThanOrEqual(1)
|
|
179
177
|
})
|
|
180
178
|
|
|
@@ -182,7 +180,7 @@ describe('PagePartiallyAdoptedWithList — patcher fixes remaining static list',
|
|
|
182
180
|
const hardcoded = findHardcodedListItems(patched)
|
|
183
181
|
expect(
|
|
184
182
|
hardcoded,
|
|
185
|
-
`Hardcoded list items remain after patching partially-adopted page:\n${hardcoded.map(m => ` "${m[1]}"`).join('\n')}`,
|
|
183
|
+
`Hardcoded list items remain after patching partially-adopted page:\n${hardcoded.map((m) => ` "${m[1]}"`).join('\n')}`,
|
|
186
184
|
).toHaveLength(0)
|
|
187
185
|
})
|
|
188
186
|
|
|
@@ -207,7 +205,7 @@ describe('PageWithListItemsAdopted — already clean (regression baseline)', ()
|
|
|
207
205
|
const hardcoded = findHardcodedListItems(source)
|
|
208
206
|
expect(
|
|
209
207
|
hardcoded,
|
|
210
|
-
`Adopted fixture should have no hardcoded list items, found:\n${hardcoded.map(m => ` "${m[1]}"`).join('\n')}`,
|
|
208
|
+
`Adopted fixture should have no hardcoded list items, found:\n${hardcoded.map((m) => ` "${m[1]}"`).join('\n')}`,
|
|
211
209
|
).toHaveLength(0)
|
|
212
210
|
})
|
|
213
211
|
|
|
@@ -246,21 +244,21 @@ describe('PageWithFormattedList — analyzer detects formatting in list items',
|
|
|
246
244
|
})
|
|
247
245
|
|
|
248
246
|
it('should detect the list as an array field', () => {
|
|
249
|
-
const arrayFields = section.fields.filter(f => f.type === 'array')
|
|
247
|
+
const arrayFields = section.fields.filter((f) => f.type === 'array')
|
|
250
248
|
expect(arrayFields.length).toBeGreaterThanOrEqual(1)
|
|
251
249
|
})
|
|
252
250
|
|
|
253
251
|
it('should set formatting: true on the array item type', () => {
|
|
254
|
-
const arrayField = section.fields.find(f => f.type === 'array')
|
|
252
|
+
const arrayField = section.fields.find((f) => f.type === 'array')
|
|
255
253
|
expect(arrayField).toBeDefined()
|
|
256
254
|
expect((arrayField?.options as any)?.arrayItem?.formatting).toBe(true)
|
|
257
255
|
})
|
|
258
256
|
|
|
259
257
|
it('should store innerHTML (with <strong>) as default values', () => {
|
|
260
|
-
const arrayField = section.fields.find(f => f.type === 'array')
|
|
258
|
+
const arrayField = section.fields.find((f) => f.type === 'array')
|
|
261
259
|
const items = arrayField?.defaultValue as string[] | undefined
|
|
262
260
|
expect(Array.isArray(items)).toBe(true)
|
|
263
|
-
const hasStrong = (items ?? []).some(v => typeof v === 'string' && v.includes('<strong'))
|
|
261
|
+
const hasStrong = (items ?? []).some((v) => typeof v === 'string' && v.includes('<strong'))
|
|
264
262
|
expect(hasStrong, 'Default values should contain <strong> HTML').toBe(true)
|
|
265
263
|
})
|
|
266
264
|
})
|
|
@@ -278,13 +276,9 @@ describe('PageWithFormattedList — patcher replaces content span with set:html'
|
|
|
278
276
|
{ mode: 'page' },
|
|
279
277
|
)
|
|
280
278
|
const groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
281
|
-
patched = await patchTemplateForFields(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
section.fields,
|
|
285
|
-
groups,
|
|
286
|
-
{ mode: 'page' },
|
|
287
|
-
)
|
|
279
|
+
patched = await patchTemplateForFields(source, '_page_formatted', section.fields, groups, {
|
|
280
|
+
mode: 'page',
|
|
281
|
+
})
|
|
288
282
|
})
|
|
289
283
|
|
|
290
284
|
it('should inject set:html={item} on the content span', () => {
|
|
@@ -298,7 +292,10 @@ describe('PageWithFormattedList — patcher replaces content span with set:html'
|
|
|
298
292
|
const withoutTemplateLiterals = patched.replace(/`[^`]*`/g, '""')
|
|
299
293
|
const withoutExpressions = withoutTemplateLiterals.replace(/\{[^}]*\}/g, '{}')
|
|
300
294
|
const strongInMarkup = withoutExpressions.match(/<strong[^>]*>[^<{]+<\/strong>/g) ?? []
|
|
301
|
-
expect(
|
|
295
|
+
expect(
|
|
296
|
+
strongInMarkup,
|
|
297
|
+
`Hardcoded <strong> in template: ${strongInMarkup.join(', ')}`,
|
|
298
|
+
).toHaveLength(0)
|
|
302
299
|
})
|
|
303
300
|
|
|
304
301
|
it('should add data-sk-field to list items', () => {
|
|
@@ -334,23 +331,26 @@ describe('PageWithCalloutDiv — callout div with inline link detected as descri
|
|
|
334
331
|
// The callout div must get the next available key: description4.
|
|
335
332
|
// Bug: descCount started at 0, so key was 'description' → collision → skipped.
|
|
336
333
|
// Fix: descCount is initialized from the count of already-used description* keys.
|
|
337
|
-
const calloutField = section.fields.find(f => f.key === 'description4')
|
|
338
|
-
expect(
|
|
334
|
+
const calloutField = section.fields.find((f) => f.key === 'description4')
|
|
335
|
+
expect(
|
|
336
|
+
calloutField,
|
|
337
|
+
'description4 field must be detected from callout div (not dropped due to key collision)',
|
|
338
|
+
).toBeDefined()
|
|
339
339
|
})
|
|
340
340
|
|
|
341
341
|
it('sets formatting: true on the callout field (contains inline <a>)', () => {
|
|
342
|
-
const calloutField = section.fields.find(f => f.key === 'description4')
|
|
342
|
+
const calloutField = section.fields.find((f) => f.key === 'description4')
|
|
343
343
|
expect((calloutField?.options as any)?.formatting).toBe(true)
|
|
344
344
|
})
|
|
345
345
|
|
|
346
346
|
it('extracts the text content as defaultValue', () => {
|
|
347
|
-
const calloutField = section.fields.find(f => f.key === 'description4')
|
|
347
|
+
const calloutField = section.fields.find((f) => f.key === 'description4')
|
|
348
348
|
expect(typeof calloutField?.defaultValue).toBe('string')
|
|
349
|
-
expect(
|
|
349
|
+
expect(calloutField?.defaultValue as string).toMatch(/kommerzielle Lizenzen/)
|
|
350
350
|
})
|
|
351
351
|
|
|
352
352
|
it('still detects heading from data-sk-field (Section 0)', () => {
|
|
353
|
-
const headingField = section.fields.find(f => f.key === 'heading')
|
|
353
|
+
const headingField = section.fields.find((f) => f.key === 'heading')
|
|
354
354
|
expect(headingField, 'heading must be detected via Section 0').toBeDefined()
|
|
355
355
|
expect(headingField?.defaultValue).toBe('Lizenz')
|
|
356
356
|
})
|
|
@@ -381,41 +381,41 @@ describe('PageWithAdoptedArrays — analyzer detects arrays from ?? [...] fallba
|
|
|
381
381
|
})
|
|
382
382
|
|
|
383
383
|
it('detects items as type array (not text)', () => {
|
|
384
|
-
const itemsField = section.fields.find(f => f.key === 'items')
|
|
384
|
+
const itemsField = section.fields.find((f) => f.key === 'items')
|
|
385
385
|
expect(itemsField, 'items field must exist').toBeDefined()
|
|
386
386
|
expect(itemsField?.type).toBe('array')
|
|
387
387
|
})
|
|
388
388
|
|
|
389
389
|
it('extracts fallback items as defaultValue', () => {
|
|
390
|
-
const itemsField = section.fields.find(f => f.key === 'items')
|
|
390
|
+
const itemsField = section.fields.find((f) => f.key === 'items')
|
|
391
391
|
expect(Array.isArray(itemsField?.defaultValue)).toBe(true)
|
|
392
392
|
expect(itemsField?.defaultValue).toEqual(['Eintrag A', 'Eintrag B', 'Eintrag C'])
|
|
393
393
|
})
|
|
394
394
|
|
|
395
395
|
it('sets no formatting on plain-text array items', () => {
|
|
396
|
-
const itemsField = section.fields.find(f => f.key === 'items')
|
|
396
|
+
const itemsField = section.fields.find((f) => f.key === 'items')
|
|
397
397
|
expect((itemsField?.options as any)?.arrayItem?.formatting).toBeFalsy()
|
|
398
398
|
})
|
|
399
399
|
|
|
400
400
|
it('detects items2 as type array', () => {
|
|
401
|
-
const items2Field = section.fields.find(f => f.key === 'items2')
|
|
401
|
+
const items2Field = section.fields.find((f) => f.key === 'items2')
|
|
402
402
|
expect(items2Field?.type).toBe('array')
|
|
403
403
|
})
|
|
404
404
|
|
|
405
405
|
it('sets formatting: true on items2 because items contain HTML', () => {
|
|
406
|
-
const items2Field = section.fields.find(f => f.key === 'items2')
|
|
406
|
+
const items2Field = section.fields.find((f) => f.key === 'items2')
|
|
407
407
|
expect((items2Field?.options as any)?.arrayItem?.formatting).toBe(true)
|
|
408
408
|
})
|
|
409
409
|
|
|
410
410
|
it('extracts HTML items as defaultValue for items2', () => {
|
|
411
|
-
const items2Field = section.fields.find(f => f.key === 'items2')
|
|
411
|
+
const items2Field = section.fields.find((f) => f.key === 'items2')
|
|
412
412
|
const items = items2Field?.defaultValue as string[] | undefined
|
|
413
413
|
expect(Array.isArray(items)).toBe(true)
|
|
414
|
-
expect(items?.some(v => v.includes('<strong'))).toBe(true)
|
|
414
|
+
expect(items?.some((v) => v.includes('<strong'))).toBe(true)
|
|
415
415
|
})
|
|
416
416
|
|
|
417
417
|
it('detects heading with fallback value', () => {
|
|
418
|
-
const headingField = section.fields.find(f => f.key === 'heading')
|
|
418
|
+
const headingField = section.fields.find((f) => f.key === 'heading')
|
|
419
419
|
expect(headingField?.type).toBe('text')
|
|
420
420
|
expect(headingField?.defaultValue).toBe('Lizenz')
|
|
421
421
|
})
|
|
@@ -445,30 +445,30 @@ describe('PageWithHtmlFallback — Section 0 sets formatting:true for HTML fallb
|
|
|
445
445
|
})
|
|
446
446
|
|
|
447
447
|
it('detects heading without formatting (plain text fallback)', () => {
|
|
448
|
-
const f = section.fields.find(field => field.key === 'heading')
|
|
448
|
+
const f = section.fields.find((field) => field.key === 'heading')
|
|
449
449
|
expect(f, 'heading must be detected').toBeDefined()
|
|
450
450
|
expect((f?.options as any)?.formatting).toBeFalsy()
|
|
451
451
|
})
|
|
452
452
|
|
|
453
453
|
it('detects description with formatting:true (set:html attr with <code> in fallback)', () => {
|
|
454
|
-
const f = section.fields.find(field => field.key === 'description')
|
|
454
|
+
const f = section.fields.find((field) => field.key === 'description')
|
|
455
455
|
expect(f, 'description must be detected').toBeDefined()
|
|
456
456
|
expect((f?.options as any)?.formatting).toBe(true)
|
|
457
457
|
})
|
|
458
458
|
|
|
459
459
|
it('description defaultValue contains the HTML with <code>', () => {
|
|
460
|
-
const f = section.fields.find(field => field.key === 'description')
|
|
461
|
-
expect(
|
|
460
|
+
const f = section.fields.find((field) => field.key === 'description')
|
|
461
|
+
expect(f?.defaultValue as string).toMatch(/<code>/)
|
|
462
462
|
})
|
|
463
463
|
|
|
464
464
|
it('detects note with formatting:true (expression fallback with <strong>)', () => {
|
|
465
|
-
const f = section.fields.find(field => field.key === 'note')
|
|
465
|
+
const f = section.fields.find((field) => field.key === 'note')
|
|
466
466
|
expect(f, 'note must be detected').toBeDefined()
|
|
467
467
|
expect((f?.options as any)?.formatting).toBe(true)
|
|
468
468
|
})
|
|
469
469
|
|
|
470
470
|
it('note defaultValue contains the HTML with <strong>', () => {
|
|
471
|
-
const f = section.fields.find(field => field.key === 'note')
|
|
472
|
-
expect(
|
|
471
|
+
const f = section.fields.find((field) => field.key === 'note')
|
|
472
|
+
expect(f?.defaultValue as string).toMatch(/<strong>/)
|
|
473
473
|
})
|
|
474
474
|
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for patchChildComponentForFieldPrefix:
|
|
3
|
+
* Injects fieldPrefix prop + data-sk-field bindings into child components
|
|
4
|
+
* used by repeated-component sections (e.g. PricingCard).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
import { detectChildImports, patchChildComponentForFieldPrefix } from '../template-patcher-v2'
|
|
11
|
+
|
|
12
|
+
const CHILD_DIR = join(
|
|
13
|
+
import.meta.dirname!,
|
|
14
|
+
'..',
|
|
15
|
+
'..',
|
|
16
|
+
'..',
|
|
17
|
+
'..',
|
|
18
|
+
'..',
|
|
19
|
+
'test-sections',
|
|
20
|
+
'child-components',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
function readFixture(name: string): string {
|
|
24
|
+
return readFileSync(join(CHILD_DIR, name), 'utf-8')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Inner fields for PricingCard (mirrors what the analyzer produces)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const PRICING_INNER_FIELDS = [
|
|
31
|
+
{ key: 'name', type: 'text' },
|
|
32
|
+
{ key: 'price', type: 'text' },
|
|
33
|
+
{ key: 'priceNote', type: 'text' },
|
|
34
|
+
{ key: 'description', type: 'text' },
|
|
35
|
+
{ key: 'features', type: 'array' },
|
|
36
|
+
{ key: 'cta', type: 'text' },
|
|
37
|
+
{ key: 'ctaHref', type: 'text' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
describe('patchChildComponentForFieldPrefix', () => {
|
|
41
|
+
it('adds fieldPrefix to Props interface', () => {
|
|
42
|
+
const source = readFixture('PricingCard.astro')
|
|
43
|
+
const patched = patchChildComponentForFieldPrefix(source, PRICING_INNER_FIELDS)
|
|
44
|
+
expect(patched).toContain('fieldPrefix?: string')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('adds fieldPrefix to destructuring', () => {
|
|
48
|
+
const source = readFixture('PricingCard.astro')
|
|
49
|
+
const patched = patchChildComponentForFieldPrefix(source, PRICING_INNER_FIELDS)
|
|
50
|
+
expect(patched).toMatch(/fieldPrefix\s*[,}]/)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('adds data-sk-field for scalar text props', () => {
|
|
54
|
+
const source = readFixture('PricingCard.astro')
|
|
55
|
+
const patched = patchChildComponentForFieldPrefix(source, PRICING_INNER_FIELDS)
|
|
56
|
+
expect(patched).toContain('fieldPrefix}.name')
|
|
57
|
+
expect(patched).toContain('fieldPrefix}.price')
|
|
58
|
+
expect(patched).toContain('fieldPrefix}.description')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('adds data-sk-field for href prop on <a>', () => {
|
|
62
|
+
const source = readFixture('PricingCard.astro')
|
|
63
|
+
const patched = patchChildComponentForFieldPrefix(source, PRICING_INNER_FIELDS)
|
|
64
|
+
expect(patched).toContain('fieldPrefix}.ctaHref')
|
|
65
|
+
expect(patched).toContain('fieldPrefix}.cta')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('adds indexed data-sk-field for array prop features', () => {
|
|
69
|
+
const source = readFixture('PricingCard.astro')
|
|
70
|
+
const patched = patchChildComponentForFieldPrefix(source, PRICING_INNER_FIELDS)
|
|
71
|
+
expect(patched).toMatch(/fieldPrefix\}\.features\._fi|fieldPrefix\}\.features\.\$\{_fi\}/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('is idempotent — second pass produces no changes', () => {
|
|
75
|
+
const source = readFixture('PricingCard.astro')
|
|
76
|
+
const patched = patchChildComponentForFieldPrefix(source, PRICING_INNER_FIELDS)
|
|
77
|
+
const patchedAgain = patchChildComponentForFieldPrefix(patched, PRICING_INNER_FIELDS)
|
|
78
|
+
expect(patchedAgain).toBe(patched)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('does not modify source when fieldPrefix already present', () => {
|
|
82
|
+
const source = readFixture('PricingCard.astro')
|
|
83
|
+
const prePatched = source.replace(
|
|
84
|
+
'highlight?: boolean;',
|
|
85
|
+
'highlight?: boolean;\n fieldPrefix?: string;',
|
|
86
|
+
)
|
|
87
|
+
const result = patchChildComponentForFieldPrefix(prePatched, PRICING_INNER_FIELDS)
|
|
88
|
+
expect(result).toContain('fieldPrefix?: string')
|
|
89
|
+
expect((result.match(/fieldPrefix\?\s*:\s*string/g) ?? []).length).toBe(1)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('detectChildImports', () => {
|
|
94
|
+
it('detects component imports from fields with sourceComponent', () => {
|
|
95
|
+
const sectionSource = `---
|
|
96
|
+
import PricingCard from '../components/PricingCard.astro'
|
|
97
|
+
interface Props { data: Record<string, any> | null }
|
|
98
|
+
const { data: skData } = Astro.props
|
|
99
|
+
---
|
|
100
|
+
<section></section>`
|
|
101
|
+
|
|
102
|
+
const fields = [
|
|
103
|
+
{
|
|
104
|
+
key: 'pricingcards',
|
|
105
|
+
type: 'array',
|
|
106
|
+
defaultValue: [],
|
|
107
|
+
options: {
|
|
108
|
+
sourceComponent: 'PricingCard',
|
|
109
|
+
arrayItem: { type: 'object', fields: PRICING_INNER_FIELDS },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const imports = detectChildImports(sectionSource, fields as any)
|
|
115
|
+
expect(imports).toHaveLength(1)
|
|
116
|
+
expect(imports[0]!.compName).toBe('PricingCard')
|
|
117
|
+
expect(imports[0]!.importPath).toBe('../components/PricingCard.astro')
|
|
118
|
+
expect(imports[0]!.innerFields).toEqual(PRICING_INNER_FIELDS)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('returns empty array when no sourceComponent fields', () => {
|
|
122
|
+
const source = `---\nconst x = 1\n---\n<div></div>`
|
|
123
|
+
const fields = [{ key: 'heading', type: 'text' }]
|
|
124
|
+
expect(detectChildImports(source, fields as any)).toHaveLength(0)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -24,8 +24,12 @@
|
|
|
24
24
|
* still inject the getSection() call without throwing.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { describe,
|
|
28
|
-
import {
|
|
27
|
+
import { describe, expect, it } from 'vitest'
|
|
28
|
+
import {
|
|
29
|
+
convertToSetHtml,
|
|
30
|
+
patchTemplateForFields,
|
|
31
|
+
stripTemplateFallbacks,
|
|
32
|
+
} from '../../init/template-patcher-v2'
|
|
29
33
|
import type { PatchField } from '../../init/template-patcher-v2'
|
|
30
34
|
|
|
31
35
|
// ---------------------------------------------------------------------------
|
|
@@ -89,22 +93,30 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
89
93
|
]
|
|
90
94
|
|
|
91
95
|
it('should produce valid output containing skData references', async () => {
|
|
92
|
-
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
96
|
+
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
97
|
+
mode: 'page',
|
|
98
|
+
})
|
|
93
99
|
expect(patched).toContain('skData')
|
|
94
100
|
})
|
|
95
101
|
|
|
96
102
|
it('should bind the heading field containing an umlaut', async () => {
|
|
97
|
-
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
103
|
+
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
104
|
+
mode: 'page',
|
|
105
|
+
})
|
|
98
106
|
expect(patched).toContain('skData?.heading')
|
|
99
107
|
})
|
|
100
108
|
|
|
101
109
|
it('should bind the body field containing umlauts', async () => {
|
|
102
|
-
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
110
|
+
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
111
|
+
mode: 'page',
|
|
112
|
+
})
|
|
103
113
|
expect(patched).toContain('skData?.body')
|
|
104
114
|
})
|
|
105
115
|
|
|
106
116
|
it('should not corrupt the surrounding markup', async () => {
|
|
107
|
-
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
117
|
+
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
118
|
+
mode: 'page',
|
|
119
|
+
})
|
|
108
120
|
// The <section> tag must still be intact
|
|
109
121
|
expect(patched).toMatch(/<section[^>]*>/)
|
|
110
122
|
// Closing tags must still be present
|
|
@@ -113,7 +125,9 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
113
125
|
})
|
|
114
126
|
|
|
115
127
|
it('should add data-sk-field to the heading element', async () => {
|
|
116
|
-
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
128
|
+
const patched = await patchTemplateForFields(source, '_page_about', fields, [], {
|
|
129
|
+
mode: 'page',
|
|
130
|
+
})
|
|
117
131
|
expect(patched).toMatch(/<h1[^>]*data-sk-field/)
|
|
118
132
|
})
|
|
119
133
|
})
|
|
@@ -151,25 +165,33 @@ const skData = getSection('_page_about')
|
|
|
151
165
|
]
|
|
152
166
|
|
|
153
167
|
it('should not duplicate the getSection import', async () => {
|
|
154
|
-
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
168
|
+
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
169
|
+
mode: 'page',
|
|
170
|
+
})
|
|
155
171
|
const importCount = (patched.match(/import \{ getSection \}/g) ?? []).length
|
|
156
172
|
expect(importCount).toBe(1)
|
|
157
173
|
})
|
|
158
174
|
|
|
159
175
|
it('should not duplicate the skData declaration', async () => {
|
|
160
|
-
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
176
|
+
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
177
|
+
mode: 'page',
|
|
178
|
+
})
|
|
161
179
|
const declCount = (patched.match(/const skData\s*=/g) ?? []).length
|
|
162
180
|
expect(declCount).toBe(1)
|
|
163
181
|
})
|
|
164
182
|
|
|
165
183
|
it('should preserve existing data-sk-field attributes', async () => {
|
|
166
|
-
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
184
|
+
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
185
|
+
mode: 'page',
|
|
186
|
+
})
|
|
167
187
|
expect(patched).toContain('data-sk-field="_page_about.heading"')
|
|
168
188
|
expect(patched).toContain('data-sk-field="_page_about.body"')
|
|
169
189
|
})
|
|
170
190
|
|
|
171
191
|
it('should not add a second section id', async () => {
|
|
172
|
-
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
192
|
+
const patched = await patchTemplateForFields(alreadyPatched, '_page_about', fields, [], {
|
|
193
|
+
mode: 'page',
|
|
194
|
+
})
|
|
173
195
|
const idCount = (patched.match(/id="section-_page_about"/g) ?? []).length
|
|
174
196
|
expect(idCount).toBe(1)
|
|
175
197
|
})
|
|
@@ -198,13 +220,11 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
198
220
|
// Convert all LF to CRLF
|
|
199
221
|
const crlf = lf.replace(/\n/g, '\r\n')
|
|
200
222
|
|
|
201
|
-
const fields: PatchField[] = [
|
|
202
|
-
{ key: 'heading', type: 'text', defaultValue: 'Hello World' },
|
|
203
|
-
]
|
|
223
|
+
const fields: PatchField[] = [{ key: 'heading', type: 'text', defaultValue: 'Hello World' }]
|
|
204
224
|
|
|
205
225
|
it('should not throw on CRLF input', async () => {
|
|
206
226
|
await expect(
|
|
207
|
-
patchTemplateForFields(crlf, '_page_test', fields, [], { mode: 'page' })
|
|
227
|
+
patchTemplateForFields(crlf, '_page_test', fields, [], { mode: 'page' }),
|
|
208
228
|
).resolves.toBeDefined()
|
|
209
229
|
})
|
|
210
230
|
|
|
@@ -240,13 +260,11 @@ import BaseLayout from '../layouts/BaseLayout.astro';---
|
|
|
240
260
|
</BaseLayout>
|
|
241
261
|
`
|
|
242
262
|
|
|
243
|
-
const fields: PatchField[] = [
|
|
244
|
-
{ key: 'heading', type: 'text', defaultValue: 'Tight Frontmatter' },
|
|
245
|
-
]
|
|
263
|
+
const fields: PatchField[] = [{ key: 'heading', type: 'text', defaultValue: 'Tight Frontmatter' }]
|
|
246
264
|
|
|
247
265
|
it('should not throw', async () => {
|
|
248
266
|
await expect(
|
|
249
|
-
patchTemplateForFields(source, '_page_test', fields, [], { mode: 'page' })
|
|
267
|
+
patchTemplateForFields(source, '_page_test', fields, [], { mode: 'page' }),
|
|
250
268
|
).resolves.toBeDefined()
|
|
251
269
|
})
|
|
252
270
|
|
|
@@ -285,7 +303,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
285
303
|
|
|
286
304
|
it('should not throw', async () => {
|
|
287
305
|
await expect(
|
|
288
|
-
patchTemplateForFields(source, '_page_test', fields, [], { mode: 'page' })
|
|
306
|
+
patchTemplateForFields(source, '_page_test', fields, [], { mode: 'page' }),
|
|
289
307
|
).resolves.toBeDefined()
|
|
290
308
|
})
|
|
291
309
|
|
|
@@ -342,7 +360,7 @@ describe('stripTemplateFallbacks', () => {
|
|
|
342
360
|
})
|
|
343
361
|
|
|
344
362
|
it('strips array fallback with backtick HTML items', () => {
|
|
345
|
-
const source =
|
|
363
|
+
const source = '{(skData?.items ?? [`<strong>Label</strong> — Text.`]).map(('
|
|
346
364
|
const { source: result, fallbacks } = stripTemplateFallbacks(source)
|
|
347
365
|
expect(result).toBe(`{(skData?.items ?? []).map((`)
|
|
348
366
|
expect((fallbacks['items'] as string[])?.[0]).toContain('<strong>')
|
|
@@ -424,7 +442,12 @@ describe('HTML fallback wins over plain-text default in sectionData merge', () =
|
|
|
424
442
|
const existing = result[key]
|
|
425
443
|
const existingIsPlain = typeof existing === 'string' && !/<[a-z]/.test(existing)
|
|
426
444
|
const newIsHtml = typeof value === 'string' && /<[a-z]/.test(value as string)
|
|
427
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
!(key in result) ||
|
|
447
|
+
existing === '' ||
|
|
448
|
+
existing === null ||
|
|
449
|
+
(existingIsPlain && newIsHtml)
|
|
450
|
+
) {
|
|
428
451
|
result[key] = value
|
|
429
452
|
}
|
|
430
453
|
}
|
|
@@ -436,7 +459,8 @@ describe('HTML fallback wins over plain-text default in sectionData merge', () =
|
|
|
436
459
|
callout: 'Für kommerzielle Lizenzen: hello@example.com — text.',
|
|
437
460
|
}
|
|
438
461
|
const fallbacks = {
|
|
439
|
-
callout:
|
|
462
|
+
callout:
|
|
463
|
+
'Für kommerzielle Lizenzen: <a href="mailto:hello@example.com">hello@example.com</a> — text.',
|
|
440
464
|
}
|
|
441
465
|
const result = mergeFallbacks(sectionData, fallbacks)
|
|
442
466
|
expect(result['callout']).toMatch(/href="mailto:/)
|