@designcrowd/library.brand-page 6.1.16 → 6.1.17-ast-header-button
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-CmNhDMIf.js → CreatedOnBrandCrowd-de-de-Cwq9enJs.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DgplxxWa.js → CreatedOnBrandCrowd-en-us-ILK04gFj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DFdhFOa1.js → CreatedOnBrandCrowd-es-es-CbjxLxtu.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-DjLCWBLB.js → CreatedOnBrandCrowd-fr-ca-BwBDiK9z.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-DjLCWBLB.js → CreatedOnBrandCrowd-fr-fr-BwBDiK9z.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-CI2XD0OS.js → CreatedOnBrandCrowd-pt-br-DvhJROpG.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-CI2XD0OS.js → CreatedOnBrandCrowd-pt-pt-DvhJROpG.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CkLkBztx.js → CreatedOnDcom-de-de-BK9imWOh.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-frnxBJJv.js → CreatedOnDcom-en-us-CyH-_qz1.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CrqgiERj.js → CreatedOnDcom-es-es-Dh51qYEA.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CPoEcusj.js → CreatedOnDcom-fr-ca-BArRxwWX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-BqrRH3dV.js → CreatedOnDcom-fr-fr-CdglI-ZB.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-CpbVi3Sw.js → CreatedOnDcom-pt-br-DCehns-G.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-s0XZ3c7y.js → CreatedOnDcom-pt-pt-B8hl-oew.js} +1 -1
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +27 -2
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +26 -0
- package/lib/src/brand-page/components/page-header/PageHeader.mixin.d.ts +3 -1
- package/lib/src/brand-page/components/page-header/PageHeader.mixin.js +12 -0
- package/lib/src/brand-page/models/index.d.ts +12 -0
- package/lib/src/brand-page/models/index.js +3 -0
- package/lib/src/brand-page/utils/button.util.d.ts +5 -0
- package/lib/src/brand-page/utils/button.util.js +4 -0
- package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
- package/lib/src/brand-page/utils/view-model.util.js +51 -1
- package/lib/src/brand-page/view-models.d.ts +3 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1008 -0
- package/lib/src/{bundled-translations-B2q5GWTQ.js → bundled-translations-B8fAEel0.js} +1 -1
- package/lib/src/bundles/bundled-translations.json +7 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +315 -122
- package/lib/src/css/library.brand-page-brandPage.min.css +315 -122
- package/lib/src/css/library.brand-page-designCom.min.css +315 -122
- package/lib/src/css/library.brand-page-preview.min.css +315 -122
- package/lib/src/index.d.ts +1 -0
- package/lib/src/index.mjs +45 -44
- package/lib/src/{lib-O_eJgJHP.js → lib-KFjPFfxn.js} +31522 -31118
- package/lib/src/lib.js +1 -0
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +1 -1
- package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.d.ts +11 -0
- package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.js +80 -0
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
- 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 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +113 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +117 -2
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +43 -1
- package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -2
- package/package.json +1 -1
- package/src/brand-page/components/button/Button.vue +35 -8
- package/src/brand-page/components/button/Button.vue.test.ts +59 -0
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +29 -1
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +36 -0
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +175 -0
- package/src/brand-page/components/page/Page.vue +6 -0
- package/src/brand-page/components/page-header/HeaderLayout.vue +198 -4
- package/src/brand-page/components/page-header/PageHeader.mixin.ts +16 -1
- package/src/brand-page/components/page-header/PageHeader.vue +23 -1
- package/src/brand-page/components/page-header/PageHeader.vue.test.ts +378 -0
- package/src/brand-page/models/header-button.model.test.ts +86 -0
- package/src/brand-page/models/index.ts +16 -0
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +6 -0
- package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +2 -0
- package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +4 -0
- package/src/brand-page/utils/button.util.ts +8 -0
- package/src/brand-page/utils/view-model.util.test.ts +146 -1
- package/src/brand-page/utils/view-model.util.ts +71 -1
- package/src/brand-page/view-models.ts +3 -0
- package/src/bundles/bundled-translations.json +7 -1
- package/src/lib.ts +1 -0
- package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.test.ts +399 -0
- package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.ts +99 -0
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.button-section.test.ts +126 -0
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +43 -6
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +167 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +237 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.test.ts +160 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue +37 -8
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.header-button.test.ts +113 -0
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +47 -0
- package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.json +7 -1
- package/src/shared/page-settings/store/page-settings-store.ts +2 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
4
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
5
|
+
import PageHeader from './PageHeader.vue';
|
|
6
|
+
import {
|
|
7
|
+
BRAND_PAGE_TYPES,
|
|
8
|
+
BUTTON_LINK_TYPE,
|
|
9
|
+
MEDIA_SIZES,
|
|
10
|
+
PageHeaderDesktop,
|
|
11
|
+
PageHeaderMobile,
|
|
12
|
+
PageStyles,
|
|
13
|
+
type BrandPageContext,
|
|
14
|
+
type HeaderButton,
|
|
15
|
+
} from '@brand-page/models';
|
|
16
|
+
import {
|
|
17
|
+
HEADER_DISPLAY,
|
|
18
|
+
type HeaderDisplayStyle,
|
|
19
|
+
} from '@src/brand-page/constants';
|
|
20
|
+
import type { NavItemViewModel } from '@brand-page/view-models';
|
|
21
|
+
import { mapToPageHeaderViewModel } from '@brand-page/utils';
|
|
22
|
+
|
|
23
|
+
// NavigationCarousel calls `new ResizeObserver(...)`, so this has to be constructible.
|
|
24
|
+
global.ResizeObserver = class {
|
|
25
|
+
observe = vi.fn();
|
|
26
|
+
unobserve = vi.fn();
|
|
27
|
+
disconnect = vi.fn();
|
|
28
|
+
} as unknown as typeof ResizeObserver;
|
|
29
|
+
|
|
30
|
+
const CTA = '[data-test-header-cta-button]';
|
|
31
|
+
const SLUG = 'my-brand';
|
|
32
|
+
|
|
33
|
+
const navItems = (): NavItemViewModel[] =>
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
pageId: 'home',
|
|
37
|
+
slug: SLUG,
|
|
38
|
+
displayName: 'Home',
|
|
39
|
+
routeName: 'home-route',
|
|
40
|
+
routeParams: { slug: SLUG },
|
|
41
|
+
isRoot: true,
|
|
42
|
+
isHidden: false,
|
|
43
|
+
relativePath: '',
|
|
44
|
+
nestedNavItems: [],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pageId: 'page-2',
|
|
48
|
+
slug: SLUG,
|
|
49
|
+
displayName: 'Second Page',
|
|
50
|
+
routeName: 'page-route',
|
|
51
|
+
routeParams: { slug: SLUG, pageSlug: 'second-page' },
|
|
52
|
+
isRoot: false,
|
|
53
|
+
isHidden: false,
|
|
54
|
+
relativePath: 'second-page',
|
|
55
|
+
nestedNavItems: [],
|
|
56
|
+
},
|
|
57
|
+
] as NavItemViewModel[];
|
|
58
|
+
|
|
59
|
+
const createContext = (): BrandPageContext =>
|
|
60
|
+
({
|
|
61
|
+
brandPageSlug: SLUG,
|
|
62
|
+
brandPageToken: '',
|
|
63
|
+
isMobile: false,
|
|
64
|
+
isPreview: false,
|
|
65
|
+
isProduction: false,
|
|
66
|
+
isServerRender: false,
|
|
67
|
+
options: { mediaSizes: MEDIA_SIZES.sizeThumb },
|
|
68
|
+
isTemplate: false,
|
|
69
|
+
isDesignCom: false,
|
|
70
|
+
pageTitle: 'My Brand',
|
|
71
|
+
runtimeEnvironment: '',
|
|
72
|
+
stripePublishableKey: undefined,
|
|
73
|
+
recaptchaSiteKey: undefined,
|
|
74
|
+
customDomain: undefined,
|
|
75
|
+
brandPageType: BRAND_PAGE_TYPES.brandSite,
|
|
76
|
+
isBrandPage: false,
|
|
77
|
+
isBrandPageOrIsBrandContact: false,
|
|
78
|
+
isBrandSite: true,
|
|
79
|
+
isBrandContact: false,
|
|
80
|
+
pages: [],
|
|
81
|
+
isCapturingPreview: false,
|
|
82
|
+
nav: { navItems: navItems() },
|
|
83
|
+
}) as BrandPageContext;
|
|
84
|
+
|
|
85
|
+
const aHeaderButton = (
|
|
86
|
+
overrides: Partial<HeaderButton> = {},
|
|
87
|
+
): HeaderButton => ({
|
|
88
|
+
isVisible: true,
|
|
89
|
+
text: 'Book now',
|
|
90
|
+
buttonLinkType: BUTTON_LINK_TYPE.url,
|
|
91
|
+
url: 'https://example.com/book',
|
|
92
|
+
buttonTextStyle: { textStyleType: 'buttons' },
|
|
93
|
+
...overrides,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const mountHeader = async (options: {
|
|
97
|
+
headerButton?: HeaderButton;
|
|
98
|
+
displayStyle?: HeaderDisplayStyle;
|
|
99
|
+
isLogoVisible?: boolean;
|
|
100
|
+
navItems?: NavItemViewModel[];
|
|
101
|
+
isMobile?: boolean;
|
|
102
|
+
isPreview?: boolean;
|
|
103
|
+
}) => {
|
|
104
|
+
const router = createRouter({
|
|
105
|
+
history: createMemoryHistory(),
|
|
106
|
+
routes: [
|
|
107
|
+
{
|
|
108
|
+
path: '/:slug/:pageSlug',
|
|
109
|
+
name: 'page-route',
|
|
110
|
+
component: { template: '<div />' },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
path: '/:slug',
|
|
114
|
+
name: 'home-route',
|
|
115
|
+
component: { template: '<div />' },
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
router.push(`/${SLUG}`);
|
|
121
|
+
await router.isReady();
|
|
122
|
+
|
|
123
|
+
const HeaderModel = options.isMobile ? PageHeaderMobile : PageHeaderDesktop;
|
|
124
|
+
const context = createContext();
|
|
125
|
+
|
|
126
|
+
if (options.navItems) {
|
|
127
|
+
context.nav.navItems = options.navItems;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.isPreview) {
|
|
131
|
+
context.isPreview = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const viewModel = mapToPageHeaderViewModel(
|
|
135
|
+
new HeaderModel({
|
|
136
|
+
headerButton: options.headerButton,
|
|
137
|
+
isLogoVisible: options.isLogoVisible ?? true,
|
|
138
|
+
...(options.displayStyle ? { displayStyle: options.displayStyle } : {}),
|
|
139
|
+
}),
|
|
140
|
+
new PageStyles(),
|
|
141
|
+
[],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return mount(PageHeader, {
|
|
145
|
+
propsData: { navId: 'nav-1', viewModel, context },
|
|
146
|
+
global: { plugins: [router] },
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
describe('PageHeader - header call-to-action button (BP-5852)', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
setActivePinia(createPinia());
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('visibility', () => {
|
|
156
|
+
test('renders no CTA when the header has no button', async () => {
|
|
157
|
+
const wrapper = await mountHeader({});
|
|
158
|
+
|
|
159
|
+
expect(wrapper.find(CTA).exists()).toBe(false);
|
|
160
|
+
expect(
|
|
161
|
+
wrapper.find('[data-component="header-layout-cta"]').exists(),
|
|
162
|
+
).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('renders no CTA when the button is configured but hidden', async () => {
|
|
166
|
+
const wrapper = await mountHeader({
|
|
167
|
+
headerButton: aHeaderButton({ isVisible: false }),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(wrapper.find(CTA).exists()).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('renders the CTA when the button is visible', async () => {
|
|
174
|
+
const wrapper = await mountHeader({ headerButton: aHeaderButton() });
|
|
175
|
+
|
|
176
|
+
expect(wrapper.find(CTA).exists()).toBe(true);
|
|
177
|
+
expect(wrapper.get(CTA).text()).toContain('Book now');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('falls back to "Contact Us" when the label is empty', async () => {
|
|
181
|
+
const wrapper = await mountHeader({
|
|
182
|
+
headerButton: aHeaderButton({ text: '' }),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(wrapper.get(CTA).text()).toContain('Contact Us');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// The header row is CTA-free on the mobile hamburger styles — the drawer carries
|
|
189
|
+
// the button there (NavigationSideMenu).
|
|
190
|
+
test.each([
|
|
191
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_1,
|
|
192
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_2,
|
|
193
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_3,
|
|
194
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_4,
|
|
195
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_1,
|
|
196
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_2,
|
|
197
|
+
HEADER_DISPLAY.MOBILE_HAMBURGER_NO_LOGO_3,
|
|
198
|
+
])('renders no CTA in the %s header row', async (displayStyle) => {
|
|
199
|
+
const wrapper = await mountHeader({
|
|
200
|
+
headerButton: aHeaderButton(),
|
|
201
|
+
displayStyle,
|
|
202
|
+
isMobile: true,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(wrapper.find(CTA).exists()).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test.each([
|
|
209
|
+
HEADER_DISPLAY.DESKTOP_HAMBURGER_1,
|
|
210
|
+
HEADER_DISPLAY.DESKTOP_HAMBURGER_2,
|
|
211
|
+
])('renders the CTA in the %s header row', async (displayStyle) => {
|
|
212
|
+
const wrapper = await mountHeader({
|
|
213
|
+
headerButton: aHeaderButton(),
|
|
214
|
+
displayStyle,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(wrapper.find(CTA).exists()).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('renders the CTA in the mobile hamburger row when the nav is hidden (single page: no icon, no drawer)', async () => {
|
|
221
|
+
const wrapper = await mountHeader({
|
|
222
|
+
headerButton: aHeaderButton(),
|
|
223
|
+
displayStyle: HEADER_DISPLAY.MOBILE_HAMBURGER_1,
|
|
224
|
+
isMobile: true,
|
|
225
|
+
navItems: [navItems()[0]],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(wrapper.find(CTA).exists()).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('render gate', () => {
|
|
233
|
+
// A single-page site with a hidden logo renders no header at all today; a visible
|
|
234
|
+
// CTA has to bring the header wrapper back.
|
|
235
|
+
const singlePageNav = () => [navItems()[0]];
|
|
236
|
+
|
|
237
|
+
test('renders no header when there is no nav, no logo and no button', async () => {
|
|
238
|
+
const wrapper = await mountHeader({
|
|
239
|
+
isLogoVisible: false,
|
|
240
|
+
navItems: singlePageNav(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(wrapper.find('[data-component="header-layout"]').exists()).toBe(
|
|
244
|
+
false,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('renders the header when only the button is visible', async () => {
|
|
249
|
+
const wrapper = await mountHeader({
|
|
250
|
+
headerButton: aHeaderButton(),
|
|
251
|
+
isLogoVisible: false,
|
|
252
|
+
navItems: singlePageNav(),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(wrapper.find('[data-component="header-layout"]').exists()).toBe(
|
|
256
|
+
true,
|
|
257
|
+
);
|
|
258
|
+
expect(wrapper.find(CTA).exists()).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('layout hooks', () => {
|
|
263
|
+
test('flags the layout with has-header-cta only while a CTA renders', async () => {
|
|
264
|
+
const withButton = await mountHeader({
|
|
265
|
+
headerButton: aHeaderButton(),
|
|
266
|
+
});
|
|
267
|
+
const without = await mountHeader({});
|
|
268
|
+
|
|
269
|
+
expect(
|
|
270
|
+
withButton.get('[data-component="header-layout"]').classes(),
|
|
271
|
+
).toContain('has-header-cta');
|
|
272
|
+
expect(
|
|
273
|
+
without.get('[data-component="header-layout"]').classes(),
|
|
274
|
+
).not.toContain('has-header-cta');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('link kinds', () => {
|
|
279
|
+
test('renders an external url as a new-tab anchor', async () => {
|
|
280
|
+
const wrapper = await mountHeader({
|
|
281
|
+
headerButton: aHeaderButton({ url: 'https://example.com/book' }),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const anchor = wrapper.get(`${CTA} a`);
|
|
285
|
+
|
|
286
|
+
expect(anchor.attributes('href')).toBe('https://example.com/book');
|
|
287
|
+
// target/rel are the external-anchor branch; the router-link branch has neither.
|
|
288
|
+
expect(anchor.attributes('target')).toBe('_blank');
|
|
289
|
+
expect(anchor.attributes('rel')).toBe('noopener');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('renders a picked page as an in-app router link', async () => {
|
|
293
|
+
const wrapper = await mountHeader({
|
|
294
|
+
headerButton: aHeaderButton({
|
|
295
|
+
buttonLinkType: BUTTON_LINK_TYPE.page,
|
|
296
|
+
buttonPageId: 'page-2',
|
|
297
|
+
url: undefined,
|
|
298
|
+
}),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const anchor = wrapper.get(`${CTA} a`);
|
|
302
|
+
|
|
303
|
+
expect(anchor.attributes('href')).toBe(`/${SLUG}/second-page`);
|
|
304
|
+
expect(anchor.attributes('target')).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('renders an email address as a mailto link', async () => {
|
|
308
|
+
const wrapper = await mountHeader({
|
|
309
|
+
headerButton: aHeaderButton({
|
|
310
|
+
buttonLinkType: BUTTON_LINK_TYPE.mailTo,
|
|
311
|
+
url: 'hi@example.com',
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(wrapper.get(`${CTA} a`).attributes('href')).toBe(
|
|
316
|
+
'mailto:hi@example.com',
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('renders a phone number as a tel link', async () => {
|
|
321
|
+
const wrapper = await mountHeader({
|
|
322
|
+
headerButton: aHeaderButton({
|
|
323
|
+
buttonLinkType: BUTTON_LINK_TYPE.tel,
|
|
324
|
+
url: '+61400000000',
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(wrapper.get(`${CTA} a`).attributes('href')).toBe(
|
|
329
|
+
'tel:+61400000000',
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('maker click guard', () => {
|
|
335
|
+
// In the maker preview the CTA is fully inert: it must not select the header
|
|
336
|
+
// panel (no bubble) and must not follow its link (default prevented).
|
|
337
|
+
test('a preview CTA click neither bubbles to the header container nor follows the link', async () => {
|
|
338
|
+
const wrapper = await mountHeader({
|
|
339
|
+
headerButton: aHeaderButton(),
|
|
340
|
+
isPreview: true,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const event = new MouseEvent('click', {
|
|
344
|
+
bubbles: true,
|
|
345
|
+
cancelable: true,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
wrapper.get(`${CTA} a`).element.dispatchEvent(event);
|
|
349
|
+
|
|
350
|
+
expect(event.defaultPrevented).toBe(true);
|
|
351
|
+
expect(wrapper.emitted('header-container-click')).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('a published CTA click follows the link (default not prevented)', async () => {
|
|
355
|
+
const wrapper = await mountHeader({ headerButton: aHeaderButton() });
|
|
356
|
+
|
|
357
|
+
const event = new MouseEvent('click', {
|
|
358
|
+
bubbles: true,
|
|
359
|
+
cancelable: true,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
wrapper.get(`${CTA} a`).element.dispatchEvent(event);
|
|
363
|
+
|
|
364
|
+
expect(event.defaultPrevented).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('still reports a click on the header container itself in preview', async () => {
|
|
368
|
+
const wrapper = await mountHeader({
|
|
369
|
+
headerButton: aHeaderButton(),
|
|
370
|
+
isPreview: true,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await wrapper.get('.c-page-header-container').trigger('click');
|
|
374
|
+
|
|
375
|
+
expect(wrapper.emitted('header-container-click')).toHaveLength(1);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
BrandPageModel,
|
|
4
|
+
BUTTON_LINK_TYPE,
|
|
5
|
+
PageHeaderDesktop,
|
|
6
|
+
PageHeaderMobile,
|
|
7
|
+
type HeaderButton,
|
|
8
|
+
} from './index';
|
|
9
|
+
|
|
10
|
+
const aHeaderButton = (): HeaderButton => ({
|
|
11
|
+
isVisible: true,
|
|
12
|
+
text: 'Contact Us',
|
|
13
|
+
buttonLinkType: BUTTON_LINK_TYPE.url,
|
|
14
|
+
url: 'https://example.com',
|
|
15
|
+
buttonTextStyle: { textStyleType: 'buttons' },
|
|
16
|
+
fillColor: '#123456',
|
|
17
|
+
borderColor: '#654321',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// The header constructors copy known fields one-by-one, so an uncopied field is
|
|
21
|
+
// silently dropped on every deserialize.
|
|
22
|
+
describe('header button persistence', () => {
|
|
23
|
+
describe('PageHeader* constructor', () => {
|
|
24
|
+
test('keeps a persisted headerButton on both viewports', () => {
|
|
25
|
+
expect(
|
|
26
|
+
new PageHeaderDesktop({ headerButton: aHeaderButton() }).headerButton,
|
|
27
|
+
).toEqual(aHeaderButton());
|
|
28
|
+
expect(
|
|
29
|
+
new PageHeaderMobile({ headerButton: aHeaderButton() }).headerButton,
|
|
30
|
+
).toEqual(aHeaderButton());
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('leaves headerButton unset when absent (no default seeded)', () => {
|
|
34
|
+
expect(new PageHeaderDesktop().headerButton).toBeUndefined();
|
|
35
|
+
expect(new PageHeaderMobile().headerButton).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('keeps a page link button with buttonPageId', () => {
|
|
39
|
+
const button: HeaderButton = {
|
|
40
|
+
isVisible: true,
|
|
41
|
+
text: 'About',
|
|
42
|
+
buttonLinkType: BUTTON_LINK_TYPE.page,
|
|
43
|
+
buttonPageId: 'page-2',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
new PageHeaderDesktop({ headerButton: button }).headerButton,
|
|
48
|
+
).toEqual(button);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('BrandPageModel round-trip', () => {
|
|
53
|
+
test('survives deserialization from persisted JSON on both viewports', () => {
|
|
54
|
+
const model = new BrandPageModel({
|
|
55
|
+
styles: {},
|
|
56
|
+
pages: [],
|
|
57
|
+
pageHeaderDesktop: { headerButton: aHeaderButton() },
|
|
58
|
+
pageHeaderMobile: { headerButton: aHeaderButton() },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(model.pageHeaderDesktop.headerButton).toEqual(aHeaderButton());
|
|
62
|
+
expect(model.pageHeaderMobile.headerButton).toEqual(aHeaderButton());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('survives a second pass through the model (re-serialize then re-hydrate)', () => {
|
|
66
|
+
const first = new BrandPageModel({
|
|
67
|
+
styles: {},
|
|
68
|
+
pages: [],
|
|
69
|
+
pageHeaderDesktop: { headerButton: aHeaderButton() },
|
|
70
|
+
pageHeaderMobile: { headerButton: aHeaderButton() },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const second = new BrandPageModel(JSON.parse(JSON.stringify(first)));
|
|
74
|
+
|
|
75
|
+
expect(second.pageHeaderDesktop.headerButton).toEqual(aHeaderButton());
|
|
76
|
+
expect(second.pageHeaderMobile.headerButton).toEqual(aHeaderButton());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('an existing site with no headerButton stays without one', () => {
|
|
80
|
+
const model = new BrandPageModel({ styles: {}, pages: [] });
|
|
81
|
+
|
|
82
|
+
expect(model.pageHeaderDesktop.headerButton).toBeUndefined();
|
|
83
|
+
expect(model.pageHeaderMobile.headerButton).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -713,6 +713,17 @@ export class NavModel {
|
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
export interface HeaderButton {
|
|
717
|
+
isVisible: boolean;
|
|
718
|
+
text: string;
|
|
719
|
+
buttonLinkType: ButtonLinkType;
|
|
720
|
+
url?: string;
|
|
721
|
+
buttonPageId?: string;
|
|
722
|
+
buttonTextStyle?: TextStyleDeclaration;
|
|
723
|
+
fillColor?: string;
|
|
724
|
+
borderColor?: string;
|
|
725
|
+
}
|
|
726
|
+
|
|
716
727
|
type AbstractPageHeaderStylesType = Partial<AbstractPageHeaderStyles>;
|
|
717
728
|
|
|
718
729
|
export abstract class AbstractPageHeaderStyles {
|
|
@@ -742,6 +753,9 @@ export abstract class AbstractPageHeaderStyles {
|
|
|
742
753
|
style: BrandPageCssStyle;
|
|
743
754
|
viewport: ViewportsType;
|
|
744
755
|
|
|
756
|
+
/** Absent means no header call-to-action button — never seed a default. */
|
|
757
|
+
headerButton?: HeaderButton;
|
|
758
|
+
|
|
745
759
|
protected constructor(options: AbstractPageHeaderStylesType = {}) {
|
|
746
760
|
if (typeof options.viewport === 'undefined') {
|
|
747
761
|
throw new Error('"viewport" is required');
|
|
@@ -779,6 +793,8 @@ export abstract class AbstractPageHeaderStyles {
|
|
|
779
793
|
...(options.backgroundStyle || {}),
|
|
780
794
|
backgroundOpacity: options.backgroundStyle?.backgroundOpacity ?? 100,
|
|
781
795
|
};
|
|
796
|
+
|
|
797
|
+
this.headerButton = options.headerButton;
|
|
782
798
|
}
|
|
783
799
|
}
|
|
784
800
|
|
|
@@ -156,6 +156,7 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
|
|
|
156
156
|
"backgroundOpacity": 100,
|
|
157
157
|
},
|
|
158
158
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
159
|
+
"headerButton": undefined,
|
|
159
160
|
"isLogoOptional": false,
|
|
160
161
|
"isLogoVisible": true,
|
|
161
162
|
"logoImage": {
|
|
@@ -231,6 +232,7 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
|
|
|
231
232
|
"backgroundOpacity": 100,
|
|
232
233
|
},
|
|
233
234
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
235
|
+
"headerButton": undefined,
|
|
234
236
|
"isLogoOptional": false,
|
|
235
237
|
"isLogoVisible": true,
|
|
236
238
|
"logoImage": {
|
|
@@ -653,6 +655,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
653
655
|
"backgroundOpacity": 100,
|
|
654
656
|
},
|
|
655
657
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
658
|
+
"headerButton": undefined,
|
|
656
659
|
"isLogoOptional": false,
|
|
657
660
|
"isLogoVisible": true,
|
|
658
661
|
"logoImage": {
|
|
@@ -728,6 +731,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
728
731
|
"backgroundOpacity": 100,
|
|
729
732
|
},
|
|
730
733
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
734
|
+
"headerButton": undefined,
|
|
731
735
|
"isLogoOptional": false,
|
|
732
736
|
"isLogoVisible": true,
|
|
733
737
|
"logoImage": {
|
|
@@ -1134,6 +1138,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
1134
1138
|
"backgroundOpacity": 100,
|
|
1135
1139
|
},
|
|
1136
1140
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
1141
|
+
"headerButton": undefined,
|
|
1137
1142
|
"isLogoOptional": false,
|
|
1138
1143
|
"isLogoVisible": true,
|
|
1139
1144
|
"logoImage": {
|
|
@@ -1209,6 +1214,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
|
|
|
1209
1214
|
"backgroundOpacity": 100,
|
|
1210
1215
|
},
|
|
1211
1216
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
1217
|
+
"headerButton": undefined,
|
|
1212
1218
|
"isLogoOptional": false,
|
|
1213
1219
|
"isLogoVisible": true,
|
|
1214
1220
|
"logoImage": {
|
|
@@ -141,6 +141,7 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
|
|
|
141
141
|
"backgroundOpacity": 100,
|
|
142
142
|
},
|
|
143
143
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
144
|
+
"headerButton": undefined,
|
|
144
145
|
"isLogoOptional": false,
|
|
145
146
|
"isLogoVisible": true,
|
|
146
147
|
"logoDraftToken": "",
|
|
@@ -217,6 +218,7 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
|
|
|
217
218
|
"backgroundOpacity": 100,
|
|
218
219
|
},
|
|
219
220
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
221
|
+
"headerButton": undefined,
|
|
220
222
|
"isLogoOptional": false,
|
|
221
223
|
"isLogoVisible": true,
|
|
222
224
|
"logoDraftToken": "",
|
|
@@ -140,6 +140,7 @@ BrandPageModel {
|
|
|
140
140
|
"backgroundOpacity": 100,
|
|
141
141
|
},
|
|
142
142
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
143
|
+
"headerButton": undefined,
|
|
143
144
|
"isLogoOptional": false,
|
|
144
145
|
"isLogoVisible": true,
|
|
145
146
|
"logoDraftToken": "",
|
|
@@ -216,6 +217,7 @@ BrandPageModel {
|
|
|
216
217
|
"backgroundOpacity": 100,
|
|
217
218
|
},
|
|
218
219
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
220
|
+
"headerButton": undefined,
|
|
219
221
|
"isLogoOptional": false,
|
|
220
222
|
"isLogoVisible": true,
|
|
221
223
|
"logoDraftToken": "",
|
|
@@ -601,6 +603,7 @@ BrandPageModel {
|
|
|
601
603
|
"backgroundOpacity": 100,
|
|
602
604
|
},
|
|
603
605
|
"displayStyle": "desktop-tab-vertical-stack-centered-logo-centered-menu",
|
|
606
|
+
"headerButton": undefined,
|
|
604
607
|
"isLogoOptional": false,
|
|
605
608
|
"isLogoVisible": true,
|
|
606
609
|
"logoDraftToken": "",
|
|
@@ -677,6 +680,7 @@ BrandPageModel {
|
|
|
677
680
|
"backgroundOpacity": 100,
|
|
678
681
|
},
|
|
679
682
|
"displayStyle": "mobile-tab-vertical-stack-centered-logo-centered-menu",
|
|
683
|
+
"headerButton": undefined,
|
|
680
684
|
"isLogoOptional": false,
|
|
681
685
|
"isLogoVisible": true,
|
|
682
686
|
"logoDraftToken": "",
|
|
@@ -45,6 +45,14 @@ export const getBoxShadowValue = (buttonStyles: ButtonStyles): string => {
|
|
|
45
45
|
return boxShadow;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export const BUTTON_VARIANTS = {
|
|
49
|
+
section: 'section',
|
|
50
|
+
header: 'header',
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export type ButtonVariant =
|
|
54
|
+
(typeof BUTTON_VARIANTS)[keyof typeof BUTTON_VARIANTS];
|
|
55
|
+
|
|
48
56
|
// The "decorative" button styles — brushstroke / arrow / torn-paper — whose visual
|
|
49
57
|
// is drawn by ButtonStyleWrapper rather than plain CSS. Their banner-like SVG
|
|
50
58
|
// decoration needs the button to be reasonably wide to render legibly, so narrow
|