meno-core 1.0.53 → 1.0.54
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 +48 -57
- package/build-astro.ts +131 -113
- package/build-next.ts +109 -96
- package/build-static.test.ts +39 -10
- package/build-static.ts +120 -122
- package/dist/bin/cli.js +34 -38
- package/dist/bin/cli.js.map +2 -2
- package/dist/build-static.js +12 -11
- package/dist/chunks/chunk-2AR55GYH.js +42 -0
- package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
- package/dist/chunks/{chunk-IGYR22T6.js → chunk-2FN4UOVO.js} +512 -239
- package/dist/chunks/chunk-2FN4UOVO.js.map +7 -0
- package/dist/chunks/chunk-3XER4E5W.js +168 -0
- package/dist/chunks/chunk-3XER4E5W.js.map +7 -0
- package/dist/chunks/{chunk-X754AHS5.js → chunk-5ETZFREW.js} +9 -12
- package/dist/chunks/chunk-5ETZFREW.js.map +7 -0
- package/dist/chunks/{chunk-2MHDV5BF.js → chunk-7E4IF5L7.js} +15 -21
- package/dist/chunks/chunk-7E4IF5L7.js.map +7 -0
- package/dist/chunks/{chunk-JGWFTO6P.js → chunk-7HWQUVTU.js} +1578 -1349
- package/dist/chunks/chunk-7HWQUVTU.js.map +7 -0
- package/dist/chunks/{chunk-O3NAGJP4.js → chunk-AE3QK5QW.js} +110 -21
- package/dist/chunks/chunk-AE3QK5QW.js.map +7 -0
- package/dist/chunks/{chunk-GZHGVVW3.js → chunk-F6KTJYGV.js} +7 -7
- package/dist/chunks/chunk-F6KTJYGV.js.map +7 -0
- package/dist/chunks/{chunk-WQFG7PAH.js → chunk-FZITJSSS.js} +2 -6
- package/dist/chunks/chunk-FZITJSSS.js.map +7 -0
- package/dist/chunks/{chunk-QB2LNO4W.js → chunk-GSYYA5GX.js} +2 -2
- package/dist/chunks/chunk-GSYYA5GX.js.map +7 -0
- package/dist/chunks/{chunk-YBLHKYFF.js → chunk-HIZMY3EP.js} +1 -1
- package/dist/chunks/chunk-HIZMY3EP.js.map +7 -0
- package/dist/chunks/{chunk-AZQYF6KE.js → chunk-I2WEGYA7.js} +41 -176
- package/dist/chunks/chunk-I2WEGYA7.js.map +7 -0
- package/dist/chunks/{chunk-I7YIGZXT.js → chunk-JNO3CNLJ.js} +6 -9
- package/dist/chunks/chunk-JNO3CNLJ.js.map +7 -0
- package/dist/chunks/{chunk-UB44F4Z2.js → chunk-NVRBTSQG.js} +2 -4
- package/dist/chunks/chunk-NVRBTSQG.js.map +7 -0
- package/dist/chunks/{chunk-JGP5A3Y5.js → chunk-Q4OBWKXG.js} +40 -33
- package/dist/chunks/chunk-Q4OBWKXG.js.map +7 -0
- package/dist/chunks/{chunk-R6XHAFBF.js → chunk-QTE32Y53.js} +250 -242
- package/dist/chunks/chunk-QTE32Y53.js.map +7 -0
- package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-STDY3OVM.js} +214 -85
- package/dist/chunks/chunk-STDY3OVM.js.map +7 -0
- package/dist/chunks/configService-PRJZF7Y6.js +14 -0
- package/dist/chunks/{constants-STK2YBIW.js → constants-KIQEYMAM.js} +2 -2
- package/dist/chunks/{fs-JGINUXGL.js → fs-ZI5JEU7V.js} +2 -2
- package/dist/entries/server-router.js +14 -19
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +638 -332
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +177 -235
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +80 -44
- package/dist/lib/shared/index.js.map +2 -2
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +38 -60
- package/dist/lib/test-utils/index.js.map +2 -2
- package/entries/client-router.tsx +10 -8
- package/entries/server-router.tsx +1 -7
- package/lib/client/ClientInitializer.ts +8 -8
- package/lib/client/ErrorBoundary.test.tsx +146 -143
- package/lib/client/ErrorBoundary.tsx +175 -127
- package/lib/client/componentRegistry.test.ts +96 -108
- package/lib/client/componentRegistry.ts +1 -2
- package/lib/client/contexts/ThemeContext.tsx +3 -2
- package/lib/client/core/ComponentBuilder.test.ts +513 -560
- package/lib/client/core/ComponentBuilder.ts +318 -146
- package/lib/client/core/ComponentRenderer.test.tsx +1 -2
- package/lib/client/core/ComponentRenderer.tsx +46 -33
- package/lib/client/core/builders/embedBuilder.ts +241 -53
- package/lib/client/core/builders/linkBuilder.ts +71 -44
- package/lib/client/core/builders/linkNodeBuilder.ts +78 -53
- package/lib/client/core/builders/listBuilder.ts +115 -86
- package/lib/client/core/builders/localeListBuilder.ts +95 -60
- package/lib/client/core/builders/types.ts +5 -5
- package/lib/client/core/cmsTemplateProcessor.ts +7 -7
- 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 +8 -0
- package/lib/client/hmrCssReload.ts +14 -8
- package/lib/client/hmrWebSocket.ts +9 -14
- package/lib/client/hooks/useColorVariables.test.ts +21 -21
- package/lib/client/hooks/useColorVariables.ts +12 -10
- package/lib/client/hooks/usePropertyAutocomplete.ts +3 -5
- package/lib/client/hooks/useVariables.ts +4 -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 +19 -21
- package/lib/client/meno-filter/MenoFilter.ts +5 -9
- package/lib/client/meno-filter/bindings.ts +15 -40
- package/lib/client/meno-filter/init.ts +1 -1
- package/lib/client/meno-filter/renderer.ts +23 -29
- package/lib/client/meno-filter/script.generated.ts +1 -3
- package/lib/client/meno-filter/ui.ts +3 -5
- package/lib/client/meno-filter/updates.ts +15 -21
- package/lib/client/navigation.test.ts +159 -159
- package/lib/client/navigation.ts +0 -1
- package/lib/client/responsiveStyleResolver.test.ts +230 -228
- package/lib/client/responsiveStyleResolver.ts +13 -16
- package/lib/client/routing/RouteLoader.test.ts +23 -24
- package/lib/client/routing/RouteLoader.ts +22 -35
- package/lib/client/routing/Router.tsx +31 -3
- package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
- package/lib/client/scripts/ScriptExecutor.ts +15 -33
- package/lib/client/services/PrefetchService.test.ts +2 -2
- package/lib/client/services/PrefetchService.ts +10 -24
- package/lib/client/styleProcessor.test.ts +9 -9
- package/lib/client/styleProcessor.ts +18 -15
- package/lib/client/styles/StyleInjector.test.ts +122 -115
- package/lib/client/styles/StyleInjector.ts +9 -6
- package/lib/client/styles/UtilityClassCollector.ts +19 -26
- package/lib/client/styles/cspNonce.test.ts +2 -5
- package/lib/client/templateEngine.test.ts +554 -482
- package/lib/client/templateEngine.ts +200 -64
- 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 +2 -5
- package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
- package/lib/server/__integration__/static-assets.test.ts +1 -1
- package/lib/server/__integration__/test-helpers.ts +84 -70
- package/lib/server/ab/generateFunctions.ts +12 -10
- package/lib/server/astro/cmsPageEmitter.ts +23 -38
- package/lib/server/astro/componentEmitter.ts +25 -36
- package/lib/server/astro/cssCollector.ts +10 -26
- package/lib/server/astro/nodeToAstro.test.ts +1750 -30
- package/lib/server/astro/nodeToAstro.ts +198 -187
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +105 -109
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +4 -12
- 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 +14 -17
- package/lib/server/createServer.ts +5 -17
- package/lib/server/cssGenerator.test.ts +35 -44
- package/lib/server/cssGenerator.ts +6 -17
- package/lib/server/fileWatcher.test.ts +124 -10
- package/lib/server/fileWatcher.ts +124 -118
- package/lib/server/index.ts +7 -1
- package/lib/server/jsonLoader.test.ts +39 -2
- package/lib/server/jsonLoader.ts +33 -31
- package/lib/server/middleware/cors.test.ts +20 -20
- package/lib/server/middleware/cors.ts +28 -4
- package/lib/server/middleware/errorHandler.test.ts +5 -3
- package/lib/server/middleware/errorHandler.ts +3 -8
- package/lib/server/middleware/index.ts +0 -1
- package/lib/server/middleware/logger.test.ts +7 -5
- package/lib/server/middleware/logger.ts +10 -22
- package/lib/server/pageCache.test.ts +76 -77
- package/lib/server/pageCache.ts +0 -1
- package/lib/server/projectContext.ts +4 -3
- package/lib/server/providers/fileSystemCMSProvider.test.ts +124 -95
- package/lib/server/providers/fileSystemCMSProvider.ts +35 -20
- package/lib/server/providers/fileSystemPageProvider.test.ts +84 -0
- package/lib/server/providers/fileSystemPageProvider.ts +31 -12
- package/lib/server/routes/api/cms.test.ts +26 -14
- package/lib/server/routes/api/cms.ts +9 -14
- package/lib/server/routes/api/components.ts +30 -34
- package/lib/server/routes/api/config.ts +0 -1
- package/lib/server/routes/api/core-routes.ts +47 -74
- package/lib/server/routes/api/functions.ts +8 -16
- 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 +1 -3
- package/lib/server/routes/api/variables.ts +1 -1
- package/lib/server/routes/index.ts +23 -26
- package/lib/server/routes/pages.ts +34 -29
- package/lib/server/routes/static.ts +16 -4
- package/lib/server/runtime/bundler.ts +47 -32
- package/lib/server/runtime/fs.ts +3 -13
- package/lib/server/runtime/httpServer.ts +5 -9
- package/lib/server/services/ColorService.ts +32 -27
- package/lib/server/services/EnumService.test.ts +2 -6
- package/lib/server/services/EnumService.ts +7 -2
- package/lib/server/services/VariableService.test.ts +1 -5
- package/lib/server/services/VariableService.ts +6 -1
- package/lib/server/services/cmsService.test.ts +116 -78
- package/lib/server/services/cmsService.ts +24 -54
- package/lib/server/services/componentService.test.ts +122 -36
- package/lib/server/services/componentService.ts +54 -38
- package/lib/server/services/configService.test.ts +9 -31
- package/lib/server/services/configService.ts +20 -27
- package/lib/server/services/fileWatcherService.ts +40 -6
- package/lib/server/services/index.ts +0 -1
- package/lib/server/services/pageService.test.ts +1 -3
- package/lib/server/services/pageService.ts +14 -13
- package/lib/server/ssr/attributeBuilder.ts +16 -6
- package/lib/server/ssr/buildErrorOverlay.ts +11 -12
- package/lib/server/ssr/clientDataInjector.ts +7 -21
- package/lib/server/ssr/cmsSSRProcessor.ts +3 -6
- 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 +3 -9
- package/lib/server/ssr/htmlGenerator.test.ts +120 -43
- package/lib/server/ssr/htmlGenerator.ts +120 -85
- package/lib/server/ssr/imageMetadata.test.ts +3 -1
- package/lib/server/ssr/imageMetadata.ts +25 -19
- package/lib/server/ssr/jsCollector.test.ts +3 -13
- package/lib/server/ssr/jsCollector.ts +3 -8
- package/lib/server/ssr/liveReloadIntegration.test.ts +69 -28
- 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 +196 -246
- package/lib/server/ssr/ssrRenderer.ts +609 -225
- package/lib/server/ssrRenderer.test.ts +1044 -950
- package/lib/server/utils/jsonLineMapper.test.ts +28 -28
- package/lib/server/utils/jsonLineMapper.ts +1 -1
- package/lib/server/validateStyleCoverage.ts +18 -20
- package/lib/server/webflow/buildWebflow.ts +41 -53
- package/lib/server/webflow/nodeToWebflow.test.ts +150 -218
- package/lib/server/webflow/nodeToWebflow.ts +195 -258
- package/lib/server/webflow/styleMapper.test.ts +15 -56
- package/lib/server/webflow/styleMapper.ts +33 -41
- package/lib/server/webflow/types.ts +1 -8
- package/lib/server/websocketManager.ts +16 -19
- package/lib/shared/attributeNodeUtils.test.ts +15 -15
- package/lib/shared/attributeNodeUtils.ts +5 -12
- package/lib/shared/breakpoints.ts +4 -11
- package/lib/shared/cmsQueryParser.test.ts +50 -42
- package/lib/shared/cmsQueryParser.ts +49 -31
- package/lib/shared/colorVariableUtils.test.ts +5 -5
- package/lib/shared/colorVariableUtils.ts +3 -8
- package/lib/shared/componentRefs.ts +1 -5
- package/lib/shared/constants.test.ts +3 -3
- package/lib/shared/constants.ts +4 -8
- package/lib/shared/cssGeneration.test.ts +262 -144
- package/lib/shared/cssGeneration.ts +189 -516
- package/lib/shared/cssNamedColors.ts +152 -30
- package/lib/shared/cssProperties.test.ts +5 -6
- package/lib/shared/cssProperties.ts +296 -112
- package/lib/shared/elementClassName.test.ts +109 -109
- package/lib/shared/elementClassName.ts +1 -1
- package/lib/shared/elementUtils.ts +12 -16
- 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 +119 -0
- package/lib/shared/expressionEvaluator.ts +43 -24
- package/lib/shared/fontLoader.test.ts +19 -5
- package/lib/shared/friendlyError.ts +2 -3
- package/lib/shared/gradientUtils.test.ts +1 -5
- package/lib/shared/gradientUtils.ts +2 -6
- package/lib/shared/hrefRefs.ts +2 -10
- package/lib/shared/i18n.test.ts +1 -1
- package/lib/shared/i18n.ts +13 -34
- package/lib/shared/index.ts +4 -0
- package/lib/shared/inlineSvgStyleRules.ts +2 -4
- package/lib/shared/interactiveStyleMappings.test.ts +11 -33
- package/lib/shared/interactiveStyleMappings.ts +9 -16
- package/lib/shared/interactiveStyles.test.ts +165 -188
- package/lib/shared/interfaces/contentProvider.ts +1 -1
- package/lib/shared/itemTemplateUtils.test.ts +6 -12
- package/lib/shared/itemTemplateUtils.ts +19 -35
- package/lib/shared/jsonRepair.ts +8 -2
- package/lib/shared/libraryLoader.test.ts +15 -49
- package/lib/shared/libraryLoader.ts +7 -22
- package/lib/shared/netlifyLocale404.test.ts +179 -0
- package/lib/shared/netlifyLocale404.ts +110 -0
- package/lib/shared/nodeUtils.test.ts +24 -16
- package/lib/shared/nodeUtils.ts +49 -19
- package/lib/shared/pathArrayUtils.test.ts +1 -2
- package/lib/shared/pathArrayUtils.ts +1 -1
- package/lib/shared/pathSecurity.ts +1 -1
- package/lib/shared/pathUtils.test.ts +4 -6
- package/lib/shared/pathUtils.ts +42 -48
- 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 +14 -17
- package/lib/shared/paths/PathUtils.ts +9 -10
- package/lib/shared/paths/PathValidator.test.ts +2 -15
- package/lib/shared/paths/PathValidator.ts +11 -9
- package/lib/shared/paths/index.ts +1 -2
- package/lib/shared/propResolver.test.ts +240 -244
- package/lib/shared/propResolver.ts +14 -25
- package/lib/shared/pxToRem.test.ts +7 -6
- package/lib/shared/pxToRem.ts +2 -5
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +9 -5
- package/lib/shared/registry/ClientRegistry.ts +0 -1
- package/lib/shared/registry/ComponentRegistry.test.ts +43 -29
- package/lib/shared/registry/ComponentRegistry.ts +9 -11
- package/lib/shared/registry/NodeTypeDefinition.ts +15 -8
- package/lib/shared/registry/RegistryManager.ts +1 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -1
- package/lib/shared/registry/createNodeType.ts +7 -9
- package/lib/shared/registry/defineNodeType.ts +2 -6
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +47 -18
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +78 -74
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -7
- package/lib/shared/registry/nodeTypes/index.ts +10 -2
- package/lib/shared/responsiveScaling.test.ts +15 -31
- package/lib/shared/responsiveScaling.ts +55 -37
- package/lib/shared/responsiveStyleUtils.ts +11 -13
- package/lib/shared/richtext/htmlToTiptap.test.ts +23 -14
- package/lib/shared/richtext/htmlToTiptap.ts +1 -3
- 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 +5 -10
- package/lib/shared/styleUtils.test.ts +87 -61
- package/lib/shared/styleUtils.ts +5 -6
- package/lib/shared/themeDefaults.test.ts +11 -11
- package/lib/shared/themeDefaults.ts +3 -4
- package/lib/shared/tree/PathBuilder.test.ts +62 -138
- package/lib/shared/tree/PathBuilder.ts +49 -39
- package/lib/shared/treePathUtils.test.ts +2 -10
- package/lib/shared/treePathUtils.ts +54 -59
- package/lib/shared/types/api.ts +1 -2
- package/lib/shared/types/cms.ts +25 -21
- package/lib/shared/types/comment.ts +49 -16
- package/lib/shared/types/components.ts +27 -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 +2 -2
- package/lib/shared/types/styles.ts +0 -1
- package/lib/shared/types/variables.test.ts +4 -13
- package/lib/shared/types/variables.ts +48 -27
- package/lib/shared/types.ts +1 -2
- package/lib/shared/utilityClassConfig.ts +648 -321
- package/lib/shared/utilityClassMapper.test.ts +203 -92
- package/lib/shared/utilityClassMapper.ts +188 -246
- package/lib/shared/utilityClassNames.ts +326 -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/propValidator.test.ts +18 -20
- package/lib/shared/validation/propValidator.ts +12 -17
- package/lib/shared/validation/schemas.test.ts +24 -33
- package/lib/shared/validation/schemas.ts +469 -344
- package/lib/shared/validation/validators.test.ts +1 -6
- package/lib/shared/validation/validators.ts +89 -68
- package/lib/shared/viewportUnits.integration.test.ts +14 -10
- package/lib/shared/viewportUnits.test.ts +11 -23
- package/lib/test-utils/dom-setup.ts +1 -1
- package/lib/test-utils/factories/ConsoleMockFactory.ts +3 -7
- package/lib/test-utils/factories/DomMockFactory.ts +7 -19
- 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 +5 -9
- package/lib/test-utils/factories/StoreMockFactory.ts +14 -25
- 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 +12 -18
- package/lib/test-utils/mocks.ts +4 -2
- package/package.json +1 -1
- package/scripts/build-meno-filter.ts +1 -4
- package/vite.config.ts +4 -4
- package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
- package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
- package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
- package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
- package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
- package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
- 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.map +0 -7
- 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 → configService-PRJZF7Y6.js.map} +0 -0
- /package/dist/chunks/{constants-STK2YBIW.js.map → constants-KIQEYMAM.js.map} +0 -0
- /package/dist/chunks/{fs-JGINUXGL.js.map → fs-ZI5JEU7V.js.map} +0 -0
|
@@ -1,361 +1,382 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, afterEach, mock } from
|
|
2
|
-
import { setGlobalTemplateContext, resetGlobalTemplateContext } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
|
|
2
|
+
import { setGlobalTemplateContext, resetGlobalTemplateContext } from '../shared/globalTemplateContext';
|
|
3
|
+
import { evaluateTemplate, resolveStyleMapping, processStructure, normalizeStyle } from './templateEngine';
|
|
4
|
+
import type {
|
|
5
|
+
ComponentNode,
|
|
6
|
+
ComponentDefinition,
|
|
7
|
+
StructuredComponentDefinition,
|
|
8
|
+
StyleMapping,
|
|
9
|
+
TemplateContext,
|
|
10
|
+
} from '../shared/types';
|
|
11
|
+
|
|
12
|
+
describe('Template Engine - evaluateTemplate', () => {
|
|
13
|
+
describe('Complex template expressions', () => {
|
|
14
|
+
test('should evaluate nested property access', () => {
|
|
15
|
+
const template = '{{variants[variant].background}}';
|
|
15
16
|
const context = {
|
|
16
17
|
variants: {
|
|
17
|
-
primary: { background:
|
|
18
|
-
secondary: { background:
|
|
18
|
+
primary: { background: '#0070f3' },
|
|
19
|
+
secondary: { background: '#f3f4f6' },
|
|
19
20
|
},
|
|
20
|
-
variant:
|
|
21
|
+
variant: 'primary',
|
|
21
22
|
};
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
const result = evaluateTemplate(template, context);
|
|
24
|
-
expect(result).toBe(
|
|
25
|
+
expect(result).toBe('#0070f3');
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
test(
|
|
28
|
+
test('should evaluate ternary operators', () => {
|
|
28
29
|
const template = "{{disabled ? '0.5' : '1'}}";
|
|
29
30
|
const context = { disabled: true };
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
const result = evaluateTemplate(template, context);
|
|
32
|
-
expect(result).toBe(
|
|
33
|
+
expect(result).toBe('0.5');
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
test(
|
|
36
|
+
test('should evaluate ternary operators with false condition', () => {
|
|
36
37
|
const template = "{{disabled ? '0.5' : '1'}}";
|
|
37
38
|
const context = { disabled: false };
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
const result = evaluateTemplate(template, context);
|
|
40
|
-
expect(result).toBe(
|
|
41
|
+
expect(result).toBe('1');
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
test(
|
|
44
|
-
const template =
|
|
44
|
+
test('should evaluate mathematical operations - addition', () => {
|
|
45
|
+
const template = '{{count + 1}}';
|
|
45
46
|
const context = { count: 5 };
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
const result = evaluateTemplate(template, context);
|
|
48
49
|
expect(result).toBe(6);
|
|
49
50
|
});
|
|
50
51
|
|
|
51
|
-
test(
|
|
52
|
-
const template =
|
|
52
|
+
test('should evaluate mathematical operations - multiplication', () => {
|
|
53
|
+
const template = '{{price * 1.1}}';
|
|
53
54
|
const context = { price: 100 };
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
const result = evaluateTemplate(template, context);
|
|
56
57
|
expect(result).toBeCloseTo(110, 5);
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
test(
|
|
60
|
+
test('should evaluate string concatenation', () => {
|
|
60
61
|
const template = "{{'Hello ' + name}}";
|
|
61
|
-
const context = { name:
|
|
62
|
-
|
|
62
|
+
const context = { name: 'World' };
|
|
63
|
+
|
|
63
64
|
const result = evaluateTemplate(template, context);
|
|
64
|
-
expect(result).toBe(
|
|
65
|
+
expect(result).toBe('Hello World');
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
test(
|
|
68
|
+
test('should evaluate logical AND operator', () => {
|
|
68
69
|
const template = "{{isActive && 'active'}}";
|
|
69
70
|
const context = { isActive: true };
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
const result = evaluateTemplate(template, context);
|
|
72
|
-
expect(result).toBe(
|
|
73
|
+
expect(result).toBe('active');
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
test(
|
|
76
|
+
test('should evaluate logical AND operator with false', () => {
|
|
76
77
|
const template = "{{isActive && 'active'}}";
|
|
77
78
|
const context = { isActive: false };
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
const result = evaluateTemplate(template, context);
|
|
80
81
|
expect(result).toBe(false);
|
|
81
82
|
});
|
|
82
83
|
|
|
83
|
-
test(
|
|
84
|
+
test('should evaluate complex nested expressions', () => {
|
|
84
85
|
const template = "{{user.profile.name || 'Anonymous'}}";
|
|
85
|
-
const context = { user: { profile: { name:
|
|
86
|
-
|
|
86
|
+
const context = { user: { profile: { name: 'John' } } };
|
|
87
|
+
|
|
87
88
|
const result = evaluateTemplate(template, context);
|
|
88
|
-
expect(result).toBe(
|
|
89
|
+
expect(result).toBe('John');
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
test(
|
|
92
|
+
test('should evaluate expressions with fallback', () => {
|
|
92
93
|
const template = "{{user.profile.name || 'Anonymous'}}";
|
|
93
94
|
const context = { user: { profile: {} } };
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
const result = evaluateTemplate(template, context);
|
|
96
|
-
expect(result).toBe(
|
|
97
|
+
expect(result).toBe('Anonymous');
|
|
97
98
|
});
|
|
98
99
|
});
|
|
99
100
|
|
|
100
|
-
describe(
|
|
101
|
-
test(
|
|
102
|
-
const template =
|
|
103
|
-
const context = { name:
|
|
104
|
-
|
|
101
|
+
describe('Edge cases', () => {
|
|
102
|
+
test('should return original string if not a template', () => {
|
|
103
|
+
const template = 'plain text';
|
|
104
|
+
const context = { name: 'John' };
|
|
105
|
+
|
|
105
106
|
const result = evaluateTemplate(template, context);
|
|
106
|
-
expect(result).toBe(
|
|
107
|
+
expect(result).toBe('plain text');
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
test(
|
|
110
|
-
const template =
|
|
110
|
+
test('should return template string on invalid expression', () => {
|
|
111
|
+
const template = '{{invalid.expression}}';
|
|
111
112
|
const context = {};
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
const result = evaluateTemplate(template, context);
|
|
114
115
|
expect(result).toBe(template); // Should return original template on error
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
test(
|
|
118
|
-
const template =
|
|
118
|
+
test('should return template string on syntax error', () => {
|
|
119
|
+
const template = '{{count +}}'; // Missing operand
|
|
119
120
|
const context = { count: 5 };
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
const result = evaluateTemplate(template, context);
|
|
122
123
|
expect(result).toBe(template); // Should return original template on error
|
|
123
124
|
});
|
|
124
125
|
|
|
125
|
-
test(
|
|
126
|
-
const template =
|
|
126
|
+
test('should handle missing context variables', () => {
|
|
127
|
+
const template = '{{undefinedVar}}';
|
|
127
128
|
const context = {};
|
|
128
|
-
|
|
129
|
+
|
|
129
130
|
const result = evaluateTemplate(template, context);
|
|
130
131
|
expect(result).toBe(template); // Should return original template when var is undefined
|
|
131
132
|
});
|
|
132
133
|
|
|
133
|
-
test(
|
|
134
|
+
test('should handle non-string templates', () => {
|
|
134
135
|
const template = 123 as unknown as string;
|
|
135
136
|
const context = {};
|
|
136
|
-
|
|
137
|
+
|
|
137
138
|
const result = evaluateTemplate(template, context);
|
|
138
139
|
expect(result).toBe(123); // Should pass through non-strings
|
|
139
140
|
});
|
|
140
141
|
|
|
141
|
-
test(
|
|
142
|
-
const template =
|
|
142
|
+
test('should handle empty template expression', () => {
|
|
143
|
+
const template = '{{}}';
|
|
143
144
|
const context = {};
|
|
144
|
-
|
|
145
|
+
|
|
145
146
|
const result = evaluateTemplate(template, context);
|
|
146
147
|
expect(result).toBe(template); // Invalid expression
|
|
147
148
|
});
|
|
148
149
|
|
|
149
|
-
test(
|
|
150
|
-
const template =
|
|
151
|
-
const context = { name:
|
|
152
|
-
|
|
150
|
+
test('should handle template with whitespace', () => {
|
|
151
|
+
const template = '{{ name }}';
|
|
152
|
+
const context = { name: 'John' };
|
|
153
|
+
|
|
153
154
|
const result = evaluateTemplate(template, context);
|
|
154
|
-
expect(result).toBe(
|
|
155
|
+
expect(result).toBe('John'); // Should trim whitespace
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('NaN result (arithmetic over a missing identifier) preserves the template', () => {
|
|
159
|
+
// A loop-var stagger evaluated by the props pass BEFORE the per-item pass:
|
|
160
|
+
// itemIndex isn't in context yet → 0.4 + undefined*0.2 = NaN. Returning NaN
|
|
161
|
+
// poisoned the attribute (React: "Received NaN for the `fade` attribute") and
|
|
162
|
+
// destroyed the template before the item pass could resolve it.
|
|
163
|
+
const template = '{{0.4+itemIndex*0.2}}';
|
|
164
|
+
|
|
165
|
+
expect(evaluateTemplate(template, {})).toBe(template);
|
|
166
|
+
expect(evaluateTemplate(template, { fade: '0.2' })).toBe(template);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the same arithmetic template resolves once the identifier is in context', () => {
|
|
170
|
+
expect(evaluateTemplate('{{0.4+itemIndex*0.2}}', { itemIndex: 2 })).toBeCloseTo(0.8);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('a legitimately-numeric result still passes through (only NaN is preserved)', () => {
|
|
174
|
+
expect(evaluateTemplate('{{count / 0}}', { count: 5 })).toBe(Infinity);
|
|
175
|
+
expect(evaluateTemplate('{{count - 5}}', { count: 5 })).toBe(0);
|
|
155
176
|
});
|
|
156
177
|
});
|
|
157
178
|
});
|
|
158
179
|
|
|
159
|
-
describe(
|
|
160
|
-
describe(
|
|
161
|
-
test(
|
|
180
|
+
describe('Template Engine - resolveStyleMapping', () => {
|
|
181
|
+
describe('Valid style mappings', () => {
|
|
182
|
+
test('should resolve prop-based style mapping', () => {
|
|
162
183
|
const mapping: StyleMapping = {
|
|
163
184
|
_mapping: true,
|
|
164
|
-
prop:
|
|
185
|
+
prop: 'variant',
|
|
165
186
|
values: {
|
|
166
|
-
primary:
|
|
167
|
-
secondary:
|
|
168
|
-
}
|
|
187
|
+
primary: '#0070f3',
|
|
188
|
+
secondary: '#f3f4f6',
|
|
189
|
+
},
|
|
169
190
|
};
|
|
170
|
-
const props = { variant:
|
|
171
|
-
|
|
191
|
+
const props = { variant: 'primary' };
|
|
192
|
+
|
|
172
193
|
const result = resolveStyleMapping(mapping, props);
|
|
173
|
-
expect(result).toBe(
|
|
194
|
+
expect(result).toBe('#0070f3');
|
|
174
195
|
});
|
|
175
196
|
|
|
176
|
-
test(
|
|
197
|
+
test('should resolve style mapping for secondary variant', () => {
|
|
177
198
|
const mapping: StyleMapping = {
|
|
178
199
|
_mapping: true,
|
|
179
|
-
prop:
|
|
200
|
+
prop: 'variant',
|
|
180
201
|
values: {
|
|
181
|
-
primary:
|
|
182
|
-
secondary:
|
|
183
|
-
}
|
|
202
|
+
primary: '#0070f3',
|
|
203
|
+
secondary: '#f3f4f6',
|
|
204
|
+
},
|
|
184
205
|
};
|
|
185
|
-
const props = { variant:
|
|
186
|
-
|
|
206
|
+
const props = { variant: 'secondary' };
|
|
207
|
+
|
|
187
208
|
const result = resolveStyleMapping(mapping, props);
|
|
188
|
-
expect(result).toBe(
|
|
209
|
+
expect(result).toBe('#f3f4f6');
|
|
189
210
|
});
|
|
190
211
|
|
|
191
|
-
test(
|
|
212
|
+
test('should resolve numeric style mapping values', () => {
|
|
192
213
|
const mapping: StyleMapping = {
|
|
193
214
|
_mapping: true,
|
|
194
|
-
prop:
|
|
215
|
+
prop: 'size',
|
|
195
216
|
values: {
|
|
196
217
|
small: 12,
|
|
197
|
-
large: 24
|
|
198
|
-
}
|
|
218
|
+
large: 24,
|
|
219
|
+
},
|
|
199
220
|
};
|
|
200
|
-
const props = { size:
|
|
201
|
-
|
|
221
|
+
const props = { size: 'small' };
|
|
222
|
+
|
|
202
223
|
const result = resolveStyleMapping(mapping, props);
|
|
203
224
|
expect(result).toBe(12);
|
|
204
225
|
});
|
|
205
226
|
});
|
|
206
227
|
|
|
207
|
-
describe(
|
|
208
|
-
test(
|
|
228
|
+
describe('Missing or invalid mappings', () => {
|
|
229
|
+
test('should return undefined for missing prop value', () => {
|
|
209
230
|
const mapping: StyleMapping = {
|
|
210
231
|
_mapping: true,
|
|
211
|
-
prop:
|
|
232
|
+
prop: 'variant',
|
|
212
233
|
values: {
|
|
213
|
-
primary:
|
|
214
|
-
secondary:
|
|
215
|
-
}
|
|
234
|
+
primary: '#0070f3',
|
|
235
|
+
secondary: '#f3f4f6',
|
|
236
|
+
},
|
|
216
237
|
};
|
|
217
238
|
const props = {}; // variant not provided
|
|
218
|
-
|
|
239
|
+
|
|
219
240
|
const result = resolveStyleMapping(mapping, props);
|
|
220
241
|
expect(result).toBeUndefined();
|
|
221
242
|
});
|
|
222
243
|
|
|
223
|
-
test(
|
|
244
|
+
test('should return undefined for null prop value', () => {
|
|
224
245
|
const mapping: StyleMapping = {
|
|
225
246
|
_mapping: true,
|
|
226
|
-
prop:
|
|
247
|
+
prop: 'variant',
|
|
227
248
|
values: {
|
|
228
|
-
primary:
|
|
229
|
-
}
|
|
249
|
+
primary: '#0070f3',
|
|
250
|
+
},
|
|
230
251
|
};
|
|
231
252
|
const props = { variant: null };
|
|
232
|
-
|
|
253
|
+
|
|
233
254
|
const result = resolveStyleMapping(mapping, props);
|
|
234
255
|
expect(result).toBeUndefined();
|
|
235
256
|
});
|
|
236
257
|
|
|
237
|
-
test(
|
|
258
|
+
test('should return undefined for prop value not in mapping', () => {
|
|
238
259
|
const mapping: StyleMapping = {
|
|
239
260
|
_mapping: true,
|
|
240
|
-
prop:
|
|
261
|
+
prop: 'variant',
|
|
241
262
|
values: {
|
|
242
|
-
primary:
|
|
243
|
-
}
|
|
263
|
+
primary: '#0070f3',
|
|
264
|
+
},
|
|
244
265
|
};
|
|
245
|
-
const props = { variant:
|
|
246
|
-
|
|
266
|
+
const props = { variant: 'invalid' };
|
|
267
|
+
|
|
247
268
|
const result = resolveStyleMapping(mapping, props);
|
|
248
269
|
expect(result).toBeUndefined();
|
|
249
270
|
});
|
|
250
271
|
|
|
251
|
-
test(
|
|
272
|
+
test('should return undefined for invalid mapping object (no _mapping)', () => {
|
|
252
273
|
const mapping = {
|
|
253
|
-
prop:
|
|
254
|
-
values: { primary:
|
|
274
|
+
prop: 'variant',
|
|
275
|
+
values: { primary: '#0070f3' },
|
|
255
276
|
};
|
|
256
|
-
const props = { variant:
|
|
257
|
-
|
|
277
|
+
const props = { variant: 'primary' };
|
|
278
|
+
|
|
258
279
|
const result = resolveStyleMapping(mapping, props);
|
|
259
280
|
expect(result).toBeUndefined();
|
|
260
281
|
});
|
|
261
282
|
|
|
262
|
-
test(
|
|
263
|
-
const mapping =
|
|
264
|
-
const props = { variant:
|
|
265
|
-
|
|
283
|
+
test('should return undefined for non-object mapping', () => {
|
|
284
|
+
const mapping = 'not an object';
|
|
285
|
+
const props = { variant: 'primary' };
|
|
286
|
+
|
|
266
287
|
const result = resolveStyleMapping(mapping, props);
|
|
267
288
|
expect(result).toBeUndefined();
|
|
268
289
|
});
|
|
269
290
|
|
|
270
|
-
test(
|
|
291
|
+
test('should return undefined for null mapping', () => {
|
|
271
292
|
const mapping = null;
|
|
272
|
-
const props = { variant:
|
|
273
|
-
|
|
293
|
+
const props = { variant: 'primary' };
|
|
294
|
+
|
|
274
295
|
const result = resolveStyleMapping(mapping, props);
|
|
275
296
|
expect(result).toBeUndefined();
|
|
276
297
|
});
|
|
277
298
|
|
|
278
|
-
test(
|
|
299
|
+
test('should return undefined for mapping with missing prop field', () => {
|
|
279
300
|
const mapping = {
|
|
280
301
|
_mapping: true,
|
|
281
|
-
values: { primary:
|
|
302
|
+
values: { primary: '#0070f3' },
|
|
282
303
|
};
|
|
283
|
-
const props = { variant:
|
|
284
|
-
|
|
304
|
+
const props = { variant: 'primary' };
|
|
305
|
+
|
|
285
306
|
const result = resolveStyleMapping(mapping, props);
|
|
286
307
|
expect(result).toBeUndefined();
|
|
287
308
|
});
|
|
288
309
|
|
|
289
|
-
test(
|
|
310
|
+
test('should return undefined for mapping with missing values field', () => {
|
|
290
311
|
const mapping = {
|
|
291
312
|
_mapping: true,
|
|
292
|
-
prop:
|
|
313
|
+
prop: 'variant',
|
|
293
314
|
};
|
|
294
|
-
const props = { variant:
|
|
295
|
-
|
|
315
|
+
const props = { variant: 'primary' };
|
|
316
|
+
|
|
296
317
|
const result = resolveStyleMapping(mapping, props);
|
|
297
318
|
expect(result).toBeUndefined();
|
|
298
319
|
});
|
|
299
320
|
});
|
|
300
321
|
});
|
|
301
322
|
|
|
302
|
-
describe(
|
|
323
|
+
describe('Template Engine - processStructure', () => {
|
|
303
324
|
const mockComponentDef: StructuredComponentDefinition = {
|
|
304
325
|
interface: {},
|
|
305
|
-
structure: { type: 'node' as const, tag: 'div' }
|
|
326
|
+
structure: { type: 'node' as const, tag: 'div' },
|
|
306
327
|
};
|
|
307
328
|
|
|
308
329
|
// Helper to create TemplateContext
|
|
309
330
|
const createContext = (props: Record<string, unknown>, componentDef = mockComponentDef): TemplateContext => ({
|
|
310
331
|
props,
|
|
311
|
-
componentDef
|
|
332
|
+
componentDef,
|
|
312
333
|
});
|
|
313
334
|
|
|
314
|
-
describe(
|
|
315
|
-
test(
|
|
316
|
-
const structure =
|
|
317
|
-
const props = { name:
|
|
335
|
+
describe('String processing', () => {
|
|
336
|
+
test('should process template strings in structure', () => {
|
|
337
|
+
const structure = '{{name}}';
|
|
338
|
+
const props = { name: 'John' };
|
|
318
339
|
|
|
319
340
|
const result = processStructure(structure, createContext(props));
|
|
320
|
-
expect(result).toBe(
|
|
341
|
+
expect(result).toBe('John');
|
|
321
342
|
});
|
|
322
343
|
|
|
323
|
-
test(
|
|
324
|
-
const structure =
|
|
344
|
+
test('should process non-template strings', () => {
|
|
345
|
+
const structure = 'plain text';
|
|
325
346
|
const props = {};
|
|
326
347
|
|
|
327
348
|
const result = processStructure(structure, createContext(props));
|
|
328
|
-
expect(result).toBe(
|
|
349
|
+
expect(result).toBe('plain text');
|
|
329
350
|
});
|
|
330
351
|
});
|
|
331
352
|
|
|
332
|
-
describe(
|
|
333
|
-
test(
|
|
334
|
-
const structure = [
|
|
335
|
-
const props = { name:
|
|
353
|
+
describe('Array processing', () => {
|
|
354
|
+
test('should process array of strings', () => {
|
|
355
|
+
const structure = ['Hello', '{{name}}', 'World'] as any;
|
|
356
|
+
const props = { name: 'John' };
|
|
336
357
|
|
|
337
358
|
const result = processStructure(structure, createContext(props));
|
|
338
359
|
expect(Array.isArray(result)).toBe(true);
|
|
339
360
|
const resultArray = result as (string | ComponentNode)[];
|
|
340
|
-
expect(resultArray).toEqual([
|
|
361
|
+
expect(resultArray).toEqual(['Hello', 'John', 'World']);
|
|
341
362
|
});
|
|
342
363
|
|
|
343
|
-
test(
|
|
364
|
+
test('should process array of component nodes', () => {
|
|
344
365
|
const structure: ComponentNode[] = [
|
|
345
|
-
{ type:
|
|
346
|
-
{ type:
|
|
366
|
+
{ type: 'node', tag: 'div', children: ['{{text}}'] },
|
|
367
|
+
{ type: 'node', tag: 'span', children: ['Static'] },
|
|
347
368
|
];
|
|
348
|
-
const props = { text:
|
|
369
|
+
const props = { text: 'Dynamic' };
|
|
349
370
|
|
|
350
371
|
const result = processStructure(structure, createContext(props));
|
|
351
372
|
expect(Array.isArray(result)).toBe(true);
|
|
352
373
|
const resultArray = result as ComponentNode[];
|
|
353
|
-
expect(resultArray[0].children).toEqual([
|
|
354
|
-
expect(resultArray[1].children).toEqual([
|
|
374
|
+
expect(resultArray[0].children).toEqual(['Dynamic']);
|
|
375
|
+
expect(resultArray[1].children).toEqual(['Static']);
|
|
355
376
|
});
|
|
356
377
|
|
|
357
|
-
test(
|
|
358
|
-
const structure = [
|
|
378
|
+
test('should filter null values from arrays', () => {
|
|
379
|
+
const structure = ['Hello', null, 'World'] as any;
|
|
359
380
|
const props = {};
|
|
360
381
|
|
|
361
382
|
const result = processStructure(structure, createContext(props));
|
|
@@ -365,217 +386,224 @@ describe("Template Engine - processStructure", () => {
|
|
|
365
386
|
});
|
|
366
387
|
});
|
|
367
388
|
|
|
368
|
-
describe(
|
|
369
|
-
test(
|
|
389
|
+
describe('Component node processing', () => {
|
|
390
|
+
test('should process simple component node', () => {
|
|
370
391
|
const structure: ComponentNode = {
|
|
371
|
-
type:
|
|
372
|
-
tag:
|
|
373
|
-
children: [
|
|
392
|
+
type: 'node',
|
|
393
|
+
tag: 'div',
|
|
394
|
+
children: ['Hello World'],
|
|
374
395
|
};
|
|
375
396
|
const props = {};
|
|
376
397
|
|
|
377
398
|
const result = processStructure(structure, createContext(props));
|
|
378
399
|
expect(typeof result === 'object' && !Array.isArray(result)).toBe(true);
|
|
379
400
|
const node = result as ComponentNode;
|
|
380
|
-
expect(node.tag).toBe(
|
|
381
|
-
expect(node.children).toEqual([
|
|
401
|
+
expect(node.tag).toBe('div');
|
|
402
|
+
expect(node.children).toEqual(['Hello World']);
|
|
382
403
|
});
|
|
383
404
|
|
|
384
|
-
test(
|
|
405
|
+
test('should process component node with template in children', () => {
|
|
385
406
|
const structure: ComponentNode = {
|
|
386
|
-
type:
|
|
387
|
-
tag:
|
|
388
|
-
children: [
|
|
407
|
+
type: 'node',
|
|
408
|
+
tag: 'div',
|
|
409
|
+
children: ['{{greeting}}'],
|
|
389
410
|
};
|
|
390
|
-
const props = { greeting:
|
|
411
|
+
const props = { greeting: 'Hello' };
|
|
391
412
|
|
|
392
413
|
const result = processStructure(structure, createContext(props));
|
|
393
414
|
const node = result as ComponentNode;
|
|
394
|
-
expect(node.children).toEqual([
|
|
415
|
+
expect(node.children).toEqual(['Hello']);
|
|
395
416
|
});
|
|
396
417
|
|
|
397
|
-
test(
|
|
418
|
+
test('should process template in tag property', () => {
|
|
398
419
|
const structure: ComponentNode = {
|
|
399
|
-
type:
|
|
400
|
-
tag:
|
|
401
|
-
children: [
|
|
420
|
+
type: 'node',
|
|
421
|
+
tag: 'h{{size}}',
|
|
422
|
+
children: ['Heading'],
|
|
402
423
|
};
|
|
403
424
|
const props = { size: 1 };
|
|
404
425
|
|
|
405
426
|
const result = processStructure(structure, createContext(props));
|
|
406
427
|
const node = result as ComponentNode;
|
|
407
|
-
expect(node.tag).toBe(
|
|
408
|
-
expect(node.children).toEqual([
|
|
428
|
+
expect(node.tag).toBe('h1');
|
|
429
|
+
expect(node.children).toEqual(['Heading']);
|
|
409
430
|
});
|
|
410
431
|
|
|
411
|
-
test(
|
|
432
|
+
test('should process template in tag with string prop', () => {
|
|
412
433
|
const structure: ComponentNode = {
|
|
413
|
-
type:
|
|
414
|
-
tag:
|
|
415
|
-
children: [
|
|
434
|
+
type: 'node',
|
|
435
|
+
tag: 'h{{level}}',
|
|
436
|
+
children: ['Title'],
|
|
416
437
|
};
|
|
417
|
-
const props = { level:
|
|
438
|
+
const props = { level: '2' };
|
|
418
439
|
|
|
419
440
|
const result = processStructure(structure, createContext(props));
|
|
420
441
|
const node = result as ComponentNode;
|
|
421
|
-
expect(node.tag).toBe(
|
|
442
|
+
expect(node.tag).toBe('h2');
|
|
422
443
|
});
|
|
423
444
|
|
|
424
|
-
test(
|
|
445
|
+
test('should process nested component structures', () => {
|
|
425
446
|
const structure: ComponentNode = {
|
|
426
|
-
type:
|
|
427
|
-
tag:
|
|
447
|
+
type: 'node',
|
|
448
|
+
tag: 'div',
|
|
428
449
|
children: [
|
|
429
450
|
{
|
|
430
|
-
type:
|
|
431
|
-
tag:
|
|
432
|
-
children: [
|
|
433
|
-
}
|
|
434
|
-
]
|
|
451
|
+
type: 'node',
|
|
452
|
+
tag: 'span',
|
|
453
|
+
children: ['{{text}}'],
|
|
454
|
+
},
|
|
455
|
+
],
|
|
435
456
|
};
|
|
436
|
-
const props = { text:
|
|
457
|
+
const props = { text: 'Nested' };
|
|
437
458
|
|
|
438
459
|
const result = processStructure(structure, createContext(props));
|
|
439
460
|
const node = result as ComponentNode;
|
|
440
461
|
const children = node.children as (string | ComponentNode)[];
|
|
441
462
|
expect(children?.[0]).toBeDefined();
|
|
442
463
|
const child = children[0] as ComponentNode;
|
|
443
|
-
expect(child.children).toEqual([
|
|
464
|
+
expect(child.children).toEqual(['Nested']);
|
|
444
465
|
});
|
|
445
466
|
});
|
|
446
467
|
|
|
447
|
-
describe(
|
|
448
|
-
test(
|
|
468
|
+
describe('Style mapping resolution in structure', () => {
|
|
469
|
+
test('should resolve style mappings in flat style objects', () => {
|
|
449
470
|
const structure: ComponentNode = {
|
|
450
|
-
type:
|
|
451
|
-
tag:
|
|
471
|
+
type: 'node',
|
|
472
|
+
tag: 'button',
|
|
452
473
|
style: {
|
|
453
474
|
base: {
|
|
454
475
|
background: {
|
|
455
476
|
_mapping: true,
|
|
456
|
-
prop:
|
|
477
|
+
prop: 'variant',
|
|
457
478
|
values: {
|
|
458
|
-
primary:
|
|
459
|
-
secondary:
|
|
460
|
-
}
|
|
479
|
+
primary: '#0070f3',
|
|
480
|
+
secondary: '#f3f4f6',
|
|
481
|
+
},
|
|
461
482
|
} as StyleMapping,
|
|
462
|
-
color:
|
|
463
|
-
}
|
|
464
|
-
}
|
|
483
|
+
color: '#000',
|
|
484
|
+
},
|
|
485
|
+
},
|
|
465
486
|
};
|
|
466
|
-
const props = { variant:
|
|
487
|
+
const props = { variant: 'primary' };
|
|
467
488
|
|
|
468
489
|
const result = processStructure(structure, createContext(props));
|
|
469
490
|
const node = result as ComponentNode;
|
|
470
491
|
const baseStyle = (node.style as any)?.base;
|
|
471
|
-
expect(baseStyle?.background).toBe(
|
|
472
|
-
expect(baseStyle?.color).toBe(
|
|
492
|
+
expect(baseStyle?.background).toBe('#0070f3');
|
|
493
|
+
expect(baseStyle?.color).toBe('#000');
|
|
473
494
|
});
|
|
474
495
|
|
|
475
|
-
test(
|
|
496
|
+
test('should resolve style mappings in responsive breakpoint styles (tablet/mobile)', () => {
|
|
476
497
|
const structure: ComponentNode = {
|
|
477
|
-
type:
|
|
478
|
-
tag:
|
|
498
|
+
type: 'node',
|
|
499
|
+
tag: 'h1',
|
|
479
500
|
style: {
|
|
480
501
|
base: {
|
|
481
502
|
fontSize: {
|
|
482
503
|
_mapping: true,
|
|
483
|
-
prop:
|
|
484
|
-
values: {
|
|
504
|
+
prop: 'size',
|
|
505
|
+
values: { '1': '48px', '2': '40px', default: '48px' },
|
|
485
506
|
} as StyleMapping,
|
|
486
|
-
fontWeight:
|
|
507
|
+
fontWeight: '700',
|
|
487
508
|
},
|
|
488
509
|
tablet: {
|
|
489
510
|
fontSize: {
|
|
490
511
|
_mapping: true,
|
|
491
|
-
prop:
|
|
492
|
-
values: {
|
|
512
|
+
prop: 'size',
|
|
513
|
+
values: { '1': '132px', '2': '35px', default: '42px' },
|
|
493
514
|
} as StyleMapping,
|
|
494
515
|
},
|
|
495
|
-
mobile: {}
|
|
496
|
-
}
|
|
516
|
+
mobile: {},
|
|
517
|
+
},
|
|
497
518
|
};
|
|
498
|
-
const props = { size:
|
|
519
|
+
const props = { size: '1' };
|
|
499
520
|
|
|
500
521
|
const result = processStructure(structure, createContext(props));
|
|
501
522
|
const node = result as ComponentNode;
|
|
502
523
|
const style = node.style as any;
|
|
503
|
-
expect(style?.base?.fontSize).toBe(
|
|
504
|
-
expect(style?.base?.fontWeight).toBe(
|
|
505
|
-
expect(style?.tablet?.fontSize).toBe(
|
|
524
|
+
expect(style?.base?.fontSize).toBe('48px');
|
|
525
|
+
expect(style?.base?.fontWeight).toBe('700');
|
|
526
|
+
expect(style?.tablet?.fontSize).toBe('132px');
|
|
506
527
|
expect(style?.mobile).toEqual({});
|
|
507
528
|
});
|
|
508
529
|
|
|
509
|
-
test(
|
|
530
|
+
test('should resolve responsive style mappings with different prop values', () => {
|
|
510
531
|
const structure: ComponentNode = {
|
|
511
|
-
type:
|
|
512
|
-
tag:
|
|
532
|
+
type: 'node',
|
|
533
|
+
tag: 'h2',
|
|
513
534
|
style: {
|
|
514
535
|
base: {
|
|
515
536
|
fontSize: {
|
|
516
537
|
_mapping: true,
|
|
517
|
-
prop:
|
|
518
|
-
values: {
|
|
538
|
+
prop: 'size',
|
|
539
|
+
values: { '1': '48px', '2': '40px' },
|
|
519
540
|
} as StyleMapping,
|
|
520
541
|
},
|
|
521
542
|
tablet: {
|
|
522
543
|
fontSize: {
|
|
523
544
|
_mapping: true,
|
|
524
|
-
prop:
|
|
525
|
-
values: {
|
|
545
|
+
prop: 'size',
|
|
546
|
+
values: { '1': '132px', '2': '35px' },
|
|
526
547
|
} as StyleMapping,
|
|
527
|
-
}
|
|
528
|
-
}
|
|
548
|
+
},
|
|
549
|
+
},
|
|
529
550
|
};
|
|
530
|
-
const props = { size:
|
|
551
|
+
const props = { size: '2' };
|
|
531
552
|
|
|
532
553
|
const result = processStructure(structure, createContext(props));
|
|
533
554
|
const node = result as ComponentNode;
|
|
534
555
|
const style = node.style as any;
|
|
535
|
-
expect(style?.base?.fontSize).toBe(
|
|
536
|
-
expect(style?.tablet?.fontSize).toBe(
|
|
556
|
+
expect(style?.base?.fontSize).toBe('40px');
|
|
557
|
+
expect(style?.tablet?.fontSize).toBe('35px');
|
|
537
558
|
});
|
|
538
559
|
|
|
539
|
-
test(
|
|
560
|
+
test('should resolve multiple style mappings', () => {
|
|
540
561
|
const structure: ComponentNode = {
|
|
541
|
-
type:
|
|
542
|
-
tag:
|
|
562
|
+
type: 'node',
|
|
563
|
+
tag: 'button',
|
|
543
564
|
style: {
|
|
544
565
|
base: {
|
|
545
566
|
background: {
|
|
546
567
|
_mapping: true,
|
|
547
|
-
prop:
|
|
548
|
-
values: { primary:
|
|
568
|
+
prop: 'variant',
|
|
569
|
+
values: { primary: 'blue', secondary: 'gray' },
|
|
549
570
|
} as StyleMapping,
|
|
550
571
|
color: {
|
|
551
572
|
_mapping: true,
|
|
552
|
-
prop:
|
|
553
|
-
values: { primary:
|
|
554
|
-
} as StyleMapping
|
|
555
|
-
}
|
|
556
|
-
}
|
|
573
|
+
prop: 'variant',
|
|
574
|
+
values: { primary: 'white', secondary: 'black' },
|
|
575
|
+
} as StyleMapping,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
557
578
|
};
|
|
558
|
-
const props = { variant:
|
|
579
|
+
const props = { variant: 'primary' };
|
|
559
580
|
|
|
560
581
|
const result = processStructure(structure, createContext(props));
|
|
561
582
|
const node = result as ComponentNode;
|
|
562
583
|
const baseStyle = (node.style as any)?.base;
|
|
563
|
-
expect(baseStyle?.background).toBe(
|
|
564
|
-
expect(baseStyle?.color).toBe(
|
|
584
|
+
expect(baseStyle?.background).toBe('blue');
|
|
585
|
+
expect(baseStyle?.color).toBe('white');
|
|
565
586
|
});
|
|
566
587
|
});
|
|
567
588
|
|
|
568
|
-
describe(
|
|
569
|
-
test(
|
|
589
|
+
describe('Template evaluation in style objects', () => {
|
|
590
|
+
test('should evaluate templates in style values', () => {
|
|
591
|
+
// NOTE: the ternary deliberately avoids decimal literals like '0.5'.
|
|
592
|
+
// Since 2bd781ed, hasItemTemplates() treats any `\w+.\w+` inside a full
|
|
593
|
+
// {{...}} template as a potential item-field access and DEFERS evaluation
|
|
594
|
+
// to item-template processing — a decimal literal ('0.5') false-positives
|
|
595
|
+
// that check, so the template would pass through unresolved here. That
|
|
596
|
+
// false positive is a known product issue (see report); this test covers
|
|
597
|
+
// the eager-evaluation path that still works.
|
|
570
598
|
const structure: ComponentNode = {
|
|
571
|
-
type:
|
|
572
|
-
tag:
|
|
599
|
+
type: 'node',
|
|
600
|
+
tag: 'div',
|
|
573
601
|
style: {
|
|
574
602
|
base: {
|
|
575
|
-
opacity: "{{disabled ? '0
|
|
576
|
-
fontSize:
|
|
577
|
-
}
|
|
578
|
-
}
|
|
603
|
+
opacity: "{{disabled ? '0' : '1'}}",
|
|
604
|
+
fontSize: '{{size}}px',
|
|
605
|
+
},
|
|
606
|
+
},
|
|
579
607
|
};
|
|
580
608
|
const props = { disabled: true, size: 16 };
|
|
581
609
|
|
|
@@ -583,127 +611,159 @@ describe("Template Engine - processStructure", () => {
|
|
|
583
611
|
const node = result as ComponentNode;
|
|
584
612
|
const baseStyle = (node.style as any)?.base;
|
|
585
613
|
expect(baseStyle).toBeDefined();
|
|
586
|
-
expect(baseStyle?.opacity).toBe(
|
|
587
|
-
expect(baseStyle?.fontSize).toBe(
|
|
614
|
+
expect(baseStyle?.opacity).toBe('0');
|
|
615
|
+
expect(baseStyle?.fontSize).toBe('16px');
|
|
588
616
|
});
|
|
589
617
|
|
|
590
|
-
test(
|
|
618
|
+
test('defers full templates containing dotted terms (e.g. decimals) to item processing', () => {
|
|
619
|
+
// Documents the current (arguably buggy) behavior: '0.5' matches the
|
|
620
|
+
// item-field-access pattern in hasItemTemplates(), so processStructure
|
|
621
|
+
// leaves the whole template unresolved for later item-template passes —
|
|
622
|
+
// even though no item context exists. Reported as a product issue.
|
|
623
|
+
const structure: ComponentNode = {
|
|
624
|
+
type: 'node',
|
|
625
|
+
tag: 'div',
|
|
626
|
+
style: {
|
|
627
|
+
base: {
|
|
628
|
+
opacity: "{{disabled ? '0.5' : '1'}}",
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
const props = { disabled: true };
|
|
633
|
+
|
|
634
|
+
const result = processStructure(structure, createContext(props));
|
|
635
|
+
const node = result as ComponentNode;
|
|
636
|
+
const baseStyle = (node.style as any)?.base;
|
|
637
|
+
expect(baseStyle?.opacity).toBe("{{disabled ? '0.5' : '1'}}");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('should evaluate templates using global template context variables', () => {
|
|
591
641
|
setGlobalTemplateContext({ isEditorMode: true });
|
|
592
642
|
|
|
593
643
|
const structure: ComponentNode = {
|
|
594
|
-
type:
|
|
595
|
-
tag:
|
|
644
|
+
type: 'node',
|
|
645
|
+
tag: 'div',
|
|
596
646
|
style: {
|
|
597
647
|
base: {
|
|
598
|
-
display: "{{isEditorMode ? 'none' : 'block'}}"
|
|
599
|
-
}
|
|
600
|
-
}
|
|
648
|
+
display: "{{isEditorMode ? 'none' : 'block'}}",
|
|
649
|
+
},
|
|
650
|
+
},
|
|
601
651
|
};
|
|
602
652
|
|
|
603
653
|
const result = processStructure(structure, createContext({}));
|
|
604
654
|
const node = result as ComponentNode;
|
|
605
655
|
const baseStyle = (node.style as any)?.base;
|
|
606
|
-
expect(baseStyle?.display).toBe(
|
|
656
|
+
expect(baseStyle?.display).toBe('none');
|
|
607
657
|
|
|
608
658
|
resetGlobalTemplateContext();
|
|
609
659
|
});
|
|
610
660
|
});
|
|
611
661
|
|
|
612
|
-
describe(
|
|
613
|
-
test(
|
|
662
|
+
describe('Edge cases and error handling', () => {
|
|
663
|
+
test('should return null for null structure', () => {
|
|
614
664
|
const result = processStructure(null, createContext({}));
|
|
615
665
|
expect(result).toBeNull();
|
|
616
666
|
});
|
|
617
667
|
|
|
618
|
-
test(
|
|
668
|
+
test('should return null for undefined structure', () => {
|
|
619
669
|
const result = processStructure(undefined, createContext({}));
|
|
620
670
|
expect(result).toBeNull();
|
|
621
671
|
});
|
|
622
672
|
|
|
623
|
-
test(
|
|
673
|
+
test('should handle numbers in structure', () => {
|
|
624
674
|
const result = processStructure(42, createContext({}));
|
|
625
675
|
expect(result).toBe(42);
|
|
626
676
|
});
|
|
627
677
|
|
|
628
|
-
test(
|
|
678
|
+
test('should handle empty objects', () => {
|
|
629
679
|
const structure: ComponentNode = {
|
|
630
|
-
type:
|
|
631
|
-
tag:
|
|
632
|
-
children: []
|
|
680
|
+
type: 'node',
|
|
681
|
+
tag: 'div',
|
|
682
|
+
children: [],
|
|
633
683
|
};
|
|
634
684
|
|
|
635
685
|
const result = processStructure(structure, createContext({}));
|
|
636
686
|
const node = result as ComponentNode;
|
|
637
|
-
expect(node.tag).toBe(
|
|
638
|
-
expect(node.type).toBe(
|
|
687
|
+
expect(node.tag).toBe('div');
|
|
688
|
+
expect(node.type).toBe('node');
|
|
639
689
|
});
|
|
640
690
|
|
|
641
|
-
test(
|
|
691
|
+
test('should handle component definition context in templates', () => {
|
|
642
692
|
const componentDef = {
|
|
643
693
|
interface: {},
|
|
644
694
|
structure: { type: 'node' as const, tag: 'div' },
|
|
695
|
+
category: 'buttons',
|
|
645
696
|
variants: {
|
|
646
|
-
primary: { background:
|
|
647
|
-
}
|
|
697
|
+
primary: { background: 'blue' },
|
|
698
|
+
},
|
|
648
699
|
} as unknown as StructuredComponentDefinition & { variants?: Record<string, any> };
|
|
649
|
-
const structure = "{{variants.primary.background}}";
|
|
650
700
|
const props = {};
|
|
651
701
|
|
|
652
|
-
|
|
653
|
-
|
|
702
|
+
// componentDef fields are merged into the eval context, so a non-dotted
|
|
703
|
+
// full template referencing one resolves eagerly.
|
|
704
|
+
expect(processStructure('{{category}}', createContext(props, componentDef))).toBe('buttons');
|
|
705
|
+
|
|
706
|
+
// Dotted paths like {{variants.primary.background}} are NO LONGER eagerly
|
|
707
|
+
// resolved: since 2bd781ed, hasItemTemplates() reserves `\w+.\w+` paths
|
|
708
|
+
// for item-template processing (e.g. {{post.title}} inside lists), so
|
|
709
|
+
// processStructure passes them through unresolved. Previously this
|
|
710
|
+
// returned "blue".
|
|
711
|
+
expect(processStructure('{{variants.primary.background}}', createContext(props, componentDef))).toBe(
|
|
712
|
+
'{{variants.primary.background}}',
|
|
713
|
+
);
|
|
654
714
|
});
|
|
655
715
|
});
|
|
656
716
|
|
|
657
|
-
describe(
|
|
658
|
-
test(
|
|
717
|
+
describe('Integration with Button.json example', () => {
|
|
718
|
+
test('should process Button component structure with style mappings', () => {
|
|
659
719
|
// Based on components/Button.json
|
|
660
720
|
const structure: ComponentNode = {
|
|
661
|
-
type:
|
|
662
|
-
tag:
|
|
721
|
+
type: 'node',
|
|
722
|
+
tag: 'button',
|
|
663
723
|
style: {
|
|
664
724
|
base: {
|
|
665
725
|
padding: {
|
|
666
726
|
_mapping: true,
|
|
667
|
-
prop:
|
|
727
|
+
prop: 'variant',
|
|
668
728
|
values: {
|
|
669
|
-
primary:
|
|
670
|
-
secondary:
|
|
671
|
-
}
|
|
729
|
+
primary: '16px 34px',
|
|
730
|
+
secondary: '16px 34px',
|
|
731
|
+
},
|
|
672
732
|
} as StyleMapping,
|
|
673
733
|
background: {
|
|
674
734
|
_mapping: true,
|
|
675
|
-
prop:
|
|
735
|
+
prop: 'variant',
|
|
676
736
|
values: {
|
|
677
|
-
primary:
|
|
678
|
-
secondary:
|
|
679
|
-
}
|
|
737
|
+
primary: 'blue',
|
|
738
|
+
secondary: '#f3f4f6',
|
|
739
|
+
},
|
|
680
740
|
} as StyleMapping,
|
|
681
741
|
color: {
|
|
682
742
|
_mapping: true,
|
|
683
|
-
prop:
|
|
743
|
+
prop: 'variant',
|
|
684
744
|
values: {
|
|
685
|
-
primary:
|
|
686
|
-
secondary:
|
|
687
|
-
}
|
|
745
|
+
primary: 'white',
|
|
746
|
+
secondary: '#374151',
|
|
747
|
+
},
|
|
688
748
|
} as StyleMapping,
|
|
689
|
-
borderRadius:
|
|
690
|
-
fontSize:
|
|
691
|
-
}
|
|
749
|
+
borderRadius: '0px',
|
|
750
|
+
fontSize: '15px',
|
|
751
|
+
},
|
|
692
752
|
},
|
|
693
|
-
children:
|
|
753
|
+
children: '{{children}}',
|
|
694
754
|
} as unknown as ComponentNode;
|
|
695
|
-
const props = { variant:
|
|
755
|
+
const props = { variant: 'primary', children: 'Click me' };
|
|
696
756
|
|
|
697
757
|
const result = processStructure(structure, createContext(props));
|
|
698
758
|
const node = result as ComponentNode;
|
|
699
759
|
|
|
700
760
|
const baseStyle = (node.style as any)?.base;
|
|
701
|
-
expect(baseStyle?.background).toBe(
|
|
702
|
-
expect(baseStyle?.color).toBe(
|
|
703
|
-
expect(baseStyle?.padding).toBe(
|
|
761
|
+
expect(baseStyle?.background).toBe('blue');
|
|
762
|
+
expect(baseStyle?.color).toBe('white');
|
|
763
|
+
expect(baseStyle?.padding).toBe('16px 34px');
|
|
704
764
|
// processStructure converts string children template to evaluated result (array)
|
|
705
765
|
if (Array.isArray(node.children)) {
|
|
706
|
-
expect(node.children).toEqual([
|
|
766
|
+
expect(node.children).toEqual(['Click me']);
|
|
707
767
|
} else {
|
|
708
768
|
// If not array, it might still be the template string or evaluated string
|
|
709
769
|
expect(node.children).toBeDefined();
|
|
@@ -712,143 +772,145 @@ describe("Template Engine - processStructure", () => {
|
|
|
712
772
|
});
|
|
713
773
|
});
|
|
714
774
|
|
|
715
|
-
describe(
|
|
775
|
+
describe('Template Engine - parentProps cascade', () => {
|
|
716
776
|
const mockComponentDef: StructuredComponentDefinition = {
|
|
717
777
|
interface: {},
|
|
718
|
-
structure: { type: 'node' as const, tag: 'div' }
|
|
778
|
+
structure: { type: 'node' as const, tag: 'div' },
|
|
719
779
|
};
|
|
720
780
|
|
|
721
781
|
test("falls back to parentProps when child doesn't declare the prop", () => {
|
|
722
782
|
const structure: ComponentNode = {
|
|
723
|
-
type:
|
|
724
|
-
tag:
|
|
725
|
-
children: [
|
|
783
|
+
type: 'node',
|
|
784
|
+
tag: 'span',
|
|
785
|
+
children: ['{{ctaText}}'],
|
|
726
786
|
};
|
|
727
787
|
const ctx: TemplateContext = {
|
|
728
788
|
props: {},
|
|
729
|
-
parentProps: { ctaText:
|
|
730
|
-
componentDef: mockComponentDef
|
|
789
|
+
parentProps: { ctaText: 'Take the assessment' },
|
|
790
|
+
componentDef: mockComponentDef,
|
|
731
791
|
};
|
|
732
792
|
|
|
733
793
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
734
|
-
expect(result.children).toEqual([
|
|
794
|
+
expect(result.children).toEqual(['Take the assessment']);
|
|
735
795
|
});
|
|
736
796
|
|
|
737
|
-
test(
|
|
797
|
+
test('child props win over parentProps on key collision', () => {
|
|
738
798
|
const structure: ComponentNode = {
|
|
739
|
-
type:
|
|
740
|
-
tag:
|
|
741
|
-
children: [
|
|
799
|
+
type: 'node',
|
|
800
|
+
tag: 'span',
|
|
801
|
+
children: ['{{label}}'],
|
|
742
802
|
};
|
|
743
803
|
const ctx: TemplateContext = {
|
|
744
|
-
props: { label:
|
|
745
|
-
parentProps: { label:
|
|
746
|
-
componentDef: mockComponentDef
|
|
804
|
+
props: { label: 'child' },
|
|
805
|
+
parentProps: { label: 'parent' },
|
|
806
|
+
componentDef: mockComponentDef,
|
|
747
807
|
};
|
|
748
808
|
|
|
749
809
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
750
|
-
expect(result.children).toEqual([
|
|
810
|
+
expect(result.children).toEqual(['child']);
|
|
751
811
|
});
|
|
752
812
|
|
|
753
|
-
test(
|
|
813
|
+
test('partial templates (prefix-{{x}}) also cascade', () => {
|
|
754
814
|
const structure: ComponentNode = {
|
|
755
|
-
type:
|
|
756
|
-
tag:
|
|
757
|
-
attributes: { class:
|
|
815
|
+
type: 'node',
|
|
816
|
+
tag: 'div',
|
|
817
|
+
attributes: { class: 'btn-{{variant}}' },
|
|
758
818
|
};
|
|
759
819
|
const ctx: TemplateContext = {
|
|
760
820
|
props: {},
|
|
761
|
-
parentProps: { variant:
|
|
762
|
-
componentDef: mockComponentDef
|
|
821
|
+
parentProps: { variant: 'primary' },
|
|
822
|
+
componentDef: mockComponentDef,
|
|
763
823
|
};
|
|
764
824
|
|
|
765
825
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
766
|
-
expect((result as any).attributes?.class).toBe(
|
|
826
|
+
expect((result as any).attributes?.class).toBe('btn-primary');
|
|
767
827
|
});
|
|
768
828
|
|
|
769
|
-
test(
|
|
829
|
+
test('style mappings cascade against parentProps', () => {
|
|
770
830
|
const structure: ComponentNode = {
|
|
771
|
-
type:
|
|
772
|
-
tag:
|
|
831
|
+
type: 'node',
|
|
832
|
+
tag: 'div',
|
|
773
833
|
style: {
|
|
774
834
|
base: {
|
|
775
835
|
color: {
|
|
776
836
|
_mapping: true,
|
|
777
|
-
prop:
|
|
778
|
-
values: { default:
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
837
|
+
prop: 'variant',
|
|
838
|
+
values: { default: 'black', featured: 'white' },
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
},
|
|
782
842
|
} as ComponentNode;
|
|
783
843
|
const ctx: TemplateContext = {
|
|
784
844
|
props: {},
|
|
785
|
-
parentProps: { variant:
|
|
786
|
-
componentDef: mockComponentDef
|
|
845
|
+
parentProps: { variant: 'featured' },
|
|
846
|
+
componentDef: mockComponentDef,
|
|
787
847
|
};
|
|
788
848
|
|
|
789
849
|
const result = processStructure(structure, ctx) as ComponentNode;
|
|
790
850
|
const baseStyle = (result.style as any)?.base;
|
|
791
|
-
expect(baseStyle?.color).toBe(
|
|
851
|
+
expect(baseStyle?.color).toBe('white');
|
|
792
852
|
});
|
|
793
853
|
});
|
|
794
854
|
|
|
795
|
-
describe(
|
|
855
|
+
describe('Template Engine - Slot Default Values', () => {
|
|
796
856
|
const mockComponentDef: StructuredComponentDefinition = {
|
|
797
857
|
interface: {},
|
|
798
|
-
structure: { type: 'node' as const, tag: 'div' }
|
|
858
|
+
structure: { type: 'node' as const, tag: 'div' },
|
|
799
859
|
};
|
|
800
860
|
|
|
801
861
|
const createContext = (props: Record<string, unknown>): TemplateContext => ({
|
|
802
862
|
props,
|
|
803
|
-
componentDef: mockComponentDef
|
|
863
|
+
componentDef: mockComponentDef,
|
|
804
864
|
});
|
|
805
865
|
|
|
806
|
-
test(
|
|
866
|
+
test('should render slot default when no instance children provided', () => {
|
|
807
867
|
const structure = [
|
|
808
|
-
{
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
868
|
+
{
|
|
869
|
+
type: 'node',
|
|
870
|
+
tag: 'div',
|
|
871
|
+
children: [
|
|
872
|
+
{ type: 'node', tag: 'h1', children: 'Header' },
|
|
873
|
+
{
|
|
874
|
+
type: 'slot',
|
|
875
|
+
default: [{ type: 'node', tag: 'p', children: 'Default paragraph' }],
|
|
876
|
+
},
|
|
877
|
+
{ type: 'node', tag: 'footer', children: 'Footer' },
|
|
878
|
+
],
|
|
879
|
+
},
|
|
818
880
|
] as unknown as ComponentNode[];
|
|
819
881
|
|
|
820
882
|
const result = processStructure(structure, createContext({}), undefined, undefined);
|
|
821
883
|
expect(result).toBeInstanceOf(Array);
|
|
822
884
|
|
|
823
885
|
const [root] = result as ComponentNode[];
|
|
824
|
-
expect(root.tag).toBe(
|
|
886
|
+
expect(root.tag).toBe('div');
|
|
825
887
|
expect(root.children).toBeInstanceOf(Array);
|
|
826
888
|
|
|
827
889
|
const children = root.children as ComponentNode[];
|
|
828
890
|
expect(children).toHaveLength(3);
|
|
829
|
-
expect(children[0].tag).toBe(
|
|
830
|
-
expect((children[1] as any).tag).toBe(
|
|
831
|
-
expect((children[1] as any).children).toEqual([
|
|
832
|
-
expect(children[2].tag).toBe(
|
|
891
|
+
expect(children[0].tag).toBe('h1');
|
|
892
|
+
expect((children[1] as any).tag).toBe('p'); // Default content
|
|
893
|
+
expect((children[1] as any).children).toEqual(['Default paragraph']);
|
|
894
|
+
expect(children[2].tag).toBe('footer');
|
|
833
895
|
});
|
|
834
896
|
|
|
835
|
-
test(
|
|
897
|
+
test('should render instance children instead of slot default when provided', () => {
|
|
836
898
|
const structure = [
|
|
837
|
-
{
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
899
|
+
{
|
|
900
|
+
type: 'node',
|
|
901
|
+
tag: 'div',
|
|
902
|
+
children: [
|
|
903
|
+
{ type: 'node', tag: 'h1', children: 'Header' },
|
|
904
|
+
{
|
|
905
|
+
type: 'slot',
|
|
906
|
+
default: [{ type: 'node', tag: 'p', children: 'Default paragraph' }],
|
|
907
|
+
},
|
|
908
|
+
{ type: 'node', tag: 'footer', children: 'Footer' },
|
|
909
|
+
],
|
|
910
|
+
},
|
|
847
911
|
] as unknown as ComponentNode[];
|
|
848
912
|
|
|
849
|
-
const instanceChildren = [
|
|
850
|
-
{ type: "node", tag: "span", children: "Custom content" }
|
|
851
|
-
] as ComponentNode[];
|
|
913
|
+
const instanceChildren = [{ type: 'node', tag: 'span', children: 'Custom content' }] as ComponentNode[];
|
|
852
914
|
|
|
853
915
|
const result = processStructure(structure, createContext({}), undefined, instanceChildren);
|
|
854
916
|
expect(result).toBeInstanceOf(Array);
|
|
@@ -857,36 +919,44 @@ describe("Template Engine - Slot Default Values", () => {
|
|
|
857
919
|
const children = root.children as ComponentNode[];
|
|
858
920
|
|
|
859
921
|
expect(children).toHaveLength(3);
|
|
860
|
-
expect(children[0].tag).toBe(
|
|
861
|
-
expect((children[1] as any).tag).toBe(
|
|
862
|
-
expect((children[1] as any).children).toEqual([
|
|
863
|
-
expect(children[2].tag).toBe(
|
|
922
|
+
expect(children[0].tag).toBe('h1');
|
|
923
|
+
expect((children[1] as any).tag).toBe('span'); // Instance content, not default
|
|
924
|
+
expect((children[1] as any).children).toEqual(['Custom content']);
|
|
925
|
+
expect(children[2].tag).toBe('footer');
|
|
864
926
|
});
|
|
865
927
|
|
|
866
|
-
test(
|
|
928
|
+
test('should handle string default for slot', () => {
|
|
867
929
|
const structure = [
|
|
868
|
-
{
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
930
|
+
{
|
|
931
|
+
type: 'node',
|
|
932
|
+
tag: 'div',
|
|
933
|
+
children: [
|
|
934
|
+
{
|
|
935
|
+
type: 'slot',
|
|
936
|
+
default: 'Default text content',
|
|
937
|
+
},
|
|
938
|
+
],
|
|
939
|
+
},
|
|
874
940
|
] as unknown as ComponentNode[];
|
|
875
941
|
|
|
876
942
|
const result = processStructure(structure, createContext({}), undefined, undefined);
|
|
877
943
|
const [root] = result as ComponentNode[];
|
|
878
944
|
const children = root.children as string[];
|
|
879
945
|
|
|
880
|
-
expect(children).toContain(
|
|
946
|
+
expect(children).toContain('Default text content');
|
|
881
947
|
});
|
|
882
948
|
|
|
883
|
-
test(
|
|
949
|
+
test('should render nothing when no instance children and no default', () => {
|
|
884
950
|
const structure = [
|
|
885
|
-
{
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
951
|
+
{
|
|
952
|
+
type: 'node',
|
|
953
|
+
tag: 'div',
|
|
954
|
+
children: [
|
|
955
|
+
{ type: 'node', tag: 'h1', children: 'Header' },
|
|
956
|
+
{ type: 'slot' }, // No default
|
|
957
|
+
{ type: 'node', tag: 'footer', children: 'Footer' },
|
|
958
|
+
],
|
|
959
|
+
},
|
|
890
960
|
] as unknown as ComponentNode[];
|
|
891
961
|
|
|
892
962
|
const result = processStructure(structure, createContext({}), undefined, undefined);
|
|
@@ -894,59 +964,61 @@ describe("Template Engine - Slot Default Values", () => {
|
|
|
894
964
|
const children = root.children as ComponentNode[];
|
|
895
965
|
|
|
896
966
|
expect(children).toHaveLength(2); // Only h1 and footer, slot is removed
|
|
897
|
-
expect(children[0].tag).toBe(
|
|
898
|
-
expect(children[1].tag).toBe(
|
|
967
|
+
expect(children[0].tag).toBe('h1');
|
|
968
|
+
expect(children[1].tag).toBe('footer');
|
|
899
969
|
});
|
|
900
970
|
|
|
901
|
-
test(
|
|
971
|
+
test('should process templates in slot default content', () => {
|
|
902
972
|
const structure = [
|
|
903
|
-
{
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
973
|
+
{
|
|
974
|
+
type: 'node',
|
|
975
|
+
tag: 'div',
|
|
976
|
+
children: [
|
|
977
|
+
{
|
|
978
|
+
type: 'slot',
|
|
979
|
+
default: [{ type: 'node', tag: 'p', children: 'Hello {{name}}' }],
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
},
|
|
911
983
|
] as unknown as ComponentNode[];
|
|
912
984
|
|
|
913
|
-
const result = processStructure(structure, createContext({ name:
|
|
985
|
+
const result = processStructure(structure, createContext({ name: 'World' }), undefined, undefined);
|
|
914
986
|
const [root] = result as ComponentNode[];
|
|
915
987
|
const children = root.children as ComponentNode[];
|
|
916
988
|
|
|
917
|
-
expect((children[0] as any).children).toEqual([
|
|
989
|
+
expect((children[0] as any).children).toEqual(['Hello World']);
|
|
918
990
|
});
|
|
919
991
|
});
|
|
920
992
|
|
|
921
|
-
describe(
|
|
922
|
-
test(
|
|
993
|
+
describe('Template Engine - normalizeStyle', () => {
|
|
994
|
+
test('should normalize flat style objects', () => {
|
|
923
995
|
const style = {
|
|
924
|
-
color:
|
|
925
|
-
fontSize:
|
|
996
|
+
color: 'red',
|
|
997
|
+
fontSize: '16px',
|
|
926
998
|
};
|
|
927
|
-
|
|
999
|
+
|
|
928
1000
|
const result = normalizeStyle(style);
|
|
929
1001
|
expect(result).toEqual(style);
|
|
930
1002
|
});
|
|
931
1003
|
|
|
932
1004
|
test("should normalize responsive style objects using 'all' strategy", () => {
|
|
933
1005
|
const style = {
|
|
934
|
-
base: { color:
|
|
935
|
-
tablet: { color:
|
|
936
|
-
mobile: { color:
|
|
1006
|
+
base: { color: 'red' },
|
|
1007
|
+
tablet: { color: 'blue' },
|
|
1008
|
+
mobile: { color: 'green' },
|
|
937
1009
|
};
|
|
938
|
-
|
|
1010
|
+
|
|
939
1011
|
const result = normalizeStyle(style);
|
|
940
1012
|
expect(result).toBeDefined();
|
|
941
|
-
expect(result?.color).toBe(
|
|
1013
|
+
expect(result?.color).toBe('green'); // 'all' strategy merges all, mobile wins
|
|
942
1014
|
});
|
|
943
1015
|
|
|
944
|
-
test(
|
|
1016
|
+
test('should return null for null style', () => {
|
|
945
1017
|
const result = normalizeStyle(null);
|
|
946
1018
|
expect(result).toBeNull();
|
|
947
1019
|
});
|
|
948
1020
|
|
|
949
|
-
test(
|
|
1021
|
+
test('should return null for undefined style', () => {
|
|
950
1022
|
const result = normalizeStyle(undefined);
|
|
951
1023
|
expect(result).toBeNull();
|
|
952
1024
|
});
|
|
@@ -957,175 +1029,175 @@ describe("Template Engine - normalizeStyle", () => {
|
|
|
957
1029
|
// These exercise specific type-cast paths to ensure safety before refactoring
|
|
958
1030
|
// ==========================================================================
|
|
959
1031
|
|
|
960
|
-
describe(
|
|
1032
|
+
describe('Template Engine - processStructure as-any paths', () => {
|
|
961
1033
|
const baseContext: TemplateContext = {
|
|
962
|
-
props: { text:
|
|
1034
|
+
props: { text: 'Hello', size: 'lg', variant: 'primary' },
|
|
963
1035
|
componentDef: {} as StructuredComponentDefinition,
|
|
964
1036
|
};
|
|
965
1037
|
|
|
966
|
-
describe(
|
|
967
|
-
test(
|
|
1038
|
+
describe('Boolean preservation (line 429)', () => {
|
|
1039
|
+
test('should preserve false boolean values in structure', () => {
|
|
968
1040
|
// processStructure should return false as-is, not convert to null
|
|
969
1041
|
const result = processStructure(false as unknown as ComponentNode, baseContext);
|
|
970
1042
|
expect(result as unknown).toBe(false);
|
|
971
1043
|
});
|
|
972
1044
|
|
|
973
|
-
test(
|
|
1045
|
+
test('should preserve true boolean values in structure', () => {
|
|
974
1046
|
const result = processStructure(true as unknown as ComponentNode, baseContext);
|
|
975
1047
|
expect(result as unknown).toBe(true);
|
|
976
1048
|
});
|
|
977
1049
|
});
|
|
978
1050
|
|
|
979
|
-
describe(
|
|
980
|
-
test(
|
|
1051
|
+
describe('Object template evaluation returning objects (line 454)', () => {
|
|
1052
|
+
test('should return object result from full template expression', () => {
|
|
981
1053
|
const context: TemplateContext = {
|
|
982
|
-
props: { link: { href:
|
|
1054
|
+
props: { link: { href: '/about', target: '_blank' } },
|
|
983
1055
|
componentDef: {} as StructuredComponentDefinition,
|
|
984
1056
|
};
|
|
985
1057
|
// Full template {{link}} should return the object as-is
|
|
986
|
-
const result = processStructure(
|
|
987
|
-
expect(result).toEqual({ href:
|
|
1058
|
+
const result = processStructure('{{link}}', context);
|
|
1059
|
+
expect(result).toEqual({ href: '/about', target: '_blank' });
|
|
988
1060
|
});
|
|
989
1061
|
});
|
|
990
1062
|
|
|
991
1063
|
describe("Slot default content via 'default' property (lines 483-486, 516-517)", () => {
|
|
992
|
-
test(
|
|
1064
|
+
test('should render slot default array when no instance children', () => {
|
|
993
1065
|
// Slot markers use type: "slot" (NODE_TYPE.SLOT)
|
|
994
1066
|
const structure = [
|
|
995
|
-
{
|
|
996
|
-
|
|
997
|
-
|
|
1067
|
+
{
|
|
1068
|
+
type: 'node',
|
|
1069
|
+
tag: 'div',
|
|
1070
|
+
children: [{ type: 'slot', default: [{ type: 'node', tag: 'span', children: 'fallback' }] }],
|
|
1071
|
+
},
|
|
998
1072
|
] as unknown as ComponentNode[];
|
|
999
1073
|
const result = processStructure(structure, baseContext);
|
|
1000
1074
|
expect(Array.isArray(result)).toBe(true);
|
|
1001
1075
|
const root = (result as any[])[0];
|
|
1002
|
-
expect(root.tag).toBe(
|
|
1076
|
+
expect(root.tag).toBe('div');
|
|
1003
1077
|
// The slot default should have been rendered
|
|
1004
1078
|
expect(root.children.length).toBe(1);
|
|
1005
|
-
expect(root.children[0].tag).toBe(
|
|
1079
|
+
expect(root.children[0].tag).toBe('span');
|
|
1006
1080
|
});
|
|
1007
1081
|
|
|
1008
|
-
test(
|
|
1082
|
+
test('should render slot default string when no instance children', () => {
|
|
1009
1083
|
const structure = [
|
|
1010
|
-
{ type:
|
|
1011
|
-
{ type: "slot", default: "Default text" }
|
|
1012
|
-
]}
|
|
1084
|
+
{ type: 'node', tag: 'div', children: [{ type: 'slot', default: 'Default text' }] },
|
|
1013
1085
|
] as unknown as ComponentNode[];
|
|
1014
1086
|
const result = processStructure(structure, baseContext);
|
|
1015
1087
|
const root = (result as any[])[0];
|
|
1016
|
-
expect(root.children[0]).toBe(
|
|
1088
|
+
expect(root.children[0]).toBe('Default text');
|
|
1017
1089
|
});
|
|
1018
1090
|
|
|
1019
|
-
test(
|
|
1091
|
+
test('should use instance children over slot default', () => {
|
|
1020
1092
|
const structure = [
|
|
1021
|
-
{
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
{ type: "node", tag: "b", children: "override" }
|
|
1093
|
+
{
|
|
1094
|
+
type: 'node',
|
|
1095
|
+
tag: 'div',
|
|
1096
|
+
children: [{ type: 'slot', default: [{ type: 'node', tag: 'span', children: 'fallback' }] }],
|
|
1097
|
+
},
|
|
1027
1098
|
] as unknown as ComponentNode[];
|
|
1099
|
+
const instanceChildren = [{ type: 'node', tag: 'b', children: 'override' }] as unknown as ComponentNode[];
|
|
1028
1100
|
const result = processStructure(structure, baseContext, undefined, instanceChildren);
|
|
1029
1101
|
const root = (result as any[])[0];
|
|
1030
|
-
expect(root.children[0].tag).toBe(
|
|
1102
|
+
expect(root.children[0].tag).toBe('b');
|
|
1031
1103
|
});
|
|
1032
1104
|
|
|
1033
|
-
test(
|
|
1105
|
+
test('should handle slot marker as standalone object with default', () => {
|
|
1034
1106
|
const slotMarker = {
|
|
1035
|
-
type:
|
|
1036
|
-
default: [{ type:
|
|
1107
|
+
type: 'slot',
|
|
1108
|
+
default: [{ type: 'node', tag: 'div', children: 'content' }],
|
|
1037
1109
|
};
|
|
1038
1110
|
const result = processStructure(slotMarker as any, baseContext);
|
|
1039
1111
|
expect(result).toBeDefined();
|
|
1040
1112
|
});
|
|
1041
1113
|
});
|
|
1042
1114
|
|
|
1043
|
-
describe(
|
|
1044
|
-
test(
|
|
1115
|
+
describe('Plain object processing (lines 538, 543)', () => {
|
|
1116
|
+
test('should process plain objects recursively and resolve templates', () => {
|
|
1045
1117
|
// An object without a valid node type gets treated as a plain props object
|
|
1046
|
-
const structure = { label:
|
|
1118
|
+
const structure = { label: '{{text}}', visible: true } as any;
|
|
1047
1119
|
const result = processStructure(structure, baseContext);
|
|
1048
1120
|
expect(result).toBeDefined();
|
|
1049
|
-
expect((result as any).label).toBe(
|
|
1121
|
+
expect((result as any).label).toBe('Hello');
|
|
1050
1122
|
expect((result as any).visible).toBe(true);
|
|
1051
1123
|
});
|
|
1052
1124
|
|
|
1053
|
-
test(
|
|
1125
|
+
test('should strip null/undefined values from plain objects', () => {
|
|
1054
1126
|
const context: TemplateContext = {
|
|
1055
|
-
props: { defined:
|
|
1127
|
+
props: { defined: 'value' },
|
|
1056
1128
|
componentDef: {} as StructuredComponentDefinition,
|
|
1057
1129
|
};
|
|
1058
|
-
const structure = { a:
|
|
1130
|
+
const structure = { a: '{{defined}}', b: '{{missing}}' } as any;
|
|
1059
1131
|
const result = processStructure(structure, context);
|
|
1060
|
-
expect((result as any).a).toBe(
|
|
1132
|
+
expect((result as any).a).toBe('value');
|
|
1061
1133
|
// Missing props resolve to empty string, not null
|
|
1062
1134
|
});
|
|
1063
1135
|
});
|
|
1064
1136
|
|
|
1065
|
-
describe(
|
|
1066
|
-
test(
|
|
1137
|
+
describe('Embed node creation (line 562)', () => {
|
|
1138
|
+
test('should process embed node with html content', () => {
|
|
1067
1139
|
const node: ComponentNode = {
|
|
1068
|
-
type:
|
|
1140
|
+
type: 'embed' as any,
|
|
1069
1141
|
html: "<script>alert('test')</script>",
|
|
1070
1142
|
} as any;
|
|
1071
1143
|
const result = processStructure(node, baseContext);
|
|
1072
1144
|
expect(result).toBeDefined();
|
|
1073
|
-
expect((result as any).type).toBe(
|
|
1145
|
+
expect((result as any).type).toBe('embed');
|
|
1074
1146
|
expect((result as any).html).toBe("<script>alert('test')</script>");
|
|
1075
1147
|
});
|
|
1076
1148
|
});
|
|
1077
1149
|
|
|
1078
|
-
describe(
|
|
1079
|
-
test(
|
|
1150
|
+
describe('Link node creation (line 569)', () => {
|
|
1151
|
+
test('should process link node with href', () => {
|
|
1080
1152
|
const node = {
|
|
1081
|
-
type:
|
|
1082
|
-
href:
|
|
1083
|
-
children: [
|
|
1153
|
+
type: 'link',
|
|
1154
|
+
href: '/about',
|
|
1155
|
+
children: ['About us'],
|
|
1084
1156
|
} as any;
|
|
1085
1157
|
const result = processStructure(node, baseContext);
|
|
1086
1158
|
expect(result).toBeDefined();
|
|
1087
|
-
expect((result as any).type).toBe(
|
|
1159
|
+
expect((result as any).type).toBe('link');
|
|
1088
1160
|
});
|
|
1089
1161
|
|
|
1090
|
-
test(
|
|
1162
|
+
test('should process link node with template href', () => {
|
|
1091
1163
|
const context: TemplateContext = {
|
|
1092
|
-
props: { url:
|
|
1164
|
+
props: { url: '/contact' },
|
|
1093
1165
|
componentDef: {} as StructuredComponentDefinition,
|
|
1094
1166
|
};
|
|
1095
1167
|
const node = {
|
|
1096
|
-
type:
|
|
1097
|
-
href:
|
|
1098
|
-
children: [
|
|
1168
|
+
type: 'link',
|
|
1169
|
+
href: '{{url}}',
|
|
1170
|
+
children: ['Contact'],
|
|
1099
1171
|
} as any;
|
|
1100
1172
|
const result = processStructure(node, context);
|
|
1101
1173
|
expect(result).toBeDefined();
|
|
1102
|
-
expect((result as any).type).toBe(
|
|
1174
|
+
expect((result as any).type).toBe('link');
|
|
1103
1175
|
});
|
|
1104
1176
|
});
|
|
1105
1177
|
|
|
1106
|
-
describe(
|
|
1107
|
-
test(
|
|
1178
|
+
describe('Locale-list node creation (line 574)', () => {
|
|
1179
|
+
test('should process locale-list node', () => {
|
|
1108
1180
|
const node = {
|
|
1109
|
-
type:
|
|
1110
|
-
style: { display:
|
|
1181
|
+
type: 'locale-list',
|
|
1182
|
+
style: { display: 'flex' },
|
|
1111
1183
|
} as any;
|
|
1112
1184
|
const result = processStructure(node, baseContext);
|
|
1113
1185
|
expect(result).toBeDefined();
|
|
1114
|
-
expect((result as any).type).toBe(
|
|
1186
|
+
expect((result as any).type).toBe('locale-list');
|
|
1115
1187
|
});
|
|
1116
1188
|
});
|
|
1117
1189
|
|
|
1118
|
-
describe(
|
|
1119
|
-
test(
|
|
1190
|
+
describe('List node with legacy cms-list type (line 575)', () => {
|
|
1191
|
+
test('should process list node', () => {
|
|
1120
1192
|
const node = {
|
|
1121
|
-
type:
|
|
1122
|
-
source:
|
|
1123
|
-
sourceType:
|
|
1124
|
-
children: [{ type:
|
|
1193
|
+
type: 'list',
|
|
1194
|
+
source: 'items',
|
|
1195
|
+
sourceType: 'prop',
|
|
1196
|
+
children: [{ type: 'html' as const, tag: 'div', children: [] }],
|
|
1125
1197
|
} as any;
|
|
1126
1198
|
const result = processStructure(node, baseContext);
|
|
1127
1199
|
expect(result).toBeDefined();
|
|
1128
|
-
expect((result as any).type).toBe(
|
|
1200
|
+
expect((result as any).type).toBe('list');
|
|
1129
1201
|
});
|
|
1130
1202
|
});
|
|
1131
1203
|
});
|