@designcrowd/library.brand-page 6.2.11 → 6.2.12
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/lib/src/{CreatedOnBrandCrowd-de-de-DDcraD5S.js → CreatedOnBrandCrowd-de-de-BtVKllNQ.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DtOeaisf.js → CreatedOnBrandCrowd-en-us-CX5V0v8u.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BZpmJPew.js → CreatedOnBrandCrowd-es-es-CbA02PCL.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CqRwC4e7.js → CreatedOnBrandCrowd-fr-ca-BQAKruBF.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CqRwC4e7.js → CreatedOnBrandCrowd-fr-fr-BQAKruBF.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DId9Dfjh.js → CreatedOnBrandCrowd-pt-br-Yk_yzB3s.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DId9Dfjh.js → CreatedOnBrandCrowd-pt-pt-Yk_yzB3s.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-BTDfmOcm.js → CreatedOnDcom-de-de-DJZQbPSD.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-CeAu9Ayx.js → CreatedOnDcom-en-us-BJvNAa4l.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-WCv0K41H.js → CreatedOnDcom-es-es-CUwB1FRY.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BlfevQDY.js → CreatedOnDcom-fr-ca-CK54U4oh.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-Cvt7MHPZ.js → CreatedOnDcom-fr-fr-DuUNBGrM.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DipZuylv.js → CreatedOnDcom-pt-br-BfODpFNV.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BCw4lcKN.js → CreatedOnDcom-pt-pt-Cf6u2GCg.js} +1 -1
- package/lib/src/brand-page/__fixtures__/production-text-style-resolver.fixture.d.ts +16 -0
- package/lib/src/brand-page/__fixtures__/production-text-style-resolver.fixture.js +35 -0
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.d.ts +3 -0
- package/lib/src/brand-page/components/contact-form/ContactForm.mixin.js +19 -13
- package/lib/src/brand-page/components/mailing-list/MailingList.mixin.d.ts +3 -1
- package/lib/src/brand-page/components/mailing-list/MailingList.mixin.js +17 -2
- package/lib/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.d.ts +11 -3
- package/lib/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.js +35 -1
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +8 -2
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +27 -3
- package/lib/src/brand-page/components/page/Page.mixin.d.ts +5 -2
- package/lib/src/brand-page/components/page/Page.mixin.js +33 -27
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +7 -2
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +21 -1
- package/lib/src/brand-page/components/payment/Payment.mixin.d.ts +5 -2
- package/lib/src/brand-page/components/payment/Payment.mixin.js +22 -2
- package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.d.ts +6 -2
- package/lib/src/brand-page/components/payment/support-me/SupportMe.mixin.js +32 -1
- package/lib/src/brand-page/components/social-links/SocialLinks.stories.js +0 -3
- package/lib/src/brand-page/components/testimonial-section/__fixtures__/data.js +0 -3
- package/lib/src/brand-page/components/text/Text.stories.js +0 -17
- package/lib/src/brand-page/constants/font.constants.d.ts +62 -0
- package/lib/src/brand-page/constants/font.constants.js +35 -0
- package/lib/src/brand-page/models/index.d.ts +23 -24
- package/lib/src/brand-page/models/index.js +20 -7
- package/lib/src/brand-page/models/navigation-styles.model.d.ts +1 -1
- package/lib/src/brand-page/models/navigation-styles.model.js +17 -8
- package/lib/src/brand-page/models/payment.model.d.ts +11 -6
- package/lib/src/brand-page/models/styles.model.d.ts +53 -7
- package/lib/src/brand-page/models/styles.model.js +115 -27
- package/lib/src/brand-page/utils/color-palette.util.d.ts +43 -23
- package/lib/src/brand-page/utils/color-palette.util.js +65 -27
- package/lib/src/brand-page/utils/color.util.d.ts +13 -9
- package/lib/src/brand-page/utils/color.util.js +56 -15
- package/lib/src/brand-page/utils/container-text-color.util.d.ts +46 -13
- package/lib/src/brand-page/utils/container-text-color.util.js +321 -41
- package/lib/src/brand-page/utils/index.d.ts +1 -0
- package/lib/src/brand-page/utils/index.js +1 -0
- package/lib/src/brand-page/utils/migration/migration-20.js +6 -2
- package/lib/src/brand-page/utils/migration/migration-21.js +34 -6
- package/lib/src/brand-page/utils/migration/migration-22.d.ts +99 -0
- package/lib/src/brand-page/utils/migration/migration-22.js +909 -0
- package/lib/src/brand-page/utils/migration.util.js +17 -0
- package/lib/src/brand-page/utils/model.util.d.ts +4 -0
- package/lib/src/brand-page/utils/model.util.js +4 -0
- package/lib/src/brand-page/utils/personalisation.util.js +9 -1
- package/lib/src/brand-page/utils/rte-role-lift.util.d.ts +72 -0
- package/lib/src/brand-page/utils/rte-role-lift.util.js +275 -0
- package/lib/src/brand-page/utils/style.util.d.ts +5 -1
- package/lib/src/brand-page/utils/style.util.js +34 -11
- package/lib/src/brand-page/utils/text-role-css.util.d.ts +19 -0
- package/lib/src/brand-page/utils/text-role-css.util.js +186 -0
- package/lib/src/brand-page/utils/text-role-reconnect.util.d.ts +90 -0
- package/lib/src/brand-page/utils/text-role-reconnect.util.js +434 -0
- package/lib/src/brand-page/utils/text-style.util.d.ts +53 -23
- package/lib/src/brand-page/utils/text-style.util.js +122 -22
- package/lib/src/brand-page/utils/view-model.util.d.ts +9 -1
- package/lib/src/brand-page/utils/view-model.util.js +124 -111
- package/lib/src/brand-page/view-models.d.ts +1 -0
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +4 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +13 -2
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +4 -4
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +4 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +0 -10
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +0 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +23 -138
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +0 -28
- package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +4 -1
- package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +5 -2
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +4 -1
- package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +4 -1
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +4 -1
- package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.d.ts +1 -0
- package/lib/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.js +1 -0
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +1 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.d.ts +16 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.js +61 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.d.ts +11 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.js +236 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +2 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +25 -6
- package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +17 -4
- package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +204 -17
- package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +29 -31
- package/lib/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.d.ts +20 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.js +60 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +2 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +22 -6
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +10 -4
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +319 -275
- package/lib/src/brand-page-maker/constants/config-settings-constant.js +1 -5
- package/lib/src/brand-page-maker/convertors/text-css-style-options.convertor.js +6 -2
- package/lib/src/brand-page-maker/convertors/text-size-value.convertor.d.ts +1 -1
- package/lib/src/brand-page-maker/convertors/text-size-value.convertor.js +3 -1
- package/lib/src/brand-page-maker/default-getters/payment-text-and-border-color.default-getter.js +7 -14
- package/lib/src/brand-page-maker/default-getters/text-color.default-getter.js +6 -13
- package/lib/src/brand-page-maker/models/content-settings.d.ts +8 -0
- package/lib/src/brand-page-maker/models/content-settings.js +112 -81
- package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.d.ts +2 -2
- package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.js +5 -2
- package/lib/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.js +4 -3
- package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1257 -117
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +6 -1
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +17 -9
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +59 -5
- package/lib/src/brand-page-maker/stores/history/history-store.js +22 -2
- package/lib/src/brand-page-maker/utils/apply-color-strategy.js +26 -8
- package/lib/src/brand-page-maker/utils/index.d.ts +2 -1
- package/lib/src/brand-page-maker/utils/index.js +2 -1
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +117 -15
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +435 -281
- package/lib/src/brand-page-maker/utils/rte-authoring.util.d.ts +4 -0
- package/lib/src/brand-page-maker/utils/rte-authoring.util.js +101 -0
- package/lib/src/brand-page-maker/utils/rte-clipboard.util.d.ts +126 -0
- package/lib/src/brand-page-maker/utils/rte-clipboard.util.js +307 -0
- package/lib/src/brand-page-maker/utils/rte-sanitize.util.js +13 -3
- package/lib/src/{bundled-translations-zfiA5Tsw.js → bundled-translations-yGrvPE5i.js} +17 -17
- package/lib/src/{bundled-translations.de-DE-ChsdSyAP.js → bundled-translations.de-DE-L4Znf8Hs.js} +17 -17
- package/lib/src/{bundled-translations.es-ES-DflsWQK0.js → bundled-translations.es-ES-D6iticrK.js} +15 -15
- package/lib/src/{bundled-translations.fr-CA-fpDRuZ52.js → bundled-translations.fr-CA-BgAVE-9k.js} +25 -25
- package/lib/src/{bundled-translations.fr-FR-Bub73Zt-.js → bundled-translations.fr-FR-B8GbyqxQ.js} +17 -17
- package/lib/src/{bundled-translations.pt-BR-BlnWE9TB.js → bundled-translations.pt-BR-DjPU3Db7.js} +25 -25
- package/lib/src/{bundled-translations.pt-PT-DmzXlnaS.js → bundled-translations.pt-PT-BaepaDpr.js} +23 -23
- package/lib/src/bundles/bundled-translations.de-DE.json +21 -2
- package/lib/src/bundles/bundled-translations.es-ES.json +21 -2
- package/lib/src/bundles/bundled-translations.fr-CA.json +21 -2
- package/lib/src/bundles/bundled-translations.fr-FR.json +21 -2
- package/lib/src/bundles/bundled-translations.json +21 -2
- package/lib/src/bundles/bundled-translations.pt-BR.json +21 -2
- package/lib/src/bundles/bundled-translations.pt-PT.json +21 -2
- package/lib/src/clients/ai-assistant-client.d.ts +1 -0
- package/lib/src/composables/useTranslate.d.ts +1 -1
- package/lib/src/composables/useTranslate.js +1 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +303 -207
- package/lib/src/css/library.brand-page-brandPage.min.css +303 -207
- package/lib/src/css/library.brand-page-designCom.min.css +303 -207
- package/lib/src/css/library.brand-page-preview.min.css +303 -207
- package/lib/src/index.d.ts +2 -1
- package/lib/src/index.mjs +195 -191
- package/lib/src/{lib-1AqtEmvQ.js → lib-kGRlD3_P.js} +36855 -33893
- package/lib/src/lib.js +2 -1
- package/lib/src/metadata-builder/builders/footer-builder.js +4 -5
- package/lib/src/metadata-builder/builders/global-role-propagation.d.ts +58 -0
- package/lib/src/metadata-builder/builders/global-role-propagation.js +246 -0
- package/lib/src/metadata-builder/builders/global-styles-builder.d.ts +1 -0
- package/lib/src/metadata-builder/builders/global-styles-builder.js +116 -45
- package/lib/src/metadata-builder/builders/header-builder.js +13 -4
- package/lib/src/metadata-builder/builders/section-builders/about-section-builder.js +3 -3
- package/lib/src/metadata-builder/builders/section-builders/banner-section-builder.js +3 -3
- package/lib/src/metadata-builder/builders/section-builders/booking-section-builder.js +3 -3
- package/lib/src/metadata-builder/builders/section-builders/contact-form-builder.js +4 -4
- package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +3 -3
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-builder.js +2 -2
- package/lib/src/metadata-builder/builders/section-builders/mailing-list-section-builder.js +2 -2
- package/lib/src/metadata-builder/builders/section-builders/profile-section-builder.js +4 -4
- package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +3 -3
- package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +8 -2
- package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +2 -2
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +60 -29
- package/lib/src/metadata-builder/builders/section-field-maps.js +1 -0
- package/lib/src/metadata-builder/builders/section-field-utils.d.ts +48 -19
- package/lib/src/metadata-builder/builders/section-field-utils.js +128 -52
- package/lib/src/metadata-builder/builders/section-rules/global-styles.rules.d.ts +4 -2
- package/lib/src/metadata-builder/builders/section-rules/global-styles.rules.js +28 -12
- package/lib/src/metadata-builder/builders/section-rules/social-links.rules.js +5 -1
- package/lib/src/metadata-builder/helpers/rte.helpers.d.ts +1 -1
- package/lib/src/metadata-builder/helpers/rte.helpers.js +97 -40
- package/lib/src/metadata-builder/metadata-executor.js +23 -1
- package/lib/src/metadata-builder/models.d.ts +2 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +18 -19
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +18 -1
- package/lib/src/metadata-builder/sanitizers/rte-schema-check.js +1 -1
- package/lib/src/metadata-builder/sanitizers/sanitizer-rules.d.ts +0 -1
- package/lib/src/metadata-builder/sanitizers/sanitizer-rules.js +5 -8
- package/lib/src/metadata-builder/sanitizers/section-args-validator.d.ts +7 -16
- package/lib/src/metadata-builder/sanitizers/section-args-validator.js +95 -22
- package/lib/src/shared/color-palette/section-color-matches.js +9 -25
- package/lib/src/shared/color-strategies/color-strategies.d.ts +15 -0
- package/lib/src/shared/color-strategies/color-strategies.js +31 -0
- package/lib/src/shared/color-strategies/rte-color.d.ts +8 -0
- package/lib/src/shared/color-strategies/rte-color.js +16 -0
- package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +23 -0
- package/lib/src/shared/color-strategies/rte-field-pairs.js +28 -0
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +9 -135
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +1 -27
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +12 -2
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +7 -0
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +75 -6
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.js +18 -7
- package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +6 -1
- package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.js +0 -7
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +1 -0
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +8 -4
- package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1255 -26
- package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +203 -59
- package/lib/src/shared/content-settings/store/content-settings-store.d.ts +6 -0
- package/lib/src/shared/content-settings/store/content-settings-store.js +105 -8
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +5 -1
- package/lib/src/shared/navigation-style-settings/store/index.d.ts +7 -3
- package/lib/src/shared/navigation-style-settings/store/index.js +40 -15
- package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +1 -0
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +17 -5
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +70 -13
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +10 -2
- package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -1
- package/lib/src/shared/page-settings/store/page-settings-store.js +17 -0
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +4 -1
- package/lib/src/shared/rte-extensions/AutoLink.d.ts +17 -0
- package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/AutoLink.js +67 -51
- package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.js +4 -1
- package/lib/src/shared/rte-extensions/TextEffects.d.ts +28 -0
- package/lib/src/shared/rte-extensions/TextEffects.js +96 -0
- package/lib/src/shared/rte-extensions/ViewDomParser.d.ts +2 -0
- package/lib/src/shared/rte-extensions/ViewDomParser.js +21 -0
- package/lib/src/shared/rte-extensions.util.d.ts +25 -0
- package/lib/src/{brand-page-maker/composables/rich-text-editor/getRteExtensions.js → shared/rte-extensions.util.js} +62 -7
- package/lib/src/shared/rte-link.util.d.ts +1 -0
- package/lib/src/shared/rte-link.util.js +1 -0
- package/lib/src/shared/rte-render.util.d.ts +112 -0
- package/lib/src/shared/rte-render.util.js +255 -0
- package/lib/src/shared/rte-shape.util.d.ts +6 -0
- package/lib/src/shared/rte-shape.util.js +18 -0
- package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +2 -1
- package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +1 -0
- package/lib/src/shared/side-bar/store/side-bar-store.d.ts +2 -0
- package/lib/src/shared/side-bar/store/side-bar-store.js +9 -0
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +155 -15
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.js +7 -1
- package/lib/src/shared/style-settings/store/style-settings-store.d.ts +1 -9
- package/lib/src/shared/style-settings/store/style-settings-store.js +3 -33
- package/lib/src/shared/style-settings/text-style-size-order.d.ts +2 -0
- package/lib/src/shared/style-settings/text-style-size-order.js +13 -0
- package/lib/src/shared/text-role-typography.util.d.ts +64 -0
- package/lib/src/shared/text-role-typography.util.js +183 -0
- package/lib/src/utils/minify-brand-page-metadata.d.ts +16 -1
- package/lib/src/utils/minify-brand-page-metadata.js +31 -9
- package/package.json +1 -1
- package/src/brand-page/__fixtures__/migration-22-v21/edited-brandsite-sections.v21.json +982 -0
- package/src/brand-page/__fixtures__/migration-22-v21/new-brandcontact-every-section.v21.json +1231 -0
- package/src/brand-page/__fixtures__/migration-22-v21/new-brandpage-every-section.v21.json +1231 -0
- package/src/brand-page/__fixtures__/migration-22-v21/new-brandsite-every-section.v21.json +1243 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandcontact.v21.json +755 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-general-link.v21.json +527 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-links.v21.json +755 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandpage-social-links.v21.json +514 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-about.v21.json +459 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-footer.v21.json +469 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-header-banner.v21.json +481 -0
- package/src/brand-page/__fixtures__/migration-22-v21/real-brandsite-sections.v21.json +800 -0
- package/src/brand-page/__fixtures__/production-text-style-resolver.fixture.ts +52 -0
- package/src/brand-page/components/about-section/AboutSection.vue.test.ts +0 -10
- package/src/brand-page/components/contact-form/ContactForm.mixin.test.ts +127 -0
- package/src/brand-page/components/contact-form/ContactForm.mixin.ts +31 -14
- package/src/brand-page/components/contact-form/ContactForm.vue +26 -12
- package/src/brand-page/components/contact-form/ContactForm.vue.test.ts +60 -0
- package/src/brand-page/components/mailing-list/MailingList.mixin.test.ts +92 -0
- package/src/brand-page/components/mailing-list/MailingList.mixin.ts +28 -3
- package/src/brand-page/components/mailing-list/MailingList.vue +9 -4
- package/src/brand-page/components/mailing-list/MailingList.vue.test.ts +63 -0
- package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.mixin.ts +51 -1
- package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.test.ts +114 -0
- package/src/brand-page/components/navigation/button-carousel/ButtonCarouselNavigation.vue +8 -2
- package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.test.ts +182 -0
- package/src/brand-page/components/navigation/navigation-carousel/NavigationCarousel.vue +59 -12
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +40 -3
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +15 -7
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +133 -3
- package/src/brand-page/components/page/Page.mixin.ts +41 -34
- package/src/brand-page/components/page/Page.vue +17 -6
- package/src/brand-page/components/page/Page.vue.test.ts +91 -1
- package/src/brand-page/components/page-footer/PageFooter.mixin.ts +32 -2
- package/src/brand-page/components/page-footer/PageFooter.test.ts +79 -0
- package/src/brand-page/components/page-footer/PageFooter.vue +12 -4
- package/src/brand-page/components/payment/Payment.mixin.ts +34 -2
- package/src/brand-page/components/payment/Payment.test.ts +188 -0
- package/src/brand-page/components/payment/Payment.vue +12 -5
- package/src/brand-page/components/payment/RequestPaymentStripe.vue +1 -0
- package/src/brand-page/components/payment/support-me/SupportMe.mixin.ts +49 -2
- package/src/brand-page/components/payment/support-me/SupportMe.test.ts +100 -0
- package/src/brand-page/components/payment/support-me/SupportMe.vue +7 -1
- package/src/brand-page/components/payment/support-me/SupportMe.vue.test.ts +66 -0
- package/src/brand-page/components/save-contact/SaveContactWidget.test.ts +100 -0
- package/src/brand-page/components/save-contact/SaveContactWidget.vue +6 -3
- package/src/brand-page/components/shop/PaymentModal.vue +2 -6
- package/src/brand-page/components/shop/PaymentShippingAddress.vue +6 -0
- package/src/brand-page/components/shop/Shop.vue +15 -2
- package/src/brand-page/components/shop/Shop.vue.test.ts +74 -2
- package/src/brand-page/components/social-links/SocialLinks.stories.ts +0 -3
- package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +0 -3
- package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +0 -3
- package/src/brand-page/components/text/Text.stories.ts +0 -17
- package/src/brand-page/components/text-wrapper/TextWrapper.parity.test.ts +136 -0
- package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +358 -0
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +82 -22
- package/src/brand-page/constants/font.constants.ts +37 -0
- package/src/brand-page/models/footer-icon-color.test.ts +22 -0
- package/src/brand-page/models/index.ts +50 -28
- package/src/brand-page/models/navigation-styles.model.test.ts +77 -1
- package/src/brand-page/models/navigation-styles.model.ts +26 -9
- package/src/brand-page/models/payment.model.ts +15 -6
- package/src/brand-page/models/styles.model.test.ts +104 -0
- package/src/brand-page/models/styles.model.ts +181 -47
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +619 -115
- package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +150 -9
- package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +300 -18
- package/src/brand-page/utils/color-palette.util.test.ts +126 -1
- package/src/brand-page/utils/color-palette.util.ts +92 -26
- package/src/brand-page/utils/color.util.test.ts +62 -29
- package/src/brand-page/utils/color.util.ts +77 -14
- package/src/brand-page/utils/container-text-color.util.test.ts +672 -26
- package/src/brand-page/utils/container-text-color.util.ts +472 -46
- package/src/brand-page/utils/index.ts +1 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/edited-brandsite-sections.v22.json +1191 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandcontact-every-section.v22.json +2026 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandpage-every-section.v22.json +2017 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/new-brandsite-every-section.v22.json +2031 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandcontact.v22.json +1145 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-general-link.v22.json +645 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-links.v22.json +1136 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandpage-social-links.v22.json +662 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-about.v22.json +638 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-footer.v22.json +610 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-header-banner.v22.json +660 -0
- package/src/brand-page/utils/migration/__snapshots__/migration-22-output/real-brandsite-sections.v22.json +1132 -0
- package/src/brand-page/utils/migration/migration-01.ts +293 -237
- package/src/brand-page/utils/migration/migration-02.ts +8 -1
- package/src/brand-page/utils/migration/migration-20.ts +6 -3
- package/src/brand-page/utils/migration/migration-20_5.test.ts +5 -2
- package/src/brand-page/utils/migration/migration-21.test.ts +34 -0
- package/src/brand-page/utils/migration/migration-21.ts +46 -10
- package/src/brand-page/utils/migration/migration-22.fixed-input.test.ts +43 -0
- package/src/brand-page/utils/migration/migration-22.test.ts +4103 -0
- package/src/brand-page/utils/migration/migration-22.text-font-family.test.ts +72 -0
- package/src/brand-page/utils/migration/migration-22.ts +1336 -0
- package/src/brand-page/utils/migration.util.test.ts +146 -32
- package/src/brand-page/utils/migration.util.ts +18 -0
- package/src/brand-page/utils/model.util.ts +4 -0
- package/src/brand-page/utils/personalisation.util.test.ts +148 -3
- package/src/brand-page/utils/personalisation.util.ts +9 -1
- package/src/brand-page/utils/rte-role-lift.util.ts +409 -0
- package/src/brand-page/utils/style.util.test.ts +35 -2
- package/src/brand-page/utils/style.util.ts +45 -11
- package/src/brand-page/utils/text-role-css.util.test.ts +525 -0
- package/src/brand-page/utils/text-role-css.util.ts +321 -0
- package/src/brand-page/utils/text-role-reconnect.util.test.ts +604 -0
- package/src/brand-page/utils/text-role-reconnect.util.ts +727 -0
- package/src/brand-page/utils/text-style.util.test.ts +305 -0
- package/src/brand-page/utils/text-style.util.ts +198 -24
- package/src/brand-page/utils/view-model.page-data.test.ts +232 -0
- package/src/brand-page/utils/view-model.util.test.ts +954 -133
- package/src/brand-page/utils/view-model.util.ts +187 -139
- package/src/brand-page/view-models.ts +4 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.blank-site.test.ts +57 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +9 -5
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +0 -10
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +0 -1
- package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +0 -44
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +253 -200
- package/src/brand-page-maker/components/shop/AddOrEditProductContainer.vue +1 -0
- package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -0
- package/src/brand-page-maker/composables/ai-chat/useAiMetadataBuilder.ts +2 -0
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +1 -0
- package/src/brand-page-maker/composables/rich-text-editor/block-typography.test.ts +519 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/FontFamily.test.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +26 -1
- package/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.test.ts +110 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/RoleTypographyFill.ts +99 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/RteClipboard.ts +397 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/ViewDomParser.test.ts +139 -0
- package/src/brand-page-maker/composables/rich-text-editor/font.ts +36 -5
- package/src/brand-page-maker/composables/rich-text-editor/formatting.test.ts +375 -1
- package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +301 -17
- package/src/brand-page-maker/composables/rich-text-editor/hyperlink.test.ts +82 -1
- package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +28 -34
- package/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.test.ts +80 -0
- package/src/brand-page-maker/composables/rich-text-editor/keepMarksOnEmpty.ts +75 -0
- package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +31 -6
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.test.ts +2485 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +435 -337
- package/src/brand-page-maker/constants/config-settings-constant.ts +0 -6
- package/src/brand-page-maker/convertors/text-css-style-options.convertor.test.ts +43 -0
- package/src/brand-page-maker/convertors/text-css-style-options.convertor.ts +6 -2
- package/src/brand-page-maker/convertors/text-size-value.convertor.test.ts +24 -0
- package/src/brand-page-maker/convertors/text-size-value.convertor.ts +17 -13
- package/src/brand-page-maker/default-getters/payment-text-and-border-color.default-getter.ts +7 -22
- package/src/brand-page-maker/default-getters/text-color.default-getter.ts +5 -21
- package/src/brand-page-maker/models/content-settings.test.ts +708 -0
- package/src/brand-page-maker/models/content-settings.ts +133 -82
- package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.test.ts +63 -1
- package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store-utils.ts +16 -3
- package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.test.ts +34 -0
- package/src/brand-page-maker/stores/admin-logo-settings/admin-logo-settings-store.ts +4 -2
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +185 -1
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +22 -34
- package/src/brand-page-maker/stores/history/history-store.ts +35 -2
- package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +528 -0
- package/src/brand-page-maker/utils/__snapshots__/rte-render-parity.test.ts.snap +12 -0
- package/src/brand-page-maker/utils/apply-color-strategy.golden.test.ts +2 -0
- package/src/brand-page-maker/utils/apply-color-strategy.test.ts +181 -3
- package/src/brand-page-maker/utils/apply-color-strategy.ts +36 -4
- package/src/brand-page-maker/utils/index.ts +2 -1
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +447 -410
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +610 -506
- package/src/brand-page-maker/utils/rte-authoring.util.test.ts +244 -0
- package/src/brand-page-maker/utils/rte-authoring.util.ts +175 -0
- package/src/brand-page-maker/utils/rte-clipboard.util.test.ts +558 -0
- package/src/brand-page-maker/utils/rte-clipboard.util.ts +473 -0
- package/src/brand-page-maker/utils/rte-features.test.ts +99 -3
- package/src/brand-page-maker/utils/rte-render-parity.test.ts +1 -1
- package/src/brand-page-maker/utils/rte-render.util.test.ts +230 -0
- package/src/brand-page-maker/utils/rte-sanitize-json.test.ts +1 -1
- package/src/brand-page-maker/utils/rte-sanitize.util.ts +14 -3
- package/src/brand-page-maker/utils/rte-schema-version.test.ts +102 -0
- package/src/bundles/bundled-translations.de-DE.json +21 -2
- package/src/bundles/bundled-translations.es-ES.json +21 -2
- package/src/bundles/bundled-translations.fr-CA.json +21 -2
- package/src/bundles/bundled-translations.fr-FR.json +21 -2
- package/src/bundles/bundled-translations.json +21 -2
- package/src/bundles/bundled-translations.pt-BR.json +21 -2
- package/src/bundles/bundled-translations.pt-PT.json +21 -2
- package/src/clients/ai-assistant-client.ts +1 -0
- package/src/composables/useTranslate.ts +2 -1
- package/src/lib.ts +2 -2
- package/src/metadata-builder/builders/footer-builder.test.ts +70 -45
- package/src/metadata-builder/builders/footer-builder.ts +4 -8
- package/src/metadata-builder/builders/global-role-propagation.test.ts +907 -0
- package/src/metadata-builder/builders/global-role-propagation.ts +430 -0
- package/src/metadata-builder/builders/global-styles-builder.test.ts +1020 -0
- package/src/metadata-builder/builders/global-styles-builder.ts +194 -48
- package/src/metadata-builder/builders/header-builder.test.ts +112 -40
- package/src/metadata-builder/builders/header-builder.ts +18 -8
- package/src/metadata-builder/builders/section-builders/about-section-builder.ts +5 -3
- package/src/metadata-builder/builders/section-builders/banner-section-builder.ts +5 -3
- package/src/metadata-builder/builders/section-builders/booking-section-builder.ts +5 -3
- package/src/metadata-builder/builders/section-builders/contact-form-builder.ts +7 -4
- package/src/metadata-builder/builders/section-builders/images-section-builder.ts +5 -3
- package/src/metadata-builder/builders/section-builders/mailing-list-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/mailing-list-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/profile-section-builder.ts +7 -7
- package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +5 -3
- package/src/metadata-builder/builders/section-builders/social-links-builder.test.ts +2 -2
- package/src/metadata-builder/builders/section-builders/social-links-builder.ts +21 -10
- package/src/metadata-builder/builders/section-builders/support-me-builder.test.ts +10 -0
- package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +3 -2
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +111 -61
- package/src/metadata-builder/builders/section-field-maps.ts +1 -0
- package/src/metadata-builder/builders/section-field-utils.test.ts +545 -2
- package/src/metadata-builder/builders/section-field-utils.ts +202 -69
- package/src/metadata-builder/builders/section-rules/global-styles.rules.ts +38 -13
- package/src/metadata-builder/builders/section-rules/social-links.rules.ts +5 -0
- package/src/metadata-builder/helpers/rte.helpers.test.ts +101 -0
- package/src/metadata-builder/helpers/rte.helpers.ts +129 -43
- package/src/metadata-builder/metadata-executor.test.ts +167 -0
- package/src/metadata-builder/metadata-executor.ts +34 -1
- package/src/metadata-builder/models.ts +2 -0
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +23 -7
- package/src/metadata-builder/parsers/section-tool-arguments.ts +21 -1
- package/src/metadata-builder/sanitizers/__tests__/section-args-validator.test.ts +215 -0
- package/src/metadata-builder/sanitizers/rte-schema-check.ts +1 -1
- package/src/metadata-builder/sanitizers/sanitizer-rules.ts +4 -8
- package/src/metadata-builder/sanitizers/section-args-validator.ts +136 -53
- package/src/shared/color-palette/section-color-matches.test.ts +15 -11
- package/src/shared/color-palette/section-color-matches.ts +9 -28
- package/src/shared/color-strategies/color-strategies.ts +35 -0
- package/src/shared/color-strategies/rte-color.test.ts +1 -1
- package/src/shared/color-strategies/rte-color.ts +19 -0
- package/src/shared/color-strategies/rte-field-pairs.test.ts +26 -5
- package/src/shared/color-strategies/rte-field-pairs.ts +33 -0
- package/src/shared/content-settings/components/color-matches-panel/ColorMatchesPanel.test.ts +43 -66
- package/src/shared/content-settings/components/color-matches-panel/ColorMatchesPanel.vue +7 -1
- package/src/shared/content-settings/components/content-settings/ContentSettings.mixin.ts +1 -48
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.ts +33 -7
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.test.ts +128 -1
- package/src/shared/content-settings/components/draggable-item-list/DraggableItemList.vue +1 -0
- package/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.ts +0 -6
- package/src/shared/content-settings/components/logo-selection-field/LogoSelection.vue +3 -0
- package/src/shared/content-settings/components/logo-size-field/LogoSizeSlider.vue +3 -0
- package/src/shared/content-settings/components/logo-size-field/LogoSizeSliderField.vue +1 -0
- package/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.vue +1 -0
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.test.ts +56 -3
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +11 -4
- package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.test.ts +335 -25
- package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +248 -70
- package/src/shared/content-settings/components/text-style-field/TextStyleField.vue +113 -25
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.de-DE.json +17 -10
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.es-ES.json +17 -10
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.fr-CA.json +17 -10
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.fr-FR.json +17 -10
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.json +8 -1
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.pt-BR.json +17 -10
- package/src/shared/content-settings/components/text-style-field/i18n/text-style-field.pt-PT.json +17 -10
- package/src/shared/content-settings/components/text-style-role-dropdown/TextStyleRoleDropdown.test.ts +141 -0
- package/src/shared/content-settings/components/text-style-role-dropdown/TextStyleRoleDropdown.vue +136 -0
- package/src/shared/content-settings/store/content-settings-store.test.ts +491 -6
- package/src/shared/content-settings/store/content-settings-store.ts +159 -17
- package/src/shared/logo-settings/components/EditLogoSettings.vue +1 -0
- package/src/shared/logo-settings/components/LogoVariations.vue +1 -0
- package/src/shared/navigation-style-settings/components/navigation-style-settings/NavigationStyleSettings.vue +1 -0
- package/src/shared/navigation-style-settings/store/index.test.ts +295 -0
- package/src/shared/navigation-style-settings/store/index.ts +65 -15
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +2 -0
- package/src/shared/page-settings/components/page-settings/PageElement.vue +1 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +44 -4
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +13 -4
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.navigation.test.ts +89 -0
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.text-style.test.ts +91 -0
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +14 -2
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.vue +2 -0
- package/src/shared/page-settings/store/page-settings-store.text-style.test.ts +84 -0
- package/src/shared/page-settings/store/page-settings-store.ts +30 -1
- package/src/shared/rte-extensions/AutoLink.test.ts +98 -0
- package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/AutoLink.ts +87 -58
- package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.ts +4 -1
- package/src/shared/rte-extensions/TextEffects.ts +151 -0
- package/src/shared/rte-extensions/ViewDomParser.ts +27 -0
- package/src/{brand-page-maker/composables/rich-text-editor/getRteExtensions.ts → shared/rte-extensions.util.ts} +66 -9
- package/src/shared/rte-link.util.ts +2 -0
- package/src/shared/rte-render.util.ts +385 -0
- package/src/shared/rte-shape.util.ts +29 -0
- package/src/shared/side-bar/store/side-bar-store.test.ts +27 -1
- package/src/shared/side-bar/store/side-bar-store.ts +11 -0
- package/src/shared/style-settings/components/StyleSettings.mixin.ts +7 -3
- package/src/shared/style-settings/components/StyleSettings.test.ts +116 -0
- package/src/shared/style-settings/components/StyleSettings.vue +75 -25
- package/src/shared/style-settings/components/i18n/style-settings.de-DE.json +32 -20
- package/src/shared/style-settings/components/i18n/style-settings.es-ES.json +32 -20
- package/src/shared/style-settings/components/i18n/style-settings.fr-CA.json +32 -20
- package/src/shared/style-settings/components/i18n/style-settings.fr-FR.json +32 -20
- package/src/shared/style-settings/components/i18n/style-settings.json +13 -3
- package/src/shared/style-settings/components/i18n/style-settings.pt-BR.json +32 -20
- package/src/shared/style-settings/components/i18n/style-settings.pt-PT.json +32 -20
- package/src/shared/style-settings/components/text-style-options-settings-with-state/TextStyleOptionsSettingsWithState.vue +14 -34
- package/src/shared/style-settings/store/style-settings-store.ts +3 -49
- package/src/shared/style-settings/text-style-size-order.test.ts +76 -0
- package/src/shared/style-settings/text-style-size-order.ts +28 -0
- package/src/shared/template-settings/components/TemplatesSettings.vue +4 -0
- package/src/shared/text-role-typography.util.ts +277 -0
- package/src/utils/minify-brand-page-metadata.test.ts +102 -7
- package/src/utils/minify-brand-page-metadata.ts +62 -8
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +0 -2
- package/lib/src/brand-page-maker/composables/rich-text-editor/getRteExtensions.d.ts +0 -28
- package/lib/src/brand-page-maker/utils/rte-render.util.d.ts +0 -63
- package/lib/src/brand-page-maker/utils/rte-render.util.js +0 -106
- package/lib/src/brand-page-maker/utils/rte-shape.d.ts +0 -18
- package/lib/src/brand-page-maker/utils/rte-shape.js +0 -21
- package/lib/src/metadata-builder/builders/global-font-propagation.d.ts +0 -41
- package/lib/src/metadata-builder/builders/global-font-propagation.js +0 -146
- package/src/brand-page-maker/utils/rte-render.util.ts +0 -179
- package/src/brand-page-maker/utils/rte-shape.ts +0 -29
- package/src/metadata-builder/builders/global-font-propagation.test.ts +0 -401
- package/src/metadata-builder/builders/global-font-propagation.ts +0 -238
- package/src/shared/style-settings/components/StyleSettingsSection.vue +0 -8
- /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.d.ts +0 -0
- /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.js +0 -0
- /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.d.ts +0 -0
- /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.js +0 -0
- /package/lib/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/MaxLength.d.ts +0 -0
- /package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/FontFamily.ts +0 -0
- /package/src/{brand-page-maker/composables/rich-text-editor/extensions → shared/rte-extensions}/LetterSpacing.ts +0 -0
|
@@ -0,0 +1,4103 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
seedRoleColours,
|
|
4
|
+
computeLegacyTargets,
|
|
5
|
+
reconnectContentRoles,
|
|
6
|
+
stripLiftContentRte,
|
|
7
|
+
stripLiftRteDoc,
|
|
8
|
+
foldSemanticFormattingMarks,
|
|
9
|
+
pinLegacyRunWeight,
|
|
10
|
+
bakeElementColours,
|
|
11
|
+
clearRoleColours,
|
|
12
|
+
stripRteStyleColours,
|
|
13
|
+
convertPlainRteFields,
|
|
14
|
+
migration22,
|
|
15
|
+
} from './migration-22';
|
|
16
|
+
import type { MigrationData, MigrationOptions } from '../migration.util';
|
|
17
|
+
import {
|
|
18
|
+
BRAND_PAGE_TYPES,
|
|
19
|
+
NavigationDesktop,
|
|
20
|
+
PageHeaderDesktop,
|
|
21
|
+
PageStyles,
|
|
22
|
+
TextStyles,
|
|
23
|
+
} from '../../models';
|
|
24
|
+
import type { Content, HeaderButton } from '../../models';
|
|
25
|
+
import {
|
|
26
|
+
mapToPageHeaderViewModel,
|
|
27
|
+
mapToViewModel,
|
|
28
|
+
resolveSaveContactTitleStyle,
|
|
29
|
+
} from '../view-model.util';
|
|
30
|
+
import { setLocaleAsync } from '@src/composables/useTranslate';
|
|
31
|
+
import { HEADER_DISPLAY } from '../../constants';
|
|
32
|
+
import {
|
|
33
|
+
createInitialBannerSectionContent,
|
|
34
|
+
createInitialBookingSectionContent,
|
|
35
|
+
createInitialButtonContent,
|
|
36
|
+
createInitialMailingListContent,
|
|
37
|
+
createInitialProfileContent,
|
|
38
|
+
} from '@brand-page-maker/models';
|
|
39
|
+
import type {
|
|
40
|
+
BrandPageModel,
|
|
41
|
+
TextStyleDeclaration,
|
|
42
|
+
TextStyleOptions,
|
|
43
|
+
} from '../../models';
|
|
44
|
+
import type { TextStyleType } from '../../models/styles.model';
|
|
45
|
+
import { isTiptapDocument } from '@brand-page-maker/utils';
|
|
46
|
+
import { resolveTextStyle, TEXT_STYLE_CASCADE_PROPS } from '../text-style.util';
|
|
47
|
+
import { DETACHED_UNSET_EFFECTIVE } from '../text-role-reconnect.util';
|
|
48
|
+
import * as textRoleCss from '../text-role-css.util';
|
|
49
|
+
import * as rteRoleLift from '../rte-role-lift.util';
|
|
50
|
+
import { applyTextContrast } from '../container-text-color.util';
|
|
51
|
+
import {
|
|
52
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
53
|
+
PRODUCTION_HEADER_BUTTON_STYLE,
|
|
54
|
+
PRODUCTION_PROFILE_DEFAULTS,
|
|
55
|
+
productionTypography,
|
|
56
|
+
} from '../../__fixtures__/production-text-style-resolver.fixture';
|
|
57
|
+
|
|
58
|
+
const modelWithContent = (content: Record<string, unknown>): BrandPageModel =>
|
|
59
|
+
({ pages: [{ contents: [content] }] }) as unknown as BrandPageModel;
|
|
60
|
+
|
|
61
|
+
// A model carrying the role options + button/palette styles that the colour bake reads.
|
|
62
|
+
const modelWith = (
|
|
63
|
+
content: Record<string, unknown>,
|
|
64
|
+
options: TextStyleOptions,
|
|
65
|
+
): BrandPageModel =>
|
|
66
|
+
({
|
|
67
|
+
pages: [{ contents: [content] }],
|
|
68
|
+
styles: {
|
|
69
|
+
textStyles: { textStyleOptions: options },
|
|
70
|
+
buttonStyles: { style: {} },
|
|
71
|
+
colors: [],
|
|
72
|
+
},
|
|
73
|
+
brandPageType: 'default',
|
|
74
|
+
}) as unknown as BrandPageModel;
|
|
75
|
+
|
|
76
|
+
// Read/write a role's colour through the retired-from-type property.
|
|
77
|
+
const roleColorOf = (style: unknown): string | undefined =>
|
|
78
|
+
(style as Record<string, unknown>).color as string | undefined;
|
|
79
|
+
const setRoleColor = (style: unknown, color: string): void => {
|
|
80
|
+
(style as Record<string, unknown>).color = color;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// The colour on the first text run's textStyle mark (after the RTE bake replaces the doc).
|
|
84
|
+
const runMarkColor = (doc: unknown): string | undefined => {
|
|
85
|
+
const run = (doc as { content: { content: { marks?: unknown[] }[] }[] })
|
|
86
|
+
.content[0].content[0];
|
|
87
|
+
|
|
88
|
+
return (run.marks as { type: string; attrs?: { color?: string } }[])?.find(
|
|
89
|
+
(mark) => mark.type === 'textStyle',
|
|
90
|
+
)?.attrs?.color;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const roleType = (style: unknown): string | undefined =>
|
|
94
|
+
(style as { textStyleType?: string }).textStyleType;
|
|
95
|
+
|
|
96
|
+
// A minimal Tiptap doc: one paragraph of text runs, each with a textStyle mark.
|
|
97
|
+
const docOf = (runs: { text: string; attrs?: Record<string, unknown> }[]) => ({
|
|
98
|
+
type: 'doc',
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: 'paragraph',
|
|
102
|
+
content: runs.map((r) => ({
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: r.text,
|
|
105
|
+
marks: r.attrs ? [{ type: 'textStyle', attrs: { ...r.attrs } }] : [],
|
|
106
|
+
})),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const attrsOf = (doc: ReturnType<typeof docOf>, i: number) =>
|
|
112
|
+
(doc.content[0].content[i].marks[0] as { attrs?: Record<string, unknown> })
|
|
113
|
+
?.attrs;
|
|
114
|
+
|
|
115
|
+
// The seven typography properties a field renders with. An unset one renders its inherited
|
|
116
|
+
// default; family and size left unset inherit from the page, so they stay undefined here.
|
|
117
|
+
const lookOf = (
|
|
118
|
+
style: object | undefined,
|
|
119
|
+
role: object | undefined,
|
|
120
|
+
): Record<string, string | undefined> => {
|
|
121
|
+
const resolved = resolveTextStyle(
|
|
122
|
+
style as TextStyleDeclaration | undefined,
|
|
123
|
+
role,
|
|
124
|
+
) as Record<string, string | undefined>;
|
|
125
|
+
|
|
126
|
+
return Object.fromEntries(
|
|
127
|
+
TEXT_STYLE_CASCADE_PROPS.map((prop) => [
|
|
128
|
+
prop,
|
|
129
|
+
resolved[prop] || DETACHED_UNSET_EFFECTIVE[prop],
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Every property the field rendered before renders the same after; one it left to the page
|
|
135
|
+
// (a field on no role) is not compared.
|
|
136
|
+
const expectLookKept = (
|
|
137
|
+
after: object | undefined,
|
|
138
|
+
afterRole: object | undefined,
|
|
139
|
+
before: object | undefined,
|
|
140
|
+
beforeRole: object | undefined,
|
|
141
|
+
): void => {
|
|
142
|
+
const look = lookOf(after, afterRole);
|
|
143
|
+
|
|
144
|
+
for (const [prop, value] of Object.entries(lookOf(before, beforeRole))) {
|
|
145
|
+
if (value !== undefined) expect(look[prop], prop).toBe(value);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const roleStyleOf = (
|
|
150
|
+
options: TextStyleOptions,
|
|
151
|
+
type: unknown,
|
|
152
|
+
): TextStyleDeclaration | undefined =>
|
|
153
|
+
options[type as TextStyleType]?.style as TextStyleDeclaration | undefined;
|
|
154
|
+
|
|
155
|
+
// A site whose legacy `buttons` role differs from Button text in family, size and line height.
|
|
156
|
+
const withCustomButtons = (): TextStyleOptions => {
|
|
157
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
158
|
+
|
|
159
|
+
options.buttons.style.fontFamily = 'Oswald';
|
|
160
|
+
options.buttons.style.fontSize = '1.25em';
|
|
161
|
+
|
|
162
|
+
return options;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const siteWith = (
|
|
166
|
+
extra: Record<string, unknown>,
|
|
167
|
+
options: TextStyleOptions,
|
|
168
|
+
// A distinct site font (default 'lato') lets a test tell a real vote-driven winner apart from
|
|
169
|
+
// A2's fallback — both would otherwise read 'Lato' and hide a fallback-vs-vote bug (B1).
|
|
170
|
+
siteFontKey = 'lato',
|
|
171
|
+
): BrandPageModel =>
|
|
172
|
+
({
|
|
173
|
+
pages: [],
|
|
174
|
+
styles: {
|
|
175
|
+
textStyles: { textFontFamily: siteFontKey, textStyleOptions: options },
|
|
176
|
+
buttonStyles: { style: {} },
|
|
177
|
+
colors: [],
|
|
178
|
+
},
|
|
179
|
+
brandPageType: BRAND_PAGE_TYPES.brandSite,
|
|
180
|
+
...extra,
|
|
181
|
+
}) as unknown as BrandPageModel;
|
|
182
|
+
|
|
183
|
+
const runMigration22 = async (model: BrandPageModel): Promise<BrandPageModel> =>
|
|
184
|
+
(
|
|
185
|
+
await migration22(
|
|
186
|
+
{ brandPageModel: model } as MigrationData,
|
|
187
|
+
{} as MigrationOptions,
|
|
188
|
+
)
|
|
189
|
+
).brandPageModel;
|
|
190
|
+
|
|
191
|
+
describe('migration-22 · computeLegacyTargets (D1 — per-site legacy → nearest tier)', () => {
|
|
192
|
+
it('pins paragraphs → paragraph1 and buttons → button', () => {
|
|
193
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
194
|
+
|
|
195
|
+
const targets = computeLegacyTargets(options);
|
|
196
|
+
|
|
197
|
+
expect(targets.paragraphs).toBe('paragraph1');
|
|
198
|
+
expect(targets.buttons).toBe('button');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('maps titles/headings to the heading tier nearest their OLD size', () => {
|
|
202
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
203
|
+
|
|
204
|
+
options.titles.style.fontSize = '2.25em'; // nearest H4 (2.1875em)
|
|
205
|
+
options.headings.style.fontSize = '1.5em'; // nearest H5 (1.4375em)
|
|
206
|
+
|
|
207
|
+
const targets = computeLegacyTargets(options);
|
|
208
|
+
|
|
209
|
+
expect(targets.titles).toBe('heading4');
|
|
210
|
+
expect(targets.headings).toBe('heading5');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('keeps titles and headings on DISTINCT tiers when their sizes collide', () => {
|
|
214
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
215
|
+
|
|
216
|
+
options.titles.style.fontSize = '2em';
|
|
217
|
+
options.headings.style.fontSize = '2em';
|
|
218
|
+
|
|
219
|
+
const targets = computeLegacyTargets(options);
|
|
220
|
+
|
|
221
|
+
expect(targets.titles).toBe('heading4'); // titles keeps the shared nearest
|
|
222
|
+
expect(targets.headings).not.toBe(targets.titles); // headings nudged off it
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('keeps headings distinct from titles even when headings has no parseable size', () => {
|
|
226
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
227
|
+
|
|
228
|
+
options.titles.style.fontSize = '2.25em'; // → heading4
|
|
229
|
+
// no parseable size → the fallback must still exclude titles' tier
|
|
230
|
+
delete (options.headings.style as unknown as Record<string, unknown>)
|
|
231
|
+
.fontSize;
|
|
232
|
+
|
|
233
|
+
const targets = computeLegacyTargets(options);
|
|
234
|
+
|
|
235
|
+
expect(targets.titles).toBe('heading4');
|
|
236
|
+
expect(targets.headings).not.toBe('heading4');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('migration-22 · seedRoleColours (A — colour only; roles keep §4 typography)', () => {
|
|
241
|
+
it('copies the paragraphs colour to paragraph1 (D3 base coat)', () => {
|
|
242
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
243
|
+
|
|
244
|
+
setRoleColor(options.paragraphs.style, 'var(--logo-text-color)');
|
|
245
|
+
|
|
246
|
+
seedRoleColours(options, computeLegacyTargets(options));
|
|
247
|
+
|
|
248
|
+
expect(roleColorOf(options.paragraph1.style)).toBe(
|
|
249
|
+
'var(--logo-text-color)',
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('copies each old role colour onto its target tier', () => {
|
|
254
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
255
|
+
|
|
256
|
+
options.titles.style.fontSize = '2.25em'; // → heading4
|
|
257
|
+
setRoleColor(options.titles.style, '#111111');
|
|
258
|
+
setRoleColor(options.buttons.style, '#222222');
|
|
259
|
+
|
|
260
|
+
const targets = computeLegacyTargets(options);
|
|
261
|
+
|
|
262
|
+
seedRoleColours(options, targets);
|
|
263
|
+
|
|
264
|
+
expect(roleColorOf(options[targets.titles].style)).toBe('#111111');
|
|
265
|
+
expect(roleColorOf(options.button.style)).toBe('#222222');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('leaves role TYPOGRAPHY at the §4 defaults (the reported bug)', () => {
|
|
269
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
270
|
+
|
|
271
|
+
// a site that customised its legacy Titles size must NOT push it onto the role
|
|
272
|
+
options.titles.style.fontSize = '2.25em';
|
|
273
|
+
setRoleColor(options.titles.style, '#111111');
|
|
274
|
+
|
|
275
|
+
seedRoleColours(options, computeLegacyTargets(options));
|
|
276
|
+
|
|
277
|
+
expect(options.heading1.style.fontSize).toBe('6.875em'); // §4 default, untouched
|
|
278
|
+
expect(options.heading4.style.fontSize).toBe('2.1875em'); // §4 default, untouched
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('no-ops a legacy role that carries no colour', () => {
|
|
282
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
283
|
+
|
|
284
|
+
delete (options.paragraphs.style as unknown as Record<string, unknown>)
|
|
285
|
+
.color;
|
|
286
|
+
const before = roleColorOf(options.paragraph1.style);
|
|
287
|
+
|
|
288
|
+
seedRoleColours(options, computeLegacyTargets(options));
|
|
289
|
+
|
|
290
|
+
expect(roleColorOf(options.paragraph1.style)).toBe(before);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('migration-22 · reconnectContentRoles (D1/D5 — nearest tier + size override)', () => {
|
|
295
|
+
const withRoles = () => TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
296
|
+
|
|
297
|
+
it('relinks legacy blocks to their target tier, keeping the old role size production drew over the block’s own (D1)', () => {
|
|
298
|
+
const options = withRoles();
|
|
299
|
+
const targets = computeLegacyTargets(options);
|
|
300
|
+
const titlesSize = options.titles.style.fontSize;
|
|
301
|
+
const content = {
|
|
302
|
+
titleStyle: {
|
|
303
|
+
textStyleType: 'titles',
|
|
304
|
+
fontSize: '3.5em',
|
|
305
|
+
} as Record<string, unknown>,
|
|
306
|
+
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
307
|
+
buttonTextStyle: { textStyleType: 'buttons' },
|
|
308
|
+
};
|
|
309
|
+
const model = modelWithContent(content);
|
|
310
|
+
|
|
311
|
+
reconnectContentRoles(model, targets, options);
|
|
312
|
+
|
|
313
|
+
expect(content.titleStyle.textStyleType).toBe(targets.titles);
|
|
314
|
+
expect(content.descriptionStyle.textStyleType).toBe('paragraph1');
|
|
315
|
+
expect(content.buttonTextStyle.textStyleType).toBe('button');
|
|
316
|
+
// production drew the titles size, not the block's 3.5em; unset draws the tier's own
|
|
317
|
+
expect(
|
|
318
|
+
content.titleStyle.fontSize ?? options[targets.titles].style.fontSize,
|
|
319
|
+
).toBe(titlesSize);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('pins the old role typography onto a non-RTE block that inherited it (honour the render)', () => {
|
|
323
|
+
const options = withRoles();
|
|
324
|
+
|
|
325
|
+
options.paragraphs.style.fontSize = '1em';
|
|
326
|
+
options.paragraphs.style.lineHeight = '1.6';
|
|
327
|
+
const targets = computeLegacyTargets(options);
|
|
328
|
+
// a non-RTE block linked to paragraphs with no own size — used to inherit from the role
|
|
329
|
+
const content = { descriptionStyle: { textStyleType: 'paragraphs' } };
|
|
330
|
+
const model = modelWithContent(content);
|
|
331
|
+
|
|
332
|
+
reconnectContentRoles(model, targets, options);
|
|
333
|
+
|
|
334
|
+
const style = content.descriptionStyle as Record<string, unknown>;
|
|
335
|
+
|
|
336
|
+
expect(style.textStyleType).toBe('paragraph1');
|
|
337
|
+
expect(style.fontSize).toBe('1em'); // pinned from the old role → override
|
|
338
|
+
expect(style.lineHeight).toBe('1.6');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('preserves an inherited size on a legacy RTE field as a run literal, not the new tier default', () => {
|
|
342
|
+
const options = withRoles();
|
|
343
|
+
|
|
344
|
+
options.titles.style.fontSize = '2.25em'; // → heading4 (whose §4 default is 2.1875em)
|
|
345
|
+
const targets = computeLegacyTargets(options);
|
|
346
|
+
// a legacy RTE title whose size was role-inherited: a bare run (no fontSize mark) and no
|
|
347
|
+
// fontSize on the style object.
|
|
348
|
+
const content = {
|
|
349
|
+
type: 'mailing-list',
|
|
350
|
+
title: docOf([{ text: 'Hi' }]),
|
|
351
|
+
titleStyle: { textStyleType: 'titles' },
|
|
352
|
+
};
|
|
353
|
+
const model = modelWithContent(content);
|
|
354
|
+
|
|
355
|
+
reconnectContentRoles(model, targets, options); // materialises styleObj.fontSize = 2.25em
|
|
356
|
+
stripLiftContentRte(model, options); // C bakes it onto the bare run as a literal
|
|
357
|
+
|
|
358
|
+
expect(content.titleStyle.textStyleType).toBe('heading4');
|
|
359
|
+
// the run renders 2.25em (old role size), NOT heading4's §4 2.1875em nor a role var
|
|
360
|
+
expect(attrsOf(content.title, 0)?.fontSize).toBe('2.25em');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('pins a not-bold legacy RTE title to normal so its bold heading tier can’t bold it (C3)', () => {
|
|
364
|
+
const options = withRoles();
|
|
365
|
+
const targets = computeLegacyTargets(options);
|
|
366
|
+
// a legacy RTE title with a bare run (no bold mark) linked to `titles` — which had no weight, so
|
|
367
|
+
// it rendered normal. It reconnects to a heading tier whose §4 default is bold.
|
|
368
|
+
const content = {
|
|
369
|
+
type: 'mailing-list',
|
|
370
|
+
title: docOf([{ text: 'Hi' }]),
|
|
371
|
+
titleStyle: { textStyleType: 'titles' },
|
|
372
|
+
};
|
|
373
|
+
const model = modelWithContent(content);
|
|
374
|
+
|
|
375
|
+
reconnectContentRoles(model, targets, options); // materialises styleObj.fontWeight = normal
|
|
376
|
+
stripLiftContentRte(model, options); // C3 bakes normal onto the bare run
|
|
377
|
+
|
|
378
|
+
expect(roleType(content.titleStyle)).toMatch(/^heading/);
|
|
379
|
+
expect(attrsOf(content.title, 0)?.fontWeight).toBe('normal'); // NOT the tier's bold
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('reconnects a detached big block to a heading tier, keeping its props (D5)', () => {
|
|
383
|
+
const options = withRoles();
|
|
384
|
+
const content = {
|
|
385
|
+
titleStyle: {
|
|
386
|
+
textStyleType: undefined,
|
|
387
|
+
fontSize: '2em',
|
|
388
|
+
fontWeight: '700',
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
const model = modelWithContent(content);
|
|
392
|
+
|
|
393
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
394
|
+
|
|
395
|
+
expect(content.titleStyle.textStyleType).toBe('heading4'); // nearest heading to 2em (H4 = 2.1875em)
|
|
396
|
+
expect(content.titleStyle.fontWeight).toBe('700'); // kept as override
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('pins a detached block’s unset weight and line height so a heading tier can’t bold it', () => {
|
|
400
|
+
const options = withRoles();
|
|
401
|
+
const content = {
|
|
402
|
+
// a normal-weight block resized big, then detached — no own weight/style/decoration
|
|
403
|
+
titleStyle: { textStyleType: undefined, fontSize: '2em' },
|
|
404
|
+
};
|
|
405
|
+
const model = modelWithContent(content);
|
|
406
|
+
|
|
407
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
408
|
+
|
|
409
|
+
const style = content.titleStyle as Record<string, unknown>;
|
|
410
|
+
|
|
411
|
+
expect(style.textStyleType).toBe('heading4'); // gets heading identity…
|
|
412
|
+
expect(style.fontWeight).toBe('normal'); // …but NOT the tier's bold default
|
|
413
|
+
expect(style.lineHeight).toBe('normal');
|
|
414
|
+
// style and decoration: the tier leaves them unset, so the neutral draws the same unpinned
|
|
415
|
+
expect(style.fontStyle).toBeUndefined();
|
|
416
|
+
expect(style.textDecoration).toBeUndefined();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('reconnects a detached small block to a paragraph tier (D5)', () => {
|
|
420
|
+
const options = withRoles();
|
|
421
|
+
const content = { textStyle: { fontSize: '0.75em' } };
|
|
422
|
+
const model = modelWithContent(content);
|
|
423
|
+
|
|
424
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
425
|
+
|
|
426
|
+
expect(roleType(content.textStyle)).toBe('paragraph3');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('reconnects a detached block with no size to paragraph1', () => {
|
|
430
|
+
const options = withRoles();
|
|
431
|
+
const content = { textStyle: { textAlign: 'center' } };
|
|
432
|
+
const model = modelWithContent(content);
|
|
433
|
+
|
|
434
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
435
|
+
|
|
436
|
+
expect(roleType(content.textStyle)).toBe('paragraph1');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it.each([
|
|
440
|
+
[
|
|
441
|
+
'mailing-list',
|
|
442
|
+
{ fontSize: '1em', fontWeight: 'bold' },
|
|
443
|
+
{ fontSize: '1em', fontWeight: 'bold' },
|
|
444
|
+
],
|
|
445
|
+
['support-me', {}, { fontWeight: 'bold' }],
|
|
446
|
+
] as const)(
|
|
447
|
+
'materialises the old no-role %s title defaults before reconnecting it',
|
|
448
|
+
(type, storedStyle, expected) => {
|
|
449
|
+
const options = withRoles();
|
|
450
|
+
const content = { type, titleStyle: { ...storedStyle } };
|
|
451
|
+
|
|
452
|
+
reconnectContentRoles(
|
|
453
|
+
modelWithContent(content),
|
|
454
|
+
computeLegacyTargets(options),
|
|
455
|
+
options,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(content.titleStyle).toMatchObject({
|
|
459
|
+
textStyleType: type === 'mailing-list' ? 'paragraph2' : 'paragraph1',
|
|
460
|
+
...expected,
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
it('keeps an explicit no-role title weight over the old component default', () => {
|
|
466
|
+
const options = withRoles();
|
|
467
|
+
const content = {
|
|
468
|
+
type: 'mailing-list',
|
|
469
|
+
titleStyle: { fontWeight: 'normal' },
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
reconnectContentRoles(
|
|
473
|
+
modelWithContent(content),
|
|
474
|
+
computeLegacyTargets(options),
|
|
475
|
+
options,
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const migrated = content.titleStyle as Record<string, string>;
|
|
479
|
+
|
|
480
|
+
expect(migrated.textStyleType).toBe('paragraph2');
|
|
481
|
+
expect(migrated.fontWeight).toBe('normal');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('does not apply the old component default when a legacy role determined the title look', () => {
|
|
485
|
+
const options = withRoles();
|
|
486
|
+
|
|
487
|
+
options.titles.style.fontWeight = 'normal';
|
|
488
|
+
const content = {
|
|
489
|
+
type: 'mailing-list',
|
|
490
|
+
titleStyle: { textStyleType: 'titles' },
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
reconnectContentRoles(
|
|
494
|
+
modelWithContent(content),
|
|
495
|
+
computeLegacyTargets(options),
|
|
496
|
+
options,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect((content.titleStyle as Record<string, unknown>).fontWeight).toBe(
|
|
500
|
+
'normal',
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('materialises a no-role text block’s legacy textSize and line-height before reconnecting it', () => {
|
|
505
|
+
const options = withRoles();
|
|
506
|
+
|
|
507
|
+
options.paragraph1.style.lineHeight = '1.2';
|
|
508
|
+
const content = {
|
|
509
|
+
type: 'text',
|
|
510
|
+
textSize: { fontSize: '2em' },
|
|
511
|
+
textStyle: {},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
reconnectContentRoles(
|
|
515
|
+
modelWithContent(content),
|
|
516
|
+
computeLegacyTargets(options),
|
|
517
|
+
options,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
expect(content.textStyle).toMatchObject({
|
|
521
|
+
textStyleType: 'heading4',
|
|
522
|
+
fontSize: '2em',
|
|
523
|
+
lineHeight: '1.5',
|
|
524
|
+
});
|
|
525
|
+
expect(content).not.toHaveProperty('textSize');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('keeps a text block’s explicit style over its legacy textSize defaults', () => {
|
|
529
|
+
const options = withRoles();
|
|
530
|
+
|
|
531
|
+
options.paragraph1.style.lineHeight = '1.2';
|
|
532
|
+
const content = {
|
|
533
|
+
type: 'text',
|
|
534
|
+
textSize: { fontSize: '2em', lineHeight: '1.4' },
|
|
535
|
+
textStyle: { fontSize: '1.5em', lineHeight: '1.3' },
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
reconnectContentRoles(
|
|
539
|
+
modelWithContent(content),
|
|
540
|
+
computeLegacyTargets(options),
|
|
541
|
+
options,
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
expect(content.textStyle).toMatchObject({
|
|
545
|
+
textStyleType: 'heading5',
|
|
546
|
+
fontSize: '1.5em',
|
|
547
|
+
lineHeight: '1.3',
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('folds a text block’s legacy alignment and colour into its persisted style', () => {
|
|
552
|
+
const options = withRoles();
|
|
553
|
+
const content = {
|
|
554
|
+
type: 'text',
|
|
555
|
+
textAlignment: { textAlign: 'left' },
|
|
556
|
+
textColor: { color: '#123456' },
|
|
557
|
+
textStyle: { textAlign: 'center' },
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
reconnectContentRoles(
|
|
561
|
+
modelWithContent(content),
|
|
562
|
+
computeLegacyTargets(options),
|
|
563
|
+
options,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
expect(content.textStyle).toMatchObject({
|
|
567
|
+
textAlign: 'center',
|
|
568
|
+
color: '#123456',
|
|
569
|
+
});
|
|
570
|
+
expect(content).not.toHaveProperty('textAlignment');
|
|
571
|
+
expect(content).not.toHaveProperty('textColor');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it.each([
|
|
575
|
+
['mailing-list', 'paragraph2'],
|
|
576
|
+
['support-me', 'paragraph1'],
|
|
577
|
+
] as const)(
|
|
578
|
+
'keeps the old no-role %s title bold through the full migration',
|
|
579
|
+
async (type, role) => {
|
|
580
|
+
const options = withRoles();
|
|
581
|
+
const content = {
|
|
582
|
+
type,
|
|
583
|
+
title: docOf([{ text: 'Legacy title' }]),
|
|
584
|
+
titleStyle: {},
|
|
585
|
+
};
|
|
586
|
+
const model = await runMigration22(
|
|
587
|
+
siteWith({ pages: [{ contents: [content] }] }, options),
|
|
588
|
+
);
|
|
589
|
+
const after = model.pages[0].contents[0] as unknown as typeof content;
|
|
590
|
+
|
|
591
|
+
expect(roleType(after.titleStyle)).toBe(role);
|
|
592
|
+
expect(attrsOf(after.title, 0)?.fontWeight).toBe('bold');
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
it('keeps a no-role text block’s legacy textSize through the full migration', async () => {
|
|
597
|
+
const options = withRoles();
|
|
598
|
+
const content = {
|
|
599
|
+
type: 'text',
|
|
600
|
+
text: docOf([{ text: 'Legacy text' }]),
|
|
601
|
+
textSize: { fontSize: '2em' },
|
|
602
|
+
textStyle: {},
|
|
603
|
+
};
|
|
604
|
+
const model = await runMigration22(
|
|
605
|
+
siteWith({ pages: [{ contents: [content] }] }, options),
|
|
606
|
+
);
|
|
607
|
+
const after = model.pages[0].contents[0] as unknown as typeof content;
|
|
608
|
+
|
|
609
|
+
expect(roleType(after.textStyle)).toBe('heading4');
|
|
610
|
+
expect(attrsOf(after.text, 0)).toMatchObject({
|
|
611
|
+
fontSize: '2em',
|
|
612
|
+
lineHeight: '1.5',
|
|
613
|
+
});
|
|
614
|
+
expect(after).not.toHaveProperty('textSize');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('skips null items and text items throughout the migration', async () => {
|
|
618
|
+
const options = withRoles();
|
|
619
|
+
const model = await runMigration22(
|
|
620
|
+
siteWith(
|
|
621
|
+
{
|
|
622
|
+
pages: [
|
|
623
|
+
{
|
|
624
|
+
contents: [
|
|
625
|
+
{ type: 'images-section', items: [null] },
|
|
626
|
+
{ type: 'text', textItems: [null], textStyle: {} },
|
|
627
|
+
],
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
},
|
|
631
|
+
options,
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
expect(model.pages[0].contents).toHaveLength(2);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it.each(['none', 'garbage'])(
|
|
639
|
+
"reconnects a '%s' role by its size instead of leaving it as if canonical (R14)",
|
|
640
|
+
(type) => {
|
|
641
|
+
const options = withRoles();
|
|
642
|
+
const content = {
|
|
643
|
+
titleStyle: { textStyleType: type, fontSize: '2.5em' },
|
|
644
|
+
textStyle: { textStyleType: type, fontSize: '0.875em' },
|
|
645
|
+
descriptionStyle: { textStyleType: type },
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
reconnectContentRoles(
|
|
649
|
+
modelWithContent(content),
|
|
650
|
+
computeLegacyTargets(options),
|
|
651
|
+
options,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
expect(roleType(content.titleStyle)).toBe('heading4');
|
|
655
|
+
expect(roleType(content.textStyle)).toBe('paragraph3');
|
|
656
|
+
expect(roleType(content.descriptionStyle)).toBe('paragraph1');
|
|
657
|
+
// the heading tier gives identity, not its bold default
|
|
658
|
+
expect((content.titleStyle as Record<string, unknown>).fontWeight).toBe(
|
|
659
|
+
'normal',
|
|
660
|
+
);
|
|
661
|
+
},
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
it('leaves an already-canonical link untouched', () => {
|
|
665
|
+
const options = withRoles();
|
|
666
|
+
const content = { titleStyle: { textStyleType: 'heading2' } };
|
|
667
|
+
const model = modelWithContent(content);
|
|
668
|
+
|
|
669
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
670
|
+
|
|
671
|
+
expect(content.titleStyle.textStyleType).toBe('heading2');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('reconnects textItems[].textStyle', () => {
|
|
675
|
+
const options = withRoles();
|
|
676
|
+
const content = {
|
|
677
|
+
textItems: [{ textStyle: { textStyleType: 'paragraphs' } }],
|
|
678
|
+
};
|
|
679
|
+
const model = modelWithContent(content);
|
|
680
|
+
|
|
681
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
682
|
+
|
|
683
|
+
expect(content.textItems[0].textStyle.textStyleType).toBe('paragraph1');
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('reconnects the footer copyright role (legacy → canonical), outside page.contents', () => {
|
|
687
|
+
const options = withRoles();
|
|
688
|
+
const model = modelWithContent({ type: 'shapes-section' });
|
|
689
|
+
|
|
690
|
+
(model as unknown as Record<string, unknown>).pageFooterDesktop = {
|
|
691
|
+
copyrightStyle: { textStyleType: 'paragraphs' },
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
reconnectContentRoles(model, computeLegacyTargets(options), options);
|
|
695
|
+
|
|
696
|
+
expect(
|
|
697
|
+
(
|
|
698
|
+
(model as unknown as { pageFooterDesktop: { copyrightStyle: unknown } })
|
|
699
|
+
.pageFooterDesktop.copyrightStyle as { textStyleType?: string }
|
|
700
|
+
).textStyleType,
|
|
701
|
+
).toBe('paragraph1');
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// The look through the real view model (not `resolveTextStyle`), so renderer defaults are modelled:
|
|
706
|
+
// the button view model's 1.4 line height, the profile's sizes. An unset family draws the site font
|
|
707
|
+
// (Lato here), an unset size its parent's (1em), the rest their inherited defaults.
|
|
708
|
+
//
|
|
709
|
+
// BP-5969 (R1): page data now carries only a field's own typography — the role's values are a
|
|
710
|
+
// CSS var reference resolved at render, not merged in here. So this helper resolves the same way
|
|
711
|
+
// the renderer does (`roleTypographyStyle`): own, else the linked role's own definition (when
|
|
712
|
+
// `options` and a canonical `textStyleType` are given), else the no-role/unset default.
|
|
713
|
+
const drawn = (
|
|
714
|
+
style: (object & { textStyleType?: string }) | undefined,
|
|
715
|
+
options?: TextStyleOptions,
|
|
716
|
+
): Record<string, string | undefined> => {
|
|
717
|
+
const record = (style ?? {}) as Record<string, string | undefined>;
|
|
718
|
+
const role =
|
|
719
|
+
options && record.textStyleType
|
|
720
|
+
? (
|
|
721
|
+
options as unknown as Record<
|
|
722
|
+
string,
|
|
723
|
+
{ style?: Record<string, string | undefined> }
|
|
724
|
+
>
|
|
725
|
+
)[record.textStyleType]?.style
|
|
726
|
+
: undefined;
|
|
727
|
+
const unset: Record<string, string | undefined> = {
|
|
728
|
+
...DETACHED_UNSET_EFFECTIVE,
|
|
729
|
+
fontFamily: 'Lato',
|
|
730
|
+
fontSize: '1em',
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
return Object.fromEntries(
|
|
734
|
+
TEXT_STYLE_CASCADE_PROPS.map((prop) => [
|
|
735
|
+
prop,
|
|
736
|
+
record[prop] || role?.[prop] || unset[prop],
|
|
737
|
+
]),
|
|
738
|
+
);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const pageStylesFor = (options: TextStyleOptions): PageStyles => {
|
|
742
|
+
const styles = new PageStyles();
|
|
743
|
+
|
|
744
|
+
styles.textStyles.textStyleOptions = options;
|
|
745
|
+
|
|
746
|
+
return styles;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const drawnHeaderButton = (
|
|
750
|
+
headerButton: HeaderButton,
|
|
751
|
+
options: TextStyleOptions,
|
|
752
|
+
) =>
|
|
753
|
+
drawn(
|
|
754
|
+
mapToPageHeaderViewModel(
|
|
755
|
+
new PageHeaderDesktop({ headerButton }),
|
|
756
|
+
pageStylesFor(options),
|
|
757
|
+
[],
|
|
758
|
+
).headerButtonViewModel?.titleStyle,
|
|
759
|
+
options,
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
const drawnContent = (
|
|
763
|
+
content: unknown,
|
|
764
|
+
options: TextStyleOptions,
|
|
765
|
+
pick: (viewModel: Record<string, any>) => object | undefined,
|
|
766
|
+
) =>
|
|
767
|
+
drawn(
|
|
768
|
+
pick(
|
|
769
|
+
mapToViewModel(
|
|
770
|
+
pageStylesFor(options),
|
|
771
|
+
BRAND_PAGE_TYPES.brandSite,
|
|
772
|
+
{},
|
|
773
|
+
)(JSON.parse(JSON.stringify(content)) as Content) as Record<string, any>,
|
|
774
|
+
),
|
|
775
|
+
options,
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
describe('migration-22 · header button (R7)', () => {
|
|
779
|
+
// Production drew a header button on `buttons` with only the role's typography, so its 1.5 line
|
|
780
|
+
// height, not the view model's 1.4, which only a button on no role drew.
|
|
781
|
+
it.each([
|
|
782
|
+
['no text style', undefined, '1.5'],
|
|
783
|
+
['buttons', { textStyleType: 'buttons' }, '1.5'],
|
|
784
|
+
['no role', { fontSize: '1.25em', fontWeight: 'bold' }, '1.4'],
|
|
785
|
+
] as const)(
|
|
786
|
+
'moves a header button on %s to button on both viewports, look kept as production drew it',
|
|
787
|
+
async (_label, buttonTextStyle, lineHeight) => {
|
|
788
|
+
const options = withCustomButtons();
|
|
789
|
+
const headerButton = (): HeaderButton => ({
|
|
790
|
+
isVisible: true,
|
|
791
|
+
text: 'Contact us',
|
|
792
|
+
buttonLinkType: 'url',
|
|
793
|
+
...(buttonTextStyle
|
|
794
|
+
? { buttonTextStyle: { ...buttonTextStyle } as TextStyleDeclaration }
|
|
795
|
+
: {}),
|
|
796
|
+
});
|
|
797
|
+
const before = drawn(
|
|
798
|
+
productionTypography(
|
|
799
|
+
(buttonTextStyle ??
|
|
800
|
+
PRODUCTION_HEADER_BUTTON_STYLE) as TextStyleDeclaration,
|
|
801
|
+
options,
|
|
802
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const model = await runMigration22(
|
|
807
|
+
siteWith(
|
|
808
|
+
{
|
|
809
|
+
pageHeaderDesktop: { headerButton: headerButton() },
|
|
810
|
+
pageHeaderMobile: { headerButton: headerButton() },
|
|
811
|
+
},
|
|
812
|
+
options,
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
for (const viewport of [
|
|
817
|
+
model.pageHeaderDesktop,
|
|
818
|
+
model.pageHeaderMobile,
|
|
819
|
+
]) {
|
|
820
|
+
const after = viewport.headerButton!;
|
|
821
|
+
|
|
822
|
+
expect(after.buttonTextStyle?.textStyleType).toBe('button');
|
|
823
|
+
expect(drawnHeaderButton(after, options)).toEqual(before);
|
|
824
|
+
expect(drawnHeaderButton(after, options).lineHeight).toBe(lineHeight);
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// Every button element starts on Button text; what makes it different stays an override, and the
|
|
831
|
+
// look is unchanged through the view model (James Joung, 2026-09-28).
|
|
832
|
+
describe('migration-22 · section button (R14, Q2)', () => {
|
|
833
|
+
// Factories translate their labels, so the table builds each content after the locale is set.
|
|
834
|
+
const withButtonStyle =
|
|
835
|
+
(create: () => unknown, key: string, style: Record<string, unknown>) =>
|
|
836
|
+
(): Record<string, unknown> => ({
|
|
837
|
+
...(create() as Record<string, unknown>),
|
|
838
|
+
[key]: { ...style },
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
beforeAll(async () => {
|
|
842
|
+
await setLocaleAsync('en-US');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it.each([
|
|
846
|
+
[
|
|
847
|
+
'a banner button on buttons',
|
|
848
|
+
withButtonStyle(
|
|
849
|
+
() => createInitialBannerSectionContent(),
|
|
850
|
+
'buttonTextStyle',
|
|
851
|
+
{ textStyleType: 'buttons', textAlign: 'center' },
|
|
852
|
+
),
|
|
853
|
+
'buttonTextStyle',
|
|
854
|
+
(vm: Record<string, any>) => vm.buttonViewModel?.titleStyle,
|
|
855
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
856
|
+
],
|
|
857
|
+
[
|
|
858
|
+
'a banner button on no role',
|
|
859
|
+
withButtonStyle(
|
|
860
|
+
() => createInitialBannerSectionContent(),
|
|
861
|
+
'buttonTextStyle',
|
|
862
|
+
{ fontSize: '1em', textAlign: 'center' },
|
|
863
|
+
),
|
|
864
|
+
'buttonTextStyle',
|
|
865
|
+
(vm: Record<string, any>) => vm.buttonViewModel?.titleStyle,
|
|
866
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
867
|
+
],
|
|
868
|
+
[
|
|
869
|
+
'a booking button on buttons',
|
|
870
|
+
withButtonStyle(
|
|
871
|
+
() => createInitialBookingSectionContent(),
|
|
872
|
+
'buttonTextStyle',
|
|
873
|
+
{ textStyleType: 'buttons' },
|
|
874
|
+
),
|
|
875
|
+
'buttonTextStyle',
|
|
876
|
+
(vm: Record<string, any>) => vm.buttonViewModel?.titleStyle,
|
|
877
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
878
|
+
],
|
|
879
|
+
[
|
|
880
|
+
'a link button on buttons',
|
|
881
|
+
withButtonStyle(
|
|
882
|
+
createInitialButtonContent('general-link', ''),
|
|
883
|
+
'titleStyle',
|
|
884
|
+
{ textStyleType: 'buttons', textAlign: 'center' },
|
|
885
|
+
),
|
|
886
|
+
'titleStyle',
|
|
887
|
+
(vm: Record<string, any>) => vm.titleStyle,
|
|
888
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
889
|
+
],
|
|
890
|
+
[
|
|
891
|
+
'a banner button on no role at line height 1.2 (equal to Button text, which the view model would draw at 1.4)',
|
|
892
|
+
withButtonStyle(
|
|
893
|
+
() => createInitialBannerSectionContent(),
|
|
894
|
+
'buttonTextStyle',
|
|
895
|
+
{ lineHeight: '1.2', textAlign: 'center' },
|
|
896
|
+
),
|
|
897
|
+
'buttonTextStyle',
|
|
898
|
+
(vm: Record<string, any>) => vm.buttonViewModel?.titleStyle,
|
|
899
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
900
|
+
],
|
|
901
|
+
[
|
|
902
|
+
'a mailing list button on no role',
|
|
903
|
+
withButtonStyle(
|
|
904
|
+
() => createInitialMailingListContent(),
|
|
905
|
+
'buttonTextStyle',
|
|
906
|
+
{ fontSize: '1.25em', textAlign: 'center' },
|
|
907
|
+
),
|
|
908
|
+
'buttonTextStyle',
|
|
909
|
+
(vm: Record<string, any>) => vm.buttonTextStyle,
|
|
910
|
+
{},
|
|
911
|
+
],
|
|
912
|
+
[
|
|
913
|
+
'a banner button on buttons carrying typography production ignored',
|
|
914
|
+
withButtonStyle(
|
|
915
|
+
() => createInitialBannerSectionContent(),
|
|
916
|
+
'buttonTextStyle',
|
|
917
|
+
{
|
|
918
|
+
textStyleType: 'buttons',
|
|
919
|
+
fontSize: '2em',
|
|
920
|
+
fontWeight: '300',
|
|
921
|
+
lineHeight: '1.1',
|
|
922
|
+
textAlign: 'center',
|
|
923
|
+
},
|
|
924
|
+
),
|
|
925
|
+
'buttonTextStyle',
|
|
926
|
+
(vm: Record<string, any>) => vm.buttonViewModel?.titleStyle,
|
|
927
|
+
PRODUCTION_BUTTON_DEFAULTS,
|
|
928
|
+
],
|
|
929
|
+
] as const)(
|
|
930
|
+
'moves %s to button, look kept as production drew it',
|
|
931
|
+
async (_label, createContent, key, pick, defaults) => {
|
|
932
|
+
const options = withCustomButtons();
|
|
933
|
+
const content = createContent();
|
|
934
|
+
const before = drawn(
|
|
935
|
+
productionTypography(
|
|
936
|
+
(content as Record<string, TextStyleDeclaration>)[key],
|
|
937
|
+
options,
|
|
938
|
+
defaults,
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
const model = await runMigration22(
|
|
943
|
+
siteWith(
|
|
944
|
+
{ pages: [{ contents: [JSON.parse(JSON.stringify(content))] }] },
|
|
945
|
+
options,
|
|
946
|
+
),
|
|
947
|
+
);
|
|
948
|
+
const after = model.pages[0].contents[0] as unknown as Record<
|
|
949
|
+
string,
|
|
950
|
+
TextStyleDeclaration
|
|
951
|
+
>;
|
|
952
|
+
|
|
953
|
+
expect(after[key].textStyleType).toBe('button');
|
|
954
|
+
expect(drawnContent(after, options, pick)).toEqual(before);
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Production drew a profile field on a role with only the role's typography; on no role, the view
|
|
960
|
+
// model's defaults for an unset size and line height (1em / 1.5 title and description, 1.5em / 1.4
|
|
961
|
+
// name). The upgrade keeps whichever it drew.
|
|
962
|
+
describe('migration-22 · profile look (R14)', () => {
|
|
963
|
+
beforeAll(async () => {
|
|
964
|
+
await setLocaleAsync('en-US');
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it.each([
|
|
968
|
+
['titleStyle', { textStyleType: 'titles', textAlign: 'center' }],
|
|
969
|
+
['descriptionStyle', { textStyleType: 'paragraphs', textAlign: 'center' }],
|
|
970
|
+
// its own size and line height never showed over the role
|
|
971
|
+
[
|
|
972
|
+
'nameStyle',
|
|
973
|
+
{ textStyleType: 'headings', fontSize: '3em', lineHeight: '1' },
|
|
974
|
+
],
|
|
975
|
+
// 0.875em lands on Paragraph 3 (equal), which the view model would draw at its own 1em
|
|
976
|
+
['descriptionStyle', { fontSize: '0.875em', textAlign: 'center' }],
|
|
977
|
+
['nameStyle', { textAlign: 'center' }],
|
|
978
|
+
] as const)(
|
|
979
|
+
'keeps a profile %s look as production drew it',
|
|
980
|
+
async (key, style) => {
|
|
981
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
982
|
+
const content = {
|
|
983
|
+
...createInitialProfileContent(),
|
|
984
|
+
[key]: { ...style },
|
|
985
|
+
};
|
|
986
|
+
const pick = (vm: Record<string, any>) => vm[key];
|
|
987
|
+
const before = drawn(
|
|
988
|
+
productionTypography(
|
|
989
|
+
style as TextStyleDeclaration,
|
|
990
|
+
options,
|
|
991
|
+
PRODUCTION_PROFILE_DEFAULTS[key],
|
|
992
|
+
),
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
const model = await runMigration22(
|
|
996
|
+
siteWith(
|
|
997
|
+
{ pages: [{ contents: [JSON.parse(JSON.stringify(content))] }] },
|
|
998
|
+
options,
|
|
999
|
+
),
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
expect(drawnContent(model.pages[0].contents[0], options, pick)).toEqual(
|
|
1003
|
+
before,
|
|
1004
|
+
);
|
|
1005
|
+
},
|
|
1006
|
+
);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
describe('migration-22 · navigation (R11)', () => {
|
|
1010
|
+
const NAV_COLOUR = 'var(--palette-1)';
|
|
1011
|
+
|
|
1012
|
+
it.each([
|
|
1013
|
+
[
|
|
1014
|
+
'tab style on no role',
|
|
1015
|
+
'tab',
|
|
1016
|
+
{ fontSize: '1em', fontFamily: 'Lato', textAlign: 'left' },
|
|
1017
|
+
'paragraph1',
|
|
1018
|
+
],
|
|
1019
|
+
['tab style on buttons', 'tab', { textStyleType: 'buttons' }, 'paragraph1'],
|
|
1020
|
+
[
|
|
1021
|
+
'hamburger style on buttons',
|
|
1022
|
+
'hamburger',
|
|
1023
|
+
{ textStyleType: 'buttons' },
|
|
1024
|
+
'button',
|
|
1025
|
+
],
|
|
1026
|
+
[
|
|
1027
|
+
'hamburger style on no role',
|
|
1028
|
+
'hamburger',
|
|
1029
|
+
{ fontSize: '1em', textAlign: 'left' },
|
|
1030
|
+
'button',
|
|
1031
|
+
],
|
|
1032
|
+
] as const)(
|
|
1033
|
+
'moves the navigation %s to %s on both viewports, look and colour kept',
|
|
1034
|
+
async (_label, place, style, expected) => {
|
|
1035
|
+
const options = withCustomButtons();
|
|
1036
|
+
const before = { ...style, color: NAV_COLOUR } as TextStyleDeclaration;
|
|
1037
|
+
const beforeRole = roleStyleOf(options, before.textStyleType);
|
|
1038
|
+
const afterRole = { ...roleStyleOf(options, expected) };
|
|
1039
|
+
const header = () => ({
|
|
1040
|
+
navigation:
|
|
1041
|
+
place === 'tab'
|
|
1042
|
+
? { defaultButtonStyle: { ...before } }
|
|
1043
|
+
: { hamburgerMenuStyle: { defaultButtonStyle: { ...before } } },
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
const model = await runMigration22(
|
|
1047
|
+
siteWith(
|
|
1048
|
+
{ pageHeaderDesktop: header(), pageHeaderMobile: header() },
|
|
1049
|
+
options,
|
|
1050
|
+
),
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
for (const viewport of [
|
|
1054
|
+
model.pageHeaderDesktop,
|
|
1055
|
+
model.pageHeaderMobile,
|
|
1056
|
+
]) {
|
|
1057
|
+
const after = (
|
|
1058
|
+
place === 'tab'
|
|
1059
|
+
? viewport.navigation.defaultButtonStyle
|
|
1060
|
+
: viewport.navigation.hamburgerMenuStyle.defaultButtonStyle
|
|
1061
|
+
) as TextStyleDeclaration;
|
|
1062
|
+
|
|
1063
|
+
expect(after.textStyleType).toBe(expected);
|
|
1064
|
+
expect(after.color).toBe(NAV_COLOUR);
|
|
1065
|
+
expectLookKept(after, afterRole, before, beforeRole);
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// A nav link resolves `element || #000000` once linked (`getTextStyleByContentTextStyle`). Before,
|
|
1072
|
+
// one with no colour of its own drew its legacy role's colour (the production resolver), or on no
|
|
1073
|
+
// role inherited its parent's: a tab the header's colour, else the page base coat (the paragraphs
|
|
1074
|
+
// colour); the hamburger menu, beside the header under the page root (`Page.vue`), the base coat.
|
|
1075
|
+
describe('migration-22 · navigation colour (R11)', () => {
|
|
1076
|
+
const PARAGRAPHS = 'var(--palette-0)';
|
|
1077
|
+
const BUTTONS = 'var(--palette-2)';
|
|
1078
|
+
const HEADER = '#445566';
|
|
1079
|
+
|
|
1080
|
+
const drawnNavColours = (
|
|
1081
|
+
navigation: unknown,
|
|
1082
|
+
options: TextStyleOptions,
|
|
1083
|
+
): [unknown, unknown] => {
|
|
1084
|
+
const vm = mapToPageHeaderViewModel(
|
|
1085
|
+
new PageHeaderDesktop({
|
|
1086
|
+
// a header with no display style is rebuilt with a default navigation
|
|
1087
|
+
displayStyle: HEADER_DISPLAY.DESKTOP_TAB_5,
|
|
1088
|
+
navigation: new NavigationDesktop(navigation as never),
|
|
1089
|
+
}),
|
|
1090
|
+
pageStylesFor(options),
|
|
1091
|
+
[],
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
return [
|
|
1095
|
+
vm.navigation.defaultButtonStyle.color,
|
|
1096
|
+
vm.navigation.hamburgerMenuStyle.defaultButtonStyle.color,
|
|
1097
|
+
];
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
it.each([
|
|
1101
|
+
[
|
|
1102
|
+
'on no role, under a coloured header',
|
|
1103
|
+
{ fontSize: '1em' },
|
|
1104
|
+
HEADER,
|
|
1105
|
+
[HEADER, PARAGRAPHS],
|
|
1106
|
+
],
|
|
1107
|
+
[
|
|
1108
|
+
'on no role, with no header colour',
|
|
1109
|
+
{ fontSize: '1em' },
|
|
1110
|
+
undefined,
|
|
1111
|
+
[PARAGRAPHS, PARAGRAPHS],
|
|
1112
|
+
],
|
|
1113
|
+
['on buttons', { textStyleType: 'buttons' }, HEADER, [BUTTONS, BUTTONS]],
|
|
1114
|
+
] as const)(
|
|
1115
|
+
'keeps the colour a navigation link %s drew with no colour of its own',
|
|
1116
|
+
async (_label, style, headerColour, expected) => {
|
|
1117
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1118
|
+
|
|
1119
|
+
(options.paragraphs.style as unknown as Record<string, unknown>).color =
|
|
1120
|
+
PARAGRAPHS;
|
|
1121
|
+
(options.buttons.style as unknown as Record<string, unknown>).color =
|
|
1122
|
+
BUTTONS;
|
|
1123
|
+
|
|
1124
|
+
const header = () => ({
|
|
1125
|
+
...(headerColour ? { style: { color: headerColour } } : {}),
|
|
1126
|
+
navigation: {
|
|
1127
|
+
defaultButtonStyle: { ...style },
|
|
1128
|
+
hamburgerMenuStyle: { defaultButtonStyle: { ...style } },
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
const model = await runMigration22(
|
|
1133
|
+
siteWith(
|
|
1134
|
+
{ pageHeaderDesktop: header(), pageHeaderMobile: header() },
|
|
1135
|
+
options,
|
|
1136
|
+
),
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
expect(
|
|
1140
|
+
drawnNavColours(model.pageHeaderDesktop.navigation, options),
|
|
1141
|
+
).toEqual(expected);
|
|
1142
|
+
},
|
|
1143
|
+
);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
describe('migration-22 · save contact (R15)', () => {
|
|
1147
|
+
it('moves a brand.contact save contact button to button, keeping the buttons look and its settings', async () => {
|
|
1148
|
+
const options = withCustomButtons();
|
|
1149
|
+
// The widget drew the retired role's style directly (`SaveContactWidget.vue`, before this change).
|
|
1150
|
+
const before = drawn(options.buttons.style);
|
|
1151
|
+
|
|
1152
|
+
const model = await runMigration22(
|
|
1153
|
+
siteWith(
|
|
1154
|
+
{
|
|
1155
|
+
brandPageType: BRAND_PAGE_TYPES.brandContact,
|
|
1156
|
+
saveContactSettings: { isSavingEnabled: false, hideBranding: true },
|
|
1157
|
+
},
|
|
1158
|
+
options,
|
|
1159
|
+
),
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
const after = model.saveContactSettings?.buttonTextStyle;
|
|
1163
|
+
|
|
1164
|
+
// Button text plus only the values that draw differently from it: weight `normal` equals the
|
|
1165
|
+
// role, and style, decoration and spacing are neutrals Button text leaves unset, so a Button text
|
|
1166
|
+
// edit to those still repaints it (R15). `isSavingEnabled: false` means the widget never renders
|
|
1167
|
+
// (S4, Q12b's "nothing to show" rule), so it casts no A2 vote; Button text stays on Lato and
|
|
1168
|
+
// Save Contact's own drawn font (Oswald, the old `buttons` role's) survives as an override.
|
|
1169
|
+
expect(after).toEqual({
|
|
1170
|
+
textStyleType: 'button',
|
|
1171
|
+
fontFamily: 'Oswald',
|
|
1172
|
+
fontSize: '1.25em',
|
|
1173
|
+
lineHeight: '1.5',
|
|
1174
|
+
});
|
|
1175
|
+
expect(
|
|
1176
|
+
drawn(
|
|
1177
|
+
resolveSaveContactTitleStyle(after, pageStylesFor(options).textStyles),
|
|
1178
|
+
),
|
|
1179
|
+
).toEqual(before);
|
|
1180
|
+
expect(model.saveContactSettings).toMatchObject({
|
|
1181
|
+
isSavingEnabled: false,
|
|
1182
|
+
hideBranding: true,
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it('creates the save contact settings with their defaults on a brand.contact site that has none', async () => {
|
|
1187
|
+
const model = await runMigration22(
|
|
1188
|
+
siteWith(
|
|
1189
|
+
{ brandPageType: BRAND_PAGE_TYPES.brandContact },
|
|
1190
|
+
withCustomButtons(),
|
|
1191
|
+
),
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
expect(model.saveContactSettings).toMatchObject({
|
|
1195
|
+
isSavingEnabled: true,
|
|
1196
|
+
hideBranding: false,
|
|
1197
|
+
buttonTextStyle: { textStyleType: 'button' },
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
it.each([BRAND_PAGE_TYPES.brandSite, BRAND_PAGE_TYPES.default])(
|
|
1202
|
+
'adds no save contact button style on a %s site',
|
|
1203
|
+
async (brandPageType) => {
|
|
1204
|
+
const withSettings = await runMigration22(
|
|
1205
|
+
siteWith(
|
|
1206
|
+
{
|
|
1207
|
+
brandPageType,
|
|
1208
|
+
saveContactSettings: { isSavingEnabled: true, hideBranding: false },
|
|
1209
|
+
},
|
|
1210
|
+
withCustomButtons(),
|
|
1211
|
+
),
|
|
1212
|
+
);
|
|
1213
|
+
const withoutSettings = await runMigration22(
|
|
1214
|
+
siteWith({ brandPageType }, withCustomButtons()),
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
expect(withSettings.saveContactSettings).toEqual({
|
|
1218
|
+
isSavingEnabled: true,
|
|
1219
|
+
hideBranding: false,
|
|
1220
|
+
});
|
|
1221
|
+
expect(withoutSettings.saveContactSettings).toBeUndefined();
|
|
1222
|
+
},
|
|
1223
|
+
);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
describe('migration-22 · role fonts on upgrade (R16)', () => {
|
|
1227
|
+
const HEADING_ROLES = [
|
|
1228
|
+
'heading1',
|
|
1229
|
+
'heading2',
|
|
1230
|
+
'heading3',
|
|
1231
|
+
'heading4',
|
|
1232
|
+
'heading5',
|
|
1233
|
+
'heading6',
|
|
1234
|
+
] as const;
|
|
1235
|
+
const PARAGRAPH_ROLES = ['paragraph1', 'paragraph2', 'paragraph3'] as const;
|
|
1236
|
+
|
|
1237
|
+
const fontsOf = (options: TextStyleOptions, roles: readonly string[]) =>
|
|
1238
|
+
roles.map((role) => options[role as TextStyleType].style.fontFamily);
|
|
1239
|
+
|
|
1240
|
+
const banner = (
|
|
1241
|
+
overrides: Record<string, unknown> = {},
|
|
1242
|
+
): Record<string, unknown> => ({
|
|
1243
|
+
type: 'banner-section',
|
|
1244
|
+
title: 'Banner title',
|
|
1245
|
+
description: 'Banner description',
|
|
1246
|
+
titleStyle: { textStyleType: 'titles' },
|
|
1247
|
+
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
1248
|
+
buttonText: false,
|
|
1249
|
+
buttonTextStyle: { textStyleType: 'buttons' },
|
|
1250
|
+
...overrides,
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const imagesSection = (
|
|
1254
|
+
overrides: Record<string, unknown> = {},
|
|
1255
|
+
): Record<string, unknown> => ({
|
|
1256
|
+
type: 'images-section',
|
|
1257
|
+
title: '',
|
|
1258
|
+
description: '',
|
|
1259
|
+
items: [],
|
|
1260
|
+
...overrides,
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it('T16.1/T16.2 — gives the heading roles the font most title/heading text shows, and the paragraph roles the font most description/paragraph text shows; the look survives the drop, and an unequal override stays', async () => {
|
|
1264
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1265
|
+
|
|
1266
|
+
options.titles.style.fontFamily = 'Anton';
|
|
1267
|
+
options.headings.style.fontFamily = 'Anton';
|
|
1268
|
+
options.paragraphs.style.fontFamily = 'Nunito';
|
|
1269
|
+
|
|
1270
|
+
const model = await runMigration22(
|
|
1271
|
+
siteWith(
|
|
1272
|
+
{
|
|
1273
|
+
pages: [
|
|
1274
|
+
{
|
|
1275
|
+
contents: [
|
|
1276
|
+
banner({
|
|
1277
|
+
titleStyle: { textStyleType: 'titles' },
|
|
1278
|
+
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
1279
|
+
}),
|
|
1280
|
+
banner({
|
|
1281
|
+
titleStyle: { textStyleType: 'headings' },
|
|
1282
|
+
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
1283
|
+
}),
|
|
1284
|
+
// A title stored as an RTE doc, baked in Anton (what the old role drew) — once its
|
|
1285
|
+
// font equals the winning role, the run must relink to the role var, not keep 'Anton'.
|
|
1286
|
+
banner({
|
|
1287
|
+
title: docOf([
|
|
1288
|
+
{ text: 'Baked title', attrs: { fontFamily: 'Anton' } },
|
|
1289
|
+
]),
|
|
1290
|
+
titleStyle: { textStyleType: 'titles' },
|
|
1291
|
+
}),
|
|
1292
|
+
// An own literal that differs from the winning role (Nunito) survives as an override.
|
|
1293
|
+
banner({
|
|
1294
|
+
descriptionStyle: { fontFamily: 'Lato', fontSize: '1em' },
|
|
1295
|
+
}),
|
|
1296
|
+
// The 'nunito' KEY spelling counts toward the Nunito vote (`voteFontName`), but is a
|
|
1297
|
+
// different literal from the winning role's NAME ('Nunito'), so it stays an override.
|
|
1298
|
+
banner({
|
|
1299
|
+
descriptionStyle: { fontFamily: 'nunito', fontSize: '1em' },
|
|
1300
|
+
}),
|
|
1301
|
+
],
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
},
|
|
1305
|
+
options,
|
|
1306
|
+
),
|
|
1307
|
+
);
|
|
1308
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
1309
|
+
const contents = model.pages[0].contents as unknown as Record<
|
|
1310
|
+
string,
|
|
1311
|
+
Record<string, unknown>
|
|
1312
|
+
>[];
|
|
1313
|
+
|
|
1314
|
+
expect(fontsOf(after, HEADING_ROLES)).toEqual(Array(6).fill('Anton'));
|
|
1315
|
+
expect(fontsOf(after, PARAGRAPH_ROLES)).toEqual(Array(3).fill('Nunito'));
|
|
1316
|
+
|
|
1317
|
+
// No title keeps a `fontFamily` override once it equals the role that now carries it.
|
|
1318
|
+
expect(contents[0].titleStyle.fontFamily).toBeUndefined();
|
|
1319
|
+
expect(contents[1].titleStyle.fontFamily).toBeUndefined();
|
|
1320
|
+
|
|
1321
|
+
// The baked run relinks to the role ref, since its literal ('Anton') now equals the role.
|
|
1322
|
+
const bakedTitle = contents[2].title as {
|
|
1323
|
+
content: {
|
|
1324
|
+
content: {
|
|
1325
|
+
marks?: { type: string; attrs?: Record<string, unknown> }[];
|
|
1326
|
+
}[];
|
|
1327
|
+
}[];
|
|
1328
|
+
};
|
|
1329
|
+
const bakedMark = bakedTitle.content[0].content[0].marks?.find(
|
|
1330
|
+
(mark) => mark.type === 'textStyle',
|
|
1331
|
+
);
|
|
1332
|
+
|
|
1333
|
+
expect(bakedMark?.attrs?.fontFamily).toBe('var(--role-font-family)');
|
|
1334
|
+
|
|
1335
|
+
// A Lato override differs from the winning role (Nunito) and survives.
|
|
1336
|
+
expect(contents[3].descriptionStyle.fontFamily).toBe('Lato');
|
|
1337
|
+
|
|
1338
|
+
// The 'nunito' key spelling differs (strictly) from the role's name ('Nunito') too.
|
|
1339
|
+
expect(contents[4].descriptionStyle.fontFamily).toBe('nunito');
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it('T16.3 — a tie goes to the first font found, reading the pages in nav (site) order from the home page, not the stored pages[] order', async () => {
|
|
1343
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1344
|
+
|
|
1345
|
+
options.paragraphs.style.fontFamily = 'Aleo'; // page "home" (routed second in `pages`)
|
|
1346
|
+
options.headings.style.fontFamily = 'Nunito'; // page "other" (stored first in `pages`)
|
|
1347
|
+
|
|
1348
|
+
const model = await runMigration22(
|
|
1349
|
+
siteWith(
|
|
1350
|
+
{
|
|
1351
|
+
pages: [
|
|
1352
|
+
{
|
|
1353
|
+
id: 'other',
|
|
1354
|
+
contents: [
|
|
1355
|
+
banner({ descriptionStyle: { textStyleType: 'headings' } }),
|
|
1356
|
+
],
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
id: 'home',
|
|
1360
|
+
contents: [
|
|
1361
|
+
banner({ descriptionStyle: { textStyleType: 'paragraphs' } }),
|
|
1362
|
+
],
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
nav: {
|
|
1366
|
+
routes: [
|
|
1367
|
+
{ id: 'r-home', pageId: 'home' },
|
|
1368
|
+
{ id: 'r-other', pageId: 'other' },
|
|
1369
|
+
],
|
|
1370
|
+
orders: [
|
|
1371
|
+
{ routeId: 'r-home', subRoutes: [] },
|
|
1372
|
+
{ routeId: 'r-other', subRoutes: [] },
|
|
1373
|
+
],
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
options,
|
|
1377
|
+
),
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
expect(
|
|
1381
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1382
|
+
).toEqual(Array(3).fill('Aleo'));
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it('T16.3 — reversing the nav order flips which page is read first', async () => {
|
|
1386
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1387
|
+
|
|
1388
|
+
options.paragraphs.style.fontFamily = 'Aleo'; // page "home", ranked second below
|
|
1389
|
+
options.headings.style.fontFamily = 'Nunito'; // page "other", ranked first below
|
|
1390
|
+
|
|
1391
|
+
const model = await runMigration22(
|
|
1392
|
+
siteWith(
|
|
1393
|
+
{
|
|
1394
|
+
pages: [
|
|
1395
|
+
{
|
|
1396
|
+
id: 'other',
|
|
1397
|
+
contents: [
|
|
1398
|
+
banner({ descriptionStyle: { textStyleType: 'headings' } }),
|
|
1399
|
+
],
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
id: 'home',
|
|
1403
|
+
contents: [
|
|
1404
|
+
banner({ descriptionStyle: { textStyleType: 'paragraphs' } }),
|
|
1405
|
+
],
|
|
1406
|
+
},
|
|
1407
|
+
],
|
|
1408
|
+
nav: {
|
|
1409
|
+
routes: [
|
|
1410
|
+
{ id: 'r-home', pageId: 'home' },
|
|
1411
|
+
{ id: 'r-other', pageId: 'other' },
|
|
1412
|
+
],
|
|
1413
|
+
// 'other' ranked first this time — the OPPOSITE of the test above.
|
|
1414
|
+
orders: [
|
|
1415
|
+
{ routeId: 'r-other', subRoutes: [] },
|
|
1416
|
+
{ routeId: 'r-home', subRoutes: [] },
|
|
1417
|
+
],
|
|
1418
|
+
},
|
|
1419
|
+
},
|
|
1420
|
+
options,
|
|
1421
|
+
),
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
expect(
|
|
1425
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1426
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it('T16.3 — a tie WITHIN one page goes to the upper section, top to bottom', async () => {
|
|
1430
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1431
|
+
|
|
1432
|
+
options.paragraphs.style.fontFamily = 'Aleo'; // the upper section
|
|
1433
|
+
options.headings.style.fontFamily = 'Nunito'; // the lower section
|
|
1434
|
+
|
|
1435
|
+
const model = await runMigration22(
|
|
1436
|
+
siteWith(
|
|
1437
|
+
{
|
|
1438
|
+
pages: [
|
|
1439
|
+
{
|
|
1440
|
+
contents: [
|
|
1441
|
+
banner({ descriptionStyle: { textStyleType: 'paragraphs' } }),
|
|
1442
|
+
banner({ descriptionStyle: { textStyleType: 'headings' } }),
|
|
1443
|
+
],
|
|
1444
|
+
},
|
|
1445
|
+
],
|
|
1446
|
+
},
|
|
1447
|
+
options,
|
|
1448
|
+
),
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
expect(
|
|
1452
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1453
|
+
).toEqual(Array(3).fill('Aleo'));
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
// Each run's own literal (Aleo, Nunito) votes and the block font (Oswald) does not — proves
|
|
1457
|
+
// per-run voting happens at all. A block-plus-literals implementation also passes this case
|
|
1458
|
+
// (see the tally comment below); the D13 case right after this one is what actually tells
|
|
1459
|
+
// the two apart.
|
|
1460
|
+
it("T16.4 — one vote for each font a doc's runs show", async () => {
|
|
1461
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1462
|
+
|
|
1463
|
+
const model = await runMigration22(
|
|
1464
|
+
siteWith(
|
|
1465
|
+
{
|
|
1466
|
+
pages: [
|
|
1467
|
+
{
|
|
1468
|
+
contents: [
|
|
1469
|
+
// Every run here carries its own literal — the block font (Oswald) must cast no
|
|
1470
|
+
// vote; a block-font-only implementation would instead read it from both runs.
|
|
1471
|
+
banner({
|
|
1472
|
+
description: docOf([
|
|
1473
|
+
{ text: 'Aleo run', attrs: { fontFamily: 'Aleo' } },
|
|
1474
|
+
{ text: 'Nunito run', attrs: { fontFamily: 'Nunito' } },
|
|
1475
|
+
]),
|
|
1476
|
+
descriptionStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1477
|
+
}),
|
|
1478
|
+
banner({
|
|
1479
|
+
description: 'Plain Nunito description',
|
|
1480
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
1481
|
+
}),
|
|
1482
|
+
banner({
|
|
1483
|
+
description: 'Another plain Nunito description',
|
|
1484
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
1485
|
+
}),
|
|
1486
|
+
],
|
|
1487
|
+
},
|
|
1488
|
+
],
|
|
1489
|
+
},
|
|
1490
|
+
options,
|
|
1491
|
+
),
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
// Nunito 3 (the mixed doc's own Nunito run + the two plain descriptions) vs Aleo 1. This
|
|
1495
|
+
// tally is also produced by a block-plus-literals implementation (it would add Oswald once
|
|
1496
|
+
// more, but Nunito's 3 still wins) — it doesn't by itself rule that rule out; the case below
|
|
1497
|
+
// does.
|
|
1498
|
+
expect(
|
|
1499
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1500
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
1501
|
+
|
|
1502
|
+
// The Aleo run's literal differs from the winning role and survives; the Nunito run's now
|
|
1503
|
+
// equals it and relinks to the role ref (look kept through `fillRteRunTypography`).
|
|
1504
|
+
const mixedDescription = (
|
|
1505
|
+
model.pages[0].contents as unknown as {
|
|
1506
|
+
description: {
|
|
1507
|
+
content: {
|
|
1508
|
+
content: {
|
|
1509
|
+
marks?: { type: string; attrs?: Record<string, unknown> }[];
|
|
1510
|
+
}[];
|
|
1511
|
+
}[];
|
|
1512
|
+
};
|
|
1513
|
+
}[]
|
|
1514
|
+
)[0].description;
|
|
1515
|
+
const runMark = (i: number) =>
|
|
1516
|
+
mixedDescription.content[0].content[i].marks?.find(
|
|
1517
|
+
(mark) => mark.type === 'textStyle',
|
|
1518
|
+
);
|
|
1519
|
+
|
|
1520
|
+
expect(runMark(0)?.attrs?.fontFamily).toBe('Aleo');
|
|
1521
|
+
expect(runMark(1)?.attrs?.fontFamily).toBe('var(--role-font-family)');
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
it('T16.4 (D13) — a plain-Oswald banner, the mixed Aleo/Nunito doc under a block Oswald, and an Aleo-only doc under a block Nunito together tell the correct per-run rule apart from three wrong ones', async () => {
|
|
1525
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1526
|
+
|
|
1527
|
+
const model = await runMigration22(
|
|
1528
|
+
siteWith(
|
|
1529
|
+
{
|
|
1530
|
+
pages: [
|
|
1531
|
+
{
|
|
1532
|
+
contents: [
|
|
1533
|
+
banner({
|
|
1534
|
+
description: 'Plain Oswald description',
|
|
1535
|
+
descriptionStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1536
|
+
}),
|
|
1537
|
+
banner({
|
|
1538
|
+
description: docOf([
|
|
1539
|
+
{ text: 'Aleo run', attrs: { fontFamily: 'Aleo' } },
|
|
1540
|
+
{ text: 'Nunito run', attrs: { fontFamily: 'Nunito' } },
|
|
1541
|
+
]),
|
|
1542
|
+
descriptionStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1543
|
+
}),
|
|
1544
|
+
banner({
|
|
1545
|
+
description: docOf([
|
|
1546
|
+
{ text: 'Aleo-only run', attrs: { fontFamily: 'Aleo' } },
|
|
1547
|
+
]),
|
|
1548
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
1549
|
+
}),
|
|
1550
|
+
],
|
|
1551
|
+
},
|
|
1552
|
+
],
|
|
1553
|
+
},
|
|
1554
|
+
options,
|
|
1555
|
+
),
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
// Correct rule: Oswald 1 (the plain banner's block vote) · Aleo 2 (both docs' own Aleo runs,
|
|
1559
|
+
// neither doc's block font votes since every run in each carries its own literal) · Nunito 1
|
|
1560
|
+
// (the mixed doc's own Nunito run) → Aleo wins outright. Three wrong rules all give Oswald
|
|
1561
|
+
// instead, so this is the case that rules each of them out:
|
|
1562
|
+
// - block-plus-literals (the block font AND every literal run vote): Oswald 2, Aleo 2,
|
|
1563
|
+
// Nunito 2 — a three-way tie, broken to Oswald as the first font read.
|
|
1564
|
+
// - block-only (a run's own literal never votes, only the block font): Oswald 2, Nunito 1.
|
|
1565
|
+
// - no-doc-vote (a doc with any literal run casts no vote at all, not even its runs'):
|
|
1566
|
+
// Oswald 1, uncontested.
|
|
1567
|
+
expect(
|
|
1568
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1569
|
+
).toEqual(Array(3).fill('Aleo'));
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
it('T16.4 (B2) — a colour-only run mark (fontFamily: "") votes the block font, not nothing', async () => {
|
|
1573
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1574
|
+
|
|
1575
|
+
const model = await runMigration22(
|
|
1576
|
+
siteWith(
|
|
1577
|
+
{
|
|
1578
|
+
pages: [
|
|
1579
|
+
{
|
|
1580
|
+
contents: [
|
|
1581
|
+
banner({
|
|
1582
|
+
descriptionStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1583
|
+
}),
|
|
1584
|
+
banner({
|
|
1585
|
+
// A colour-only mark parsed from pasted HTML carries `fontFamily: ''`
|
|
1586
|
+
// (FontFamily.ts) — it must vote the block font (Nunito), not cast no vote.
|
|
1587
|
+
description: docOf([
|
|
1588
|
+
{
|
|
1589
|
+
text: 'Coloured run',
|
|
1590
|
+
attrs: { color: '#111111', fontFamily: '' },
|
|
1591
|
+
},
|
|
1592
|
+
]),
|
|
1593
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
1594
|
+
}),
|
|
1595
|
+
banner({
|
|
1596
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
1597
|
+
}),
|
|
1598
|
+
],
|
|
1599
|
+
},
|
|
1600
|
+
],
|
|
1601
|
+
},
|
|
1602
|
+
options,
|
|
1603
|
+
),
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
// Nunito 2 (the coloured run's block font + the plain description) beats Oswald 1 — red
|
|
1607
|
+
// without the fix: the coloured run casts no vote, so it ties 1-1 and Oswald (voted first)
|
|
1608
|
+
// wins the tie-break instead.
|
|
1609
|
+
expect(
|
|
1610
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1611
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
it('T16.5 — testimonial name/position, profile name, itemTextStyle and success messages cast no vote', async () => {
|
|
1615
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1616
|
+
|
|
1617
|
+
const model = await runMigration22(
|
|
1618
|
+
siteWith(
|
|
1619
|
+
{
|
|
1620
|
+
pages: [
|
|
1621
|
+
{
|
|
1622
|
+
contents: [
|
|
1623
|
+
{
|
|
1624
|
+
type: 'testimonial-section',
|
|
1625
|
+
title: '',
|
|
1626
|
+
items: [
|
|
1627
|
+
{
|
|
1628
|
+
id: '1',
|
|
1629
|
+
name: 'A name',
|
|
1630
|
+
position: 'A position',
|
|
1631
|
+
testimonials: 'A quote',
|
|
1632
|
+
},
|
|
1633
|
+
],
|
|
1634
|
+
nameStyle: { fontFamily: 'Oswald' },
|
|
1635
|
+
positionStyle: { fontFamily: 'Oswald' },
|
|
1636
|
+
testimonialStyle: { fontFamily: 'Oswald' },
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
type: 'support-me',
|
|
1640
|
+
title: '',
|
|
1641
|
+
description: 'Support description',
|
|
1642
|
+
titleStyle: {},
|
|
1643
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
1644
|
+
headingStyle: { fontFamily: 'Oswald' }, // fixed i18n labels, not a vote source
|
|
1645
|
+
successMessageStyle: { fontFamily: 'Oswald' },
|
|
1646
|
+
itemTextStyle: { fontFamily: 'Oswald' },
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
},
|
|
1650
|
+
],
|
|
1651
|
+
},
|
|
1652
|
+
options,
|
|
1653
|
+
),
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
expect(
|
|
1657
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1658
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
1659
|
+
// No title/heading text anywhere on this site — the family keeps its default.
|
|
1660
|
+
expect(
|
|
1661
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, HEADING_ROLES),
|
|
1662
|
+
).toEqual(Array(6).fill('Lato'));
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
it("T16.6 — counts the font a legacy-role field DREW (the role's), not its own stored override; a title on Paragraphs counts for the headings", async () => {
|
|
1666
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1667
|
+
|
|
1668
|
+
options.paragraphs.style.fontFamily = 'Tenor Sans';
|
|
1669
|
+
|
|
1670
|
+
const model = await runMigration22(
|
|
1671
|
+
siteWith(
|
|
1672
|
+
{
|
|
1673
|
+
pages: [
|
|
1674
|
+
{
|
|
1675
|
+
contents: [
|
|
1676
|
+
banner({
|
|
1677
|
+
// Production ignored this field's own font (it drew the role's) — the count
|
|
1678
|
+
// must agree, or it would disagree with what B keeps.
|
|
1679
|
+
descriptionStyle: {
|
|
1680
|
+
textStyleType: 'paragraphs',
|
|
1681
|
+
fontFamily: 'Oswald',
|
|
1682
|
+
},
|
|
1683
|
+
// A title linked to the (legacy) Paragraphs role counts for the HEADINGS family.
|
|
1684
|
+
titleStyle: { textStyleType: 'paragraphs' },
|
|
1685
|
+
}),
|
|
1686
|
+
],
|
|
1687
|
+
},
|
|
1688
|
+
],
|
|
1689
|
+
},
|
|
1690
|
+
options,
|
|
1691
|
+
),
|
|
1692
|
+
);
|
|
1693
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
1694
|
+
|
|
1695
|
+
expect(fontsOf(after, PARAGRAPH_ROLES)).toEqual(
|
|
1696
|
+
Array(3).fill('Tenor Sans'),
|
|
1697
|
+
);
|
|
1698
|
+
expect(fontsOf(after, HEADING_ROLES)).toEqual(Array(6).fill('Tenor Sans'));
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
it("T16.6 — a description on no role with no font counts the site's own font, read from its stored KEY", async () => {
|
|
1702
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1703
|
+
|
|
1704
|
+
const model = await runMigration22(
|
|
1705
|
+
siteWith(
|
|
1706
|
+
{
|
|
1707
|
+
pages: [{ contents: [banner({ descriptionStyle: {} })] }],
|
|
1708
|
+
},
|
|
1709
|
+
options,
|
|
1710
|
+
'anton', // the site's font KEY — resolves to the display name 'Anton'
|
|
1711
|
+
),
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
expect(
|
|
1715
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
1716
|
+
).toEqual(Array(3).fill('Anton'));
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
it('T16.7 — the button family counts the header button, a link button label and Save Contact, by field kind not role', async () => {
|
|
1720
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1721
|
+
|
|
1722
|
+
options.buttons.style.fontFamily = 'Oswald';
|
|
1723
|
+
|
|
1724
|
+
const model = await runMigration22(
|
|
1725
|
+
siteWith(
|
|
1726
|
+
{
|
|
1727
|
+
brandPageType: BRAND_PAGE_TYPES.brandSite,
|
|
1728
|
+
pageHeaderDesktop: {
|
|
1729
|
+
headerButton: {
|
|
1730
|
+
isVisible: true,
|
|
1731
|
+
text: 'Contact us',
|
|
1732
|
+
buttonLinkType: 'url',
|
|
1733
|
+
},
|
|
1734
|
+
},
|
|
1735
|
+
pageHeaderMobile: {
|
|
1736
|
+
headerButton: { isVisible: false, text: '', buttonLinkType: 'url' },
|
|
1737
|
+
},
|
|
1738
|
+
pages: [
|
|
1739
|
+
{
|
|
1740
|
+
contents: [
|
|
1741
|
+
{
|
|
1742
|
+
type: 'general-link',
|
|
1743
|
+
title: 'Visit us',
|
|
1744
|
+
titleStyle: { textStyleType: 'buttons' },
|
|
1745
|
+
url: 'https://example.com',
|
|
1746
|
+
},
|
|
1747
|
+
],
|
|
1748
|
+
},
|
|
1749
|
+
],
|
|
1750
|
+
},
|
|
1751
|
+
options,
|
|
1752
|
+
),
|
|
1753
|
+
);
|
|
1754
|
+
|
|
1755
|
+
// Both the (visible, desktop) header button and the link button label drew `buttons`'s Oswald.
|
|
1756
|
+
expect(
|
|
1757
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
1758
|
+
).toBe('Oswald');
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
it('T16.7(b) — the header button counts once even when both desktop and mobile are visible', async () => {
|
|
1762
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1763
|
+
|
|
1764
|
+
const model = await runMigration22(
|
|
1765
|
+
siteWith(
|
|
1766
|
+
{
|
|
1767
|
+
pageHeaderDesktop: {
|
|
1768
|
+
headerButton: {
|
|
1769
|
+
isVisible: true,
|
|
1770
|
+
text: 'Contact us',
|
|
1771
|
+
buttonLinkType: 'url',
|
|
1772
|
+
buttonTextStyle: { fontFamily: 'Aleo', fontSize: '1em' },
|
|
1773
|
+
},
|
|
1774
|
+
},
|
|
1775
|
+
pageHeaderMobile: {
|
|
1776
|
+
headerButton: {
|
|
1777
|
+
isVisible: true,
|
|
1778
|
+
text: 'Contact us',
|
|
1779
|
+
buttonLinkType: 'url',
|
|
1780
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1781
|
+
},
|
|
1782
|
+
},
|
|
1783
|
+
pages: [
|
|
1784
|
+
{
|
|
1785
|
+
contents: [
|
|
1786
|
+
{
|
|
1787
|
+
type: 'general-link',
|
|
1788
|
+
title: 'Visit us',
|
|
1789
|
+
titleStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1790
|
+
url: 'https://example.com',
|
|
1791
|
+
},
|
|
1792
|
+
],
|
|
1793
|
+
},
|
|
1794
|
+
],
|
|
1795
|
+
},
|
|
1796
|
+
options,
|
|
1797
|
+
),
|
|
1798
|
+
);
|
|
1799
|
+
|
|
1800
|
+
// Aleo (desktop, counted once, voted before the page content) wins the 1-1 tie against the
|
|
1801
|
+
// link button's Oswald vote — double-counting the mobile header too would make Oswald 2-1.
|
|
1802
|
+
expect(
|
|
1803
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
1804
|
+
).toBe('Aleo');
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
it('T16.7(b) — a profile title votes for the headings, not the button family', async () => {
|
|
1808
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1809
|
+
|
|
1810
|
+
const model = await runMigration22(
|
|
1811
|
+
siteWith(
|
|
1812
|
+
{
|
|
1813
|
+
pages: [
|
|
1814
|
+
{
|
|
1815
|
+
contents: [
|
|
1816
|
+
{
|
|
1817
|
+
type: 'profile',
|
|
1818
|
+
name: 'A name',
|
|
1819
|
+
title: 'Head of Design',
|
|
1820
|
+
description: '',
|
|
1821
|
+
titleStyle: { fontFamily: 'Anton', fontSize: '1em' },
|
|
1822
|
+
nameStyle: {},
|
|
1823
|
+
descriptionStyle: {},
|
|
1824
|
+
},
|
|
1825
|
+
],
|
|
1826
|
+
},
|
|
1827
|
+
],
|
|
1828
|
+
},
|
|
1829
|
+
options,
|
|
1830
|
+
),
|
|
1831
|
+
);
|
|
1832
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
1833
|
+
|
|
1834
|
+
expect(fontsOf(after, HEADING_ROLES)).toEqual(Array(6).fill('Anton'));
|
|
1835
|
+
expect(after.button.style.fontFamily).toBe('Lato'); // no vote from this field
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
it('T16.7(c) — a hidden header button, a banner with its button off and an empty title cast no vote', async () => {
|
|
1839
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1840
|
+
|
|
1841
|
+
options.buttons.style.fontFamily = 'Oswald'; // would win if anything voted
|
|
1842
|
+
|
|
1843
|
+
const model = await runMigration22(
|
|
1844
|
+
siteWith(
|
|
1845
|
+
{
|
|
1846
|
+
pageHeaderDesktop: {
|
|
1847
|
+
headerButton: {
|
|
1848
|
+
isVisible: false,
|
|
1849
|
+
text: 'Hidden',
|
|
1850
|
+
buttonLinkType: 'url',
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
pages: [
|
|
1854
|
+
{
|
|
1855
|
+
contents: [
|
|
1856
|
+
banner({
|
|
1857
|
+
buttonText: false,
|
|
1858
|
+
buttonTextStyle: { textStyleType: 'buttons' },
|
|
1859
|
+
}),
|
|
1860
|
+
{
|
|
1861
|
+
type: 'general-link',
|
|
1862
|
+
title: '',
|
|
1863
|
+
titleStyle: { textStyleType: 'buttons' },
|
|
1864
|
+
url: '',
|
|
1865
|
+
},
|
|
1866
|
+
],
|
|
1867
|
+
},
|
|
1868
|
+
],
|
|
1869
|
+
},
|
|
1870
|
+
options,
|
|
1871
|
+
),
|
|
1872
|
+
);
|
|
1873
|
+
|
|
1874
|
+
expect(
|
|
1875
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
1876
|
+
).toBe('Lato');
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
it('T16.7(d) — Save Contact votes button text on brand.contact only, from its own font when it has one', async () => {
|
|
1880
|
+
const withSaveContact = await runMigration22(
|
|
1881
|
+
siteWith(
|
|
1882
|
+
{
|
|
1883
|
+
brandPageType: BRAND_PAGE_TYPES.brandContact,
|
|
1884
|
+
pages: [{ contents: [{ type: 'profile', name: 'A name' }] }],
|
|
1885
|
+
saveContactSettings: {
|
|
1886
|
+
isSavingEnabled: true,
|
|
1887
|
+
hideBranding: false,
|
|
1888
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1889
|
+
},
|
|
1890
|
+
},
|
|
1891
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
1892
|
+
),
|
|
1893
|
+
);
|
|
1894
|
+
|
|
1895
|
+
expect(
|
|
1896
|
+
withSaveContact.styles!.textStyles!.textStyleOptions.button.style
|
|
1897
|
+
.fontFamily,
|
|
1898
|
+
).toBe('Oswald');
|
|
1899
|
+
|
|
1900
|
+
// brand.site: the same field never votes (the widget doesn't render there).
|
|
1901
|
+
const brandSite = await runMigration22(
|
|
1902
|
+
siteWith(
|
|
1903
|
+
{
|
|
1904
|
+
brandPageType: BRAND_PAGE_TYPES.brandSite,
|
|
1905
|
+
pages: [{ contents: [{ type: 'profile', name: 'A name' }] }],
|
|
1906
|
+
saveContactSettings: {
|
|
1907
|
+
isSavingEnabled: true,
|
|
1908
|
+
hideBranding: false,
|
|
1909
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1910
|
+
},
|
|
1911
|
+
},
|
|
1912
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
1913
|
+
),
|
|
1914
|
+
);
|
|
1915
|
+
|
|
1916
|
+
expect(
|
|
1917
|
+
brandSite.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
1918
|
+
).toBe('Lato');
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
it('T16.7(d) — Save Contact casts no vote on a brand.contact site with no profile section (S4: the widget never renders without one)', async () => {
|
|
1922
|
+
const model = await runMigration22(
|
|
1923
|
+
siteWith(
|
|
1924
|
+
{
|
|
1925
|
+
brandPageType: BRAND_PAGE_TYPES.brandContact,
|
|
1926
|
+
// A page WITH a (non-profile) section, so this proves the no-vote rule keys off the
|
|
1927
|
+
// missing profile section specifically, not merely an empty `pages` array.
|
|
1928
|
+
pages: [{ contents: [banner()] }],
|
|
1929
|
+
saveContactSettings: {
|
|
1930
|
+
isSavingEnabled: true,
|
|
1931
|
+
hideBranding: false,
|
|
1932
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1933
|
+
},
|
|
1934
|
+
},
|
|
1935
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
1936
|
+
),
|
|
1937
|
+
);
|
|
1938
|
+
|
|
1939
|
+
expect(
|
|
1940
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
1941
|
+
).toBe('Lato');
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it('T16.7(e) — three link-button labels vote the button family, not headings (Q10: a link button is a button element); a profile title votes the headings', async () => {
|
|
1945
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1946
|
+
|
|
1947
|
+
const model = await runMigration22(
|
|
1948
|
+
siteWith(
|
|
1949
|
+
{
|
|
1950
|
+
brandPageType: BRAND_PAGE_TYPES.default,
|
|
1951
|
+
pages: [
|
|
1952
|
+
{
|
|
1953
|
+
contents: [
|
|
1954
|
+
{
|
|
1955
|
+
type: 'general-link',
|
|
1956
|
+
title: 'Visit us',
|
|
1957
|
+
titleStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1958
|
+
url: 'https://example.com',
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
type: 'general-link',
|
|
1962
|
+
title: 'Follow us',
|
|
1963
|
+
titleStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1964
|
+
url: 'https://example.com',
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
type: 'general-link',
|
|
1968
|
+
title: 'Email us',
|
|
1969
|
+
titleStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
1970
|
+
url: 'mailto:hi@example.com',
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
type: 'profile',
|
|
1974
|
+
name: 'A name',
|
|
1975
|
+
title: 'Head of Design',
|
|
1976
|
+
description: '',
|
|
1977
|
+
titleStyle: { fontFamily: 'Anton', fontSize: '1em' },
|
|
1978
|
+
nameStyle: {},
|
|
1979
|
+
descriptionStyle: {},
|
|
1980
|
+
},
|
|
1981
|
+
],
|
|
1982
|
+
},
|
|
1983
|
+
],
|
|
1984
|
+
},
|
|
1985
|
+
options,
|
|
1986
|
+
),
|
|
1987
|
+
);
|
|
1988
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
1989
|
+
|
|
1990
|
+
// If a link's title voted the headings instead, headings would flip to Oswald (3-1) and button
|
|
1991
|
+
// would stay on Lato (no vote) — both assertions below catch that misclassification.
|
|
1992
|
+
expect(after.button.style.fontFamily).toBe('Oswald');
|
|
1993
|
+
expect(fontsOf(after, HEADING_ROLES)).toEqual(Array(6).fill('Anton'));
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
it('T16.7(f) — a direct section button (banner) counts its own non-blank buttonText, not just a link label or the header button', async () => {
|
|
1997
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
1998
|
+
|
|
1999
|
+
const model = await runMigration22(
|
|
2000
|
+
siteWith(
|
|
2001
|
+
{
|
|
2002
|
+
pages: [
|
|
2003
|
+
{
|
|
2004
|
+
contents: [
|
|
2005
|
+
banner({
|
|
2006
|
+
buttonText: 'Shop',
|
|
2007
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2008
|
+
}),
|
|
2009
|
+
],
|
|
2010
|
+
},
|
|
2011
|
+
],
|
|
2012
|
+
},
|
|
2013
|
+
options,
|
|
2014
|
+
),
|
|
2015
|
+
);
|
|
2016
|
+
|
|
2017
|
+
expect(
|
|
2018
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2019
|
+
).toBe('Oswald');
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
it("T16.7(g) — a visible-but-blank desktop header button (S3) doesn't stop the visible mobile one from voting", async () => {
|
|
2023
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2024
|
+
|
|
2025
|
+
const model = await runMigration22(
|
|
2026
|
+
siteWith(
|
|
2027
|
+
{
|
|
2028
|
+
pageHeaderDesktop: {
|
|
2029
|
+
headerButton: { isVisible: true, text: '', buttonLinkType: 'url' },
|
|
2030
|
+
},
|
|
2031
|
+
pageHeaderMobile: {
|
|
2032
|
+
headerButton: {
|
|
2033
|
+
isVisible: true,
|
|
2034
|
+
text: 'Contact us',
|
|
2035
|
+
buttonLinkType: 'url',
|
|
2036
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2037
|
+
},
|
|
2038
|
+
},
|
|
2039
|
+
},
|
|
2040
|
+
options,
|
|
2041
|
+
),
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
expect(
|
|
2045
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2046
|
+
).toBe('Oswald');
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
it('T16.7(h) — a fixed-label button (request-payment) votes the button family from its own font (S3)', async () => {
|
|
2050
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2051
|
+
|
|
2052
|
+
const model = await runMigration22(
|
|
2053
|
+
siteWith(
|
|
2054
|
+
{
|
|
2055
|
+
pages: [
|
|
2056
|
+
{
|
|
2057
|
+
contents: [
|
|
2058
|
+
{
|
|
2059
|
+
type: 'request-payment',
|
|
2060
|
+
title: 'Support us',
|
|
2061
|
+
description: '',
|
|
2062
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2063
|
+
},
|
|
2064
|
+
],
|
|
2065
|
+
},
|
|
2066
|
+
],
|
|
2067
|
+
},
|
|
2068
|
+
options,
|
|
2069
|
+
),
|
|
2070
|
+
);
|
|
2071
|
+
|
|
2072
|
+
expect(
|
|
2073
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2074
|
+
).toBe('Oswald');
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
it('T16.7(h) — support-me votes the button family the same way, with no persisted label to check for blank (S3)', async () => {
|
|
2078
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2079
|
+
|
|
2080
|
+
const model = await runMigration22(
|
|
2081
|
+
siteWith(
|
|
2082
|
+
{
|
|
2083
|
+
pages: [
|
|
2084
|
+
{
|
|
2085
|
+
contents: [
|
|
2086
|
+
{
|
|
2087
|
+
type: 'support-me',
|
|
2088
|
+
title: 'Buy me a coffee',
|
|
2089
|
+
description: '',
|
|
2090
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2091
|
+
},
|
|
2092
|
+
],
|
|
2093
|
+
},
|
|
2094
|
+
],
|
|
2095
|
+
},
|
|
2096
|
+
options,
|
|
2097
|
+
),
|
|
2098
|
+
);
|
|
2099
|
+
|
|
2100
|
+
expect(
|
|
2101
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2102
|
+
).toBe('Oswald');
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
it("T16.7(h) — shop's fixed-label button votes only once it has a product to sell (S3)", async () => {
|
|
2106
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2107
|
+
|
|
2108
|
+
const shopSection = (
|
|
2109
|
+
products: Record<string, unknown>[],
|
|
2110
|
+
): Record<string, unknown> => ({
|
|
2111
|
+
type: 'shop-section',
|
|
2112
|
+
title: '',
|
|
2113
|
+
description: '',
|
|
2114
|
+
products,
|
|
2115
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
const noProducts = await runMigration22(
|
|
2119
|
+
siteWith({ pages: [{ contents: [shopSection([])] }] }, options),
|
|
2120
|
+
);
|
|
2121
|
+
|
|
2122
|
+
expect(
|
|
2123
|
+
noProducts.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2124
|
+
).toBe('Lato');
|
|
2125
|
+
|
|
2126
|
+
const oneProduct = await runMigration22(
|
|
2127
|
+
siteWith(
|
|
2128
|
+
{ pages: [{ contents: [shopSection([{ id: '1' }])] }] },
|
|
2129
|
+
options,
|
|
2130
|
+
),
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
expect(
|
|
2134
|
+
oneProduct.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2135
|
+
).toBe('Oswald');
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
it('T16.8 — nav, footer copyright and their fonts cast no vote', async () => {
|
|
2139
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2140
|
+
|
|
2141
|
+
const model = await runMigration22(
|
|
2142
|
+
siteWith(
|
|
2143
|
+
{
|
|
2144
|
+
pageHeaderDesktop: {
|
|
2145
|
+
navigation: { defaultButtonStyle: { fontFamily: 'Oswald' } },
|
|
2146
|
+
},
|
|
2147
|
+
pageFooterDesktop: { copyrightStyle: { fontFamily: 'Oswald' } },
|
|
2148
|
+
pages: [
|
|
2149
|
+
{
|
|
2150
|
+
contents: [
|
|
2151
|
+
banner({
|
|
2152
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2153
|
+
}),
|
|
2154
|
+
],
|
|
2155
|
+
},
|
|
2156
|
+
],
|
|
2157
|
+
},
|
|
2158
|
+
options,
|
|
2159
|
+
),
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
expect(
|
|
2163
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
2164
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
2165
|
+
});
|
|
2166
|
+
|
|
2167
|
+
it('T16.10 — a site with no counted text at all keeps every role on Lato', async () => {
|
|
2168
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2169
|
+
|
|
2170
|
+
const model = await runMigration22(siteWith({ pages: [] }, options));
|
|
2171
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
2172
|
+
|
|
2173
|
+
expect(
|
|
2174
|
+
fontsOf(after, [...HEADING_ROLES, ...PARAGRAPH_ROLES, 'button']),
|
|
2175
|
+
).toEqual(Array(10).fill('Lato'));
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
it('T16.10 (B1) — a family with no vote keeps Lato, even when the site text font is something else', async () => {
|
|
2179
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2180
|
+
|
|
2181
|
+
// A distinct site font (Anton) — red without the fix: a zero-vote family fell back to the
|
|
2182
|
+
// SITE font, not Lato.
|
|
2183
|
+
const model = await runMigration22(
|
|
2184
|
+
siteWith({ pages: [] }, options, 'anton'),
|
|
2185
|
+
);
|
|
2186
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
2187
|
+
|
|
2188
|
+
expect(
|
|
2189
|
+
fontsOf(after, [...HEADING_ROLES, ...PARAGRAPH_ROLES, 'button']),
|
|
2190
|
+
).toEqual(Array(10).fill('Lato'));
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
it('T16.10 — counted text that is all Lato wins on its own votes, not because it happens to match the site font', async () => {
|
|
2194
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2195
|
+
|
|
2196
|
+
const model = await runMigration22(
|
|
2197
|
+
siteWith(
|
|
2198
|
+
{
|
|
2199
|
+
pages: [
|
|
2200
|
+
{
|
|
2201
|
+
contents: [
|
|
2202
|
+
banner({
|
|
2203
|
+
descriptionStyle: { fontFamily: 'Lato', fontSize: '1em' },
|
|
2204
|
+
}),
|
|
2205
|
+
],
|
|
2206
|
+
},
|
|
2207
|
+
],
|
|
2208
|
+
},
|
|
2209
|
+
options,
|
|
2210
|
+
'anton', // a DIFFERENT site font, so a Lato winner can only come from the vote
|
|
2211
|
+
),
|
|
2212
|
+
);
|
|
2213
|
+
|
|
2214
|
+
expect(
|
|
2215
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
2216
|
+
).toEqual(Array(3).fill('Lato'));
|
|
2217
|
+
|
|
2218
|
+
// S6: the counted field's own literal (Lato) now equals the winning role and drops, rather
|
|
2219
|
+
// than surviving as a redundant override.
|
|
2220
|
+
const contents = model.pages[0].contents as unknown as {
|
|
2221
|
+
descriptionStyle: { fontFamily?: string };
|
|
2222
|
+
}[];
|
|
2223
|
+
|
|
2224
|
+
expect(contents[0].descriptionStyle.fontFamily).toBeUndefined();
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
it('T16.11 — a font outside the font list casts no vote, and its own text keeps it as an override', async () => {
|
|
2228
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2229
|
+
|
|
2230
|
+
const model = await runMigration22(
|
|
2231
|
+
siteWith(
|
|
2232
|
+
{
|
|
2233
|
+
pages: [
|
|
2234
|
+
{
|
|
2235
|
+
contents: [
|
|
2236
|
+
banner({
|
|
2237
|
+
descriptionStyle: { fontFamily: 'Calibri', fontSize: '1em' },
|
|
2238
|
+
}),
|
|
2239
|
+
banner({
|
|
2240
|
+
descriptionStyle: { fontFamily: 'Calibri', fontSize: '1em' },
|
|
2241
|
+
}),
|
|
2242
|
+
banner({
|
|
2243
|
+
descriptionStyle: { fontFamily: 'Calibri', fontSize: '1em' },
|
|
2244
|
+
}),
|
|
2245
|
+
banner({
|
|
2246
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2247
|
+
}),
|
|
2248
|
+
],
|
|
2249
|
+
},
|
|
2250
|
+
],
|
|
2251
|
+
},
|
|
2252
|
+
options,
|
|
2253
|
+
),
|
|
2254
|
+
);
|
|
2255
|
+
|
|
2256
|
+
expect(
|
|
2257
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
2258
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
it("T16.12 — each list item votes on its own, against the section's single paragraphStyle", async () => {
|
|
2262
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2263
|
+
|
|
2264
|
+
const model = await runMigration22(
|
|
2265
|
+
siteWith(
|
|
2266
|
+
{
|
|
2267
|
+
pages: [
|
|
2268
|
+
{
|
|
2269
|
+
contents: [
|
|
2270
|
+
imagesSection({
|
|
2271
|
+
paragraphStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2272
|
+
items: [
|
|
2273
|
+
{ id: '1', paragraph: 'one' },
|
|
2274
|
+
{ id: '2', paragraph: 'two' },
|
|
2275
|
+
{ id: '3', paragraph: 'three' },
|
|
2276
|
+
{ id: '4', paragraph: 'four' },
|
|
2277
|
+
],
|
|
2278
|
+
}),
|
|
2279
|
+
banner({
|
|
2280
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2281
|
+
}),
|
|
2282
|
+
banner({
|
|
2283
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2284
|
+
}),
|
|
2285
|
+
banner({
|
|
2286
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2287
|
+
}),
|
|
2288
|
+
],
|
|
2289
|
+
},
|
|
2290
|
+
],
|
|
2291
|
+
},
|
|
2292
|
+
options,
|
|
2293
|
+
),
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
// 4 item votes for Oswald vs 3 for Nunito.
|
|
2297
|
+
expect(
|
|
2298
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
2299
|
+
).toEqual(Array(3).fill('Oswald'));
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
it('T16.12 — an item with a blank paragraph casts no vote', async () => {
|
|
2303
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2304
|
+
|
|
2305
|
+
const model = await runMigration22(
|
|
2306
|
+
siteWith(
|
|
2307
|
+
{
|
|
2308
|
+
pages: [
|
|
2309
|
+
{
|
|
2310
|
+
contents: [
|
|
2311
|
+
imagesSection({
|
|
2312
|
+
paragraphStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2313
|
+
items: [
|
|
2314
|
+
{ id: '1', paragraph: 'one' },
|
|
2315
|
+
{ id: '2', paragraph: 'two' },
|
|
2316
|
+
{ id: '3', paragraph: '' }, // blank — no vote
|
|
2317
|
+
{ id: '4', paragraph: ' ' }, // whitespace-only — no vote
|
|
2318
|
+
],
|
|
2319
|
+
}),
|
|
2320
|
+
banner({
|
|
2321
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2322
|
+
}),
|
|
2323
|
+
banner({
|
|
2324
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2325
|
+
}),
|
|
2326
|
+
banner({
|
|
2327
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2328
|
+
}),
|
|
2329
|
+
],
|
|
2330
|
+
},
|
|
2331
|
+
],
|
|
2332
|
+
},
|
|
2333
|
+
options,
|
|
2334
|
+
),
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
// 2 real item votes for Oswald vs 3 for Nunito — the two blank items must not tip Oswald to
|
|
2338
|
+
// a tie or a win at 4.
|
|
2339
|
+
expect(
|
|
2340
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
2341
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
it('T16.12 (S8) — a left/right-aligned images-section item past the render cutoff casts no vote', async () => {
|
|
2345
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2346
|
+
|
|
2347
|
+
const model = await runMigration22(
|
|
2348
|
+
siteWith(
|
|
2349
|
+
{
|
|
2350
|
+
pages: [
|
|
2351
|
+
{
|
|
2352
|
+
contents: [
|
|
2353
|
+
// Only the first 3 items render in a left-aligned layout
|
|
2354
|
+
// (`MAX_IMAGES_SECTION_ALIGNMENT_ITEMS_LENGTH`) — 3 real Oswald votes, not 4.
|
|
2355
|
+
imagesSection({
|
|
2356
|
+
displayStyle: 'left-aligned',
|
|
2357
|
+
paragraphStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2358
|
+
items: [
|
|
2359
|
+
{ id: '1', paragraph: 'one' },
|
|
2360
|
+
{ id: '2', paragraph: 'two' },
|
|
2361
|
+
{ id: '3', paragraph: 'three' },
|
|
2362
|
+
{ id: '4', paragraph: 'four' }, // past the cutoff — no vote
|
|
2363
|
+
],
|
|
2364
|
+
}),
|
|
2365
|
+
banner({
|
|
2366
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2367
|
+
}),
|
|
2368
|
+
banner({
|
|
2369
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2370
|
+
}),
|
|
2371
|
+
banner({
|
|
2372
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2373
|
+
}),
|
|
2374
|
+
banner({
|
|
2375
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2376
|
+
}),
|
|
2377
|
+
],
|
|
2378
|
+
},
|
|
2379
|
+
],
|
|
2380
|
+
},
|
|
2381
|
+
options,
|
|
2382
|
+
),
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2385
|
+
// 3 real item votes for Oswald vs 4 for Nunito — Nunito wins. Counting the excluded 4th item
|
|
2386
|
+
// too would tie it 4-4, and Oswald (voted first, the images-section comes first) would win
|
|
2387
|
+
// the tie-break instead.
|
|
2388
|
+
expect(
|
|
2389
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, PARAGRAPH_ROLES),
|
|
2390
|
+
).toEqual(Array(3).fill('Nunito'));
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2393
|
+
it("T16.12 (S5) — each item title votes the headings family against a shapes section's single headingStyle", async () => {
|
|
2394
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2395
|
+
|
|
2396
|
+
const model = await runMigration22(
|
|
2397
|
+
siteWith(
|
|
2398
|
+
{
|
|
2399
|
+
pages: [
|
|
2400
|
+
{
|
|
2401
|
+
contents: [
|
|
2402
|
+
imagesSection({
|
|
2403
|
+
type: 'shapes-section',
|
|
2404
|
+
headingStyle: { fontFamily: 'Anton', fontSize: '1em' },
|
|
2405
|
+
items: [
|
|
2406
|
+
{ id: '1', title: 'One' },
|
|
2407
|
+
{ id: '2', title: 'Two' },
|
|
2408
|
+
],
|
|
2409
|
+
}),
|
|
2410
|
+
banner({
|
|
2411
|
+
titleStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2412
|
+
}),
|
|
2413
|
+
],
|
|
2414
|
+
},
|
|
2415
|
+
],
|
|
2416
|
+
},
|
|
2417
|
+
options,
|
|
2418
|
+
),
|
|
2419
|
+
);
|
|
2420
|
+
|
|
2421
|
+
// 2 item votes for Anton vs 1 for the banner's own title (Nunito).
|
|
2422
|
+
expect(
|
|
2423
|
+
fontsOf(model.styles!.textStyles!.textStyleOptions, HEADING_ROLES),
|
|
2424
|
+
).toEqual(Array(6).fill('Anton'));
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
it("T16.12 (S5) — each item's buttonText votes the button family against an images section's single buttonTextStyle", async () => {
|
|
2428
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2429
|
+
|
|
2430
|
+
const model = await runMigration22(
|
|
2431
|
+
siteWith(
|
|
2432
|
+
{
|
|
2433
|
+
pages: [
|
|
2434
|
+
{
|
|
2435
|
+
contents: [
|
|
2436
|
+
imagesSection({
|
|
2437
|
+
buttonTextStyle: { fontFamily: 'Oswald', fontSize: '1em' },
|
|
2438
|
+
items: [
|
|
2439
|
+
{ id: '1', buttonText: 'Shop' },
|
|
2440
|
+
{ id: '2', buttonText: 'Buy' },
|
|
2441
|
+
],
|
|
2442
|
+
}),
|
|
2443
|
+
banner({
|
|
2444
|
+
buttonText: 'Contact',
|
|
2445
|
+
buttonTextStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2446
|
+
}),
|
|
2447
|
+
],
|
|
2448
|
+
},
|
|
2449
|
+
],
|
|
2450
|
+
},
|
|
2451
|
+
options,
|
|
2452
|
+
),
|
|
2453
|
+
);
|
|
2454
|
+
|
|
2455
|
+
// 2 item votes for Oswald vs 1 for the banner's own direct button (Nunito).
|
|
2456
|
+
expect(
|
|
2457
|
+
model.styles!.textStyles!.textStyleOptions.button.style.fontFamily,
|
|
2458
|
+
).toBe('Oswald');
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
it('T16.9 — a dropped (role-equal) override follows a later role font edit; a kept literal (outside the font list) does not', async () => {
|
|
2462
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2463
|
+
|
|
2464
|
+
const model = await runMigration22(
|
|
2465
|
+
siteWith(
|
|
2466
|
+
{
|
|
2467
|
+
pages: [
|
|
2468
|
+
{
|
|
2469
|
+
contents: [
|
|
2470
|
+
banner({
|
|
2471
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2472
|
+
}),
|
|
2473
|
+
banner({
|
|
2474
|
+
descriptionStyle: { fontFamily: 'Nunito', fontSize: '1em' },
|
|
2475
|
+
}),
|
|
2476
|
+
// Outside the font list — casts no vote, and keeps its own literal.
|
|
2477
|
+
banner({
|
|
2478
|
+
descriptionStyle: { fontFamily: 'Calibri', fontSize: '1em' },
|
|
2479
|
+
}),
|
|
2480
|
+
],
|
|
2481
|
+
},
|
|
2482
|
+
],
|
|
2483
|
+
},
|
|
2484
|
+
options,
|
|
2485
|
+
),
|
|
2486
|
+
);
|
|
2487
|
+
const after = model.styles!.textStyles!.textStyleOptions;
|
|
2488
|
+
const contents = model.pages[0].contents as unknown as Record<
|
|
2489
|
+
string,
|
|
2490
|
+
Record<string, unknown>
|
|
2491
|
+
>[];
|
|
2492
|
+
|
|
2493
|
+
expect(after.paragraph1.style.fontFamily).toBe('Nunito');
|
|
2494
|
+
// A new item on Paragraph 1 (`addOption`'s shape) resolves the role's current font.
|
|
2495
|
+
expect(
|
|
2496
|
+
(
|
|
2497
|
+
resolveTextStyle(
|
|
2498
|
+
{ textStyleType: 'paragraph1' } as unknown as TextStyleDeclaration,
|
|
2499
|
+
after.paragraph1.style,
|
|
2500
|
+
) as Record<string, string>
|
|
2501
|
+
).fontFamily,
|
|
2502
|
+
).toBe('Nunito');
|
|
2503
|
+
|
|
2504
|
+
// A later role font edit reaches the content whose (role-equal) override B dropped…
|
|
2505
|
+
after.paragraph1.style.fontFamily = 'Aleo';
|
|
2506
|
+
|
|
2507
|
+
expect(
|
|
2508
|
+
(
|
|
2509
|
+
resolveTextStyle(
|
|
2510
|
+
contents[0].descriptionStyle as TextStyleDeclaration,
|
|
2511
|
+
after.paragraph1.style,
|
|
2512
|
+
) as Record<string, string>
|
|
2513
|
+
).fontFamily,
|
|
2514
|
+
).toBe('Aleo');
|
|
2515
|
+
// …but not the Calibri block, whose literal survived (it never equalled the role).
|
|
2516
|
+
expect(
|
|
2517
|
+
(
|
|
2518
|
+
resolveTextStyle(
|
|
2519
|
+
contents[2].descriptionStyle as TextStyleDeclaration,
|
|
2520
|
+
after.paragraph1.style,
|
|
2521
|
+
) as Record<string, string>
|
|
2522
|
+
).fontFamily,
|
|
2523
|
+
).toBe('Calibri');
|
|
2524
|
+
});
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
describe('migration-22 · stripLiftRteDoc (C — baked → role var / literal)', () => {
|
|
2528
|
+
const role = { fontFamily: 'Lato', fontSize: '2em', lineHeight: '1.2' };
|
|
2529
|
+
|
|
2530
|
+
it('converts a baked attr that EQUALS the role to the role var token', () => {
|
|
2531
|
+
const doc = docOf([
|
|
2532
|
+
{ text: 'Our ', attrs: { fontSize: '2em', color: 'red' } },
|
|
2533
|
+
{ text: 'Mission', attrs: { fontSize: '2em', color: 'red' } },
|
|
2534
|
+
]);
|
|
2535
|
+
|
|
2536
|
+
stripLiftRteDoc(doc, role);
|
|
2537
|
+
|
|
2538
|
+
expect(attrsOf(doc, 0)?.fontSize).toBe('var(--role-font-size)'); // referenced
|
|
2539
|
+
expect(attrsOf(doc, 1)?.fontSize).toBe('var(--role-font-size)');
|
|
2540
|
+
expect(attrsOf(doc, 0)?.color).toBe('red'); // colour untouched
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
it('keeps a baked attr that DIFFERS from the role as a literal override', () => {
|
|
2544
|
+
const doc = docOf([
|
|
2545
|
+
{ text: 'Our ', attrs: { fontSize: '3em' } },
|
|
2546
|
+
{ text: 'Mission', attrs: { fontSize: '3em' } },
|
|
2547
|
+
]);
|
|
2548
|
+
|
|
2549
|
+
stripLiftRteDoc(doc, role);
|
|
2550
|
+
|
|
2551
|
+
expect(attrsOf(doc, 0)?.fontSize).toBe('3em'); // kept literal (an override)
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
it('reconciles per-run: role-equal → var, divergent → literal', () => {
|
|
2555
|
+
const doc = docOf([
|
|
2556
|
+
{ text: 'Our ', attrs: { fontSize: '2em' } }, // equals role
|
|
2557
|
+
{ text: 'Mission', attrs: { fontSize: '3em' } }, // one word differs
|
|
2558
|
+
]);
|
|
2559
|
+
|
|
2560
|
+
stripLiftRteDoc(doc, role);
|
|
2561
|
+
|
|
2562
|
+
expect(attrsOf(doc, 0)?.fontSize).toBe('var(--role-font-size)');
|
|
2563
|
+
expect(attrsOf(doc, 1)?.fontSize).toBe('3em');
|
|
2564
|
+
});
|
|
2565
|
+
|
|
2566
|
+
it('leaves an already-referenced value untouched', () => {
|
|
2567
|
+
const doc = docOf([
|
|
2568
|
+
{ text: 'hi', attrs: { fontSize: 'var(--role-font-size)' } },
|
|
2569
|
+
]);
|
|
2570
|
+
|
|
2571
|
+
stripLiftRteDoc(doc, role);
|
|
2572
|
+
|
|
2573
|
+
expect(attrsOf(doc, 0)?.fontSize).toBe('var(--role-font-size)');
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
it('fills a run missing a role-defined attr with the role ref (container applies nothing)', () => {
|
|
2577
|
+
// colour-only run: the role's typography must be referenced on the run itself, since the
|
|
2578
|
+
// container class no longer applies it.
|
|
2579
|
+
const doc = docOf([{ text: 'hi', attrs: { color: 'red' } }]);
|
|
2580
|
+
|
|
2581
|
+
stripLiftRteDoc(doc, role);
|
|
2582
|
+
|
|
2583
|
+
const attrs = attrsOf(doc, 0);
|
|
2584
|
+
|
|
2585
|
+
expect(attrs?.fontFamily).toBe('var(--role-font-family)'); // filled
|
|
2586
|
+
expect(attrs?.fontSize).toBe('var(--role-font-size)'); // filled
|
|
2587
|
+
expect(attrs?.lineHeight).toBe('var(--role-line-height)'); // filled
|
|
2588
|
+
expect(attrs?.letterSpacing).toBeUndefined(); // role has none → not filled
|
|
2589
|
+
expect(attrs?.color).toBe('red'); // colour untouched
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
it('bakes a block-level typography override onto the runs as a literal (element applies nothing now)', () => {
|
|
2593
|
+
// colour-only run; the block (styleObj) overrides family to Inter (role is Lato).
|
|
2594
|
+
const doc = docOf([{ text: 'hi', attrs: { color: 'red' } }]);
|
|
2595
|
+
|
|
2596
|
+
stripLiftRteDoc(doc, role, { fontFamily: 'Inter' });
|
|
2597
|
+
|
|
2598
|
+
const attrs = attrsOf(doc, 0);
|
|
2599
|
+
|
|
2600
|
+
expect(attrs?.fontFamily).toBe('Inter'); // block override → literal on the run
|
|
2601
|
+
expect(attrs?.fontSize).toBe('var(--role-font-size)'); // matches role → ref
|
|
2602
|
+
expect(attrs?.lineHeight).toBe('var(--role-line-height)'); // matches role → ref
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
it('keeps a per-run override even when the block also overrides (per-run wins)', () => {
|
|
2606
|
+
const doc = docOf([{ text: 'hi', attrs: { fontFamily: 'Georgia' } }]);
|
|
2607
|
+
|
|
2608
|
+
stripLiftRteDoc(doc, role, { fontFamily: 'Inter' });
|
|
2609
|
+
|
|
2610
|
+
// the run's own override wins over both the block override and the role
|
|
2611
|
+
expect(attrsOf(doc, 0)?.fontFamily).toBe('Georgia');
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
it('never touches colour', () => {
|
|
2615
|
+
const doc = docOf([{ text: 'hi', attrs: { color: 'var(--x)' } }]);
|
|
2616
|
+
|
|
2617
|
+
stripLiftRteDoc(doc, role);
|
|
2618
|
+
|
|
2619
|
+
expect(attrsOf(doc, 0)?.color).toBe('var(--x)');
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2622
|
+
it('skips a non-Tiptap value (legacy HTML string)', () => {
|
|
2623
|
+
expect(() => stripLiftRteDoc('<p>legacy</p>', role)).not.toThrow();
|
|
2624
|
+
});
|
|
2625
|
+
});
|
|
2626
|
+
|
|
2627
|
+
describe('migration-22 · foldSemanticFormattingMarks (C2 — marks → textStyle attrs)', () => {
|
|
2628
|
+
const markDoc = (runs: { text: string; marks?: string[] }[]) => ({
|
|
2629
|
+
type: 'doc',
|
|
2630
|
+
content: [
|
|
2631
|
+
{
|
|
2632
|
+
type: 'paragraph',
|
|
2633
|
+
content: runs.map((r) => ({
|
|
2634
|
+
type: 'text',
|
|
2635
|
+
text: r.text,
|
|
2636
|
+
marks: (r.marks ?? []).map((type) => ({ type })),
|
|
2637
|
+
})),
|
|
2638
|
+
},
|
|
2639
|
+
],
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
const firstRun = (doc: ReturnType<typeof markDoc>) =>
|
|
2643
|
+
doc.content[0].content[0] as {
|
|
2644
|
+
marks: { type: string; attrs?: Record<string, string> }[];
|
|
2645
|
+
};
|
|
2646
|
+
const attrsOf = (doc: ReturnType<typeof markDoc>) =>
|
|
2647
|
+
firstRun(doc).marks.find((m) => m.type === 'textStyle')?.attrs ?? {};
|
|
2648
|
+
const markTypes = (doc: ReturnType<typeof markDoc>) =>
|
|
2649
|
+
firstRun(doc).marks.map((m) => m.type);
|
|
2650
|
+
|
|
2651
|
+
it('folds a bold mark into a fontWeight literal and removes the mark (role not bold)', () => {
|
|
2652
|
+
const doc = markDoc([{ text: 'hi', marks: ['bold'] }]);
|
|
2653
|
+
|
|
2654
|
+
foldSemanticFormattingMarks(doc, { fontWeight: 'normal' });
|
|
2655
|
+
|
|
2656
|
+
expect(attrsOf(doc).fontWeight).toBe('bold'); // differs from role → literal override
|
|
2657
|
+
expect(markTypes(doc)).not.toContain('bold');
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
it('re-links a bold mark to the role var when the role is bold', () => {
|
|
2661
|
+
const doc = markDoc([{ text: 'hi', marks: ['bold'] }]);
|
|
2662
|
+
|
|
2663
|
+
foldSemanticFormattingMarks(doc, { fontWeight: 'bold' });
|
|
2664
|
+
|
|
2665
|
+
expect(attrsOf(doc).fontWeight).toBe('var(--role-font-weight)');
|
|
2666
|
+
expect(markTypes(doc)).not.toContain('bold');
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
it('composes underline + strike into one textDecoration attr and removes both marks', () => {
|
|
2670
|
+
const doc = markDoc([{ text: 'hi', marks: ['underline', 'strike'] }]);
|
|
2671
|
+
|
|
2672
|
+
foldSemanticFormattingMarks(doc, {});
|
|
2673
|
+
|
|
2674
|
+
expect(attrsOf(doc).textDecoration).toBe('underline line-through');
|
|
2675
|
+
expect(markTypes(doc)).not.toContain('underline');
|
|
2676
|
+
expect(markTypes(doc)).not.toContain('strike');
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
it('leaves a run with no formatting marks untouched', () => {
|
|
2680
|
+
const doc = markDoc([
|
|
2681
|
+
{
|
|
2682
|
+
text: 'hi',
|
|
2683
|
+
marks: [], // will carry only a textStyle colour mark below
|
|
2684
|
+
},
|
|
2685
|
+
]);
|
|
2686
|
+
|
|
2687
|
+
firstRun(doc).marks.push({ type: 'textStyle', attrs: { color: 'red' } });
|
|
2688
|
+
|
|
2689
|
+
foldSemanticFormattingMarks(doc, { fontWeight: 'bold' });
|
|
2690
|
+
|
|
2691
|
+
expect(attrsOf(doc)).toEqual({ color: 'red' }); // no weight added
|
|
2692
|
+
expect(markTypes(doc)).toEqual(['textStyle']);
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
it('keeps an existing textStyle attr rather than overwriting it', () => {
|
|
2696
|
+
const doc = markDoc([{ text: 'hi', marks: ['bold'] }]);
|
|
2697
|
+
|
|
2698
|
+
firstRun(doc).marks.push({
|
|
2699
|
+
type: 'textStyle',
|
|
2700
|
+
attrs: { fontWeight: '900' },
|
|
2701
|
+
});
|
|
2702
|
+
|
|
2703
|
+
foldSemanticFormattingMarks(doc, { fontWeight: 'bold' });
|
|
2704
|
+
|
|
2705
|
+
expect(attrsOf(doc).fontWeight).toBe('900'); // pre-existing attr wins
|
|
2706
|
+
expect(markTypes(doc)).not.toContain('bold');
|
|
2707
|
+
});
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2710
|
+
describe('migration-22 · pinLegacyRunWeight (C3 — not-bold legacy weight override)', () => {
|
|
2711
|
+
const runDoc = (attrs?: Record<string, string>) => ({
|
|
2712
|
+
type: 'doc',
|
|
2713
|
+
content: [
|
|
2714
|
+
{
|
|
2715
|
+
type: 'paragraph',
|
|
2716
|
+
content: [
|
|
2717
|
+
{
|
|
2718
|
+
type: 'text',
|
|
2719
|
+
text: 'hi',
|
|
2720
|
+
marks: attrs ? [{ type: 'textStyle', attrs }] : [],
|
|
2721
|
+
},
|
|
2722
|
+
],
|
|
2723
|
+
},
|
|
2724
|
+
],
|
|
2725
|
+
});
|
|
2726
|
+
const weightOf = (doc: ReturnType<typeof runDoc>) =>
|
|
2727
|
+
(
|
|
2728
|
+
doc.content[0].content[0].marks.find((m) => m.type === 'textStyle') as
|
|
2729
|
+
| { attrs?: Record<string, string> }
|
|
2730
|
+
| undefined
|
|
2731
|
+
)?.attrs?.fontWeight;
|
|
2732
|
+
|
|
2733
|
+
it('pins a not-bold legacy block to a `normal` literal under a bold heading tier', () => {
|
|
2734
|
+
const doc = runDoc();
|
|
2735
|
+
|
|
2736
|
+
// block weight resolved to `normal` by job B; the reconnected tier is bold.
|
|
2737
|
+
pinLegacyRunWeight(doc, 'bold', 'normal');
|
|
2738
|
+
|
|
2739
|
+
expect(weightOf(doc)).toBe('normal'); // literal override beats the tier's bold at render
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
it('no-ops when block weight equals the tier (render var-fill follows the role)', () => {
|
|
2743
|
+
const doc = runDoc();
|
|
2744
|
+
|
|
2745
|
+
pinLegacyRunWeight(doc, 'bold', 'bold');
|
|
2746
|
+
|
|
2747
|
+
expect(weightOf(doc)).toBeUndefined(); // left bare → the fill supplies var(--role-font-weight)
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
it('no-ops when the block carries no weight (a non-legacy/canonical block)', () => {
|
|
2751
|
+
const doc = runDoc();
|
|
2752
|
+
|
|
2753
|
+
pinLegacyRunWeight(doc, 'bold', undefined);
|
|
2754
|
+
|
|
2755
|
+
expect(weightOf(doc)).toBeUndefined();
|
|
2756
|
+
});
|
|
2757
|
+
|
|
2758
|
+
it('leaves a run that already carries a weight (a folded bold mark) untouched', () => {
|
|
2759
|
+
const doc = runDoc({ fontWeight: 'bold' });
|
|
2760
|
+
|
|
2761
|
+
pinLegacyRunWeight(doc, 'bold', 'normal');
|
|
2762
|
+
|
|
2763
|
+
expect(weightOf(doc)).toBe('bold'); // per-run weight is authoritative
|
|
2764
|
+
});
|
|
2765
|
+
|
|
2766
|
+
it('leaves a folded role-var weight untouched (bold + bare runs coexisting in one block)', () => {
|
|
2767
|
+
// a bold run under a bold tier folds to the role var — the pin must not overwrite it with the
|
|
2768
|
+
// block’s `normal`, or a mixed heading block would lose its bold runs.
|
|
2769
|
+
const doc = runDoc({ fontWeight: 'var(--role-font-weight)' });
|
|
2770
|
+
|
|
2771
|
+
pinLegacyRunWeight(doc, 'bold', 'normal');
|
|
2772
|
+
|
|
2773
|
+
expect(weightOf(doc)).toBe('var(--role-font-weight)');
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
it('pins a block-bold override as a literal under a normal tier', () => {
|
|
2777
|
+
const doc = runDoc();
|
|
2778
|
+
|
|
2779
|
+
pinLegacyRunWeight(doc, 'normal', 'bold');
|
|
2780
|
+
|
|
2781
|
+
expect(weightOf(doc)).toBe('bold');
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
it('skips a non-Tiptap value (legacy HTML string)', () => {
|
|
2785
|
+
expect(() =>
|
|
2786
|
+
pinLegacyRunWeight('<p>legacy</p>', 'bold', 'normal'),
|
|
2787
|
+
).not.toThrow();
|
|
2788
|
+
});
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
describe('migration-22 · bakeElementColours (D — freeze resolved colour)', () => {
|
|
2792
|
+
const optionsWithBaseCoat = (baseCoat: string): TextStyleOptions => {
|
|
2793
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2794
|
+
|
|
2795
|
+
setRoleColor(options.paragraph1.style, baseCoat);
|
|
2796
|
+
|
|
2797
|
+
return options;
|
|
2798
|
+
};
|
|
2799
|
+
|
|
2800
|
+
it('bakes the base coat onto a non-RTE field that has no colour', () => {
|
|
2801
|
+
const options = optionsWithBaseCoat('#123456');
|
|
2802
|
+
// headingStyle is a shapes per-item style — non-RTE, so baked onto style.color.
|
|
2803
|
+
const content = {
|
|
2804
|
+
type: 'shapes-section',
|
|
2805
|
+
headingStyle: { textStyleType: 'heading2' },
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2809
|
+
|
|
2810
|
+
expect((content.headingStyle as { color?: string }).color).toBe('#123456');
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
it('prefers the role colour over the base coat', () => {
|
|
2814
|
+
const options = optionsWithBaseCoat('#000000');
|
|
2815
|
+
|
|
2816
|
+
setRoleColor(options.heading2.style, '#ff0000');
|
|
2817
|
+
|
|
2818
|
+
const content = {
|
|
2819
|
+
type: 'shapes-section',
|
|
2820
|
+
headingStyle: { textStyleType: 'heading2' },
|
|
2821
|
+
};
|
|
2822
|
+
|
|
2823
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2824
|
+
|
|
2825
|
+
expect((content.headingStyle as { color?: string }).color).toBe('#ff0000');
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
it('keeps a no-role field on the original base coat after reconnect assigns a heading role', async () => {
|
|
2829
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
2830
|
+
|
|
2831
|
+
options.titles.style.fontSize = '2.25em';
|
|
2832
|
+
setRoleColor(options.titles.style, '#ff0000');
|
|
2833
|
+
setRoleColor(options.paragraphs.style, '#000000');
|
|
2834
|
+
|
|
2835
|
+
const content = {
|
|
2836
|
+
type: 'banner-section',
|
|
2837
|
+
title: 'Unlinked heading',
|
|
2838
|
+
titleStyle: { fontSize: '2.25em' },
|
|
2839
|
+
};
|
|
2840
|
+
const model = await runMigration22(
|
|
2841
|
+
siteWith({ pages: [{ contents: [content] }] }, options),
|
|
2842
|
+
);
|
|
2843
|
+
const migrated = model.pages[0].contents[0] as unknown as {
|
|
2844
|
+
title: unknown;
|
|
2845
|
+
titleStyle: { textStyleType?: string; color?: string };
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
expect(migrated.titleStyle.textStyleType).toBe('heading4');
|
|
2849
|
+
expect(runMarkColor(migrated.title)).toBe('#000000');
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
it('leaves an already-coloured field untouched', () => {
|
|
2853
|
+
const options = optionsWithBaseCoat('#000000');
|
|
2854
|
+
const content = {
|
|
2855
|
+
type: 'shapes-section',
|
|
2856
|
+
headingStyle: { textStyleType: 'heading2', color: '#abcdef' },
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2860
|
+
|
|
2861
|
+
expect(content.headingStyle.color).toBe('#abcdef');
|
|
2862
|
+
});
|
|
2863
|
+
|
|
2864
|
+
it('never bakes onto buttonTextStyle (button label colour comes from buttonStyles)', () => {
|
|
2865
|
+
const options = optionsWithBaseCoat('#000000');
|
|
2866
|
+
const content = {
|
|
2867
|
+
type: 'banner-section',
|
|
2868
|
+
buttonTextStyle: { textStyleType: 'button' },
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2871
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2872
|
+
|
|
2873
|
+
expect(
|
|
2874
|
+
(content.buttonTextStyle as { color?: string }).color,
|
|
2875
|
+
).toBeUndefined();
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
it('fills an uncoloured RTE run with the resolved colour (into the mark)', () => {
|
|
2879
|
+
const options = optionsWithBaseCoat('#00aa00');
|
|
2880
|
+
const content = {
|
|
2881
|
+
type: 'banner-section',
|
|
2882
|
+
title: docOf([{ text: 'Hello' }]), // no attrs → uncoloured run
|
|
2883
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2887
|
+
|
|
2888
|
+
expect(runMarkColor(content.title)).toBe('#00aa00');
|
|
2889
|
+
});
|
|
2890
|
+
|
|
2891
|
+
it('leaves an already-coloured RTE run alone (fill-only)', () => {
|
|
2892
|
+
const options = optionsWithBaseCoat('#000000');
|
|
2893
|
+
const content = {
|
|
2894
|
+
type: 'banner-section',
|
|
2895
|
+
title: docOf([{ text: 'Hello', attrs: { color: '#eeeeee' } }]),
|
|
2896
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2900
|
+
|
|
2901
|
+
expect(runMarkColor(content.title)).toBe('#eeeeee');
|
|
2902
|
+
});
|
|
2903
|
+
|
|
2904
|
+
it('bakes shop title colour into the run mark (shop is RTE, BP-5969)', () => {
|
|
2905
|
+
const options = optionsWithBaseCoat('#654321');
|
|
2906
|
+
// shop is RTE now: B1 already made the title a doc, so D bakes colour into the mark.
|
|
2907
|
+
const content = {
|
|
2908
|
+
type: 'shop-section',
|
|
2909
|
+
title: docOf([{ text: 'My shop' }]),
|
|
2910
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
2911
|
+
};
|
|
2912
|
+
|
|
2913
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2914
|
+
|
|
2915
|
+
expect(runMarkColor(content.title)).toBe('#654321');
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
it('bakes onto the style object when a normally-RTE field is a plain string (old site, no mark)', () => {
|
|
2919
|
+
const options = optionsWithBaseCoat('#00aa00');
|
|
2920
|
+
// banner title is normally a doc, but an un-migrated old site left it plain text — there
|
|
2921
|
+
// is no run mark to hold colour, so D must bake onto the style object like a non-RTE field.
|
|
2922
|
+
const content = {
|
|
2923
|
+
type: 'banner-section',
|
|
2924
|
+
title: 'Untouched legacy title',
|
|
2925
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
2926
|
+
};
|
|
2927
|
+
|
|
2928
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2929
|
+
|
|
2930
|
+
expect((content.titleStyle as { color?: string }).color).toBe('#00aa00');
|
|
2931
|
+
});
|
|
2932
|
+
|
|
2933
|
+
it('D then G leave a plain-string RTE field with a resolvable colour (totality gap closed)', () => {
|
|
2934
|
+
const options = optionsWithBaseCoat('#00aa00');
|
|
2935
|
+
const content = {
|
|
2936
|
+
type: 'banner-section',
|
|
2937
|
+
title: 'Untouched legacy title', // plain string → no mark
|
|
2938
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
2939
|
+
};
|
|
2940
|
+
const model = modelWith(content, options);
|
|
2941
|
+
|
|
2942
|
+
bakeElementColours(model, options); // D bakes the resolved colour onto the object
|
|
2943
|
+
stripRteStyleColours(model); // G must NOT strip it — there's no mark to hold it
|
|
2944
|
+
|
|
2945
|
+
expect((content.titleStyle as { color?: string }).color).toBe('#00aa00');
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
it('bakes the footer copyright colour (it lives outside page.contents)', () => {
|
|
2949
|
+
const options = optionsWithBaseCoat('#334455');
|
|
2950
|
+
const model = modelWith({ type: 'shapes-section' }, options);
|
|
2951
|
+
|
|
2952
|
+
// Footer copyright: role-linked, no colour of its own — must be frozen or it loses its
|
|
2953
|
+
// colour once E clears the role and the base coat is removed.
|
|
2954
|
+
(model as unknown as Record<string, unknown>).pageFooterDesktop = {
|
|
2955
|
+
copyrightStyle: { textStyleType: 'paragraph1', textAlign: 'center' },
|
|
2956
|
+
};
|
|
2957
|
+
(model as unknown as Record<string, unknown>).pageFooterMobile = {
|
|
2958
|
+
copyrightStyle: { textStyleType: 'paragraph1', textAlign: 'center' },
|
|
2959
|
+
};
|
|
2960
|
+
|
|
2961
|
+
bakeElementColours(model, options);
|
|
2962
|
+
|
|
2963
|
+
const footerColour = (m: BrandPageModel, key: string): string | undefined =>
|
|
2964
|
+
(m as unknown as Record<string, { copyrightStyle: { color?: string } }>)[
|
|
2965
|
+
key
|
|
2966
|
+
].copyrightStyle.color;
|
|
2967
|
+
|
|
2968
|
+
expect(footerColour(model, 'pageFooterDesktop')).toBe('#334455');
|
|
2969
|
+
expect(footerColour(model, 'pageFooterMobile')).toBe('#334455');
|
|
2970
|
+
});
|
|
2971
|
+
|
|
2972
|
+
// FB-1 (QA scope): a plain text block on a dark section band freezes to the dark base coat.
|
|
2973
|
+
// bakeElementColours resolves colour from style → containerDefaults → role → base coat, and
|
|
2974
|
+
// NEVER reads the block's own section background. Only the container-bound types
|
|
2975
|
+
// (mailing-list / request-payment / support-me / contact-form) re-contrast against their
|
|
2976
|
+
// surface, so a plain text block on a dark band is baked dark = black-on-dark. This test
|
|
2977
|
+
// characterises the current behaviour; if migration ever contrasts plain text blocks, flip it.
|
|
2978
|
+
it('FB-1 — freezes the dark base coat onto a text block on a dark band (no band contrast)', () => {
|
|
2979
|
+
const options = optionsWithBaseCoat('#000000');
|
|
2980
|
+
const content = {
|
|
2981
|
+
type: 'banner-section',
|
|
2982
|
+
backgroundStyle: { backgroundColor: '#0a0a0a' }, // dark band — not read by the bake
|
|
2983
|
+
title: 'On a dark band', // plain string → colour bakes onto the style object
|
|
2984
|
+
titleStyle: { textStyleType: 'paragraph1' },
|
|
2985
|
+
};
|
|
2986
|
+
|
|
2987
|
+
bakeElementColours(modelWith(content, options), options);
|
|
2988
|
+
|
|
2989
|
+
expect((content.titleStyle as { color?: string }).color).toBe('#000000');
|
|
2990
|
+
});
|
|
2991
|
+
|
|
2992
|
+
describe('social-links icon fill (always stored, never read from the title)', () => {
|
|
2993
|
+
const socialLinks = (
|
|
2994
|
+
iconFillStyles?: BrandPageCssStyle,
|
|
2995
|
+
): Record<string, unknown> => ({
|
|
2996
|
+
type: 'social-links',
|
|
2997
|
+
title: docOf([{ text: 'Follow us', attrs: { color: '#0000ff' } }]),
|
|
2998
|
+
titleStyle: { textStyleType: 'heading4' },
|
|
2999
|
+
...(iconFillStyles ? { iconFillStyles } : {}),
|
|
3000
|
+
});
|
|
3001
|
+
|
|
3002
|
+
const iconFill = (content: Record<string, unknown>): string | undefined =>
|
|
3003
|
+
(content.iconFillStyles as BrandPageCssStyle | undefined)?.fill;
|
|
3004
|
+
|
|
3005
|
+
it('freezes the base coat the icons painted onto an unset fill', () => {
|
|
3006
|
+
const options = optionsWithBaseCoat('#123456');
|
|
3007
|
+
const content = socialLinks({});
|
|
3008
|
+
|
|
3009
|
+
bakeElementColours(modelWith(content, options), options);
|
|
3010
|
+
|
|
3011
|
+
expect(iconFill(content)).toBe('#123456');
|
|
3012
|
+
});
|
|
3013
|
+
|
|
3014
|
+
it('bakes a section with no iconFillStyles at all', () => {
|
|
3015
|
+
const options = optionsWithBaseCoat('#123456');
|
|
3016
|
+
const content = socialLinks();
|
|
3017
|
+
|
|
3018
|
+
bakeElementColours(modelWith(content, options), options);
|
|
3019
|
+
|
|
3020
|
+
expect(iconFill(content)).toBe('#123456');
|
|
3021
|
+
});
|
|
3022
|
+
|
|
3023
|
+
it('leaves an explicit icon fill untouched', () => {
|
|
3024
|
+
const options = optionsWithBaseCoat('#123456');
|
|
3025
|
+
const content = socialLinks({ fill: '#abcdef' });
|
|
3026
|
+
|
|
3027
|
+
bakeElementColours(modelWith(content, options), options);
|
|
3028
|
+
|
|
3029
|
+
expect(iconFill(content)).toBe('#abcdef');
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3032
|
+
it('bakes over a stored empty-string fill, same as an absent one', () => {
|
|
3033
|
+
const options = optionsWithBaseCoat('#123456');
|
|
3034
|
+
const content = socialLinks({ fill: '' });
|
|
3035
|
+
|
|
3036
|
+
bakeElementColours(modelWith(content, options), options);
|
|
3037
|
+
|
|
3038
|
+
expect(iconFill(content)).toBe('#123456');
|
|
3039
|
+
});
|
|
3040
|
+
|
|
3041
|
+
// migration-22.ts's docblock names this job D's one exception (ADR-0020). Run through the
|
|
3042
|
+
// real pipeline (`runMigration22`), not hand-strung internal functions, so what composing
|
|
3043
|
+
// A and D actually does — not each in isolation — is what's proved.
|
|
3044
|
+
it('the v21 → A → D chain: no legacy paragraphs colour means no base coat for D to freeze', async () => {
|
|
3045
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3046
|
+
|
|
3047
|
+
expect(roleColorOf(options.paragraphs.style)).toBeUndefined(); // the v21 fixture's premise
|
|
3048
|
+
|
|
3049
|
+
const content = socialLinks({});
|
|
3050
|
+
const model = await runMigration22(
|
|
3051
|
+
siteWith({ pages: [{ contents: [content] }] }, options),
|
|
3052
|
+
);
|
|
3053
|
+
const migrated = model.pages[0].contents[0] as unknown as Record<
|
|
3054
|
+
string,
|
|
3055
|
+
unknown
|
|
3056
|
+
>;
|
|
3057
|
+
|
|
3058
|
+
// No base coat for D to freeze → the icon gets the same NEW contrast colour a fresh
|
|
3059
|
+
// title would, not the title's own run mark colour (#0000ff).
|
|
3060
|
+
expect(iconFill(migrated)).toBeTruthy();
|
|
3061
|
+
expect(iconFill(migrated)).not.toBe('#0000ff');
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
it('the v21 → A → D chain: a legacy paragraphs colour becomes the base coat D freezes', async () => {
|
|
3065
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3066
|
+
|
|
3067
|
+
setRoleColor(options.paragraphs.style, '#123456');
|
|
3068
|
+
|
|
3069
|
+
const content = socialLinks({});
|
|
3070
|
+
const model = await runMigration22(
|
|
3071
|
+
siteWith({ pages: [{ contents: [content] }] }, options),
|
|
3072
|
+
);
|
|
3073
|
+
const migrated = model.pages[0].contents[0] as unknown as Record<
|
|
3074
|
+
string,
|
|
3075
|
+
unknown
|
|
3076
|
+
>;
|
|
3077
|
+
|
|
3078
|
+
// A seeds paragraph1 from the legacy `paragraphs` colour; D freezes THAT base coat,
|
|
3079
|
+
// not a fresh contrast colour.
|
|
3080
|
+
expect(iconFill(migrated)).toBe('#123456');
|
|
3081
|
+
});
|
|
3082
|
+
|
|
3083
|
+
it('with no base coat, bakes the contrast colour a new title gets — not the title mark', () => {
|
|
3084
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3085
|
+
const content = socialLinks({});
|
|
3086
|
+
const model = modelWith(content, options);
|
|
3087
|
+
const newTitle = {
|
|
3088
|
+
type: 'social-links',
|
|
3089
|
+
titleStyle: { textStyleType: 'heading4' },
|
|
3090
|
+
} as unknown as Content;
|
|
3091
|
+
|
|
3092
|
+
bakeElementColours(model, options);
|
|
3093
|
+
applyTextContrast(newTitle, model.styles, model.brandPageType);
|
|
3094
|
+
|
|
3095
|
+
const contrastColour = (
|
|
3096
|
+
newTitle as unknown as { titleStyle: { color?: string } }
|
|
3097
|
+
).titleStyle.color;
|
|
3098
|
+
|
|
3099
|
+
expect(contrastColour).toBeTruthy();
|
|
3100
|
+
expect(iconFill(content)).toBe(contrastColour);
|
|
3101
|
+
expect(iconFill(content)).not.toBe('#0000ff');
|
|
3102
|
+
});
|
|
3103
|
+
});
|
|
3104
|
+
});
|
|
3105
|
+
|
|
3106
|
+
describe('migration-22 · convertPlainRteFields (B1 — plain text → Tiptap doc)', () => {
|
|
3107
|
+
it('converts a plain-string RTE field to a Tiptap doc', async () => {
|
|
3108
|
+
const content = {
|
|
3109
|
+
type: 'banner-section',
|
|
3110
|
+
title: 'Hello world',
|
|
3111
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
await convertPlainRteFields(
|
|
3115
|
+
modelWithContent(content),
|
|
3116
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3117
|
+
);
|
|
3118
|
+
|
|
3119
|
+
expect(isTiptapDocument(content.title)).toBe(true);
|
|
3120
|
+
expect(JSON.stringify(content.title)).toContain('Hello world');
|
|
3121
|
+
});
|
|
3122
|
+
|
|
3123
|
+
it('stamps textAlign from the style object onto the block node', async () => {
|
|
3124
|
+
const content = {
|
|
3125
|
+
type: 'banner-section',
|
|
3126
|
+
title: 'Centered',
|
|
3127
|
+
titleStyle: { textStyleType: 'heading1', textAlign: 'center' },
|
|
3128
|
+
};
|
|
3129
|
+
|
|
3130
|
+
await convertPlainRteFields(
|
|
3131
|
+
modelWithContent(content),
|
|
3132
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3133
|
+
);
|
|
3134
|
+
|
|
3135
|
+
const doc = content.title as unknown as {
|
|
3136
|
+
content: { attrs?: { textAlign?: string } }[];
|
|
3137
|
+
};
|
|
3138
|
+
|
|
3139
|
+
expect(doc.content[0].attrs?.textAlign).toBe('center');
|
|
3140
|
+
});
|
|
3141
|
+
|
|
3142
|
+
it('bakes a not-bold legacy weight as a normal literal when converting under a bold tier (C3)', async () => {
|
|
3143
|
+
const content = {
|
|
3144
|
+
type: 'banner-section',
|
|
3145
|
+
title: 'Plain heading',
|
|
3146
|
+
// the post-reconnect state: linked to a bold heading tier, weight materialised to normal.
|
|
3147
|
+
titleStyle: { textStyleType: 'heading1', fontWeight: 'normal' },
|
|
3148
|
+
};
|
|
3149
|
+
|
|
3150
|
+
await convertPlainRteFields(
|
|
3151
|
+
modelWithContent(content),
|
|
3152
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3153
|
+
);
|
|
3154
|
+
|
|
3155
|
+
const run = (
|
|
3156
|
+
content.title as unknown as {
|
|
3157
|
+
content: {
|
|
3158
|
+
content: {
|
|
3159
|
+
marks?: { type: string; attrs?: Record<string, string> }[];
|
|
3160
|
+
}[];
|
|
3161
|
+
}[];
|
|
3162
|
+
}
|
|
3163
|
+
).content[0].content[0];
|
|
3164
|
+
const weight = run.marks?.find((m) => m.type === 'textStyle')?.attrs
|
|
3165
|
+
?.fontWeight;
|
|
3166
|
+
|
|
3167
|
+
expect(weight).toBe('normal'); // literal override, NOT var(--role-font-weight) = bold
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
// S4 — this job bakes the block override + role typography onto the run with migration-22's
|
|
3171
|
+
// local resolver snapshot. It preserves D3's current effective-role rule: a stored neutral is
|
|
3172
|
+
// literal only when it differs from what the role actually resolves to, not merely its raw value.
|
|
3173
|
+
it('bakes a neutral fontStyle/textDecoration/letterSpacing as a literal under a role that sets the opposite (D3)', async () => {
|
|
3174
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3175
|
+
|
|
3176
|
+
options.heading1.style.fontStyle = 'italic';
|
|
3177
|
+
options.heading1.style.textDecoration = 'underline';
|
|
3178
|
+
options.heading1.style.letterSpacing = '0.5px';
|
|
3179
|
+
|
|
3180
|
+
const content = {
|
|
3181
|
+
type: 'banner-section',
|
|
3182
|
+
title: 'Plain heading',
|
|
3183
|
+
titleStyle: {
|
|
3184
|
+
textStyleType: 'heading1',
|
|
3185
|
+
fontStyle: 'normal',
|
|
3186
|
+
textDecoration: 'none',
|
|
3187
|
+
letterSpacing: 'normal',
|
|
3188
|
+
},
|
|
3189
|
+
};
|
|
3190
|
+
|
|
3191
|
+
await convertPlainRteFields(modelWithContent(content), options);
|
|
3192
|
+
|
|
3193
|
+
const run = (
|
|
3194
|
+
content.title as unknown as {
|
|
3195
|
+
content: {
|
|
3196
|
+
content: {
|
|
3197
|
+
marks?: { type: string; attrs?: Record<string, string> }[];
|
|
3198
|
+
}[];
|
|
3199
|
+
}[];
|
|
3200
|
+
}
|
|
3201
|
+
).content[0].content[0];
|
|
3202
|
+
const attrs = run.marks?.find((m) => m.type === 'textStyle')?.attrs;
|
|
3203
|
+
|
|
3204
|
+
expect(attrs?.fontStyle).toBe('normal'); // literal, NOT var(--role-font-style) = italic
|
|
3205
|
+
expect(attrs?.textDecoration).toBe('none'); // literal, NOT var(--role-text-decoration)
|
|
3206
|
+
expect(attrs?.letterSpacing).toBe('normal'); // literal, NOT var(--role-letter-spacing)
|
|
3207
|
+
});
|
|
3208
|
+
|
|
3209
|
+
it('uses the migration-local typography snapshot, not the mutable render helper', async () => {
|
|
3210
|
+
const runtimeFill = vi
|
|
3211
|
+
.spyOn(textRoleCss, 'fillRteRunTypography')
|
|
3212
|
+
.mockImplementation(() => {
|
|
3213
|
+
throw new Error('runtime fill must not affect migration-22');
|
|
3214
|
+
});
|
|
3215
|
+
const runtimeWeightPin = vi
|
|
3216
|
+
.spyOn(rteRoleLift, 'pinLegacyRunWeight')
|
|
3217
|
+
.mockImplementation(() => {
|
|
3218
|
+
throw new Error('runtime C3 pin must not affect migration-22');
|
|
3219
|
+
});
|
|
3220
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3221
|
+
const content = {
|
|
3222
|
+
type: 'banner-section',
|
|
3223
|
+
title: 'Frozen output',
|
|
3224
|
+
titleStyle: {
|
|
3225
|
+
textStyleType: 'heading1',
|
|
3226
|
+
fontStyle: 'normal',
|
|
3227
|
+
},
|
|
3228
|
+
};
|
|
3229
|
+
|
|
3230
|
+
try {
|
|
3231
|
+
await convertPlainRteFields(modelWithContent(content), options);
|
|
3232
|
+
|
|
3233
|
+
const attrs = (
|
|
3234
|
+
content.title as unknown as {
|
|
3235
|
+
content: {
|
|
3236
|
+
content: {
|
|
3237
|
+
marks: { type: string; attrs: Record<string, string> }[];
|
|
3238
|
+
}[];
|
|
3239
|
+
}[];
|
|
3240
|
+
}
|
|
3241
|
+
).content[0].content[0].marks.find(
|
|
3242
|
+
(mark) => mark.type === 'textStyle',
|
|
3243
|
+
)?.attrs;
|
|
3244
|
+
|
|
3245
|
+
expect(runtimeFill).not.toHaveBeenCalled();
|
|
3246
|
+
expect(runtimeWeightPin).not.toHaveBeenCalled();
|
|
3247
|
+
expect(attrs?.fontStyle).toBe('var(--role-font-style)');
|
|
3248
|
+
} finally {
|
|
3249
|
+
runtimeFill.mockRestore();
|
|
3250
|
+
runtimeWeightPin.mockRestore();
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
it('converts shop title (plain string) to a Tiptap doc — RTE restored (BP-5969)', async () => {
|
|
3255
|
+
const content = {
|
|
3256
|
+
type: 'shop-section',
|
|
3257
|
+
title: 'My shop',
|
|
3258
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
3259
|
+
};
|
|
3260
|
+
|
|
3261
|
+
await convertPlainRteFields(
|
|
3262
|
+
modelWithContent(content),
|
|
3263
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3264
|
+
);
|
|
3265
|
+
|
|
3266
|
+
expect(isTiptapDocument(content.title)).toBe(true);
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3269
|
+
it('leaves an existing Tiptap doc untouched (no reconversion)', async () => {
|
|
3270
|
+
const doc = docOf([{ text: 'Rich' }]);
|
|
3271
|
+
const content = {
|
|
3272
|
+
type: 'banner-section',
|
|
3273
|
+
title: doc,
|
|
3274
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3277
|
+
await convertPlainRteFields(
|
|
3278
|
+
modelWithContent(content),
|
|
3279
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3280
|
+
);
|
|
3281
|
+
|
|
3282
|
+
expect(content.title).toBe(doc); // same reference — untouched
|
|
3283
|
+
});
|
|
3284
|
+
|
|
3285
|
+
it('converts a plain-string textItems[].text to a Tiptap doc', async () => {
|
|
3286
|
+
const content = {
|
|
3287
|
+
type: 'text',
|
|
3288
|
+
textItems: [
|
|
3289
|
+
{ text: 'plain item', textStyle: { textStyleType: 'paragraph1' } },
|
|
3290
|
+
],
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3293
|
+
await convertPlainRteFields(
|
|
3294
|
+
modelWithContent(content),
|
|
3295
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3296
|
+
);
|
|
3297
|
+
|
|
3298
|
+
expect(isTiptapDocument(content.textItems[0].text)).toBe(true);
|
|
3299
|
+
expect(JSON.stringify(content.textItems[0].text)).toContain('plain item');
|
|
3300
|
+
});
|
|
3301
|
+
|
|
3302
|
+
it('leaves an empty string alone', async () => {
|
|
3303
|
+
const content = {
|
|
3304
|
+
type: 'banner-section',
|
|
3305
|
+
title: '',
|
|
3306
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
3307
|
+
};
|
|
3308
|
+
|
|
3309
|
+
await convertPlainRteFields(
|
|
3310
|
+
modelWithContent(content),
|
|
3311
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3312
|
+
);
|
|
3313
|
+
|
|
3314
|
+
expect(content.title).toBe('');
|
|
3315
|
+
});
|
|
3316
|
+
|
|
3317
|
+
it('converts percent-encoded newlines (%0A) into hard breaks, not a literal run', async () => {
|
|
3318
|
+
const content = {
|
|
3319
|
+
type: 'banner-section',
|
|
3320
|
+
title: 'Line one%0ALine two',
|
|
3321
|
+
titleStyle: { textStyleType: 'heading1' },
|
|
3322
|
+
};
|
|
3323
|
+
|
|
3324
|
+
await convertPlainRteFields(
|
|
3325
|
+
modelWithContent(content),
|
|
3326
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3327
|
+
);
|
|
3328
|
+
|
|
3329
|
+
expect(isTiptapDocument(content.title)).toBe(true);
|
|
3330
|
+
|
|
3331
|
+
const runs = (
|
|
3332
|
+
content.title as unknown as {
|
|
3333
|
+
content: { content: { type: string; text?: string }[] }[];
|
|
3334
|
+
}
|
|
3335
|
+
).content[0].content;
|
|
3336
|
+
|
|
3337
|
+
// The line break survives as text + hardBreak + text — never a literal "%0A".
|
|
3338
|
+
expect(runs.some((n) => n.type === 'hardBreak')).toBe(true);
|
|
3339
|
+
expect(JSON.stringify(content.title)).not.toContain('%0A');
|
|
3340
|
+
expect(runs.filter((n) => n.type === 'text').map((n) => n.text)).toEqual([
|
|
3341
|
+
'Line one',
|
|
3342
|
+
'Line two',
|
|
3343
|
+
]);
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
// Regression (fa0e0db3 "Lift off"): a plain-text field carrying a block-level typography
|
|
3347
|
+
// override never passed through job C (still a string), so the converted run must be baked
|
|
3348
|
+
// here — else it holds colour only and the maker toolbar falls back to the role, showing the
|
|
3349
|
+
// wrong font/size while the renderer still paints the override (editor ≠ render).
|
|
3350
|
+
it('bakes the block-level typography override onto the converted run (role var or literal)', async () => {
|
|
3351
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3352
|
+
const role = options.heading2.style;
|
|
3353
|
+
|
|
3354
|
+
role.fontFamily = 'Lato';
|
|
3355
|
+
role.fontSize = '5.25em';
|
|
3356
|
+
role.fontWeight = 'bold';
|
|
3357
|
+
|
|
3358
|
+
const content = {
|
|
3359
|
+
type: 'banner-section',
|
|
3360
|
+
title: 'Lift off',
|
|
3361
|
+
titleStyle: {
|
|
3362
|
+
textStyleType: 'heading2',
|
|
3363
|
+
fontFamily: 'Playwrite AT', // block override ≠ role → literal
|
|
3364
|
+
fontSize: '5.75em', // block override ≠ role → literal
|
|
3365
|
+
fontWeight: 'bold', // equals role → role var (follows role edits)
|
|
3366
|
+
fontStyle: 'normal', // inert neutral → role var
|
|
3367
|
+
textDecoration: 'none', // inert neutral → role var
|
|
3368
|
+
},
|
|
3369
|
+
};
|
|
3370
|
+
|
|
3371
|
+
await convertPlainRteFields(modelWithContent(content), options);
|
|
3372
|
+
|
|
3373
|
+
const attrs = (
|
|
3374
|
+
content.title as unknown as {
|
|
3375
|
+
content: {
|
|
3376
|
+
content: {
|
|
3377
|
+
marks: { type: string; attrs: Record<string, string> }[];
|
|
3378
|
+
}[];
|
|
3379
|
+
}[];
|
|
3380
|
+
}
|
|
3381
|
+
).content[0].content[0].marks.find((m) => m.type === 'textStyle')?.attrs;
|
|
3382
|
+
|
|
3383
|
+
// Genuine overrides ride through as raw literals — the toolbar reads the real font + size.
|
|
3384
|
+
expect(attrs?.fontFamily).toBe('Playwrite AT');
|
|
3385
|
+
expect(attrs?.fontSize).toBe('5.75em');
|
|
3386
|
+
// Role-equal / inert-neutral attrs follow the role via its var (a role edit repaints them).
|
|
3387
|
+
expect(attrs?.fontWeight).toBe('var(--role-font-weight)');
|
|
3388
|
+
expect(attrs?.fontStyle).toBe('var(--role-font-style)');
|
|
3389
|
+
expect(attrs?.textDecoration).toBe('var(--role-text-decoration)');
|
|
3390
|
+
});
|
|
3391
|
+
});
|
|
3392
|
+
|
|
3393
|
+
describe('migration-22 · clearRoleColours (E — retire role colour)', () => {
|
|
3394
|
+
it('clears colour from every role definition', () => {
|
|
3395
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3396
|
+
|
|
3397
|
+
setRoleColor(options.heading1.style, '#ff0000');
|
|
3398
|
+
setRoleColor(options.paragraph1.style, '#00ff00');
|
|
3399
|
+
|
|
3400
|
+
clearRoleColours(options);
|
|
3401
|
+
|
|
3402
|
+
expect(roleColorOf(options.heading1.style)).toBeUndefined();
|
|
3403
|
+
expect(roleColorOf(options.paragraph1.style)).toBeUndefined();
|
|
3404
|
+
// typography is untouched
|
|
3405
|
+
expect(options.heading1.style.fontSize).toBeDefined();
|
|
3406
|
+
});
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3409
|
+
describe('migration-22 · stripRteStyleColours (G — colour-decouple RTE style objects)', () => {
|
|
3410
|
+
it('removes ONLY colour from an RTE-paired field (typography kept) and leaves non-RTE fields untouched', () => {
|
|
3411
|
+
const content = {
|
|
3412
|
+
type: 'shapes-section',
|
|
3413
|
+
// titleStyle/descriptionStyle are RTE-paired on shapes → colour removed, typography kept
|
|
3414
|
+
titleStyle: {
|
|
3415
|
+
textStyleType: 'heading1',
|
|
3416
|
+
color: '#ff0000',
|
|
3417
|
+
fontSize: '2em',
|
|
3418
|
+
textAlign: 'center',
|
|
3419
|
+
applyAutoScale: true,
|
|
3420
|
+
},
|
|
3421
|
+
// headingStyle is a per-item style → NOT RTE → left fully intact (colour included)
|
|
3422
|
+
headingStyle: {
|
|
3423
|
+
textStyleType: 'heading2',
|
|
3424
|
+
color: '#0000ff',
|
|
3425
|
+
fontSize: '1em',
|
|
3426
|
+
},
|
|
3427
|
+
};
|
|
3428
|
+
|
|
3429
|
+
stripRteStyleColours(modelWithContent(content));
|
|
3430
|
+
|
|
3431
|
+
// colour gone; typography/textAlign/applyAutoScale (a real block override) preserved.
|
|
3432
|
+
expect(content.titleStyle).toEqual({
|
|
3433
|
+
textStyleType: 'heading1',
|
|
3434
|
+
fontSize: '2em',
|
|
3435
|
+
textAlign: 'center',
|
|
3436
|
+
applyAutoScale: true,
|
|
3437
|
+
});
|
|
3438
|
+
expect(content.headingStyle).toEqual({
|
|
3439
|
+
textStyleType: 'heading2',
|
|
3440
|
+
color: '#0000ff',
|
|
3441
|
+
fontSize: '1em',
|
|
3442
|
+
});
|
|
3443
|
+
});
|
|
3444
|
+
|
|
3445
|
+
it('strips shop titleStyle colour once it is doc-backed (shop is RTE, BP-5969)', () => {
|
|
3446
|
+
const content = {
|
|
3447
|
+
type: 'shop-section',
|
|
3448
|
+
// colour now lives in the run mark, so G removes it from the style object; typography stays.
|
|
3449
|
+
title: docOf([{ text: 'My shop', attrs: { color: '#ff0000' } }]),
|
|
3450
|
+
titleStyle: {
|
|
3451
|
+
textStyleType: 'heading1',
|
|
3452
|
+
color: '#ff0000',
|
|
3453
|
+
fontSize: '2em',
|
|
3454
|
+
},
|
|
3455
|
+
};
|
|
3456
|
+
|
|
3457
|
+
stripRteStyleColours(modelWithContent(content));
|
|
3458
|
+
|
|
3459
|
+
expect(content.titleStyle).toEqual({
|
|
3460
|
+
textStyleType: 'heading1',
|
|
3461
|
+
fontSize: '2em',
|
|
3462
|
+
});
|
|
3463
|
+
});
|
|
3464
|
+
|
|
3465
|
+
it('keeps colour on a plain-string RTE field (no mark) but strips a doc-backed sibling', () => {
|
|
3466
|
+
const content = {
|
|
3467
|
+
type: 'banner-section',
|
|
3468
|
+
// plain string → colour lives on the object (its only home) → must survive G
|
|
3469
|
+
title: 'Legacy plain title',
|
|
3470
|
+
titleStyle: {
|
|
3471
|
+
textStyleType: 'heading1',
|
|
3472
|
+
color: '#ff0000',
|
|
3473
|
+
fontSize: '2em',
|
|
3474
|
+
},
|
|
3475
|
+
// real doc → colour lives in the mark → style-object colour is stripped
|
|
3476
|
+
description: docOf([{ text: 'Rich', attrs: { color: '#00ff00' } }]),
|
|
3477
|
+
descriptionStyle: {
|
|
3478
|
+
textStyleType: 'paragraph1',
|
|
3479
|
+
color: '#00ff00',
|
|
3480
|
+
fontSize: '1em',
|
|
3481
|
+
},
|
|
3482
|
+
};
|
|
3483
|
+
|
|
3484
|
+
stripRteStyleColours(modelWithContent(content));
|
|
3485
|
+
|
|
3486
|
+
expect(content.titleStyle).toEqual({
|
|
3487
|
+
textStyleType: 'heading1',
|
|
3488
|
+
color: '#ff0000',
|
|
3489
|
+
fontSize: '2em',
|
|
3490
|
+
});
|
|
3491
|
+
expect(content.descriptionStyle).toEqual({
|
|
3492
|
+
textStyleType: 'paragraph1',
|
|
3493
|
+
fontSize: '1em',
|
|
3494
|
+
});
|
|
3495
|
+
});
|
|
3496
|
+
|
|
3497
|
+
it('keeps colour on a plain-string textItems[].text but strips a doc-backed one', () => {
|
|
3498
|
+
const content = {
|
|
3499
|
+
type: 'text',
|
|
3500
|
+
textItems: [
|
|
3501
|
+
{
|
|
3502
|
+
text: 'legacy plain item',
|
|
3503
|
+
textStyle: { textStyleType: 'paragraph1', color: '#111111' },
|
|
3504
|
+
},
|
|
3505
|
+
{
|
|
3506
|
+
text: docOf([{ text: 'rich item', attrs: { color: '#222222' } }]),
|
|
3507
|
+
textStyle: { textStyleType: 'paragraph2', color: '#222222' },
|
|
3508
|
+
},
|
|
3509
|
+
],
|
|
3510
|
+
};
|
|
3511
|
+
|
|
3512
|
+
stripRteStyleColours(modelWithContent(content));
|
|
3513
|
+
|
|
3514
|
+
expect(content.textItems[0].textStyle).toEqual({
|
|
3515
|
+
textStyleType: 'paragraph1',
|
|
3516
|
+
color: '#111111',
|
|
3517
|
+
});
|
|
3518
|
+
expect(content.textItems[1].textStyle).toEqual({
|
|
3519
|
+
textStyleType: 'paragraph2',
|
|
3520
|
+
});
|
|
3521
|
+
});
|
|
3522
|
+
|
|
3523
|
+
it('removes colour from the text section textStyle and each textItems[].textStyle (typography kept)', () => {
|
|
3524
|
+
const content = {
|
|
3525
|
+
type: 'text',
|
|
3526
|
+
textStyle: {
|
|
3527
|
+
textStyleType: 'paragraph1',
|
|
3528
|
+
color: '#ff0000',
|
|
3529
|
+
fontSize: '1em',
|
|
3530
|
+
},
|
|
3531
|
+
textItems: [
|
|
3532
|
+
{
|
|
3533
|
+
textStyle: {
|
|
3534
|
+
textStyleType: 'paragraph2',
|
|
3535
|
+
color: '#00ff00',
|
|
3536
|
+
applyAutoScale: false,
|
|
3537
|
+
},
|
|
3538
|
+
},
|
|
3539
|
+
],
|
|
3540
|
+
};
|
|
3541
|
+
|
|
3542
|
+
stripRteStyleColours(modelWithContent(content));
|
|
3543
|
+
|
|
3544
|
+
expect(content.textStyle).toEqual({
|
|
3545
|
+
textStyleType: 'paragraph1',
|
|
3546
|
+
fontSize: '1em',
|
|
3547
|
+
});
|
|
3548
|
+
expect(content.textItems[0].textStyle).toEqual({
|
|
3549
|
+
textStyleType: 'paragraph2',
|
|
3550
|
+
applyAutoScale: false,
|
|
3551
|
+
});
|
|
3552
|
+
});
|
|
3553
|
+
});
|
|
3554
|
+
|
|
3555
|
+
// The version gate owns "runs once" (v→22), so this is hygiene, not a correctness gate
|
|
3556
|
+
// (legacy-migrations rule): a re-run over already-converted output must be a no-op.
|
|
3557
|
+
describe('migration-22 · migration22 (idempotency hygiene)', () => {
|
|
3558
|
+
const legacyModel = (): BrandPageModel =>
|
|
3559
|
+
({
|
|
3560
|
+
pages: [
|
|
3561
|
+
{
|
|
3562
|
+
contents: [
|
|
3563
|
+
{
|
|
3564
|
+
type: 'banner-section',
|
|
3565
|
+
title: 'Hello',
|
|
3566
|
+
titleStyle: { textStyleType: 'titles' },
|
|
3567
|
+
},
|
|
3568
|
+
],
|
|
3569
|
+
},
|
|
3570
|
+
],
|
|
3571
|
+
styles: {
|
|
3572
|
+
textStyles: {
|
|
3573
|
+
textStyleOptions: TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3574
|
+
},
|
|
3575
|
+
buttonStyles: { style: {} },
|
|
3576
|
+
colors: [],
|
|
3577
|
+
},
|
|
3578
|
+
brandPageType: 'default',
|
|
3579
|
+
}) as unknown as BrandPageModel;
|
|
3580
|
+
|
|
3581
|
+
it('a second run over converted output changes nothing', async () => {
|
|
3582
|
+
const once = await migration22(
|
|
3583
|
+
{ brandPageModel: legacyModel() } as MigrationData,
|
|
3584
|
+
{} as MigrationOptions,
|
|
3585
|
+
);
|
|
3586
|
+
const snapshot = JSON.parse(JSON.stringify(once.brandPageModel));
|
|
3587
|
+
|
|
3588
|
+
const twice = await migration22(
|
|
3589
|
+
{
|
|
3590
|
+
brandPageModel: JSON.parse(JSON.stringify(once.brandPageModel)),
|
|
3591
|
+
} as MigrationData,
|
|
3592
|
+
{} as MigrationOptions,
|
|
3593
|
+
);
|
|
3594
|
+
|
|
3595
|
+
expect(twice.brandPageModel).toEqual(snapshot);
|
|
3596
|
+
});
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
// Production drew the payment success message with the description's style (`Payment.vue`). The
|
|
3600
|
+
// upgrade gives it its own, copied from the reconnected description, so it draws the same.
|
|
3601
|
+
describe('migration-22 · payment success message (R11)', () => {
|
|
3602
|
+
const BUTTON_FILL = '#101010';
|
|
3603
|
+
const BUTTON_TEXT = '#fafafa';
|
|
3604
|
+
|
|
3605
|
+
const paymentSite = (
|
|
3606
|
+
content: Record<string, unknown>,
|
|
3607
|
+
options: TextStyleOptions,
|
|
3608
|
+
): BrandPageModel =>
|
|
3609
|
+
siteWith(
|
|
3610
|
+
{
|
|
3611
|
+
pages: [{ contents: [content] }],
|
|
3612
|
+
styles: {
|
|
3613
|
+
textStyles: { textFontFamily: 'lato', textStyleOptions: options },
|
|
3614
|
+
buttonStyles: {
|
|
3615
|
+
style: { backgroundColor: BUTTON_FILL, color: BUTTON_TEXT },
|
|
3616
|
+
},
|
|
3617
|
+
colors: [],
|
|
3618
|
+
},
|
|
3619
|
+
},
|
|
3620
|
+
options,
|
|
3621
|
+
);
|
|
3622
|
+
|
|
3623
|
+
const paymentContent = (
|
|
3624
|
+
type: string,
|
|
3625
|
+
extra: Record<string, unknown>,
|
|
3626
|
+
): Record<string, unknown> => ({
|
|
3627
|
+
id: `${type}-1`,
|
|
3628
|
+
type,
|
|
3629
|
+
title: 'Pay',
|
|
3630
|
+
description: 'Pay me',
|
|
3631
|
+
successMessage: '',
|
|
3632
|
+
...extra,
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
// A site whose Paragraphs role differs from Paragraph 1, so its look stays as overrides.
|
|
3636
|
+
const withCustomParagraphs = (): TextStyleOptions => {
|
|
3637
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3638
|
+
|
|
3639
|
+
options.paragraphs.style.fontFamily = 'Oswald';
|
|
3640
|
+
options.paragraphs.style.fontSize = '1.125em';
|
|
3641
|
+
|
|
3642
|
+
return options;
|
|
3643
|
+
};
|
|
3644
|
+
|
|
3645
|
+
const viewModelOf = (
|
|
3646
|
+
content: unknown,
|
|
3647
|
+
model: BrandPageModel,
|
|
3648
|
+
): Record<string, any> => {
|
|
3649
|
+
const styles = pageStylesFor(model.styles.textStyles.textStyleOptions);
|
|
3650
|
+
|
|
3651
|
+
styles.buttonStyles.style = { ...model.styles.buttonStyles.style };
|
|
3652
|
+
|
|
3653
|
+
return mapToViewModel(
|
|
3654
|
+
styles,
|
|
3655
|
+
BRAND_PAGE_TYPES.brandSite,
|
|
3656
|
+
{},
|
|
3657
|
+
)(clone(content) as Content) as Record<string, any>;
|
|
3658
|
+
};
|
|
3659
|
+
|
|
3660
|
+
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
|
|
3661
|
+
|
|
3662
|
+
const withoutColour = (style: Record<string, unknown>) => {
|
|
3663
|
+
const { color: _color, ...rest } = style;
|
|
3664
|
+
|
|
3665
|
+
return rest;
|
|
3666
|
+
};
|
|
3667
|
+
|
|
3668
|
+
it.each([
|
|
3669
|
+
[
|
|
3670
|
+
'support-me',
|
|
3671
|
+
'a legacy role',
|
|
3672
|
+
{ textStyleType: 'paragraphs', textAlign: 'center' },
|
|
3673
|
+
],
|
|
3674
|
+
[
|
|
3675
|
+
'request-payment',
|
|
3676
|
+
'a legacy role',
|
|
3677
|
+
{ textStyleType: 'paragraphs', textAlign: 'center' },
|
|
3678
|
+
],
|
|
3679
|
+
[
|
|
3680
|
+
'support-me',
|
|
3681
|
+
'no role',
|
|
3682
|
+
{ fontSize: '1.25em', fontFamily: 'Oswald', textAlign: 'center' },
|
|
3683
|
+
],
|
|
3684
|
+
[
|
|
3685
|
+
'request-payment',
|
|
3686
|
+
'no role',
|
|
3687
|
+
{ fontSize: '1.25em', fontFamily: 'Oswald', textAlign: 'center' },
|
|
3688
|
+
],
|
|
3689
|
+
] as const)(
|
|
3690
|
+
'gives a %s success message the description on %s: its role and overrides, look kept',
|
|
3691
|
+
async (type, _label, descriptionStyle) => {
|
|
3692
|
+
const options = withCustomParagraphs();
|
|
3693
|
+
const before = drawn(
|
|
3694
|
+
productionTypography(
|
|
3695
|
+
descriptionStyle as TextStyleDeclaration,
|
|
3696
|
+
withCustomParagraphs(),
|
|
3697
|
+
),
|
|
3698
|
+
);
|
|
3699
|
+
|
|
3700
|
+
const model = await runMigration22(
|
|
3701
|
+
paymentSite(
|
|
3702
|
+
paymentContent(type, { descriptionStyle: { ...descriptionStyle } }),
|
|
3703
|
+
options,
|
|
3704
|
+
),
|
|
3705
|
+
);
|
|
3706
|
+
const after = model.pages[0].contents[0] as unknown as Record<
|
|
3707
|
+
string,
|
|
3708
|
+
any
|
|
3709
|
+
>;
|
|
3710
|
+
|
|
3711
|
+
expect(after.successMessageStyle).toBeDefined();
|
|
3712
|
+
expect(after.successMessageStyle).not.toBe(after.descriptionStyle);
|
|
3713
|
+
expect(withoutColour(after.successMessageStyle)).toEqual(
|
|
3714
|
+
withoutColour(after.descriptionStyle),
|
|
3715
|
+
);
|
|
3716
|
+
|
|
3717
|
+
const vm = viewModelOf(after, model);
|
|
3718
|
+
|
|
3719
|
+
expect(drawn(vm.successMessageStyle, options)).toEqual(before);
|
|
3720
|
+
expect(drawn(vm.successMessageStyle, options)).toEqual(
|
|
3721
|
+
drawn(vm.descriptionStyle, options),
|
|
3722
|
+
);
|
|
3723
|
+
},
|
|
3724
|
+
);
|
|
3725
|
+
|
|
3726
|
+
it.each(['support-me', 'request-payment'])(
|
|
3727
|
+
'keeps the colour a %s success message drew from its description',
|
|
3728
|
+
async (type) => {
|
|
3729
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3730
|
+
|
|
3731
|
+
// On the button fill, a description with no colour of its own drew the button text colour.
|
|
3732
|
+
const onFill = await runMigration22(
|
|
3733
|
+
paymentSite(
|
|
3734
|
+
paymentContent(type, {
|
|
3735
|
+
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
3736
|
+
}),
|
|
3737
|
+
options,
|
|
3738
|
+
),
|
|
3739
|
+
);
|
|
3740
|
+
|
|
3741
|
+
expect(
|
|
3742
|
+
viewModelOf(onFill.pages[0].contents[0], onFill).successMessageStyle
|
|
3743
|
+
.color,
|
|
3744
|
+
).toBe(BUTTON_TEXT);
|
|
3745
|
+
|
|
3746
|
+
// Its own colour (migration-21's frame bake) it keeps.
|
|
3747
|
+
const ownColour = await runMigration22(
|
|
3748
|
+
paymentSite(
|
|
3749
|
+
paymentContent(type, {
|
|
3750
|
+
backgroundStyle: { backgroundColor: '#ecb6d7' },
|
|
3751
|
+
descriptionStyle: { textStyleType: 'paragraphs', color: '#654321' },
|
|
3752
|
+
}),
|
|
3753
|
+
TextStyles.GetDefaultTextStyle().textStyleOptions,
|
|
3754
|
+
),
|
|
3755
|
+
);
|
|
3756
|
+
|
|
3757
|
+
expect(
|
|
3758
|
+
viewModelOf(ownColour.pages[0].contents[0], ownColour)
|
|
3759
|
+
.successMessageStyle.color,
|
|
3760
|
+
).toBe('#654321');
|
|
3761
|
+
},
|
|
3762
|
+
);
|
|
3763
|
+
|
|
3764
|
+
it('keeps a success message style of its own, and leaves the message untouched', async () => {
|
|
3765
|
+
const options = TextStyles.GetDefaultTextStyle().textStyleOptions;
|
|
3766
|
+
const successMessage = {
|
|
3767
|
+
type: 'doc',
|
|
3768
|
+
content: [
|
|
3769
|
+
{
|
|
3770
|
+
type: 'paragraph',
|
|
3771
|
+
content: [
|
|
3772
|
+
{
|
|
3773
|
+
type: 'text',
|
|
3774
|
+
text: 'Thanks!',
|
|
3775
|
+
marks: [{ type: 'textStyle', attrs: { color: '#e11d48' } }],
|
|
3776
|
+
},
|
|
3777
|
+
],
|
|
3778
|
+
},
|
|
3779
|
+
],
|
|
3780
|
+
};
|
|
3781
|
+
|
|
3782
|
+
const model = await runMigration22(
|
|
3783
|
+
paymentSite(
|
|
3784
|
+
paymentContent('support-me', {
|
|
3785
|
+
successMessage: clone(successMessage),
|
|
3786
|
+
successMessageStyle: { textStyleType: 'heading6' },
|
|
3787
|
+
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
3788
|
+
}),
|
|
3789
|
+
options,
|
|
3790
|
+
),
|
|
3791
|
+
);
|
|
3792
|
+
const after = model.pages[0].contents[0] as unknown as Record<string, any>;
|
|
3793
|
+
|
|
3794
|
+
expect(after.successMessageStyle.textStyleType).toBe('heading6');
|
|
3795
|
+
expect(after.successMessage).toEqual(successMessage);
|
|
3796
|
+
});
|
|
3797
|
+
|
|
3798
|
+
// The old maker editor wrote `{ textAlign }` here, but production never drew it.
|
|
3799
|
+
it.each(['support-me', 'request-payment'])(
|
|
3800
|
+
'replaces a %s success message style it never drew with a copy of the description',
|
|
3801
|
+
async (type) => {
|
|
3802
|
+
const options = withCustomParagraphs();
|
|
3803
|
+
const descriptionStyle = {
|
|
3804
|
+
textStyleType: 'paragraphs',
|
|
3805
|
+
textAlign: 'center',
|
|
3806
|
+
};
|
|
3807
|
+
const before = drawn(
|
|
3808
|
+
productionTypography(
|
|
3809
|
+
descriptionStyle as TextStyleDeclaration,
|
|
3810
|
+
withCustomParagraphs(),
|
|
3811
|
+
),
|
|
3812
|
+
);
|
|
3813
|
+
|
|
3814
|
+
const model = await runMigration22(
|
|
3815
|
+
paymentSite(
|
|
3816
|
+
paymentContent(type, {
|
|
3817
|
+
descriptionStyle: { ...descriptionStyle },
|
|
3818
|
+
successMessageStyle: { textAlign: 'left' },
|
|
3819
|
+
}),
|
|
3820
|
+
options,
|
|
3821
|
+
),
|
|
3822
|
+
);
|
|
3823
|
+
const after = model.pages[0].contents[0] as unknown as Record<
|
|
3824
|
+
string,
|
|
3825
|
+
any
|
|
3826
|
+
>;
|
|
3827
|
+
|
|
3828
|
+
expect(withoutColour(after.successMessageStyle)).toEqual(
|
|
3829
|
+
withoutColour(after.descriptionStyle),
|
|
3830
|
+
);
|
|
3831
|
+
expect(
|
|
3832
|
+
drawn(viewModelOf(after, model).successMessageStyle, options),
|
|
3833
|
+
).toEqual(before);
|
|
3834
|
+
},
|
|
3835
|
+
);
|
|
3836
|
+
|
|
3837
|
+
it.each(['support-me', 'request-payment'])(
|
|
3838
|
+
'starts a %s success message with no description on Paragraph 2, keeping the page defaults it drew',
|
|
3839
|
+
async (type) => {
|
|
3840
|
+
const options = withCustomParagraphs();
|
|
3841
|
+
const model = await runMigration22(
|
|
3842
|
+
paymentSite(paymentContent(type, { successMessage: '' }), options),
|
|
3843
|
+
);
|
|
3844
|
+
const after = model.pages[0].contents[0] as unknown as Record<
|
|
3845
|
+
string,
|
|
3846
|
+
any
|
|
3847
|
+
>;
|
|
3848
|
+
const vm = viewModelOf(after, model);
|
|
3849
|
+
|
|
3850
|
+
expect(after.successMessageStyle.textStyleType).toBe('paragraph2');
|
|
3851
|
+
// S5 — the drawn look it kept is pinned as literal overrides (the reconnect's inherited-
|
|
3852
|
+
// font-size/DETACHED_UNSET_EFFECTIVE pins), not paragraph2's real role default, so the
|
|
3853
|
+
// RHS stays `drawn(undefined)` (the pinned values happen to equal the generic no-role
|
|
3854
|
+
// draw). Passing `options` on the LHS only is what makes this catch a missing pin: an
|
|
3855
|
+
// un-pinned prop would fall through to paragraph2's REAL default (1.125em/1.5em) instead
|
|
3856
|
+
// of coincidentally matching the RHS, where the old comparison (no options either side)
|
|
3857
|
+
// couldn't tell the two apart.
|
|
3858
|
+
expect(drawn(vm.successMessageStyle, options)).toEqual(drawn(undefined));
|
|
3859
|
+
// It inherited the frame's colour, the button text colour on the button fill.
|
|
3860
|
+
expect(vm.successMessageStyle.color).toBe(BUTTON_TEXT);
|
|
3861
|
+
},
|
|
3862
|
+
);
|
|
3863
|
+
|
|
3864
|
+
// A rich-text run draws from its marks, filled at render against its role (`TextWrapper`); a role
|
|
3865
|
+
// var draws the role's value, or the neutral the role class emits for one it leaves unset.
|
|
3866
|
+
const drawnRun = (
|
|
3867
|
+
doc: unknown,
|
|
3868
|
+
style: Record<string, unknown> | undefined,
|
|
3869
|
+
role: Record<string, unknown> | undefined,
|
|
3870
|
+
): Record<string, string | undefined> => {
|
|
3871
|
+
const filled = textRoleCss.fillRteRunTypography(
|
|
3872
|
+
doc as never,
|
|
3873
|
+
role as never,
|
|
3874
|
+
style as never,
|
|
3875
|
+
) as {
|
|
3876
|
+
content: {
|
|
3877
|
+
content: {
|
|
3878
|
+
marks?: { type: string; attrs: Record<string, string> }[];
|
|
3879
|
+
}[];
|
|
3880
|
+
}[];
|
|
3881
|
+
};
|
|
3882
|
+
const attrs =
|
|
3883
|
+
filled.content[0].content[0].marks?.find(
|
|
3884
|
+
(mark) => mark.type === 'textStyle',
|
|
3885
|
+
)?.attrs ?? {};
|
|
3886
|
+
const neutral: Record<string, string> = {
|
|
3887
|
+
fontWeight: 'normal',
|
|
3888
|
+
fontStyle: 'normal',
|
|
3889
|
+
textDecoration: 'none',
|
|
3890
|
+
letterSpacing: 'normal',
|
|
3891
|
+
};
|
|
3892
|
+
|
|
3893
|
+
return Object.fromEntries(
|
|
3894
|
+
TEXT_STYLE_CASCADE_PROPS.map((prop) => {
|
|
3895
|
+
const value = attrs[prop];
|
|
3896
|
+
|
|
3897
|
+
if (!value) return [prop, drawn(undefined)[prop]];
|
|
3898
|
+
|
|
3899
|
+
if (value.startsWith('var(--role-'))
|
|
3900
|
+
return [prop, (role?.[prop] as string) || neutral[prop]];
|
|
3901
|
+
|
|
3902
|
+
return [prop, value.replace(/['"]/g, '')];
|
|
3903
|
+
}),
|
|
3904
|
+
);
|
|
3905
|
+
};
|
|
3906
|
+
|
|
3907
|
+
// migration-21 wrapped a non-empty message into a doc whose runs carry only colour, and
|
|
3908
|
+
// production's `TextWrapper` gave rich text no typography from its style object.
|
|
3909
|
+
it.each([
|
|
3910
|
+
['support-me', 'a legacy role', { textStyleType: 'paragraphs' }],
|
|
3911
|
+
['request-payment', 'a legacy role', { textStyleType: 'paragraphs' }],
|
|
3912
|
+
['support-me', 'no role', { fontSize: '1.25em', fontFamily: 'Oswald' }],
|
|
3913
|
+
[
|
|
3914
|
+
'request-payment',
|
|
3915
|
+
'no role',
|
|
3916
|
+
{ fontSize: '1.25em', fontFamily: 'Oswald' },
|
|
3917
|
+
],
|
|
3918
|
+
] as const)(
|
|
3919
|
+
'keeps a rich-text %s success message drawing the page defaults, on the role of its description on %s',
|
|
3920
|
+
async (type, _label, descriptionStyle) => {
|
|
3921
|
+
const options = withCustomParagraphs();
|
|
3922
|
+
const successMessage = {
|
|
3923
|
+
type: 'doc',
|
|
3924
|
+
content: [
|
|
3925
|
+
{
|
|
3926
|
+
type: 'paragraph',
|
|
3927
|
+
attrs: { textAlign: 'center' },
|
|
3928
|
+
content: [
|
|
3929
|
+
{
|
|
3930
|
+
type: 'text',
|
|
3931
|
+
text: 'Thanks!',
|
|
3932
|
+
marks: [{ type: 'textStyle', attrs: { color: '#654321' } }],
|
|
3933
|
+
},
|
|
3934
|
+
],
|
|
3935
|
+
},
|
|
3936
|
+
],
|
|
3937
|
+
};
|
|
3938
|
+
const before = drawnRun(successMessage, undefined, undefined);
|
|
3939
|
+
|
|
3940
|
+
const model = await runMigration22(
|
|
3941
|
+
paymentSite(
|
|
3942
|
+
paymentContent(type, {
|
|
3943
|
+
successMessage: clone(successMessage),
|
|
3944
|
+
descriptionStyle: { ...descriptionStyle, textAlign: 'center' },
|
|
3945
|
+
}),
|
|
3946
|
+
options,
|
|
3947
|
+
),
|
|
3948
|
+
);
|
|
3949
|
+
const after = model.pages[0].contents[0] as unknown as Record<
|
|
3950
|
+
string,
|
|
3951
|
+
any
|
|
3952
|
+
>;
|
|
3953
|
+
const style = after.successMessageStyle;
|
|
3954
|
+
|
|
3955
|
+
expect(style.textStyleType).toBe(after.descriptionStyle.textStyleType);
|
|
3956
|
+
expect(
|
|
3957
|
+
drawnRun(
|
|
3958
|
+
after.successMessage,
|
|
3959
|
+
style,
|
|
3960
|
+
roleStyleOf(options, style.textStyleType) as never,
|
|
3961
|
+
),
|
|
3962
|
+
).toEqual(before);
|
|
3963
|
+
expect(runMarkColor(after.successMessage)).toBe('#654321');
|
|
3964
|
+
},
|
|
3965
|
+
);
|
|
3966
|
+
|
|
3967
|
+
// migration-21 actually bakes the description's fontFamily/fontSize onto the run too
|
|
3968
|
+
// (migration-21.ts:760-765), not colour alone — a doc from that step already carries them, and
|
|
3969
|
+
// re-lifting it for migration-22's role must leave what it drew unchanged.
|
|
3970
|
+
it('keeps a rich-text support-me success message drawing what migration-21 already baked onto its runs', async () => {
|
|
3971
|
+
const options = withCustomParagraphs();
|
|
3972
|
+
const descriptionStyle = {
|
|
3973
|
+
fontSize: '1.25em',
|
|
3974
|
+
fontFamily: 'Oswald',
|
|
3975
|
+
textAlign: 'center' as const,
|
|
3976
|
+
};
|
|
3977
|
+
const successMessage = {
|
|
3978
|
+
type: 'doc',
|
|
3979
|
+
content: [
|
|
3980
|
+
{
|
|
3981
|
+
type: 'paragraph',
|
|
3982
|
+
attrs: { textAlign: 'center' },
|
|
3983
|
+
content: [
|
|
3984
|
+
{
|
|
3985
|
+
type: 'text',
|
|
3986
|
+
text: 'Thanks!',
|
|
3987
|
+
marks: [
|
|
3988
|
+
{
|
|
3989
|
+
type: 'textStyle',
|
|
3990
|
+
attrs: {
|
|
3991
|
+
color: '#654321',
|
|
3992
|
+
fontFamily: descriptionStyle.fontFamily,
|
|
3993
|
+
fontSize: descriptionStyle.fontSize,
|
|
3994
|
+
},
|
|
3995
|
+
},
|
|
3996
|
+
],
|
|
3997
|
+
},
|
|
3998
|
+
],
|
|
3999
|
+
},
|
|
4000
|
+
],
|
|
4001
|
+
};
|
|
4002
|
+
const before = drawnRun(successMessage, undefined, undefined);
|
|
4003
|
+
|
|
4004
|
+
const model = await runMigration22(
|
|
4005
|
+
paymentSite(
|
|
4006
|
+
paymentContent('support-me', {
|
|
4007
|
+
successMessage: clone(successMessage),
|
|
4008
|
+
descriptionStyle,
|
|
4009
|
+
}),
|
|
4010
|
+
options,
|
|
4011
|
+
),
|
|
4012
|
+
);
|
|
4013
|
+
const after = model.pages[0].contents[0] as unknown as Record<string, any>;
|
|
4014
|
+
const style = after.successMessageStyle;
|
|
4015
|
+
|
|
4016
|
+
expect(style.textStyleType).toBe(after.descriptionStyle.textStyleType);
|
|
4017
|
+
expect(
|
|
4018
|
+
drawnRun(
|
|
4019
|
+
after.successMessage,
|
|
4020
|
+
style,
|
|
4021
|
+
roleStyleOf(options, style.textStyleType) as never,
|
|
4022
|
+
),
|
|
4023
|
+
).toEqual(before);
|
|
4024
|
+
expect(runMarkColor(after.successMessage)).toBe('#654321');
|
|
4025
|
+
});
|
|
4026
|
+
|
|
4027
|
+
// The shop checkout (`PaymentModal.vue`) drew its message with only the description's colour.
|
|
4028
|
+
it.each([
|
|
4029
|
+
['its own colour', { textStyleType: 'paragraphs', color: '#654321' }],
|
|
4030
|
+
['its role colour', { textStyleType: 'paragraphs' }],
|
|
4031
|
+
] as const)(
|
|
4032
|
+
'starts the shop checkout success message on Paragraph 2, keeping the page defaults and the description colour (%s)',
|
|
4033
|
+
async (_label, descriptionStyle) => {
|
|
4034
|
+
const options = withCustomParagraphs();
|
|
4035
|
+
|
|
4036
|
+
(options.paragraphs.style as unknown as Record<string, string>).color =
|
|
4037
|
+
'#123456';
|
|
4038
|
+
|
|
4039
|
+
const model = await runMigration22(
|
|
4040
|
+
paymentSite(
|
|
4041
|
+
{
|
|
4042
|
+
id: 'shop-1',
|
|
4043
|
+
type: 'shop-section',
|
|
4044
|
+
title: 'Shop',
|
|
4045
|
+
description: '',
|
|
4046
|
+
successMessage: 'Thanks!',
|
|
4047
|
+
descriptionStyle: { ...descriptionStyle },
|
|
4048
|
+
successMessageStyle: { textAlign: 'left' },
|
|
4049
|
+
},
|
|
4050
|
+
options,
|
|
4051
|
+
),
|
|
4052
|
+
);
|
|
4053
|
+
const after = model.pages[0].contents[0] as unknown as Record<
|
|
4054
|
+
string,
|
|
4055
|
+
any
|
|
4056
|
+
>;
|
|
4057
|
+
const vm = viewModelOf(after, model);
|
|
4058
|
+
|
|
4059
|
+
expect(after.successMessageStyle.textStyleType).toBe('paragraph2');
|
|
4060
|
+
// S5 — see the support-me/request-payment case above: the RHS stays `drawn(undefined)`
|
|
4061
|
+
// (the pinned drawn look happens to equal the generic no-role draw); passing `options` on
|
|
4062
|
+
// the LHS only is what makes this catch a missing pin.
|
|
4063
|
+
expect(drawn(vm.successMessageStyle, options)).toEqual(drawn(undefined));
|
|
4064
|
+
expect(vm.successMessageStyle.color).toBe(
|
|
4065
|
+
(descriptionStyle as { color?: string }).color ?? '#123456',
|
|
4066
|
+
);
|
|
4067
|
+
expect(vm.successMessageStyle.color).toBe(vm.descriptionStyle.color);
|
|
4068
|
+
expect(vm.successMessageStyle.textAlign).toBe('center');
|
|
4069
|
+
},
|
|
4070
|
+
);
|
|
4071
|
+
|
|
4072
|
+
// A description on no role and with no colour gave the modal `inherit`: fe-shared-lib's `Modal`
|
|
4073
|
+
// sets no colour, so it drew the page root's, the Paragraphs colour (`Page.mixin`
|
|
4074
|
+
// `rootElementStyles`). Never the colour of the tier the description reconnects to.
|
|
4075
|
+
it('keeps the page colour a shop checkout success message inherited from a description on no role', async () => {
|
|
4076
|
+
const options = withCustomParagraphs();
|
|
4077
|
+
const colourOf = (type: TextStyleType, color: string) => {
|
|
4078
|
+
(options[type].style as unknown as Record<string, string>).color = color;
|
|
4079
|
+
};
|
|
4080
|
+
|
|
4081
|
+
colourOf('paragraphs', '#123456');
|
|
4082
|
+
colourOf('paragraph3', '#abcdef');
|
|
4083
|
+
|
|
4084
|
+
const model = await runMigration22(
|
|
4085
|
+
paymentSite(
|
|
4086
|
+
{
|
|
4087
|
+
id: 'shop-1',
|
|
4088
|
+
type: 'shop-section',
|
|
4089
|
+
title: 'Shop',
|
|
4090
|
+
description: '',
|
|
4091
|
+
successMessage: 'Thanks!',
|
|
4092
|
+
// Paragraph 3's size, so the description reconnects to a tier with its own colour.
|
|
4093
|
+
descriptionStyle: { fontSize: '0.875em' },
|
|
4094
|
+
},
|
|
4095
|
+
options,
|
|
4096
|
+
),
|
|
4097
|
+
);
|
|
4098
|
+
const after = model.pages[0].contents[0] as unknown as Record<string, any>;
|
|
4099
|
+
|
|
4100
|
+
expect(after.descriptionStyle.textStyleType).toBe('paragraph3');
|
|
4101
|
+
expect(viewModelOf(after, model).successMessageStyle.color).toBe('#123456');
|
|
4102
|
+
});
|
|
4103
|
+
});
|