meno-core 1.0.53 → 1.1.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/.claude/settings.local.json +1 -3
- package/bin/cli.ts +66 -261
- package/build-astro.ts +150 -128
- package/dist/bin/cli.js +42 -185
- package/dist/bin/cli.js.map +2 -2
- package/dist/chunks/chunk-2AR55GYH.js +42 -0
- package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
- package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
- package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
- package/dist/chunks/chunk-4ZRU52J2.js +169 -0
- package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
- package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
- package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
- package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
- package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
- package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
- package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
- package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
- package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
- package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
- package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
- package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
- package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
- package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
- package/dist/lib/client/index.js +1003 -475
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14393 -6371
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +342 -101
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +64 -80
- package/dist/lib/test-utils/index.js.map +2 -2
- package/entries/client-router.tsx +10 -8
- package/lib/client/ClientInitializer.ts +8 -8
- package/lib/client/ErrorBoundary.test.tsx +246 -186
- package/lib/client/ErrorBoundary.tsx +183 -132
- package/lib/client/componentRegistry.test.ts +96 -108
- package/lib/client/componentRegistry.ts +1 -3
- package/lib/client/contexts/ThemeContext.tsx +3 -2
- package/lib/client/core/ComponentBuilder.test.ts +663 -560
- package/lib/client/core/ComponentBuilder.ts +427 -155
- package/lib/client/core/ComponentRenderer.test.tsx +2 -4
- package/lib/client/core/ComponentRenderer.tsx +46 -33
- package/lib/client/core/builders/embedBuilder.ts +242 -54
- package/lib/client/core/builders/linkBuilder.ts +72 -45
- package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
- package/lib/client/core/builders/listBuilder.ts +155 -89
- package/lib/client/core/builders/localeListBuilder.ts +97 -62
- package/lib/client/core/builders/types.ts +5 -5
- package/lib/client/core/cmsTemplateProcessor.ts +8 -9
- package/lib/client/elementRegistry.ts +3 -3
- package/lib/client/fontFamiliesService.test.ts +2 -10
- package/lib/client/fontFamiliesService.ts +3 -3
- package/lib/client/hmr/HMRManager.tsx +47 -30
- package/lib/client/hmrCssReload.ts +49 -14
- package/lib/client/hmrWebSocket.ts +14 -19
- package/lib/client/hooks/useColorVariables.test.ts +21 -21
- package/lib/client/hooks/useColorVariables.ts +23 -18
- package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
- package/lib/client/hooks/useVariables.ts +6 -4
- package/lib/client/hydration/HydrationUtils.test.ts +24 -25
- package/lib/client/hydration/HydrationUtils.ts +3 -4
- package/lib/client/i18nConfigService.test.ts +2 -7
- package/lib/client/i18nConfigService.ts +2 -2
- package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
- package/lib/client/meno-filter/MenoFilter.ts +14 -15
- package/lib/client/meno-filter/bindings.ts +18 -43
- package/lib/client/meno-filter/init.ts +4 -4
- package/lib/client/meno-filter/renderer.ts +30 -32
- package/lib/client/meno-filter/script.generated.ts +1 -3
- package/lib/client/meno-filter/ui.ts +4 -6
- package/lib/client/meno-filter/updates.ts +18 -23
- package/lib/client/meno-filter/utils.ts +1 -1
- package/lib/client/navigation.test.ts +174 -178
- package/lib/client/navigation.ts +0 -1
- package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
- package/lib/client/responsiveStyleResolver.test.ts +234 -232
- package/lib/client/responsiveStyleResolver.ts +74 -20
- package/lib/client/routing/RouteLoader.test.ts +24 -25
- package/lib/client/routing/RouteLoader.ts +27 -35
- package/lib/client/routing/Router.tsx +210 -153
- package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
- package/lib/client/scripts/ScriptExecutor.ts +18 -36
- package/lib/client/services/PrefetchService.test.ts +3 -3
- package/lib/client/services/PrefetchService.ts +23 -29
- package/lib/client/styleProcessor.test.ts +9 -9
- package/lib/client/styleProcessor.ts +24 -15
- package/lib/client/styles/StyleInjector.test.ts +122 -115
- package/lib/client/styles/StyleInjector.ts +10 -7
- package/lib/client/styles/UtilityClassCollector.ts +62 -28
- package/lib/client/styles/cspNonce.test.ts +2 -5
- package/lib/client/templateEngine.test.ts +554 -488
- package/lib/client/templateEngine.ts +203 -73
- package/lib/client/theme.test.ts +1 -1
- package/lib/client/theme.ts +0 -1
- package/lib/client/utils/toast.ts +0 -1
- package/lib/server/__integration__/api-routes.test.ts +8 -4
- package/lib/server/__integration__/cms-integration.test.ts +1 -4
- package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
- package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
- package/lib/server/__integration__/static-assets.test.ts +3 -3
- package/lib/server/__integration__/test-helpers.ts +87 -74
- package/lib/server/ab/generateFunctions.ts +12 -10
- package/lib/server/astro/cmsPageEmitter.ts +26 -42
- package/lib/server/astro/componentEmitter.ts +35 -43
- package/lib/server/astro/cssCollector.ts +10 -26
- package/lib/server/astro/nodeToAstro.test.ts +1754 -34
- package/lib/server/astro/nodeToAstro.ts +232 -216
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
- package/lib/server/astro/pageEmitter.ts +9 -13
- package/lib/server/astro/tailwindMapper.test.ts +10 -37
- package/lib/server/astro/tailwindMapper.ts +33 -40
- package/lib/server/astro/templateTransformer.ts +17 -20
- package/lib/server/createServer.ts +7 -19
- package/lib/server/cssGenerator.test.ts +35 -44
- package/lib/server/cssGenerator.ts +8 -19
- package/lib/server/fileWatcher.test.ts +164 -13
- package/lib/server/fileWatcher.ts +209 -124
- package/lib/server/index.ts +14 -17
- package/lib/server/jsonLoader.test.ts +39 -2
- package/lib/server/jsonLoader.ts +39 -34
- package/lib/server/middleware/cors.test.ts +21 -21
- package/lib/server/middleware/cors.ts +28 -4
- package/lib/server/middleware/errorHandler.test.ts +7 -5
- package/lib/server/middleware/errorHandler.ts +3 -8
- package/lib/server/middleware/index.ts +0 -1
- package/lib/server/middleware/logger.test.ts +10 -8
- package/lib/server/middleware/logger.ts +17 -29
- package/lib/server/migrateTemplates.ts +3 -3
- package/lib/server/pageCache.test.ts +76 -77
- package/lib/server/pageCache.ts +0 -1
- package/lib/server/projectContext.ts +5 -4
- package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
- package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
- package/lib/server/routes/api/cms.test.ts +26 -14
- package/lib/server/routes/api/cms.ts +9 -14
- package/lib/server/routes/api/colors.test.ts +2 -2
- package/lib/server/routes/api/components.ts +31 -35
- package/lib/server/routes/api/config.ts +3 -4
- package/lib/server/routes/api/core-routes.ts +49 -76
- package/lib/server/routes/api/enums.test.ts +2 -2
- package/lib/server/routes/api/functions.ts +9 -17
- package/lib/server/routes/api/index.ts +3 -6
- package/lib/server/routes/api/pages.ts +8 -11
- package/lib/server/routes/api/shared.test.ts +1 -1
- package/lib/server/routes/api/shared.ts +12 -9
- package/lib/server/routes/api/variables.test.ts +3 -5
- package/lib/server/routes/api/variables.ts +1 -1
- package/lib/server/routes/index.ts +25 -28
- package/lib/server/routes/pages.ts +35 -31
- package/lib/server/routes/static.ts +17 -6
- package/lib/server/runtime/bundler.ts +98 -61
- package/lib/server/runtime/fs.ts +16 -26
- package/lib/server/runtime/httpServer.ts +8 -12
- package/lib/server/services/ColorService.test.ts +3 -3
- package/lib/server/services/ColorService.ts +37 -29
- package/lib/server/services/EnumService.test.ts +4 -8
- package/lib/server/services/EnumService.ts +11 -3
- package/lib/server/services/VariableService.test.ts +8 -12
- package/lib/server/services/VariableService.ts +11 -3
- package/lib/server/services/cmsService.test.ts +158 -120
- package/lib/server/services/cmsService.ts +36 -146
- package/lib/server/services/componentService.test.ts +125 -45
- package/lib/server/services/componentService.ts +70 -46
- package/lib/server/services/configService.test.ts +22 -45
- package/lib/server/services/configService.ts +50 -27
- package/lib/server/services/fileWatcherService.ts +42 -8
- package/lib/server/services/index.ts +0 -1
- package/lib/server/services/pageService.test.ts +8 -10
- package/lib/server/services/pageService.ts +32 -18
- package/lib/server/ssr/attributeBuilder.ts +18 -8
- package/lib/server/ssr/buildErrorOverlay.ts +11 -12
- package/lib/server/ssr/clientDataInjector.ts +7 -21
- package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
- package/lib/server/ssr/cssCollector.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +1 -1
- package/lib/server/ssr/errorOverlay.ts +3 -9
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
- package/lib/server/ssr/htmlGenerator.test.ts +120 -44
- package/lib/server/ssr/htmlGenerator.ts +126 -91
- package/lib/server/ssr/imageMetadata.test.ts +3 -1
- package/lib/server/ssr/imageMetadata.ts +27 -21
- package/lib/server/ssr/jsCollector.test.ts +8 -18
- package/lib/server/ssr/jsCollector.ts +3 -8
- package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
- package/lib/server/ssr/metaTagGenerator.ts +2 -2
- package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
- package/lib/server/ssr/ssrRenderer.test.ts +316 -256
- package/lib/server/ssr/ssrRenderer.ts +795 -243
- package/lib/server/ssrRenderer.test.ts +1048 -954
- package/lib/server/utils/jsonLineMapper.test.ts +28 -28
- package/lib/server/utils/jsonLineMapper.ts +7 -7
- package/lib/server/validateStyleCoverage.ts +18 -21
- package/lib/server/websocketManager.ts +28 -19
- package/lib/shared/attributeNodeUtils.test.ts +15 -15
- package/lib/shared/attributeNodeUtils.ts +5 -12
- package/lib/shared/breakpoints.test.ts +5 -5
- package/lib/shared/breakpoints.ts +4 -11
- package/lib/shared/cmsQuery.test.ts +81 -0
- package/lib/shared/cmsQuery.ts +92 -0
- package/lib/shared/cmsQueryParser.test.ts +50 -42
- package/lib/shared/cmsQueryParser.ts +60 -38
- package/lib/shared/colorConversions.ts +9 -9
- package/lib/shared/colorVariableUtils.test.ts +27 -5
- package/lib/shared/colorVariableUtils.ts +21 -12
- package/lib/shared/componentRefs.ts +1 -5
- package/lib/shared/constants.test.ts +3 -3
- package/lib/shared/constants.ts +8 -8
- package/lib/shared/cssGeneration.test.ts +285 -144
- package/lib/shared/cssGeneration.ts +217 -531
- package/lib/shared/cssNamedColors.ts +152 -30
- package/lib/shared/cssProperties.test.ts +69 -70
- package/lib/shared/cssProperties.ts +298 -114
- package/lib/shared/elementClassName.test.ts +109 -109
- package/lib/shared/elementClassName.ts +3 -3
- package/lib/shared/elementUtils.ts +12 -16
- package/lib/shared/errorLogger.test.ts +12 -12
- package/lib/shared/errorLogger.ts +2 -10
- package/lib/shared/errors.test.ts +2 -13
- package/lib/shared/errors.ts +2 -8
- package/lib/shared/expressionEvaluator.test.ts +145 -0
- package/lib/shared/expressionEvaluator.ts +52 -24
- package/lib/shared/fontLoader.test.ts +32 -18
- package/lib/shared/fontLoader.ts +2 -2
- package/lib/shared/friendlyError.ts +2 -3
- package/lib/shared/gradientUtils.test.ts +3 -7
- package/lib/shared/gradientUtils.ts +17 -14
- package/lib/shared/hrefRefs.test.ts +2 -2
- package/lib/shared/hrefRefs.ts +2 -10
- package/lib/shared/i18n.test.ts +4 -4
- package/lib/shared/i18n.ts +18 -38
- package/lib/shared/index.ts +27 -0
- package/lib/shared/inlineSvgStyleRules.ts +14 -13
- package/lib/shared/interactiveStyleMappings.test.ts +32 -54
- package/lib/shared/interactiveStyleMappings.ts +11 -24
- package/lib/shared/interactiveStyles.test.ts +165 -188
- package/lib/shared/interfaces/contentProvider.ts +1 -1
- package/lib/shared/itemTemplateUtils.test.ts +11 -17
- package/lib/shared/itemTemplateUtils.ts +27 -39
- package/lib/shared/jsonRepair.ts +16 -10
- package/lib/shared/libraryLoader.test.ts +21 -55
- package/lib/shared/libraryLoader.ts +7 -22
- package/lib/shared/linkUtils.ts +1 -1
- package/lib/shared/logger.test.ts +66 -0
- package/lib/shared/logger.ts +94 -0
- package/lib/shared/markdown.ts +25 -0
- package/lib/shared/netlifyLocale404.test.ts +179 -0
- package/lib/shared/netlifyLocale404.ts +110 -0
- package/lib/shared/nodeUtils.test.ts +28 -16
- package/lib/shared/nodeUtils.ts +76 -23
- package/lib/shared/pathArrayUtils.test.ts +1 -2
- package/lib/shared/pathArrayUtils.ts +1 -1
- package/lib/shared/pathSecurity.ts +2 -2
- package/lib/shared/pathUtils.test.ts +4 -6
- package/lib/shared/pathUtils.ts +42 -49
- package/lib/shared/paths/Path.test.ts +2 -2
- package/lib/shared/paths/Path.ts +0 -1
- package/lib/shared/paths/PathConverter.test.ts +1 -1
- package/lib/shared/paths/PathConverter.ts +17 -20
- package/lib/shared/paths/PathUtils.ts +13 -13
- package/lib/shared/paths/PathValidator.test.ts +2 -15
- package/lib/shared/paths/PathValidator.ts +12 -10
- package/lib/shared/paths/index.ts +1 -2
- package/lib/shared/permissions.test.ts +168 -0
- package/lib/shared/permissions.ts +162 -0
- package/lib/shared/propResolver.test.ts +240 -244
- package/lib/shared/propResolver.ts +15 -26
- package/lib/shared/pxToRem.test.ts +7 -6
- package/lib/shared/pxToRem.ts +2 -5
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
- package/lib/shared/registry/ClientRegistry.ts +0 -2
- package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
- package/lib/shared/registry/ComponentRegistry.ts +12 -9
- package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
- package/lib/shared/registry/RegistryManager.ts +1 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -2
- package/lib/shared/registry/createNodeType.ts +7 -9
- package/lib/shared/registry/defineNodeType.ts +2 -6
- package/lib/shared/registry/fieldPresets.ts +3 -1
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
- package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
- package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
- package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
- package/lib/shared/registry/nodeTypes/index.ts +26 -3
- package/lib/shared/responsiveScaling.test.ts +16 -34
- package/lib/shared/responsiveScaling.ts +61 -42
- package/lib/shared/responsiveStyleUtils.test.ts +0 -1
- package/lib/shared/responsiveStyleUtils.ts +11 -13
- package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
- package/lib/shared/richtext/htmlToTiptap.ts +2 -4
- package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
- package/lib/shared/richtext/types.ts +1 -8
- package/lib/shared/slugTranslator.test.ts +13 -13
- package/lib/shared/slugTranslator.ts +12 -16
- package/lib/shared/slugify.ts +9 -15
- package/lib/shared/styleNodeUtils.test.ts +8 -8
- package/lib/shared/styleNodeUtils.ts +18 -20
- package/lib/shared/styleUtils.test.ts +87 -61
- package/lib/shared/styleUtils.ts +5 -6
- package/lib/shared/themeDefaults.test.ts +11 -14
- package/lib/shared/themeDefaults.ts +3 -4
- package/lib/shared/tree/PathBuilder.test.ts +63 -145
- package/lib/shared/tree/PathBuilder.ts +59 -47
- package/lib/shared/treePathUtils.test.ts +51 -12
- package/lib/shared/treePathUtils.ts +91 -60
- package/lib/shared/types/api.ts +105 -8
- package/lib/shared/types/cms.test.ts +34 -0
- package/lib/shared/types/cms.ts +90 -34
- package/lib/shared/types/comment.ts +49 -16
- package/lib/shared/types/components.ts +51 -25
- package/lib/shared/types/errors.test.ts +1 -6
- package/lib/shared/types/errors.ts +3 -7
- package/lib/shared/types/experiments.ts +28 -28
- package/lib/shared/types/index.ts +25 -2
- package/lib/shared/types/permissions.ts +139 -0
- package/lib/shared/types/styles.ts +0 -1
- package/lib/shared/types/variables.test.ts +4 -13
- package/lib/shared/types/variables.ts +51 -30
- package/lib/shared/types.ts +1 -2
- package/lib/shared/utilityClassConfig.ts +751 -321
- package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
- package/lib/shared/utilityClassMapper.test.ts +433 -95
- package/lib/shared/utilityClassMapper.ts +322 -249
- package/lib/shared/utilityClassNames.test.ts +38 -0
- package/lib/shared/utilityClassNames.ts +458 -0
- package/lib/shared/utils.test.ts +2 -10
- package/lib/shared/utils.ts +19 -10
- package/lib/shared/validation/cmsValidators.ts +2 -1
- package/lib/shared/validation/commentValidators.test.ts +53 -0
- package/lib/shared/validation/commentValidators.ts +12 -1
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/validation/permissionsValidators.test.ts +49 -0
- package/lib/shared/validation/permissionsValidators.ts +49 -0
- package/lib/shared/validation/propValidator.test.ts +19 -21
- package/lib/shared/validation/propValidator.ts +13 -18
- package/lib/shared/validation/schemas.test.ts +64 -32
- package/lib/shared/validation/schemas.ts +616 -345
- package/lib/shared/validation/validators.test.ts +3 -8
- package/lib/shared/validation/validators.ts +89 -68
- package/lib/shared/viewportUnits.integration.test.ts +14 -10
- package/lib/shared/viewportUnits.test.ts +24 -23
- package/lib/shared/viewportUnits.ts +12 -5
- package/lib/test-utils/dom-setup.ts +1 -1
- package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
- package/lib/test-utils/factories/DomMockFactory.ts +17 -23
- package/lib/test-utils/factories/EventMockFactory.ts +7 -13
- package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
- package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
- package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
- package/lib/test-utils/fixtures.ts +45 -45
- package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
- package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
- package/lib/test-utils/helpers.ts +1 -5
- package/lib/test-utils/index.ts +0 -4
- package/lib/test-utils/mockFactories.ts +13 -19
- package/lib/test-utils/mocks.ts +6 -4
- package/package.json +3 -5
- package/scripts/build-for-publish.mjs +2 -4
- package/scripts/build-meno-filter.ts +4 -7
- package/tsconfig.json +1 -0
- package/vite.config.ts +4 -4
- package/build-next.ts +0 -1361
- package/build-static.test.ts +0 -424
- package/build-static.ts +0 -1074
- package/dist/build-static.js +0 -36
- package/dist/chunks/chunk-2MHDV5BF.js +0 -251
- package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
- package/dist/chunks/chunk-AZQYF6KE.js +0 -559
- package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
- package/dist/chunks/chunk-GZHGVVW3.js +0 -322
- package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
- package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
- package/dist/chunks/chunk-I7YIGZXT.js +0 -467
- package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
- package/dist/chunks/chunk-IGYR22T6.js +0 -6192
- package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
- package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
- package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
- package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
- package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
- package/dist/chunks/chunk-QB2LNO4W.js +0 -77
- package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
- package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
- package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
- package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
- package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
- package/dist/chunks/chunk-X754AHS5.js.map +0 -7
- package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
- package/dist/chunks/configService-R3OGU2UD.js +0 -13
- package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
- package/dist/chunks/constants-STK2YBIW.js +0 -46
- package/dist/chunks/constants-STK2YBIW.js.map +0 -7
- package/dist/chunks/fs-JGINUXGL.js.map +0 -7
- package/dist/entries/server-router.js +0 -67
- package/dist/entries/server-router.js.map +0 -7
- package/entries/server-router.tsx +0 -81
- package/lib/server/providers/fileSystemPageProvider.ts +0 -160
- package/lib/server/webflow/buildWebflow.ts +0 -635
- package/lib/server/webflow/index.ts +0 -23
- package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
- package/lib/server/webflow/nodeToWebflow.ts +0 -2435
- package/lib/server/webflow/styleMapper.test.ts +0 -389
- package/lib/server/webflow/styleMapper.ts +0 -695
- package/lib/server/webflow/templateWrapper.ts +0 -49
- package/lib/server/webflow/types.ts +0 -396
- /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
|
@@ -1,141 +1,136 @@
|
|
|
1
|
-
import { test, expect, describe,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
extractPageMeta,
|
|
6
|
-
generateMetaTags
|
|
7
|
-
} from "./ssrRenderer";
|
|
8
|
-
import type { ComponentNode, ComponentDefinition, JSONPage, CMSListNode, CMSItem } from "../shared/types";
|
|
9
|
-
import { CMSService } from "./services/cmsService";
|
|
1
|
+
import { test, expect, describe, spyOn } from 'bun:test';
|
|
2
|
+
import { generateSSRHTML, renderPageSSR, extractPageMeta, generateMetaTags } from './ssrRenderer';
|
|
3
|
+
import type { ComponentNode, ComponentDefinition, JSONPage, CMSItem } from '../shared/types';
|
|
4
|
+
import { CMSService } from './services/cmsService';
|
|
10
5
|
|
|
11
6
|
// Mock jsonLoader
|
|
12
|
-
const
|
|
7
|
+
const _mockLoadBreakpointConfig = async () => ({
|
|
13
8
|
tablet: 1024,
|
|
14
|
-
mobile: 540
|
|
9
|
+
mobile: 540,
|
|
15
10
|
});
|
|
16
11
|
|
|
17
12
|
// Mock the jsonLoader module
|
|
18
|
-
const originalJsonLoader = await import(
|
|
13
|
+
const originalJsonLoader = await import('./jsonLoader');
|
|
19
14
|
if (originalJsonLoader) {
|
|
20
15
|
// We'll need to test with actual imports, but can provide mocks where needed
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
describe(
|
|
24
|
-
test(
|
|
18
|
+
describe('SSR Renderer - extractPageMeta', () => {
|
|
19
|
+
test('should extract basic meta information', () => {
|
|
25
20
|
const pageData: JSONPage = {
|
|
26
21
|
meta: {
|
|
27
|
-
title:
|
|
28
|
-
description:
|
|
29
|
-
keywords:
|
|
30
|
-
}
|
|
22
|
+
title: 'Test Page',
|
|
23
|
+
description: 'Test description',
|
|
24
|
+
keywords: 'test, page',
|
|
25
|
+
},
|
|
31
26
|
};
|
|
32
|
-
|
|
27
|
+
|
|
33
28
|
const meta = extractPageMeta(pageData);
|
|
34
|
-
|
|
35
|
-
expect(meta.title).toBe(
|
|
36
|
-
expect(meta.description).toBe(
|
|
37
|
-
expect(meta.keywords).toBe(
|
|
29
|
+
|
|
30
|
+
expect(meta.title).toBe('Test Page');
|
|
31
|
+
expect(meta.description).toBe('Test description');
|
|
32
|
+
expect(meta.keywords).toBe('test, page');
|
|
38
33
|
});
|
|
39
34
|
|
|
40
|
-
test(
|
|
35
|
+
test('should extract Open Graph meta', () => {
|
|
41
36
|
const pageData: JSONPage = {
|
|
42
37
|
meta: {
|
|
43
|
-
title:
|
|
44
|
-
ogTitle:
|
|
45
|
-
ogDescription:
|
|
46
|
-
ogImage:
|
|
47
|
-
ogType:
|
|
48
|
-
}
|
|
38
|
+
title: 'Test Page',
|
|
39
|
+
ogTitle: 'OG Title',
|
|
40
|
+
ogDescription: 'OG Description',
|
|
41
|
+
ogImage: 'https://example.com/image.jpg',
|
|
42
|
+
ogType: 'article',
|
|
43
|
+
},
|
|
49
44
|
};
|
|
50
|
-
|
|
45
|
+
|
|
51
46
|
const meta = extractPageMeta(pageData);
|
|
52
|
-
|
|
53
|
-
expect(meta.ogTitle).toBe(
|
|
54
|
-
expect(meta.ogDescription).toBe(
|
|
55
|
-
expect(meta.ogImage).toBe(
|
|
56
|
-
expect(meta.ogType).toBe(
|
|
47
|
+
|
|
48
|
+
expect(meta.ogTitle).toBe('OG Title');
|
|
49
|
+
expect(meta.ogDescription).toBe('OG Description');
|
|
50
|
+
expect(meta.ogImage).toBe('https://example.com/image.jpg');
|
|
51
|
+
expect(meta.ogType).toBe('article');
|
|
57
52
|
});
|
|
58
53
|
|
|
59
|
-
test(
|
|
54
|
+
test('should use title as fallback for ogTitle', () => {
|
|
60
55
|
const pageData: JSONPage = {
|
|
61
56
|
meta: {
|
|
62
|
-
title:
|
|
63
|
-
}
|
|
57
|
+
title: 'Test Page',
|
|
58
|
+
},
|
|
64
59
|
};
|
|
65
|
-
|
|
60
|
+
|
|
66
61
|
const meta = extractPageMeta(pageData);
|
|
67
|
-
|
|
68
|
-
expect(meta.ogTitle).toBe(
|
|
62
|
+
|
|
63
|
+
expect(meta.ogTitle).toBe('Test Page');
|
|
69
64
|
});
|
|
70
65
|
|
|
71
|
-
test(
|
|
66
|
+
test('should use description as fallback for ogDescription', () => {
|
|
72
67
|
const pageData: JSONPage = {
|
|
73
68
|
meta: {
|
|
74
|
-
description:
|
|
75
|
-
}
|
|
69
|
+
description: 'Test description',
|
|
70
|
+
},
|
|
76
71
|
};
|
|
77
|
-
|
|
72
|
+
|
|
78
73
|
const meta = extractPageMeta(pageData);
|
|
79
|
-
|
|
80
|
-
expect(meta.ogDescription).toBe(
|
|
74
|
+
|
|
75
|
+
expect(meta.ogDescription).toBe('Test description');
|
|
81
76
|
});
|
|
82
77
|
|
|
83
78
|
test("should default ogType to 'website'", () => {
|
|
84
79
|
const pageData: JSONPage = {
|
|
85
|
-
meta: {}
|
|
80
|
+
meta: {},
|
|
86
81
|
};
|
|
87
|
-
|
|
82
|
+
|
|
88
83
|
const meta = extractPageMeta(pageData);
|
|
89
|
-
|
|
90
|
-
expect(meta.ogType).toBe(
|
|
84
|
+
|
|
85
|
+
expect(meta.ogType).toBe('website');
|
|
91
86
|
});
|
|
92
87
|
|
|
93
|
-
test(
|
|
88
|
+
test('should handle missing meta object', () => {
|
|
94
89
|
const pageData: JSONPage = {};
|
|
95
|
-
|
|
90
|
+
|
|
96
91
|
const meta = extractPageMeta(pageData);
|
|
97
|
-
|
|
92
|
+
|
|
98
93
|
expect(Object.keys(meta).length).toBe(0);
|
|
99
94
|
});
|
|
100
95
|
});
|
|
101
96
|
|
|
102
|
-
describe(
|
|
103
|
-
test(
|
|
97
|
+
describe('SSR Renderer - generateMetaTags', () => {
|
|
98
|
+
test('should generate title tag', () => {
|
|
104
99
|
const meta = {
|
|
105
|
-
title:
|
|
100
|
+
title: 'Test Page',
|
|
106
101
|
};
|
|
107
|
-
|
|
102
|
+
|
|
108
103
|
const tags = generateMetaTags(meta);
|
|
109
|
-
|
|
110
|
-
expect(tags).toContain(
|
|
104
|
+
|
|
105
|
+
expect(tags).toContain('<title>Test Page</title>');
|
|
111
106
|
});
|
|
112
107
|
|
|
113
|
-
test(
|
|
108
|
+
test('should generate description meta tag', () => {
|
|
114
109
|
const meta = {
|
|
115
|
-
description:
|
|
110
|
+
description: 'Test description',
|
|
116
111
|
};
|
|
117
|
-
|
|
112
|
+
|
|
118
113
|
const tags = generateMetaTags(meta);
|
|
119
|
-
|
|
114
|
+
|
|
120
115
|
expect(tags).toContain(`<meta name="description" content="Test description" />`);
|
|
121
116
|
});
|
|
122
117
|
|
|
123
|
-
test(
|
|
118
|
+
test('should generate keywords meta tag', () => {
|
|
124
119
|
const meta = {
|
|
125
|
-
keywords:
|
|
120
|
+
keywords: 'test, keywords',
|
|
126
121
|
};
|
|
127
|
-
|
|
122
|
+
|
|
128
123
|
const tags = generateMetaTags(meta);
|
|
129
|
-
|
|
124
|
+
|
|
130
125
|
expect(tags).toContain(`<meta name="keywords" content="test, keywords" />`);
|
|
131
126
|
});
|
|
132
127
|
|
|
133
|
-
test(
|
|
128
|
+
test('should generate Open Graph tags', () => {
|
|
134
129
|
const meta = {
|
|
135
|
-
ogTitle:
|
|
136
|
-
ogDescription:
|
|
137
|
-
ogImage:
|
|
138
|
-
ogType:
|
|
130
|
+
ogTitle: 'OG Title',
|
|
131
|
+
ogDescription: 'OG Description',
|
|
132
|
+
ogImage: 'https://example.com/image.jpg',
|
|
133
|
+
ogType: 'article',
|
|
139
134
|
};
|
|
140
135
|
|
|
141
136
|
const tags = generateMetaTags(meta);
|
|
@@ -146,11 +141,11 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
146
141
|
expect(tags).toContain(`<meta property="og:type" content="article" />`);
|
|
147
142
|
});
|
|
148
143
|
|
|
149
|
-
test(
|
|
144
|
+
test('should emit twitter:card=summary_large_image when ogImage present', () => {
|
|
150
145
|
const meta = {
|
|
151
|
-
ogTitle:
|
|
152
|
-
ogDescription:
|
|
153
|
-
ogImage:
|
|
146
|
+
ogTitle: 'OG Title',
|
|
147
|
+
ogDescription: 'OG Description',
|
|
148
|
+
ogImage: 'https://example.com/image.jpg',
|
|
154
149
|
};
|
|
155
150
|
|
|
156
151
|
const tags = generateMetaTags(meta);
|
|
@@ -160,10 +155,10 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
160
155
|
expect(tags).toContain(`<meta name="twitter:description" content="OG Description" />`);
|
|
161
156
|
});
|
|
162
157
|
|
|
163
|
-
test(
|
|
158
|
+
test('should emit twitter:card=summary when no ogImage', () => {
|
|
164
159
|
const meta = {
|
|
165
|
-
title:
|
|
166
|
-
description:
|
|
160
|
+
title: 'Plain Title',
|
|
161
|
+
description: 'Plain Description',
|
|
167
162
|
};
|
|
168
163
|
|
|
169
164
|
const tags = generateMetaTags(meta);
|
|
@@ -171,10 +166,10 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
171
166
|
expect(tags).toContain(`<meta name="twitter:card" content="summary" />`);
|
|
172
167
|
});
|
|
173
168
|
|
|
174
|
-
test(
|
|
169
|
+
test('twitter:title falls back to title when ogTitle is absent', () => {
|
|
175
170
|
const meta = {
|
|
176
|
-
title:
|
|
177
|
-
ogImage:
|
|
171
|
+
title: 'Plain Title',
|
|
172
|
+
ogImage: 'https://example.com/image.jpg',
|
|
178
173
|
};
|
|
179
174
|
|
|
180
175
|
const tags = generateMetaTags(meta);
|
|
@@ -182,10 +177,10 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
182
177
|
expect(tags).toContain(`<meta name="twitter:title" content="Plain Title" />`);
|
|
183
178
|
});
|
|
184
179
|
|
|
185
|
-
test(
|
|
180
|
+
test('twitter:description falls back to description when ogDescription is absent', () => {
|
|
186
181
|
const meta = {
|
|
187
|
-
description:
|
|
188
|
-
ogImage:
|
|
182
|
+
description: 'Plain Description',
|
|
183
|
+
ogImage: 'https://example.com/image.jpg',
|
|
189
184
|
};
|
|
190
185
|
|
|
191
186
|
const tags = generateMetaTags(meta);
|
|
@@ -193,8 +188,8 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
193
188
|
expect(tags).toContain(`<meta name="twitter:description" content="Plain Description" />`);
|
|
194
189
|
});
|
|
195
190
|
|
|
196
|
-
test(
|
|
197
|
-
const meta = { title:
|
|
191
|
+
test('should emit twitter:site and twitter:creator with @-normalized handle', () => {
|
|
192
|
+
const meta = { title: 'Page' };
|
|
198
193
|
|
|
199
194
|
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
200
195
|
social: { twitterHandle: 'meno' },
|
|
@@ -204,8 +199,8 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
204
199
|
expect(tags).toContain(`<meta name="twitter:creator" content="@meno" />`);
|
|
205
200
|
});
|
|
206
201
|
|
|
207
|
-
test(
|
|
208
|
-
const meta = { title:
|
|
202
|
+
test('should not double-prefix handle that already starts with @', () => {
|
|
203
|
+
const meta = { title: 'Page' };
|
|
209
204
|
|
|
210
205
|
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
211
206
|
social: { twitterHandle: '@meno' },
|
|
@@ -215,15 +210,15 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
215
210
|
expect(tags).not.toContain(`@@meno`);
|
|
216
211
|
});
|
|
217
212
|
|
|
218
|
-
test(
|
|
213
|
+
test('should emit no twitter tags when meta is empty', () => {
|
|
219
214
|
const tags = generateMetaTags({});
|
|
220
215
|
|
|
221
216
|
expect(tags).not.toContain('twitter:');
|
|
222
217
|
});
|
|
223
218
|
|
|
224
|
-
test(
|
|
219
|
+
test('should escape HTML in twitter content', () => {
|
|
225
220
|
const meta = {
|
|
226
|
-
ogTitle:
|
|
221
|
+
ogTitle: 'Title & <stuff>',
|
|
227
222
|
};
|
|
228
223
|
|
|
229
224
|
const tags = generateMetaTags(meta);
|
|
@@ -231,24 +226,24 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
231
226
|
expect(tags).toContain(`<meta name="twitter:title" content="Title & <stuff>" />`);
|
|
232
227
|
});
|
|
233
228
|
|
|
234
|
-
test(
|
|
235
|
-
const meta = { ogImage:
|
|
229
|
+
test('og:image swaps /images/*.webp → .jpg', () => {
|
|
230
|
+
const meta = { ogImage: '/images/photo.webp' };
|
|
236
231
|
|
|
237
232
|
const tags = generateMetaTags(meta);
|
|
238
233
|
|
|
239
234
|
expect(tags).toContain(`<meta property="og:image" content="/images/photo.jpg" />`);
|
|
240
235
|
});
|
|
241
236
|
|
|
242
|
-
test(
|
|
243
|
-
const meta = { ogImage:
|
|
237
|
+
test('og:image swaps /images/*.avif → .jpg', () => {
|
|
238
|
+
const meta = { ogImage: '/images/photo.avif' };
|
|
244
239
|
|
|
245
240
|
const tags = generateMetaTags(meta);
|
|
246
241
|
|
|
247
242
|
expect(tags).toContain(`<meta property="og:image" content="/images/photo.jpg" />`);
|
|
248
243
|
});
|
|
249
244
|
|
|
250
|
-
test(
|
|
251
|
-
const meta = { ogImage:
|
|
245
|
+
test('og:image is made absolute when options.baseUrl is provided', () => {
|
|
246
|
+
const meta = { ogImage: '/images/photo.webp' };
|
|
252
247
|
|
|
253
248
|
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
254
249
|
baseUrl: 'https://example.com',
|
|
@@ -257,8 +252,8 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
257
252
|
expect(tags).toContain(`<meta property="og:image" content="https://example.com/images/photo.jpg" />`);
|
|
258
253
|
});
|
|
259
254
|
|
|
260
|
-
test(
|
|
261
|
-
const meta = { ogImage:
|
|
255
|
+
test('og:image strips trailing slash from baseUrl', () => {
|
|
256
|
+
const meta = { ogImage: '/images/photo.webp' };
|
|
262
257
|
|
|
263
258
|
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
264
259
|
baseUrl: 'https://example.com/',
|
|
@@ -267,32 +262,32 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
267
262
|
expect(tags).toContain(`<meta property="og:image" content="https://example.com/images/photo.jpg" />`);
|
|
268
263
|
});
|
|
269
264
|
|
|
270
|
-
test(
|
|
271
|
-
const meta = { ogImage:
|
|
265
|
+
test('og:image preserves .png unchanged (other than baseUrl absolutization)', () => {
|
|
266
|
+
const meta = { ogImage: '/images/photo.png' };
|
|
272
267
|
|
|
273
268
|
const tags = generateMetaTags(meta);
|
|
274
269
|
|
|
275
270
|
expect(tags).toContain(`<meta property="og:image" content="/images/photo.png" />`);
|
|
276
271
|
});
|
|
277
272
|
|
|
278
|
-
test(
|
|
279
|
-
const meta = { ogImage:
|
|
273
|
+
test('og:image preserves .jpg unchanged', () => {
|
|
274
|
+
const meta = { ogImage: '/images/photo.jpg' };
|
|
280
275
|
|
|
281
276
|
const tags = generateMetaTags(meta);
|
|
282
277
|
|
|
283
278
|
expect(tags).toContain(`<meta property="og:image" content="/images/photo.jpg" />`);
|
|
284
279
|
});
|
|
285
280
|
|
|
286
|
-
test(
|
|
287
|
-
const meta = { ogImage:
|
|
281
|
+
test('og:image preserves external absolute URL unchanged', () => {
|
|
282
|
+
const meta = { ogImage: 'https://cdn.example.com/photo.webp' };
|
|
288
283
|
|
|
289
284
|
const tags = generateMetaTags(meta);
|
|
290
285
|
|
|
291
286
|
expect(tags).toContain(`<meta property="og:image" content="https://cdn.example.com/photo.webp" />`);
|
|
292
287
|
});
|
|
293
288
|
|
|
294
|
-
test(
|
|
295
|
-
const meta = { ogImage:
|
|
289
|
+
test('og:image does not rewrite non-/images/ local paths but still absolutizes them', () => {
|
|
290
|
+
const meta = { ogImage: '/cdn-cgi/photo.webp' };
|
|
296
291
|
|
|
297
292
|
const tags = generateMetaTags(meta, '', 'en', undefined, {
|
|
298
293
|
baseUrl: 'https://example.com',
|
|
@@ -301,38 +296,38 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
301
296
|
expect(tags).toContain(`<meta property="og:image" content="https://example.com/cdn-cgi/photo.webp" />`);
|
|
302
297
|
});
|
|
303
298
|
|
|
304
|
-
test(
|
|
299
|
+
test('should generate canonical URL', () => {
|
|
305
300
|
const meta = {};
|
|
306
|
-
const url =
|
|
307
|
-
|
|
301
|
+
const url = 'https://example.com/page';
|
|
302
|
+
|
|
308
303
|
const tags = generateMetaTags(meta, url);
|
|
309
|
-
|
|
304
|
+
|
|
310
305
|
expect(tags).toContain(`<link rel="canonical" href="https://example.com/page" />`);
|
|
311
306
|
expect(tags).toContain(`<meta property="og:url" content="https://example.com/page" />`);
|
|
312
307
|
});
|
|
313
308
|
|
|
314
|
-
test(
|
|
309
|
+
test('should escape HTML in meta content', () => {
|
|
315
310
|
const meta = {
|
|
316
|
-
title:
|
|
317
|
-
description:
|
|
311
|
+
title: 'Test & Page',
|
|
312
|
+
description: 'Test <description>',
|
|
318
313
|
};
|
|
319
|
-
|
|
314
|
+
|
|
320
315
|
const tags = generateMetaTags(meta);
|
|
321
|
-
|
|
322
|
-
expect(tags).toContain(
|
|
316
|
+
|
|
317
|
+
expect(tags).toContain('<title>Test & Page</title>');
|
|
323
318
|
expect(tags).toContain(`content="Test <description>"`);
|
|
324
319
|
});
|
|
325
320
|
|
|
326
|
-
test(
|
|
321
|
+
test('should handle empty meta', () => {
|
|
327
322
|
const meta = {};
|
|
328
323
|
|
|
329
324
|
const tags = generateMetaTags(meta);
|
|
330
325
|
|
|
331
|
-
expect(tags).toBe(
|
|
326
|
+
expect(tags).toBe('');
|
|
332
327
|
});
|
|
333
328
|
|
|
334
|
-
test(
|
|
335
|
-
const meta = { title:
|
|
329
|
+
test('should generate hreflang tags when slugMappings provided', () => {
|
|
330
|
+
const meta = { title: 'About Us' };
|
|
336
331
|
const i18nConfig = {
|
|
337
332
|
defaultLocale: 'en',
|
|
338
333
|
locales: [
|
|
@@ -341,9 +336,7 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
341
336
|
{ code: 'de', name: 'German', nativeName: 'Deutsch', langTag: 'de-DE' },
|
|
342
337
|
],
|
|
343
338
|
};
|
|
344
|
-
const slugMappings = [
|
|
345
|
-
{ pageId: 'about', slugs: { en: 'about', pl: 'o-nas', de: 'uber-uns' } },
|
|
346
|
-
];
|
|
339
|
+
const slugMappings = [{ pageId: 'about', slugs: { en: 'about', pl: 'o-nas', de: 'uber-uns' } }];
|
|
347
340
|
|
|
348
341
|
const tags = generateMetaTags(meta, 'https://example.com/about', 'en', i18nConfig, {
|
|
349
342
|
slugMappings,
|
|
@@ -357,17 +350,13 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
357
350
|
expect(tags).toContain('<link rel="alternate" hreflang="x-default" href="https://example.com/about" />');
|
|
358
351
|
});
|
|
359
352
|
|
|
360
|
-
test(
|
|
361
|
-
const meta = { title:
|
|
353
|
+
test('should not generate hreflang tags for single locale', () => {
|
|
354
|
+
const meta = { title: 'About Us' };
|
|
362
355
|
const i18nConfig = {
|
|
363
356
|
defaultLocale: 'en',
|
|
364
|
-
locales: [
|
|
365
|
-
{ code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' },
|
|
366
|
-
],
|
|
357
|
+
locales: [{ code: 'en', name: 'English', nativeName: 'English', langTag: 'en-US' }],
|
|
367
358
|
};
|
|
368
|
-
const slugMappings = [
|
|
369
|
-
{ pageId: 'about', slugs: { en: 'about' } },
|
|
370
|
-
];
|
|
359
|
+
const slugMappings = [{ pageId: 'about', slugs: { en: 'about' } }];
|
|
371
360
|
|
|
372
361
|
const tags = generateMetaTags(meta, 'https://example.com/about', 'en', i18nConfig, {
|
|
373
362
|
slugMappings,
|
|
@@ -378,8 +367,8 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
378
367
|
expect(tags).not.toContain('hreflang');
|
|
379
368
|
});
|
|
380
369
|
|
|
381
|
-
test(
|
|
382
|
-
const meta = { title:
|
|
370
|
+
test('should not generate hreflang tags without slugMappings', () => {
|
|
371
|
+
const meta = { title: 'About Us' };
|
|
383
372
|
const i18nConfig = {
|
|
384
373
|
defaultLocale: 'en',
|
|
385
374
|
locales: [
|
|
@@ -393,8 +382,8 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
393
382
|
expect(tags).not.toContain('hreflang');
|
|
394
383
|
});
|
|
395
384
|
|
|
396
|
-
test(
|
|
397
|
-
const meta = { title:
|
|
385
|
+
test('should escape special characters in hreflang URLs', () => {
|
|
386
|
+
const meta = { title: 'Test' };
|
|
398
387
|
const i18nConfig = {
|
|
399
388
|
defaultLocale: 'en',
|
|
400
389
|
locales: [
|
|
@@ -402,9 +391,7 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
402
391
|
{ code: 'pl', name: 'Polish', nativeName: 'Polski', langTag: 'pl-PL' },
|
|
403
392
|
],
|
|
404
393
|
};
|
|
405
|
-
const slugMappings = [
|
|
406
|
-
{ pageId: 'test', slugs: { en: 'test&page', pl: 'test-strona' } },
|
|
407
|
-
];
|
|
394
|
+
const slugMappings = [{ pageId: 'test', slugs: { en: 'test&page', pl: 'test-strona' } }];
|
|
408
395
|
|
|
409
396
|
const tags = generateMetaTags(meta, 'https://example.com/test&page', 'en', i18nConfig, {
|
|
410
397
|
slugMappings,
|
|
@@ -416,363 +403,363 @@ describe("SSR Renderer - generateMetaTags", () => {
|
|
|
416
403
|
});
|
|
417
404
|
});
|
|
418
405
|
|
|
419
|
-
describe(
|
|
420
|
-
describe(
|
|
421
|
-
test(
|
|
406
|
+
describe('SSR Renderer - renderPageSSR', () => {
|
|
407
|
+
describe('HTML generation', () => {
|
|
408
|
+
test('should render simple div element', async () => {
|
|
422
409
|
const pageData: JSONPage = {
|
|
423
410
|
root: {
|
|
424
|
-
type:
|
|
425
|
-
tag:
|
|
426
|
-
children: [
|
|
427
|
-
}
|
|
411
|
+
type: 'node',
|
|
412
|
+
tag: 'div',
|
|
413
|
+
children: ['Hello World'],
|
|
414
|
+
},
|
|
428
415
|
};
|
|
429
|
-
|
|
416
|
+
|
|
430
417
|
const result = await renderPageSSR(pageData);
|
|
431
|
-
|
|
432
|
-
expect(result.html).toContain(
|
|
433
|
-
expect(result.html).toContain(
|
|
434
|
-
expect(result.html).toContain(
|
|
418
|
+
|
|
419
|
+
expect(result.html).toContain('<div');
|
|
420
|
+
expect(result.html).toContain('Hello World');
|
|
421
|
+
expect(result.html).toContain('</div>');
|
|
435
422
|
});
|
|
436
423
|
|
|
437
|
-
test(
|
|
424
|
+
test('should render nested component structures', async () => {
|
|
438
425
|
const pageData: JSONPage = {
|
|
439
426
|
root: {
|
|
440
|
-
type:
|
|
441
|
-
tag:
|
|
427
|
+
type: 'node',
|
|
428
|
+
tag: 'div',
|
|
442
429
|
children: [
|
|
443
430
|
{
|
|
444
|
-
type:
|
|
445
|
-
|
|
431
|
+
type: 'node',
|
|
432
|
+
tag: 'div',
|
|
446
433
|
children: [
|
|
447
434
|
{
|
|
448
|
-
type:
|
|
449
|
-
|
|
450
|
-
children: [
|
|
451
|
-
}
|
|
452
|
-
]
|
|
453
|
-
}
|
|
454
|
-
]
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
|
|
435
|
+
type: 'node',
|
|
436
|
+
tag: 'span',
|
|
437
|
+
children: ['Nested'],
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
458
445
|
const result = await renderPageSSR(pageData);
|
|
459
|
-
|
|
460
|
-
expect(result.html).toContain(
|
|
461
|
-
expect(result.html).toContain(
|
|
462
|
-
expect(result.html).toContain(
|
|
446
|
+
|
|
447
|
+
expect(result.html).toContain('<div');
|
|
448
|
+
expect(result.html).toContain('<span');
|
|
449
|
+
expect(result.html).toContain('Nested');
|
|
463
450
|
});
|
|
464
451
|
|
|
465
|
-
test(
|
|
452
|
+
test('should render children arrays', async () => {
|
|
466
453
|
const pageData: JSONPage = {
|
|
467
454
|
root: {
|
|
468
|
-
type:
|
|
469
|
-
tag:
|
|
455
|
+
type: 'node',
|
|
456
|
+
tag: 'div',
|
|
470
457
|
children: [
|
|
471
|
-
{ type:
|
|
472
|
-
{ type:
|
|
473
|
-
]
|
|
474
|
-
}
|
|
458
|
+
{ type: 'node', tag: 'p', children: ['First'] },
|
|
459
|
+
{ type: 'node', tag: 'p', children: ['Second'] },
|
|
460
|
+
],
|
|
461
|
+
},
|
|
475
462
|
};
|
|
476
463
|
|
|
477
464
|
const result = await renderPageSSR(pageData);
|
|
478
465
|
|
|
479
|
-
expect(result.html).toContain(
|
|
480
|
-
expect(result.html).toContain(
|
|
466
|
+
expect(result.html).toContain('First');
|
|
467
|
+
expect(result.html).toContain('Second');
|
|
481
468
|
});
|
|
482
469
|
|
|
483
|
-
test(
|
|
470
|
+
test('should render string children', async () => {
|
|
484
471
|
const pageData: JSONPage = {
|
|
485
472
|
root: {
|
|
486
|
-
type:
|
|
487
|
-
tag:
|
|
488
|
-
children: [
|
|
489
|
-
}
|
|
473
|
+
type: 'node',
|
|
474
|
+
tag: 'p',
|
|
475
|
+
children: ['Simple text'],
|
|
476
|
+
},
|
|
490
477
|
};
|
|
491
478
|
|
|
492
479
|
const result = await renderPageSSR(pageData);
|
|
493
480
|
|
|
494
|
-
expect(result.html).toContain(
|
|
481
|
+
expect(result.html).toContain('Simple text');
|
|
495
482
|
});
|
|
496
483
|
|
|
497
|
-
test(
|
|
484
|
+
test('should render number children', async () => {
|
|
498
485
|
const pageData: JSONPage = {
|
|
499
486
|
root: {
|
|
500
|
-
type:
|
|
501
|
-
tag:
|
|
502
|
-
children: [42]
|
|
503
|
-
}
|
|
487
|
+
type: 'node',
|
|
488
|
+
tag: 'span',
|
|
489
|
+
children: [42],
|
|
490
|
+
},
|
|
504
491
|
};
|
|
505
492
|
|
|
506
493
|
const result = await renderPageSSR(pageData);
|
|
507
494
|
|
|
508
|
-
expect(result.html).toContain(
|
|
495
|
+
expect(result.html).toContain('42');
|
|
509
496
|
});
|
|
510
497
|
|
|
511
|
-
test(
|
|
498
|
+
test('should escape HTML in text content', async () => {
|
|
512
499
|
const pageData: JSONPage = {
|
|
513
500
|
root: {
|
|
514
|
-
type:
|
|
515
|
-
tag:
|
|
516
|
-
children: ["<script>alert('xss')</script>"]
|
|
517
|
-
}
|
|
501
|
+
type: 'node',
|
|
502
|
+
tag: 'div',
|
|
503
|
+
children: ["<script>alert('xss')</script>"],
|
|
504
|
+
},
|
|
518
505
|
};
|
|
519
|
-
|
|
506
|
+
|
|
520
507
|
const result = await renderPageSSR(pageData);
|
|
521
|
-
|
|
522
|
-
expect(result.html).toContain(
|
|
523
|
-
expect(result.html).not.toContain(
|
|
508
|
+
|
|
509
|
+
expect(result.html).toContain('<script>');
|
|
510
|
+
expect(result.html).not.toContain('<script>');
|
|
524
511
|
});
|
|
525
512
|
|
|
526
|
-
test(
|
|
513
|
+
test('should render anchor tag', async () => {
|
|
527
514
|
const pageData: JSONPage = {
|
|
528
515
|
root: {
|
|
529
|
-
type:
|
|
530
|
-
tag:
|
|
531
|
-
children: [
|
|
532
|
-
}
|
|
516
|
+
type: 'node',
|
|
517
|
+
tag: 'a',
|
|
518
|
+
children: ['Go to About'],
|
|
519
|
+
},
|
|
533
520
|
};
|
|
534
521
|
|
|
535
522
|
const result = await renderPageSSR(pageData);
|
|
536
523
|
|
|
537
524
|
expect(result.html).toContain(`<a`);
|
|
538
|
-
expect(result.html).toContain(
|
|
539
|
-
expect(result.html).toContain(
|
|
525
|
+
expect(result.html).toContain('Go to About');
|
|
526
|
+
expect(result.html).toContain('</a>');
|
|
540
527
|
});
|
|
541
528
|
});
|
|
542
529
|
|
|
543
|
-
describe(
|
|
544
|
-
test(
|
|
530
|
+
describe('CSS generation', () => {
|
|
531
|
+
test('should generate CSS for responsive styles', async () => {
|
|
545
532
|
const pageData: JSONPage = {
|
|
546
533
|
root: {
|
|
547
|
-
type:
|
|
548
|
-
tag:
|
|
534
|
+
type: 'node',
|
|
535
|
+
tag: 'div',
|
|
549
536
|
style: {
|
|
550
537
|
base: {
|
|
551
|
-
fontSize:
|
|
552
|
-
color:
|
|
538
|
+
fontSize: '16px',
|
|
539
|
+
color: 'red',
|
|
553
540
|
},
|
|
554
541
|
tablet: {
|
|
555
|
-
fontSize:
|
|
542
|
+
fontSize: '14px',
|
|
556
543
|
},
|
|
557
544
|
mobile: {
|
|
558
|
-
fontSize:
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
545
|
+
fontSize: '12px',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
},
|
|
562
549
|
};
|
|
563
550
|
|
|
564
551
|
const result = await renderPageSSR(pageData);
|
|
565
552
|
|
|
566
553
|
// SSR renders HTML with styles (may be inline or via classes)
|
|
567
554
|
expect(result.html).toBeDefined();
|
|
568
|
-
expect(result.html).toContain(
|
|
555
|
+
expect(result.html).toContain('<div');
|
|
569
556
|
});
|
|
570
557
|
|
|
571
|
-
test(
|
|
558
|
+
test('should generate CSS class names for responsive styles', async () => {
|
|
572
559
|
const pageData: JSONPage = {
|
|
573
560
|
root: {
|
|
574
|
-
type:
|
|
575
|
-
tag:
|
|
561
|
+
type: 'node',
|
|
562
|
+
tag: 'div',
|
|
576
563
|
style: {
|
|
577
564
|
base: {
|
|
578
|
-
color:
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
565
|
+
color: 'red',
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
582
569
|
};
|
|
583
570
|
|
|
584
571
|
const result = await renderPageSSR(pageData);
|
|
585
572
|
|
|
586
573
|
// HTML should be generated with styles
|
|
587
574
|
expect(result.html).toBeDefined();
|
|
588
|
-
expect(result.html).toContain(
|
|
575
|
+
expect(result.html).toContain('<div');
|
|
589
576
|
});
|
|
590
577
|
|
|
591
|
-
test(
|
|
578
|
+
test('should handle empty responsive styles', async () => {
|
|
592
579
|
const pageData: JSONPage = {
|
|
593
580
|
root: {
|
|
594
|
-
type:
|
|
595
|
-
tag:
|
|
581
|
+
type: 'node',
|
|
582
|
+
tag: 'div',
|
|
596
583
|
style: {
|
|
597
584
|
base: {},
|
|
598
585
|
tablet: {},
|
|
599
|
-
mobile: {}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
586
|
+
mobile: {},
|
|
587
|
+
},
|
|
588
|
+
},
|
|
602
589
|
};
|
|
603
590
|
|
|
604
591
|
const result = await renderPageSSR(pageData);
|
|
605
592
|
|
|
606
593
|
// Should still render HTML
|
|
607
|
-
expect(result.html).toContain(
|
|
594
|
+
expect(result.html).toContain('<div');
|
|
608
595
|
// componentCSS might be empty or minimal
|
|
609
596
|
expect(result.componentCSS).toBeDefined();
|
|
610
597
|
});
|
|
611
598
|
});
|
|
612
599
|
|
|
613
|
-
describe(
|
|
614
|
-
test(
|
|
600
|
+
describe('Component rendering', () => {
|
|
601
|
+
test('should render Button component (from Button.json)', async () => {
|
|
615
602
|
const buttonComponent: ComponentDefinition = {
|
|
616
|
-
type:
|
|
603
|
+
type: 'Button',
|
|
617
604
|
component: {
|
|
618
605
|
interface: {
|
|
619
606
|
variant: {
|
|
620
|
-
type:
|
|
621
|
-
default:
|
|
607
|
+
type: 'select',
|
|
608
|
+
default: 'primary',
|
|
622
609
|
},
|
|
623
610
|
children: {
|
|
624
|
-
type:
|
|
625
|
-
default:
|
|
626
|
-
}
|
|
611
|
+
type: 'string',
|
|
612
|
+
default: 'Click me',
|
|
613
|
+
},
|
|
627
614
|
},
|
|
628
615
|
structure: {
|
|
629
|
-
type:
|
|
630
|
-
tag:
|
|
616
|
+
type: 'node',
|
|
617
|
+
tag: 'button',
|
|
631
618
|
style: {
|
|
632
619
|
base: {
|
|
633
620
|
background: {
|
|
634
621
|
_mapping: true,
|
|
635
|
-
prop:
|
|
622
|
+
prop: 'variant',
|
|
636
623
|
values: {
|
|
637
|
-
primary:
|
|
638
|
-
secondary:
|
|
639
|
-
}
|
|
624
|
+
primary: 'blue',
|
|
625
|
+
secondary: 'gray',
|
|
626
|
+
},
|
|
640
627
|
} as any,
|
|
641
|
-
color:
|
|
642
|
-
}
|
|
628
|
+
color: 'white',
|
|
629
|
+
},
|
|
643
630
|
},
|
|
644
|
-
children:
|
|
645
|
-
}
|
|
646
|
-
}
|
|
631
|
+
children: '{{children}}',
|
|
632
|
+
},
|
|
633
|
+
},
|
|
647
634
|
};
|
|
648
|
-
|
|
635
|
+
|
|
649
636
|
const pageData: JSONPage = {
|
|
650
637
|
root: {
|
|
651
|
-
type:
|
|
652
|
-
component:
|
|
638
|
+
type: 'component',
|
|
639
|
+
component: 'Button',
|
|
653
640
|
props: {
|
|
654
|
-
variant:
|
|
641
|
+
variant: 'primary',
|
|
655
642
|
},
|
|
656
|
-
children: [
|
|
657
|
-
}
|
|
643
|
+
children: ['Custom Button'],
|
|
644
|
+
},
|
|
658
645
|
};
|
|
659
646
|
|
|
660
647
|
const globalComponents = {
|
|
661
|
-
Button: buttonComponent
|
|
648
|
+
Button: buttonComponent,
|
|
662
649
|
};
|
|
663
650
|
|
|
664
651
|
const result = await renderPageSSR(pageData, globalComponents);
|
|
665
652
|
|
|
666
653
|
// Should render button element
|
|
667
|
-
expect(result.html).toContain(
|
|
654
|
+
expect(result.html).toContain('<button');
|
|
668
655
|
// Should render children
|
|
669
|
-
expect(result.html).toContain(
|
|
656
|
+
expect(result.html).toContain('Custom Button');
|
|
670
657
|
});
|
|
671
658
|
|
|
672
|
-
test(
|
|
659
|
+
test('should render BasicCard component with template evaluation', async () => {
|
|
673
660
|
const cardComponent: ComponentDefinition = {
|
|
674
|
-
type:
|
|
661
|
+
type: 'BasicCard',
|
|
675
662
|
component: {
|
|
676
663
|
interface: {
|
|
677
664
|
title: {
|
|
678
|
-
type:
|
|
679
|
-
default:
|
|
680
|
-
}
|
|
665
|
+
type: 'string',
|
|
666
|
+
default: 'Card Title',
|
|
667
|
+
},
|
|
681
668
|
},
|
|
682
669
|
structure: {
|
|
683
|
-
type:
|
|
684
|
-
tag:
|
|
670
|
+
type: 'node',
|
|
671
|
+
tag: 'div',
|
|
685
672
|
children: [
|
|
686
673
|
{
|
|
687
|
-
type:
|
|
688
|
-
tag:
|
|
689
|
-
children: [
|
|
690
|
-
}
|
|
691
|
-
]
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
|
|
674
|
+
type: 'node',
|
|
675
|
+
tag: 'h3',
|
|
676
|
+
children: ['{{title}}'],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
|
|
696
683
|
const pageData: JSONPage = {
|
|
697
684
|
root: {
|
|
698
|
-
type:
|
|
699
|
-
component:
|
|
685
|
+
type: 'component',
|
|
686
|
+
component: 'BasicCard',
|
|
700
687
|
props: {
|
|
701
|
-
title:
|
|
702
|
-
}
|
|
703
|
-
}
|
|
688
|
+
title: 'My Card',
|
|
689
|
+
},
|
|
690
|
+
},
|
|
704
691
|
};
|
|
705
692
|
|
|
706
693
|
const globalComponents = {
|
|
707
|
-
BasicCard: cardComponent
|
|
694
|
+
BasicCard: cardComponent,
|
|
708
695
|
};
|
|
709
696
|
|
|
710
697
|
const result = await renderPageSSR(pageData, globalComponents);
|
|
711
698
|
|
|
712
|
-
expect(result.html).toContain(
|
|
713
|
-
expect(result.html).toContain(
|
|
714
|
-
expect(result.html).toContain(
|
|
699
|
+
expect(result.html).toContain('<div');
|
|
700
|
+
expect(result.html).toContain('<h3');
|
|
701
|
+
expect(result.html).toContain('My Card');
|
|
715
702
|
});
|
|
716
703
|
});
|
|
717
704
|
|
|
718
|
-
describe(
|
|
719
|
-
test(
|
|
705
|
+
describe('Template evaluation in SSR', () => {
|
|
706
|
+
test('should evaluate template expressions in structure', async () => {
|
|
720
707
|
const pageData: JSONPage = {
|
|
721
708
|
root: {
|
|
722
|
-
type:
|
|
723
|
-
tag:
|
|
724
|
-
children: [
|
|
725
|
-
}
|
|
709
|
+
type: 'node',
|
|
710
|
+
tag: 'div',
|
|
711
|
+
children: ['Hello {{name}}'],
|
|
712
|
+
},
|
|
726
713
|
};
|
|
727
|
-
|
|
714
|
+
|
|
728
715
|
// Note: This test may need adjustment based on actual template evaluation in SSR
|
|
729
716
|
const result = await renderPageSSR(pageData);
|
|
730
|
-
|
|
717
|
+
|
|
731
718
|
// Template should be evaluated or passed through
|
|
732
719
|
expect(result.html).toBeDefined();
|
|
733
720
|
});
|
|
734
721
|
|
|
735
|
-
test(
|
|
722
|
+
test('should handle style mappings in component structures', async () => {
|
|
736
723
|
const buttonComponent: ComponentDefinition = {
|
|
737
|
-
type:
|
|
724
|
+
type: 'Button',
|
|
738
725
|
component: {
|
|
739
726
|
interface: {
|
|
740
727
|
variant: {
|
|
741
|
-
type:
|
|
742
|
-
default:
|
|
743
|
-
}
|
|
728
|
+
type: 'select',
|
|
729
|
+
default: 'primary',
|
|
730
|
+
},
|
|
744
731
|
},
|
|
745
732
|
structure: {
|
|
746
|
-
type:
|
|
747
|
-
tag:
|
|
733
|
+
type: 'node',
|
|
734
|
+
tag: 'button',
|
|
748
735
|
style: {
|
|
749
736
|
base: {
|
|
750
737
|
background: {
|
|
751
738
|
_mapping: true,
|
|
752
|
-
prop:
|
|
739
|
+
prop: 'variant',
|
|
753
740
|
values: {
|
|
754
|
-
primary:
|
|
755
|
-
secondary:
|
|
756
|
-
}
|
|
757
|
-
} as any
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
|
|
741
|
+
primary: 'blue',
|
|
742
|
+
secondary: 'gray',
|
|
743
|
+
},
|
|
744
|
+
} as any,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
|
|
764
751
|
const pageData: JSONPage = {
|
|
765
752
|
root: {
|
|
766
|
-
type:
|
|
767
|
-
component:
|
|
753
|
+
type: 'component',
|
|
754
|
+
component: 'Button',
|
|
768
755
|
props: {
|
|
769
|
-
variant:
|
|
770
|
-
}
|
|
771
|
-
}
|
|
756
|
+
variant: 'primary',
|
|
757
|
+
},
|
|
758
|
+
},
|
|
772
759
|
};
|
|
773
760
|
|
|
774
761
|
const globalComponents = {
|
|
775
|
-
Button: buttonComponent
|
|
762
|
+
Button: buttonComponent,
|
|
776
763
|
};
|
|
777
764
|
|
|
778
765
|
const result = await renderPageSSR(pageData, globalComponents);
|
|
@@ -784,319 +771,316 @@ describe("SSR Renderer - renderPageSSR", () => {
|
|
|
784
771
|
});
|
|
785
772
|
});
|
|
786
773
|
|
|
787
|
-
describe(
|
|
788
|
-
test(
|
|
774
|
+
describe('Error handling', () => {
|
|
775
|
+
test('should handle missing root node', async () => {
|
|
789
776
|
const pageData: JSONPage = {};
|
|
790
777
|
|
|
791
778
|
// Implementation throws error for missing root
|
|
792
779
|
await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
|
|
793
780
|
});
|
|
794
781
|
|
|
795
|
-
test(
|
|
782
|
+
test('should handle null root node', async () => {
|
|
796
783
|
const pageData: JSONPage = {
|
|
797
|
-
root: null as any
|
|
784
|
+
root: null as any,
|
|
798
785
|
};
|
|
799
786
|
|
|
800
787
|
// Implementation throws error for null root
|
|
801
788
|
await expect(renderPageSSR(pageData)).rejects.toThrow('Page data must have a root node');
|
|
802
789
|
});
|
|
803
790
|
|
|
804
|
-
test(
|
|
791
|
+
test('should handle component with missing structure', async () => {
|
|
805
792
|
const invalidComponent: ComponentDefinition = {
|
|
806
|
-
type:
|
|
807
|
-
component: {} as any
|
|
793
|
+
type: 'Invalid',
|
|
794
|
+
component: {} as any,
|
|
808
795
|
};
|
|
809
796
|
|
|
810
797
|
const pageData: JSONPage = {
|
|
811
798
|
root: {
|
|
812
|
-
type:
|
|
813
|
-
tag:
|
|
814
|
-
children: []
|
|
815
|
-
}
|
|
799
|
+
type: 'node' as const,
|
|
800
|
+
tag: 'div',
|
|
801
|
+
children: [],
|
|
802
|
+
},
|
|
816
803
|
};
|
|
817
804
|
|
|
818
805
|
const globalComponents = {
|
|
819
|
-
Invalid: invalidComponent
|
|
806
|
+
Invalid: invalidComponent,
|
|
820
807
|
};
|
|
821
808
|
|
|
822
809
|
// Should not throw - page has valid root structure even if component is invalid
|
|
823
810
|
const result = await renderPageSSR(pageData, globalComponents);
|
|
824
811
|
|
|
825
812
|
// The page's root div renders normally since it's valid
|
|
826
|
-
expect(result.html).toBe(
|
|
813
|
+
expect(result.html).toBe('<div></div>');
|
|
827
814
|
});
|
|
828
815
|
|
|
829
|
-
test(
|
|
816
|
+
test('should handle malformed JSON structures gracefully', async () => {
|
|
830
817
|
const pageData: JSONPage = {
|
|
831
818
|
root: {
|
|
832
|
-
type:
|
|
833
|
-
tag:
|
|
834
|
-
children: [
|
|
835
|
-
|
|
836
|
-
undefined as any
|
|
837
|
-
]
|
|
838
|
-
}
|
|
819
|
+
type: 'node',
|
|
820
|
+
tag: 'div',
|
|
821
|
+
children: [null as any, undefined as any],
|
|
822
|
+
},
|
|
839
823
|
};
|
|
840
|
-
|
|
824
|
+
|
|
841
825
|
const result = await renderPageSSR(pageData);
|
|
842
|
-
|
|
826
|
+
|
|
843
827
|
// Should skip null/undefined children
|
|
844
828
|
expect(result.html).toBeDefined();
|
|
845
|
-
expect(result.html).toContain(
|
|
829
|
+
expect(result.html).toContain('<div');
|
|
846
830
|
});
|
|
847
831
|
});
|
|
848
832
|
|
|
849
|
-
describe(
|
|
850
|
-
test(
|
|
833
|
+
describe('Meta information in SSR', () => {
|
|
834
|
+
test('should extract and return meta information', async () => {
|
|
851
835
|
const pageData: JSONPage = {
|
|
852
836
|
meta: {
|
|
853
|
-
title:
|
|
854
|
-
description:
|
|
837
|
+
title: 'Test Page',
|
|
838
|
+
description: 'Test description',
|
|
855
839
|
},
|
|
856
840
|
root: {
|
|
857
|
-
type:
|
|
858
|
-
tag:
|
|
859
|
-
children: [
|
|
860
|
-
}
|
|
841
|
+
type: 'node',
|
|
842
|
+
tag: 'div',
|
|
843
|
+
children: ['Content'],
|
|
844
|
+
},
|
|
861
845
|
};
|
|
862
|
-
|
|
846
|
+
|
|
863
847
|
const result = await renderPageSSR(pageData);
|
|
864
|
-
|
|
865
|
-
expect(result.title).toBe(
|
|
866
|
-
expect(result.meta).toContain(
|
|
867
|
-
expect(result.meta).toContain(
|
|
848
|
+
|
|
849
|
+
expect(result.title).toBe('Test Page');
|
|
850
|
+
expect(result.meta).toContain('Test Page');
|
|
851
|
+
expect(result.meta).toContain('Test description');
|
|
868
852
|
});
|
|
869
853
|
|
|
870
|
-
test(
|
|
854
|
+
test('should use default title when meta not provided', async () => {
|
|
871
855
|
const pageData: JSONPage = {
|
|
872
856
|
root: {
|
|
873
|
-
type:
|
|
874
|
-
tag:
|
|
875
|
-
}
|
|
857
|
+
type: 'node',
|
|
858
|
+
tag: 'div',
|
|
859
|
+
},
|
|
876
860
|
};
|
|
877
861
|
|
|
878
862
|
const result = await renderPageSSR(pageData);
|
|
879
863
|
|
|
880
|
-
expect(result.title).toBe(
|
|
864
|
+
expect(result.title).toBe('UPLO');
|
|
881
865
|
});
|
|
882
866
|
});
|
|
883
867
|
});
|
|
884
868
|
|
|
885
|
-
describe(
|
|
886
|
-
describe(
|
|
887
|
-
test(
|
|
869
|
+
describe('SSR Renderer - Security (XSS Prevention)', () => {
|
|
870
|
+
describe('Text content sanitization', () => {
|
|
871
|
+
test('should escape script tags in text content', async () => {
|
|
888
872
|
const pageData: JSONPage = {
|
|
889
873
|
root: {
|
|
890
|
-
type:
|
|
891
|
-
tag:
|
|
892
|
-
children: ["<script>alert('xss')</script>"]
|
|
893
|
-
}
|
|
874
|
+
type: 'node',
|
|
875
|
+
tag: 'div',
|
|
876
|
+
children: ["<script>alert('xss')</script>"],
|
|
877
|
+
},
|
|
894
878
|
};
|
|
895
879
|
|
|
896
880
|
const result = await renderPageSSR(pageData);
|
|
897
881
|
|
|
898
|
-
expect(result.html).toContain(
|
|
899
|
-
expect(result.html).not.toContain(
|
|
882
|
+
expect(result.html).toContain('<script>');
|
|
883
|
+
expect(result.html).not.toContain('<script>alert');
|
|
900
884
|
});
|
|
901
885
|
|
|
902
|
-
test(
|
|
886
|
+
test('should escape event handlers in text content', async () => {
|
|
903
887
|
const pageData: JSONPage = {
|
|
904
888
|
root: {
|
|
905
|
-
type:
|
|
906
|
-
tag:
|
|
907
|
-
children: ['<img onerror="alert(1)" src="x">']
|
|
908
|
-
}
|
|
889
|
+
type: 'node',
|
|
890
|
+
tag: 'div',
|
|
891
|
+
children: ['<img onerror="alert(1)" src="x">'],
|
|
892
|
+
},
|
|
909
893
|
};
|
|
910
894
|
|
|
911
895
|
const result = await renderPageSSR(pageData);
|
|
912
896
|
|
|
913
897
|
expect(result.html).not.toContain('onerror="alert');
|
|
914
|
-
expect(result.html).toContain(
|
|
898
|
+
expect(result.html).toContain('<img');
|
|
915
899
|
});
|
|
916
900
|
|
|
917
|
-
test(
|
|
901
|
+
test('should escape javascript: URLs in text content', async () => {
|
|
918
902
|
const pageData: JSONPage = {
|
|
919
903
|
root: {
|
|
920
|
-
type:
|
|
921
|
-
tag:
|
|
922
|
-
children: ['<a href="javascript:alert(1)">click</a>']
|
|
923
|
-
}
|
|
904
|
+
type: 'node',
|
|
905
|
+
tag: 'div',
|
|
906
|
+
children: ['<a href="javascript:alert(1)">click</a>'],
|
|
907
|
+
},
|
|
924
908
|
};
|
|
925
909
|
|
|
926
910
|
const result = await renderPageSSR(pageData);
|
|
927
911
|
|
|
928
912
|
expect(result.html).not.toContain('href="javascript:');
|
|
929
|
-
expect(result.html).toContain(
|
|
913
|
+
expect(result.html).toContain('<a');
|
|
930
914
|
});
|
|
931
915
|
|
|
932
|
-
test(
|
|
916
|
+
test('should escape HTML entities in text content', async () => {
|
|
933
917
|
const pageData: JSONPage = {
|
|
934
918
|
root: {
|
|
935
|
-
type:
|
|
936
|
-
tag:
|
|
937
|
-
children: ['Test & <special> "quotes" \'apostrophe\'']
|
|
938
|
-
}
|
|
919
|
+
type: 'node',
|
|
920
|
+
tag: 'p',
|
|
921
|
+
children: ['Test & <special> "quotes" \'apostrophe\''],
|
|
922
|
+
},
|
|
939
923
|
};
|
|
940
924
|
|
|
941
925
|
const result = await renderPageSSR(pageData);
|
|
942
926
|
|
|
943
|
-
expect(result.html).toContain(
|
|
944
|
-
expect(result.html).toContain(
|
|
945
|
-
expect(result.html).toContain(
|
|
927
|
+
expect(result.html).toContain('&');
|
|
928
|
+
expect(result.html).toContain('<special>');
|
|
929
|
+
expect(result.html).toContain('"quotes"');
|
|
946
930
|
});
|
|
947
931
|
|
|
948
|
-
test(
|
|
932
|
+
test('should escape nested malicious content', async () => {
|
|
949
933
|
const pageData: JSONPage = {
|
|
950
934
|
root: {
|
|
951
|
-
type:
|
|
952
|
-
tag:
|
|
935
|
+
type: 'node',
|
|
936
|
+
tag: 'div',
|
|
953
937
|
children: [
|
|
954
938
|
{
|
|
955
|
-
type:
|
|
956
|
-
tag:
|
|
957
|
-
children: [
|
|
958
|
-
}
|
|
959
|
-
]
|
|
960
|
-
}
|
|
939
|
+
type: 'node',
|
|
940
|
+
tag: 'p',
|
|
941
|
+
children: ['<script>document.cookie</script>'],
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
},
|
|
961
945
|
};
|
|
962
946
|
|
|
963
947
|
const result = await renderPageSSR(pageData);
|
|
964
948
|
|
|
965
|
-
expect(result.html).not.toContain(
|
|
966
|
-
expect(result.html).toContain(
|
|
949
|
+
expect(result.html).not.toContain('<script>document');
|
|
950
|
+
expect(result.html).toContain('<script>');
|
|
967
951
|
});
|
|
968
952
|
});
|
|
969
953
|
|
|
970
|
-
describe(
|
|
971
|
-
test(
|
|
954
|
+
describe('Meta tag sanitization', () => {
|
|
955
|
+
test('should escape HTML in page title', () => {
|
|
972
956
|
const meta = {
|
|
973
|
-
title: "Test <script>alert('xss')</script> Page"
|
|
957
|
+
title: "Test <script>alert('xss')</script> Page",
|
|
974
958
|
};
|
|
975
959
|
|
|
976
960
|
const tags = generateMetaTags(meta);
|
|
977
961
|
|
|
978
|
-
expect(tags).toContain(
|
|
979
|
-
expect(tags).not.toContain(
|
|
962
|
+
expect(tags).toContain('<title>Test <script>');
|
|
963
|
+
expect(tags).not.toContain('<script>alert');
|
|
980
964
|
});
|
|
981
965
|
|
|
982
|
-
test(
|
|
966
|
+
test('should escape HTML in meta description', () => {
|
|
983
967
|
const meta = {
|
|
984
|
-
description: 'Description with "quotes" and <tags>'
|
|
968
|
+
description: 'Description with "quotes" and <tags>',
|
|
985
969
|
};
|
|
986
970
|
|
|
987
971
|
const tags = generateMetaTags(meta);
|
|
988
972
|
|
|
989
|
-
expect(tags).toContain(
|
|
990
|
-
expect(tags).toContain(
|
|
973
|
+
expect(tags).toContain('"quotes"');
|
|
974
|
+
expect(tags).toContain('<tags>');
|
|
991
975
|
});
|
|
992
976
|
|
|
993
|
-
test(
|
|
977
|
+
test('should escape HTML in Open Graph tags', () => {
|
|
994
978
|
const meta = {
|
|
995
979
|
ogTitle: "<script>alert('og')</script>",
|
|
996
|
-
ogDescription:
|
|
980
|
+
ogDescription: 'Description <img src=x onerror=alert(1)>',
|
|
997
981
|
};
|
|
998
982
|
|
|
999
983
|
const tags = generateMetaTags(meta);
|
|
1000
984
|
|
|
1001
985
|
// Script tags should be escaped
|
|
1002
|
-
expect(tags).not.toContain(
|
|
1003
|
-
expect(tags).toContain(
|
|
986
|
+
expect(tags).not.toContain('<script>alert');
|
|
987
|
+
expect(tags).toContain('<script>');
|
|
1004
988
|
// IMG tag should be escaped, preventing onerror from executing
|
|
1005
|
-
expect(tags).not.toContain(
|
|
1006
|
-
expect(tags).toContain(
|
|
989
|
+
expect(tags).not.toContain('<img src=x');
|
|
990
|
+
expect(tags).toContain('<img');
|
|
1007
991
|
});
|
|
1008
992
|
});
|
|
1009
993
|
|
|
1010
|
-
describe(
|
|
1011
|
-
test(
|
|
994
|
+
describe('Attribute sanitization', () => {
|
|
995
|
+
test('should not allow malicious attributes through style', async () => {
|
|
1012
996
|
const pageData: JSONPage = {
|
|
1013
997
|
root: {
|
|
1014
|
-
type:
|
|
1015
|
-
tag:
|
|
998
|
+
type: 'node',
|
|
999
|
+
tag: 'div',
|
|
1016
1000
|
style: {
|
|
1017
1001
|
base: {
|
|
1018
1002
|
// Attempting to inject via style properties
|
|
1019
|
-
color: 'red; background-image: url("javascript:alert(1)")'
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1003
|
+
color: 'red; background-image: url("javascript:alert(1)")',
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1023
1007
|
};
|
|
1024
1008
|
|
|
1025
1009
|
const result = await renderPageSSR(pageData);
|
|
1026
1010
|
|
|
1027
1011
|
// Style should be processed safely via utility classes
|
|
1028
1012
|
expect(result.html).toBeDefined();
|
|
1029
|
-
expect(result.html).toContain(
|
|
1013
|
+
expect(result.html).toContain('<div');
|
|
1030
1014
|
});
|
|
1031
1015
|
|
|
1032
|
-
test(
|
|
1016
|
+
test('should handle unicode escape sequences safely', async () => {
|
|
1033
1017
|
const pageData: JSONPage = {
|
|
1034
1018
|
root: {
|
|
1035
|
-
type:
|
|
1036
|
-
tag:
|
|
1037
|
-
children: ["\u003cscript\u003ealert('unicode')\u003c/script\u003e"]
|
|
1038
|
-
}
|
|
1019
|
+
type: 'node',
|
|
1020
|
+
tag: 'div',
|
|
1021
|
+
children: ["\u003cscript\u003ealert('unicode')\u003c/script\u003e"],
|
|
1022
|
+
},
|
|
1039
1023
|
};
|
|
1040
1024
|
|
|
1041
1025
|
const result = await renderPageSSR(pageData);
|
|
1042
1026
|
|
|
1043
1027
|
// Unicode-encoded script tags should still be escaped
|
|
1044
|
-
expect(result.html).not.toContain(
|
|
1028
|
+
expect(result.html).not.toContain('<script>alert');
|
|
1045
1029
|
});
|
|
1046
1030
|
});
|
|
1047
1031
|
|
|
1048
|
-
describe(
|
|
1049
|
-
test(
|
|
1032
|
+
describe('Component prop sanitization', () => {
|
|
1033
|
+
test('should sanitize component props with HTML content', async () => {
|
|
1050
1034
|
const cardComponent: ComponentDefinition = {
|
|
1051
|
-
type:
|
|
1035
|
+
type: 'Card',
|
|
1052
1036
|
component: {
|
|
1053
1037
|
interface: {
|
|
1054
|
-
title: { type:
|
|
1038
|
+
title: { type: 'string', default: '' },
|
|
1055
1039
|
},
|
|
1056
1040
|
structure: {
|
|
1057
|
-
type:
|
|
1058
|
-
tag:
|
|
1041
|
+
type: 'node',
|
|
1042
|
+
tag: 'div',
|
|
1059
1043
|
children: [
|
|
1060
1044
|
{
|
|
1061
|
-
type:
|
|
1062
|
-
tag:
|
|
1063
|
-
children: [
|
|
1064
|
-
}
|
|
1065
|
-
]
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1045
|
+
type: 'node',
|
|
1046
|
+
tag: 'h3',
|
|
1047
|
+
children: ['{{title}}'],
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1068
1052
|
};
|
|
1069
1053
|
|
|
1070
1054
|
const pageData: JSONPage = {
|
|
1071
1055
|
root: {
|
|
1072
|
-
type:
|
|
1073
|
-
component:
|
|
1056
|
+
type: 'component',
|
|
1057
|
+
component: 'Card',
|
|
1074
1058
|
props: {
|
|
1075
|
-
title: "<script>alert('props')</script>"
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1059
|
+
title: "<script>alert('props')</script>",
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1078
1062
|
};
|
|
1079
1063
|
|
|
1080
1064
|
const globalComponents = {
|
|
1081
|
-
Card: cardComponent
|
|
1065
|
+
Card: cardComponent,
|
|
1082
1066
|
};
|
|
1083
1067
|
|
|
1084
1068
|
const result = await renderPageSSR(pageData, globalComponents);
|
|
1085
1069
|
|
|
1086
1070
|
// Script tags should be escaped when rendering
|
|
1087
|
-
expect(result.html).not.toContain(
|
|
1071
|
+
expect(result.html).not.toContain('<script>alert');
|
|
1088
1072
|
});
|
|
1089
1073
|
});
|
|
1090
1074
|
|
|
1091
|
-
describe(
|
|
1092
|
-
test(
|
|
1075
|
+
describe('Image sanitization', () => {
|
|
1076
|
+
test('should escape src attribute in image nodes', async () => {
|
|
1093
1077
|
const pageData: JSONPage = {
|
|
1094
1078
|
root: {
|
|
1095
|
-
type:
|
|
1096
|
-
tag:
|
|
1079
|
+
type: 'image',
|
|
1080
|
+
tag: 'img',
|
|
1097
1081
|
src: 'javascript:alert("img")',
|
|
1098
|
-
alt:
|
|
1099
|
-
} as any
|
|
1082
|
+
alt: 'test',
|
|
1083
|
+
} as any,
|
|
1100
1084
|
};
|
|
1101
1085
|
|
|
1102
1086
|
const result = await renderPageSSR(pageData);
|
|
@@ -1105,14 +1089,14 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
|
|
|
1105
1089
|
expect(result.html).toBeDefined();
|
|
1106
1090
|
});
|
|
1107
1091
|
|
|
1108
|
-
test(
|
|
1092
|
+
test('should escape alt attribute in image nodes', async () => {
|
|
1109
1093
|
const pageData: JSONPage = {
|
|
1110
1094
|
root: {
|
|
1111
|
-
type:
|
|
1112
|
-
tag:
|
|
1113
|
-
src:
|
|
1114
|
-
alt: '"><script>alert("alt")</script><img x="'
|
|
1115
|
-
} as any
|
|
1095
|
+
type: 'image',
|
|
1096
|
+
tag: 'img',
|
|
1097
|
+
src: '/test.jpg',
|
|
1098
|
+
alt: '"><script>alert("alt")</script><img x="',
|
|
1099
|
+
} as any,
|
|
1116
1100
|
};
|
|
1117
1101
|
|
|
1118
1102
|
const result = await renderPageSSR(pageData);
|
|
@@ -1122,35 +1106,34 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
|
|
|
1122
1106
|
});
|
|
1123
1107
|
});
|
|
1124
1108
|
|
|
1125
|
-
describe(
|
|
1126
|
-
test(
|
|
1109
|
+
describe('Image LCP optimization', () => {
|
|
1110
|
+
test('should render fetchpriority attribute', async () => {
|
|
1127
1111
|
const pageData: JSONPage = {
|
|
1128
1112
|
root: {
|
|
1129
|
-
type:
|
|
1130
|
-
tag:
|
|
1113
|
+
type: 'node',
|
|
1114
|
+
tag: 'img',
|
|
1131
1115
|
attributes: {
|
|
1132
|
-
src:
|
|
1133
|
-
alt:
|
|
1134
|
-
fetchpriority:
|
|
1135
|
-
}
|
|
1136
|
-
} as any
|
|
1116
|
+
src: '/test.jpg',
|
|
1117
|
+
alt: 'test',
|
|
1118
|
+
fetchpriority: 'high',
|
|
1119
|
+
},
|
|
1120
|
+
} as any,
|
|
1137
1121
|
};
|
|
1138
1122
|
|
|
1139
1123
|
const result = await renderPageSSR(pageData);
|
|
1140
1124
|
|
|
1141
1125
|
expect(result.html).toContain('fetchpriority="high"');
|
|
1142
1126
|
});
|
|
1143
|
-
|
|
1144
1127
|
});
|
|
1145
1128
|
|
|
1146
|
-
describe(
|
|
1147
|
-
test(
|
|
1129
|
+
describe('Embed content sanitization', () => {
|
|
1130
|
+
test('should sanitize embed HTML with DOMPurify', async () => {
|
|
1148
1131
|
const pageData: JSONPage = {
|
|
1149
1132
|
root: {
|
|
1150
|
-
type:
|
|
1151
|
-
tag:
|
|
1152
|
-
html: '<div onclick="alert(1)"><script>alert(2)</script><p>safe</p></div>'
|
|
1153
|
-
} as any
|
|
1133
|
+
type: 'embed',
|
|
1134
|
+
tag: 'div',
|
|
1135
|
+
html: '<div onclick="alert(1)"><script>alert(2)</script><p>safe</p></div>',
|
|
1136
|
+
} as any,
|
|
1154
1137
|
};
|
|
1155
1138
|
|
|
1156
1139
|
const result = await renderPageSSR(pageData);
|
|
@@ -1160,105 +1143,105 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
|
|
|
1160
1143
|
// script tags should be stripped
|
|
1161
1144
|
expect(result.html).not.toContain('<script>alert');
|
|
1162
1145
|
// Safe content should remain
|
|
1163
|
-
expect(result.html).toContain(
|
|
1146
|
+
expect(result.html).toContain('safe');
|
|
1164
1147
|
});
|
|
1165
1148
|
|
|
1166
|
-
test(
|
|
1149
|
+
test('should allow safe SVG in embeds', async () => {
|
|
1167
1150
|
const pageData: JSONPage = {
|
|
1168
1151
|
root: {
|
|
1169
|
-
type:
|
|
1170
|
-
tag:
|
|
1171
|
-
html: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>'
|
|
1172
|
-
} as any
|
|
1152
|
+
type: 'embed',
|
|
1153
|
+
tag: 'div',
|
|
1154
|
+
html: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="red"/></svg>',
|
|
1155
|
+
} as any,
|
|
1173
1156
|
};
|
|
1174
1157
|
|
|
1175
1158
|
const result = await renderPageSSR(pageData);
|
|
1176
1159
|
|
|
1177
1160
|
// Safe SVG should be preserved
|
|
1178
|
-
expect(result.html).toContain(
|
|
1179
|
-
expect(result.html).toContain(
|
|
1161
|
+
expect(result.html).toContain('<svg');
|
|
1162
|
+
expect(result.html).toContain('<circle');
|
|
1180
1163
|
});
|
|
1181
1164
|
});
|
|
1182
1165
|
});
|
|
1183
1166
|
|
|
1184
|
-
describe(
|
|
1185
|
-
test(
|
|
1167
|
+
describe('SSR Renderer - generateSSRHTML', () => {
|
|
1168
|
+
test('should generate complete HTML document', async () => {
|
|
1186
1169
|
const pageData: JSONPage = {
|
|
1187
1170
|
meta: {
|
|
1188
|
-
title:
|
|
1171
|
+
title: 'Test Page',
|
|
1189
1172
|
},
|
|
1190
1173
|
root: {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
children: [
|
|
1194
|
-
}
|
|
1174
|
+
type: 'node',
|
|
1175
|
+
tag: 'div',
|
|
1176
|
+
children: ['Hello World'],
|
|
1177
|
+
},
|
|
1195
1178
|
};
|
|
1196
|
-
|
|
1197
|
-
const html = await generateSSRHTML(pageData, {},
|
|
1198
|
-
|
|
1199
|
-
expect(html).toContain(
|
|
1200
|
-
expect(html).toContain(
|
|
1201
|
-
expect(html).toContain(
|
|
1202
|
-
expect(html).toContain(
|
|
1179
|
+
|
|
1180
|
+
const html = await generateSSRHTML(pageData, {}, '/', '');
|
|
1181
|
+
|
|
1182
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
1183
|
+
expect(html).toContain('<html');
|
|
1184
|
+
expect(html).toContain('<head');
|
|
1185
|
+
expect(html).toContain('<body');
|
|
1203
1186
|
expect(html).toContain('<div id="root">');
|
|
1204
|
-
expect(html).toContain(
|
|
1205
|
-
expect(html).toContain(
|
|
1187
|
+
expect(html).toContain('Hello World');
|
|
1188
|
+
expect(html).toContain('Test Page');
|
|
1206
1189
|
});
|
|
1207
1190
|
|
|
1208
|
-
test(
|
|
1191
|
+
test('should include CSS in style tag', async () => {
|
|
1209
1192
|
const pageData: JSONPage = {
|
|
1210
1193
|
root: {
|
|
1211
|
-
type:
|
|
1212
|
-
tag:
|
|
1194
|
+
type: 'node',
|
|
1195
|
+
tag: 'div',
|
|
1213
1196
|
style: {
|
|
1214
1197
|
base: {
|
|
1215
|
-
color:
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1198
|
+
color: 'red',
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1219
1202
|
};
|
|
1220
1203
|
|
|
1221
1204
|
const html = await generateSSRHTML(pageData);
|
|
1222
1205
|
|
|
1223
1206
|
// HTML should be generated with styles (either inline or in style tag)
|
|
1224
|
-
expect(html).toContain(
|
|
1207
|
+
expect(html).toContain('<div');
|
|
1225
1208
|
expect(html).toBeDefined();
|
|
1226
1209
|
});
|
|
1227
1210
|
|
|
1228
|
-
test(
|
|
1211
|
+
test('should include client script', async () => {
|
|
1229
1212
|
const pageData: JSONPage = {
|
|
1230
1213
|
root: {
|
|
1231
|
-
type:
|
|
1232
|
-
tag:
|
|
1233
|
-
}
|
|
1214
|
+
type: 'node',
|
|
1215
|
+
tag: 'div',
|
|
1216
|
+
},
|
|
1234
1217
|
};
|
|
1235
1218
|
|
|
1236
|
-
const html = await generateSSRHTML(pageData, {},
|
|
1219
|
+
const html = await generateSSRHTML(pageData, {}, '/', '', false);
|
|
1237
1220
|
|
|
1238
1221
|
expect(html).toContain('src="/client-router.tsx"');
|
|
1239
1222
|
});
|
|
1240
1223
|
|
|
1241
|
-
test(
|
|
1224
|
+
test('should use built bundle when specified', async () => {
|
|
1242
1225
|
const pageData: JSONPage = {
|
|
1243
1226
|
root: {
|
|
1244
|
-
type:
|
|
1245
|
-
tag:
|
|
1246
|
-
}
|
|
1227
|
+
type: 'node',
|
|
1228
|
+
tag: 'div',
|
|
1229
|
+
},
|
|
1247
1230
|
};
|
|
1248
1231
|
|
|
1249
|
-
const html = await generateSSRHTML(pageData, {},
|
|
1232
|
+
const html = await generateSSRHTML(pageData, {}, '/', '', true);
|
|
1250
1233
|
|
|
1251
1234
|
// When useBuiltBundle is true, implementation generates static HTML without client script
|
|
1252
1235
|
expect(html).toBeDefined();
|
|
1253
|
-
expect(html).toContain(
|
|
1236
|
+
expect(html).toContain('<div');
|
|
1254
1237
|
});
|
|
1255
1238
|
|
|
1256
|
-
test(
|
|
1239
|
+
test('should include viewport meta tag', async () => {
|
|
1257
1240
|
const pageData: JSONPage = {
|
|
1258
1241
|
root: {
|
|
1259
|
-
type:
|
|
1260
|
-
tag:
|
|
1261
|
-
}
|
|
1242
|
+
type: 'node',
|
|
1243
|
+
tag: 'div',
|
|
1244
|
+
},
|
|
1262
1245
|
};
|
|
1263
1246
|
|
|
1264
1247
|
const html = await generateSSRHTML(pageData);
|
|
@@ -1268,30 +1251,38 @@ describe("SSR Renderer - generateSSRHTML", () => {
|
|
|
1268
1251
|
});
|
|
1269
1252
|
});
|
|
1270
1253
|
|
|
1271
|
-
describe(
|
|
1254
|
+
describe('SSR Renderer - CMSList rendering', () => {
|
|
1272
1255
|
// Mock CMS items for testing
|
|
1273
1256
|
const mockCmsItems: CMSItem[] = [
|
|
1274
1257
|
{ _id: '1', slug: 'post-1', title: 'First Post', category: 'Tech' },
|
|
1275
1258
|
{ _id: '2', slug: 'post-2', title: 'Second Post', category: 'Design' },
|
|
1276
1259
|
];
|
|
1277
1260
|
|
|
1278
|
-
test(
|
|
1261
|
+
test('should render children for each CMS item', async () => {
|
|
1279
1262
|
const pageData: JSONPage = {
|
|
1280
1263
|
root: {
|
|
1281
|
-
type:
|
|
1282
|
-
collection:
|
|
1264
|
+
type: 'cms-list',
|
|
1265
|
+
collection: 'blog-posts',
|
|
1283
1266
|
limit: 2,
|
|
1284
|
-
children: [
|
|
1285
|
-
|
|
1286
|
-
]
|
|
1287
|
-
} as unknown as ComponentNode
|
|
1267
|
+
children: [{ type: 'node', tag: 'h2', children: ['{{item.title}}'] }],
|
|
1268
|
+
} as unknown as ComponentNode,
|
|
1288
1269
|
};
|
|
1289
1270
|
|
|
1290
1271
|
// Create a mock CMS service
|
|
1291
1272
|
const mockCmsService = new CMSService();
|
|
1292
1273
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1293
1274
|
|
|
1294
|
-
const result = await renderPageSSR(
|
|
1275
|
+
const result = await renderPageSSR(
|
|
1276
|
+
pageData,
|
|
1277
|
+
{},
|
|
1278
|
+
'/',
|
|
1279
|
+
'',
|
|
1280
|
+
undefined,
|
|
1281
|
+
undefined,
|
|
1282
|
+
undefined,
|
|
1283
|
+
undefined,
|
|
1284
|
+
mockCmsService,
|
|
1285
|
+
);
|
|
1295
1286
|
|
|
1296
1287
|
expect(result.html).toContain('<h2');
|
|
1297
1288
|
expect(result.html).toContain('First Post');
|
|
@@ -1299,81 +1290,109 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1299
1290
|
expect(result.html).toContain('</h2>');
|
|
1300
1291
|
});
|
|
1301
1292
|
|
|
1302
|
-
test(
|
|
1293
|
+
test('should provide item context variables (itemIndex)', async () => {
|
|
1303
1294
|
const pageData: JSONPage = {
|
|
1304
1295
|
root: {
|
|
1305
|
-
type:
|
|
1306
|
-
collection:
|
|
1307
|
-
children: [
|
|
1308
|
-
|
|
1309
|
-
]
|
|
1310
|
-
} as unknown as ComponentNode
|
|
1296
|
+
type: 'cms-list',
|
|
1297
|
+
collection: 'blog-posts',
|
|
1298
|
+
children: [{ type: 'node', tag: 'span', children: ['{{itemIndex}}-{{item.title}}'] }],
|
|
1299
|
+
} as unknown as ComponentNode,
|
|
1311
1300
|
};
|
|
1312
1301
|
|
|
1313
1302
|
const mockCmsService = new CMSService();
|
|
1314
1303
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1315
1304
|
|
|
1316
|
-
const result = await renderPageSSR(
|
|
1305
|
+
const result = await renderPageSSR(
|
|
1306
|
+
pageData,
|
|
1307
|
+
{},
|
|
1308
|
+
'/',
|
|
1309
|
+
'',
|
|
1310
|
+
undefined,
|
|
1311
|
+
undefined,
|
|
1312
|
+
undefined,
|
|
1313
|
+
undefined,
|
|
1314
|
+
mockCmsService,
|
|
1315
|
+
);
|
|
1317
1316
|
|
|
1318
1317
|
expect(result.html).toContain('0-First Post');
|
|
1319
1318
|
expect(result.html).toContain('1-Second Post');
|
|
1320
1319
|
});
|
|
1321
1320
|
|
|
1322
|
-
test(
|
|
1321
|
+
test('should provide itemFirst and itemLast flags', async () => {
|
|
1323
1322
|
const pageData: JSONPage = {
|
|
1324
1323
|
root: {
|
|
1325
|
-
type:
|
|
1326
|
-
collection:
|
|
1324
|
+
type: 'cms-list',
|
|
1325
|
+
collection: 'blog-posts',
|
|
1327
1326
|
children: [
|
|
1328
1327
|
{
|
|
1329
|
-
type:
|
|
1330
|
-
tag:
|
|
1328
|
+
type: 'node',
|
|
1329
|
+
tag: 'div',
|
|
1331
1330
|
attributes: { 'data-first': '{{itemFirst}}', 'data-last': '{{itemLast}}' },
|
|
1332
|
-
children: [
|
|
1333
|
-
}
|
|
1334
|
-
]
|
|
1335
|
-
} as unknown as ComponentNode
|
|
1331
|
+
children: ['{{item.title}}'],
|
|
1332
|
+
},
|
|
1333
|
+
],
|
|
1334
|
+
} as unknown as ComponentNode,
|
|
1336
1335
|
};
|
|
1337
1336
|
|
|
1338
1337
|
const mockCmsService = new CMSService();
|
|
1339
1338
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1340
1339
|
|
|
1341
|
-
const result = await renderPageSSR(
|
|
1340
|
+
const result = await renderPageSSR(
|
|
1341
|
+
pageData,
|
|
1342
|
+
{},
|
|
1343
|
+
'/',
|
|
1344
|
+
'',
|
|
1345
|
+
undefined,
|
|
1346
|
+
undefined,
|
|
1347
|
+
undefined,
|
|
1348
|
+
undefined,
|
|
1349
|
+
mockCmsService,
|
|
1350
|
+
);
|
|
1342
1351
|
|
|
1343
1352
|
expect(result.html).toContain('data-first="true"');
|
|
1344
1353
|
expect(result.html).toContain('data-last="true"');
|
|
1345
1354
|
});
|
|
1346
1355
|
|
|
1347
|
-
test(
|
|
1356
|
+
test('should render empty when no items match', async () => {
|
|
1348
1357
|
const pageData: JSONPage = {
|
|
1349
1358
|
root: {
|
|
1350
|
-
type:
|
|
1351
|
-
collection:
|
|
1359
|
+
type: 'cms-list',
|
|
1360
|
+
collection: 'blog-posts',
|
|
1352
1361
|
filter: { category: 'NonExistent' },
|
|
1353
|
-
children: [{ type:
|
|
1354
|
-
} as unknown as ComponentNode
|
|
1362
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
1363
|
+
} as unknown as ComponentNode,
|
|
1355
1364
|
};
|
|
1356
1365
|
|
|
1357
1366
|
const mockCmsService = new CMSService();
|
|
1358
1367
|
mockCmsService.queryItems = async () => [];
|
|
1359
1368
|
|
|
1360
|
-
const result = await renderPageSSR(
|
|
1369
|
+
const result = await renderPageSSR(
|
|
1370
|
+
pageData,
|
|
1371
|
+
{},
|
|
1372
|
+
'/',
|
|
1373
|
+
'',
|
|
1374
|
+
undefined,
|
|
1375
|
+
undefined,
|
|
1376
|
+
undefined,
|
|
1377
|
+
undefined,
|
|
1378
|
+
mockCmsService,
|
|
1379
|
+
);
|
|
1361
1380
|
|
|
1362
1381
|
// Should not contain item content since no items match
|
|
1363
1382
|
expect(result.html).not.toContain('First Post');
|
|
1364
1383
|
expect(result.html).not.toContain('Second Post');
|
|
1365
1384
|
});
|
|
1366
1385
|
|
|
1367
|
-
test(
|
|
1386
|
+
test('should warn when CMS service is not available', async () => {
|
|
1368
1387
|
const consoleSpy = spyOn(console, 'warn').mockImplementation(() => {});
|
|
1369
1388
|
|
|
1370
1389
|
const pageData: JSONPage = {
|
|
1371
1390
|
root: {
|
|
1372
|
-
type:
|
|
1373
|
-
sourceType:
|
|
1374
|
-
source:
|
|
1375
|
-
children: []
|
|
1376
|
-
} as unknown as ComponentNode
|
|
1391
|
+
type: 'list',
|
|
1392
|
+
sourceType: 'collection',
|
|
1393
|
+
source: 'blog-posts',
|
|
1394
|
+
children: [],
|
|
1395
|
+
} as unknown as ComponentNode,
|
|
1377
1396
|
};
|
|
1378
1397
|
|
|
1379
1398
|
// No CMS service provided
|
|
@@ -1385,15 +1404,13 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1385
1404
|
consoleSpy.mockRestore();
|
|
1386
1405
|
});
|
|
1387
1406
|
|
|
1388
|
-
test(
|
|
1407
|
+
test('should pass through parent context to children', async () => {
|
|
1389
1408
|
const pageData: JSONPage = {
|
|
1390
1409
|
root: {
|
|
1391
|
-
type:
|
|
1392
|
-
collection:
|
|
1393
|
-
children: [
|
|
1394
|
-
|
|
1395
|
-
]
|
|
1396
|
-
} as unknown as ComponentNode
|
|
1410
|
+
type: 'cms-list',
|
|
1411
|
+
collection: 'blog-posts',
|
|
1412
|
+
children: [{ type: 'node', tag: 'span', children: ['{{cms.siteTitle}}-{{item.title}}'] }],
|
|
1413
|
+
} as unknown as ComponentNode,
|
|
1397
1414
|
};
|
|
1398
1415
|
|
|
1399
1416
|
const mockCmsService = new CMSService();
|
|
@@ -1402,34 +1419,54 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1402
1419
|
// Pass CMS context with parent variables
|
|
1403
1420
|
const cmsContext = { cms: { siteTitle: 'MySite' } };
|
|
1404
1421
|
|
|
1405
|
-
const result = await renderPageSSR(
|
|
1422
|
+
const result = await renderPageSSR(
|
|
1423
|
+
pageData,
|
|
1424
|
+
{},
|
|
1425
|
+
'/',
|
|
1426
|
+
'',
|
|
1427
|
+
undefined,
|
|
1428
|
+
undefined,
|
|
1429
|
+
undefined,
|
|
1430
|
+
cmsContext,
|
|
1431
|
+
mockCmsService,
|
|
1432
|
+
);
|
|
1406
1433
|
|
|
1407
1434
|
expect(result.html).toContain('MySite-First Post');
|
|
1408
1435
|
expect(result.html).toContain('MySite-Second Post');
|
|
1409
1436
|
});
|
|
1410
1437
|
|
|
1411
|
-
test(
|
|
1438
|
+
test('should support nested HTML structures in children', async () => {
|
|
1412
1439
|
const pageData: JSONPage = {
|
|
1413
1440
|
root: {
|
|
1414
|
-
type:
|
|
1415
|
-
collection:
|
|
1441
|
+
type: 'cms-list',
|
|
1442
|
+
collection: 'blog-posts',
|
|
1416
1443
|
children: [
|
|
1417
1444
|
{
|
|
1418
|
-
type:
|
|
1419
|
-
tag:
|
|
1445
|
+
type: 'node',
|
|
1446
|
+
tag: 'article',
|
|
1420
1447
|
children: [
|
|
1421
|
-
{ type:
|
|
1422
|
-
{ type:
|
|
1423
|
-
]
|
|
1424
|
-
}
|
|
1425
|
-
]
|
|
1426
|
-
} as unknown as ComponentNode
|
|
1448
|
+
{ type: 'node', tag: 'h2', children: ['{{item.title}}'] },
|
|
1449
|
+
{ type: 'node', tag: 'p', children: ['Category: {{item.category}}'] },
|
|
1450
|
+
],
|
|
1451
|
+
},
|
|
1452
|
+
],
|
|
1453
|
+
} as unknown as ComponentNode,
|
|
1427
1454
|
};
|
|
1428
1455
|
|
|
1429
1456
|
const mockCmsService = new CMSService();
|
|
1430
1457
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1431
1458
|
|
|
1432
|
-
const result = await renderPageSSR(
|
|
1459
|
+
const result = await renderPageSSR(
|
|
1460
|
+
pageData,
|
|
1461
|
+
{},
|
|
1462
|
+
'/',
|
|
1463
|
+
'',
|
|
1464
|
+
undefined,
|
|
1465
|
+
undefined,
|
|
1466
|
+
undefined,
|
|
1467
|
+
undefined,
|
|
1468
|
+
mockCmsService,
|
|
1469
|
+
);
|
|
1433
1470
|
|
|
1434
1471
|
expect(result.html).toContain('<article');
|
|
1435
1472
|
expect(result.html).toContain('<h2');
|
|
@@ -1439,11 +1476,15 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1439
1476
|
expect(result.html).toContain('</article>');
|
|
1440
1477
|
});
|
|
1441
1478
|
|
|
1442
|
-
test(
|
|
1479
|
+
test('should render CMS list as a pure repeater (no container) even when style is present', async () => {
|
|
1480
|
+
// Current behavior: list nodes are pure repeaters — they emit only their
|
|
1481
|
+
// per-item children with no wrapper element, so node-level style produces
|
|
1482
|
+
// no utility classes (style a parent node instead). Previously the list
|
|
1483
|
+
// rendered a styled <div data-cms-list> container.
|
|
1443
1484
|
const pageData: JSONPage = {
|
|
1444
1485
|
root: {
|
|
1445
|
-
type:
|
|
1446
|
-
collection:
|
|
1486
|
+
type: 'cms-list',
|
|
1487
|
+
collection: 'blog-posts',
|
|
1447
1488
|
style: {
|
|
1448
1489
|
base: {
|
|
1449
1490
|
display: 'flex',
|
|
@@ -1451,76 +1492,105 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1451
1492
|
gap: '24px',
|
|
1452
1493
|
},
|
|
1453
1494
|
},
|
|
1454
|
-
children: [
|
|
1455
|
-
|
|
1456
|
-
]
|
|
1457
|
-
} as unknown as ComponentNode
|
|
1495
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
1496
|
+
} as unknown as ComponentNode,
|
|
1458
1497
|
};
|
|
1459
1498
|
|
|
1460
1499
|
const mockCmsService = new CMSService();
|
|
1461
1500
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1462
1501
|
|
|
1463
|
-
const result = await renderPageSSR(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1502
|
+
const result = await renderPageSSR(
|
|
1503
|
+
pageData,
|
|
1504
|
+
{},
|
|
1505
|
+
'/',
|
|
1506
|
+
'',
|
|
1507
|
+
undefined,
|
|
1508
|
+
undefined,
|
|
1509
|
+
undefined,
|
|
1510
|
+
undefined,
|
|
1511
|
+
mockCmsService,
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
// Only the repeated children are emitted — once per item, in order
|
|
1515
|
+
expect(result.html).toContain('<div>First Post</div>');
|
|
1516
|
+
expect(result.html).toContain('<div>Second Post</div>');
|
|
1517
|
+
// No container element means no class attribute and no list data attributes
|
|
1518
|
+
expect(result.html).not.toContain('class="');
|
|
1519
|
+
expect(result.html).not.toContain('data-cms-list="true"');
|
|
1520
|
+
expect(result.html).not.toContain('data-collection="blog-posts"');
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
test('should render CMS list as a pure repeater (no container) even when attributes are present', async () => {
|
|
1524
|
+
// Current behavior: list nodes are pure repeaters with no wrapper element,
|
|
1525
|
+
// so node-level attributes are not rendered (set them on a parent node
|
|
1526
|
+
// instead). Previously the list rendered a <div data-cms-list> container
|
|
1527
|
+
// carrying these attributes.
|
|
1476
1528
|
const pageData: JSONPage = {
|
|
1477
1529
|
root: {
|
|
1478
|
-
type:
|
|
1479
|
-
collection:
|
|
1530
|
+
type: 'cms-list',
|
|
1531
|
+
collection: 'blog-posts',
|
|
1480
1532
|
attributes: {
|
|
1481
1533
|
id: 'blog-list',
|
|
1482
1534
|
'data-testid': 'cms-blog',
|
|
1483
1535
|
},
|
|
1484
|
-
children: [
|
|
1485
|
-
|
|
1486
|
-
]
|
|
1487
|
-
} as unknown as ComponentNode
|
|
1536
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
1537
|
+
} as unknown as ComponentNode,
|
|
1488
1538
|
};
|
|
1489
1539
|
|
|
1490
1540
|
const mockCmsService = new CMSService();
|
|
1491
1541
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1492
1542
|
|
|
1493
|
-
const result = await renderPageSSR(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1543
|
+
const result = await renderPageSSR(
|
|
1544
|
+
pageData,
|
|
1545
|
+
{},
|
|
1546
|
+
'/',
|
|
1547
|
+
'',
|
|
1548
|
+
undefined,
|
|
1549
|
+
undefined,
|
|
1550
|
+
undefined,
|
|
1551
|
+
undefined,
|
|
1552
|
+
mockCmsService,
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
// Only the repeated children are emitted — once per item, in order
|
|
1556
|
+
expect(result.html).toContain('<div>First Post</div>');
|
|
1557
|
+
expect(result.html).toContain('<div>Second Post</div>');
|
|
1558
|
+
// No container element means no custom attributes and no list data attributes
|
|
1559
|
+
expect(result.html).not.toContain('id="blog-list"');
|
|
1560
|
+
expect(result.html).not.toContain('data-testid="cms-blog"');
|
|
1561
|
+
expect(result.html).not.toContain('data-cms-list="true"');
|
|
1562
|
+
expect(result.html).not.toContain('data-collection="blog-posts"');
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test('should process item templates in link href', async () => {
|
|
1504
1566
|
const pageData: JSONPage = {
|
|
1505
1567
|
root: {
|
|
1506
|
-
type:
|
|
1507
|
-
collection:
|
|
1568
|
+
type: 'cms-list',
|
|
1569
|
+
collection: 'blog-posts',
|
|
1508
1570
|
children: [
|
|
1509
1571
|
{
|
|
1510
|
-
type:
|
|
1511
|
-
href:
|
|
1512
|
-
children: [
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
]
|
|
1517
|
-
} as unknown as ComponentNode
|
|
1572
|
+
type: 'link',
|
|
1573
|
+
href: '/blog/{{item.slug}}',
|
|
1574
|
+
children: [{ type: 'node', tag: 'span', children: ['{{item.title}}'] }],
|
|
1575
|
+
},
|
|
1576
|
+
],
|
|
1577
|
+
} as unknown as ComponentNode,
|
|
1518
1578
|
};
|
|
1519
1579
|
|
|
1520
1580
|
const mockCmsService = new CMSService();
|
|
1521
1581
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1522
1582
|
|
|
1523
|
-
const result = await renderPageSSR(
|
|
1583
|
+
const result = await renderPageSSR(
|
|
1584
|
+
pageData,
|
|
1585
|
+
{},
|
|
1586
|
+
'/',
|
|
1587
|
+
'',
|
|
1588
|
+
undefined,
|
|
1589
|
+
undefined,
|
|
1590
|
+
undefined,
|
|
1591
|
+
undefined,
|
|
1592
|
+
mockCmsService,
|
|
1593
|
+
);
|
|
1524
1594
|
|
|
1525
1595
|
// Should render link as <a> with processed href
|
|
1526
1596
|
expect(result.html).toContain('href="/blog/post-1"');
|
|
@@ -1529,26 +1599,36 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1529
1599
|
expect(result.html).toContain('Second Post');
|
|
1530
1600
|
});
|
|
1531
1601
|
|
|
1532
|
-
test(
|
|
1602
|
+
test('should process item templates in anchor tag attributes', async () => {
|
|
1533
1603
|
const pageData: JSONPage = {
|
|
1534
1604
|
root: {
|
|
1535
|
-
type:
|
|
1536
|
-
collection:
|
|
1605
|
+
type: 'cms-list',
|
|
1606
|
+
collection: 'blog-posts',
|
|
1537
1607
|
children: [
|
|
1538
1608
|
{
|
|
1539
|
-
type:
|
|
1540
|
-
tag:
|
|
1541
|
-
attributes: { href:
|
|
1542
|
-
children: [
|
|
1543
|
-
}
|
|
1544
|
-
]
|
|
1545
|
-
} as unknown as ComponentNode
|
|
1609
|
+
type: 'node',
|
|
1610
|
+
tag: 'a',
|
|
1611
|
+
attributes: { href: '/blog/{{item.slug}}' },
|
|
1612
|
+
children: ['{{item.title}}'],
|
|
1613
|
+
},
|
|
1614
|
+
],
|
|
1615
|
+
} as unknown as ComponentNode,
|
|
1546
1616
|
};
|
|
1547
1617
|
|
|
1548
1618
|
const mockCmsService = new CMSService();
|
|
1549
1619
|
mockCmsService.queryItems = async () => mockCmsItems;
|
|
1550
1620
|
|
|
1551
|
-
const result = await renderPageSSR(
|
|
1621
|
+
const result = await renderPageSSR(
|
|
1622
|
+
pageData,
|
|
1623
|
+
{},
|
|
1624
|
+
'/',
|
|
1625
|
+
'',
|
|
1626
|
+
undefined,
|
|
1627
|
+
undefined,
|
|
1628
|
+
undefined,
|
|
1629
|
+
undefined,
|
|
1630
|
+
mockCmsService,
|
|
1631
|
+
);
|
|
1552
1632
|
|
|
1553
1633
|
// Should render anchor with processed href from attributes
|
|
1554
1634
|
expect(result.html).toContain('href="/blog/post-1"');
|
|
@@ -1557,7 +1637,7 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1557
1637
|
expect(result.html).toContain('Second Post');
|
|
1558
1638
|
});
|
|
1559
1639
|
|
|
1560
|
-
test(
|
|
1640
|
+
test('should resolve items from cms context using {{cms.field}} template', async () => {
|
|
1561
1641
|
// This tests nested cms-list in CMS template pages where items
|
|
1562
1642
|
// come from a reference field like {{cms.category}}
|
|
1563
1643
|
const mockCategories: CMSItem[] = [
|
|
@@ -1567,104 +1647,122 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1567
1647
|
|
|
1568
1648
|
const pageData: JSONPage = {
|
|
1569
1649
|
root: {
|
|
1570
|
-
type:
|
|
1571
|
-
collection:
|
|
1572
|
-
items:
|
|
1573
|
-
itemAs:
|
|
1574
|
-
children: [
|
|
1575
|
-
|
|
1576
|
-
]
|
|
1577
|
-
} as unknown as ComponentNode
|
|
1650
|
+
type: 'cms-list',
|
|
1651
|
+
collection: 'categories',
|
|
1652
|
+
items: '{{cms.categoryIds}}', // Reference field from CMS context
|
|
1653
|
+
itemAs: 'category',
|
|
1654
|
+
children: [{ type: 'node', tag: 'span', children: ['{{category.title}}'] }],
|
|
1655
|
+
} as unknown as ComponentNode,
|
|
1578
1656
|
};
|
|
1579
1657
|
|
|
1580
1658
|
const mockCmsService = new CMSService();
|
|
1581
|
-
mockCmsService.getItemsByIds = async (
|
|
1659
|
+
mockCmsService.getItemsByIds = async (_collection: string, ids: string[]) => {
|
|
1582
1660
|
// Return matching categories
|
|
1583
|
-
return mockCategories.filter(c => ids.includes(c._id));
|
|
1661
|
+
return mockCategories.filter((c) => ids.includes(c._id));
|
|
1584
1662
|
};
|
|
1585
1663
|
|
|
1586
1664
|
// CMS context has reference field with category IDs
|
|
1587
1665
|
const cmsContext = { cms: { categoryIds: ['cat-1', 'cat-2'] } };
|
|
1588
1666
|
|
|
1589
|
-
const result = await renderPageSSR(
|
|
1667
|
+
const result = await renderPageSSR(
|
|
1668
|
+
pageData,
|
|
1669
|
+
{},
|
|
1670
|
+
'/',
|
|
1671
|
+
'',
|
|
1672
|
+
undefined,
|
|
1673
|
+
undefined,
|
|
1674
|
+
undefined,
|
|
1675
|
+
cmsContext,
|
|
1676
|
+
mockCmsService,
|
|
1677
|
+
);
|
|
1590
1678
|
|
|
1591
1679
|
// Should render categories from resolved reference field
|
|
1592
1680
|
expect(result.html).toContain('Technology');
|
|
1593
1681
|
expect(result.html).toContain('Design');
|
|
1594
1682
|
});
|
|
1595
1683
|
|
|
1596
|
-
test(
|
|
1684
|
+
test('should resolve single item from cms context using {{cms.field}} template', async () => {
|
|
1597
1685
|
// Test single reference (not array)
|
|
1598
|
-
const mockCategories: CMSItem[] = [
|
|
1599
|
-
{ _id: 'cat-1', slug: 'tech', title: 'Technology' },
|
|
1600
|
-
];
|
|
1686
|
+
const mockCategories: CMSItem[] = [{ _id: 'cat-1', slug: 'tech', title: 'Technology' }];
|
|
1601
1687
|
|
|
1602
1688
|
const pageData: JSONPage = {
|
|
1603
1689
|
root: {
|
|
1604
|
-
type:
|
|
1605
|
-
collection:
|
|
1606
|
-
items:
|
|
1607
|
-
itemAs:
|
|
1608
|
-
children: [
|
|
1609
|
-
|
|
1610
|
-
]
|
|
1611
|
-
} as unknown as ComponentNode
|
|
1690
|
+
type: 'cms-list',
|
|
1691
|
+
collection: 'categories',
|
|
1692
|
+
items: '{{cms.category}}', // Single reference field
|
|
1693
|
+
itemAs: 'category',
|
|
1694
|
+
children: [{ type: 'node', tag: 'span', children: ['{{category.title}}'] }],
|
|
1695
|
+
} as unknown as ComponentNode,
|
|
1612
1696
|
};
|
|
1613
1697
|
|
|
1614
1698
|
const mockCmsService = new CMSService();
|
|
1615
|
-
mockCmsService.getItemsByIds = async (
|
|
1616
|
-
return mockCategories.filter(c => ids.includes(c._id));
|
|
1699
|
+
mockCmsService.getItemsByIds = async (_collection: string, ids: string[]) => {
|
|
1700
|
+
return mockCategories.filter((c) => ids.includes(c._id));
|
|
1617
1701
|
};
|
|
1618
1702
|
|
|
1619
1703
|
// CMS context has single reference (string, not array)
|
|
1620
1704
|
const cmsContext = { cms: { category: 'cat-1' } };
|
|
1621
1705
|
|
|
1622
|
-
const result = await renderPageSSR(
|
|
1706
|
+
const result = await renderPageSSR(
|
|
1707
|
+
pageData,
|
|
1708
|
+
{},
|
|
1709
|
+
'/',
|
|
1710
|
+
'',
|
|
1711
|
+
undefined,
|
|
1712
|
+
undefined,
|
|
1713
|
+
undefined,
|
|
1714
|
+
cmsContext,
|
|
1715
|
+
mockCmsService,
|
|
1716
|
+
);
|
|
1623
1717
|
|
|
1624
1718
|
expect(result.html).toContain('Technology');
|
|
1625
1719
|
});
|
|
1626
1720
|
|
|
1627
|
-
test(
|
|
1721
|
+
test('should resolve nested cms field path using {{cms.nested.field}} template', async () => {
|
|
1628
1722
|
// Test nested path resolution like {{cms.author.categories}}
|
|
1629
|
-
const mockCategories: CMSItem[] = [
|
|
1630
|
-
{ _id: 'cat-1', slug: 'tech', title: 'Technology' },
|
|
1631
|
-
];
|
|
1723
|
+
const mockCategories: CMSItem[] = [{ _id: 'cat-1', slug: 'tech', title: 'Technology' }];
|
|
1632
1724
|
|
|
1633
1725
|
const pageData: JSONPage = {
|
|
1634
1726
|
root: {
|
|
1635
|
-
type:
|
|
1636
|
-
collection:
|
|
1637
|
-
items:
|
|
1638
|
-
itemAs:
|
|
1639
|
-
children: [
|
|
1640
|
-
|
|
1641
|
-
]
|
|
1642
|
-
} as unknown as ComponentNode
|
|
1727
|
+
type: 'cms-list',
|
|
1728
|
+
collection: 'categories',
|
|
1729
|
+
items: '{{cms.author.categories}}',
|
|
1730
|
+
itemAs: 'category',
|
|
1731
|
+
children: [{ type: 'node', tag: 'span', children: ['{{category.title}}'] }],
|
|
1732
|
+
} as unknown as ComponentNode,
|
|
1643
1733
|
};
|
|
1644
1734
|
|
|
1645
1735
|
const mockCmsService = new CMSService();
|
|
1646
|
-
mockCmsService.getItemsByIds = async (
|
|
1647
|
-
return mockCategories.filter(c => ids.includes(c._id));
|
|
1736
|
+
mockCmsService.getItemsByIds = async (_collection: string, ids: string[]) => {
|
|
1737
|
+
return mockCategories.filter((c) => ids.includes(c._id));
|
|
1648
1738
|
};
|
|
1649
1739
|
|
|
1650
1740
|
// CMS context has nested structure
|
|
1651
1741
|
const cmsContext = { cms: { author: { categories: ['cat-1'] } } };
|
|
1652
1742
|
|
|
1653
|
-
const result = await renderPageSSR(
|
|
1743
|
+
const result = await renderPageSSR(
|
|
1744
|
+
pageData,
|
|
1745
|
+
{},
|
|
1746
|
+
'/',
|
|
1747
|
+
'',
|
|
1748
|
+
undefined,
|
|
1749
|
+
undefined,
|
|
1750
|
+
undefined,
|
|
1751
|
+
cmsContext,
|
|
1752
|
+
mockCmsService,
|
|
1753
|
+
);
|
|
1654
1754
|
|
|
1655
1755
|
expect(result.html).toContain('Technology');
|
|
1656
1756
|
});
|
|
1657
1757
|
|
|
1658
|
-
test(
|
|
1758
|
+
test('should return empty when cms field is missing', async () => {
|
|
1659
1759
|
const pageData: JSONPage = {
|
|
1660
1760
|
root: {
|
|
1661
|
-
type:
|
|
1662
|
-
collection:
|
|
1663
|
-
items:
|
|
1664
|
-
children: [
|
|
1665
|
-
|
|
1666
|
-
]
|
|
1667
|
-
} as unknown as ComponentNode
|
|
1761
|
+
type: 'cms-list',
|
|
1762
|
+
collection: 'categories',
|
|
1763
|
+
items: '{{cms.nonexistent}}',
|
|
1764
|
+
children: [{ type: 'node', tag: 'span', children: ['{{item.title}}'] }],
|
|
1765
|
+
} as unknown as ComponentNode,
|
|
1668
1766
|
};
|
|
1669
1767
|
|
|
1670
1768
|
const mockCmsService = new CMSService();
|
|
@@ -1673,80 +1771,84 @@ describe("SSR Renderer - CMSList rendering", () => {
|
|
|
1673
1771
|
// CMS context without the referenced field
|
|
1674
1772
|
const cmsContext = { cms: { title: 'Page Title' } };
|
|
1675
1773
|
|
|
1676
|
-
const result = await renderPageSSR(
|
|
1774
|
+
const result = await renderPageSSR(
|
|
1775
|
+
pageData,
|
|
1776
|
+
{},
|
|
1777
|
+
'/',
|
|
1778
|
+
'',
|
|
1779
|
+
undefined,
|
|
1780
|
+
undefined,
|
|
1781
|
+
undefined,
|
|
1782
|
+
cmsContext,
|
|
1783
|
+
mockCmsService,
|
|
1784
|
+
);
|
|
1677
1785
|
|
|
1678
1786
|
// Should return empty (no items to render)
|
|
1679
1787
|
expect(result.html).toBe('');
|
|
1680
1788
|
});
|
|
1681
1789
|
});
|
|
1682
1790
|
|
|
1683
|
-
describe(
|
|
1791
|
+
describe('SSR Renderer - Interactive Styles', () => {
|
|
1684
1792
|
/**
|
|
1685
1793
|
* Tests for interactive styles (hover, active, etc.) in SSR rendering.
|
|
1686
1794
|
* Verifies that element classes are generated correctly for both
|
|
1687
1795
|
* page elements and component elements.
|
|
1688
1796
|
*/
|
|
1689
1797
|
|
|
1690
|
-
describe(
|
|
1691
|
-
test(
|
|
1798
|
+
describe('Page element interactive styles', () => {
|
|
1799
|
+
test('should add element class for interactive styles', async () => {
|
|
1692
1800
|
const pageData: JSONPage = {
|
|
1693
1801
|
root: {
|
|
1694
|
-
type:
|
|
1695
|
-
tag:
|
|
1696
|
-
label:
|
|
1697
|
-
interactiveStyles: [
|
|
1698
|
-
|
|
1699
|
-
],
|
|
1700
|
-
children: ["Submit"],
|
|
1802
|
+
type: 'node',
|
|
1803
|
+
tag: 'button',
|
|
1804
|
+
label: 'submit-btn',
|
|
1805
|
+
interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
|
|
1806
|
+
children: ['Submit'],
|
|
1701
1807
|
},
|
|
1702
1808
|
};
|
|
1703
1809
|
|
|
1704
|
-
const result = await renderPageSSR(pageData, {},
|
|
1810
|
+
const result = await renderPageSSR(pageData, {}, '/contact', '');
|
|
1705
1811
|
|
|
1706
1812
|
// Should have element class in HTML
|
|
1707
|
-
expect(result.html).toContain(
|
|
1813
|
+
expect(result.html).toContain('class=');
|
|
1708
1814
|
// Page elements should have p_ prefix
|
|
1709
|
-
expect(result.html).toContain(
|
|
1815
|
+
expect(result.html).toContain('p_contact_submit-btn');
|
|
1710
1816
|
});
|
|
1711
1817
|
|
|
1712
|
-
test(
|
|
1818
|
+
test('should generate interactive CSS for page elements', async () => {
|
|
1713
1819
|
const pageData: JSONPage = {
|
|
1714
1820
|
root: {
|
|
1715
|
-
type:
|
|
1716
|
-
tag:
|
|
1717
|
-
label:
|
|
1718
|
-
interactiveStyles: [
|
|
1719
|
-
|
|
1720
|
-
],
|
|
1721
|
-
children: ["Card content"],
|
|
1821
|
+
type: 'node',
|
|
1822
|
+
tag: 'div',
|
|
1823
|
+
label: 'card',
|
|
1824
|
+
interactiveStyles: [{ postfix: ':hover', style: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' } }],
|
|
1825
|
+
children: ['Card content'],
|
|
1722
1826
|
},
|
|
1723
1827
|
};
|
|
1724
1828
|
|
|
1725
|
-
const result = await generateSSRHTML(pageData, {},
|
|
1829
|
+
const result = await generateSSRHTML(pageData, {}, '/about');
|
|
1726
1830
|
|
|
1727
1831
|
// Generated CSS should include interactive styles
|
|
1728
1832
|
// generateSSRHTML returns the full HTML string directly
|
|
1729
|
-
expect(result).toContain(
|
|
1833
|
+
expect(result).toContain('p_about_card');
|
|
1730
1834
|
});
|
|
1731
1835
|
|
|
1732
|
-
test(
|
|
1836
|
+
test('should generate correct class for nested page elements', async () => {
|
|
1733
1837
|
const pageData: JSONPage = {
|
|
1734
1838
|
root: {
|
|
1735
|
-
type:
|
|
1736
|
-
tag:
|
|
1839
|
+
type: 'node',
|
|
1840
|
+
tag: 'div',
|
|
1737
1841
|
children: [
|
|
1738
1842
|
{
|
|
1739
|
-
type:
|
|
1740
|
-
tag:
|
|
1843
|
+
type: 'node',
|
|
1844
|
+
tag: 'div',
|
|
1741
1845
|
children: [
|
|
1742
1846
|
{
|
|
1743
|
-
type:
|
|
1744
|
-
tag:
|
|
1745
|
-
label:
|
|
1746
|
-
interactiveStyles: [
|
|
1747
|
-
|
|
1748
|
-
],
|
|
1749
|
-
children: ["Click"],
|
|
1847
|
+
type: 'node',
|
|
1848
|
+
tag: 'button',
|
|
1849
|
+
label: 'nested-btn',
|
|
1850
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1851
|
+
children: ['Click'],
|
|
1750
1852
|
},
|
|
1751
1853
|
],
|
|
1752
1854
|
},
|
|
@@ -1754,295 +1856,291 @@ describe("SSR Renderer - Interactive Styles", () => {
|
|
|
1754
1856
|
},
|
|
1755
1857
|
};
|
|
1756
1858
|
|
|
1757
|
-
const result = await renderPageSSR(pageData, {},
|
|
1859
|
+
const result = await renderPageSSR(pageData, {}, '/services', '');
|
|
1758
1860
|
|
|
1759
1861
|
// Nested element should have correct page class
|
|
1760
|
-
expect(result.html).toContain(
|
|
1862
|
+
expect(result.html).toContain('p_services_nested-btn');
|
|
1761
1863
|
});
|
|
1762
1864
|
|
|
1763
|
-
test(
|
|
1865
|
+
test('should handle multiple interactive states', async () => {
|
|
1764
1866
|
const pageData: JSONPage = {
|
|
1765
1867
|
root: {
|
|
1766
|
-
type:
|
|
1767
|
-
tag:
|
|
1768
|
-
label:
|
|
1868
|
+
type: 'node',
|
|
1869
|
+
tag: 'a',
|
|
1870
|
+
label: 'nav-link',
|
|
1769
1871
|
interactiveStyles: [
|
|
1770
|
-
{ postfix:
|
|
1771
|
-
{ postfix:
|
|
1772
|
-
{ postfix:
|
|
1872
|
+
{ postfix: ':hover', style: { color: 'primary' } },
|
|
1873
|
+
{ postfix: ':active', style: { color: 'primary-dark' } },
|
|
1874
|
+
{ postfix: ':focus', style: { outline: '2px solid var(--primary)' } },
|
|
1773
1875
|
],
|
|
1774
|
-
children: [
|
|
1876
|
+
children: ['Link'],
|
|
1775
1877
|
},
|
|
1776
1878
|
};
|
|
1777
1879
|
|
|
1778
|
-
const result = await renderPageSSR(pageData, {},
|
|
1880
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1779
1881
|
|
|
1780
1882
|
// Should have single class for all states
|
|
1781
|
-
expect(result.html).toContain(
|
|
1883
|
+
expect(result.html).toContain('p_index_nav-link');
|
|
1782
1884
|
});
|
|
1783
1885
|
});
|
|
1784
1886
|
|
|
1785
|
-
describe(
|
|
1786
|
-
test(
|
|
1887
|
+
describe('Responsive interactive styles', () => {
|
|
1888
|
+
test('should handle responsive hover styles', async () => {
|
|
1787
1889
|
const pageData: JSONPage = {
|
|
1788
1890
|
root: {
|
|
1789
|
-
type:
|
|
1790
|
-
tag:
|
|
1791
|
-
label:
|
|
1891
|
+
type: 'node',
|
|
1892
|
+
tag: 'div',
|
|
1893
|
+
label: 'responsive-card',
|
|
1792
1894
|
interactiveStyles: [
|
|
1793
1895
|
{
|
|
1794
|
-
postfix:
|
|
1896
|
+
postfix: ':hover',
|
|
1795
1897
|
style: {
|
|
1796
|
-
base: { padding:
|
|
1797
|
-
tablet: { padding:
|
|
1798
|
-
mobile: { padding:
|
|
1898
|
+
base: { padding: '20px' },
|
|
1899
|
+
tablet: { padding: '16px' },
|
|
1900
|
+
mobile: { padding: '12px' },
|
|
1799
1901
|
},
|
|
1800
1902
|
},
|
|
1801
1903
|
],
|
|
1802
|
-
children: [
|
|
1904
|
+
children: ['Responsive content'],
|
|
1803
1905
|
},
|
|
1804
1906
|
};
|
|
1805
1907
|
|
|
1806
|
-
const result = await renderPageSSR(pageData, {},
|
|
1908
|
+
const result = await renderPageSSR(pageData, {}, '/test', '');
|
|
1807
1909
|
|
|
1808
1910
|
// Should have element class
|
|
1809
|
-
expect(result.html).toContain(
|
|
1911
|
+
expect(result.html).toContain('p_test_responsive-card');
|
|
1810
1912
|
});
|
|
1811
1913
|
});
|
|
1812
1914
|
|
|
1813
|
-
describe(
|
|
1814
|
-
test(
|
|
1915
|
+
describe('Interactive styles with generateElementClass flag', () => {
|
|
1916
|
+
test('should add element class when generateElementClass is true', async () => {
|
|
1815
1917
|
const pageData: JSONPage = {
|
|
1816
1918
|
root: {
|
|
1817
|
-
type:
|
|
1818
|
-
tag:
|
|
1819
|
-
label:
|
|
1919
|
+
type: 'node',
|
|
1920
|
+
tag: 'div',
|
|
1921
|
+
label: 'target-element',
|
|
1820
1922
|
generateElementClass: true,
|
|
1821
|
-
children: [
|
|
1923
|
+
children: ['Has class flag'],
|
|
1822
1924
|
},
|
|
1823
1925
|
};
|
|
1824
1926
|
|
|
1825
|
-
const result = await renderPageSSR(pageData, {},
|
|
1927
|
+
const result = await renderPageSSR(pageData, {}, '/page', '');
|
|
1826
1928
|
|
|
1827
1929
|
// Should have element class even without interactive styles
|
|
1828
|
-
expect(result.html).toContain(
|
|
1930
|
+
expect(result.html).toContain('p_page_target-element');
|
|
1829
1931
|
});
|
|
1830
1932
|
});
|
|
1831
1933
|
|
|
1832
|
-
describe(
|
|
1833
|
-
test(
|
|
1934
|
+
describe('Edge cases', () => {
|
|
1935
|
+
test('should not add element class when no interactive styles or flag', async () => {
|
|
1834
1936
|
const pageData: JSONPage = {
|
|
1835
1937
|
root: {
|
|
1836
|
-
type:
|
|
1837
|
-
tag:
|
|
1838
|
-
children: [
|
|
1938
|
+
type: 'node',
|
|
1939
|
+
tag: 'div',
|
|
1940
|
+
children: ['Plain element'],
|
|
1839
1941
|
},
|
|
1840
1942
|
};
|
|
1841
1943
|
|
|
1842
|
-
const result = await renderPageSSR(pageData, {},
|
|
1944
|
+
const result = await renderPageSSR(pageData, {}, '/plain', '');
|
|
1843
1945
|
|
|
1844
1946
|
// Should not have p_ prefixed class
|
|
1845
|
-
expect(result.html).not.toContain(
|
|
1947
|
+
expect(result.html).not.toContain('p_plain_');
|
|
1846
1948
|
});
|
|
1847
1949
|
|
|
1848
|
-
test(
|
|
1950
|
+
test('should handle empty interactive styles array', async () => {
|
|
1849
1951
|
const pageData: JSONPage = {
|
|
1850
1952
|
root: {
|
|
1851
|
-
type:
|
|
1852
|
-
tag:
|
|
1953
|
+
type: 'node',
|
|
1954
|
+
tag: 'div',
|
|
1853
1955
|
interactiveStyles: [],
|
|
1854
|
-
children: [
|
|
1956
|
+
children: ['Empty styles'],
|
|
1855
1957
|
},
|
|
1856
1958
|
};
|
|
1857
1959
|
|
|
1858
|
-
const result = await renderPageSSR(pageData, {},
|
|
1960
|
+
const result = await renderPageSSR(pageData, {}, '/empty', '');
|
|
1859
1961
|
|
|
1860
1962
|
// Empty array should not add class
|
|
1861
|
-
expect(result.html).not.toContain(
|
|
1963
|
+
expect(result.html).not.toContain('p_empty_');
|
|
1862
1964
|
});
|
|
1863
1965
|
|
|
1864
|
-
test(
|
|
1966
|
+
test('should handle index page path', async () => {
|
|
1865
1967
|
const pageData: JSONPage = {
|
|
1866
1968
|
root: {
|
|
1867
|
-
type:
|
|
1868
|
-
tag:
|
|
1869
|
-
label:
|
|
1870
|
-
interactiveStyles: [
|
|
1871
|
-
|
|
1872
|
-
],
|
|
1873
|
-
children: ["Get Started"],
|
|
1969
|
+
type: 'node',
|
|
1970
|
+
tag: 'button',
|
|
1971
|
+
label: 'hero-cta',
|
|
1972
|
+
interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
|
|
1973
|
+
children: ['Get Started'],
|
|
1874
1974
|
},
|
|
1875
1975
|
};
|
|
1876
1976
|
|
|
1877
|
-
const result = await renderPageSSR(pageData, {},
|
|
1977
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1878
1978
|
|
|
1879
1979
|
// Root path "/" should use "index" as file name
|
|
1880
|
-
expect(result.html).toContain(
|
|
1980
|
+
expect(result.html).toContain('p_index_hero-cta');
|
|
1881
1981
|
});
|
|
1882
1982
|
|
|
1883
|
-
test(
|
|
1983
|
+
test('should handle nested page paths', async () => {
|
|
1884
1984
|
const pageData: JSONPage = {
|
|
1885
1985
|
root: {
|
|
1886
|
-
type:
|
|
1887
|
-
tag:
|
|
1888
|
-
label:
|
|
1889
|
-
interactiveStyles: [
|
|
1890
|
-
|
|
1891
|
-
],
|
|
1892
|
-
children: ["Blog post"],
|
|
1986
|
+
type: 'node',
|
|
1987
|
+
tag: 'div',
|
|
1988
|
+
label: 'blog-card',
|
|
1989
|
+
interactiveStyles: [{ postfix: ':hover', style: { transform: 'translateY(-2px)' } }],
|
|
1990
|
+
children: ['Blog post'],
|
|
1893
1991
|
},
|
|
1894
1992
|
};
|
|
1895
1993
|
|
|
1896
|
-
const result = await renderPageSSR(pageData, {},
|
|
1994
|
+
const result = await renderPageSSR(pageData, {}, '/blog/posts', '');
|
|
1897
1995
|
|
|
1898
1996
|
// Nested path should be sanitized (slashes become underscores)
|
|
1899
|
-
expect(result.html).toContain(
|
|
1997
|
+
expect(result.html).toContain('p_blog_posts_blog-card');
|
|
1900
1998
|
});
|
|
1901
1999
|
});
|
|
1902
2000
|
|
|
1903
|
-
describe(
|
|
1904
|
-
test(
|
|
2001
|
+
describe('SSR Renderer - Conditional Rendering (if property)', () => {
|
|
2002
|
+
test('should render node when if is true', async () => {
|
|
1905
2003
|
const pageData: JSONPage = {
|
|
1906
2004
|
root: {
|
|
1907
|
-
type:
|
|
1908
|
-
tag:
|
|
2005
|
+
type: 'node',
|
|
2006
|
+
tag: 'div',
|
|
1909
2007
|
if: true,
|
|
1910
|
-
children: [
|
|
2008
|
+
children: ['Visible content'],
|
|
1911
2009
|
} as any,
|
|
1912
2010
|
};
|
|
1913
2011
|
|
|
1914
|
-
const result = await renderPageSSR(pageData, {},
|
|
2012
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1915
2013
|
|
|
1916
|
-
expect(result.html).toContain(
|
|
2014
|
+
expect(result.html).toContain('Visible content');
|
|
1917
2015
|
});
|
|
1918
2016
|
|
|
1919
|
-
test(
|
|
2017
|
+
test('should skip node when if is false', async () => {
|
|
1920
2018
|
const pageData: JSONPage = {
|
|
1921
2019
|
root: {
|
|
1922
|
-
type:
|
|
1923
|
-
tag:
|
|
2020
|
+
type: 'node',
|
|
2021
|
+
tag: 'div',
|
|
1924
2022
|
if: false,
|
|
1925
|
-
children: [
|
|
2023
|
+
children: ['Hidden content'],
|
|
1926
2024
|
} as any,
|
|
1927
2025
|
};
|
|
1928
2026
|
|
|
1929
|
-
const result = await renderPageSSR(pageData, {},
|
|
2027
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1930
2028
|
|
|
1931
|
-
expect(result.html).not.toContain(
|
|
1932
|
-
expect(result.html).toBe(
|
|
2029
|
+
expect(result.html).not.toContain('Hidden content');
|
|
2030
|
+
expect(result.html).toBe('');
|
|
1933
2031
|
});
|
|
1934
2032
|
|
|
1935
|
-
test(
|
|
2033
|
+
test('should render node when if property is absent', async () => {
|
|
1936
2034
|
const pageData: JSONPage = {
|
|
1937
2035
|
root: {
|
|
1938
|
-
type:
|
|
1939
|
-
tag:
|
|
1940
|
-
children: [
|
|
2036
|
+
type: 'node',
|
|
2037
|
+
tag: 'div',
|
|
2038
|
+
children: ['Default visible'],
|
|
1941
2039
|
},
|
|
1942
2040
|
};
|
|
1943
2041
|
|
|
1944
|
-
const result = await renderPageSSR(pageData, {},
|
|
2042
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1945
2043
|
|
|
1946
|
-
expect(result.html).toContain(
|
|
2044
|
+
expect(result.html).toContain('Default visible');
|
|
1947
2045
|
});
|
|
1948
2046
|
|
|
1949
|
-
test(
|
|
2047
|
+
test('should skip children when parent if is false', async () => {
|
|
1950
2048
|
const pageData: JSONPage = {
|
|
1951
2049
|
root: {
|
|
1952
|
-
type:
|
|
1953
|
-
tag:
|
|
2050
|
+
type: 'node',
|
|
2051
|
+
tag: 'div',
|
|
1954
2052
|
if: false,
|
|
1955
2053
|
children: [
|
|
1956
|
-
{ type:
|
|
1957
|
-
{ type:
|
|
2054
|
+
{ type: 'node', tag: 'span', children: ['Child 1'] },
|
|
2055
|
+
{ type: 'node', tag: 'span', children: ['Child 2'] },
|
|
1958
2056
|
],
|
|
1959
2057
|
} as any,
|
|
1960
2058
|
};
|
|
1961
2059
|
|
|
1962
|
-
const result = await renderPageSSR(pageData, {},
|
|
2060
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1963
2061
|
|
|
1964
|
-
expect(result.html).not.toContain(
|
|
1965
|
-
expect(result.html).not.toContain(
|
|
2062
|
+
expect(result.html).not.toContain('Child 1');
|
|
2063
|
+
expect(result.html).not.toContain('Child 2');
|
|
1966
2064
|
});
|
|
1967
2065
|
|
|
1968
|
-
test(
|
|
2066
|
+
test('should handle if with embed nodes', async () => {
|
|
1969
2067
|
const pageData: JSONPage = {
|
|
1970
2068
|
root: {
|
|
1971
|
-
type:
|
|
1972
|
-
html:
|
|
2069
|
+
type: 'embed',
|
|
2070
|
+
html: '<svg></svg>',
|
|
1973
2071
|
if: false,
|
|
1974
2072
|
} as any,
|
|
1975
2073
|
};
|
|
1976
2074
|
|
|
1977
|
-
const result = await renderPageSSR(pageData, {},
|
|
2075
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1978
2076
|
|
|
1979
|
-
expect(result.html).not.toContain(
|
|
2077
|
+
expect(result.html).not.toContain('<svg>');
|
|
1980
2078
|
});
|
|
1981
2079
|
|
|
1982
|
-
test(
|
|
2080
|
+
test('should handle if with link nodes', async () => {
|
|
1983
2081
|
const pageData: JSONPage = {
|
|
1984
2082
|
root: {
|
|
1985
|
-
type:
|
|
1986
|
-
href:
|
|
2083
|
+
type: 'link',
|
|
2084
|
+
href: '/page',
|
|
1987
2085
|
if: false,
|
|
1988
|
-
children: [
|
|
2086
|
+
children: ['Hidden link'],
|
|
1989
2087
|
} as any,
|
|
1990
2088
|
};
|
|
1991
2089
|
|
|
1992
|
-
const result = await renderPageSSR(pageData, {},
|
|
2090
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
1993
2091
|
|
|
1994
|
-
expect(result.html).not.toContain(
|
|
2092
|
+
expect(result.html).not.toContain('Hidden link');
|
|
1995
2093
|
});
|
|
1996
2094
|
|
|
1997
|
-
test(
|
|
2095
|
+
test('should handle if with locale-list nodes', async () => {
|
|
1998
2096
|
const pageData: JSONPage = {
|
|
1999
2097
|
root: {
|
|
2000
|
-
type:
|
|
2098
|
+
type: 'locale-list',
|
|
2001
2099
|
if: false,
|
|
2002
2100
|
} as any,
|
|
2003
2101
|
};
|
|
2004
2102
|
|
|
2005
|
-
const result = await renderPageSSR(pageData, {},
|
|
2103
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
2006
2104
|
|
|
2007
|
-
expect(result.html).not.toContain(
|
|
2105
|
+
expect(result.html).not.toContain('locale-list');
|
|
2008
2106
|
});
|
|
2009
2107
|
|
|
2010
|
-
test(
|
|
2108
|
+
test('should render conditional child nodes independently', async () => {
|
|
2011
2109
|
const pageData: JSONPage = {
|
|
2012
2110
|
root: {
|
|
2013
|
-
type:
|
|
2014
|
-
tag:
|
|
2111
|
+
type: 'node',
|
|
2112
|
+
tag: 'div',
|
|
2015
2113
|
children: [
|
|
2016
|
-
{ type:
|
|
2017
|
-
{ type:
|
|
2018
|
-
{ type:
|
|
2114
|
+
{ type: 'node', tag: 'span', if: true, children: ['Visible'] } as any,
|
|
2115
|
+
{ type: 'node', tag: 'span', if: false, children: ['Hidden'] } as any,
|
|
2116
|
+
{ type: 'node', tag: 'span', children: ['Always visible'] },
|
|
2019
2117
|
],
|
|
2020
2118
|
},
|
|
2021
2119
|
};
|
|
2022
2120
|
|
|
2023
|
-
const result = await renderPageSSR(pageData, {},
|
|
2121
|
+
const result = await renderPageSSR(pageData, {}, '/', '');
|
|
2024
2122
|
|
|
2025
|
-
expect(result.html).toContain(
|
|
2026
|
-
expect(result.html).not.toContain(
|
|
2027
|
-
expect(result.html).toContain(
|
|
2123
|
+
expect(result.html).toContain('Visible');
|
|
2124
|
+
expect(result.html).not.toContain('Hidden');
|
|
2125
|
+
expect(result.html).toContain('Always visible');
|
|
2028
2126
|
});
|
|
2029
2127
|
|
|
2030
|
-
test(
|
|
2128
|
+
test('should handle if mapping with component props in SSR', async () => {
|
|
2031
2129
|
const components: Record<string, ComponentDefinition> = {
|
|
2032
2130
|
ConditionalBanner: {
|
|
2033
2131
|
component: {
|
|
2034
2132
|
interface: {
|
|
2035
|
-
showBanner: { type:
|
|
2133
|
+
showBanner: { type: 'boolean', default: false },
|
|
2036
2134
|
},
|
|
2037
2135
|
structure: {
|
|
2038
|
-
type:
|
|
2039
|
-
tag:
|
|
2136
|
+
type: 'node',
|
|
2137
|
+
tag: 'div',
|
|
2040
2138
|
if: {
|
|
2041
2139
|
_mapping: true,
|
|
2042
|
-
prop:
|
|
2043
|
-
values: {
|
|
2140
|
+
prop: 'showBanner',
|
|
2141
|
+
values: { true: true, false: false },
|
|
2044
2142
|
},
|
|
2045
|
-
children: [
|
|
2143
|
+
children: ['Banner content'],
|
|
2046
2144
|
} as any,
|
|
2047
2145
|
},
|
|
2048
2146
|
},
|
|
@@ -2051,32 +2149,32 @@ describe("SSR Renderer - Interactive Styles", () => {
|
|
|
2051
2149
|
// Test with showBanner: true
|
|
2052
2150
|
const pageDataVisible: JSONPage = {
|
|
2053
2151
|
root: {
|
|
2054
|
-
type:
|
|
2055
|
-
component:
|
|
2152
|
+
type: 'component',
|
|
2153
|
+
component: 'ConditionalBanner',
|
|
2056
2154
|
props: { showBanner: true },
|
|
2057
2155
|
} as any,
|
|
2058
2156
|
};
|
|
2059
2157
|
|
|
2060
|
-
const resultVisible = await renderPageSSR(pageDataVisible, components,
|
|
2061
|
-
expect(resultVisible.html).toContain(
|
|
2158
|
+
const resultVisible = await renderPageSSR(pageDataVisible, components, '/', '');
|
|
2159
|
+
expect(resultVisible.html).toContain('Banner content');
|
|
2062
2160
|
|
|
2063
2161
|
// Test with showBanner: false
|
|
2064
2162
|
const pageDataHidden: JSONPage = {
|
|
2065
2163
|
root: {
|
|
2066
|
-
type:
|
|
2067
|
-
component:
|
|
2164
|
+
type: 'component',
|
|
2165
|
+
component: 'ConditionalBanner',
|
|
2068
2166
|
props: { showBanner: false },
|
|
2069
2167
|
} as any,
|
|
2070
2168
|
};
|
|
2071
2169
|
|
|
2072
|
-
const resultHidden = await renderPageSSR(pageDataHidden, components,
|
|
2073
|
-
expect(resultHidden.html).not.toContain(
|
|
2170
|
+
const resultHidden = await renderPageSSR(pageDataHidden, components, '/', '');
|
|
2171
|
+
expect(resultHidden.html).not.toContain('Banner content');
|
|
2074
2172
|
});
|
|
2075
2173
|
});
|
|
2076
2174
|
});
|
|
2077
2175
|
|
|
2078
|
-
describe(
|
|
2079
|
-
test(
|
|
2176
|
+
describe('SSR Renderer - List node rendering', () => {
|
|
2177
|
+
test('should render children for each item in list prop', async () => {
|
|
2080
2178
|
// Component with list prop definition
|
|
2081
2179
|
const components: Record<string, ComponentDefinition> = {
|
|
2082
2180
|
ItemsList: {
|
|
@@ -2085,34 +2183,28 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2085
2183
|
items: {
|
|
2086
2184
|
type: 'list',
|
|
2087
2185
|
itemSchema: {
|
|
2088
|
-
title: { type: 'string' }
|
|
2186
|
+
title: { type: 'string' },
|
|
2089
2187
|
},
|
|
2090
|
-
default: []
|
|
2091
|
-
}
|
|
2188
|
+
default: [],
|
|
2189
|
+
},
|
|
2092
2190
|
},
|
|
2093
2191
|
structure: {
|
|
2094
|
-
type:
|
|
2095
|
-
source:
|
|
2096
|
-
children: [
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2192
|
+
type: 'list',
|
|
2193
|
+
source: 'items',
|
|
2194
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
2195
|
+
} as any,
|
|
2196
|
+
},
|
|
2197
|
+
},
|
|
2102
2198
|
};
|
|
2103
2199
|
|
|
2104
2200
|
const pageData: JSONPage = {
|
|
2105
2201
|
root: {
|
|
2106
|
-
type:
|
|
2107
|
-
component:
|
|
2202
|
+
type: 'component',
|
|
2203
|
+
component: 'ItemsList',
|
|
2108
2204
|
props: {
|
|
2109
|
-
items: [
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
{ title: "Third Item" }
|
|
2113
|
-
]
|
|
2114
|
-
}
|
|
2115
|
-
} as any
|
|
2205
|
+
items: [{ title: 'First Item' }, { title: 'Second Item' }, { title: 'Third Item' }],
|
|
2206
|
+
},
|
|
2207
|
+
} as any,
|
|
2116
2208
|
};
|
|
2117
2209
|
|
|
2118
2210
|
const result = await renderPageSSR(pageData, components, '/', '');
|
|
@@ -2122,7 +2214,7 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2122
2214
|
expect(result.html).toContain('Third Item');
|
|
2123
2215
|
});
|
|
2124
2216
|
|
|
2125
|
-
test(
|
|
2217
|
+
test('should provide item context variables (itemIndex, itemFirst, itemLast)', async () => {
|
|
2126
2218
|
const components: Record<string, ComponentDefinition> = {
|
|
2127
2219
|
TagsList: {
|
|
2128
2220
|
component: {
|
|
@@ -2130,34 +2222,36 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2130
2222
|
tags: {
|
|
2131
2223
|
type: 'list',
|
|
2132
2224
|
itemSchema: {
|
|
2133
|
-
name: { type: 'string' }
|
|
2225
|
+
name: { type: 'string' },
|
|
2134
2226
|
},
|
|
2135
|
-
default: []
|
|
2136
|
-
}
|
|
2227
|
+
default: [],
|
|
2228
|
+
},
|
|
2137
2229
|
},
|
|
2138
2230
|
structure: {
|
|
2139
|
-
type:
|
|
2140
|
-
source:
|
|
2141
|
-
itemAs:
|
|
2231
|
+
type: 'list',
|
|
2232
|
+
source: 'tags',
|
|
2233
|
+
itemAs: 'tag',
|
|
2142
2234
|
children: [
|
|
2143
|
-
{
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2235
|
+
{
|
|
2236
|
+
type: 'node',
|
|
2237
|
+
tag: 'span',
|
|
2238
|
+
attributes: { 'data-index': '{{tagIndex}}', 'data-first': '{{tagFirst}}', 'data-last': '{{tagLast}}' },
|
|
2239
|
+
children: ['{{tag.name}}'],
|
|
2240
|
+
},
|
|
2241
|
+
],
|
|
2242
|
+
} as any,
|
|
2243
|
+
},
|
|
2244
|
+
},
|
|
2148
2245
|
};
|
|
2149
2246
|
|
|
2150
2247
|
const pageData: JSONPage = {
|
|
2151
2248
|
root: {
|
|
2152
|
-
type:
|
|
2153
|
-
component:
|
|
2249
|
+
type: 'component',
|
|
2250
|
+
component: 'TagsList',
|
|
2154
2251
|
props: {
|
|
2155
|
-
tags: [
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
]
|
|
2159
|
-
}
|
|
2160
|
-
} as any
|
|
2252
|
+
tags: [{ name: 'Tag1' }, { name: 'Tag2' }],
|
|
2253
|
+
},
|
|
2254
|
+
} as any,
|
|
2161
2255
|
};
|
|
2162
2256
|
|
|
2163
2257
|
const result = await renderPageSSR(pageData, components, '/', '');
|
|
@@ -2170,7 +2264,7 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2170
2264
|
expect(result.html).toContain('Tag2');
|
|
2171
2265
|
});
|
|
2172
2266
|
|
|
2173
|
-
test(
|
|
2267
|
+
test('should render empty when list prop is empty', async () => {
|
|
2174
2268
|
const components: Record<string, ComponentDefinition> = {
|
|
2175
2269
|
EmptyList: {
|
|
2176
2270
|
component: {
|
|
@@ -2178,30 +2272,28 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2178
2272
|
items: {
|
|
2179
2273
|
type: 'list',
|
|
2180
2274
|
itemSchema: {
|
|
2181
|
-
title: { type: 'string' }
|
|
2275
|
+
title: { type: 'string' },
|
|
2182
2276
|
},
|
|
2183
|
-
default: []
|
|
2184
|
-
}
|
|
2277
|
+
default: [],
|
|
2278
|
+
},
|
|
2185
2279
|
},
|
|
2186
2280
|
structure: {
|
|
2187
|
-
type:
|
|
2188
|
-
source:
|
|
2189
|
-
children: [
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2281
|
+
type: 'list',
|
|
2282
|
+
source: 'items',
|
|
2283
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
2284
|
+
} as any,
|
|
2285
|
+
},
|
|
2286
|
+
},
|
|
2195
2287
|
};
|
|
2196
2288
|
|
|
2197
2289
|
const pageData: JSONPage = {
|
|
2198
2290
|
root: {
|
|
2199
|
-
type:
|
|
2200
|
-
component:
|
|
2291
|
+
type: 'component',
|
|
2292
|
+
component: 'EmptyList',
|
|
2201
2293
|
props: {
|
|
2202
|
-
items: []
|
|
2203
|
-
}
|
|
2204
|
-
} as any
|
|
2294
|
+
items: [],
|
|
2295
|
+
},
|
|
2296
|
+
} as any,
|
|
2205
2297
|
};
|
|
2206
2298
|
|
|
2207
2299
|
const result = await renderPageSSR(pageData, components, '/', '');
|
|
@@ -2210,7 +2302,7 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2210
2302
|
expect(result.html).not.toContain('item.');
|
|
2211
2303
|
});
|
|
2212
2304
|
|
|
2213
|
-
test(
|
|
2305
|
+
test('should support nested lists', async () => {
|
|
2214
2306
|
const components: Record<string, ComponentDefinition> = {
|
|
2215
2307
|
CategoryList: {
|
|
2216
2308
|
component: {
|
|
@@ -2218,44 +2310,46 @@ describe("SSR Renderer - List node rendering", () => {
|
|
|
2218
2310
|
categories: {
|
|
2219
2311
|
type: 'list',
|
|
2220
2312
|
itemSchema: {
|
|
2221
|
-
name: { type: 'string' }
|
|
2313
|
+
name: { type: 'string' },
|
|
2222
2314
|
},
|
|
2223
|
-
default: []
|
|
2224
|
-
}
|
|
2315
|
+
default: [],
|
|
2316
|
+
},
|
|
2225
2317
|
},
|
|
2226
2318
|
structure: {
|
|
2227
|
-
type:
|
|
2228
|
-
source:
|
|
2229
|
-
itemAs:
|
|
2319
|
+
type: 'list',
|
|
2320
|
+
source: 'categories',
|
|
2321
|
+
itemAs: 'category',
|
|
2230
2322
|
children: [
|
|
2231
|
-
{
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2323
|
+
{
|
|
2324
|
+
type: 'node',
|
|
2325
|
+
tag: 'div',
|
|
2326
|
+
children: [
|
|
2327
|
+
'{{category.name}}',
|
|
2328
|
+
{
|
|
2329
|
+
type: 'list',
|
|
2330
|
+
source: '{{category.items}}',
|
|
2331
|
+
itemAs: 'subItem',
|
|
2332
|
+
children: [{ type: 'node', tag: 'span', children: ['{{subItem.label}}'] }],
|
|
2333
|
+
},
|
|
2334
|
+
],
|
|
2335
|
+
},
|
|
2336
|
+
],
|
|
2337
|
+
} as any,
|
|
2338
|
+
},
|
|
2339
|
+
},
|
|
2246
2340
|
};
|
|
2247
2341
|
|
|
2248
2342
|
const pageData: JSONPage = {
|
|
2249
2343
|
root: {
|
|
2250
|
-
type:
|
|
2251
|
-
component:
|
|
2344
|
+
type: 'component',
|
|
2345
|
+
component: 'CategoryList',
|
|
2252
2346
|
props: {
|
|
2253
2347
|
categories: [
|
|
2254
|
-
{ name:
|
|
2255
|
-
{ name:
|
|
2256
|
-
]
|
|
2257
|
-
}
|
|
2258
|
-
} as any
|
|
2348
|
+
{ name: 'Category A', items: [{ label: 'Item A1' }, { label: 'Item A2' }] },
|
|
2349
|
+
{ name: 'Category B', items: [{ label: 'Item B1' }] },
|
|
2350
|
+
],
|
|
2351
|
+
},
|
|
2352
|
+
} as any,
|
|
2259
2353
|
};
|
|
2260
2354
|
|
|
2261
2355
|
const result = await renderPageSSR(pageData, components, '/', '');
|