@designcrowd/library.brand-page 6.2.0 → 6.2.1

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.
Files changed (121) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-VQCQvlMg.js → CreatedOnBrandCrowd-de-de-CtvPtYF6.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DSf3BjCG.js → CreatedOnBrandCrowd-en-us-eGF-EjAX.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-DGcdM9Ec.js → CreatedOnBrandCrowd-es-es-CSemsOtT.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-C9g1fc39.js → CreatedOnBrandCrowd-fr-ca-DzylX-AT.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-C9g1fc39.js → CreatedOnBrandCrowd-fr-fr-DzylX-AT.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DSXr7Bxl.js → CreatedOnBrandCrowd-pt-br-QMUxXQsE.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DSXr7Bxl.js → CreatedOnBrandCrowd-pt-pt-QMUxXQsE.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BPdCL6wS.js → CreatedOnDcom-de-de-M7OoJt-1.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CLkSTQRu.js → CreatedOnDcom-en-us-D7hMM0AI.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CRknMAGv.js → CreatedOnDcom-es-es-CLw-BLp2.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-ClAY_dHI.js → CreatedOnDcom-fr-ca-BRu_kmeu.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DQtv9rR-.js → CreatedOnDcom-fr-fr-IMnEv_yM.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BbIxvmxv.js → CreatedOnDcom-pt-br-Bt6v1p27.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BOfOesBj.js → CreatedOnDcom-pt-pt-BVsFJIJg.js} +1 -1
  15. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +16 -2
  16. package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +17 -0
  17. package/lib/src/brand-page/components/page/Page.mixin.d.ts +2 -1
  18. package/lib/src/brand-page/components/page/Page.mixin.js +12 -7
  19. package/lib/src/brand-page/components/page-header/HeaderLayout.stories.d.ts +1 -0
  20. package/lib/src/brand-page/components/page-header/HeaderLayout.stories.js +9 -0
  21. package/lib/src/brand-page/components/page-header/PageHeader.mixin.d.ts +3 -1
  22. package/lib/src/brand-page/components/page-header/PageHeader.mixin.js +10 -4
  23. package/lib/src/brand-page/constants/sections/header-display.constants.d.ts +3 -0
  24. package/lib/src/brand-page/constants/sections/header-display.constants.js +15 -0
  25. package/lib/src/brand-page/models/index.d.ts +12 -0
  26. package/lib/src/brand-page/models/index.js +4 -0
  27. package/lib/src/brand-page/utils/context.util.d.ts +1 -0
  28. package/lib/src/brand-page/utils/context.util.js +9 -0
  29. package/lib/src/brand-page/utils/route.util.d.ts +1 -0
  30. package/lib/src/brand-page/utils/route.util.js +9 -0
  31. package/lib/src/brand-page/utils/view-model.util.js +53 -1
  32. package/lib/src/brand-page/view-models.d.ts +3 -1
  33. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +2 -1
  34. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +2 -1
  35. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1626 -0
  36. package/lib/src/brand-page-maker/validators/section-button.validator.d.ts +5 -2
  37. package/lib/src/brand-page-maker/validators/section-button.validator.js +11 -15
  38. package/lib/src/{bundled-translations-B2q5GWTQ.js → bundled-translations-KY0aSmMp.js} +9 -9
  39. package/lib/src/{bundled-translations.de-DE-FhH2H_rd.js → bundled-translations.de-DE-BIFi4bYj.js} +9 -9
  40. package/lib/src/{bundled-translations.es-ES-_ZExxMi7.js → bundled-translations.es-ES-CHlKuPLO.js} +5 -5
  41. package/lib/src/{bundled-translations.fr-CA-Tg-ODIiC.js → bundled-translations.fr-CA-DR1fvm8e.js} +1 -1
  42. package/lib/src/{bundled-translations.fr-FR-C0sU51Mz.js → bundled-translations.fr-FR-DnpWVIQg.js} +9 -9
  43. package/lib/src/{bundled-translations.pt-BR-MYFIhA4u.js → bundled-translations.pt-BR-p8szTOiT.js} +17 -17
  44. package/lib/src/{bundled-translations.pt-PT-DeUBSzCB.js → bundled-translations.pt-PT-DEk39NhW.js} +22 -22
  45. package/lib/src/bundles/bundled-translations.de-DE.json +6 -1
  46. package/lib/src/bundles/bundled-translations.es-ES.json +6 -1
  47. package/lib/src/bundles/bundled-translations.fr-CA.json +6 -1
  48. package/lib/src/bundles/bundled-translations.fr-FR.json +6 -1
  49. package/lib/src/bundles/bundled-translations.json +6 -1
  50. package/lib/src/bundles/bundled-translations.pt-BR.json +6 -1
  51. package/lib/src/bundles/bundled-translations.pt-PT.json +6 -1
  52. package/lib/src/css/library.brand-page-brandCrowd.min.css +573 -135
  53. package/lib/src/css/library.brand-page-brandPage.min.css +573 -135
  54. package/lib/src/css/library.brand-page-designCom.min.css +573 -135
  55. package/lib/src/css/library.brand-page-preview.min.css +573 -135
  56. package/lib/src/index.mjs +1 -1
  57. package/lib/src/{lib-BMVF4uNy.js → lib-B38VVCSw.js} +20304 -19843
  58. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +2 -1
  59. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  60. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  61. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +1 -1
  62. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +14 -2
  63. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.js +14 -0
  64. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  65. package/lib/src/shared/page-navigation/store/page-navigation-store.d.ts +1 -0
  66. package/lib/src/shared/page-navigation/store/page-navigation-store.js +12 -0
  67. package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
  68. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +2 -1
  69. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +190 -1
  70. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +184 -2
  71. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +34 -2
  72. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -2
  73. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +3 -0
  74. package/package.json +2 -2
  75. package/src/brand-page/components/navigation/hamburger/HamburgerNavigation.vue +1 -0
  76. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +18 -1
  77. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +73 -3
  78. package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +181 -0
  79. package/src/brand-page/components/page/Page.mixin.ts +21 -11
  80. package/src/brand-page/components/page/Page.vue +2 -0
  81. package/src/brand-page/components/page-header/HeaderLayout.stories.ts +10 -0
  82. package/src/brand-page/components/page-header/HeaderLayout.vue +415 -71
  83. package/src/brand-page/components/page-header/PageHeader.mixin.ts +14 -10
  84. package/src/brand-page/components/page-header/PageHeader.vue +16 -1
  85. package/src/brand-page/components/page-header/PageHeader.vue.test.ts +339 -0
  86. package/src/brand-page/constants/sections/header-display.constants.test.ts +44 -0
  87. package/src/brand-page/constants/sections/header-display.constants.ts +35 -0
  88. package/src/brand-page/models/header-button.model.test.ts +85 -0
  89. package/src/brand-page/models/index.ts +19 -0
  90. package/src/brand-page/utils/context.util.ts +15 -0
  91. package/src/brand-page/utils/route.util.ts +13 -0
  92. package/src/brand-page/utils/view-model.util.test.ts +151 -1
  93. package/src/brand-page/utils/view-model.util.ts +73 -1
  94. package/src/brand-page/view-models.ts +3 -0
  95. package/src/brand-page-maker/validators/section-button.validator.ts +23 -19
  96. package/src/bundles/bundled-translations.de-DE.json +6 -1
  97. package/src/bundles/bundled-translations.es-ES.json +6 -1
  98. package/src/bundles/bundled-translations.fr-CA.json +6 -1
  99. package/src/bundles/bundled-translations.fr-FR.json +6 -1
  100. package/src/bundles/bundled-translations.json +6 -1
  101. package/src/bundles/bundled-translations.pt-BR.json +6 -1
  102. package/src/bundles/bundled-translations.pt-PT.json +6 -1
  103. package/src/shared/content-settings/components/text-field/TextField.mixin.ts +15 -0
  104. package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
  105. package/src/shared/page-navigation/store/page-navigation-store.ts +16 -0
  106. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.button-section.test.ts +131 -0
  107. package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +65 -10
  108. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +305 -0
  109. package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +265 -0
  110. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.test.ts +354 -0
  111. package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue +48 -9
  112. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.header-button.test.ts +114 -0
  113. package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +38 -1
  114. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.de-DE.json +6 -1
  115. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.es-ES.json +6 -1
  116. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.fr-CA.json +6 -1
  117. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.fr-FR.json +6 -1
  118. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.json +6 -1
  119. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.pt-BR.json +6 -1
  120. package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.pt-PT.json +6 -1
  121. package/src/shared/page-settings/store/page-settings-store.ts +2 -0
@@ -0,0 +1,131 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { shallowMount } from '@vue/test-utils';
4
+ import { Toggle } from '@designcrowd/fe-shared-lib';
5
+ import type { LogoTheme } from '@brand-page';
6
+ import { VIEWPORTS } from '@brand-page';
7
+ import HeaderSettingsPanel from './HeaderSettingsPanel.vue';
8
+ import HeaderButtonSettings from '../page-settings-section-styles/HeaderButtonSettings.vue';
9
+ import { useUiPageSettingsStore } from '../../store';
10
+ import { FabricRenderClient } from '../../../../clients/fabric-render-client';
11
+
12
+ // Button section: brandsite-only, header toggle shows/hides the fields, writes sync both viewports.
13
+ describe('HeaderSettingsPanel — Button section', () => {
14
+ beforeEach(() => {
15
+ setActivePinia(createPinia());
16
+ // The Logo section renders alongside and needs the client singleton.
17
+ FabricRenderClient.setInstance({ baseUrl: 'http://localhost:3000' });
18
+ });
19
+
20
+ const CollapsiblePanelStub = {
21
+ template: '<div><slot name="header-actions" /><slot /></div>',
22
+ };
23
+
24
+ const mountPanel = (isBrandSite: boolean) =>
25
+ shallowMount(HeaderSettingsPanel, {
26
+ props: {
27
+ isAdminMaker: false,
28
+ isBrandSite,
29
+ logoTheme: {} as LogoTheme,
30
+ sectionName: 'header',
31
+ styleOptions: [],
32
+ },
33
+ global: {
34
+ // Render CollapsiblePanel slots so the toggle and body aren't swallowed.
35
+ stubs: { CollapsiblePanel: CollapsiblePanelStub },
36
+ },
37
+ });
38
+
39
+ const buttonToggle = (wrapper: ReturnType<typeof mountPanel>) =>
40
+ wrapper
41
+ .findAllComponents(Toggle)
42
+ .find(
43
+ (t) => t.attributes('data-test-header-button-toggle') !== undefined,
44
+ );
45
+
46
+ test('brandsite: the header toggle renders, off, and the fields stay hidden', () => {
47
+ const wrapper = mountPanel(true);
48
+
49
+ expect(buttonToggle(wrapper)).toBeDefined();
50
+ // the stubbed Toggle carries its props as attributes
51
+ expect(buttonToggle(wrapper)?.attributes('model-value')).toBe('false');
52
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
53
+ });
54
+
55
+ test('brand page / brand contact: no Button section at all', () => {
56
+ const wrapper = mountPanel(false);
57
+
58
+ expect(buttonToggle(wrapper)).toBeUndefined();
59
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
60
+ });
61
+
62
+ test('toggling on writes isVisible to BOTH viewports and reveals the fields', async () => {
63
+ const wrapper = mountPanel(true);
64
+
65
+ await buttonToggle(wrapper)?.vm.$emit('update:model-value', true);
66
+ await wrapper.vm.$nextTick();
67
+
68
+ const sections = useUiPageSettingsStore().sections.header;
69
+
70
+ expect(sections[VIEWPORTS.desktop].headerButton?.isVisible).toBe(true);
71
+ expect(sections[VIEWPORTS.mobile].headerButton?.isVisible).toBe(true);
72
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(true);
73
+ });
74
+
75
+ test('expanding via the chevron while off switches the button on; later collapses freely', async () => {
76
+ const wrapper = mountPanel(true);
77
+ const sectionHeader = wrapper.get(
78
+ '[data-test-header-button-section-header]',
79
+ );
80
+
81
+ // off + collapsed: expanding turns the button on and shows the fields
82
+ await sectionHeader.trigger('click');
83
+
84
+ const sections = useUiPageSettingsStore().sections.header;
85
+
86
+ expect(sections[VIEWPORTS.desktop].headerButton?.isVisible).toBe(true);
87
+ expect(sections[VIEWPORTS.mobile].headerButton?.isVisible).toBe(true);
88
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(true);
89
+
90
+ // on: the chevron only collapses/expands, never switches the button off
91
+ await sectionHeader.trigger('click');
92
+
93
+ expect(sections[VIEWPORTS.desktop].headerButton?.isVisible).toBe(true);
94
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
95
+
96
+ await sectionHeader.trigger('click');
97
+
98
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(true);
99
+ });
100
+
101
+ test('toggling off hides the fields but keeps the configuration for a re-enable', async () => {
102
+ const wrapper = mountPanel(true);
103
+
104
+ await buttonToggle(wrapper)?.vm.$emit('update:model-value', true);
105
+ // simulate a label edit, which the mixin dual-writes to both viewports
106
+ [VIEWPORTS.desktop, VIEWPORTS.mobile].forEach((viewport) => {
107
+ useUiPageSettingsStore().setPageSettingsSectionValue({
108
+ sectionName: 'header',
109
+ propName: 'headerButton',
110
+ value: { isVisible: true, text: 'Book now', buttonLinkType: 'url' },
111
+ viewport,
112
+ });
113
+ });
114
+ await buttonToggle(wrapper)?.vm.$emit('update:model-value', false);
115
+ await wrapper.vm.$nextTick();
116
+
117
+ const desktop = useUiPageSettingsStore().sections.header[VIEWPORTS.desktop];
118
+
119
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
120
+ expect(desktop.headerButton?.isVisible).toBe(false);
121
+ expect(desktop.headerButton?.text).toBe('Book now');
122
+
123
+ // Toggling off also collapses, so one chevron click re-enables — no dead first click.
124
+ await wrapper
125
+ .get('[data-test-header-button-section-header]')
126
+ .trigger('click');
127
+
128
+ expect(desktop.headerButton?.isVisible).toBe(true);
129
+ expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(true);
130
+ });
131
+ });
@@ -64,6 +64,7 @@
64
64
  :on-select="handleHeaderDisplayStyleChange"
65
65
  :viewport="viewport"
66
66
  :is-logo-visible="isLogoVisible"
67
+ :show-header-button="isHeaderButtonVisible"
67
68
  />
68
69
 
69
70
  <NavigationStyleSettings
@@ -144,27 +145,63 @@
144
145
  </div>
145
146
  </CollapsiblePanel>
146
147
 
147
- <!-- Button section hidden for now (BP-5854) — re-enable once BP-5852 implements
148
- the header button. Left commented rather than removed since the shell
149
- (CollapsiblePanel + collapsedSections.button state) is already wired up.
150
- <CollapsiblePanel
151
- data-component="collapsible-panel"
152
- :title="pageSettingsTr('buttonSectionTitle')"
153
- :is-collapsed="collapsedSections.button"
154
- @changed="toggleSectionCollapsed('button')"
155
- />
156
- -->
148
+ <div
149
+ v-if="isBrandSite"
150
+ data-test-header-button-section
151
+ class="tw-bg-white tw-border tw-border-grayscale-500 tw-border-solid tw-flex tw-flex-col tw-mb-4 tw-p-4 tw-rounded-md"
152
+ >
153
+ <div
154
+ data-test-header-button-section-header
155
+ class="tw-cursor-pointer tw-text-secondary-500 tw-flex tw-items-center tw-select-none"
156
+ :class="{
157
+ 'tw-mb-2': !collapsedSections.button && isHeaderButtonVisible,
158
+ }"
159
+ @click="onButtonSectionChanged"
160
+ >
161
+ <div
162
+ class="tw-grow tw-text-md tw-font-sans tw-font-bold tw-text-secondary-800"
163
+ >
164
+ {{ pageSettingsTr('buttonSectionTitle') }}
165
+ </div>
166
+ <div class="tw-ml-2" @click.stop>
167
+ <Toggle
168
+ id="header-button-visible"
169
+ data-test-header-button-toggle
170
+ color="primary"
171
+ :model-value="isHeaderButtonVisible"
172
+ @update:model-value="onHeaderButtonToggle"
173
+ />
174
+ </div>
175
+ <Icon
176
+ class="tw-ml-2 tw-text-grayscale-600"
177
+ :name="collapsedSections.button ? 'chevron-down' : 'chevron-up'"
178
+ size="sm"
179
+ />
180
+ </div>
181
+ <HeaderButtonSettings
182
+ v-if="!collapsedSections.button && isHeaderButtonVisible"
183
+ :value="headerButton"
184
+ :is-admin-maker="isAdminMaker"
185
+ :logo-theme="logoTheme"
186
+ @on-change="onHeaderButtonChange"
187
+ />
188
+ </div>
189
+
190
+ <!-- The Page dropdown's ADD PAGE opens this; every panel hosting a PageDropdownField mounts its own. -->
191
+ <NewPageModal />
157
192
  </div>
158
193
  </template>
159
194
  <script>
160
195
  import {
161
196
  CollapsiblePanel,
162
197
  HelloBar,
198
+ Icon,
163
199
  Slider,
164
200
  TabMenu,
165
201
  Toggle,
166
202
  } from '@designcrowd/fe-shared-lib';
167
203
  import PageSettingsSectionStyleMixin from '../page-settings-section-styles/PageSettingsSectionStyles.mixin.ts';
204
+ import HeaderButtonSettings from '../page-settings-section-styles/HeaderButtonSettings.vue';
168
205
  import HeaderDisplayStyle from '../page-settings-section-styles/HeaderDisplayStyle.vue';
169
206
  import HeaderNavigationType from '../page-settings-section-styles/HeaderNavigationType.vue';
170
207
  import HeaderNavigationStyle from '../page-settings-section-styles/HeaderNavigationStyle.vue';
@@ -173,16 +210,20 @@ import ColorField from '../../../content-settings/components/color-field/ColorFi
173
210
  import LogoSelection from '@shared/content-settings/components/logo-selection-field/LogoSelection.vue';
174
211
  import LogoSizeSlider from '@shared/content-settings/components/logo-size-field/LogoSizeSlider.vue';
175
212
  import SideBarSettingsHeader from '../../../common/SideBarSettingsHeader.vue';
213
+ import NewPageModal from '../new-page-modal/NewPageModal.vue';
176
214
  import { tr, commonTr, pageSettingsTr } from '@src/composables/useTranslate';
177
215
 
178
216
  export default {
179
217
  components: {
218
+ NewPageModal,
180
219
  CollapsiblePanel,
181
220
  ColorField,
182
221
  HelloBar,
222
+ Icon,
183
223
  Slider,
184
224
  TabMenu,
185
225
  Toggle,
226
+ HeaderButtonSettings,
186
227
  HeaderDisplayStyle,
187
228
  HeaderNavigationType,
188
229
  HeaderNavigationStyle,
@@ -222,6 +263,20 @@ export default {
222
263
  toggleSectionCollapsed(section) {
223
264
  this.collapsedSections[section] = !this.collapsedSections[section];
224
265
  },
266
+ onHeaderButtonToggle(isVisible) {
267
+ this.onHeaderButtonChange({ isVisible });
268
+
269
+ this.collapsedSections.button = !isVisible;
270
+ },
271
+ onButtonSectionChanged() {
272
+ const isExpanding = this.collapsedSections.button;
273
+
274
+ this.toggleSectionCollapsed('button');
275
+
276
+ if (isExpanding && !this.isHeaderButtonVisible) {
277
+ this.onHeaderButtonChange({ isVisible: true });
278
+ }
279
+ },
225
280
  },
226
281
  };
227
282
  </script>
@@ -0,0 +1,305 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { shallowMount } from '@vue/test-utils';
4
+ import type { VueWrapper } from '@vue/test-utils';
5
+ import type { HeaderButton, LogoTheme } from '@brand-page';
6
+ import HeaderButtonSettings from './HeaderButtonSettings.vue';
7
+ import { useUiPageSettingsStore } from '../../store';
8
+ import { useUiStyleSettingsStore } from '../../../style-settings/store/style-settings-store';
9
+ import { availableButtonStyleConfigs } from '@brand-page-maker/models';
10
+ import type { ContentPage } from '../../models';
11
+
12
+ // Explicit stub: the auto-stub misses mixin-declared props, so props('value') would be undefined.
13
+ const TextStyleFieldStub = {
14
+ name: 'TextStyleField',
15
+ props: ['value'],
16
+ template: '<div />',
17
+ };
18
+
19
+ const UrlFieldStub = {
20
+ name: 'UrlField',
21
+ props: ['settings'],
22
+ template: '<div />',
23
+ };
24
+
25
+ const TextFieldStub = {
26
+ name: 'TextField',
27
+ // showErrorWhenEmpty typed so the bare attribute casts to true, as on the real component.
28
+ props: { settings: Object, showErrorWhenEmpty: Boolean },
29
+ template: '<div />',
30
+ };
31
+
32
+ // Fields are found by data-test hook (two of them are ColorFields).
33
+ const emitFrom = (
34
+ wrapper: VueWrapper,
35
+ hook: string,
36
+ payload: unknown,
37
+ ): void => {
38
+ (wrapper.findComponent(hook) as unknown as VueWrapper).vm.$emit(
39
+ 'update:modelValue',
40
+ payload,
41
+ );
42
+ };
43
+
44
+ // Every edit leaves through one on-change patch (the mixin writes it to both viewports).
45
+ describe('HeaderButtonSettings', () => {
46
+ beforeEach(() => {
47
+ setActivePinia(createPinia());
48
+ });
49
+
50
+ const mountPanel = (value?: HeaderButton) =>
51
+ shallowMount(HeaderButtonSettings, {
52
+ props: {
53
+ value,
54
+ isAdminMaker: false,
55
+ logoTheme: {} as LogoTheme,
56
+ },
57
+ global: {
58
+ stubs: {
59
+ TextStyleField: TextStyleFieldStub,
60
+ UrlField: UrlFieldStub,
61
+ TextField: TextFieldStub,
62
+ },
63
+ },
64
+ });
65
+
66
+ const enabled: HeaderButton = {
67
+ isVisible: true,
68
+ text: '',
69
+ buttonLinkType: 'url',
70
+ };
71
+
72
+ const fieldHooks = [
73
+ '[data-test-header-button-text]',
74
+ '[data-test-header-button-link-type]',
75
+ '[data-test-header-button-text-style]',
76
+ '[data-test-header-button-fill-color]',
77
+ ];
78
+
79
+ // The show/hide toggle lives in HeaderSettingsPanel, so every field here renders unconditionally.
80
+ test('every field renders', () => {
81
+ const wrapper = mountPanel(enabled);
82
+
83
+ fieldHooks.forEach((hook) => {
84
+ expect(wrapper.find(hook).exists()).toBe(true);
85
+ });
86
+ });
87
+
88
+ // Border color only shows when the selected button style draws a border.
89
+ test('the border colour field is hidden while the selected button style draws no border', () => {
90
+ // default selectedButtonStyleId is BUTTON_STYLE_1, whose options carry no border
91
+ const wrapper = mountPanel(enabled);
92
+
93
+ expect(
94
+ wrapper.find('[data-test-header-button-border-color]').exists(),
95
+ ).toBe(false);
96
+ });
97
+
98
+ test('the border colour field renders when the selected button style has a border', () => {
99
+ useUiStyleSettingsStore().$patch(($state) => {
100
+ // availableButtonStyleConfigs starts empty until the store initialises
101
+ $state.availableButtonStyleConfigs = availableButtonStyleConfigs;
102
+ $state.buttonStyles.selectedButtonStyleId = 'BUTTON_STYLE_4';
103
+ });
104
+
105
+ const wrapper = mountPanel(enabled);
106
+
107
+ expect(
108
+ wrapper.find('[data-test-header-button-border-color]').exists(),
109
+ ).toBe(true);
110
+ });
111
+
112
+ test('the link-value field follows the selected link kind', () => {
113
+ const kinds = {
114
+ url: '[data-test-header-button-url]',
115
+ page: '[data-test-header-button-page]',
116
+ mailTo: '[data-test-header-button-email]',
117
+ tel: '[data-test-header-button-tel]',
118
+ } as const;
119
+
120
+ Object.entries(kinds).forEach(([kind, hook]) => {
121
+ const wrapper = mountPanel({
122
+ ...enabled,
123
+ buttonLinkType: kind as HeaderButton['buttonLinkType'],
124
+ });
125
+
126
+ Object.values(kinds).forEach((otherHook) => {
127
+ expect(wrapper.find(otherHook).exists()).toBe(otherHook === hook);
128
+ });
129
+ });
130
+ });
131
+
132
+ const linkTypeValues = (wrapper: VueWrapper): string[] => {
133
+ const group = wrapper.findComponent(
134
+ '[data-test-header-button-link-type]',
135
+ ) as unknown as { props(name: string): unknown };
136
+
137
+ return (group.props('options') as { value: string }[]).map(
138
+ (option) => option.value,
139
+ );
140
+ };
141
+
142
+ test('the four link kinds are offered in a 2-column group', () => {
143
+ const wrapper = mountPanel(enabled);
144
+
145
+ expect(linkTypeValues(wrapper)).toEqual(['url', 'page', 'mailTo', 'tel']);
146
+ expect(
147
+ wrapper.find('[data-test-header-button-link-type]').attributes('columns'),
148
+ ).toBe('2');
149
+ });
150
+
151
+ test('a single-page site drops the Page kind (buttonLinkStyleConvertor)', () => {
152
+ useUiPageSettingsStore().setPages({
153
+ pages: [{ id: 'page-1', title: 'Home', contents: [] }] as ContentPage[],
154
+ });
155
+
156
+ expect(linkTypeValues(mountPanel(enabled))).toEqual([
157
+ 'url',
158
+ 'mailTo',
159
+ 'tel',
160
+ ]);
161
+ });
162
+
163
+ test('a label edit emits only the text patch', () => {
164
+ const wrapper = mountPanel(enabled);
165
+
166
+ emitFrom(wrapper, '[data-test-header-button-text]', {
167
+ id: 'headerButton',
168
+ name: 'text',
169
+ value: 'Book now',
170
+ });
171
+
172
+ expect(wrapper.emitted('on-change')).toEqual([[{ text: 'Book now' }]]);
173
+ });
174
+
175
+ // Unset text style must show the renderer's Buttons default, not None/blank.
176
+ test('an unset text style presents the Buttons-linked default', () => {
177
+ const wrapper = mountPanel(enabled);
178
+
179
+ expect(
180
+ wrapper.findComponent({ name: 'TextStyleField' }).props('value'),
181
+ ).toEqual({
182
+ textStyleType: 'buttons',
183
+ });
184
+ });
185
+
186
+ test('an authored text style is passed through untouched', () => {
187
+ const wrapper = mountPanel({
188
+ ...enabled,
189
+ buttonTextStyle: { fontFamily: 'Lora', textAlign: 'center' },
190
+ });
191
+
192
+ expect(
193
+ wrapper.findComponent({ name: 'TextStyleField' }).props('value'),
194
+ ).toEqual({
195
+ fontFamily: 'Lora',
196
+ textAlign: 'center',
197
+ });
198
+ });
199
+
200
+ // Stricter than the banner: the link is always required, whatever the label holds.
201
+ test('a labelled button with an empty link shows the error', () => {
202
+ const wrapper = mountPanel({ ...enabled, text: 'Contact Us' });
203
+
204
+ expect(
205
+ wrapper.findComponent({ name: 'UrlField' }).props('settings'),
206
+ ).toMatchObject({ isValid: false });
207
+ });
208
+
209
+ test('an unlabelled button with an empty link also shows the error', () => {
210
+ const wrapper = mountPanel(enabled);
211
+
212
+ expect(
213
+ wrapper.findComponent({ name: 'UrlField' }).props('settings'),
214
+ ).toMatchObject({ isValid: false });
215
+ });
216
+
217
+ test('a valid link is accepted while the label is still empty', () => {
218
+ const wrapper = mountPanel({ ...enabled, url: 'https://example.com' });
219
+
220
+ expect(
221
+ wrapper.findComponent({ name: 'UrlField' }).props('settings'),
222
+ ).toMatchObject({ isValid: true });
223
+ });
224
+
225
+ test('switching the link kind clears the previous value', () => {
226
+ const wrapper = mountPanel({ ...enabled, url: 'https://example.com' });
227
+
228
+ (
229
+ wrapper.findComponent(
230
+ '[data-test-header-button-link-type]',
231
+ ) as unknown as VueWrapper
232
+ ).vm.$emit('on-select', { value: 'mailTo' });
233
+
234
+ expect(wrapper.emitted('on-change')).toEqual([
235
+ [{ buttonLinkType: 'mailTo', url: '' }],
236
+ ]);
237
+ });
238
+
239
+ // The label is required — an empty one hides the button on the site.
240
+ test.each([[''], [' ']])(
241
+ 'a blank label shows the required error (%j)',
242
+ (text) => {
243
+ const wrapper = mountPanel({ ...enabled, text });
244
+
245
+ expect(
246
+ wrapper.findComponent({ name: 'TextField' }).props('settings'),
247
+ ).toMatchObject({ isValid: false });
248
+ },
249
+ );
250
+
251
+ test('the label shows its error as soon as it is cleared', () => {
252
+ const wrapper = mountPanel(enabled);
253
+
254
+ expect(
255
+ wrapper.findComponent({ name: 'TextField' }).props('showErrorWhenEmpty'),
256
+ ).toBe(true);
257
+ });
258
+
259
+ test('a filled label is valid', () => {
260
+ const wrapper = mountPanel({ ...enabled, text: 'Contact Us' });
261
+
262
+ expect(
263
+ wrapper.findComponent({ name: 'TextField' }).props('settings'),
264
+ ).toMatchObject({ isValid: true });
265
+ });
266
+
267
+ // A transparent fill or border would leave a ghost button, so neither offers the option.
268
+ test.each([
269
+ ['fill', '[data-test-header-button-fill-color]', 'BUTTON_STYLE_1'],
270
+ ['border', '[data-test-header-button-border-color]', 'BUTTON_STYLE_4'],
271
+ ])(
272
+ 'the %s colour field does not offer transparent',
273
+ (_name, hook, styleId) => {
274
+ useUiStyleSettingsStore().$patch(($state) => {
275
+ $state.availableButtonStyleConfigs = availableButtonStyleConfigs;
276
+ // BUTTON_STYLE_4 draws a border, so the border field renders.
277
+ $state.buttonStyles.selectedButtonStyleId = styleId;
278
+ });
279
+
280
+ const wrapper = mountPanel(enabled);
281
+
282
+ expect(
283
+ (
284
+ wrapper.findComponent(hook) as unknown as {
285
+ props(name: string): unknown;
286
+ }
287
+ ).props('disableTransparent'),
288
+ ).toBe(true);
289
+ },
290
+ );
291
+
292
+ test('a fill-colour pick emits only the fillColor patch', () => {
293
+ const wrapper = mountPanel(enabled);
294
+
295
+ emitFrom(wrapper, '[data-test-header-button-fill-color]', {
296
+ id: 'fillColor',
297
+ name: 'fillColor',
298
+ value: 'var(--palette-2)',
299
+ });
300
+
301
+ expect(wrapper.emitted('on-change')).toEqual([
302
+ [{ fillColor: 'var(--palette-2)' }],
303
+ ]);
304
+ });
305
+ });