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,14 +1,14 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, mock } from
|
|
2
|
-
import { ComponentBuilder } from
|
|
3
|
-
import { ComponentRegistry } from
|
|
4
|
-
import { ElementRegistry } from
|
|
5
|
-
import type { ComponentNode, ListNode, CMSItem } from
|
|
6
|
-
import { NODE_TYPE } from
|
|
7
|
-
import { createMockElementRegistry } from
|
|
1
|
+
import { test, expect, describe, beforeEach, mock } from 'bun:test';
|
|
2
|
+
import { ComponentBuilder } from './ComponentBuilder';
|
|
3
|
+
import { ComponentRegistry } from '../componentRegistry';
|
|
4
|
+
import type { ElementRegistry } from '../elementRegistry';
|
|
5
|
+
import type { ComponentNode, ListNode, CMSItem } from '../../shared/types';
|
|
6
|
+
import { NODE_TYPE } from '../../shared/constants';
|
|
7
|
+
import { createMockElementRegistry } from '../../test-utils/mocks';
|
|
8
8
|
|
|
9
9
|
// Note: Using typed mocks from test-utils/mocks instead of inline 'as any' casts
|
|
10
10
|
|
|
11
|
-
describe(
|
|
11
|
+
describe('ComponentBuilder', () => {
|
|
12
12
|
let componentRegistry: ComponentRegistry;
|
|
13
13
|
let elementRegistry: ElementRegistry;
|
|
14
14
|
let builder: ComponentBuilder;
|
|
@@ -32,52 +32,52 @@ describe("ComponentBuilder", () => {
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
describe(
|
|
36
|
-
test(
|
|
35
|
+
describe('buildComponent - Basic Cases', () => {
|
|
36
|
+
test('should return null for null node', () => {
|
|
37
37
|
const result = builder.buildComponent({ node: null });
|
|
38
38
|
expect(result).toBeNull();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test(
|
|
41
|
+
test('should return null for undefined node', () => {
|
|
42
42
|
const result = builder.buildComponent({ node: undefined });
|
|
43
43
|
expect(result).toBeNull();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
test(
|
|
47
|
-
const result = builder.buildComponent({ node:
|
|
48
|
-
expect(result).toBe(
|
|
46
|
+
test('should return string for string node', () => {
|
|
47
|
+
const result = builder.buildComponent({ node: 'Hello' });
|
|
48
|
+
expect(result).toBe('Hello');
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test(
|
|
51
|
+
test('should return number for number node', () => {
|
|
52
52
|
const result = builder.buildComponent({ node: 42 });
|
|
53
53
|
expect(result).toBe(42);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test(
|
|
56
|
+
test('should return array for array of nodes', () => {
|
|
57
57
|
const nodes: ComponentNode[] = [
|
|
58
|
-
{ type:
|
|
59
|
-
{ type:
|
|
58
|
+
{ type: 'node', tag: 'div', children: ['Hello'] },
|
|
59
|
+
{ type: 'node', tag: 'span', children: ['World'] },
|
|
60
60
|
];
|
|
61
61
|
const result = builder.buildComponent({ node: nodes });
|
|
62
62
|
expect(Array.isArray(result)).toBe(true);
|
|
63
63
|
expect((result as any[]).length).toBe(2);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
test(
|
|
66
|
+
test('should return ReactElement for HTML node', () => {
|
|
67
67
|
const node: ComponentNode = {
|
|
68
|
-
type:
|
|
69
|
-
tag:
|
|
70
|
-
children: [
|
|
68
|
+
type: 'node',
|
|
69
|
+
tag: 'div',
|
|
70
|
+
children: ['Test'],
|
|
71
71
|
};
|
|
72
72
|
const result = builder.buildComponent({ node });
|
|
73
73
|
expect(result).not.toBeNull();
|
|
74
|
-
expect(typeof result).toBe(
|
|
74
|
+
expect(typeof result).toBe('object');
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
test(
|
|
77
|
+
test('should handle HTML node with empty tag gracefully', () => {
|
|
78
78
|
const node: ComponentNode = {
|
|
79
|
-
type:
|
|
80
|
-
tag:
|
|
79
|
+
type: 'node',
|
|
80
|
+
tag: '',
|
|
81
81
|
children: [],
|
|
82
82
|
};
|
|
83
83
|
|
|
@@ -85,124 +85,124 @@ describe("ComponentBuilder", () => {
|
|
|
85
85
|
|
|
86
86
|
// Implementation returns a div element for empty tag (graceful fallback)
|
|
87
87
|
expect(result).not.toBeNull();
|
|
88
|
-
expect(typeof result).toBe(
|
|
88
|
+
expect(typeof result).toBe('object');
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
test(
|
|
91
|
+
test('resolves an _i18n value object as the node to the default locale string', () => {
|
|
92
92
|
// Mirrors the SSR behavior: authors can pass localized strings anywhere
|
|
93
93
|
// `children` is accepted. The client resolves _i18n at the buildComponent
|
|
94
94
|
// entry point so all downstream code sees a plain string.
|
|
95
95
|
const i18nConfig = {
|
|
96
|
-
defaultLocale:
|
|
96
|
+
defaultLocale: 'en',
|
|
97
97
|
locales: [
|
|
98
|
-
{ code:
|
|
99
|
-
{ code:
|
|
98
|
+
{ code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
|
|
99
|
+
{ code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
|
|
100
100
|
],
|
|
101
101
|
};
|
|
102
102
|
const result = builder.buildComponent({
|
|
103
|
-
node: { _i18n: true, en:
|
|
103
|
+
node: { _i18n: true, en: 'Hello', pl: 'Cześć' } as any,
|
|
104
104
|
i18nConfig,
|
|
105
105
|
});
|
|
106
|
-
expect(result).toBe(
|
|
106
|
+
expect(result).toBe('Hello');
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test(
|
|
109
|
+
test('resolves an _i18n value object to the active locale', () => {
|
|
110
110
|
const i18nConfig = {
|
|
111
|
-
defaultLocale:
|
|
111
|
+
defaultLocale: 'en',
|
|
112
112
|
locales: [
|
|
113
|
-
{ code:
|
|
114
|
-
{ code:
|
|
113
|
+
{ code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
|
|
114
|
+
{ code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
|
|
115
115
|
],
|
|
116
116
|
};
|
|
117
117
|
const result = builder.buildComponent({
|
|
118
|
-
node: { _i18n: true, en:
|
|
118
|
+
node: { _i18n: true, en: 'Hello', pl: 'Cześć' } as any,
|
|
119
119
|
i18nConfig,
|
|
120
|
-
locale:
|
|
120
|
+
locale: 'pl',
|
|
121
121
|
});
|
|
122
|
-
expect(result).toBe(
|
|
122
|
+
expect(result).toBe('Cześć');
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
describe(
|
|
127
|
-
test(
|
|
126
|
+
describe('buildComponent - Component Instances', () => {
|
|
127
|
+
test('should handle non-existent component gracefully', () => {
|
|
128
128
|
const node: ComponentNode = {
|
|
129
|
-
type:
|
|
130
|
-
component:
|
|
129
|
+
type: 'component',
|
|
130
|
+
component: 'NonExistent',
|
|
131
131
|
};
|
|
132
132
|
const result = builder.buildComponent({ node });
|
|
133
133
|
// Returns error boundary or placeholder element for missing component
|
|
134
134
|
expect(result).not.toBeNull();
|
|
135
|
-
expect(typeof result).toBe(
|
|
135
|
+
expect(typeof result).toBe('object');
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
test(
|
|
138
|
+
test('should build component instance with structure', () => {
|
|
139
139
|
const componentDef = {
|
|
140
140
|
component: {
|
|
141
141
|
interface: {},
|
|
142
142
|
structure: {
|
|
143
|
-
type:
|
|
144
|
-
tag:
|
|
145
|
-
children: [
|
|
143
|
+
type: 'node',
|
|
144
|
+
tag: 'div',
|
|
145
|
+
children: ['Component Content'],
|
|
146
146
|
},
|
|
147
147
|
},
|
|
148
148
|
};
|
|
149
|
-
componentRegistry.register(
|
|
149
|
+
componentRegistry.register('TestComponent', componentDef as any);
|
|
150
150
|
|
|
151
151
|
const node: ComponentNode = {
|
|
152
|
-
type:
|
|
153
|
-
component:
|
|
152
|
+
type: 'component',
|
|
153
|
+
component: 'TestComponent',
|
|
154
154
|
};
|
|
155
155
|
const result = builder.buildComponent({ node });
|
|
156
156
|
expect(result).not.toBeNull();
|
|
157
|
-
expect(typeof result).toBe(
|
|
157
|
+
expect(typeof result).toBe('object');
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test(
|
|
160
|
+
test('should handle component missing structure gracefully', () => {
|
|
161
161
|
// Test with component that has no component property at all
|
|
162
162
|
const componentDef = {
|
|
163
163
|
// Missing component property entirely
|
|
164
164
|
};
|
|
165
|
-
componentRegistry.register(
|
|
165
|
+
componentRegistry.register('BrokenComponent', componentDef as any);
|
|
166
166
|
|
|
167
167
|
const node: ComponentNode = {
|
|
168
|
-
type:
|
|
169
|
-
component:
|
|
168
|
+
type: 'component',
|
|
169
|
+
component: 'BrokenComponent',
|
|
170
170
|
};
|
|
171
171
|
|
|
172
172
|
const result = builder.buildComponent({ node });
|
|
173
173
|
|
|
174
174
|
// When componentDef.component is missing, it should return ErrorBoundary
|
|
175
175
|
expect(result).not.toBeNull();
|
|
176
|
-
expect(typeof result).toBe(
|
|
176
|
+
expect(typeof result).toBe('object');
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
test(
|
|
179
|
+
test('should resolve props with defaults from interface', () => {
|
|
180
180
|
const componentDef = {
|
|
181
181
|
component: {
|
|
182
182
|
interface: {
|
|
183
183
|
title: {
|
|
184
|
-
type:
|
|
185
|
-
default:
|
|
184
|
+
type: 'string',
|
|
185
|
+
default: 'Default Title',
|
|
186
186
|
},
|
|
187
187
|
},
|
|
188
188
|
structure: {
|
|
189
|
-
type:
|
|
190
|
-
tag:
|
|
189
|
+
type: 'node',
|
|
190
|
+
tag: 'div',
|
|
191
191
|
children: [],
|
|
192
192
|
},
|
|
193
193
|
},
|
|
194
194
|
};
|
|
195
|
-
componentRegistry.register(
|
|
195
|
+
componentRegistry.register('ComponentWithDefaults', componentDef as any);
|
|
196
196
|
|
|
197
197
|
const node: ComponentNode = {
|
|
198
|
-
type:
|
|
199
|
-
component:
|
|
198
|
+
type: 'component',
|
|
199
|
+
component: 'ComponentWithDefaults',
|
|
200
200
|
};
|
|
201
201
|
const result = builder.buildComponent({ node });
|
|
202
202
|
expect(result).not.toBeNull();
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
test(
|
|
205
|
+
test('should handle component building errors gracefully', () => {
|
|
206
206
|
// This test verifies error handling is in place
|
|
207
207
|
// We can't easily mock processStructure in Bun, so we test with a valid component
|
|
208
208
|
// Error handling is verified by the try-catch in buildComponent
|
|
@@ -210,65 +210,65 @@ describe("ComponentBuilder", () => {
|
|
|
210
210
|
component: {
|
|
211
211
|
interface: {},
|
|
212
212
|
structure: {
|
|
213
|
-
type:
|
|
214
|
-
tag:
|
|
213
|
+
type: 'node',
|
|
214
|
+
tag: 'div',
|
|
215
215
|
children: [],
|
|
216
216
|
},
|
|
217
217
|
},
|
|
218
218
|
};
|
|
219
|
-
componentRegistry.register(
|
|
219
|
+
componentRegistry.register('ValidComponent', componentDef as any);
|
|
220
220
|
|
|
221
221
|
const node: ComponentNode = {
|
|
222
|
-
type:
|
|
223
|
-
component:
|
|
222
|
+
type: 'component',
|
|
223
|
+
component: 'ValidComponent',
|
|
224
224
|
};
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
const result = builder.buildComponent({ node });
|
|
227
|
-
|
|
227
|
+
|
|
228
228
|
// Component should build successfully
|
|
229
229
|
expect(result).not.toBeNull();
|
|
230
|
-
expect(typeof result).toBe(
|
|
230
|
+
expect(typeof result).toBe('object');
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
describe(
|
|
235
|
-
test(
|
|
234
|
+
describe('buildComponent - Link Component', () => {
|
|
235
|
+
test('should render Link component as anchor tag', () => {
|
|
236
236
|
const node: ComponentNode = {
|
|
237
|
-
type:
|
|
238
|
-
tag:
|
|
237
|
+
type: 'node',
|
|
238
|
+
tag: 'Link',
|
|
239
239
|
props: {
|
|
240
|
-
to:
|
|
240
|
+
to: '/about',
|
|
241
241
|
},
|
|
242
|
-
children: [
|
|
242
|
+
children: ['Click me'],
|
|
243
243
|
};
|
|
244
244
|
const result = builder.buildComponent({ node });
|
|
245
245
|
expect(result).not.toBeNull();
|
|
246
|
-
expect(typeof result).toBe(
|
|
246
|
+
expect(typeof result).toBe('object');
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
test(
|
|
249
|
+
test('should handle Link navigation when selecting mode is disabled', () => {
|
|
250
250
|
const node: ComponentNode = {
|
|
251
|
-
type:
|
|
252
|
-
tag:
|
|
251
|
+
type: 'node',
|
|
252
|
+
tag: 'Link',
|
|
253
253
|
props: {
|
|
254
|
-
to:
|
|
254
|
+
to: '/test',
|
|
255
255
|
},
|
|
256
256
|
children: [],
|
|
257
257
|
};
|
|
258
|
-
|
|
258
|
+
|
|
259
259
|
// Link component should render regardless of selecting mode
|
|
260
260
|
// The actual behavior is tested through integration tests
|
|
261
261
|
const result = builder.buildComponent({ node });
|
|
262
262
|
expect(result).not.toBeNull();
|
|
263
|
-
expect(typeof result).toBe(
|
|
263
|
+
expect(typeof result).toBe('object');
|
|
264
264
|
});
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
describe(
|
|
268
|
-
test(
|
|
267
|
+
describe('buildComponent - HTML Elements', () => {
|
|
268
|
+
test('should register element via ref callback', () => {
|
|
269
269
|
const node: ComponentNode = {
|
|
270
|
-
type:
|
|
271
|
-
tag:
|
|
270
|
+
type: 'node',
|
|
271
|
+
tag: 'div',
|
|
272
272
|
children: [],
|
|
273
273
|
};
|
|
274
274
|
const result = builder.buildComponent({ node });
|
|
@@ -277,14 +277,14 @@ describe("ComponentBuilder", () => {
|
|
|
277
277
|
// We can't easily test this without rendering, but we verify the ref is set
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
test(
|
|
280
|
+
test('should extract and merge styles', () => {
|
|
281
281
|
const node: ComponentNode = {
|
|
282
|
-
type:
|
|
283
|
-
tag:
|
|
282
|
+
type: 'node',
|
|
283
|
+
tag: 'div',
|
|
284
284
|
style: {
|
|
285
285
|
base: {
|
|
286
|
-
color:
|
|
287
|
-
fontSize:
|
|
286
|
+
color: 'red',
|
|
287
|
+
fontSize: '16px',
|
|
288
288
|
},
|
|
289
289
|
},
|
|
290
290
|
children: [],
|
|
@@ -293,13 +293,13 @@ describe("ComponentBuilder", () => {
|
|
|
293
293
|
expect(result).not.toBeNull();
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
test(
|
|
296
|
+
test('should extract and merge attributes', () => {
|
|
297
297
|
const node: ComponentNode = {
|
|
298
|
-
type:
|
|
299
|
-
tag:
|
|
298
|
+
type: 'node',
|
|
299
|
+
tag: 'div',
|
|
300
300
|
attributes: {
|
|
301
|
-
id:
|
|
302
|
-
className:
|
|
301
|
+
id: 'test-id',
|
|
302
|
+
className: 'test-class',
|
|
303
303
|
},
|
|
304
304
|
children: [],
|
|
305
305
|
};
|
|
@@ -307,16 +307,16 @@ describe("ComponentBuilder", () => {
|
|
|
307
307
|
expect(result).not.toBeNull();
|
|
308
308
|
});
|
|
309
309
|
|
|
310
|
-
test(
|
|
310
|
+
test('should filter out internal props', () => {
|
|
311
311
|
const node: ComponentNode = {
|
|
312
|
-
type:
|
|
313
|
-
tag:
|
|
312
|
+
type: 'node',
|
|
313
|
+
tag: 'div',
|
|
314
314
|
props: {
|
|
315
|
-
type:
|
|
316
|
-
tag:
|
|
317
|
-
component:
|
|
318
|
-
children:
|
|
319
|
-
validProp:
|
|
315
|
+
type: 'should-be-removed',
|
|
316
|
+
tag: 'should-be-removed',
|
|
317
|
+
component: 'should-be-removed',
|
|
318
|
+
children: 'should-be-removed',
|
|
319
|
+
validProp: 'should-remain',
|
|
320
320
|
},
|
|
321
321
|
children: [],
|
|
322
322
|
};
|
|
@@ -324,44 +324,44 @@ describe("ComponentBuilder", () => {
|
|
|
324
324
|
expect(result).not.toBeNull();
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
-
test(
|
|
327
|
+
test('should detect component root via __componentProps', () => {
|
|
328
328
|
const node: ComponentNode = {
|
|
329
|
-
type:
|
|
330
|
-
tag:
|
|
329
|
+
type: 'node',
|
|
330
|
+
tag: 'div',
|
|
331
331
|
children: [],
|
|
332
332
|
};
|
|
333
333
|
const result = builder.buildComponent({
|
|
334
334
|
node,
|
|
335
335
|
customProps: {
|
|
336
|
-
__componentProps: { title:
|
|
336
|
+
__componentProps: { title: 'Test' },
|
|
337
337
|
},
|
|
338
338
|
});
|
|
339
339
|
expect(result).not.toBeNull();
|
|
340
340
|
});
|
|
341
341
|
|
|
342
|
-
test(
|
|
342
|
+
test('should remap body tag to div to keep SSR and client DOMs identical', () => {
|
|
343
343
|
const node: ComponentNode = {
|
|
344
|
-
type:
|
|
345
|
-
tag:
|
|
346
|
-
attributes: { className:
|
|
347
|
-
children: [
|
|
344
|
+
type: 'node',
|
|
345
|
+
tag: 'body',
|
|
346
|
+
attributes: { className: 'page-root' },
|
|
347
|
+
children: ['Content'],
|
|
348
348
|
};
|
|
349
349
|
const result = builder.buildComponent({ node }) as any;
|
|
350
350
|
expect(result).not.toBeNull();
|
|
351
|
-
expect(result.type).toBe(
|
|
352
|
-
expect(result.props.className).toBe(
|
|
351
|
+
expect(result.type).toBe('div');
|
|
352
|
+
expect(result.props.className).toBe('page-root');
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
test(
|
|
356
|
-
for (const tag of [
|
|
357
|
-
const node: ComponentNode = { type:
|
|
355
|
+
test('should remap html and head tags to div', () => {
|
|
356
|
+
for (const tag of ['html', 'head', 'BODY'] as const) {
|
|
357
|
+
const node: ComponentNode = { type: 'node', tag, children: [] };
|
|
358
358
|
const result = builder.buildComponent({ node }) as any;
|
|
359
|
-
expect(result.type).toBe(
|
|
359
|
+
expect(result.type).toBe('div');
|
|
360
360
|
}
|
|
361
361
|
});
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
-
describe(
|
|
364
|
+
describe('buildChildren', () => {
|
|
365
365
|
const defaultCtx = {
|
|
366
366
|
elementPath: [0] as number[],
|
|
367
367
|
parentComponentName: null,
|
|
@@ -369,42 +369,42 @@ describe("ComponentBuilder", () => {
|
|
|
369
369
|
componentContext: null,
|
|
370
370
|
};
|
|
371
371
|
|
|
372
|
-
test(
|
|
372
|
+
test('should return array for array of children', () => {
|
|
373
373
|
const children: ComponentNode[] = [
|
|
374
|
-
{ type:
|
|
375
|
-
{ type:
|
|
374
|
+
{ type: 'node', tag: 'div', children: ['Child 1'] },
|
|
375
|
+
{ type: 'node', tag: 'span', children: ['Child 2'] },
|
|
376
376
|
];
|
|
377
377
|
const result = builder.buildChildren(children, defaultCtx);
|
|
378
378
|
expect(Array.isArray(result)).toBe(true);
|
|
379
379
|
expect((result as any[]).length).toBe(2);
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
-
test(
|
|
382
|
+
test('should return single element for single child', () => {
|
|
383
383
|
const child: ComponentNode = {
|
|
384
|
-
type:
|
|
385
|
-
tag:
|
|
386
|
-
children: [
|
|
384
|
+
type: 'node',
|
|
385
|
+
tag: 'div',
|
|
386
|
+
children: ['Single child'],
|
|
387
387
|
};
|
|
388
388
|
const result = builder.buildChildren(child, defaultCtx);
|
|
389
389
|
expect(result).not.toBeNull();
|
|
390
390
|
expect(Array.isArray(result)).toBe(false);
|
|
391
391
|
});
|
|
392
392
|
|
|
393
|
-
test(
|
|
393
|
+
test('should return null for undefined children', () => {
|
|
394
394
|
const result = builder.buildChildren(undefined, defaultCtx);
|
|
395
395
|
expect(result).toBeNull();
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
-
test(
|
|
398
|
+
test('should return empty array for empty array', () => {
|
|
399
399
|
const result = builder.buildChildren([], defaultCtx);
|
|
400
400
|
expect(Array.isArray(result)).toBe(true);
|
|
401
401
|
expect((result as any[]).length).toBe(0);
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
-
test(
|
|
404
|
+
test('should generate correct paths for children', () => {
|
|
405
405
|
const children: ComponentNode[] = [
|
|
406
|
-
{ type:
|
|
407
|
-
{ type:
|
|
406
|
+
{ type: 'node', tag: 'div', children: [] },
|
|
407
|
+
{ type: 'node', tag: 'span', children: [] },
|
|
408
408
|
];
|
|
409
409
|
const result = builder.buildChildren(children, { ...defaultCtx, elementPath: [0, 1] });
|
|
410
410
|
expect(Array.isArray(result)).toBe(true);
|
|
@@ -412,51 +412,51 @@ describe("ComponentBuilder", () => {
|
|
|
412
412
|
});
|
|
413
413
|
});
|
|
414
414
|
|
|
415
|
-
describe(
|
|
416
|
-
test(
|
|
415
|
+
describe('Helper Functions', () => {
|
|
416
|
+
test('getParentComponentNameForNestedComponent should prioritize componentContext', () => {
|
|
417
417
|
// This is tested indirectly through component building
|
|
418
418
|
// We can't directly test private methods, but we can verify behavior
|
|
419
419
|
const componentDef = {
|
|
420
420
|
component: {
|
|
421
421
|
interface: {},
|
|
422
422
|
structure: {
|
|
423
|
-
type:
|
|
424
|
-
tag:
|
|
423
|
+
type: 'node',
|
|
424
|
+
tag: 'div',
|
|
425
425
|
children: [],
|
|
426
426
|
},
|
|
427
427
|
},
|
|
428
428
|
};
|
|
429
|
-
componentRegistry.register(
|
|
429
|
+
componentRegistry.register('ParentComponent', componentDef as any);
|
|
430
430
|
|
|
431
431
|
const node: ComponentNode = {
|
|
432
|
-
type:
|
|
433
|
-
component:
|
|
432
|
+
type: 'component',
|
|
433
|
+
component: 'ParentComponent',
|
|
434
434
|
};
|
|
435
435
|
const result = builder.buildComponent({
|
|
436
436
|
node,
|
|
437
|
-
componentContext:
|
|
438
|
-
parentComponentName:
|
|
437
|
+
componentContext: 'ContextComponent',
|
|
438
|
+
parentComponentName: 'ParentName',
|
|
439
439
|
});
|
|
440
440
|
expect(result).not.toBeNull();
|
|
441
441
|
});
|
|
442
442
|
|
|
443
|
-
test(
|
|
443
|
+
test('getEffectiveParentComponentName should use componentContext when available', () => {
|
|
444
444
|
// Tested indirectly through component building
|
|
445
445
|
const node: ComponentNode = {
|
|
446
|
-
type:
|
|
447
|
-
tag:
|
|
446
|
+
type: 'node',
|
|
447
|
+
tag: 'div',
|
|
448
448
|
children: [],
|
|
449
449
|
};
|
|
450
450
|
const result = builder.buildComponent({
|
|
451
451
|
node,
|
|
452
|
-
componentContext:
|
|
453
|
-
parentComponentName:
|
|
452
|
+
componentContext: 'ContextComponent',
|
|
453
|
+
parentComponentName: 'ParentName',
|
|
454
454
|
});
|
|
455
455
|
expect(result).not.toBeNull();
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
458
|
|
|
459
|
-
describe(
|
|
459
|
+
describe('Path Assignment for Slot Markers', () => {
|
|
460
460
|
/**
|
|
461
461
|
* KNOWN ISSUE: Slot markers consume path indices incorrectly
|
|
462
462
|
*
|
|
@@ -474,21 +474,18 @@ describe("ComponentBuilder", () => {
|
|
|
474
474
|
* TODO: Fix path assignment to skip slot markers when assigning indices,
|
|
475
475
|
* or adjust indices after slot expansion.
|
|
476
476
|
*/
|
|
477
|
-
test.skip(
|
|
477
|
+
test.skip('slot markers should not consume path indices', () => {
|
|
478
478
|
const sectionDef = {
|
|
479
479
|
component: {
|
|
480
480
|
interface: {},
|
|
481
481
|
structure: {
|
|
482
|
-
type:
|
|
483
|
-
tag:
|
|
482
|
+
type: 'node',
|
|
483
|
+
tag: 'div',
|
|
484
484
|
children: [
|
|
485
485
|
{
|
|
486
|
-
type:
|
|
487
|
-
tag:
|
|
488
|
-
children: [
|
|
489
|
-
{ type: "slot" },
|
|
490
|
-
{ type: "component", component: "Card" },
|
|
491
|
-
],
|
|
486
|
+
type: 'node',
|
|
487
|
+
tag: 'div',
|
|
488
|
+
children: [{ type: 'slot' }, { type: 'component', component: 'Card' }],
|
|
492
489
|
},
|
|
493
490
|
],
|
|
494
491
|
},
|
|
@@ -499,20 +496,20 @@ describe("ComponentBuilder", () => {
|
|
|
499
496
|
component: {
|
|
500
497
|
interface: {},
|
|
501
498
|
structure: {
|
|
502
|
-
type:
|
|
503
|
-
tag:
|
|
504
|
-
children: [
|
|
499
|
+
type: 'node',
|
|
500
|
+
tag: 'div',
|
|
501
|
+
children: ['Card Content'],
|
|
505
502
|
},
|
|
506
503
|
},
|
|
507
504
|
};
|
|
508
505
|
|
|
509
|
-
componentRegistry.register(
|
|
510
|
-
componentRegistry.register(
|
|
506
|
+
componentRegistry.register('Section', sectionDef as any);
|
|
507
|
+
componentRegistry.register('Card', cardDef as any);
|
|
511
508
|
|
|
512
509
|
const pageNode: ComponentNode = {
|
|
513
|
-
type:
|
|
514
|
-
component:
|
|
515
|
-
children: [{ type:
|
|
510
|
+
type: 'component',
|
|
511
|
+
component: 'Section',
|
|
512
|
+
children: [{ type: 'node', tag: 'p', children: ['Instance Child'] }],
|
|
516
513
|
};
|
|
517
514
|
|
|
518
515
|
const result = builder.buildComponent({ node: pageNode });
|
|
@@ -524,51 +521,51 @@ describe("ComponentBuilder", () => {
|
|
|
524
521
|
// OR Card gets a path that properly reflects its position relative to expanded slot content
|
|
525
522
|
});
|
|
526
523
|
|
|
527
|
-
test(
|
|
524
|
+
test('should build component with slot marker correctly', () => {
|
|
528
525
|
// Button.json: button > children
|
|
529
526
|
const buttonDef = {
|
|
530
527
|
component: {
|
|
531
528
|
interface: {
|
|
532
|
-
children: { type:
|
|
529
|
+
children: { type: 'string', default: 'Click me' },
|
|
533
530
|
},
|
|
534
531
|
structure: {
|
|
535
|
-
type:
|
|
536
|
-
tag:
|
|
537
|
-
children: [{ type:
|
|
532
|
+
type: 'node',
|
|
533
|
+
tag: 'button',
|
|
534
|
+
children: [{ type: 'slot' }],
|
|
538
535
|
},
|
|
539
536
|
},
|
|
540
537
|
};
|
|
541
538
|
|
|
542
|
-
componentRegistry.register(
|
|
539
|
+
componentRegistry.register('Button', buttonDef as any);
|
|
543
540
|
|
|
544
541
|
const pageNode: ComponentNode = {
|
|
545
|
-
type:
|
|
546
|
-
component:
|
|
547
|
-
children: [{ type:
|
|
542
|
+
type: 'component',
|
|
543
|
+
component: 'Button',
|
|
544
|
+
children: [{ type: 'node', tag: 'span', children: ['Submit'] }],
|
|
548
545
|
};
|
|
549
546
|
|
|
550
547
|
const result = builder.buildComponent({ node: pageNode });
|
|
551
548
|
expect(result).not.toBeNull();
|
|
552
|
-
expect(typeof result).toBe(
|
|
549
|
+
expect(typeof result).toBe('object');
|
|
553
550
|
});
|
|
554
551
|
|
|
555
|
-
test(
|
|
552
|
+
test('should build nested children with multiple siblings after slot', () => {
|
|
556
553
|
// Structure: div > div > [slot, Card, Button]
|
|
557
554
|
|
|
558
555
|
const sectionDef = {
|
|
559
556
|
component: {
|
|
560
557
|
interface: {},
|
|
561
558
|
structure: {
|
|
562
|
-
type:
|
|
563
|
-
tag:
|
|
559
|
+
type: 'node',
|
|
560
|
+
tag: 'div',
|
|
564
561
|
children: [
|
|
565
562
|
{
|
|
566
|
-
type:
|
|
567
|
-
tag:
|
|
563
|
+
type: 'node',
|
|
564
|
+
tag: 'div',
|
|
568
565
|
children: [
|
|
569
|
-
{ type:
|
|
570
|
-
{ type:
|
|
571
|
-
{ type:
|
|
566
|
+
{ type: 'slot' },
|
|
567
|
+
{ type: 'component', component: 'Card' },
|
|
568
|
+
{ type: 'component', component: 'Button' },
|
|
572
569
|
],
|
|
573
570
|
},
|
|
574
571
|
],
|
|
@@ -580,9 +577,9 @@ describe("ComponentBuilder", () => {
|
|
|
580
577
|
component: {
|
|
581
578
|
interface: {},
|
|
582
579
|
structure: {
|
|
583
|
-
type:
|
|
584
|
-
tag:
|
|
585
|
-
children: [
|
|
580
|
+
type: 'node',
|
|
581
|
+
tag: 'div',
|
|
582
|
+
children: ['Card'],
|
|
586
583
|
},
|
|
587
584
|
},
|
|
588
585
|
};
|
|
@@ -591,57 +588,54 @@ describe("ComponentBuilder", () => {
|
|
|
591
588
|
component: {
|
|
592
589
|
interface: {},
|
|
593
590
|
structure: {
|
|
594
|
-
type:
|
|
595
|
-
tag:
|
|
596
|
-
children: [
|
|
591
|
+
type: 'node',
|
|
592
|
+
tag: 'button',
|
|
593
|
+
children: ['Button'],
|
|
597
594
|
},
|
|
598
595
|
},
|
|
599
596
|
};
|
|
600
597
|
|
|
601
|
-
componentRegistry.register(
|
|
602
|
-
componentRegistry.register(
|
|
603
|
-
componentRegistry.register(
|
|
598
|
+
componentRegistry.register('Section', sectionDef as any);
|
|
599
|
+
componentRegistry.register('Card', cardDef as any);
|
|
600
|
+
componentRegistry.register('Button', buttonDef as any);
|
|
604
601
|
|
|
605
602
|
const pageNode: ComponentNode = {
|
|
606
|
-
type:
|
|
607
|
-
component:
|
|
608
|
-
children: [{ type:
|
|
603
|
+
type: 'component',
|
|
604
|
+
component: 'Section',
|
|
605
|
+
children: [{ type: 'node', tag: 'p', children: ['Content'] }],
|
|
609
606
|
};
|
|
610
607
|
|
|
611
608
|
const result = builder.buildComponent({ node: pageNode });
|
|
612
609
|
expect(result).not.toBeNull();
|
|
613
|
-
expect(typeof result).toBe(
|
|
610
|
+
expect(typeof result).toBe('object');
|
|
614
611
|
});
|
|
615
612
|
});
|
|
616
613
|
|
|
617
|
-
describe(
|
|
618
|
-
test(
|
|
614
|
+
describe('Integration Tests', () => {
|
|
615
|
+
test('should build complex component tree', () => {
|
|
619
616
|
const cardDef = {
|
|
620
617
|
component: {
|
|
621
618
|
interface: {
|
|
622
|
-
title: { type:
|
|
619
|
+
title: { type: 'string', default: 'Card Title' },
|
|
623
620
|
},
|
|
624
621
|
structure: {
|
|
625
|
-
type:
|
|
626
|
-
tag:
|
|
627
|
-
children: [
|
|
628
|
-
{ type: "node", tag: "h2", children: ["{{title}}"] },
|
|
629
|
-
{ type: "slot" },
|
|
630
|
-
],
|
|
622
|
+
type: 'node',
|
|
623
|
+
tag: 'div',
|
|
624
|
+
children: [{ type: 'node', tag: 'h2', children: ['{{title}}'] }, { type: 'slot' }],
|
|
631
625
|
},
|
|
632
626
|
},
|
|
633
627
|
};
|
|
634
|
-
componentRegistry.register(
|
|
628
|
+
componentRegistry.register('Card', cardDef as any);
|
|
635
629
|
|
|
636
630
|
const pageNode: ComponentNode = {
|
|
637
|
-
type:
|
|
638
|
-
tag:
|
|
631
|
+
type: 'node',
|
|
632
|
+
tag: 'div',
|
|
639
633
|
children: [
|
|
640
634
|
{
|
|
641
|
-
type:
|
|
642
|
-
component:
|
|
643
|
-
props: { title:
|
|
644
|
-
children: [{ type:
|
|
635
|
+
type: 'component',
|
|
636
|
+
component: 'Card',
|
|
637
|
+
props: { title: 'My Card' },
|
|
638
|
+
children: [{ type: 'node', tag: 'p', children: ['Card content'] }],
|
|
645
639
|
},
|
|
646
640
|
],
|
|
647
641
|
};
|
|
@@ -649,90 +643,86 @@ describe("ComponentBuilder", () => {
|
|
|
649
643
|
expect(result).not.toBeNull();
|
|
650
644
|
});
|
|
651
645
|
|
|
652
|
-
test(
|
|
646
|
+
test('should handle nested component instances', () => {
|
|
653
647
|
const innerDef = {
|
|
654
648
|
component: {
|
|
655
649
|
interface: {},
|
|
656
650
|
structure: {
|
|
657
|
-
type:
|
|
658
|
-
tag:
|
|
659
|
-
children: [
|
|
651
|
+
type: 'node',
|
|
652
|
+
tag: 'span',
|
|
653
|
+
children: ['Inner'],
|
|
660
654
|
},
|
|
661
655
|
},
|
|
662
656
|
};
|
|
663
|
-
componentRegistry.register(
|
|
657
|
+
componentRegistry.register('Inner', innerDef as any);
|
|
664
658
|
|
|
665
659
|
const outerDef = {
|
|
666
660
|
component: {
|
|
667
661
|
interface: {},
|
|
668
662
|
structure: {
|
|
669
|
-
type:
|
|
670
|
-
tag:
|
|
671
|
-
children: [
|
|
672
|
-
{ type: "component", component: "Inner" },
|
|
673
|
-
],
|
|
663
|
+
type: 'node',
|
|
664
|
+
tag: 'div',
|
|
665
|
+
children: [{ type: 'component', component: 'Inner' }],
|
|
674
666
|
},
|
|
675
667
|
},
|
|
676
668
|
};
|
|
677
|
-
componentRegistry.register(
|
|
669
|
+
componentRegistry.register('Outer', outerDef as any);
|
|
678
670
|
|
|
679
671
|
const node: ComponentNode = {
|
|
680
|
-
type:
|
|
681
|
-
component:
|
|
672
|
+
type: 'component',
|
|
673
|
+
component: 'Outer',
|
|
682
674
|
};
|
|
683
675
|
const result = builder.buildComponent({ node });
|
|
684
676
|
expect(result).not.toBeNull();
|
|
685
677
|
});
|
|
686
678
|
|
|
687
|
-
test(
|
|
679
|
+
test('should propagate component context correctly', () => {
|
|
688
680
|
const componentDef = {
|
|
689
681
|
component: {
|
|
690
682
|
interface: {},
|
|
691
683
|
structure: {
|
|
692
|
-
type:
|
|
693
|
-
tag:
|
|
694
|
-
children: [
|
|
695
|
-
{ type: "node", tag: "span", children: ["Child"] },
|
|
696
|
-
],
|
|
684
|
+
type: 'node',
|
|
685
|
+
tag: 'div',
|
|
686
|
+
children: [{ type: 'node', tag: 'span', children: ['Child'] }],
|
|
697
687
|
},
|
|
698
688
|
},
|
|
699
689
|
};
|
|
700
|
-
componentRegistry.register(
|
|
690
|
+
componentRegistry.register('TestComponent', componentDef as any);
|
|
701
691
|
|
|
702
692
|
const node: ComponentNode = {
|
|
703
|
-
type:
|
|
704
|
-
component:
|
|
693
|
+
type: 'component',
|
|
694
|
+
component: 'TestComponent',
|
|
705
695
|
};
|
|
706
696
|
const result = builder.buildComponent({
|
|
707
697
|
node,
|
|
708
|
-
componentContext:
|
|
698
|
+
componentContext: 'TestComponent',
|
|
709
699
|
});
|
|
710
700
|
expect(result).not.toBeNull();
|
|
711
701
|
});
|
|
712
702
|
|
|
713
|
-
test(
|
|
703
|
+
test('should respect viewportWidth for responsive styles', () => {
|
|
714
704
|
const node: ComponentNode = {
|
|
715
|
-
type:
|
|
716
|
-
tag:
|
|
705
|
+
type: 'node',
|
|
706
|
+
tag: 'div',
|
|
717
707
|
style: {
|
|
718
|
-
base: { fontSize:
|
|
719
|
-
tablet: { fontSize:
|
|
720
|
-
mobile: { fontSize:
|
|
708
|
+
base: { fontSize: '24px' },
|
|
709
|
+
tablet: { fontSize: '18px' },
|
|
710
|
+
mobile: { fontSize: '16px' },
|
|
721
711
|
},
|
|
722
712
|
children: [],
|
|
723
713
|
};
|
|
724
|
-
|
|
714
|
+
|
|
725
715
|
const resultDesktop = builder.buildComponent({ node, viewportWidth: 1920 });
|
|
726
716
|
const resultTablet = builder.buildComponent({ node, viewportWidth: 768 });
|
|
727
717
|
const resultMobile = builder.buildComponent({ node, viewportWidth: 375 });
|
|
728
|
-
|
|
718
|
+
|
|
729
719
|
expect(resultDesktop).not.toBeNull();
|
|
730
720
|
expect(resultTablet).not.toBeNull();
|
|
731
721
|
expect(resultMobile).not.toBeNull();
|
|
732
722
|
});
|
|
733
723
|
});
|
|
734
724
|
|
|
735
|
-
describe(
|
|
725
|
+
describe('buildComponent - List (Collection Mode)', () => {
|
|
736
726
|
// Helper to create mock CMS items
|
|
737
727
|
const createMockCMSItems = (count: number): CMSItem[] => {
|
|
738
728
|
return Array.from({ length: count }, (_, i) => ({
|
|
@@ -747,7 +737,7 @@ describe("ComponentBuilder", () => {
|
|
|
747
737
|
const createCollectionListNode = (
|
|
748
738
|
source: string,
|
|
749
739
|
children: ComponentNode[],
|
|
750
|
-
options: { limit?: number; offset?: number } = {}
|
|
740
|
+
options: { limit?: number; offset?: number } = {},
|
|
751
741
|
): ListNode => ({
|
|
752
742
|
type: NODE_TYPE.LIST,
|
|
753
743
|
sourceType: 'collection',
|
|
@@ -756,11 +746,9 @@ describe("ComponentBuilder", () => {
|
|
|
756
746
|
...options,
|
|
757
747
|
});
|
|
758
748
|
|
|
759
|
-
test(
|
|
749
|
+
test('should render children for each CMS item', () => {
|
|
760
750
|
const items = createMockCMSItems(3);
|
|
761
|
-
const node = createCollectionListNode(
|
|
762
|
-
{ type: "node", tag: "div", children: ["Item content"] },
|
|
763
|
-
]);
|
|
751
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item content'] }]);
|
|
764
752
|
|
|
765
753
|
const result = builder.buildComponent({
|
|
766
754
|
node: node as unknown as ComponentNode,
|
|
@@ -768,18 +756,16 @@ describe("ComponentBuilder", () => {
|
|
|
768
756
|
});
|
|
769
757
|
|
|
770
758
|
expect(result).not.toBeNull();
|
|
771
|
-
expect(typeof result).toBe(
|
|
759
|
+
expect(typeof result).toBe('object');
|
|
772
760
|
// Result should have props.children with 3 items
|
|
773
761
|
const element = result as { props?: { children?: unknown[] } };
|
|
774
762
|
expect(Array.isArray(element?.props?.children)).toBe(true);
|
|
775
763
|
expect(element?.props?.children?.length).toBe(3);
|
|
776
764
|
});
|
|
777
765
|
|
|
778
|
-
test(
|
|
766
|
+
test('should add data-cms-item-index attribute to each item wrapper', () => {
|
|
779
767
|
const items = createMockCMSItems(3);
|
|
780
|
-
const node = createCollectionListNode(
|
|
781
|
-
{ type: "node", tag: "div", children: ["Item content"] },
|
|
782
|
-
]);
|
|
768
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item content'] }]);
|
|
783
769
|
|
|
784
770
|
const result = builder.buildComponent({
|
|
785
771
|
node: node as unknown as ComponentNode,
|
|
@@ -801,13 +787,9 @@ describe("ComponentBuilder", () => {
|
|
|
801
787
|
});
|
|
802
788
|
});
|
|
803
789
|
|
|
804
|
-
test(
|
|
805
|
-
const items: CMSItem[] = [
|
|
806
|
-
|
|
807
|
-
];
|
|
808
|
-
const node = createCollectionListNode("posts", [
|
|
809
|
-
{ type: "node", tag: "h1", children: ["{{item.title}}"] },
|
|
810
|
-
]);
|
|
790
|
+
test('should interpolate {{item.field}} in text', () => {
|
|
791
|
+
const items: CMSItem[] = [{ _id: '1', _slug: 'test', title: 'Hello World' }];
|
|
792
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'h1', children: ['{{item.title}}'] }]);
|
|
811
793
|
|
|
812
794
|
const result = builder.buildComponent({
|
|
813
795
|
node: node as unknown as ComponentNode,
|
|
@@ -816,13 +798,13 @@ describe("ComponentBuilder", () => {
|
|
|
816
798
|
|
|
817
799
|
expect(result).not.toBeNull();
|
|
818
800
|
// The text should be interpolated - we verify by checking result structure
|
|
819
|
-
expect(typeof result).toBe(
|
|
801
|
+
expect(typeof result).toBe('object');
|
|
820
802
|
});
|
|
821
803
|
|
|
822
|
-
test(
|
|
804
|
+
test('should interpolate {{itemIndex}}, {{itemFirst}}, {{itemLast}}', () => {
|
|
823
805
|
const items = createMockCMSItems(3);
|
|
824
|
-
const node = createCollectionListNode(
|
|
825
|
-
{ type:
|
|
806
|
+
const node = createCollectionListNode('posts', [
|
|
807
|
+
{ type: 'node', tag: 'span', children: ['Index: {{itemIndex}}, First: {{itemFirst}}, Last: {{itemLast}}'] },
|
|
826
808
|
]);
|
|
827
809
|
|
|
828
810
|
const result = builder.buildComponent({
|
|
@@ -831,16 +813,14 @@ describe("ComponentBuilder", () => {
|
|
|
831
813
|
});
|
|
832
814
|
|
|
833
815
|
expect(result).not.toBeNull();
|
|
834
|
-
expect(typeof result).toBe(
|
|
816
|
+
expect(typeof result).toBe('object');
|
|
835
817
|
// Should have 3 item containers
|
|
836
818
|
const element = result as { props?: { children?: unknown[] } };
|
|
837
819
|
expect(element?.props?.children?.length).toBe(3);
|
|
838
820
|
});
|
|
839
821
|
|
|
840
|
-
test(
|
|
841
|
-
const node = createCollectionListNode(
|
|
842
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
843
|
-
]);
|
|
822
|
+
test('should show empty state when no items', () => {
|
|
823
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }]);
|
|
844
824
|
|
|
845
825
|
const result = builder.buildComponent({
|
|
846
826
|
node: node as unknown as ComponentNode,
|
|
@@ -848,16 +828,14 @@ describe("ComponentBuilder", () => {
|
|
|
848
828
|
});
|
|
849
829
|
|
|
850
830
|
expect(result).not.toBeNull();
|
|
851
|
-
expect(typeof result).toBe(
|
|
831
|
+
expect(typeof result).toBe('object');
|
|
852
832
|
// Empty state should be a single div with message
|
|
853
833
|
const element = result as { props?: { children?: unknown } };
|
|
854
834
|
expect(element?.props?.children).not.toBeNull();
|
|
855
835
|
});
|
|
856
836
|
|
|
857
|
-
test(
|
|
858
|
-
const node = createCollectionListNode(
|
|
859
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
860
|
-
]);
|
|
837
|
+
test('should show empty state when collection not in map', () => {
|
|
838
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }]);
|
|
861
839
|
|
|
862
840
|
const result = builder.buildComponent({
|
|
863
841
|
node: node as unknown as ComponentNode,
|
|
@@ -865,14 +843,12 @@ describe("ComponentBuilder", () => {
|
|
|
865
843
|
});
|
|
866
844
|
|
|
867
845
|
expect(result).not.toBeNull();
|
|
868
|
-
expect(typeof result).toBe(
|
|
846
|
+
expect(typeof result).toBe('object');
|
|
869
847
|
});
|
|
870
848
|
|
|
871
|
-
test(
|
|
849
|
+
test('should apply limit to items', () => {
|
|
872
850
|
const items = createMockCMSItems(5);
|
|
873
|
-
const node = createCollectionListNode(
|
|
874
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
875
|
-
], { limit: 2 });
|
|
851
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], { limit: 2 });
|
|
876
852
|
|
|
877
853
|
const result = builder.buildComponent({
|
|
878
854
|
node: node as unknown as ComponentNode,
|
|
@@ -885,11 +861,9 @@ describe("ComponentBuilder", () => {
|
|
|
885
861
|
expect(element?.props?.children?.length).toBe(2);
|
|
886
862
|
});
|
|
887
863
|
|
|
888
|
-
test(
|
|
864
|
+
test('should apply offset to items', () => {
|
|
889
865
|
const items = createMockCMSItems(5);
|
|
890
|
-
const node = createCollectionListNode(
|
|
891
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
892
|
-
], { offset: 2 });
|
|
866
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], { offset: 2 });
|
|
893
867
|
|
|
894
868
|
const result = builder.buildComponent({
|
|
895
869
|
node: node as unknown as ComponentNode,
|
|
@@ -902,11 +876,12 @@ describe("ComponentBuilder", () => {
|
|
|
902
876
|
expect(element?.props?.children?.length).toBe(3); // 5 - 2 = 3
|
|
903
877
|
});
|
|
904
878
|
|
|
905
|
-
test(
|
|
879
|
+
test('should apply both limit and offset', () => {
|
|
906
880
|
const items = createMockCMSItems(5);
|
|
907
|
-
const node = createCollectionListNode(
|
|
908
|
-
|
|
909
|
-
|
|
881
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], {
|
|
882
|
+
offset: 1,
|
|
883
|
+
limit: 2,
|
|
884
|
+
});
|
|
910
885
|
|
|
911
886
|
const result = builder.buildComponent({
|
|
912
887
|
node: node as unknown as ComponentNode,
|
|
@@ -919,19 +894,19 @@ describe("ComponentBuilder", () => {
|
|
|
919
894
|
expect(element?.props?.children?.length).toBe(2);
|
|
920
895
|
});
|
|
921
896
|
|
|
922
|
-
test(
|
|
897
|
+
test('should filter items with simple object filter', () => {
|
|
923
898
|
const items: CMSItem[] = [
|
|
924
|
-
{ _id:
|
|
925
|
-
{ _id:
|
|
926
|
-
{ _id:
|
|
927
|
-
{ _id:
|
|
899
|
+
{ _id: '1', _slug: 'post1', featured: true, title: 'Featured Post' },
|
|
900
|
+
{ _id: '2', _slug: 'post2', featured: false, title: 'Regular Post 1' },
|
|
901
|
+
{ _id: '3', _slug: 'post3', featured: true, title: 'Another Featured' },
|
|
902
|
+
{ _id: '4', _slug: 'post4', featured: false, title: 'Regular Post 2' },
|
|
928
903
|
];
|
|
929
904
|
const node: ListNode = {
|
|
930
905
|
type: NODE_TYPE.LIST,
|
|
931
906
|
sourceType: 'collection',
|
|
932
|
-
source:
|
|
907
|
+
source: 'posts',
|
|
933
908
|
filter: { featured: true },
|
|
934
|
-
children: [{ type:
|
|
909
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
935
910
|
};
|
|
936
911
|
|
|
937
912
|
const result = builder.buildComponent({
|
|
@@ -945,19 +920,19 @@ describe("ComponentBuilder", () => {
|
|
|
945
920
|
expect(element?.props?.children?.length).toBe(2); // Only featured items
|
|
946
921
|
});
|
|
947
922
|
|
|
948
|
-
test(
|
|
923
|
+
test('should filter items with operator condition', () => {
|
|
949
924
|
const items: CMSItem[] = [
|
|
950
|
-
{ _id:
|
|
951
|
-
{ _id:
|
|
952
|
-
{ _id:
|
|
953
|
-
{ _id:
|
|
925
|
+
{ _id: '1', _slug: 'post1', price: 50, title: 'Cheap' },
|
|
926
|
+
{ _id: '2', _slug: 'post2', price: 150, title: 'Expensive 1' },
|
|
927
|
+
{ _id: '3', _slug: 'post3', price: 200, title: 'Expensive 2' },
|
|
928
|
+
{ _id: '4', _slug: 'post4', price: 75, title: 'Mid' },
|
|
954
929
|
];
|
|
955
930
|
const node: ListNode = {
|
|
956
931
|
type: NODE_TYPE.LIST,
|
|
957
932
|
sourceType: 'collection',
|
|
958
|
-
source:
|
|
959
|
-
filter: { field:
|
|
960
|
-
children: [{ type:
|
|
933
|
+
source: 'posts',
|
|
934
|
+
filter: { field: 'price', operator: 'gt', value: 100 },
|
|
935
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
961
936
|
};
|
|
962
937
|
|
|
963
938
|
const result = builder.buildComponent({
|
|
@@ -971,24 +946,24 @@ describe("ComponentBuilder", () => {
|
|
|
971
946
|
expect(element?.props?.children?.length).toBe(2); // Only items with price > 100
|
|
972
947
|
});
|
|
973
948
|
|
|
974
|
-
test(
|
|
949
|
+
test('should filter items with template value from cmsContext', () => {
|
|
975
950
|
const items: CMSItem[] = [
|
|
976
|
-
{ _id:
|
|
977
|
-
{ _id:
|
|
978
|
-
{ _id:
|
|
951
|
+
{ _id: '1', _slug: 'post1', category: 'tech', title: 'Tech Post 1' },
|
|
952
|
+
{ _id: '2', _slug: 'post2', category: 'news', title: 'News Post' },
|
|
953
|
+
{ _id: '3', _slug: 'post3', category: 'tech', title: 'Tech Post 2' },
|
|
979
954
|
];
|
|
980
955
|
const node: ListNode = {
|
|
981
956
|
type: NODE_TYPE.LIST,
|
|
982
957
|
sourceType: 'collection',
|
|
983
|
-
source:
|
|
984
|
-
filter: { category:
|
|
985
|
-
children: [{ type:
|
|
958
|
+
source: 'posts',
|
|
959
|
+
filter: { category: '{{cms.category}}' },
|
|
960
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
986
961
|
};
|
|
987
962
|
|
|
988
963
|
const result = builder.buildComponent({
|
|
989
964
|
node: node as unknown as ComponentNode,
|
|
990
965
|
collectionItemsMap: { posts: items },
|
|
991
|
-
cmsContext: { category:
|
|
966
|
+
cmsContext: { category: 'tech' },
|
|
992
967
|
});
|
|
993
968
|
|
|
994
969
|
expect(result).not.toBeNull();
|
|
@@ -997,18 +972,18 @@ describe("ComponentBuilder", () => {
|
|
|
997
972
|
expect(element?.props?.children?.length).toBe(2); // Only tech posts
|
|
998
973
|
});
|
|
999
974
|
|
|
1000
|
-
test(
|
|
975
|
+
test('should sort items ascending', () => {
|
|
1001
976
|
const items: CMSItem[] = [
|
|
1002
|
-
{ _id:
|
|
1003
|
-
{ _id:
|
|
1004
|
-
{ _id:
|
|
977
|
+
{ _id: '1', _slug: 'c', title: 'C' },
|
|
978
|
+
{ _id: '2', _slug: 'a', title: 'A' },
|
|
979
|
+
{ _id: '3', _slug: 'b', title: 'B' },
|
|
1005
980
|
];
|
|
1006
981
|
const node: ListNode = {
|
|
1007
982
|
type: NODE_TYPE.LIST,
|
|
1008
983
|
sourceType: 'collection',
|
|
1009
|
-
source:
|
|
1010
|
-
sort: { field:
|
|
1011
|
-
children: [{ type:
|
|
984
|
+
source: 'posts',
|
|
985
|
+
sort: { field: 'title', order: 'asc' },
|
|
986
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
1012
987
|
};
|
|
1013
988
|
|
|
1014
989
|
const result = builder.buildComponent({
|
|
@@ -1023,18 +998,18 @@ describe("ComponentBuilder", () => {
|
|
|
1023
998
|
// We can't easily check the order of rendered children without more complex inspection
|
|
1024
999
|
});
|
|
1025
1000
|
|
|
1026
|
-
test(
|
|
1001
|
+
test('should sort items descending', () => {
|
|
1027
1002
|
const items: CMSItem[] = [
|
|
1028
|
-
{ _id:
|
|
1029
|
-
{ _id:
|
|
1030
|
-
{ _id:
|
|
1003
|
+
{ _id: '1', _slug: 'p1', date: 100 },
|
|
1004
|
+
{ _id: '2', _slug: 'p2', date: 300 },
|
|
1005
|
+
{ _id: '3', _slug: 'p3', date: 200 },
|
|
1031
1006
|
];
|
|
1032
1007
|
const node: ListNode = {
|
|
1033
1008
|
type: NODE_TYPE.LIST,
|
|
1034
1009
|
sourceType: 'collection',
|
|
1035
|
-
source:
|
|
1036
|
-
sort: { field:
|
|
1037
|
-
children: [{ type:
|
|
1010
|
+
source: 'posts',
|
|
1011
|
+
sort: { field: 'date', order: 'desc' },
|
|
1012
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1038
1013
|
};
|
|
1039
1014
|
|
|
1040
1015
|
const result = builder.buildComponent({
|
|
@@ -1048,22 +1023,22 @@ describe("ComponentBuilder", () => {
|
|
|
1048
1023
|
expect(element?.props?.children?.length).toBe(3);
|
|
1049
1024
|
});
|
|
1050
1025
|
|
|
1051
|
-
test(
|
|
1026
|
+
test('should apply filter BEFORE limit', () => {
|
|
1052
1027
|
// 5 items: 2 featured, 3 not featured
|
|
1053
1028
|
const items: CMSItem[] = [
|
|
1054
|
-
{ _id:
|
|
1055
|
-
{ _id:
|
|
1056
|
-
{ _id:
|
|
1057
|
-
{ _id:
|
|
1058
|
-
{ _id:
|
|
1029
|
+
{ _id: '1', _slug: 'p1', featured: true, title: 'Featured 1' },
|
|
1030
|
+
{ _id: '2', _slug: 'p2', featured: false, title: 'Regular 1' },
|
|
1031
|
+
{ _id: '3', _slug: 'p3', featured: false, title: 'Regular 2' },
|
|
1032
|
+
{ _id: '4', _slug: 'p4', featured: true, title: 'Featured 2' },
|
|
1033
|
+
{ _id: '5', _slug: 'p5', featured: false, title: 'Regular 3' },
|
|
1059
1034
|
];
|
|
1060
1035
|
const node: ListNode = {
|
|
1061
1036
|
type: NODE_TYPE.LIST,
|
|
1062
1037
|
sourceType: 'collection',
|
|
1063
|
-
source:
|
|
1038
|
+
source: 'posts',
|
|
1064
1039
|
filter: { featured: true },
|
|
1065
1040
|
limit: 3, // Without proper order, this would limit before filtering
|
|
1066
|
-
children: [{ type:
|
|
1041
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1067
1042
|
};
|
|
1068
1043
|
|
|
1069
1044
|
const result = builder.buildComponent({
|
|
@@ -1078,21 +1053,21 @@ describe("ComponentBuilder", () => {
|
|
|
1078
1053
|
expect(element?.props?.children?.length).toBe(2);
|
|
1079
1054
|
});
|
|
1080
1055
|
|
|
1081
|
-
test(
|
|
1056
|
+
test('should apply sort BEFORE limit', () => {
|
|
1082
1057
|
const items: CMSItem[] = [
|
|
1083
|
-
{ _id:
|
|
1084
|
-
{ _id:
|
|
1085
|
-
{ _id:
|
|
1086
|
-
{ _id:
|
|
1087
|
-
{ _id:
|
|
1058
|
+
{ _id: '1', _slug: 'p1', date: 100 },
|
|
1059
|
+
{ _id: '2', _slug: 'p2', date: 300 },
|
|
1060
|
+
{ _id: '3', _slug: 'p3', date: 200 },
|
|
1061
|
+
{ _id: '4', _slug: 'p4', date: 400 },
|
|
1062
|
+
{ _id: '5', _slug: 'p5', date: 50 },
|
|
1088
1063
|
];
|
|
1089
1064
|
const node: ListNode = {
|
|
1090
1065
|
type: NODE_TYPE.LIST,
|
|
1091
1066
|
sourceType: 'collection',
|
|
1092
|
-
source:
|
|
1093
|
-
sort: { field:
|
|
1067
|
+
source: 'posts',
|
|
1068
|
+
sort: { field: 'date', order: 'desc' },
|
|
1094
1069
|
limit: 2, // Should get 2 most recent (highest date)
|
|
1095
|
-
children: [{ type:
|
|
1070
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1096
1071
|
};
|
|
1097
1072
|
|
|
1098
1073
|
const result = builder.buildComponent({
|
|
@@ -1106,22 +1081,22 @@ describe("ComponentBuilder", () => {
|
|
|
1106
1081
|
expect(element?.props?.children?.length).toBe(2);
|
|
1107
1082
|
});
|
|
1108
1083
|
|
|
1109
|
-
test(
|
|
1084
|
+
test('should apply multiple filter conditions with AND logic', () => {
|
|
1110
1085
|
const items: CMSItem[] = [
|
|
1111
|
-
{ _id:
|
|
1112
|
-
{ _id:
|
|
1113
|
-
{ _id:
|
|
1114
|
-
{ _id:
|
|
1086
|
+
{ _id: '1', _slug: 'p1', featured: true, price: 150 },
|
|
1087
|
+
{ _id: '2', _slug: 'p2', featured: false, price: 200 },
|
|
1088
|
+
{ _id: '3', _slug: 'p3', featured: true, price: 50 },
|
|
1089
|
+
{ _id: '4', _slug: 'p4', featured: true, price: 200 },
|
|
1115
1090
|
];
|
|
1116
1091
|
const node: ListNode = {
|
|
1117
1092
|
type: NODE_TYPE.LIST,
|
|
1118
1093
|
sourceType: 'collection',
|
|
1119
|
-
source:
|
|
1094
|
+
source: 'posts',
|
|
1120
1095
|
filter: [
|
|
1121
|
-
{ field:
|
|
1122
|
-
{ field:
|
|
1096
|
+
{ field: 'featured', value: true },
|
|
1097
|
+
{ field: 'price', operator: 'gte', value: 100 },
|
|
1123
1098
|
],
|
|
1124
|
-
children: [{ type:
|
|
1099
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1125
1100
|
};
|
|
1126
1101
|
|
|
1127
1102
|
const result = builder.buildComponent({
|
|
@@ -1137,7 +1112,7 @@ describe("ComponentBuilder", () => {
|
|
|
1137
1112
|
});
|
|
1138
1113
|
});
|
|
1139
1114
|
|
|
1140
|
-
describe(
|
|
1115
|
+
describe('Interactive Styles - Element Class Generation', () => {
|
|
1141
1116
|
/**
|
|
1142
1117
|
* These tests verify that element classes are generated correctly
|
|
1143
1118
|
* for interactive styles in both page and component contexts.
|
|
@@ -1149,11 +1124,11 @@ describe("ComponentBuilder", () => {
|
|
|
1149
1124
|
* 4. The same component element always gets the same class
|
|
1150
1125
|
*/
|
|
1151
1126
|
|
|
1152
|
-
test(
|
|
1127
|
+
test('should not add element class when no interactive styles', () => {
|
|
1153
1128
|
const node: ComponentNode = {
|
|
1154
|
-
type:
|
|
1155
|
-
tag:
|
|
1156
|
-
children: [
|
|
1129
|
+
type: 'node',
|
|
1130
|
+
tag: 'div',
|
|
1131
|
+
children: ['No interactive styles'],
|
|
1157
1132
|
};
|
|
1158
1133
|
|
|
1159
1134
|
const result = builder.buildComponent({ node });
|
|
@@ -1161,12 +1136,12 @@ describe("ComponentBuilder", () => {
|
|
|
1161
1136
|
// No element class should be added when there are no interactive styles
|
|
1162
1137
|
});
|
|
1163
1138
|
|
|
1164
|
-
test(
|
|
1139
|
+
test('should add element class when node has generateElementClass flag', () => {
|
|
1165
1140
|
const node: ComponentNode = {
|
|
1166
|
-
type:
|
|
1167
|
-
tag:
|
|
1141
|
+
type: 'node',
|
|
1142
|
+
tag: 'div',
|
|
1168
1143
|
generateElementClass: true,
|
|
1169
|
-
children: [
|
|
1144
|
+
children: ['Has element class flag'],
|
|
1170
1145
|
};
|
|
1171
1146
|
|
|
1172
1147
|
const result = builder.buildComponent({ node });
|
|
@@ -1174,14 +1149,12 @@ describe("ComponentBuilder", () => {
|
|
|
1174
1149
|
// Element class should be generated when flag is set
|
|
1175
1150
|
});
|
|
1176
1151
|
|
|
1177
|
-
test(
|
|
1152
|
+
test('should add element class when node has interactive styles', () => {
|
|
1178
1153
|
const node: ComponentNode = {
|
|
1179
|
-
type:
|
|
1180
|
-
tag:
|
|
1181
|
-
interactiveStyles: [
|
|
1182
|
-
|
|
1183
|
-
],
|
|
1184
|
-
children: ["Has interactive styles"],
|
|
1154
|
+
type: 'node',
|
|
1155
|
+
tag: 'div',
|
|
1156
|
+
interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
|
|
1157
|
+
children: ['Has interactive styles'],
|
|
1185
1158
|
};
|
|
1186
1159
|
|
|
1187
1160
|
const result = builder.buildComponent({ node });
|
|
@@ -1189,23 +1162,21 @@ describe("ComponentBuilder", () => {
|
|
|
1189
1162
|
// Element class should be generated for interactive styles
|
|
1190
1163
|
});
|
|
1191
1164
|
|
|
1192
|
-
test(
|
|
1165
|
+
test('should use page-based class for page elements', () => {
|
|
1193
1166
|
// Create builder with page name getter
|
|
1194
1167
|
const pageBuilder = new ComponentBuilder({
|
|
1195
1168
|
componentRegistry,
|
|
1196
1169
|
elementRegistry,
|
|
1197
|
-
getCurrentPageName: () =>
|
|
1198
|
-
getCurrentFileType: () =>
|
|
1170
|
+
getCurrentPageName: () => 'about',
|
|
1171
|
+
getCurrentFileType: () => 'page',
|
|
1199
1172
|
});
|
|
1200
1173
|
|
|
1201
1174
|
const node: ComponentNode = {
|
|
1202
|
-
type:
|
|
1203
|
-
tag:
|
|
1204
|
-
label:
|
|
1205
|
-
interactiveStyles: [
|
|
1206
|
-
|
|
1207
|
-
],
|
|
1208
|
-
children: ["Submit"],
|
|
1175
|
+
type: 'node',
|
|
1176
|
+
tag: 'button',
|
|
1177
|
+
label: 'submit-button',
|
|
1178
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1179
|
+
children: ['Submit'],
|
|
1209
1180
|
};
|
|
1210
1181
|
|
|
1211
1182
|
const result = pageBuilder.buildComponent({ node });
|
|
@@ -1213,28 +1184,26 @@ describe("ComponentBuilder", () => {
|
|
|
1213
1184
|
// Element class should be p_about_submit-button for page elements
|
|
1214
1185
|
});
|
|
1215
1186
|
|
|
1216
|
-
test(
|
|
1187
|
+
test('should use component-based class when inside component context', () => {
|
|
1217
1188
|
const node: ComponentNode = {
|
|
1218
|
-
type:
|
|
1219
|
-
tag:
|
|
1220
|
-
label:
|
|
1221
|
-
interactiveStyles: [
|
|
1222
|
-
|
|
1223
|
-
],
|
|
1224
|
-
children: ["Click"],
|
|
1189
|
+
type: 'node',
|
|
1190
|
+
tag: 'button',
|
|
1191
|
+
label: 'action-button',
|
|
1192
|
+
interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary-dark' } }],
|
|
1193
|
+
children: ['Click'],
|
|
1225
1194
|
};
|
|
1226
1195
|
|
|
1227
1196
|
// Build with componentContext set (inside a component)
|
|
1228
1197
|
const result = builder.buildComponent({
|
|
1229
1198
|
node,
|
|
1230
|
-
componentContext:
|
|
1199
|
+
componentContext: 'Button',
|
|
1231
1200
|
});
|
|
1232
1201
|
|
|
1233
1202
|
expect(result).not.toBeNull();
|
|
1234
1203
|
// Element class should be c_button_action-button for component elements
|
|
1235
1204
|
});
|
|
1236
1205
|
|
|
1237
|
-
test(
|
|
1206
|
+
test('should use relative path for component elements', () => {
|
|
1238
1207
|
// When a component is placed at different positions on a page,
|
|
1239
1208
|
// its internal elements should always get the same class
|
|
1240
1209
|
// because they use paths relative to the component root.
|
|
@@ -1243,32 +1212,28 @@ describe("ComponentBuilder", () => {
|
|
|
1243
1212
|
component: {
|
|
1244
1213
|
interface: {},
|
|
1245
1214
|
structure: {
|
|
1246
|
-
type:
|
|
1247
|
-
tag:
|
|
1248
|
-
label:
|
|
1249
|
-
interactiveStyles: [
|
|
1250
|
-
{ postfix: ":hover", style: { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" } },
|
|
1251
|
-
],
|
|
1215
|
+
type: 'node',
|
|
1216
|
+
tag: 'div',
|
|
1217
|
+
label: 'root',
|
|
1218
|
+
interactiveStyles: [{ postfix: ':hover', style: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' } }],
|
|
1252
1219
|
children: [
|
|
1253
1220
|
{
|
|
1254
|
-
type:
|
|
1255
|
-
tag:
|
|
1256
|
-
label:
|
|
1257
|
-
interactiveStyles: [
|
|
1258
|
-
|
|
1259
|
-
],
|
|
1260
|
-
children: ["Label"],
|
|
1221
|
+
type: 'node',
|
|
1222
|
+
tag: 'span',
|
|
1223
|
+
label: 'label',
|
|
1224
|
+
interactiveStyles: [{ postfix: ':hover', style: { color: 'primary' } }],
|
|
1225
|
+
children: ['Label'],
|
|
1261
1226
|
},
|
|
1262
1227
|
],
|
|
1263
1228
|
},
|
|
1264
1229
|
},
|
|
1265
1230
|
};
|
|
1266
|
-
componentRegistry.register(
|
|
1231
|
+
componentRegistry.register('HoverCard', componentDef as any);
|
|
1267
1232
|
|
|
1268
1233
|
// First instance at page path [0]
|
|
1269
1234
|
const node1: ComponentNode = {
|
|
1270
|
-
type:
|
|
1271
|
-
component:
|
|
1235
|
+
type: 'component',
|
|
1236
|
+
component: 'HoverCard',
|
|
1272
1237
|
};
|
|
1273
1238
|
|
|
1274
1239
|
const result1 = builder.buildComponent({
|
|
@@ -1291,7 +1256,7 @@ describe("ComponentBuilder", () => {
|
|
|
1291
1256
|
// component-relative paths, not page-absolute paths
|
|
1292
1257
|
});
|
|
1293
1258
|
|
|
1294
|
-
test(
|
|
1259
|
+
test('should generate different classes for page vs component with same name', () => {
|
|
1295
1260
|
// A page named "button" and a component named "Button" should
|
|
1296
1261
|
// produce different element classes (p_button_... vs c_button_...)
|
|
1297
1262
|
|
|
@@ -1299,22 +1264,20 @@ describe("ComponentBuilder", () => {
|
|
|
1299
1264
|
component: {
|
|
1300
1265
|
interface: {},
|
|
1301
1266
|
structure: {
|
|
1302
|
-
type:
|
|
1303
|
-
tag:
|
|
1304
|
-
label:
|
|
1305
|
-
interactiveStyles: [
|
|
1306
|
-
|
|
1307
|
-
],
|
|
1308
|
-
children: ["Component Button"],
|
|
1267
|
+
type: 'node',
|
|
1268
|
+
tag: 'button',
|
|
1269
|
+
label: 'root',
|
|
1270
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1271
|
+
children: ['Component Button'],
|
|
1309
1272
|
},
|
|
1310
1273
|
},
|
|
1311
1274
|
};
|
|
1312
|
-
componentRegistry.register(
|
|
1275
|
+
componentRegistry.register('Button', componentDef as any);
|
|
1313
1276
|
|
|
1314
1277
|
// Component instance
|
|
1315
1278
|
const componentNode: ComponentNode = {
|
|
1316
|
-
type:
|
|
1317
|
-
component:
|
|
1279
|
+
type: 'component',
|
|
1280
|
+
component: 'Button',
|
|
1318
1281
|
};
|
|
1319
1282
|
|
|
1320
1283
|
const componentResult = builder.buildComponent({ node: componentNode });
|
|
@@ -1324,18 +1287,16 @@ describe("ComponentBuilder", () => {
|
|
|
1324
1287
|
const pageBuilder = new ComponentBuilder({
|
|
1325
1288
|
componentRegistry,
|
|
1326
1289
|
elementRegistry,
|
|
1327
|
-
getCurrentPageName: () =>
|
|
1328
|
-
getCurrentFileType: () =>
|
|
1290
|
+
getCurrentPageName: () => 'button',
|
|
1291
|
+
getCurrentFileType: () => 'page',
|
|
1329
1292
|
});
|
|
1330
1293
|
|
|
1331
1294
|
const pageNode: ComponentNode = {
|
|
1332
|
-
type:
|
|
1333
|
-
tag:
|
|
1334
|
-
label:
|
|
1335
|
-
interactiveStyles: [
|
|
1336
|
-
|
|
1337
|
-
],
|
|
1338
|
-
children: ["Page Button"],
|
|
1295
|
+
type: 'node',
|
|
1296
|
+
tag: 'button',
|
|
1297
|
+
label: 'root',
|
|
1298
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1299
|
+
children: ['Page Button'],
|
|
1339
1300
|
};
|
|
1340
1301
|
|
|
1341
1302
|
const pageResult = pageBuilder.buildComponent({ node: pageNode });
|
|
@@ -1344,26 +1305,24 @@ describe("ComponentBuilder", () => {
|
|
|
1344
1305
|
// Component uses c_button_root, page uses p_button_root
|
|
1345
1306
|
});
|
|
1346
1307
|
|
|
1347
|
-
test(
|
|
1308
|
+
test('should propagate componentContext through nested children', () => {
|
|
1348
1309
|
const componentDef = {
|
|
1349
1310
|
component: {
|
|
1350
1311
|
interface: {},
|
|
1351
1312
|
structure: {
|
|
1352
|
-
type:
|
|
1353
|
-
tag:
|
|
1313
|
+
type: 'node',
|
|
1314
|
+
tag: 'div',
|
|
1354
1315
|
children: [
|
|
1355
1316
|
{
|
|
1356
|
-
type:
|
|
1357
|
-
tag:
|
|
1317
|
+
type: 'node',
|
|
1318
|
+
tag: 'div',
|
|
1358
1319
|
children: [
|
|
1359
1320
|
{
|
|
1360
|
-
type:
|
|
1361
|
-
tag:
|
|
1362
|
-
label:
|
|
1363
|
-
interactiveStyles: [
|
|
1364
|
-
|
|
1365
|
-
],
|
|
1366
|
-
children: ["Nested Button"],
|
|
1321
|
+
type: 'node',
|
|
1322
|
+
tag: 'button',
|
|
1323
|
+
label: 'deeply-nested',
|
|
1324
|
+
interactiveStyles: [{ postfix: ':hover', style: { color: 'primary' } }],
|
|
1325
|
+
children: ['Nested Button'],
|
|
1367
1326
|
},
|
|
1368
1327
|
],
|
|
1369
1328
|
},
|
|
@@ -1371,11 +1330,11 @@ describe("ComponentBuilder", () => {
|
|
|
1371
1330
|
},
|
|
1372
1331
|
},
|
|
1373
1332
|
};
|
|
1374
|
-
componentRegistry.register(
|
|
1333
|
+
componentRegistry.register('DeepComponent', componentDef as any);
|
|
1375
1334
|
|
|
1376
1335
|
const node: ComponentNode = {
|
|
1377
|
-
type:
|
|
1378
|
-
component:
|
|
1336
|
+
type: 'component',
|
|
1337
|
+
component: 'DeepComponent',
|
|
1379
1338
|
};
|
|
1380
1339
|
|
|
1381
1340
|
const result = builder.buildComponent({ node });
|
|
@@ -1385,50 +1344,50 @@ describe("ComponentBuilder", () => {
|
|
|
1385
1344
|
// because componentContext is propagated through the tree
|
|
1386
1345
|
});
|
|
1387
1346
|
|
|
1388
|
-
test(
|
|
1347
|
+
test('should handle multiple interactive states on same element', () => {
|
|
1389
1348
|
const node: ComponentNode = {
|
|
1390
|
-
type:
|
|
1391
|
-
tag:
|
|
1392
|
-
label:
|
|
1349
|
+
type: 'node',
|
|
1350
|
+
tag: 'button',
|
|
1351
|
+
label: 'multi-state',
|
|
1393
1352
|
interactiveStyles: [
|
|
1394
|
-
{ postfix:
|
|
1395
|
-
{ postfix:
|
|
1396
|
-
{ postfix:
|
|
1397
|
-
{ postfix:
|
|
1353
|
+
{ postfix: ':hover', style: { backgroundColor: 'primary-light' } },
|
|
1354
|
+
{ postfix: ':active', style: { backgroundColor: 'primary-dark' } },
|
|
1355
|
+
{ postfix: ':focus', style: { outline: '2px solid var(--primary)' } },
|
|
1356
|
+
{ postfix: ':disabled', style: { opacity: '0.5', cursor: 'not-allowed' } },
|
|
1398
1357
|
],
|
|
1399
|
-
children: [
|
|
1358
|
+
children: ['Multi-state Button'],
|
|
1400
1359
|
};
|
|
1401
1360
|
|
|
1402
1361
|
const result = builder.buildComponent({
|
|
1403
1362
|
node,
|
|
1404
|
-
componentContext:
|
|
1363
|
+
componentContext: 'MultiButton',
|
|
1405
1364
|
});
|
|
1406
1365
|
|
|
1407
1366
|
expect(result).not.toBeNull();
|
|
1408
1367
|
// All states should use the same element class: c_multibutton_multi-state
|
|
1409
1368
|
});
|
|
1410
1369
|
|
|
1411
|
-
test(
|
|
1370
|
+
test('should handle responsive interactive styles', () => {
|
|
1412
1371
|
const node: ComponentNode = {
|
|
1413
|
-
type:
|
|
1414
|
-
tag:
|
|
1415
|
-
label:
|
|
1372
|
+
type: 'node',
|
|
1373
|
+
tag: 'div',
|
|
1374
|
+
label: 'responsive-hover',
|
|
1416
1375
|
interactiveStyles: [
|
|
1417
1376
|
{
|
|
1418
|
-
postfix:
|
|
1377
|
+
postfix: ':hover',
|
|
1419
1378
|
style: {
|
|
1420
|
-
base: { padding:
|
|
1421
|
-
tablet: { padding:
|
|
1422
|
-
mobile: { padding:
|
|
1379
|
+
base: { padding: '20px' },
|
|
1380
|
+
tablet: { padding: '16px' },
|
|
1381
|
+
mobile: { padding: '12px' },
|
|
1423
1382
|
},
|
|
1424
1383
|
},
|
|
1425
1384
|
],
|
|
1426
|
-
children: [
|
|
1385
|
+
children: ['Responsive hover'],
|
|
1427
1386
|
};
|
|
1428
1387
|
|
|
1429
1388
|
const result = builder.buildComponent({
|
|
1430
1389
|
node,
|
|
1431
|
-
componentContext:
|
|
1390
|
+
componentContext: 'ResponsiveCard',
|
|
1432
1391
|
});
|
|
1433
1392
|
|
|
1434
1393
|
expect(result).not.toBeNull();
|
|
@@ -1436,27 +1395,27 @@ describe("ComponentBuilder", () => {
|
|
|
1436
1395
|
});
|
|
1437
1396
|
});
|
|
1438
1397
|
|
|
1439
|
-
describe(
|
|
1440
|
-
test(
|
|
1398
|
+
describe('buildComponent - Conditional Rendering (if property)', () => {
|
|
1399
|
+
test('should render node when if is true', () => {
|
|
1441
1400
|
const node: ComponentNode = {
|
|
1442
|
-
type:
|
|
1443
|
-
tag:
|
|
1401
|
+
type: 'node',
|
|
1402
|
+
tag: 'div',
|
|
1444
1403
|
if: true,
|
|
1445
|
-
children: [
|
|
1404
|
+
children: ['Visible content'],
|
|
1446
1405
|
} as any;
|
|
1447
1406
|
|
|
1448
1407
|
const result = builder.buildComponent({ node });
|
|
1449
1408
|
|
|
1450
1409
|
expect(result).not.toBeNull();
|
|
1451
|
-
expect(typeof result).toBe(
|
|
1410
|
+
expect(typeof result).toBe('object');
|
|
1452
1411
|
});
|
|
1453
1412
|
|
|
1454
|
-
test(
|
|
1413
|
+
test('should skip node when if is false', () => {
|
|
1455
1414
|
const node: ComponentNode = {
|
|
1456
|
-
type:
|
|
1457
|
-
tag:
|
|
1415
|
+
type: 'node',
|
|
1416
|
+
tag: 'div',
|
|
1458
1417
|
if: false,
|
|
1459
|
-
children: [
|
|
1418
|
+
children: ['Hidden content'],
|
|
1460
1419
|
} as any;
|
|
1461
1420
|
|
|
1462
1421
|
const result = builder.buildComponent({ node });
|
|
@@ -1464,11 +1423,11 @@ describe("ComponentBuilder", () => {
|
|
|
1464
1423
|
expect(result).toBeNull();
|
|
1465
1424
|
});
|
|
1466
1425
|
|
|
1467
|
-
test(
|
|
1426
|
+
test('should render node when if property is absent', () => {
|
|
1468
1427
|
const node: ComponentNode = {
|
|
1469
|
-
type:
|
|
1470
|
-
tag:
|
|
1471
|
-
children: [
|
|
1428
|
+
type: 'node',
|
|
1429
|
+
tag: 'div',
|
|
1430
|
+
children: ['Default visible'],
|
|
1472
1431
|
};
|
|
1473
1432
|
|
|
1474
1433
|
const result = builder.buildComponent({ node });
|
|
@@ -1476,41 +1435,41 @@ describe("ComponentBuilder", () => {
|
|
|
1476
1435
|
expect(result).not.toBeNull();
|
|
1477
1436
|
});
|
|
1478
1437
|
|
|
1479
|
-
test(
|
|
1438
|
+
test('should resolve if mapping from component props', () => {
|
|
1480
1439
|
const node: ComponentNode = {
|
|
1481
|
-
type:
|
|
1482
|
-
tag:
|
|
1440
|
+
type: 'node',
|
|
1441
|
+
tag: 'div',
|
|
1483
1442
|
if: {
|
|
1484
1443
|
_mapping: true,
|
|
1485
|
-
prop:
|
|
1486
|
-
values: {
|
|
1444
|
+
prop: 'showBanner',
|
|
1445
|
+
values: { true: true, false: false },
|
|
1487
1446
|
},
|
|
1488
|
-
children: [
|
|
1447
|
+
children: ['Conditional banner'],
|
|
1489
1448
|
} as any;
|
|
1490
1449
|
|
|
1491
1450
|
// When prop is 'true', should render
|
|
1492
1451
|
const resultVisible = builder.buildComponent({
|
|
1493
1452
|
node,
|
|
1494
|
-
componentResolvedProps: { showBanner:
|
|
1453
|
+
componentResolvedProps: { showBanner: 'true' },
|
|
1495
1454
|
});
|
|
1496
1455
|
expect(resultVisible).not.toBeNull();
|
|
1497
1456
|
|
|
1498
1457
|
// When prop is 'false', should skip
|
|
1499
1458
|
const resultHidden = builder.buildComponent({
|
|
1500
1459
|
node,
|
|
1501
|
-
componentResolvedProps: { showBanner:
|
|
1460
|
+
componentResolvedProps: { showBanner: 'false' },
|
|
1502
1461
|
});
|
|
1503
1462
|
expect(resultHidden).toBeNull();
|
|
1504
1463
|
});
|
|
1505
1464
|
|
|
1506
|
-
test(
|
|
1465
|
+
test('should skip children when parent if is false', () => {
|
|
1507
1466
|
const node: ComponentNode = {
|
|
1508
|
-
type:
|
|
1509
|
-
tag:
|
|
1467
|
+
type: 'node',
|
|
1468
|
+
tag: 'div',
|
|
1510
1469
|
if: false,
|
|
1511
1470
|
children: [
|
|
1512
|
-
{ type:
|
|
1513
|
-
{ type:
|
|
1471
|
+
{ type: 'node', tag: 'span', children: ['Child 1'] },
|
|
1472
|
+
{ type: 'node', tag: 'span', children: ['Child 2'] },
|
|
1514
1473
|
],
|
|
1515
1474
|
} as any;
|
|
1516
1475
|
|
|
@@ -1519,16 +1478,16 @@ describe("ComponentBuilder", () => {
|
|
|
1519
1478
|
expect(result).toBeNull();
|
|
1520
1479
|
});
|
|
1521
1480
|
|
|
1522
|
-
test(
|
|
1523
|
-
componentRegistry.register(
|
|
1481
|
+
test('should handle if with component nodes', () => {
|
|
1482
|
+
componentRegistry.register('TestComponent', {
|
|
1524
1483
|
component: {
|
|
1525
|
-
structure: { type:
|
|
1484
|
+
structure: { type: 'node', tag: 'div', children: ['Component content'] },
|
|
1526
1485
|
},
|
|
1527
1486
|
} as any);
|
|
1528
1487
|
|
|
1529
1488
|
const node: ComponentNode = {
|
|
1530
|
-
type:
|
|
1531
|
-
component:
|
|
1489
|
+
type: 'component',
|
|
1490
|
+
component: 'TestComponent',
|
|
1532
1491
|
if: false,
|
|
1533
1492
|
} as any;
|
|
1534
1493
|
|
|
@@ -1537,10 +1496,10 @@ describe("ComponentBuilder", () => {
|
|
|
1537
1496
|
expect(result).toBeNull();
|
|
1538
1497
|
});
|
|
1539
1498
|
|
|
1540
|
-
test(
|
|
1499
|
+
test('should handle if with embed nodes', () => {
|
|
1541
1500
|
const node: ComponentNode = {
|
|
1542
|
-
type:
|
|
1543
|
-
html:
|
|
1501
|
+
type: 'embed',
|
|
1502
|
+
html: '<svg></svg>',
|
|
1544
1503
|
if: false,
|
|
1545
1504
|
} as any;
|
|
1546
1505
|
|
|
@@ -1549,12 +1508,12 @@ describe("ComponentBuilder", () => {
|
|
|
1549
1508
|
expect(result).toBeNull();
|
|
1550
1509
|
});
|
|
1551
1510
|
|
|
1552
|
-
test(
|
|
1511
|
+
test('should handle if with link nodes', () => {
|
|
1553
1512
|
const node: ComponentNode = {
|
|
1554
|
-
type:
|
|
1555
|
-
href:
|
|
1513
|
+
type: 'link',
|
|
1514
|
+
href: '/page',
|
|
1556
1515
|
if: false,
|
|
1557
|
-
children: [
|
|
1516
|
+
children: ['Hidden link'],
|
|
1558
1517
|
} as any;
|
|
1559
1518
|
|
|
1560
1519
|
const result = builder.buildComponent({ node });
|
|
@@ -1562,16 +1521,16 @@ describe("ComponentBuilder", () => {
|
|
|
1562
1521
|
expect(result).toBeNull();
|
|
1563
1522
|
});
|
|
1564
1523
|
|
|
1565
|
-
test(
|
|
1524
|
+
test('treats unresolved string-template if as falsy', () => {
|
|
1566
1525
|
// When `if: "{{buttonText}}"` references an undefined prop,
|
|
1567
1526
|
// evaluateTemplate preserves the original template string (backward-compat).
|
|
1568
1527
|
// Without component/CMS/item context to resolve it, treat the node as hidden
|
|
1569
1528
|
// — matches user expectation when binding visibility to an unset prop.
|
|
1570
1529
|
const node: ComponentNode = {
|
|
1571
|
-
type:
|
|
1572
|
-
href:
|
|
1573
|
-
if:
|
|
1574
|
-
children: [
|
|
1530
|
+
type: 'link',
|
|
1531
|
+
href: '/page',
|
|
1532
|
+
if: '{{buttonText}}',
|
|
1533
|
+
children: ['Hidden link'],
|
|
1575
1534
|
} as any;
|
|
1576
1535
|
|
|
1577
1536
|
const result = builder.buildComponent({ node });
|
|
@@ -1579,9 +1538,9 @@ describe("ComponentBuilder", () => {
|
|
|
1579
1538
|
expect(result).toBeNull();
|
|
1580
1539
|
});
|
|
1581
1540
|
|
|
1582
|
-
test(
|
|
1541
|
+
test('should handle if with locale-list nodes', () => {
|
|
1583
1542
|
const node: ComponentNode = {
|
|
1584
|
-
type:
|
|
1543
|
+
type: 'locale-list',
|
|
1585
1544
|
if: false,
|
|
1586
1545
|
} as any;
|
|
1587
1546
|
|
|
@@ -1590,29 +1549,29 @@ describe("ComponentBuilder", () => {
|
|
|
1590
1549
|
expect(result).toBeNull();
|
|
1591
1550
|
});
|
|
1592
1551
|
|
|
1593
|
-
test(
|
|
1552
|
+
test('should render when mapping prop value not found (default true)', () => {
|
|
1594
1553
|
const node: ComponentNode = {
|
|
1595
|
-
type:
|
|
1596
|
-
tag:
|
|
1554
|
+
type: 'node',
|
|
1555
|
+
tag: 'div',
|
|
1597
1556
|
if: {
|
|
1598
1557
|
_mapping: true,
|
|
1599
|
-
prop:
|
|
1600
|
-
values: {
|
|
1558
|
+
prop: 'status',
|
|
1559
|
+
values: { active: true, inactive: false },
|
|
1601
1560
|
},
|
|
1602
|
-
children: [
|
|
1561
|
+
children: ['Content'],
|
|
1603
1562
|
} as any;
|
|
1604
1563
|
|
|
1605
1564
|
// 'unknown' is not in the values map, should default to true
|
|
1606
1565
|
const result = builder.buildComponent({
|
|
1607
1566
|
node,
|
|
1608
|
-
componentResolvedProps: { status:
|
|
1567
|
+
componentResolvedProps: { status: 'unknown' },
|
|
1609
1568
|
});
|
|
1610
1569
|
|
|
1611
1570
|
expect(result).not.toBeNull();
|
|
1612
1571
|
});
|
|
1613
1572
|
});
|
|
1614
1573
|
|
|
1615
|
-
describe(
|
|
1574
|
+
describe('Edge Case 7: List Paths Tracking (Collection Mode)', () => {
|
|
1616
1575
|
const createMockCMSItems = (count: number): CMSItem[] => {
|
|
1617
1576
|
return Array.from({ length: count }, (_, i) => ({
|
|
1618
1577
|
_id: `item-${i}`,
|
|
@@ -1621,13 +1580,13 @@ describe("ComponentBuilder", () => {
|
|
|
1621
1580
|
}));
|
|
1622
1581
|
};
|
|
1623
1582
|
|
|
1624
|
-
test(
|
|
1583
|
+
test('should pass listPaths through buildChildren', () => {
|
|
1625
1584
|
// Create a simple list with items
|
|
1626
1585
|
const node: ListNode = {
|
|
1627
1586
|
type: NODE_TYPE.LIST,
|
|
1628
1587
|
sourceType: 'collection',
|
|
1629
|
-
source:
|
|
1630
|
-
children: [{ type:
|
|
1588
|
+
source: 'posts',
|
|
1589
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1631
1590
|
};
|
|
1632
1591
|
|
|
1633
1592
|
const items = createMockCMSItems(2);
|
|
@@ -1643,18 +1602,18 @@ describe("ComponentBuilder", () => {
|
|
|
1643
1602
|
expect(element?.props?.children?.length).toBe(2);
|
|
1644
1603
|
});
|
|
1645
1604
|
|
|
1646
|
-
test(
|
|
1605
|
+
test('should track listPaths in nested lists', () => {
|
|
1647
1606
|
// Outer list with inner list
|
|
1648
1607
|
const outerNode: ListNode = {
|
|
1649
1608
|
type: NODE_TYPE.LIST,
|
|
1650
1609
|
sourceType: 'collection',
|
|
1651
|
-
source:
|
|
1610
|
+
source: 'categories',
|
|
1652
1611
|
children: [
|
|
1653
1612
|
{
|
|
1654
1613
|
type: NODE_TYPE.LIST,
|
|
1655
1614
|
sourceType: 'collection',
|
|
1656
|
-
source:
|
|
1657
|
-
children: [{ type:
|
|
1615
|
+
source: 'posts',
|
|
1616
|
+
children: [{ type: 'node', tag: 'span', children: ['Post'] }],
|
|
1658
1617
|
} as any,
|
|
1659
1618
|
],
|
|
1660
1619
|
};
|
|
@@ -1673,28 +1632,26 @@ describe("ComponentBuilder", () => {
|
|
|
1673
1632
|
});
|
|
1674
1633
|
});
|
|
1675
1634
|
|
|
1676
|
-
describe(
|
|
1677
|
-
test(
|
|
1635
|
+
describe('Edge Case 5: Slot Content Context Handling', () => {
|
|
1636
|
+
test('should not pass componentContext to slot content elements', () => {
|
|
1678
1637
|
// Register a component with a slot
|
|
1679
|
-
componentRegistry.register(
|
|
1680
|
-
type:
|
|
1638
|
+
componentRegistry.register('Container', {
|
|
1639
|
+
type: 'Container',
|
|
1681
1640
|
component: {
|
|
1682
1641
|
interface: {},
|
|
1683
1642
|
structure: {
|
|
1684
|
-
type:
|
|
1685
|
-
tag:
|
|
1686
|
-
children: [{ type:
|
|
1643
|
+
type: 'node',
|
|
1644
|
+
tag: 'div',
|
|
1645
|
+
children: [{ type: 'slot' }],
|
|
1687
1646
|
},
|
|
1688
1647
|
},
|
|
1689
1648
|
});
|
|
1690
1649
|
|
|
1691
1650
|
// Component instance with children (slot content)
|
|
1692
1651
|
const node: ComponentNode = {
|
|
1693
|
-
type:
|
|
1694
|
-
component:
|
|
1695
|
-
children: [
|
|
1696
|
-
{ type: "node", tag: "span", children: ["Slot content"] },
|
|
1697
|
-
],
|
|
1652
|
+
type: 'component',
|
|
1653
|
+
component: 'Container',
|
|
1654
|
+
children: [{ type: 'node', tag: 'span', children: ['Slot content'] }],
|
|
1698
1655
|
};
|
|
1699
1656
|
|
|
1700
1657
|
const result = builder.buildComponent({
|
|
@@ -1704,29 +1661,29 @@ describe("ComponentBuilder", () => {
|
|
|
1704
1661
|
|
|
1705
1662
|
// Should render successfully
|
|
1706
1663
|
expect(result).not.toBeNull();
|
|
1707
|
-
expect(typeof result).toBe(
|
|
1664
|
+
expect(typeof result).toBe('object');
|
|
1708
1665
|
});
|
|
1709
1666
|
|
|
1710
|
-
test(
|
|
1667
|
+
test('should mark slot content with _isSlotContent flag', () => {
|
|
1711
1668
|
// This tests that slot content is marked, which affects context handling
|
|
1712
|
-
componentRegistry.register(
|
|
1713
|
-
type:
|
|
1669
|
+
componentRegistry.register('Wrapper', {
|
|
1670
|
+
type: 'Wrapper',
|
|
1714
1671
|
component: {
|
|
1715
1672
|
interface: {},
|
|
1716
1673
|
structure: {
|
|
1717
|
-
type:
|
|
1718
|
-
tag:
|
|
1719
|
-
children: [{ type:
|
|
1674
|
+
type: 'node',
|
|
1675
|
+
tag: 'section',
|
|
1676
|
+
children: [{ type: 'slot' }],
|
|
1720
1677
|
},
|
|
1721
1678
|
},
|
|
1722
1679
|
});
|
|
1723
1680
|
|
|
1724
1681
|
const node: ComponentNode = {
|
|
1725
|
-
type:
|
|
1726
|
-
component:
|
|
1682
|
+
type: 'component',
|
|
1683
|
+
component: 'Wrapper',
|
|
1727
1684
|
children: [
|
|
1728
|
-
{ type:
|
|
1729
|
-
{ type:
|
|
1685
|
+
{ type: 'node', tag: 'div', children: ['Child 1'] },
|
|
1686
|
+
{ type: 'node', tag: 'div', children: ['Child 2'] },
|
|
1730
1687
|
],
|
|
1731
1688
|
};
|
|
1732
1689
|
|
|
@@ -1738,27 +1695,24 @@ describe("ComponentBuilder", () => {
|
|
|
1738
1695
|
expect(element?.props?.children).toBeDefined();
|
|
1739
1696
|
});
|
|
1740
1697
|
|
|
1741
|
-
test(
|
|
1698
|
+
test('should preserve componentContext for component structure elements', () => {
|
|
1742
1699
|
// Register a component
|
|
1743
|
-
componentRegistry.register(
|
|
1744
|
-
type:
|
|
1700
|
+
componentRegistry.register('Card', {
|
|
1701
|
+
type: 'Card',
|
|
1745
1702
|
component: {
|
|
1746
1703
|
interface: {},
|
|
1747
1704
|
structure: {
|
|
1748
|
-
type:
|
|
1749
|
-
tag:
|
|
1750
|
-
children: [
|
|
1751
|
-
{ type: "node", tag: "h2", children: ["Header"] },
|
|
1752
|
-
{ type: "slot" },
|
|
1753
|
-
],
|
|
1705
|
+
type: 'node',
|
|
1706
|
+
tag: 'article',
|
|
1707
|
+
children: [{ type: 'node', tag: 'h2', children: ['Header'] }, { type: 'slot' }],
|
|
1754
1708
|
},
|
|
1755
1709
|
},
|
|
1756
1710
|
});
|
|
1757
1711
|
|
|
1758
1712
|
const node: ComponentNode = {
|
|
1759
|
-
type:
|
|
1760
|
-
component:
|
|
1761
|
-
children: [{ type:
|
|
1713
|
+
type: 'component',
|
|
1714
|
+
component: 'Card',
|
|
1715
|
+
children: [{ type: 'node', tag: 'p', children: ['Content'] }],
|
|
1762
1716
|
};
|
|
1763
1717
|
|
|
1764
1718
|
const result = builder.buildComponent({ node });
|
|
@@ -1768,4 +1722,3 @@ describe("ComponentBuilder", () => {
|
|
|
1768
1722
|
});
|
|
1769
1723
|
});
|
|
1770
1724
|
});
|
|
1771
|
-
|