meno-core 1.0.53 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +1 -3
- package/bin/cli.ts +66 -261
- package/build-astro.ts +150 -128
- package/dist/bin/cli.js +42 -185
- package/dist/bin/cli.js.map +2 -2
- package/dist/chunks/chunk-2AR55GYH.js +42 -0
- package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
- package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
- package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
- package/dist/chunks/chunk-4ZRU52J2.js +169 -0
- package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
- package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
- package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
- package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
- package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
- package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
- package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
- package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
- package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
- package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
- package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
- package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
- package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
- package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
- package/dist/lib/client/index.js +1003 -475
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14393 -6371
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +342 -101
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +64 -80
- package/dist/lib/test-utils/index.js.map +2 -2
- package/entries/client-router.tsx +10 -8
- package/lib/client/ClientInitializer.ts +8 -8
- package/lib/client/ErrorBoundary.test.tsx +246 -186
- package/lib/client/ErrorBoundary.tsx +183 -132
- package/lib/client/componentRegistry.test.ts +96 -108
- package/lib/client/componentRegistry.ts +1 -3
- package/lib/client/contexts/ThemeContext.tsx +3 -2
- package/lib/client/core/ComponentBuilder.test.ts +663 -560
- package/lib/client/core/ComponentBuilder.ts +427 -155
- package/lib/client/core/ComponentRenderer.test.tsx +2 -4
- package/lib/client/core/ComponentRenderer.tsx +46 -33
- package/lib/client/core/builders/embedBuilder.ts +242 -54
- package/lib/client/core/builders/linkBuilder.ts +72 -45
- package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
- package/lib/client/core/builders/listBuilder.ts +155 -89
- package/lib/client/core/builders/localeListBuilder.ts +97 -62
- package/lib/client/core/builders/types.ts +5 -5
- package/lib/client/core/cmsTemplateProcessor.ts +8 -9
- package/lib/client/elementRegistry.ts +3 -3
- package/lib/client/fontFamiliesService.test.ts +2 -10
- package/lib/client/fontFamiliesService.ts +3 -3
- package/lib/client/hmr/HMRManager.tsx +47 -30
- package/lib/client/hmrCssReload.ts +49 -14
- package/lib/client/hmrWebSocket.ts +14 -19
- package/lib/client/hooks/useColorVariables.test.ts +21 -21
- package/lib/client/hooks/useColorVariables.ts +23 -18
- package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
- package/lib/client/hooks/useVariables.ts +6 -4
- package/lib/client/hydration/HydrationUtils.test.ts +24 -25
- package/lib/client/hydration/HydrationUtils.ts +3 -4
- package/lib/client/i18nConfigService.test.ts +2 -7
- package/lib/client/i18nConfigService.ts +2 -2
- package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
- package/lib/client/meno-filter/MenoFilter.ts +14 -15
- package/lib/client/meno-filter/bindings.ts +18 -43
- package/lib/client/meno-filter/init.ts +4 -4
- package/lib/client/meno-filter/renderer.ts +30 -32
- package/lib/client/meno-filter/script.generated.ts +1 -3
- package/lib/client/meno-filter/ui.ts +4 -6
- package/lib/client/meno-filter/updates.ts +18 -23
- package/lib/client/meno-filter/utils.ts +1 -1
- package/lib/client/navigation.test.ts +174 -178
- package/lib/client/navigation.ts +0 -1
- package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
- package/lib/client/responsiveStyleResolver.test.ts +234 -232
- package/lib/client/responsiveStyleResolver.ts +74 -20
- package/lib/client/routing/RouteLoader.test.ts +24 -25
- package/lib/client/routing/RouteLoader.ts +27 -35
- package/lib/client/routing/Router.tsx +210 -153
- package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
- package/lib/client/scripts/ScriptExecutor.ts +18 -36
- package/lib/client/services/PrefetchService.test.ts +3 -3
- package/lib/client/services/PrefetchService.ts +23 -29
- package/lib/client/styleProcessor.test.ts +9 -9
- package/lib/client/styleProcessor.ts +24 -15
- package/lib/client/styles/StyleInjector.test.ts +122 -115
- package/lib/client/styles/StyleInjector.ts +10 -7
- package/lib/client/styles/UtilityClassCollector.ts +62 -28
- package/lib/client/styles/cspNonce.test.ts +2 -5
- package/lib/client/templateEngine.test.ts +554 -488
- package/lib/client/templateEngine.ts +203 -73
- package/lib/client/theme.test.ts +1 -1
- package/lib/client/theme.ts +0 -1
- package/lib/client/utils/toast.ts +0 -1
- package/lib/server/__integration__/api-routes.test.ts +8 -4
- package/lib/server/__integration__/cms-integration.test.ts +1 -4
- package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
- package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
- package/lib/server/__integration__/static-assets.test.ts +3 -3
- package/lib/server/__integration__/test-helpers.ts +87 -74
- package/lib/server/ab/generateFunctions.ts +12 -10
- package/lib/server/astro/cmsPageEmitter.ts +26 -42
- package/lib/server/astro/componentEmitter.ts +35 -43
- package/lib/server/astro/cssCollector.ts +10 -26
- package/lib/server/astro/nodeToAstro.test.ts +1754 -34
- package/lib/server/astro/nodeToAstro.ts +232 -216
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
- package/lib/server/astro/pageEmitter.ts +9 -13
- package/lib/server/astro/tailwindMapper.test.ts +10 -37
- package/lib/server/astro/tailwindMapper.ts +33 -40
- package/lib/server/astro/templateTransformer.ts +17 -20
- package/lib/server/createServer.ts +7 -19
- package/lib/server/cssGenerator.test.ts +35 -44
- package/lib/server/cssGenerator.ts +8 -19
- package/lib/server/fileWatcher.test.ts +164 -13
- package/lib/server/fileWatcher.ts +209 -124
- package/lib/server/index.ts +14 -17
- package/lib/server/jsonLoader.test.ts +39 -2
- package/lib/server/jsonLoader.ts +39 -34
- package/lib/server/middleware/cors.test.ts +21 -21
- package/lib/server/middleware/cors.ts +28 -4
- package/lib/server/middleware/errorHandler.test.ts +7 -5
- package/lib/server/middleware/errorHandler.ts +3 -8
- package/lib/server/middleware/index.ts +0 -1
- package/lib/server/middleware/logger.test.ts +10 -8
- package/lib/server/middleware/logger.ts +17 -29
- package/lib/server/migrateTemplates.ts +3 -3
- package/lib/server/pageCache.test.ts +76 -77
- package/lib/server/pageCache.ts +0 -1
- package/lib/server/projectContext.ts +5 -4
- package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
- package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
- package/lib/server/routes/api/cms.test.ts +26 -14
- package/lib/server/routes/api/cms.ts +9 -14
- package/lib/server/routes/api/colors.test.ts +2 -2
- package/lib/server/routes/api/components.ts +31 -35
- package/lib/server/routes/api/config.ts +3 -4
- package/lib/server/routes/api/core-routes.ts +49 -76
- package/lib/server/routes/api/enums.test.ts +2 -2
- package/lib/server/routes/api/functions.ts +9 -17
- package/lib/server/routes/api/index.ts +3 -6
- package/lib/server/routes/api/pages.ts +8 -11
- package/lib/server/routes/api/shared.test.ts +1 -1
- package/lib/server/routes/api/shared.ts +12 -9
- package/lib/server/routes/api/variables.test.ts +3 -5
- package/lib/server/routes/api/variables.ts +1 -1
- package/lib/server/routes/index.ts +25 -28
- package/lib/server/routes/pages.ts +35 -31
- package/lib/server/routes/static.ts +17 -6
- package/lib/server/runtime/bundler.ts +98 -61
- package/lib/server/runtime/fs.ts +16 -26
- package/lib/server/runtime/httpServer.ts +8 -12
- package/lib/server/services/ColorService.test.ts +3 -3
- package/lib/server/services/ColorService.ts +37 -29
- package/lib/server/services/EnumService.test.ts +4 -8
- package/lib/server/services/EnumService.ts +11 -3
- package/lib/server/services/VariableService.test.ts +8 -12
- package/lib/server/services/VariableService.ts +11 -3
- package/lib/server/services/cmsService.test.ts +158 -120
- package/lib/server/services/cmsService.ts +36 -146
- package/lib/server/services/componentService.test.ts +125 -45
- package/lib/server/services/componentService.ts +70 -46
- package/lib/server/services/configService.test.ts +22 -45
- package/lib/server/services/configService.ts +50 -27
- package/lib/server/services/fileWatcherService.ts +42 -8
- package/lib/server/services/index.ts +0 -1
- package/lib/server/services/pageService.test.ts +8 -10
- package/lib/server/services/pageService.ts +32 -18
- package/lib/server/ssr/attributeBuilder.ts +18 -8
- package/lib/server/ssr/buildErrorOverlay.ts +11 -12
- package/lib/server/ssr/clientDataInjector.ts +7 -21
- package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
- package/lib/server/ssr/cssCollector.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +1 -1
- package/lib/server/ssr/errorOverlay.ts +3 -9
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
- package/lib/server/ssr/htmlGenerator.test.ts +120 -44
- package/lib/server/ssr/htmlGenerator.ts +126 -91
- package/lib/server/ssr/imageMetadata.test.ts +3 -1
- package/lib/server/ssr/imageMetadata.ts +27 -21
- package/lib/server/ssr/jsCollector.test.ts +8 -18
- package/lib/server/ssr/jsCollector.ts +3 -8
- package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
- package/lib/server/ssr/metaTagGenerator.ts +2 -2
- package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
- package/lib/server/ssr/ssrRenderer.test.ts +316 -256
- package/lib/server/ssr/ssrRenderer.ts +795 -243
- package/lib/server/ssrRenderer.test.ts +1048 -954
- package/lib/server/utils/jsonLineMapper.test.ts +28 -28
- package/lib/server/utils/jsonLineMapper.ts +7 -7
- package/lib/server/validateStyleCoverage.ts +18 -21
- package/lib/server/websocketManager.ts +28 -19
- package/lib/shared/attributeNodeUtils.test.ts +15 -15
- package/lib/shared/attributeNodeUtils.ts +5 -12
- package/lib/shared/breakpoints.test.ts +5 -5
- package/lib/shared/breakpoints.ts +4 -11
- package/lib/shared/cmsQuery.test.ts +81 -0
- package/lib/shared/cmsQuery.ts +92 -0
- package/lib/shared/cmsQueryParser.test.ts +50 -42
- package/lib/shared/cmsQueryParser.ts +60 -38
- package/lib/shared/colorConversions.ts +9 -9
- package/lib/shared/colorVariableUtils.test.ts +27 -5
- package/lib/shared/colorVariableUtils.ts +21 -12
- package/lib/shared/componentRefs.ts +1 -5
- package/lib/shared/constants.test.ts +3 -3
- package/lib/shared/constants.ts +8 -8
- package/lib/shared/cssGeneration.test.ts +285 -144
- package/lib/shared/cssGeneration.ts +217 -531
- package/lib/shared/cssNamedColors.ts +152 -30
- package/lib/shared/cssProperties.test.ts +69 -70
- package/lib/shared/cssProperties.ts +298 -114
- package/lib/shared/elementClassName.test.ts +109 -109
- package/lib/shared/elementClassName.ts +3 -3
- package/lib/shared/elementUtils.ts +12 -16
- package/lib/shared/errorLogger.test.ts +12 -12
- package/lib/shared/errorLogger.ts +2 -10
- package/lib/shared/errors.test.ts +2 -13
- package/lib/shared/errors.ts +2 -8
- package/lib/shared/expressionEvaluator.test.ts +145 -0
- package/lib/shared/expressionEvaluator.ts +52 -24
- package/lib/shared/fontLoader.test.ts +32 -18
- package/lib/shared/fontLoader.ts +2 -2
- package/lib/shared/friendlyError.ts +2 -3
- package/lib/shared/gradientUtils.test.ts +3 -7
- package/lib/shared/gradientUtils.ts +17 -14
- package/lib/shared/hrefRefs.test.ts +2 -2
- package/lib/shared/hrefRefs.ts +2 -10
- package/lib/shared/i18n.test.ts +4 -4
- package/lib/shared/i18n.ts +18 -38
- package/lib/shared/index.ts +27 -0
- package/lib/shared/inlineSvgStyleRules.ts +14 -13
- package/lib/shared/interactiveStyleMappings.test.ts +32 -54
- package/lib/shared/interactiveStyleMappings.ts +11 -24
- package/lib/shared/interactiveStyles.test.ts +165 -188
- package/lib/shared/interfaces/contentProvider.ts +1 -1
- package/lib/shared/itemTemplateUtils.test.ts +11 -17
- package/lib/shared/itemTemplateUtils.ts +27 -39
- package/lib/shared/jsonRepair.ts +16 -10
- package/lib/shared/libraryLoader.test.ts +21 -55
- package/lib/shared/libraryLoader.ts +7 -22
- package/lib/shared/linkUtils.ts +1 -1
- package/lib/shared/logger.test.ts +66 -0
- package/lib/shared/logger.ts +94 -0
- package/lib/shared/markdown.ts +25 -0
- package/lib/shared/netlifyLocale404.test.ts +179 -0
- package/lib/shared/netlifyLocale404.ts +110 -0
- package/lib/shared/nodeUtils.test.ts +28 -16
- package/lib/shared/nodeUtils.ts +76 -23
- package/lib/shared/pathArrayUtils.test.ts +1 -2
- package/lib/shared/pathArrayUtils.ts +1 -1
- package/lib/shared/pathSecurity.ts +2 -2
- package/lib/shared/pathUtils.test.ts +4 -6
- package/lib/shared/pathUtils.ts +42 -49
- package/lib/shared/paths/Path.test.ts +2 -2
- package/lib/shared/paths/Path.ts +0 -1
- package/lib/shared/paths/PathConverter.test.ts +1 -1
- package/lib/shared/paths/PathConverter.ts +17 -20
- package/lib/shared/paths/PathUtils.ts +13 -13
- package/lib/shared/paths/PathValidator.test.ts +2 -15
- package/lib/shared/paths/PathValidator.ts +12 -10
- package/lib/shared/paths/index.ts +1 -2
- package/lib/shared/permissions.test.ts +168 -0
- package/lib/shared/permissions.ts +162 -0
- package/lib/shared/propResolver.test.ts +240 -244
- package/lib/shared/propResolver.ts +15 -26
- package/lib/shared/pxToRem.test.ts +7 -6
- package/lib/shared/pxToRem.ts +2 -5
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
- package/lib/shared/registry/ClientRegistry.ts +0 -2
- package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
- package/lib/shared/registry/ComponentRegistry.ts +12 -9
- package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
- package/lib/shared/registry/RegistryManager.ts +1 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -2
- package/lib/shared/registry/createNodeType.ts +7 -9
- package/lib/shared/registry/defineNodeType.ts +2 -6
- package/lib/shared/registry/fieldPresets.ts +3 -1
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
- package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
- package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
- package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
- package/lib/shared/registry/nodeTypes/index.ts +26 -3
- package/lib/shared/responsiveScaling.test.ts +16 -34
- package/lib/shared/responsiveScaling.ts +61 -42
- package/lib/shared/responsiveStyleUtils.test.ts +0 -1
- package/lib/shared/responsiveStyleUtils.ts +11 -13
- package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
- package/lib/shared/richtext/htmlToTiptap.ts +2 -4
- package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
- package/lib/shared/richtext/types.ts +1 -8
- package/lib/shared/slugTranslator.test.ts +13 -13
- package/lib/shared/slugTranslator.ts +12 -16
- package/lib/shared/slugify.ts +9 -15
- package/lib/shared/styleNodeUtils.test.ts +8 -8
- package/lib/shared/styleNodeUtils.ts +18 -20
- package/lib/shared/styleUtils.test.ts +87 -61
- package/lib/shared/styleUtils.ts +5 -6
- package/lib/shared/themeDefaults.test.ts +11 -14
- package/lib/shared/themeDefaults.ts +3 -4
- package/lib/shared/tree/PathBuilder.test.ts +63 -145
- package/lib/shared/tree/PathBuilder.ts +59 -47
- package/lib/shared/treePathUtils.test.ts +51 -12
- package/lib/shared/treePathUtils.ts +91 -60
- package/lib/shared/types/api.ts +105 -8
- package/lib/shared/types/cms.test.ts +34 -0
- package/lib/shared/types/cms.ts +90 -34
- package/lib/shared/types/comment.ts +49 -16
- package/lib/shared/types/components.ts +51 -25
- package/lib/shared/types/errors.test.ts +1 -6
- package/lib/shared/types/errors.ts +3 -7
- package/lib/shared/types/experiments.ts +28 -28
- package/lib/shared/types/index.ts +25 -2
- package/lib/shared/types/permissions.ts +139 -0
- package/lib/shared/types/styles.ts +0 -1
- package/lib/shared/types/variables.test.ts +4 -13
- package/lib/shared/types/variables.ts +51 -30
- package/lib/shared/types.ts +1 -2
- package/lib/shared/utilityClassConfig.ts +751 -321
- package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
- package/lib/shared/utilityClassMapper.test.ts +433 -95
- package/lib/shared/utilityClassMapper.ts +322 -249
- package/lib/shared/utilityClassNames.test.ts +38 -0
- package/lib/shared/utilityClassNames.ts +458 -0
- package/lib/shared/utils.test.ts +2 -10
- package/lib/shared/utils.ts +19 -10
- package/lib/shared/validation/cmsValidators.ts +2 -1
- package/lib/shared/validation/commentValidators.test.ts +53 -0
- package/lib/shared/validation/commentValidators.ts +12 -1
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/validation/permissionsValidators.test.ts +49 -0
- package/lib/shared/validation/permissionsValidators.ts +49 -0
- package/lib/shared/validation/propValidator.test.ts +19 -21
- package/lib/shared/validation/propValidator.ts +13 -18
- package/lib/shared/validation/schemas.test.ts +64 -32
- package/lib/shared/validation/schemas.ts +616 -345
- package/lib/shared/validation/validators.test.ts +3 -8
- package/lib/shared/validation/validators.ts +89 -68
- package/lib/shared/viewportUnits.integration.test.ts +14 -10
- package/lib/shared/viewportUnits.test.ts +24 -23
- package/lib/shared/viewportUnits.ts +12 -5
- package/lib/test-utils/dom-setup.ts +1 -1
- package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
- package/lib/test-utils/factories/DomMockFactory.ts +17 -23
- package/lib/test-utils/factories/EventMockFactory.ts +7 -13
- package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
- package/lib/test-utils/factories/ServerMockFactory.ts +6 -10
- package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
- package/lib/test-utils/fixtures.ts +45 -45
- package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
- package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
- package/lib/test-utils/helpers.ts +1 -5
- package/lib/test-utils/index.ts +0 -4
- package/lib/test-utils/mockFactories.ts +13 -19
- package/lib/test-utils/mocks.ts +6 -4
- package/package.json +3 -5
- package/scripts/build-for-publish.mjs +2 -4
- package/scripts/build-meno-filter.ts +4 -7
- package/tsconfig.json +1 -0
- package/vite.config.ts +4 -4
- package/build-next.ts +0 -1361
- package/build-static.test.ts +0 -424
- package/build-static.ts +0 -1074
- package/dist/build-static.js +0 -36
- package/dist/chunks/chunk-2MHDV5BF.js +0 -251
- package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
- package/dist/chunks/chunk-AZQYF6KE.js +0 -559
- package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
- package/dist/chunks/chunk-GZHGVVW3.js +0 -322
- package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
- package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
- package/dist/chunks/chunk-I7YIGZXT.js +0 -467
- package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
- package/dist/chunks/chunk-IGYR22T6.js +0 -6192
- package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
- package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
- package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
- package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
- package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
- package/dist/chunks/chunk-QB2LNO4W.js +0 -77
- package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
- package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
- package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
- package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
- package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
- package/dist/chunks/chunk-X754AHS5.js.map +0 -7
- package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
- package/dist/chunks/configService-R3OGU2UD.js +0 -13
- package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
- package/dist/chunks/constants-STK2YBIW.js +0 -46
- package/dist/chunks/constants-STK2YBIW.js.map +0 -7
- package/dist/chunks/fs-JGINUXGL.js.map +0 -7
- package/dist/entries/server-router.js +0 -67
- package/dist/entries/server-router.js.map +0 -7
- package/entries/server-router.tsx +0 -81
- package/lib/server/providers/fileSystemPageProvider.ts +0 -160
- package/lib/server/webflow/buildWebflow.ts +0 -635
- package/lib/server/webflow/index.ts +0 -23
- package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
- package/lib/server/webflow/nodeToWebflow.ts +0 -2435
- package/lib/server/webflow/styleMapper.test.ts +0 -389
- package/lib/server/webflow/styleMapper.ts +0 -695
- package/lib/server/webflow/templateWrapper.ts +0 -49
- package/lib/server/webflow/types.ts +0 -396
- /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
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
|
+
import { UtilityClassCollector } from '../styles/UtilityClassCollector';
|
|
8
9
|
|
|
9
10
|
// Note: Using typed mocks from test-utils/mocks instead of inline 'as any' casts
|
|
10
11
|
|
|
11
|
-
describe(
|
|
12
|
+
describe('ComponentBuilder', () => {
|
|
12
13
|
let componentRegistry: ComponentRegistry;
|
|
13
14
|
let elementRegistry: ElementRegistry;
|
|
14
15
|
let builder: ComponentBuilder;
|
|
@@ -32,52 +33,52 @@ describe("ComponentBuilder", () => {
|
|
|
32
33
|
});
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
describe(
|
|
36
|
-
test(
|
|
36
|
+
describe('buildComponent - Basic Cases', () => {
|
|
37
|
+
test('should return null for null node', () => {
|
|
37
38
|
const result = builder.buildComponent({ node: null });
|
|
38
39
|
expect(result).toBeNull();
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
test(
|
|
42
|
+
test('should return null for undefined node', () => {
|
|
42
43
|
const result = builder.buildComponent({ node: undefined });
|
|
43
44
|
expect(result).toBeNull();
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
test(
|
|
47
|
-
const result = builder.buildComponent({ node:
|
|
48
|
-
expect(result).toBe(
|
|
47
|
+
test('should return string for string node', () => {
|
|
48
|
+
const result = builder.buildComponent({ node: 'Hello' });
|
|
49
|
+
expect(result).toBe('Hello');
|
|
49
50
|
});
|
|
50
51
|
|
|
51
|
-
test(
|
|
52
|
+
test('should return number for number node', () => {
|
|
52
53
|
const result = builder.buildComponent({ node: 42 });
|
|
53
54
|
expect(result).toBe(42);
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
test(
|
|
57
|
+
test('should return array for array of nodes', () => {
|
|
57
58
|
const nodes: ComponentNode[] = [
|
|
58
|
-
{ type:
|
|
59
|
-
{ type:
|
|
59
|
+
{ type: 'node', tag: 'div', children: ['Hello'] },
|
|
60
|
+
{ type: 'node', tag: 'span', children: ['World'] },
|
|
60
61
|
];
|
|
61
62
|
const result = builder.buildComponent({ node: nodes });
|
|
62
63
|
expect(Array.isArray(result)).toBe(true);
|
|
63
64
|
expect((result as any[]).length).toBe(2);
|
|
64
65
|
});
|
|
65
66
|
|
|
66
|
-
test(
|
|
67
|
+
test('should return ReactElement for HTML node', () => {
|
|
67
68
|
const node: ComponentNode = {
|
|
68
|
-
type:
|
|
69
|
-
tag:
|
|
70
|
-
children: [
|
|
69
|
+
type: 'node',
|
|
70
|
+
tag: 'div',
|
|
71
|
+
children: ['Test'],
|
|
71
72
|
};
|
|
72
73
|
const result = builder.buildComponent({ node });
|
|
73
74
|
expect(result).not.toBeNull();
|
|
74
|
-
expect(typeof result).toBe(
|
|
75
|
+
expect(typeof result).toBe('object');
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
test(
|
|
78
|
+
test('should handle HTML node with empty tag gracefully', () => {
|
|
78
79
|
const node: ComponentNode = {
|
|
79
|
-
type:
|
|
80
|
-
tag:
|
|
80
|
+
type: 'node',
|
|
81
|
+
tag: '',
|
|
81
82
|
children: [],
|
|
82
83
|
};
|
|
83
84
|
|
|
@@ -85,124 +86,124 @@ describe("ComponentBuilder", () => {
|
|
|
85
86
|
|
|
86
87
|
// Implementation returns a div element for empty tag (graceful fallback)
|
|
87
88
|
expect(result).not.toBeNull();
|
|
88
|
-
expect(typeof result).toBe(
|
|
89
|
+
expect(typeof result).toBe('object');
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
test(
|
|
92
|
+
test('resolves an _i18n value object as the node to the default locale string', () => {
|
|
92
93
|
// Mirrors the SSR behavior: authors can pass localized strings anywhere
|
|
93
94
|
// `children` is accepted. The client resolves _i18n at the buildComponent
|
|
94
95
|
// entry point so all downstream code sees a plain string.
|
|
95
96
|
const i18nConfig = {
|
|
96
|
-
defaultLocale:
|
|
97
|
+
defaultLocale: 'en',
|
|
97
98
|
locales: [
|
|
98
|
-
{ code:
|
|
99
|
-
{ code:
|
|
99
|
+
{ code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
|
|
100
|
+
{ code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
|
|
100
101
|
],
|
|
101
102
|
};
|
|
102
103
|
const result = builder.buildComponent({
|
|
103
|
-
node: { _i18n: true, en:
|
|
104
|
+
node: { _i18n: true, en: 'Hello', pl: 'Cześć' } as any,
|
|
104
105
|
i18nConfig,
|
|
105
106
|
});
|
|
106
|
-
expect(result).toBe(
|
|
107
|
+
expect(result).toBe('Hello');
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
test(
|
|
110
|
+
test('resolves an _i18n value object to the active locale', () => {
|
|
110
111
|
const i18nConfig = {
|
|
111
|
-
defaultLocale:
|
|
112
|
+
defaultLocale: 'en',
|
|
112
113
|
locales: [
|
|
113
|
-
{ code:
|
|
114
|
-
{ code:
|
|
114
|
+
{ code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
|
|
115
|
+
{ code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
|
|
115
116
|
],
|
|
116
117
|
};
|
|
117
118
|
const result = builder.buildComponent({
|
|
118
|
-
node: { _i18n: true, en:
|
|
119
|
+
node: { _i18n: true, en: 'Hello', pl: 'Cześć' } as any,
|
|
119
120
|
i18nConfig,
|
|
120
|
-
locale:
|
|
121
|
+
locale: 'pl',
|
|
121
122
|
});
|
|
122
|
-
expect(result).toBe(
|
|
123
|
+
expect(result).toBe('Cześć');
|
|
123
124
|
});
|
|
124
125
|
});
|
|
125
126
|
|
|
126
|
-
describe(
|
|
127
|
-
test(
|
|
127
|
+
describe('buildComponent - Component Instances', () => {
|
|
128
|
+
test('should handle non-existent component gracefully', () => {
|
|
128
129
|
const node: ComponentNode = {
|
|
129
|
-
type:
|
|
130
|
-
component:
|
|
130
|
+
type: 'component',
|
|
131
|
+
component: 'NonExistent',
|
|
131
132
|
};
|
|
132
133
|
const result = builder.buildComponent({ node });
|
|
133
134
|
// Returns error boundary or placeholder element for missing component
|
|
134
135
|
expect(result).not.toBeNull();
|
|
135
|
-
expect(typeof result).toBe(
|
|
136
|
+
expect(typeof result).toBe('object');
|
|
136
137
|
});
|
|
137
138
|
|
|
138
|
-
test(
|
|
139
|
+
test('should build component instance with structure', () => {
|
|
139
140
|
const componentDef = {
|
|
140
141
|
component: {
|
|
141
142
|
interface: {},
|
|
142
143
|
structure: {
|
|
143
|
-
type:
|
|
144
|
-
tag:
|
|
145
|
-
children: [
|
|
144
|
+
type: 'node',
|
|
145
|
+
tag: 'div',
|
|
146
|
+
children: ['Component Content'],
|
|
146
147
|
},
|
|
147
148
|
},
|
|
148
149
|
};
|
|
149
|
-
componentRegistry.register(
|
|
150
|
+
componentRegistry.register('TestComponent', componentDef as any);
|
|
150
151
|
|
|
151
152
|
const node: ComponentNode = {
|
|
152
|
-
type:
|
|
153
|
-
component:
|
|
153
|
+
type: 'component',
|
|
154
|
+
component: 'TestComponent',
|
|
154
155
|
};
|
|
155
156
|
const result = builder.buildComponent({ node });
|
|
156
157
|
expect(result).not.toBeNull();
|
|
157
|
-
expect(typeof result).toBe(
|
|
158
|
+
expect(typeof result).toBe('object');
|
|
158
159
|
});
|
|
159
160
|
|
|
160
|
-
test(
|
|
161
|
+
test('should handle component missing structure gracefully', () => {
|
|
161
162
|
// Test with component that has no component property at all
|
|
162
163
|
const componentDef = {
|
|
163
164
|
// Missing component property entirely
|
|
164
165
|
};
|
|
165
|
-
componentRegistry.register(
|
|
166
|
+
componentRegistry.register('BrokenComponent', componentDef as any);
|
|
166
167
|
|
|
167
168
|
const node: ComponentNode = {
|
|
168
|
-
type:
|
|
169
|
-
component:
|
|
169
|
+
type: 'component',
|
|
170
|
+
component: 'BrokenComponent',
|
|
170
171
|
};
|
|
171
172
|
|
|
172
173
|
const result = builder.buildComponent({ node });
|
|
173
174
|
|
|
174
175
|
// When componentDef.component is missing, it should return ErrorBoundary
|
|
175
176
|
expect(result).not.toBeNull();
|
|
176
|
-
expect(typeof result).toBe(
|
|
177
|
+
expect(typeof result).toBe('object');
|
|
177
178
|
});
|
|
178
179
|
|
|
179
|
-
test(
|
|
180
|
+
test('should resolve props with defaults from interface', () => {
|
|
180
181
|
const componentDef = {
|
|
181
182
|
component: {
|
|
182
183
|
interface: {
|
|
183
184
|
title: {
|
|
184
|
-
type:
|
|
185
|
-
default:
|
|
185
|
+
type: 'string',
|
|
186
|
+
default: 'Default Title',
|
|
186
187
|
},
|
|
187
188
|
},
|
|
188
189
|
structure: {
|
|
189
|
-
type:
|
|
190
|
-
tag:
|
|
190
|
+
type: 'node',
|
|
191
|
+
tag: 'div',
|
|
191
192
|
children: [],
|
|
192
193
|
},
|
|
193
194
|
},
|
|
194
195
|
};
|
|
195
|
-
componentRegistry.register(
|
|
196
|
+
componentRegistry.register('ComponentWithDefaults', componentDef as any);
|
|
196
197
|
|
|
197
198
|
const node: ComponentNode = {
|
|
198
|
-
type:
|
|
199
|
-
component:
|
|
199
|
+
type: 'component',
|
|
200
|
+
component: 'ComponentWithDefaults',
|
|
200
201
|
};
|
|
201
202
|
const result = builder.buildComponent({ node });
|
|
202
203
|
expect(result).not.toBeNull();
|
|
203
204
|
});
|
|
204
205
|
|
|
205
|
-
test(
|
|
206
|
+
test('should handle component building errors gracefully', () => {
|
|
206
207
|
// This test verifies error handling is in place
|
|
207
208
|
// We can't easily mock processStructure in Bun, so we test with a valid component
|
|
208
209
|
// Error handling is verified by the try-catch in buildComponent
|
|
@@ -210,65 +211,65 @@ describe("ComponentBuilder", () => {
|
|
|
210
211
|
component: {
|
|
211
212
|
interface: {},
|
|
212
213
|
structure: {
|
|
213
|
-
type:
|
|
214
|
-
tag:
|
|
214
|
+
type: 'node',
|
|
215
|
+
tag: 'div',
|
|
215
216
|
children: [],
|
|
216
217
|
},
|
|
217
218
|
},
|
|
218
219
|
};
|
|
219
|
-
componentRegistry.register(
|
|
220
|
+
componentRegistry.register('ValidComponent', componentDef as any);
|
|
220
221
|
|
|
221
222
|
const node: ComponentNode = {
|
|
222
|
-
type:
|
|
223
|
-
component:
|
|
223
|
+
type: 'component',
|
|
224
|
+
component: 'ValidComponent',
|
|
224
225
|
};
|
|
225
|
-
|
|
226
|
+
|
|
226
227
|
const result = builder.buildComponent({ node });
|
|
227
|
-
|
|
228
|
+
|
|
228
229
|
// Component should build successfully
|
|
229
230
|
expect(result).not.toBeNull();
|
|
230
|
-
expect(typeof result).toBe(
|
|
231
|
+
expect(typeof result).toBe('object');
|
|
231
232
|
});
|
|
232
233
|
});
|
|
233
234
|
|
|
234
|
-
describe(
|
|
235
|
-
test(
|
|
235
|
+
describe('buildComponent - Link Component', () => {
|
|
236
|
+
test('should render Link component as anchor tag', () => {
|
|
236
237
|
const node: ComponentNode = {
|
|
237
|
-
type:
|
|
238
|
-
tag:
|
|
238
|
+
type: 'node',
|
|
239
|
+
tag: 'Link',
|
|
239
240
|
props: {
|
|
240
|
-
to:
|
|
241
|
+
to: '/about',
|
|
241
242
|
},
|
|
242
|
-
children: [
|
|
243
|
+
children: ['Click me'],
|
|
243
244
|
};
|
|
244
245
|
const result = builder.buildComponent({ node });
|
|
245
246
|
expect(result).not.toBeNull();
|
|
246
|
-
expect(typeof result).toBe(
|
|
247
|
+
expect(typeof result).toBe('object');
|
|
247
248
|
});
|
|
248
249
|
|
|
249
|
-
test(
|
|
250
|
+
test('should handle Link navigation when selecting mode is disabled', () => {
|
|
250
251
|
const node: ComponentNode = {
|
|
251
|
-
type:
|
|
252
|
-
tag:
|
|
252
|
+
type: 'node',
|
|
253
|
+
tag: 'Link',
|
|
253
254
|
props: {
|
|
254
|
-
to:
|
|
255
|
+
to: '/test',
|
|
255
256
|
},
|
|
256
257
|
children: [],
|
|
257
258
|
};
|
|
258
|
-
|
|
259
|
+
|
|
259
260
|
// Link component should render regardless of selecting mode
|
|
260
261
|
// The actual behavior is tested through integration tests
|
|
261
262
|
const result = builder.buildComponent({ node });
|
|
262
263
|
expect(result).not.toBeNull();
|
|
263
|
-
expect(typeof result).toBe(
|
|
264
|
+
expect(typeof result).toBe('object');
|
|
264
265
|
});
|
|
265
266
|
});
|
|
266
267
|
|
|
267
|
-
describe(
|
|
268
|
-
test(
|
|
268
|
+
describe('buildComponent - HTML Elements', () => {
|
|
269
|
+
test('should register element via ref callback', () => {
|
|
269
270
|
const node: ComponentNode = {
|
|
270
|
-
type:
|
|
271
|
-
tag:
|
|
271
|
+
type: 'node',
|
|
272
|
+
tag: 'div',
|
|
272
273
|
children: [],
|
|
273
274
|
};
|
|
274
275
|
const result = builder.buildComponent({ node });
|
|
@@ -277,14 +278,14 @@ describe("ComponentBuilder", () => {
|
|
|
277
278
|
// We can't easily test this without rendering, but we verify the ref is set
|
|
278
279
|
});
|
|
279
280
|
|
|
280
|
-
test(
|
|
281
|
+
test('should extract and merge styles', () => {
|
|
281
282
|
const node: ComponentNode = {
|
|
282
|
-
type:
|
|
283
|
-
tag:
|
|
283
|
+
type: 'node',
|
|
284
|
+
tag: 'div',
|
|
284
285
|
style: {
|
|
285
286
|
base: {
|
|
286
|
-
color:
|
|
287
|
-
fontSize:
|
|
287
|
+
color: 'red',
|
|
288
|
+
fontSize: '16px',
|
|
288
289
|
},
|
|
289
290
|
},
|
|
290
291
|
children: [],
|
|
@@ -293,13 +294,13 @@ describe("ComponentBuilder", () => {
|
|
|
293
294
|
expect(result).not.toBeNull();
|
|
294
295
|
});
|
|
295
296
|
|
|
296
|
-
test(
|
|
297
|
+
test('should extract and merge attributes', () => {
|
|
297
298
|
const node: ComponentNode = {
|
|
298
|
-
type:
|
|
299
|
-
tag:
|
|
299
|
+
type: 'node',
|
|
300
|
+
tag: 'div',
|
|
300
301
|
attributes: {
|
|
301
|
-
id:
|
|
302
|
-
className:
|
|
302
|
+
id: 'test-id',
|
|
303
|
+
className: 'test-class',
|
|
303
304
|
},
|
|
304
305
|
children: [],
|
|
305
306
|
};
|
|
@@ -307,16 +308,38 @@ describe("ComponentBuilder", () => {
|
|
|
307
308
|
expect(result).not.toBeNull();
|
|
308
309
|
});
|
|
309
310
|
|
|
310
|
-
test(
|
|
311
|
+
test('class-string styling: utility tokens in attributes.class inject canvas CSS', () => {
|
|
312
|
+
// The design canvas builds React elements via ComponentBuilder. A node styled via
|
|
313
|
+
// attributes.class (not node.style) must still get its utility CSS injected into the
|
|
314
|
+
// live <style id="utility-css">, exactly like the node.style path — else the class
|
|
315
|
+
// lands on the element but renders unstyled.
|
|
316
|
+
UtilityClassCollector.destroy();
|
|
311
317
|
const node: ComponentNode = {
|
|
312
|
-
type:
|
|
313
|
-
tag:
|
|
318
|
+
type: 'node',
|
|
319
|
+
tag: 'p',
|
|
320
|
+
attributes: { class: 'p-[24px] swiper' },
|
|
321
|
+
children: ['styled'],
|
|
322
|
+
};
|
|
323
|
+
const result = builder.buildComponent({ node });
|
|
324
|
+
expect(result).not.toBeNull();
|
|
325
|
+
const css = document.getElementById('utility-css')?.textContent || '';
|
|
326
|
+
// Utility token p-[24px] generated a rule...
|
|
327
|
+
expect(css).toContain('padding: 24px');
|
|
328
|
+
// ...and the foreign token (swiper) produced none (collect() skips no-rule tokens).
|
|
329
|
+
expect(css).not.toContain('swiper');
|
|
330
|
+
UtilityClassCollector.destroy();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('should filter out internal props', () => {
|
|
334
|
+
const node: ComponentNode = {
|
|
335
|
+
type: 'node',
|
|
336
|
+
tag: 'div',
|
|
314
337
|
props: {
|
|
315
|
-
type:
|
|
316
|
-
tag:
|
|
317
|
-
component:
|
|
318
|
-
children:
|
|
319
|
-
validProp:
|
|
338
|
+
type: 'should-be-removed',
|
|
339
|
+
tag: 'should-be-removed',
|
|
340
|
+
component: 'should-be-removed',
|
|
341
|
+
children: 'should-be-removed',
|
|
342
|
+
validProp: 'should-remain',
|
|
320
343
|
},
|
|
321
344
|
children: [],
|
|
322
345
|
};
|
|
@@ -324,44 +347,44 @@ describe("ComponentBuilder", () => {
|
|
|
324
347
|
expect(result).not.toBeNull();
|
|
325
348
|
});
|
|
326
349
|
|
|
327
|
-
test(
|
|
350
|
+
test('should detect component root via __componentProps', () => {
|
|
328
351
|
const node: ComponentNode = {
|
|
329
|
-
type:
|
|
330
|
-
tag:
|
|
352
|
+
type: 'node',
|
|
353
|
+
tag: 'div',
|
|
331
354
|
children: [],
|
|
332
355
|
};
|
|
333
356
|
const result = builder.buildComponent({
|
|
334
357
|
node,
|
|
335
358
|
customProps: {
|
|
336
|
-
__componentProps: { title:
|
|
359
|
+
__componentProps: { title: 'Test' },
|
|
337
360
|
},
|
|
338
361
|
});
|
|
339
362
|
expect(result).not.toBeNull();
|
|
340
363
|
});
|
|
341
364
|
|
|
342
|
-
test(
|
|
365
|
+
test('should remap body tag to div to keep SSR and client DOMs identical', () => {
|
|
343
366
|
const node: ComponentNode = {
|
|
344
|
-
type:
|
|
345
|
-
tag:
|
|
346
|
-
attributes: { className:
|
|
347
|
-
children: [
|
|
367
|
+
type: 'node',
|
|
368
|
+
tag: 'body',
|
|
369
|
+
attributes: { className: 'page-root' },
|
|
370
|
+
children: ['Content'],
|
|
348
371
|
};
|
|
349
372
|
const result = builder.buildComponent({ node }) as any;
|
|
350
373
|
expect(result).not.toBeNull();
|
|
351
|
-
expect(result.type).toBe(
|
|
352
|
-
expect(result.props.className).toBe(
|
|
374
|
+
expect(result.type).toBe('div');
|
|
375
|
+
expect(result.props.className).toBe('page-root');
|
|
353
376
|
});
|
|
354
377
|
|
|
355
|
-
test(
|
|
356
|
-
for (const tag of [
|
|
357
|
-
const node: ComponentNode = { type:
|
|
378
|
+
test('should remap html and head tags to div', () => {
|
|
379
|
+
for (const tag of ['html', 'head', 'BODY'] as const) {
|
|
380
|
+
const node: ComponentNode = { type: 'node', tag, children: [] };
|
|
358
381
|
const result = builder.buildComponent({ node }) as any;
|
|
359
|
-
expect(result.type).toBe(
|
|
382
|
+
expect(result.type).toBe('div');
|
|
360
383
|
}
|
|
361
384
|
});
|
|
362
385
|
});
|
|
363
386
|
|
|
364
|
-
describe(
|
|
387
|
+
describe('buildChildren', () => {
|
|
365
388
|
const defaultCtx = {
|
|
366
389
|
elementPath: [0] as number[],
|
|
367
390
|
parentComponentName: null,
|
|
@@ -369,42 +392,42 @@ describe("ComponentBuilder", () => {
|
|
|
369
392
|
componentContext: null,
|
|
370
393
|
};
|
|
371
394
|
|
|
372
|
-
test(
|
|
395
|
+
test('should return array for array of children', () => {
|
|
373
396
|
const children: ComponentNode[] = [
|
|
374
|
-
{ type:
|
|
375
|
-
{ type:
|
|
397
|
+
{ type: 'node', tag: 'div', children: ['Child 1'] },
|
|
398
|
+
{ type: 'node', tag: 'span', children: ['Child 2'] },
|
|
376
399
|
];
|
|
377
400
|
const result = builder.buildChildren(children, defaultCtx);
|
|
378
401
|
expect(Array.isArray(result)).toBe(true);
|
|
379
402
|
expect((result as any[]).length).toBe(2);
|
|
380
403
|
});
|
|
381
404
|
|
|
382
|
-
test(
|
|
405
|
+
test('should return single element for single child', () => {
|
|
383
406
|
const child: ComponentNode = {
|
|
384
|
-
type:
|
|
385
|
-
tag:
|
|
386
|
-
children: [
|
|
407
|
+
type: 'node',
|
|
408
|
+
tag: 'div',
|
|
409
|
+
children: ['Single child'],
|
|
387
410
|
};
|
|
388
411
|
const result = builder.buildChildren(child, defaultCtx);
|
|
389
412
|
expect(result).not.toBeNull();
|
|
390
413
|
expect(Array.isArray(result)).toBe(false);
|
|
391
414
|
});
|
|
392
415
|
|
|
393
|
-
test(
|
|
416
|
+
test('should return null for undefined children', () => {
|
|
394
417
|
const result = builder.buildChildren(undefined, defaultCtx);
|
|
395
418
|
expect(result).toBeNull();
|
|
396
419
|
});
|
|
397
420
|
|
|
398
|
-
test(
|
|
421
|
+
test('should return empty array for empty array', () => {
|
|
399
422
|
const result = builder.buildChildren([], defaultCtx);
|
|
400
423
|
expect(Array.isArray(result)).toBe(true);
|
|
401
424
|
expect((result as any[]).length).toBe(0);
|
|
402
425
|
});
|
|
403
426
|
|
|
404
|
-
test(
|
|
427
|
+
test('should generate correct paths for children', () => {
|
|
405
428
|
const children: ComponentNode[] = [
|
|
406
|
-
{ type:
|
|
407
|
-
{ type:
|
|
429
|
+
{ type: 'node', tag: 'div', children: [] },
|
|
430
|
+
{ type: 'node', tag: 'span', children: [] },
|
|
408
431
|
];
|
|
409
432
|
const result = builder.buildChildren(children, { ...defaultCtx, elementPath: [0, 1] });
|
|
410
433
|
expect(Array.isArray(result)).toBe(true);
|
|
@@ -412,51 +435,51 @@ describe("ComponentBuilder", () => {
|
|
|
412
435
|
});
|
|
413
436
|
});
|
|
414
437
|
|
|
415
|
-
describe(
|
|
416
|
-
test(
|
|
438
|
+
describe('Helper Functions', () => {
|
|
439
|
+
test('getParentComponentNameForNestedComponent should prioritize componentContext', () => {
|
|
417
440
|
// This is tested indirectly through component building
|
|
418
441
|
// We can't directly test private methods, but we can verify behavior
|
|
419
442
|
const componentDef = {
|
|
420
443
|
component: {
|
|
421
444
|
interface: {},
|
|
422
445
|
structure: {
|
|
423
|
-
type:
|
|
424
|
-
tag:
|
|
446
|
+
type: 'node',
|
|
447
|
+
tag: 'div',
|
|
425
448
|
children: [],
|
|
426
449
|
},
|
|
427
450
|
},
|
|
428
451
|
};
|
|
429
|
-
componentRegistry.register(
|
|
452
|
+
componentRegistry.register('ParentComponent', componentDef as any);
|
|
430
453
|
|
|
431
454
|
const node: ComponentNode = {
|
|
432
|
-
type:
|
|
433
|
-
component:
|
|
455
|
+
type: 'component',
|
|
456
|
+
component: 'ParentComponent',
|
|
434
457
|
};
|
|
435
458
|
const result = builder.buildComponent({
|
|
436
459
|
node,
|
|
437
|
-
componentContext:
|
|
438
|
-
parentComponentName:
|
|
460
|
+
componentContext: 'ContextComponent',
|
|
461
|
+
parentComponentName: 'ParentName',
|
|
439
462
|
});
|
|
440
463
|
expect(result).not.toBeNull();
|
|
441
464
|
});
|
|
442
465
|
|
|
443
|
-
test(
|
|
466
|
+
test('getEffectiveParentComponentName should use componentContext when available', () => {
|
|
444
467
|
// Tested indirectly through component building
|
|
445
468
|
const node: ComponentNode = {
|
|
446
|
-
type:
|
|
447
|
-
tag:
|
|
469
|
+
type: 'node',
|
|
470
|
+
tag: 'div',
|
|
448
471
|
children: [],
|
|
449
472
|
};
|
|
450
473
|
const result = builder.buildComponent({
|
|
451
474
|
node,
|
|
452
|
-
componentContext:
|
|
453
|
-
parentComponentName:
|
|
475
|
+
componentContext: 'ContextComponent',
|
|
476
|
+
parentComponentName: 'ParentName',
|
|
454
477
|
});
|
|
455
478
|
expect(result).not.toBeNull();
|
|
456
479
|
});
|
|
457
480
|
});
|
|
458
481
|
|
|
459
|
-
describe(
|
|
482
|
+
describe('Path Assignment for Slot Markers', () => {
|
|
460
483
|
/**
|
|
461
484
|
* KNOWN ISSUE: Slot markers consume path indices incorrectly
|
|
462
485
|
*
|
|
@@ -474,21 +497,18 @@ describe("ComponentBuilder", () => {
|
|
|
474
497
|
* TODO: Fix path assignment to skip slot markers when assigning indices,
|
|
475
498
|
* or adjust indices after slot expansion.
|
|
476
499
|
*/
|
|
477
|
-
test.skip(
|
|
500
|
+
test.skip('slot markers should not consume path indices', () => {
|
|
478
501
|
const sectionDef = {
|
|
479
502
|
component: {
|
|
480
503
|
interface: {},
|
|
481
504
|
structure: {
|
|
482
|
-
type:
|
|
483
|
-
tag:
|
|
505
|
+
type: 'node',
|
|
506
|
+
tag: 'div',
|
|
484
507
|
children: [
|
|
485
508
|
{
|
|
486
|
-
type:
|
|
487
|
-
tag:
|
|
488
|
-
children: [
|
|
489
|
-
{ type: "slot" },
|
|
490
|
-
{ type: "component", component: "Card" },
|
|
491
|
-
],
|
|
509
|
+
type: 'node',
|
|
510
|
+
tag: 'div',
|
|
511
|
+
children: [{ type: 'slot' }, { type: 'component', component: 'Card' }],
|
|
492
512
|
},
|
|
493
513
|
],
|
|
494
514
|
},
|
|
@@ -499,20 +519,20 @@ describe("ComponentBuilder", () => {
|
|
|
499
519
|
component: {
|
|
500
520
|
interface: {},
|
|
501
521
|
structure: {
|
|
502
|
-
type:
|
|
503
|
-
tag:
|
|
504
|
-
children: [
|
|
522
|
+
type: 'node',
|
|
523
|
+
tag: 'div',
|
|
524
|
+
children: ['Card Content'],
|
|
505
525
|
},
|
|
506
526
|
},
|
|
507
527
|
};
|
|
508
528
|
|
|
509
|
-
componentRegistry.register(
|
|
510
|
-
componentRegistry.register(
|
|
529
|
+
componentRegistry.register('Section', sectionDef as any);
|
|
530
|
+
componentRegistry.register('Card', cardDef as any);
|
|
511
531
|
|
|
512
532
|
const pageNode: ComponentNode = {
|
|
513
|
-
type:
|
|
514
|
-
component:
|
|
515
|
-
children: [{ type:
|
|
533
|
+
type: 'component',
|
|
534
|
+
component: 'Section',
|
|
535
|
+
children: [{ type: 'node', tag: 'p', children: ['Instance Child'] }],
|
|
516
536
|
};
|
|
517
537
|
|
|
518
538
|
const result = builder.buildComponent({ node: pageNode });
|
|
@@ -524,51 +544,51 @@ describe("ComponentBuilder", () => {
|
|
|
524
544
|
// OR Card gets a path that properly reflects its position relative to expanded slot content
|
|
525
545
|
});
|
|
526
546
|
|
|
527
|
-
test(
|
|
547
|
+
test('should build component with slot marker correctly', () => {
|
|
528
548
|
// Button.json: button > children
|
|
529
549
|
const buttonDef = {
|
|
530
550
|
component: {
|
|
531
551
|
interface: {
|
|
532
|
-
children: { type:
|
|
552
|
+
children: { type: 'string', default: 'Click me' },
|
|
533
553
|
},
|
|
534
554
|
structure: {
|
|
535
|
-
type:
|
|
536
|
-
tag:
|
|
537
|
-
children: [{ type:
|
|
555
|
+
type: 'node',
|
|
556
|
+
tag: 'button',
|
|
557
|
+
children: [{ type: 'slot' }],
|
|
538
558
|
},
|
|
539
559
|
},
|
|
540
560
|
};
|
|
541
561
|
|
|
542
|
-
componentRegistry.register(
|
|
562
|
+
componentRegistry.register('Button', buttonDef as any);
|
|
543
563
|
|
|
544
564
|
const pageNode: ComponentNode = {
|
|
545
|
-
type:
|
|
546
|
-
component:
|
|
547
|
-
children: [{ type:
|
|
565
|
+
type: 'component',
|
|
566
|
+
component: 'Button',
|
|
567
|
+
children: [{ type: 'node', tag: 'span', children: ['Submit'] }],
|
|
548
568
|
};
|
|
549
569
|
|
|
550
570
|
const result = builder.buildComponent({ node: pageNode });
|
|
551
571
|
expect(result).not.toBeNull();
|
|
552
|
-
expect(typeof result).toBe(
|
|
572
|
+
expect(typeof result).toBe('object');
|
|
553
573
|
});
|
|
554
574
|
|
|
555
|
-
test(
|
|
575
|
+
test('should build nested children with multiple siblings after slot', () => {
|
|
556
576
|
// Structure: div > div > [slot, Card, Button]
|
|
557
577
|
|
|
558
578
|
const sectionDef = {
|
|
559
579
|
component: {
|
|
560
580
|
interface: {},
|
|
561
581
|
structure: {
|
|
562
|
-
type:
|
|
563
|
-
tag:
|
|
582
|
+
type: 'node',
|
|
583
|
+
tag: 'div',
|
|
564
584
|
children: [
|
|
565
585
|
{
|
|
566
|
-
type:
|
|
567
|
-
tag:
|
|
586
|
+
type: 'node',
|
|
587
|
+
tag: 'div',
|
|
568
588
|
children: [
|
|
569
|
-
{ type:
|
|
570
|
-
{ type:
|
|
571
|
-
{ type:
|
|
589
|
+
{ type: 'slot' },
|
|
590
|
+
{ type: 'component', component: 'Card' },
|
|
591
|
+
{ type: 'component', component: 'Button' },
|
|
572
592
|
],
|
|
573
593
|
},
|
|
574
594
|
],
|
|
@@ -580,9 +600,9 @@ describe("ComponentBuilder", () => {
|
|
|
580
600
|
component: {
|
|
581
601
|
interface: {},
|
|
582
602
|
structure: {
|
|
583
|
-
type:
|
|
584
|
-
tag:
|
|
585
|
-
children: [
|
|
603
|
+
type: 'node',
|
|
604
|
+
tag: 'div',
|
|
605
|
+
children: ['Card'],
|
|
586
606
|
},
|
|
587
607
|
},
|
|
588
608
|
};
|
|
@@ -591,57 +611,54 @@ describe("ComponentBuilder", () => {
|
|
|
591
611
|
component: {
|
|
592
612
|
interface: {},
|
|
593
613
|
structure: {
|
|
594
|
-
type:
|
|
595
|
-
tag:
|
|
596
|
-
children: [
|
|
614
|
+
type: 'node',
|
|
615
|
+
tag: 'button',
|
|
616
|
+
children: ['Button'],
|
|
597
617
|
},
|
|
598
618
|
},
|
|
599
619
|
};
|
|
600
620
|
|
|
601
|
-
componentRegistry.register(
|
|
602
|
-
componentRegistry.register(
|
|
603
|
-
componentRegistry.register(
|
|
621
|
+
componentRegistry.register('Section', sectionDef as any);
|
|
622
|
+
componentRegistry.register('Card', cardDef as any);
|
|
623
|
+
componentRegistry.register('Button', buttonDef as any);
|
|
604
624
|
|
|
605
625
|
const pageNode: ComponentNode = {
|
|
606
|
-
type:
|
|
607
|
-
component:
|
|
608
|
-
children: [{ type:
|
|
626
|
+
type: 'component',
|
|
627
|
+
component: 'Section',
|
|
628
|
+
children: [{ type: 'node', tag: 'p', children: ['Content'] }],
|
|
609
629
|
};
|
|
610
630
|
|
|
611
631
|
const result = builder.buildComponent({ node: pageNode });
|
|
612
632
|
expect(result).not.toBeNull();
|
|
613
|
-
expect(typeof result).toBe(
|
|
633
|
+
expect(typeof result).toBe('object');
|
|
614
634
|
});
|
|
615
635
|
});
|
|
616
636
|
|
|
617
|
-
describe(
|
|
618
|
-
test(
|
|
637
|
+
describe('Integration Tests', () => {
|
|
638
|
+
test('should build complex component tree', () => {
|
|
619
639
|
const cardDef = {
|
|
620
640
|
component: {
|
|
621
641
|
interface: {
|
|
622
|
-
title: { type:
|
|
642
|
+
title: { type: 'string', default: 'Card Title' },
|
|
623
643
|
},
|
|
624
644
|
structure: {
|
|
625
|
-
type:
|
|
626
|
-
tag:
|
|
627
|
-
children: [
|
|
628
|
-
{ type: "node", tag: "h2", children: ["{{title}}"] },
|
|
629
|
-
{ type: "slot" },
|
|
630
|
-
],
|
|
645
|
+
type: 'node',
|
|
646
|
+
tag: 'div',
|
|
647
|
+
children: [{ type: 'node', tag: 'h2', children: ['{{title}}'] }, { type: 'slot' }],
|
|
631
648
|
},
|
|
632
649
|
},
|
|
633
650
|
};
|
|
634
|
-
componentRegistry.register(
|
|
651
|
+
componentRegistry.register('Card', cardDef as any);
|
|
635
652
|
|
|
636
653
|
const pageNode: ComponentNode = {
|
|
637
|
-
type:
|
|
638
|
-
tag:
|
|
654
|
+
type: 'node',
|
|
655
|
+
tag: 'div',
|
|
639
656
|
children: [
|
|
640
657
|
{
|
|
641
|
-
type:
|
|
642
|
-
component:
|
|
643
|
-
props: { title:
|
|
644
|
-
children: [{ type:
|
|
658
|
+
type: 'component',
|
|
659
|
+
component: 'Card',
|
|
660
|
+
props: { title: 'My Card' },
|
|
661
|
+
children: [{ type: 'node', tag: 'p', children: ['Card content'] }],
|
|
645
662
|
},
|
|
646
663
|
],
|
|
647
664
|
};
|
|
@@ -649,90 +666,86 @@ describe("ComponentBuilder", () => {
|
|
|
649
666
|
expect(result).not.toBeNull();
|
|
650
667
|
});
|
|
651
668
|
|
|
652
|
-
test(
|
|
669
|
+
test('should handle nested component instances', () => {
|
|
653
670
|
const innerDef = {
|
|
654
671
|
component: {
|
|
655
672
|
interface: {},
|
|
656
673
|
structure: {
|
|
657
|
-
type:
|
|
658
|
-
tag:
|
|
659
|
-
children: [
|
|
674
|
+
type: 'node',
|
|
675
|
+
tag: 'span',
|
|
676
|
+
children: ['Inner'],
|
|
660
677
|
},
|
|
661
678
|
},
|
|
662
679
|
};
|
|
663
|
-
componentRegistry.register(
|
|
680
|
+
componentRegistry.register('Inner', innerDef as any);
|
|
664
681
|
|
|
665
682
|
const outerDef = {
|
|
666
683
|
component: {
|
|
667
684
|
interface: {},
|
|
668
685
|
structure: {
|
|
669
|
-
type:
|
|
670
|
-
tag:
|
|
671
|
-
children: [
|
|
672
|
-
{ type: "component", component: "Inner" },
|
|
673
|
-
],
|
|
686
|
+
type: 'node',
|
|
687
|
+
tag: 'div',
|
|
688
|
+
children: [{ type: 'component', component: 'Inner' }],
|
|
674
689
|
},
|
|
675
690
|
},
|
|
676
691
|
};
|
|
677
|
-
componentRegistry.register(
|
|
692
|
+
componentRegistry.register('Outer', outerDef as any);
|
|
678
693
|
|
|
679
694
|
const node: ComponentNode = {
|
|
680
|
-
type:
|
|
681
|
-
component:
|
|
695
|
+
type: 'component',
|
|
696
|
+
component: 'Outer',
|
|
682
697
|
};
|
|
683
698
|
const result = builder.buildComponent({ node });
|
|
684
699
|
expect(result).not.toBeNull();
|
|
685
700
|
});
|
|
686
701
|
|
|
687
|
-
test(
|
|
702
|
+
test('should propagate component context correctly', () => {
|
|
688
703
|
const componentDef = {
|
|
689
704
|
component: {
|
|
690
705
|
interface: {},
|
|
691
706
|
structure: {
|
|
692
|
-
type:
|
|
693
|
-
tag:
|
|
694
|
-
children: [
|
|
695
|
-
{ type: "node", tag: "span", children: ["Child"] },
|
|
696
|
-
],
|
|
707
|
+
type: 'node',
|
|
708
|
+
tag: 'div',
|
|
709
|
+
children: [{ type: 'node', tag: 'span', children: ['Child'] }],
|
|
697
710
|
},
|
|
698
711
|
},
|
|
699
712
|
};
|
|
700
|
-
componentRegistry.register(
|
|
713
|
+
componentRegistry.register('TestComponent', componentDef as any);
|
|
701
714
|
|
|
702
715
|
const node: ComponentNode = {
|
|
703
|
-
type:
|
|
704
|
-
component:
|
|
716
|
+
type: 'component',
|
|
717
|
+
component: 'TestComponent',
|
|
705
718
|
};
|
|
706
719
|
const result = builder.buildComponent({
|
|
707
720
|
node,
|
|
708
|
-
componentContext:
|
|
721
|
+
componentContext: 'TestComponent',
|
|
709
722
|
});
|
|
710
723
|
expect(result).not.toBeNull();
|
|
711
724
|
});
|
|
712
725
|
|
|
713
|
-
test(
|
|
726
|
+
test('should respect viewportWidth for responsive styles', () => {
|
|
714
727
|
const node: ComponentNode = {
|
|
715
|
-
type:
|
|
716
|
-
tag:
|
|
728
|
+
type: 'node',
|
|
729
|
+
tag: 'div',
|
|
717
730
|
style: {
|
|
718
|
-
base: { fontSize:
|
|
719
|
-
tablet: { fontSize:
|
|
720
|
-
mobile: { fontSize:
|
|
731
|
+
base: { fontSize: '24px' },
|
|
732
|
+
tablet: { fontSize: '18px' },
|
|
733
|
+
mobile: { fontSize: '16px' },
|
|
721
734
|
},
|
|
722
735
|
children: [],
|
|
723
736
|
};
|
|
724
|
-
|
|
737
|
+
|
|
725
738
|
const resultDesktop = builder.buildComponent({ node, viewportWidth: 1920 });
|
|
726
739
|
const resultTablet = builder.buildComponent({ node, viewportWidth: 768 });
|
|
727
740
|
const resultMobile = builder.buildComponent({ node, viewportWidth: 375 });
|
|
728
|
-
|
|
741
|
+
|
|
729
742
|
expect(resultDesktop).not.toBeNull();
|
|
730
743
|
expect(resultTablet).not.toBeNull();
|
|
731
744
|
expect(resultMobile).not.toBeNull();
|
|
732
745
|
});
|
|
733
746
|
});
|
|
734
747
|
|
|
735
|
-
describe(
|
|
748
|
+
describe('buildComponent - List (Collection Mode)', () => {
|
|
736
749
|
// Helper to create mock CMS items
|
|
737
750
|
const createMockCMSItems = (count: number): CMSItem[] => {
|
|
738
751
|
return Array.from({ length: count }, (_, i) => ({
|
|
@@ -747,7 +760,7 @@ describe("ComponentBuilder", () => {
|
|
|
747
760
|
const createCollectionListNode = (
|
|
748
761
|
source: string,
|
|
749
762
|
children: ComponentNode[],
|
|
750
|
-
options: { limit?: number; offset?: number } = {}
|
|
763
|
+
options: { limit?: number; offset?: number } = {},
|
|
751
764
|
): ListNode => ({
|
|
752
765
|
type: NODE_TYPE.LIST,
|
|
753
766
|
sourceType: 'collection',
|
|
@@ -756,11 +769,9 @@ describe("ComponentBuilder", () => {
|
|
|
756
769
|
...options,
|
|
757
770
|
});
|
|
758
771
|
|
|
759
|
-
test(
|
|
772
|
+
test('should render children for each CMS item', () => {
|
|
760
773
|
const items = createMockCMSItems(3);
|
|
761
|
-
const node = createCollectionListNode(
|
|
762
|
-
{ type: "node", tag: "div", children: ["Item content"] },
|
|
763
|
-
]);
|
|
774
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item content'] }]);
|
|
764
775
|
|
|
765
776
|
const result = builder.buildComponent({
|
|
766
777
|
node: node as unknown as ComponentNode,
|
|
@@ -768,18 +779,16 @@ describe("ComponentBuilder", () => {
|
|
|
768
779
|
});
|
|
769
780
|
|
|
770
781
|
expect(result).not.toBeNull();
|
|
771
|
-
expect(typeof result).toBe(
|
|
782
|
+
expect(typeof result).toBe('object');
|
|
772
783
|
// Result should have props.children with 3 items
|
|
773
784
|
const element = result as { props?: { children?: unknown[] } };
|
|
774
785
|
expect(Array.isArray(element?.props?.children)).toBe(true);
|
|
775
786
|
expect(element?.props?.children?.length).toBe(3);
|
|
776
787
|
});
|
|
777
788
|
|
|
778
|
-
test(
|
|
789
|
+
test('should add data-cms-item-index attribute to each item wrapper', () => {
|
|
779
790
|
const items = createMockCMSItems(3);
|
|
780
|
-
const node = createCollectionListNode(
|
|
781
|
-
{ type: "node", tag: "div", children: ["Item content"] },
|
|
782
|
-
]);
|
|
791
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item content'] }]);
|
|
783
792
|
|
|
784
793
|
const result = builder.buildComponent({
|
|
785
794
|
node: node as unknown as ComponentNode,
|
|
@@ -801,13 +810,9 @@ describe("ComponentBuilder", () => {
|
|
|
801
810
|
});
|
|
802
811
|
});
|
|
803
812
|
|
|
804
|
-
test(
|
|
805
|
-
const items: CMSItem[] = [
|
|
806
|
-
|
|
807
|
-
];
|
|
808
|
-
const node = createCollectionListNode("posts", [
|
|
809
|
-
{ type: "node", tag: "h1", children: ["{{item.title}}"] },
|
|
810
|
-
]);
|
|
813
|
+
test('should interpolate {{item.field}} in text', () => {
|
|
814
|
+
const items: CMSItem[] = [{ _id: '1', _slug: 'test', title: 'Hello World' }];
|
|
815
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'h1', children: ['{{item.title}}'] }]);
|
|
811
816
|
|
|
812
817
|
const result = builder.buildComponent({
|
|
813
818
|
node: node as unknown as ComponentNode,
|
|
@@ -816,13 +821,13 @@ describe("ComponentBuilder", () => {
|
|
|
816
821
|
|
|
817
822
|
expect(result).not.toBeNull();
|
|
818
823
|
// The text should be interpolated - we verify by checking result structure
|
|
819
|
-
expect(typeof result).toBe(
|
|
824
|
+
expect(typeof result).toBe('object');
|
|
820
825
|
});
|
|
821
826
|
|
|
822
|
-
test(
|
|
827
|
+
test('should interpolate {{itemIndex}}, {{itemFirst}}, {{itemLast}}', () => {
|
|
823
828
|
const items = createMockCMSItems(3);
|
|
824
|
-
const node = createCollectionListNode(
|
|
825
|
-
{ type:
|
|
829
|
+
const node = createCollectionListNode('posts', [
|
|
830
|
+
{ type: 'node', tag: 'span', children: ['Index: {{itemIndex}}, First: {{itemFirst}}, Last: {{itemLast}}'] },
|
|
826
831
|
]);
|
|
827
832
|
|
|
828
833
|
const result = builder.buildComponent({
|
|
@@ -831,16 +836,14 @@ describe("ComponentBuilder", () => {
|
|
|
831
836
|
});
|
|
832
837
|
|
|
833
838
|
expect(result).not.toBeNull();
|
|
834
|
-
expect(typeof result).toBe(
|
|
839
|
+
expect(typeof result).toBe('object');
|
|
835
840
|
// Should have 3 item containers
|
|
836
841
|
const element = result as { props?: { children?: unknown[] } };
|
|
837
842
|
expect(element?.props?.children?.length).toBe(3);
|
|
838
843
|
});
|
|
839
844
|
|
|
840
|
-
test(
|
|
841
|
-
const node = createCollectionListNode(
|
|
842
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
843
|
-
]);
|
|
845
|
+
test('should show empty state when no items', () => {
|
|
846
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }]);
|
|
844
847
|
|
|
845
848
|
const result = builder.buildComponent({
|
|
846
849
|
node: node as unknown as ComponentNode,
|
|
@@ -848,16 +851,14 @@ describe("ComponentBuilder", () => {
|
|
|
848
851
|
});
|
|
849
852
|
|
|
850
853
|
expect(result).not.toBeNull();
|
|
851
|
-
expect(typeof result).toBe(
|
|
854
|
+
expect(typeof result).toBe('object');
|
|
852
855
|
// Empty state should be a single div with message
|
|
853
856
|
const element = result as { props?: { children?: unknown } };
|
|
854
857
|
expect(element?.props?.children).not.toBeNull();
|
|
855
858
|
});
|
|
856
859
|
|
|
857
|
-
test(
|
|
858
|
-
const node = createCollectionListNode(
|
|
859
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
860
|
-
]);
|
|
860
|
+
test('should show empty state when collection not in map', () => {
|
|
861
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }]);
|
|
861
862
|
|
|
862
863
|
const result = builder.buildComponent({
|
|
863
864
|
node: node as unknown as ComponentNode,
|
|
@@ -865,14 +866,12 @@ describe("ComponentBuilder", () => {
|
|
|
865
866
|
});
|
|
866
867
|
|
|
867
868
|
expect(result).not.toBeNull();
|
|
868
|
-
expect(typeof result).toBe(
|
|
869
|
+
expect(typeof result).toBe('object');
|
|
869
870
|
});
|
|
870
871
|
|
|
871
|
-
test(
|
|
872
|
+
test('should apply limit to items', () => {
|
|
872
873
|
const items = createMockCMSItems(5);
|
|
873
|
-
const node = createCollectionListNode(
|
|
874
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
875
|
-
], { limit: 2 });
|
|
874
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], { limit: 2 });
|
|
876
875
|
|
|
877
876
|
const result = builder.buildComponent({
|
|
878
877
|
node: node as unknown as ComponentNode,
|
|
@@ -885,11 +884,9 @@ describe("ComponentBuilder", () => {
|
|
|
885
884
|
expect(element?.props?.children?.length).toBe(2);
|
|
886
885
|
});
|
|
887
886
|
|
|
888
|
-
test(
|
|
887
|
+
test('should apply offset to items', () => {
|
|
889
888
|
const items = createMockCMSItems(5);
|
|
890
|
-
const node = createCollectionListNode(
|
|
891
|
-
{ type: "node", tag: "div", children: ["Item"] },
|
|
892
|
-
], { offset: 2 });
|
|
889
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], { offset: 2 });
|
|
893
890
|
|
|
894
891
|
const result = builder.buildComponent({
|
|
895
892
|
node: node as unknown as ComponentNode,
|
|
@@ -902,11 +899,12 @@ describe("ComponentBuilder", () => {
|
|
|
902
899
|
expect(element?.props?.children?.length).toBe(3); // 5 - 2 = 3
|
|
903
900
|
});
|
|
904
901
|
|
|
905
|
-
test(
|
|
902
|
+
test('should apply both limit and offset', () => {
|
|
906
903
|
const items = createMockCMSItems(5);
|
|
907
|
-
const node = createCollectionListNode(
|
|
908
|
-
|
|
909
|
-
|
|
904
|
+
const node = createCollectionListNode('posts', [{ type: 'node', tag: 'div', children: ['Item'] }], {
|
|
905
|
+
offset: 1,
|
|
906
|
+
limit: 2,
|
|
907
|
+
});
|
|
910
908
|
|
|
911
909
|
const result = builder.buildComponent({
|
|
912
910
|
node: node as unknown as ComponentNode,
|
|
@@ -919,19 +917,19 @@ describe("ComponentBuilder", () => {
|
|
|
919
917
|
expect(element?.props?.children?.length).toBe(2);
|
|
920
918
|
});
|
|
921
919
|
|
|
922
|
-
test(
|
|
920
|
+
test('should filter items with simple object filter', () => {
|
|
923
921
|
const items: CMSItem[] = [
|
|
924
|
-
{ _id:
|
|
925
|
-
{ _id:
|
|
926
|
-
{ _id:
|
|
927
|
-
{ _id:
|
|
922
|
+
{ _id: '1', _slug: 'post1', featured: true, title: 'Featured Post' },
|
|
923
|
+
{ _id: '2', _slug: 'post2', featured: false, title: 'Regular Post 1' },
|
|
924
|
+
{ _id: '3', _slug: 'post3', featured: true, title: 'Another Featured' },
|
|
925
|
+
{ _id: '4', _slug: 'post4', featured: false, title: 'Regular Post 2' },
|
|
928
926
|
];
|
|
929
927
|
const node: ListNode = {
|
|
930
928
|
type: NODE_TYPE.LIST,
|
|
931
929
|
sourceType: 'collection',
|
|
932
|
-
source:
|
|
930
|
+
source: 'posts',
|
|
933
931
|
filter: { featured: true },
|
|
934
|
-
children: [{ type:
|
|
932
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
935
933
|
};
|
|
936
934
|
|
|
937
935
|
const result = builder.buildComponent({
|
|
@@ -945,19 +943,19 @@ describe("ComponentBuilder", () => {
|
|
|
945
943
|
expect(element?.props?.children?.length).toBe(2); // Only featured items
|
|
946
944
|
});
|
|
947
945
|
|
|
948
|
-
test(
|
|
946
|
+
test('should filter items with operator condition', () => {
|
|
949
947
|
const items: CMSItem[] = [
|
|
950
|
-
{ _id:
|
|
951
|
-
{ _id:
|
|
952
|
-
{ _id:
|
|
953
|
-
{ _id:
|
|
948
|
+
{ _id: '1', _slug: 'post1', price: 50, title: 'Cheap' },
|
|
949
|
+
{ _id: '2', _slug: 'post2', price: 150, title: 'Expensive 1' },
|
|
950
|
+
{ _id: '3', _slug: 'post3', price: 200, title: 'Expensive 2' },
|
|
951
|
+
{ _id: '4', _slug: 'post4', price: 75, title: 'Mid' },
|
|
954
952
|
];
|
|
955
953
|
const node: ListNode = {
|
|
956
954
|
type: NODE_TYPE.LIST,
|
|
957
955
|
sourceType: 'collection',
|
|
958
|
-
source:
|
|
959
|
-
filter: { field:
|
|
960
|
-
children: [{ type:
|
|
956
|
+
source: 'posts',
|
|
957
|
+
filter: { field: 'price', operator: 'gt', value: 100 },
|
|
958
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
961
959
|
};
|
|
962
960
|
|
|
963
961
|
const result = builder.buildComponent({
|
|
@@ -971,24 +969,24 @@ describe("ComponentBuilder", () => {
|
|
|
971
969
|
expect(element?.props?.children?.length).toBe(2); // Only items with price > 100
|
|
972
970
|
});
|
|
973
971
|
|
|
974
|
-
test(
|
|
972
|
+
test('should filter items with template value from cmsContext', () => {
|
|
975
973
|
const items: CMSItem[] = [
|
|
976
|
-
{ _id:
|
|
977
|
-
{ _id:
|
|
978
|
-
{ _id:
|
|
974
|
+
{ _id: '1', _slug: 'post1', category: 'tech', title: 'Tech Post 1' },
|
|
975
|
+
{ _id: '2', _slug: 'post2', category: 'news', title: 'News Post' },
|
|
976
|
+
{ _id: '3', _slug: 'post3', category: 'tech', title: 'Tech Post 2' },
|
|
979
977
|
];
|
|
980
978
|
const node: ListNode = {
|
|
981
979
|
type: NODE_TYPE.LIST,
|
|
982
980
|
sourceType: 'collection',
|
|
983
|
-
source:
|
|
984
|
-
filter: { category:
|
|
985
|
-
children: [{ type:
|
|
981
|
+
source: 'posts',
|
|
982
|
+
filter: { category: '{{cms.category}}' },
|
|
983
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
986
984
|
};
|
|
987
985
|
|
|
988
986
|
const result = builder.buildComponent({
|
|
989
987
|
node: node as unknown as ComponentNode,
|
|
990
988
|
collectionItemsMap: { posts: items },
|
|
991
|
-
cmsContext: { category:
|
|
989
|
+
cmsContext: { category: 'tech' },
|
|
992
990
|
});
|
|
993
991
|
|
|
994
992
|
expect(result).not.toBeNull();
|
|
@@ -997,18 +995,18 @@ describe("ComponentBuilder", () => {
|
|
|
997
995
|
expect(element?.props?.children?.length).toBe(2); // Only tech posts
|
|
998
996
|
});
|
|
999
997
|
|
|
1000
|
-
test(
|
|
998
|
+
test('should sort items ascending', () => {
|
|
1001
999
|
const items: CMSItem[] = [
|
|
1002
|
-
{ _id:
|
|
1003
|
-
{ _id:
|
|
1004
|
-
{ _id:
|
|
1000
|
+
{ _id: '1', _slug: 'c', title: 'C' },
|
|
1001
|
+
{ _id: '2', _slug: 'a', title: 'A' },
|
|
1002
|
+
{ _id: '3', _slug: 'b', title: 'B' },
|
|
1005
1003
|
];
|
|
1006
1004
|
const node: ListNode = {
|
|
1007
1005
|
type: NODE_TYPE.LIST,
|
|
1008
1006
|
sourceType: 'collection',
|
|
1009
|
-
source:
|
|
1010
|
-
sort: { field:
|
|
1011
|
-
children: [{ type:
|
|
1007
|
+
source: 'posts',
|
|
1008
|
+
sort: { field: 'title', order: 'asc' },
|
|
1009
|
+
children: [{ type: 'node', tag: 'div', children: ['{{item.title}}'] }],
|
|
1012
1010
|
};
|
|
1013
1011
|
|
|
1014
1012
|
const result = builder.buildComponent({
|
|
@@ -1023,18 +1021,18 @@ describe("ComponentBuilder", () => {
|
|
|
1023
1021
|
// We can't easily check the order of rendered children without more complex inspection
|
|
1024
1022
|
});
|
|
1025
1023
|
|
|
1026
|
-
test(
|
|
1024
|
+
test('should sort items descending', () => {
|
|
1027
1025
|
const items: CMSItem[] = [
|
|
1028
|
-
{ _id:
|
|
1029
|
-
{ _id:
|
|
1030
|
-
{ _id:
|
|
1026
|
+
{ _id: '1', _slug: 'p1', date: 100 },
|
|
1027
|
+
{ _id: '2', _slug: 'p2', date: 300 },
|
|
1028
|
+
{ _id: '3', _slug: 'p3', date: 200 },
|
|
1031
1029
|
];
|
|
1032
1030
|
const node: ListNode = {
|
|
1033
1031
|
type: NODE_TYPE.LIST,
|
|
1034
1032
|
sourceType: 'collection',
|
|
1035
|
-
source:
|
|
1036
|
-
sort: { field:
|
|
1037
|
-
children: [{ type:
|
|
1033
|
+
source: 'posts',
|
|
1034
|
+
sort: { field: 'date', order: 'desc' },
|
|
1035
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1038
1036
|
};
|
|
1039
1037
|
|
|
1040
1038
|
const result = builder.buildComponent({
|
|
@@ -1048,22 +1046,22 @@ describe("ComponentBuilder", () => {
|
|
|
1048
1046
|
expect(element?.props?.children?.length).toBe(3);
|
|
1049
1047
|
});
|
|
1050
1048
|
|
|
1051
|
-
test(
|
|
1049
|
+
test('should apply filter BEFORE limit', () => {
|
|
1052
1050
|
// 5 items: 2 featured, 3 not featured
|
|
1053
1051
|
const items: CMSItem[] = [
|
|
1054
|
-
{ _id:
|
|
1055
|
-
{ _id:
|
|
1056
|
-
{ _id:
|
|
1057
|
-
{ _id:
|
|
1058
|
-
{ _id:
|
|
1052
|
+
{ _id: '1', _slug: 'p1', featured: true, title: 'Featured 1' },
|
|
1053
|
+
{ _id: '2', _slug: 'p2', featured: false, title: 'Regular 1' },
|
|
1054
|
+
{ _id: '3', _slug: 'p3', featured: false, title: 'Regular 2' },
|
|
1055
|
+
{ _id: '4', _slug: 'p4', featured: true, title: 'Featured 2' },
|
|
1056
|
+
{ _id: '5', _slug: 'p5', featured: false, title: 'Regular 3' },
|
|
1059
1057
|
];
|
|
1060
1058
|
const node: ListNode = {
|
|
1061
1059
|
type: NODE_TYPE.LIST,
|
|
1062
1060
|
sourceType: 'collection',
|
|
1063
|
-
source:
|
|
1061
|
+
source: 'posts',
|
|
1064
1062
|
filter: { featured: true },
|
|
1065
1063
|
limit: 3, // Without proper order, this would limit before filtering
|
|
1066
|
-
children: [{ type:
|
|
1064
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1067
1065
|
};
|
|
1068
1066
|
|
|
1069
1067
|
const result = builder.buildComponent({
|
|
@@ -1078,21 +1076,21 @@ describe("ComponentBuilder", () => {
|
|
|
1078
1076
|
expect(element?.props?.children?.length).toBe(2);
|
|
1079
1077
|
});
|
|
1080
1078
|
|
|
1081
|
-
test(
|
|
1079
|
+
test('should apply sort BEFORE limit', () => {
|
|
1082
1080
|
const items: CMSItem[] = [
|
|
1083
|
-
{ _id:
|
|
1084
|
-
{ _id:
|
|
1085
|
-
{ _id:
|
|
1086
|
-
{ _id:
|
|
1087
|
-
{ _id:
|
|
1081
|
+
{ _id: '1', _slug: 'p1', date: 100 },
|
|
1082
|
+
{ _id: '2', _slug: 'p2', date: 300 },
|
|
1083
|
+
{ _id: '3', _slug: 'p3', date: 200 },
|
|
1084
|
+
{ _id: '4', _slug: 'p4', date: 400 },
|
|
1085
|
+
{ _id: '5', _slug: 'p5', date: 50 },
|
|
1088
1086
|
];
|
|
1089
1087
|
const node: ListNode = {
|
|
1090
1088
|
type: NODE_TYPE.LIST,
|
|
1091
1089
|
sourceType: 'collection',
|
|
1092
|
-
source:
|
|
1093
|
-
sort: { field:
|
|
1090
|
+
source: 'posts',
|
|
1091
|
+
sort: { field: 'date', order: 'desc' },
|
|
1094
1092
|
limit: 2, // Should get 2 most recent (highest date)
|
|
1095
|
-
children: [{ type:
|
|
1093
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1096
1094
|
};
|
|
1097
1095
|
|
|
1098
1096
|
const result = builder.buildComponent({
|
|
@@ -1106,22 +1104,22 @@ describe("ComponentBuilder", () => {
|
|
|
1106
1104
|
expect(element?.props?.children?.length).toBe(2);
|
|
1107
1105
|
});
|
|
1108
1106
|
|
|
1109
|
-
test(
|
|
1107
|
+
test('should apply multiple filter conditions with AND logic', () => {
|
|
1110
1108
|
const items: CMSItem[] = [
|
|
1111
|
-
{ _id:
|
|
1112
|
-
{ _id:
|
|
1113
|
-
{ _id:
|
|
1114
|
-
{ _id:
|
|
1109
|
+
{ _id: '1', _slug: 'p1', featured: true, price: 150 },
|
|
1110
|
+
{ _id: '2', _slug: 'p2', featured: false, price: 200 },
|
|
1111
|
+
{ _id: '3', _slug: 'p3', featured: true, price: 50 },
|
|
1112
|
+
{ _id: '4', _slug: 'p4', featured: true, price: 200 },
|
|
1115
1113
|
];
|
|
1116
1114
|
const node: ListNode = {
|
|
1117
1115
|
type: NODE_TYPE.LIST,
|
|
1118
1116
|
sourceType: 'collection',
|
|
1119
|
-
source:
|
|
1117
|
+
source: 'posts',
|
|
1120
1118
|
filter: [
|
|
1121
|
-
{ field:
|
|
1122
|
-
{ field:
|
|
1119
|
+
{ field: 'featured', value: true },
|
|
1120
|
+
{ field: 'price', operator: 'gte', value: 100 },
|
|
1123
1121
|
],
|
|
1124
|
-
children: [{ type:
|
|
1122
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1125
1123
|
};
|
|
1126
1124
|
|
|
1127
1125
|
const result = builder.buildComponent({
|
|
@@ -1137,7 +1135,69 @@ describe("ComponentBuilder", () => {
|
|
|
1137
1135
|
});
|
|
1138
1136
|
});
|
|
1139
1137
|
|
|
1140
|
-
describe(
|
|
1138
|
+
describe('buildComponent - List (Remote Mode)', () => {
|
|
1139
|
+
// Remote lists (sourceType: 'remote') have no Astro runtime in the editor; the studio
|
|
1140
|
+
// pre-fetches the endpoint and injects items into collectionItemsMap keyed by the URL.
|
|
1141
|
+
const URL = 'https://api.example.com/markets';
|
|
1142
|
+
|
|
1143
|
+
const createRemoteItems = (count: number): CMSItem[] =>
|
|
1144
|
+
Array.from({ length: count }, (_, i) => ({ _id: String(i), name: `Coin ${i}`, rank: i }));
|
|
1145
|
+
|
|
1146
|
+
const createRemoteListNode = (
|
|
1147
|
+
children: ComponentNode[],
|
|
1148
|
+
options: { url?: string; limit?: number; offset?: number; sort?: unknown } = {},
|
|
1149
|
+
): ListNode =>
|
|
1150
|
+
({
|
|
1151
|
+
type: NODE_TYPE.LIST,
|
|
1152
|
+
sourceType: 'remote',
|
|
1153
|
+
url: options.url ?? URL,
|
|
1154
|
+
children,
|
|
1155
|
+
...options,
|
|
1156
|
+
}) as unknown as ListNode;
|
|
1157
|
+
|
|
1158
|
+
test('renders an item for each injected remote item (keyed by URL)', () => {
|
|
1159
|
+
const items = createRemoteItems(3);
|
|
1160
|
+
const node = createRemoteListNode([{ type: 'node', tag: 'div', children: ['{{item.name}}'] }]);
|
|
1161
|
+
|
|
1162
|
+
const result = builder.buildComponent({
|
|
1163
|
+
node: node as unknown as ComponentNode,
|
|
1164
|
+
collectionItemsMap: { [URL]: items },
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
const element = result as { props?: { children?: unknown[] } };
|
|
1168
|
+
expect(Array.isArray(element?.props?.children)).toBe(true);
|
|
1169
|
+
expect(element?.props?.children?.length).toBe(3);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
test('applies limit to the injected remote items', () => {
|
|
1173
|
+
const items = createRemoteItems(10);
|
|
1174
|
+
const node = createRemoteListNode([{ type: 'node', tag: 'div', children: ['{{item.name}}'] }], { limit: 4 });
|
|
1175
|
+
|
|
1176
|
+
const result = builder.buildComponent({
|
|
1177
|
+
node: node as unknown as ComponentNode,
|
|
1178
|
+
collectionItemsMap: { [URL]: items },
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const element = result as { props?: { children?: unknown[] } };
|
|
1182
|
+
// The regression: changing Limit must NOT empty the list — it slices the cached items.
|
|
1183
|
+
expect(element?.props?.children?.length).toBe(4);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
test('shows empty state (not a crash) when the URL has no injected items yet', () => {
|
|
1187
|
+
const node = createRemoteListNode([{ type: 'node', tag: 'div', children: ['{{item.name}}'] }]);
|
|
1188
|
+
|
|
1189
|
+
const result = builder.buildComponent({
|
|
1190
|
+
node: node as unknown as ComponentNode,
|
|
1191
|
+
collectionItemsMap: {}, // not fetched yet
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
expect(result).not.toBeNull();
|
|
1195
|
+
const element = result as { props?: { children?: unknown } };
|
|
1196
|
+
expect(element?.props?.children).not.toBeNull();
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
describe('Interactive Styles - Element Class Generation', () => {
|
|
1141
1201
|
/**
|
|
1142
1202
|
* These tests verify that element classes are generated correctly
|
|
1143
1203
|
* for interactive styles in both page and component contexts.
|
|
@@ -1149,11 +1209,11 @@ describe("ComponentBuilder", () => {
|
|
|
1149
1209
|
* 4. The same component element always gets the same class
|
|
1150
1210
|
*/
|
|
1151
1211
|
|
|
1152
|
-
test(
|
|
1212
|
+
test('should not add element class when no interactive styles', () => {
|
|
1153
1213
|
const node: ComponentNode = {
|
|
1154
|
-
type:
|
|
1155
|
-
tag:
|
|
1156
|
-
children: [
|
|
1214
|
+
type: 'node',
|
|
1215
|
+
tag: 'div',
|
|
1216
|
+
children: ['No interactive styles'],
|
|
1157
1217
|
};
|
|
1158
1218
|
|
|
1159
1219
|
const result = builder.buildComponent({ node });
|
|
@@ -1161,12 +1221,12 @@ describe("ComponentBuilder", () => {
|
|
|
1161
1221
|
// No element class should be added when there are no interactive styles
|
|
1162
1222
|
});
|
|
1163
1223
|
|
|
1164
|
-
test(
|
|
1224
|
+
test('should add element class when node has generateElementClass flag', () => {
|
|
1165
1225
|
const node: ComponentNode = {
|
|
1166
|
-
type:
|
|
1167
|
-
tag:
|
|
1226
|
+
type: 'node',
|
|
1227
|
+
tag: 'div',
|
|
1168
1228
|
generateElementClass: true,
|
|
1169
|
-
children: [
|
|
1229
|
+
children: ['Has element class flag'],
|
|
1170
1230
|
};
|
|
1171
1231
|
|
|
1172
1232
|
const result = builder.buildComponent({ node });
|
|
@@ -1174,14 +1234,12 @@ describe("ComponentBuilder", () => {
|
|
|
1174
1234
|
// Element class should be generated when flag is set
|
|
1175
1235
|
});
|
|
1176
1236
|
|
|
1177
|
-
test(
|
|
1237
|
+
test('should add element class when node has interactive styles', () => {
|
|
1178
1238
|
const node: ComponentNode = {
|
|
1179
|
-
type:
|
|
1180
|
-
tag:
|
|
1181
|
-
interactiveStyles: [
|
|
1182
|
-
|
|
1183
|
-
],
|
|
1184
|
-
children: ["Has interactive styles"],
|
|
1239
|
+
type: 'node',
|
|
1240
|
+
tag: 'div',
|
|
1241
|
+
interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary' } }],
|
|
1242
|
+
children: ['Has interactive styles'],
|
|
1185
1243
|
};
|
|
1186
1244
|
|
|
1187
1245
|
const result = builder.buildComponent({ node });
|
|
@@ -1189,23 +1247,21 @@ describe("ComponentBuilder", () => {
|
|
|
1189
1247
|
// Element class should be generated for interactive styles
|
|
1190
1248
|
});
|
|
1191
1249
|
|
|
1192
|
-
test(
|
|
1250
|
+
test('should use page-based class for page elements', () => {
|
|
1193
1251
|
// Create builder with page name getter
|
|
1194
1252
|
const pageBuilder = new ComponentBuilder({
|
|
1195
1253
|
componentRegistry,
|
|
1196
1254
|
elementRegistry,
|
|
1197
|
-
getCurrentPageName: () =>
|
|
1198
|
-
getCurrentFileType: () =>
|
|
1255
|
+
getCurrentPageName: () => 'about',
|
|
1256
|
+
getCurrentFileType: () => 'page',
|
|
1199
1257
|
});
|
|
1200
1258
|
|
|
1201
1259
|
const node: ComponentNode = {
|
|
1202
|
-
type:
|
|
1203
|
-
tag:
|
|
1204
|
-
label:
|
|
1205
|
-
interactiveStyles: [
|
|
1206
|
-
|
|
1207
|
-
],
|
|
1208
|
-
children: ["Submit"],
|
|
1260
|
+
type: 'node',
|
|
1261
|
+
tag: 'button',
|
|
1262
|
+
label: 'submit-button',
|
|
1263
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1264
|
+
children: ['Submit'],
|
|
1209
1265
|
};
|
|
1210
1266
|
|
|
1211
1267
|
const result = pageBuilder.buildComponent({ node });
|
|
@@ -1213,28 +1269,26 @@ describe("ComponentBuilder", () => {
|
|
|
1213
1269
|
// Element class should be p_about_submit-button for page elements
|
|
1214
1270
|
});
|
|
1215
1271
|
|
|
1216
|
-
test(
|
|
1272
|
+
test('should use component-based class when inside component context', () => {
|
|
1217
1273
|
const node: ComponentNode = {
|
|
1218
|
-
type:
|
|
1219
|
-
tag:
|
|
1220
|
-
label:
|
|
1221
|
-
interactiveStyles: [
|
|
1222
|
-
|
|
1223
|
-
],
|
|
1224
|
-
children: ["Click"],
|
|
1274
|
+
type: 'node',
|
|
1275
|
+
tag: 'button',
|
|
1276
|
+
label: 'action-button',
|
|
1277
|
+
interactiveStyles: [{ postfix: ':hover', style: { backgroundColor: 'primary-dark' } }],
|
|
1278
|
+
children: ['Click'],
|
|
1225
1279
|
};
|
|
1226
1280
|
|
|
1227
1281
|
// Build with componentContext set (inside a component)
|
|
1228
1282
|
const result = builder.buildComponent({
|
|
1229
1283
|
node,
|
|
1230
|
-
componentContext:
|
|
1284
|
+
componentContext: 'Button',
|
|
1231
1285
|
});
|
|
1232
1286
|
|
|
1233
1287
|
expect(result).not.toBeNull();
|
|
1234
1288
|
// Element class should be c_button_action-button for component elements
|
|
1235
1289
|
});
|
|
1236
1290
|
|
|
1237
|
-
test(
|
|
1291
|
+
test('should use relative path for component elements', () => {
|
|
1238
1292
|
// When a component is placed at different positions on a page,
|
|
1239
1293
|
// its internal elements should always get the same class
|
|
1240
1294
|
// because they use paths relative to the component root.
|
|
@@ -1243,32 +1297,28 @@ describe("ComponentBuilder", () => {
|
|
|
1243
1297
|
component: {
|
|
1244
1298
|
interface: {},
|
|
1245
1299
|
structure: {
|
|
1246
|
-
type:
|
|
1247
|
-
tag:
|
|
1248
|
-
label:
|
|
1249
|
-
interactiveStyles: [
|
|
1250
|
-
{ postfix: ":hover", style: { boxShadow: "0 4px 12px rgba(0,0,0,0.1)" } },
|
|
1251
|
-
],
|
|
1300
|
+
type: 'node',
|
|
1301
|
+
tag: 'div',
|
|
1302
|
+
label: 'root',
|
|
1303
|
+
interactiveStyles: [{ postfix: ':hover', style: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' } }],
|
|
1252
1304
|
children: [
|
|
1253
1305
|
{
|
|
1254
|
-
type:
|
|
1255
|
-
tag:
|
|
1256
|
-
label:
|
|
1257
|
-
interactiveStyles: [
|
|
1258
|
-
|
|
1259
|
-
],
|
|
1260
|
-
children: ["Label"],
|
|
1306
|
+
type: 'node',
|
|
1307
|
+
tag: 'span',
|
|
1308
|
+
label: 'label',
|
|
1309
|
+
interactiveStyles: [{ postfix: ':hover', style: { color: 'primary' } }],
|
|
1310
|
+
children: ['Label'],
|
|
1261
1311
|
},
|
|
1262
1312
|
],
|
|
1263
1313
|
},
|
|
1264
1314
|
},
|
|
1265
1315
|
};
|
|
1266
|
-
componentRegistry.register(
|
|
1316
|
+
componentRegistry.register('HoverCard', componentDef as any);
|
|
1267
1317
|
|
|
1268
1318
|
// First instance at page path [0]
|
|
1269
1319
|
const node1: ComponentNode = {
|
|
1270
|
-
type:
|
|
1271
|
-
component:
|
|
1320
|
+
type: 'component',
|
|
1321
|
+
component: 'HoverCard',
|
|
1272
1322
|
};
|
|
1273
1323
|
|
|
1274
1324
|
const result1 = builder.buildComponent({
|
|
@@ -1291,7 +1341,7 @@ describe("ComponentBuilder", () => {
|
|
|
1291
1341
|
// component-relative paths, not page-absolute paths
|
|
1292
1342
|
});
|
|
1293
1343
|
|
|
1294
|
-
test(
|
|
1344
|
+
test('should generate different classes for page vs component with same name', () => {
|
|
1295
1345
|
// A page named "button" and a component named "Button" should
|
|
1296
1346
|
// produce different element classes (p_button_... vs c_button_...)
|
|
1297
1347
|
|
|
@@ -1299,22 +1349,20 @@ describe("ComponentBuilder", () => {
|
|
|
1299
1349
|
component: {
|
|
1300
1350
|
interface: {},
|
|
1301
1351
|
structure: {
|
|
1302
|
-
type:
|
|
1303
|
-
tag:
|
|
1304
|
-
label:
|
|
1305
|
-
interactiveStyles: [
|
|
1306
|
-
|
|
1307
|
-
],
|
|
1308
|
-
children: ["Component Button"],
|
|
1352
|
+
type: 'node',
|
|
1353
|
+
tag: 'button',
|
|
1354
|
+
label: 'root',
|
|
1355
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1356
|
+
children: ['Component Button'],
|
|
1309
1357
|
},
|
|
1310
1358
|
},
|
|
1311
1359
|
};
|
|
1312
|
-
componentRegistry.register(
|
|
1360
|
+
componentRegistry.register('Button', componentDef as any);
|
|
1313
1361
|
|
|
1314
1362
|
// Component instance
|
|
1315
1363
|
const componentNode: ComponentNode = {
|
|
1316
|
-
type:
|
|
1317
|
-
component:
|
|
1364
|
+
type: 'component',
|
|
1365
|
+
component: 'Button',
|
|
1318
1366
|
};
|
|
1319
1367
|
|
|
1320
1368
|
const componentResult = builder.buildComponent({ node: componentNode });
|
|
@@ -1324,18 +1372,16 @@ describe("ComponentBuilder", () => {
|
|
|
1324
1372
|
const pageBuilder = new ComponentBuilder({
|
|
1325
1373
|
componentRegistry,
|
|
1326
1374
|
elementRegistry,
|
|
1327
|
-
getCurrentPageName: () =>
|
|
1328
|
-
getCurrentFileType: () =>
|
|
1375
|
+
getCurrentPageName: () => 'button',
|
|
1376
|
+
getCurrentFileType: () => 'page',
|
|
1329
1377
|
});
|
|
1330
1378
|
|
|
1331
1379
|
const pageNode: ComponentNode = {
|
|
1332
|
-
type:
|
|
1333
|
-
tag:
|
|
1334
|
-
label:
|
|
1335
|
-
interactiveStyles: [
|
|
1336
|
-
|
|
1337
|
-
],
|
|
1338
|
-
children: ["Page Button"],
|
|
1380
|
+
type: 'node',
|
|
1381
|
+
tag: 'button',
|
|
1382
|
+
label: 'root',
|
|
1383
|
+
interactiveStyles: [{ postfix: ':hover', style: { opacity: '0.9' } }],
|
|
1384
|
+
children: ['Page Button'],
|
|
1339
1385
|
};
|
|
1340
1386
|
|
|
1341
1387
|
const pageResult = pageBuilder.buildComponent({ node: pageNode });
|
|
@@ -1344,26 +1390,24 @@ describe("ComponentBuilder", () => {
|
|
|
1344
1390
|
// Component uses c_button_root, page uses p_button_root
|
|
1345
1391
|
});
|
|
1346
1392
|
|
|
1347
|
-
test(
|
|
1393
|
+
test('should propagate componentContext through nested children', () => {
|
|
1348
1394
|
const componentDef = {
|
|
1349
1395
|
component: {
|
|
1350
1396
|
interface: {},
|
|
1351
1397
|
structure: {
|
|
1352
|
-
type:
|
|
1353
|
-
tag:
|
|
1398
|
+
type: 'node',
|
|
1399
|
+
tag: 'div',
|
|
1354
1400
|
children: [
|
|
1355
1401
|
{
|
|
1356
|
-
type:
|
|
1357
|
-
tag:
|
|
1402
|
+
type: 'node',
|
|
1403
|
+
tag: 'div',
|
|
1358
1404
|
children: [
|
|
1359
1405
|
{
|
|
1360
|
-
type:
|
|
1361
|
-
tag:
|
|
1362
|
-
label:
|
|
1363
|
-
interactiveStyles: [
|
|
1364
|
-
|
|
1365
|
-
],
|
|
1366
|
-
children: ["Nested Button"],
|
|
1406
|
+
type: 'node',
|
|
1407
|
+
tag: 'button',
|
|
1408
|
+
label: 'deeply-nested',
|
|
1409
|
+
interactiveStyles: [{ postfix: ':hover', style: { color: 'primary' } }],
|
|
1410
|
+
children: ['Nested Button'],
|
|
1367
1411
|
},
|
|
1368
1412
|
],
|
|
1369
1413
|
},
|
|
@@ -1371,11 +1415,11 @@ describe("ComponentBuilder", () => {
|
|
|
1371
1415
|
},
|
|
1372
1416
|
},
|
|
1373
1417
|
};
|
|
1374
|
-
componentRegistry.register(
|
|
1418
|
+
componentRegistry.register('DeepComponent', componentDef as any);
|
|
1375
1419
|
|
|
1376
1420
|
const node: ComponentNode = {
|
|
1377
|
-
type:
|
|
1378
|
-
component:
|
|
1421
|
+
type: 'component',
|
|
1422
|
+
component: 'DeepComponent',
|
|
1379
1423
|
};
|
|
1380
1424
|
|
|
1381
1425
|
const result = builder.buildComponent({ node });
|
|
@@ -1385,50 +1429,50 @@ describe("ComponentBuilder", () => {
|
|
|
1385
1429
|
// because componentContext is propagated through the tree
|
|
1386
1430
|
});
|
|
1387
1431
|
|
|
1388
|
-
test(
|
|
1432
|
+
test('should handle multiple interactive states on same element', () => {
|
|
1389
1433
|
const node: ComponentNode = {
|
|
1390
|
-
type:
|
|
1391
|
-
tag:
|
|
1392
|
-
label:
|
|
1434
|
+
type: 'node',
|
|
1435
|
+
tag: 'button',
|
|
1436
|
+
label: 'multi-state',
|
|
1393
1437
|
interactiveStyles: [
|
|
1394
|
-
{ postfix:
|
|
1395
|
-
{ postfix:
|
|
1396
|
-
{ postfix:
|
|
1397
|
-
{ postfix:
|
|
1438
|
+
{ postfix: ':hover', style: { backgroundColor: 'primary-light' } },
|
|
1439
|
+
{ postfix: ':active', style: { backgroundColor: 'primary-dark' } },
|
|
1440
|
+
{ postfix: ':focus', style: { outline: '2px solid var(--primary)' } },
|
|
1441
|
+
{ postfix: ':disabled', style: { opacity: '0.5', cursor: 'not-allowed' } },
|
|
1398
1442
|
],
|
|
1399
|
-
children: [
|
|
1443
|
+
children: ['Multi-state Button'],
|
|
1400
1444
|
};
|
|
1401
1445
|
|
|
1402
1446
|
const result = builder.buildComponent({
|
|
1403
1447
|
node,
|
|
1404
|
-
componentContext:
|
|
1448
|
+
componentContext: 'MultiButton',
|
|
1405
1449
|
});
|
|
1406
1450
|
|
|
1407
1451
|
expect(result).not.toBeNull();
|
|
1408
1452
|
// All states should use the same element class: c_multibutton_multi-state
|
|
1409
1453
|
});
|
|
1410
1454
|
|
|
1411
|
-
test(
|
|
1455
|
+
test('should handle responsive interactive styles', () => {
|
|
1412
1456
|
const node: ComponentNode = {
|
|
1413
|
-
type:
|
|
1414
|
-
tag:
|
|
1415
|
-
label:
|
|
1457
|
+
type: 'node',
|
|
1458
|
+
tag: 'div',
|
|
1459
|
+
label: 'responsive-hover',
|
|
1416
1460
|
interactiveStyles: [
|
|
1417
1461
|
{
|
|
1418
|
-
postfix:
|
|
1462
|
+
postfix: ':hover',
|
|
1419
1463
|
style: {
|
|
1420
|
-
base: { padding:
|
|
1421
|
-
tablet: { padding:
|
|
1422
|
-
mobile: { padding:
|
|
1464
|
+
base: { padding: '20px' },
|
|
1465
|
+
tablet: { padding: '16px' },
|
|
1466
|
+
mobile: { padding: '12px' },
|
|
1423
1467
|
},
|
|
1424
1468
|
},
|
|
1425
1469
|
],
|
|
1426
|
-
children: [
|
|
1470
|
+
children: ['Responsive hover'],
|
|
1427
1471
|
};
|
|
1428
1472
|
|
|
1429
1473
|
const result = builder.buildComponent({
|
|
1430
1474
|
node,
|
|
1431
|
-
componentContext:
|
|
1475
|
+
componentContext: 'ResponsiveCard',
|
|
1432
1476
|
});
|
|
1433
1477
|
|
|
1434
1478
|
expect(result).not.toBeNull();
|
|
@@ -1436,27 +1480,27 @@ describe("ComponentBuilder", () => {
|
|
|
1436
1480
|
});
|
|
1437
1481
|
});
|
|
1438
1482
|
|
|
1439
|
-
describe(
|
|
1440
|
-
test(
|
|
1483
|
+
describe('buildComponent - Conditional Rendering (if property)', () => {
|
|
1484
|
+
test('should render node when if is true', () => {
|
|
1441
1485
|
const node: ComponentNode = {
|
|
1442
|
-
type:
|
|
1443
|
-
tag:
|
|
1486
|
+
type: 'node',
|
|
1487
|
+
tag: 'div',
|
|
1444
1488
|
if: true,
|
|
1445
|
-
children: [
|
|
1489
|
+
children: ['Visible content'],
|
|
1446
1490
|
} as any;
|
|
1447
1491
|
|
|
1448
1492
|
const result = builder.buildComponent({ node });
|
|
1449
1493
|
|
|
1450
1494
|
expect(result).not.toBeNull();
|
|
1451
|
-
expect(typeof result).toBe(
|
|
1495
|
+
expect(typeof result).toBe('object');
|
|
1452
1496
|
});
|
|
1453
1497
|
|
|
1454
|
-
test(
|
|
1498
|
+
test('should skip node when if is false', () => {
|
|
1455
1499
|
const node: ComponentNode = {
|
|
1456
|
-
type:
|
|
1457
|
-
tag:
|
|
1500
|
+
type: 'node',
|
|
1501
|
+
tag: 'div',
|
|
1458
1502
|
if: false,
|
|
1459
|
-
children: [
|
|
1503
|
+
children: ['Hidden content'],
|
|
1460
1504
|
} as any;
|
|
1461
1505
|
|
|
1462
1506
|
const result = builder.buildComponent({ node });
|
|
@@ -1464,11 +1508,11 @@ describe("ComponentBuilder", () => {
|
|
|
1464
1508
|
expect(result).toBeNull();
|
|
1465
1509
|
});
|
|
1466
1510
|
|
|
1467
|
-
test(
|
|
1511
|
+
test('should render node when if property is absent', () => {
|
|
1468
1512
|
const node: ComponentNode = {
|
|
1469
|
-
type:
|
|
1470
|
-
tag:
|
|
1471
|
-
children: [
|
|
1513
|
+
type: 'node',
|
|
1514
|
+
tag: 'div',
|
|
1515
|
+
children: ['Default visible'],
|
|
1472
1516
|
};
|
|
1473
1517
|
|
|
1474
1518
|
const result = builder.buildComponent({ node });
|
|
@@ -1476,41 +1520,41 @@ describe("ComponentBuilder", () => {
|
|
|
1476
1520
|
expect(result).not.toBeNull();
|
|
1477
1521
|
});
|
|
1478
1522
|
|
|
1479
|
-
test(
|
|
1523
|
+
test('should resolve if mapping from component props', () => {
|
|
1480
1524
|
const node: ComponentNode = {
|
|
1481
|
-
type:
|
|
1482
|
-
tag:
|
|
1525
|
+
type: 'node',
|
|
1526
|
+
tag: 'div',
|
|
1483
1527
|
if: {
|
|
1484
1528
|
_mapping: true,
|
|
1485
|
-
prop:
|
|
1486
|
-
values: {
|
|
1529
|
+
prop: 'showBanner',
|
|
1530
|
+
values: { true: true, false: false },
|
|
1487
1531
|
},
|
|
1488
|
-
children: [
|
|
1532
|
+
children: ['Conditional banner'],
|
|
1489
1533
|
} as any;
|
|
1490
1534
|
|
|
1491
1535
|
// When prop is 'true', should render
|
|
1492
1536
|
const resultVisible = builder.buildComponent({
|
|
1493
1537
|
node,
|
|
1494
|
-
componentResolvedProps: { showBanner:
|
|
1538
|
+
componentResolvedProps: { showBanner: 'true' },
|
|
1495
1539
|
});
|
|
1496
1540
|
expect(resultVisible).not.toBeNull();
|
|
1497
1541
|
|
|
1498
1542
|
// When prop is 'false', should skip
|
|
1499
1543
|
const resultHidden = builder.buildComponent({
|
|
1500
1544
|
node,
|
|
1501
|
-
componentResolvedProps: { showBanner:
|
|
1545
|
+
componentResolvedProps: { showBanner: 'false' },
|
|
1502
1546
|
});
|
|
1503
1547
|
expect(resultHidden).toBeNull();
|
|
1504
1548
|
});
|
|
1505
1549
|
|
|
1506
|
-
test(
|
|
1550
|
+
test('should skip children when parent if is false', () => {
|
|
1507
1551
|
const node: ComponentNode = {
|
|
1508
|
-
type:
|
|
1509
|
-
tag:
|
|
1552
|
+
type: 'node',
|
|
1553
|
+
tag: 'div',
|
|
1510
1554
|
if: false,
|
|
1511
1555
|
children: [
|
|
1512
|
-
{ type:
|
|
1513
|
-
{ type:
|
|
1556
|
+
{ type: 'node', tag: 'span', children: ['Child 1'] },
|
|
1557
|
+
{ type: 'node', tag: 'span', children: ['Child 2'] },
|
|
1514
1558
|
],
|
|
1515
1559
|
} as any;
|
|
1516
1560
|
|
|
@@ -1519,16 +1563,16 @@ describe("ComponentBuilder", () => {
|
|
|
1519
1563
|
expect(result).toBeNull();
|
|
1520
1564
|
});
|
|
1521
1565
|
|
|
1522
|
-
test(
|
|
1523
|
-
componentRegistry.register(
|
|
1566
|
+
test('should handle if with component nodes', () => {
|
|
1567
|
+
componentRegistry.register('TestComponent', {
|
|
1524
1568
|
component: {
|
|
1525
|
-
structure: { type:
|
|
1569
|
+
structure: { type: 'node', tag: 'div', children: ['Component content'] },
|
|
1526
1570
|
},
|
|
1527
1571
|
} as any);
|
|
1528
1572
|
|
|
1529
1573
|
const node: ComponentNode = {
|
|
1530
|
-
type:
|
|
1531
|
-
component:
|
|
1574
|
+
type: 'component',
|
|
1575
|
+
component: 'TestComponent',
|
|
1532
1576
|
if: false,
|
|
1533
1577
|
} as any;
|
|
1534
1578
|
|
|
@@ -1537,10 +1581,10 @@ describe("ComponentBuilder", () => {
|
|
|
1537
1581
|
expect(result).toBeNull();
|
|
1538
1582
|
});
|
|
1539
1583
|
|
|
1540
|
-
test(
|
|
1584
|
+
test('should handle if with embed nodes', () => {
|
|
1541
1585
|
const node: ComponentNode = {
|
|
1542
|
-
type:
|
|
1543
|
-
html:
|
|
1586
|
+
type: 'embed',
|
|
1587
|
+
html: '<svg></svg>',
|
|
1544
1588
|
if: false,
|
|
1545
1589
|
} as any;
|
|
1546
1590
|
|
|
@@ -1549,12 +1593,12 @@ describe("ComponentBuilder", () => {
|
|
|
1549
1593
|
expect(result).toBeNull();
|
|
1550
1594
|
});
|
|
1551
1595
|
|
|
1552
|
-
test(
|
|
1596
|
+
test('should handle if with link nodes', () => {
|
|
1553
1597
|
const node: ComponentNode = {
|
|
1554
|
-
type:
|
|
1555
|
-
href:
|
|
1598
|
+
type: 'link',
|
|
1599
|
+
href: '/page',
|
|
1556
1600
|
if: false,
|
|
1557
|
-
children: [
|
|
1601
|
+
children: ['Hidden link'],
|
|
1558
1602
|
} as any;
|
|
1559
1603
|
|
|
1560
1604
|
const result = builder.buildComponent({ node });
|
|
@@ -1562,16 +1606,16 @@ describe("ComponentBuilder", () => {
|
|
|
1562
1606
|
expect(result).toBeNull();
|
|
1563
1607
|
});
|
|
1564
1608
|
|
|
1565
|
-
test(
|
|
1609
|
+
test('treats unresolved string-template if as falsy', () => {
|
|
1566
1610
|
// When `if: "{{buttonText}}"` references an undefined prop,
|
|
1567
1611
|
// evaluateTemplate preserves the original template string (backward-compat).
|
|
1568
1612
|
// Without component/CMS/item context to resolve it, treat the node as hidden
|
|
1569
1613
|
// — matches user expectation when binding visibility to an unset prop.
|
|
1570
1614
|
const node: ComponentNode = {
|
|
1571
|
-
type:
|
|
1572
|
-
href:
|
|
1573
|
-
if:
|
|
1574
|
-
children: [
|
|
1615
|
+
type: 'link',
|
|
1616
|
+
href: '/page',
|
|
1617
|
+
if: '{{buttonText}}',
|
|
1618
|
+
children: ['Hidden link'],
|
|
1575
1619
|
} as any;
|
|
1576
1620
|
|
|
1577
1621
|
const result = builder.buildComponent({ node });
|
|
@@ -1579,9 +1623,9 @@ describe("ComponentBuilder", () => {
|
|
|
1579
1623
|
expect(result).toBeNull();
|
|
1580
1624
|
});
|
|
1581
1625
|
|
|
1582
|
-
test(
|
|
1626
|
+
test('should handle if with locale-list nodes', () => {
|
|
1583
1627
|
const node: ComponentNode = {
|
|
1584
|
-
type:
|
|
1628
|
+
type: 'locale-list',
|
|
1585
1629
|
if: false,
|
|
1586
1630
|
} as any;
|
|
1587
1631
|
|
|
@@ -1590,29 +1634,29 @@ describe("ComponentBuilder", () => {
|
|
|
1590
1634
|
expect(result).toBeNull();
|
|
1591
1635
|
});
|
|
1592
1636
|
|
|
1593
|
-
test(
|
|
1637
|
+
test('should render when mapping prop value not found (default true)', () => {
|
|
1594
1638
|
const node: ComponentNode = {
|
|
1595
|
-
type:
|
|
1596
|
-
tag:
|
|
1639
|
+
type: 'node',
|
|
1640
|
+
tag: 'div',
|
|
1597
1641
|
if: {
|
|
1598
1642
|
_mapping: true,
|
|
1599
|
-
prop:
|
|
1600
|
-
values: {
|
|
1643
|
+
prop: 'status',
|
|
1644
|
+
values: { active: true, inactive: false },
|
|
1601
1645
|
},
|
|
1602
|
-
children: [
|
|
1646
|
+
children: ['Content'],
|
|
1603
1647
|
} as any;
|
|
1604
1648
|
|
|
1605
1649
|
// 'unknown' is not in the values map, should default to true
|
|
1606
1650
|
const result = builder.buildComponent({
|
|
1607
1651
|
node,
|
|
1608
|
-
componentResolvedProps: { status:
|
|
1652
|
+
componentResolvedProps: { status: 'unknown' },
|
|
1609
1653
|
});
|
|
1610
1654
|
|
|
1611
1655
|
expect(result).not.toBeNull();
|
|
1612
1656
|
});
|
|
1613
1657
|
});
|
|
1614
1658
|
|
|
1615
|
-
describe(
|
|
1659
|
+
describe('Edge Case 7: List Paths Tracking (Collection Mode)', () => {
|
|
1616
1660
|
const createMockCMSItems = (count: number): CMSItem[] => {
|
|
1617
1661
|
return Array.from({ length: count }, (_, i) => ({
|
|
1618
1662
|
_id: `item-${i}`,
|
|
@@ -1621,13 +1665,13 @@ describe("ComponentBuilder", () => {
|
|
|
1621
1665
|
}));
|
|
1622
1666
|
};
|
|
1623
1667
|
|
|
1624
|
-
test(
|
|
1668
|
+
test('should pass listPaths through buildChildren', () => {
|
|
1625
1669
|
// Create a simple list with items
|
|
1626
1670
|
const node: ListNode = {
|
|
1627
1671
|
type: NODE_TYPE.LIST,
|
|
1628
1672
|
sourceType: 'collection',
|
|
1629
|
-
source:
|
|
1630
|
-
children: [{ type:
|
|
1673
|
+
source: 'posts',
|
|
1674
|
+
children: [{ type: 'node', tag: 'div', children: ['Item'] }],
|
|
1631
1675
|
};
|
|
1632
1676
|
|
|
1633
1677
|
const items = createMockCMSItems(2);
|
|
@@ -1643,18 +1687,18 @@ describe("ComponentBuilder", () => {
|
|
|
1643
1687
|
expect(element?.props?.children?.length).toBe(2);
|
|
1644
1688
|
});
|
|
1645
1689
|
|
|
1646
|
-
test(
|
|
1690
|
+
test('should track listPaths in nested lists', () => {
|
|
1647
1691
|
// Outer list with inner list
|
|
1648
1692
|
const outerNode: ListNode = {
|
|
1649
1693
|
type: NODE_TYPE.LIST,
|
|
1650
1694
|
sourceType: 'collection',
|
|
1651
|
-
source:
|
|
1695
|
+
source: 'categories',
|
|
1652
1696
|
children: [
|
|
1653
1697
|
{
|
|
1654
1698
|
type: NODE_TYPE.LIST,
|
|
1655
1699
|
sourceType: 'collection',
|
|
1656
|
-
source:
|
|
1657
|
-
children: [{ type:
|
|
1700
|
+
source: 'posts',
|
|
1701
|
+
children: [{ type: 'node', tag: 'span', children: ['Post'] }],
|
|
1658
1702
|
} as any,
|
|
1659
1703
|
],
|
|
1660
1704
|
};
|
|
@@ -1673,28 +1717,26 @@ describe("ComponentBuilder", () => {
|
|
|
1673
1717
|
});
|
|
1674
1718
|
});
|
|
1675
1719
|
|
|
1676
|
-
describe(
|
|
1677
|
-
test(
|
|
1720
|
+
describe('Edge Case 5: Slot Content Context Handling', () => {
|
|
1721
|
+
test('should not pass componentContext to slot content elements', () => {
|
|
1678
1722
|
// Register a component with a slot
|
|
1679
|
-
componentRegistry.register(
|
|
1680
|
-
type:
|
|
1723
|
+
componentRegistry.register('Container', {
|
|
1724
|
+
type: 'Container',
|
|
1681
1725
|
component: {
|
|
1682
1726
|
interface: {},
|
|
1683
1727
|
structure: {
|
|
1684
|
-
type:
|
|
1685
|
-
tag:
|
|
1686
|
-
children: [{ type:
|
|
1728
|
+
type: 'node',
|
|
1729
|
+
tag: 'div',
|
|
1730
|
+
children: [{ type: 'slot' }],
|
|
1687
1731
|
},
|
|
1688
1732
|
},
|
|
1689
1733
|
});
|
|
1690
1734
|
|
|
1691
1735
|
// Component instance with children (slot content)
|
|
1692
1736
|
const node: ComponentNode = {
|
|
1693
|
-
type:
|
|
1694
|
-
component:
|
|
1695
|
-
children: [
|
|
1696
|
-
{ type: "node", tag: "span", children: ["Slot content"] },
|
|
1697
|
-
],
|
|
1737
|
+
type: 'component',
|
|
1738
|
+
component: 'Container',
|
|
1739
|
+
children: [{ type: 'node', tag: 'span', children: ['Slot content'] }],
|
|
1698
1740
|
};
|
|
1699
1741
|
|
|
1700
1742
|
const result = builder.buildComponent({
|
|
@@ -1704,29 +1746,29 @@ describe("ComponentBuilder", () => {
|
|
|
1704
1746
|
|
|
1705
1747
|
// Should render successfully
|
|
1706
1748
|
expect(result).not.toBeNull();
|
|
1707
|
-
expect(typeof result).toBe(
|
|
1749
|
+
expect(typeof result).toBe('object');
|
|
1708
1750
|
});
|
|
1709
1751
|
|
|
1710
|
-
test(
|
|
1752
|
+
test('should mark slot content with _isSlotContent flag', () => {
|
|
1711
1753
|
// This tests that slot content is marked, which affects context handling
|
|
1712
|
-
componentRegistry.register(
|
|
1713
|
-
type:
|
|
1754
|
+
componentRegistry.register('Wrapper', {
|
|
1755
|
+
type: 'Wrapper',
|
|
1714
1756
|
component: {
|
|
1715
1757
|
interface: {},
|
|
1716
1758
|
structure: {
|
|
1717
|
-
type:
|
|
1718
|
-
tag:
|
|
1719
|
-
children: [{ type:
|
|
1759
|
+
type: 'node',
|
|
1760
|
+
tag: 'section',
|
|
1761
|
+
children: [{ type: 'slot' }],
|
|
1720
1762
|
},
|
|
1721
1763
|
},
|
|
1722
1764
|
});
|
|
1723
1765
|
|
|
1724
1766
|
const node: ComponentNode = {
|
|
1725
|
-
type:
|
|
1726
|
-
component:
|
|
1767
|
+
type: 'component',
|
|
1768
|
+
component: 'Wrapper',
|
|
1727
1769
|
children: [
|
|
1728
|
-
{ type:
|
|
1729
|
-
{ type:
|
|
1770
|
+
{ type: 'node', tag: 'div', children: ['Child 1'] },
|
|
1771
|
+
{ type: 'node', tag: 'div', children: ['Child 2'] },
|
|
1730
1772
|
],
|
|
1731
1773
|
};
|
|
1732
1774
|
|
|
@@ -1738,27 +1780,24 @@ describe("ComponentBuilder", () => {
|
|
|
1738
1780
|
expect(element?.props?.children).toBeDefined();
|
|
1739
1781
|
});
|
|
1740
1782
|
|
|
1741
|
-
test(
|
|
1783
|
+
test('should preserve componentContext for component structure elements', () => {
|
|
1742
1784
|
// Register a component
|
|
1743
|
-
componentRegistry.register(
|
|
1744
|
-
type:
|
|
1785
|
+
componentRegistry.register('Card', {
|
|
1786
|
+
type: 'Card',
|
|
1745
1787
|
component: {
|
|
1746
1788
|
interface: {},
|
|
1747
1789
|
structure: {
|
|
1748
|
-
type:
|
|
1749
|
-
tag:
|
|
1750
|
-
children: [
|
|
1751
|
-
{ type: "node", tag: "h2", children: ["Header"] },
|
|
1752
|
-
{ type: "slot" },
|
|
1753
|
-
],
|
|
1790
|
+
type: 'node',
|
|
1791
|
+
tag: 'article',
|
|
1792
|
+
children: [{ type: 'node', tag: 'h2', children: ['Header'] }, { type: 'slot' }],
|
|
1754
1793
|
},
|
|
1755
1794
|
},
|
|
1756
1795
|
});
|
|
1757
1796
|
|
|
1758
1797
|
const node: ComponentNode = {
|
|
1759
|
-
type:
|
|
1760
|
-
component:
|
|
1761
|
-
children: [{ type:
|
|
1798
|
+
type: 'component',
|
|
1799
|
+
component: 'Card',
|
|
1800
|
+
children: [{ type: 'node', tag: 'p', children: ['Content'] }],
|
|
1762
1801
|
};
|
|
1763
1802
|
|
|
1764
1803
|
const result = builder.buildComponent({ node });
|
|
@@ -1767,5 +1806,69 @@ describe("ComponentBuilder", () => {
|
|
|
1767
1806
|
expect(result).not.toBeNull();
|
|
1768
1807
|
});
|
|
1769
1808
|
});
|
|
1770
|
-
});
|
|
1771
1809
|
|
|
1810
|
+
// Islands and Custom nodes are black boxes meno-core can't run — the canvas
|
|
1811
|
+
// shows a placeholder. The wrapper still wires data-element-path via a ref so a
|
|
1812
|
+
// click on the placeholder selects the node like any regular element.
|
|
1813
|
+
describe('buildComponent - Island / Custom node selection', () => {
|
|
1814
|
+
// Drive the ref the same way React would on mount and capture what it stamps.
|
|
1815
|
+
const captureRefAttrs = (ref: unknown): Record<string, string> => {
|
|
1816
|
+
const attrs: Record<string, string> = {};
|
|
1817
|
+
const fakeEl = {
|
|
1818
|
+
setAttribute: (name: string, value: string) => {
|
|
1819
|
+
attrs[name] = value;
|
|
1820
|
+
},
|
|
1821
|
+
style: { setProperty: () => {} },
|
|
1822
|
+
} as unknown as HTMLElement;
|
|
1823
|
+
(ref as (el: HTMLElement | null) => void)(fakeEl);
|
|
1824
|
+
return attrs;
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
test('island wrapper carries data-island, display:contents, and a selection ref', () => {
|
|
1828
|
+
const node: ComponentNode = { type: 'island', src: 'Counter.tsx', client: { directive: 'load' } } as any;
|
|
1829
|
+
const result = builder.buildComponent({ node, elementPath: [0, 2] }) as any;
|
|
1830
|
+
|
|
1831
|
+
expect(result.type).toBe('div');
|
|
1832
|
+
expect(result.props['data-island']).toBe('Counter.tsx');
|
|
1833
|
+
expect(result.props.style.display).toBe('contents');
|
|
1834
|
+
expect(typeof (result.ref ?? result.props.ref)).toBe('function');
|
|
1835
|
+
|
|
1836
|
+
const attrs = captureRefAttrs(result.ref ?? result.props.ref);
|
|
1837
|
+
expect(attrs['data-element-path']).toBe('0,2');
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
test('empty island renders its basename as the placeholder', () => {
|
|
1841
|
+
const node: ComponentNode = { type: 'island', src: 'widgets/Chart.vue' } as any;
|
|
1842
|
+
const result = builder.buildComponent({ node }) as any;
|
|
1843
|
+
// inner is the placeholder span carrying the basename.
|
|
1844
|
+
expect(result.props.children.props.children).toBe('Chart');
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
test('custom wrapper carries data-custom and a selection ref', () => {
|
|
1848
|
+
const node: ComponentNode = { type: 'custom', src: 'Fancy.astro' } as any;
|
|
1849
|
+
const result = builder.buildComponent({ node, elementPath: [0, 1] }) as any;
|
|
1850
|
+
|
|
1851
|
+
expect(result.type).toBe('div');
|
|
1852
|
+
expect(result.props['data-custom']).toBe('Fancy.astro');
|
|
1853
|
+
expect(result.props.style.display).toBe('contents');
|
|
1854
|
+
|
|
1855
|
+
const attrs = captureRefAttrs(result.ref ?? result.props.ref);
|
|
1856
|
+
expect(attrs['data-element-path']).toBe('0,1');
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
test('island ref stamps CMS-list context when present', () => {
|
|
1860
|
+
const node: ComponentNode = { type: 'island', src: 'Counter.tsx' } as any;
|
|
1861
|
+
const result = builder.buildComponent({
|
|
1862
|
+
node,
|
|
1863
|
+
elementPath: [0, 0],
|
|
1864
|
+
cmsItemIndexPath: [1],
|
|
1865
|
+
cmsListPaths: [[0]],
|
|
1866
|
+
}) as any;
|
|
1867
|
+
|
|
1868
|
+
const attrs = captureRefAttrs(result.ref ?? result.props.ref);
|
|
1869
|
+
expect(attrs['data-element-path']).toBe('0,0');
|
|
1870
|
+
expect(attrs['data-cms-item-index']).toBe('1');
|
|
1871
|
+
expect(attrs['data-cms-context']).toContain('"itemIndexPath":[1]');
|
|
1872
|
+
});
|
|
1873
|
+
});
|
|
1874
|
+
});
|