@designcrowd/library.brand-page 6.2.8 → 6.2.10

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 (84) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Bi71mUjJ.js → CreatedOnBrandCrowd-de-de-CTEj6D-p.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-C2kl91cc.js → CreatedOnBrandCrowd-en-us-ByFNuMoA.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CNK-VpMH.js → CreatedOnBrandCrowd-es-es-lA_3mWIj.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-B_WTCv5k.js → CreatedOnBrandCrowd-fr-ca-CS_YkUz3.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-B_WTCv5k.js → CreatedOnBrandCrowd-fr-fr-CS_YkUz3.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Dk-k5-Nh.js → CreatedOnBrandCrowd-pt-br-BoduQ7zJ.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Dk-k5-Nh.js → CreatedOnBrandCrowd-pt-pt-BoduQ7zJ.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BVaJyfAR.js → CreatedOnDcom-de-de-Co1OO4_u.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DVzzynRf.js → CreatedOnDcom-en-us-DqFyqxOJ.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BS2u-QHG.js → CreatedOnDcom-es-es-DhQWyb7C.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BavFpbyC.js → CreatedOnDcom-fr-ca-DIxstLTf.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DRchqDHL.js → CreatedOnDcom-fr-fr-B-z8V1cH.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-N7XAWRj_.js → CreatedOnDcom-pt-br-B7AMR1sc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-Bct14us6.js → CreatedOnDcom-pt-pt-Cdci4yr3.js} +1 -1
  15. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +6 -5
  16. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +6 -5
  17. package/lib/src/brand-page-maker/components/color-picker/BpColorPicker.stories.d.ts +3 -0
  18. package/lib/src/brand-page-maker/components/color-picker/BpColorPicker.stories.js +10 -0
  19. package/lib/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.d.ts +2 -2
  20. package/lib/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.js +2 -2
  21. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +6 -5
  22. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +7 -5
  23. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.js +5 -0
  24. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +6 -5
  25. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +6 -5
  26. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +6 -5
  27. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +6 -5
  28. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +7 -6
  29. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +52 -5
  30. package/lib/src/{bundled-translations-BNA5h0Dx.js → bundled-translations-zfiA5Tsw.js} +1 -1
  31. package/lib/src/{bundled-translations.de-DE-fyTHrRxT.js → bundled-translations.de-DE-ChsdSyAP.js} +13 -13
  32. package/lib/src/{bundled-translations.es-ES-BMmvJGGC.js → bundled-translations.es-ES-DflsWQK0.js} +17 -17
  33. package/lib/src/{bundled-translations.fr-CA-Bo69L_BP.js → bundled-translations.fr-CA-fpDRuZ52.js} +1 -1
  34. package/lib/src/{bundled-translations.fr-FR-C4yTVFOW.js → bundled-translations.fr-FR-Bub73Zt-.js} +17 -17
  35. package/lib/src/{bundled-translations.pt-BR-CPMPfny0.js → bundled-translations.pt-BR-BlnWE9TB.js} +13 -13
  36. package/lib/src/{bundled-translations.pt-PT-C6ZwYs37.js → bundled-translations.pt-PT-DmzXlnaS.js} +30 -30
  37. package/lib/src/bundles/bundled-translations.de-DE.json +7 -1
  38. package/lib/src/bundles/bundled-translations.es-ES.json +7 -1
  39. package/lib/src/bundles/bundled-translations.fr-CA.json +7 -1
  40. package/lib/src/bundles/bundled-translations.fr-FR.json +7 -1
  41. package/lib/src/bundles/bundled-translations.json +7 -1
  42. package/lib/src/bundles/bundled-translations.pt-BR.json +7 -1
  43. package/lib/src/bundles/bundled-translations.pt-PT.json +7 -1
  44. package/lib/src/css/library.brand-page-brandCrowd.min.css +4 -4
  45. package/lib/src/css/library.brand-page-brandPage.min.css +4 -4
  46. package/lib/src/css/library.brand-page-designCom.min.css +4 -4
  47. package/lib/src/css/library.brand-page-preview.min.css +4 -4
  48. package/lib/src/index.mjs +1 -1
  49. package/lib/src/{lib-BE9sCFvu.js → lib-Y-hakSU_.js} +5456 -5414
  50. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +6 -5
  51. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +6 -5
  52. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +6 -5
  53. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +6 -5
  54. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +6 -5
  55. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +6 -5
  56. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +6 -5
  57. package/package.json +1 -1
  58. package/src/brand-page-maker/components/brandpage-settings/BrandPageSettings.test.ts +323 -0
  59. package/src/brand-page-maker/components/brandpage-settings/BrandPageSettings.vue +3 -3
  60. package/src/brand-page-maker/components/color-picker/BpColorPicker.stories.ts +10 -0
  61. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.ts +2 -2
  62. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.test.ts +116 -0
  63. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.vue +7 -1
  64. package/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.test.ts +157 -0
  65. package/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.vue +10 -4
  66. package/src/brand-page-maker/components/sensitive-content/SensitiveContent.vue +23 -3
  67. package/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.ts +5 -0
  68. package/src/brand-page-maker/components/seo-settings/SeoPageSettings.vue +2 -0
  69. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts +332 -1
  70. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +83 -5
  71. package/src/bundles/bundled-translations.de-DE.json +7 -1
  72. package/src/bundles/bundled-translations.es-ES.json +7 -1
  73. package/src/bundles/bundled-translations.fr-CA.json +7 -1
  74. package/src/bundles/bundled-translations.fr-FR.json +7 -1
  75. package/src/bundles/bundled-translations.json +7 -1
  76. package/src/bundles/bundled-translations.pt-BR.json +7 -1
  77. package/src/bundles/bundled-translations.pt-PT.json +7 -1
  78. package/src/shared/color-picker/components/i18n/color-picker.de-DE.json +8 -1
  79. package/src/shared/color-picker/components/i18n/color-picker.es-ES.json +8 -1
  80. package/src/shared/color-picker/components/i18n/color-picker.fr-CA.json +8 -1
  81. package/src/shared/color-picker/components/i18n/color-picker.fr-FR.json +8 -1
  82. package/src/shared/color-picker/components/i18n/color-picker.json +8 -1
  83. package/src/shared/color-picker/components/i18n/color-picker.pt-BR.json +8 -1
  84. package/src/shared/color-picker/components/i18n/color-picker.pt-PT.json +8 -1
@@ -10,15 +10,21 @@
10
10
  :key="index"
11
11
  :value="mode.value"
12
12
  >
13
- {{ mode.displayName }}
13
+ {{ colorPickerTr(mode.labelKey) }}
14
14
  </option>
15
15
  </select>
16
16
  </template>
17
17
 
18
18
  <script>
19
19
  import ColorModeSelectMixin from './ColorModeSelect.mixin';
20
+ import { colorPickerTr } from '@src/composables/useTranslate';
20
21
 
21
22
  export default {
22
23
  mixins: [ColorModeSelectMixin],
24
+ setup() {
25
+ return {
26
+ colorPickerTr,
27
+ };
28
+ },
23
29
  };
24
30
  </script>
@@ -0,0 +1,157 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import i18next from 'i18next';
3
+ import { describe, expect, test } from 'vitest';
4
+ import {
5
+ setLocaleAsync,
6
+ supportedLocales,
7
+ type LocaleTypes,
8
+ } from '@src/composables/useTranslate';
9
+ import GradientTypeSelector from './GradientTypeSelector.vue';
10
+ import en from '@src/shared/color-picker/components/i18n/color-picker.json';
11
+ import deDE from '@src/shared/color-picker/components/i18n/color-picker.de-DE.json';
12
+ import esES from '@src/shared/color-picker/components/i18n/color-picker.es-ES.json';
13
+ import frCA from '@src/shared/color-picker/components/i18n/color-picker.fr-CA.json';
14
+ import frFR from '@src/shared/color-picker/components/i18n/color-picker.fr-FR.json';
15
+ import ptBR from '@src/shared/color-picker/components/i18n/color-picker.pt-BR.json';
16
+ import ptPT from '@src/shared/color-picker/components/i18n/color-picker.pt-PT.json';
17
+
18
+ // Expected values come from the locale files, not literals, so Smartling's
19
+ // rewrite of the wording can't turn these tests red.
20
+ const localeFiles: Record<LocaleTypes, typeof en> = {
21
+ 'en-US': en,
22
+ 'de-DE': deDE,
23
+ 'es-ES': esES,
24
+ 'fr-CA': frCA,
25
+ 'fr-FR': frFR,
26
+ 'pt-BR': ptBR,
27
+ 'pt-PT': ptPT,
28
+ };
29
+
30
+ const stringsFor = (locale: LocaleTypes): (typeof en)['colorPicker'] =>
31
+ localeFiles[locale].colorPicker;
32
+
33
+ const NON_ENGLISH_LOCALES = supportedLocales.filter(
34
+ (locale) => locale !== 'en-US',
35
+ );
36
+
37
+ // See RecaptchaDisclosure.vue.test.ts: the shared i18next singleton only takes
38
+ // the language from `setLocaleAsync` on its first call.
39
+ const useLocale = async (locale: string): Promise<void> => {
40
+ await setLocaleAsync(locale as LocaleTypes);
41
+ await i18next.changeLanguage(locale);
42
+ };
43
+
44
+ const buttons = (
45
+ wrapper: ReturnType<typeof mount>,
46
+ ): ReturnType<ReturnType<typeof mount>['findAll']> =>
47
+ wrapper.findAll('.gradient-color-switch-button');
48
+
49
+ const mountSelector = (modelValue?: string): ReturnType<typeof mount> =>
50
+ mount(GradientTypeSelector, { props: { modelValue } });
51
+
52
+ describe('GradientTypeSelector', () => {
53
+ test.each(NON_ENGLISH_LOCALES)(
54
+ 'gradient buttons and hints use the %s locale file',
55
+ async (locale) => {
56
+ await useLocale(locale);
57
+
58
+ const wrapper = mountSelector();
59
+ const [linear, radial] = buttons(wrapper);
60
+
61
+ expect(linear.text()).toBe(stringsFor(locale).gradientTypeLinear);
62
+ expect(radial.text()).toBe(stringsFor(locale).gradientTypeRadial);
63
+ expect(linear.attributes('title')).toBe(
64
+ stringsFor(locale).gradientTypeLinearTitle,
65
+ );
66
+ expect(radial.attributes('title')).toBe(
67
+ stringsFor(locale).gradientTypeRadialTitle,
68
+ );
69
+ [linear.text(), radial.text()].forEach((text) => {
70
+ expect(text).not.toMatch(/^colorPicker\./);
71
+ });
72
+ expect(linear.classes()).toContain('gradient-color-switch-button');
73
+ expect(radial.classes()).toContain('gradient-color-switch-button');
74
+ },
75
+ );
76
+
77
+ test('English gradient buttons and hints read word for word', async () => {
78
+ await useLocale('en-US');
79
+
80
+ const wrapper = mountSelector();
81
+ const [linear, radial] = buttons(wrapper);
82
+
83
+ expect(linear.text()).toBe('Linear');
84
+ expect(radial.text()).toBe('Radial');
85
+ expect(linear.attributes('title')).toBe('Linear Gradient Color');
86
+ expect(radial.attributes('title')).toBe('Radial Gradient Color');
87
+ });
88
+
89
+ // A key missing from one locale file renders as its key path on the site;
90
+ // the shared test singleton would hide it behind the en-US fallback.
91
+ test.each(supportedLocales)(
92
+ 'every locale file defines the gradient-type keys: %s',
93
+ (locale) => {
94
+ (
95
+ [
96
+ 'gradientTypeLinear',
97
+ 'gradientTypeRadial',
98
+ 'gradientTypeLinearTitle',
99
+ 'gradientTypeRadialTitle',
100
+ ] as const
101
+ ).forEach((key) => {
102
+ const value = stringsFor(locale)[key];
103
+
104
+ expect(typeof value, `${locale} › ${key}`).toBe('string');
105
+ expect(value.trim(), `${locale} › ${key}`).not.toBe('');
106
+ });
107
+ },
108
+ );
109
+
110
+ test('clicking a gradient button emits the same value as today', async () => {
111
+ await useLocale('de-DE');
112
+
113
+ const wrapper = mountSelector('linear');
114
+ const [, radial] = buttons(wrapper);
115
+
116
+ await radial.trigger('click');
117
+
118
+ const [linear] = buttons(wrapper);
119
+
120
+ await linear.trigger('click');
121
+
122
+ expect(wrapper.emitted('update:modelValue')).toEqual([
123
+ ['radial'],
124
+ ['linear'],
125
+ ]);
126
+ });
127
+
128
+ // gradientTypeRadial is "Radial" in every locale file, so a literal
129
+ // <span>Radial</span> would also pass the assertions above. Override the
130
+ // resource at runtime to prove the label is actually looked up.
131
+ test('the radial label is looked up, not hard-coded', async () => {
132
+ await useLocale('de-DE');
133
+
134
+ const original = stringsFor('de-DE').gradientTypeRadial;
135
+
136
+ i18next.addResource(
137
+ 'de-DE',
138
+ 'bp',
139
+ 'colorPicker.gradientTypeRadial',
140
+ 'probe-radial',
141
+ );
142
+
143
+ try {
144
+ const wrapper = mountSelector();
145
+ const [, radial] = buttons(wrapper);
146
+
147
+ expect(radial.text()).toBe('probe-radial');
148
+ } finally {
149
+ i18next.addResource(
150
+ 'de-DE',
151
+ 'bp',
152
+ 'colorPicker.gradientTypeRadial',
153
+ original,
154
+ );
155
+ }
156
+ });
157
+ });
@@ -4,7 +4,7 @@
4
4
  class="tw-flex tw-border-2 tw-border-solid tw-border-secondary-500 tw-rounded"
5
5
  >
6
6
  <button
7
- title="Linear Gradient Color"
7
+ :title="colorPickerTr('gradientTypeLinearTitle')"
8
8
  class="gradient-color-switch-button"
9
9
  :class="{
10
10
  'gradient-color-switch-button_is-on': isLinear,
@@ -12,10 +12,10 @@
12
12
  }"
13
13
  @click.stop="onGradientTypeClick('linear')"
14
14
  >
15
- <span> Linear </span>
15
+ <span>{{ colorPickerTr('gradientTypeLinear') }}</span>
16
16
  </button>
17
17
  <button
18
- title="Radial Gradient Color"
18
+ :title="colorPickerTr('gradientTypeRadialTitle')"
19
19
  class="gradient-color-switch-button"
20
20
  :class="{
21
21
  'gradient-color-switch-button_is-on': isRadial,
@@ -23,16 +23,22 @@
23
23
  }"
24
24
  @click.stop="onGradientTypeClick('radial')"
25
25
  >
26
- <span> Radial </span>
26
+ <span>{{ colorPickerTr('gradientTypeRadial') }}</span>
27
27
  </button>
28
28
  </div>
29
29
  </template>
30
30
 
31
31
  <script>
32
32
  import GradientTypeSelectorMixin from './GradientTypeSelector.mixin';
33
+ import { colorPickerTr } from '@src/composables/useTranslate';
33
34
 
34
35
  export default {
35
36
  mixins: [GradientTypeSelectorMixin],
37
+ setup() {
38
+ return {
39
+ colorPickerTr,
40
+ };
41
+ },
36
42
  };
37
43
  </script>
38
44
 
@@ -40,6 +40,7 @@
40
40
  name="sensitiveContent"
41
41
  :checked="data.selectedSensitiveSettings === opt.value"
42
42
  :value="opt.value"
43
+ :disabled="isSaving || isPublishing"
43
44
  />
44
45
  <input
45
46
  hidden
@@ -68,7 +69,7 @@
68
69
  :label="commonTr('updateSettings')"
69
70
  variant="primary"
70
71
  type="submit"
71
- :disabled="isNoChange"
72
+ :disabled="isNoChange || isPublishing || isSaving"
72
73
  @click="onSensitiveContentUpdate"
73
74
  />
74
75
  </div>
@@ -97,7 +98,10 @@ import {
97
98
  SENSITIVE_CONTENT_TYPES,
98
99
  type SensitiveContent,
99
100
  } from '@brand-page/models/brand-page-context.model';
100
- import { useBrandPageMakerStore } from '@brand-page-maker/stores';
101
+ import {
102
+ useBrandPageContainerStore,
103
+ useBrandPageMakerStore,
104
+ } from '@brand-page-maker/stores';
101
105
  import { sensitiveSettingTr, commonTr } from '@src/composables/useTranslate';
102
106
 
103
107
  interface Props {
@@ -106,6 +110,7 @@ interface Props {
106
110
  designDraftToken?: string;
107
111
  }
108
112
  const brandPageMakerStore = useBrandPageMakerStore();
113
+ const brandPageContainerStore = useBrandPageContainerStore();
109
114
 
110
115
  const props = defineProps<Props>();
111
116
 
@@ -137,12 +142,27 @@ const isNoChange = computed(() => {
137
142
  return data.selectedSensitiveSettings === props.value;
138
143
  });
139
144
 
145
+ // Scoped to isMaker: the Manage page's own publish/save must never lock the maker's radios, and vice versa.
146
+ const isPublishing = computed<boolean>(
147
+ () => props.isMaker && brandPageContainerStore.isPublishing,
148
+ );
149
+ const isSaving = computed<boolean>(
150
+ () => props.isMaker && brandPageContainerStore.isSaving,
151
+ );
152
+
140
153
  const emit = defineEmits([
141
154
  'change-sensitive-settings',
142
155
  'pre-submit-edit-sensitive-content',
143
156
  ]);
144
157
 
145
- const onSensitiveSettingsClick = (value) => {
158
+ const onSensitiveSettingsClick = (value: SensitiveContent) => {
159
+ // The radio input's own `disabled` doesn't stop this handler — it's on the
160
+ // wrapping div so the whole row is clickable — so gate it here too, or a
161
+ // click would still change the choice while a save is in flight.
162
+ if (isSaving.value || isPublishing.value) {
163
+ return;
164
+ }
165
+
146
166
  data.selectedSensitiveSettings = value;
147
167
  };
148
168
 
@@ -105,12 +105,17 @@ export default defineComponent({
105
105
  isPublishing() {
106
106
  return this.brandPageContainerStore.isPublishing;
107
107
  },
108
+ // Scoped to isMaker: the Manage page's save must never gate on the maker's own in-flight state.
109
+ isSaving(): boolean {
110
+ return this.isMaker && this.brandPageContainerStore.isSaving;
111
+ },
108
112
  isUpdateButtonEnabled(): any {
109
113
  return (
110
114
  (this.myPageDescription?.length ?? 0) <=
111
115
  BRANDPAGE_DESCRIPTION_CHARACTER_LIMIT &&
112
116
  (this.myPageTitle?.length ?? 0) <= BRANDPAGE_TITLE_CHARACTER_LIMIT &&
113
117
  !this.brandPageContainerStore.isPublishing &&
118
+ !this.isSaving &&
114
119
  this.isDirty &&
115
120
  this.isInternalDescriptionValid &&
116
121
  this.isInternalTitleValid
@@ -69,6 +69,7 @@
69
69
  v-model="internalTitle"
70
70
  :label="commonTr('pageTitle')"
71
71
  :error="!!internalTitleErrorMessage"
72
+ :disabled="isSaving || isPublishing"
72
73
  />
73
74
  <p
74
75
  v-show="!isInternalTitleValid"
@@ -83,6 +84,7 @@
83
84
  :placeholder="seoSettingsTr('websiteDescription')"
84
85
  :error="!!internalDescriptionErrorMessage"
85
86
  :resize="false"
87
+ :disabled="isSaving || isPublishing"
86
88
  />
87
89
  <p
88
90
  v-show="!isInternalDescriptionValid"
@@ -1,11 +1,17 @@
1
1
  import { describe, test, expect, beforeEach, vi } from 'vitest';
2
2
  import { createPinia, setActivePinia } from 'pinia';
3
- import { useBrandPageContainerStore } from '@brand-page-maker/stores';
3
+ import {
4
+ useBrandPageContainerStore,
5
+ useBrandPageMakerStore,
6
+ } from '@brand-page-maker/stores';
7
+ import { SENSITIVE_CONTENT_TYPES } from '@brand-page';
4
8
  import brandPageClient from '../../../clients/brand-page-client';
5
9
 
6
10
  vi.mock('../../../clients/brand-page-client', () => ({
7
11
  default: {
8
12
  saveBrandPageAsync: vi.fn(),
13
+ updateBrandPageMetaAsync: vi.fn(),
14
+ updateSensitiveContentAsync: vi.fn(),
9
15
  },
10
16
  }));
11
17
 
@@ -16,6 +22,40 @@ const SAVED = {
16
22
  canPublish: true,
17
23
  };
18
24
 
25
+ // Both settings writes share `saveSiteBeforeFirstSettingsWrite`; run each
26
+ // never-saved-site scenario over both so a guard dropped from one write
27
+ // method still fails.
28
+ const settingsActions: [
29
+ string,
30
+ (store: ReturnType<typeof useBrandPageContainerStore>) => Promise<void>,
31
+ ReturnType<typeof vi.fn>,
32
+ Record<string, unknown>,
33
+ ][] = [
34
+ [
35
+ 'updateBrandPageSettings',
36
+ (store) =>
37
+ store.updateBrandPageSettings({
38
+ description: 'New description',
39
+ title: 'New title',
40
+ }),
41
+ vi.mocked(brandPageClient.updateBrandPageMetaAsync),
42
+ {
43
+ pageTitle: 'New title',
44
+ pageDescription: 'New description',
45
+ metaKeywords: [],
46
+ },
47
+ ],
48
+ [
49
+ 'updateSensitiveContent',
50
+ (store) =>
51
+ store.updateSensitiveContent({
52
+ sensitiveContent: SENSITIVE_CONTENT_TYPES.plus18,
53
+ }),
54
+ vi.mocked(brandPageClient.updateSensitiveContentAsync),
55
+ { sensitiveContentType: SENSITIVE_CONTENT_TYPES.plus18 },
56
+ ],
57
+ ];
58
+
19
59
  describe('brand-page-container-store saveBrandPage', () => {
20
60
  let store: ReturnType<typeof useBrandPageContainerStore>;
21
61
 
@@ -104,3 +144,294 @@ describe('brand-page-container-store saveBrandPage', () => {
104
144
  expect(requestBody()).toMatchObject({ shouldPublish: true });
105
145
  });
106
146
  });
147
+
148
+ describe('settings on a never-saved site', () => {
149
+ let store: ReturnType<typeof useBrandPageContainerStore>;
150
+ let brandPageMakerStore: ReturnType<typeof useBrandPageMakerStore>;
151
+
152
+ beforeEach(() => {
153
+ vi.clearAllMocks();
154
+ setActivePinia(createPinia());
155
+
156
+ store = useBrandPageContainerStore();
157
+ brandPageMakerStore = useBrandPageMakerStore();
158
+
159
+ // The export is exercised by its own tests; stub it so these cover only the
160
+ // settings-write sequencing.
161
+ vi.spyOn(store, 'exportDesignAsync').mockResolvedValue({} as any);
162
+
163
+ // Real timers inside showNextNotificationMessage would make every test wait
164
+ // out the message-visible duration and dequeue the very message these tests
165
+ // assert on, so only its own invocation is observed here.
166
+ vi.spyOn(
167
+ brandPageMakerStore,
168
+ 'showNextNotificationMessage',
169
+ ).mockResolvedValue(undefined);
170
+
171
+ vi.mocked(brandPageClient.updateBrandPageMetaAsync).mockResolvedValue({
172
+ pageTitle: 'New title',
173
+ pageDescription: 'New description',
174
+ } as any);
175
+
176
+ vi.mocked(brandPageClient.updateSensitiveContentAsync).mockResolvedValue({
177
+ sensitiveContentType: SENSITIVE_CONTENT_TYPES.plus18,
178
+ } as any);
179
+ });
180
+
181
+ describe('a saved site', () => {
182
+ beforeEach(() => {
183
+ store.designDraftToken = 'draft-123';
184
+ store.brandPageStatus = 'Published';
185
+ });
186
+
187
+ // Once a token exists — whether from an earlier save or the host's, mid-
188
+ // session — a pre-purchase site writes directly, the same as any other
189
+ // saved site.
190
+ test('saved site writes only the setting', async () => {
191
+ store.isPrePurchase = true;
192
+
193
+ const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
194
+
195
+ await store.updateSensitiveContent({
196
+ sensitiveContent: SENSITIVE_CONTENT_TYPES.plus18,
197
+ });
198
+
199
+ expect(
200
+ brandPageClient.updateSensitiveContentAsync,
201
+ ).toHaveBeenCalledOnce();
202
+ expect(brandPageClient.updateSensitiveContentAsync).toHaveBeenCalledWith(
203
+ expect.anything(),
204
+ 'draft-123',
205
+ expect.objectContaining({
206
+ sensitiveContentType: SENSITIVE_CONTENT_TYPES.plus18,
207
+ }),
208
+ );
209
+ expect(brandPageClient.saveBrandPageAsync).not.toHaveBeenCalled();
210
+ expect(dispatchSpy).not.toHaveBeenCalled();
211
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(1);
212
+ expect(brandPageMakerStore.notificationMessageQueue[0].type).toBe(
213
+ 'success',
214
+ );
215
+ });
216
+ });
217
+
218
+ describe('post-purchase, never saved', () => {
219
+ beforeEach(() => {
220
+ store.designDraftToken = undefined;
221
+ store.isPrePurchase = false;
222
+ });
223
+
224
+ test.each(settingsActions)(
225
+ 'saves the site first (%s)',
226
+ async (_name, callAction, endpoint, expectedPayload) => {
227
+ vi.mocked(brandPageClient.saveBrandPageAsync).mockResolvedValue(
228
+ SAVED as any,
229
+ );
230
+
231
+ await callAction(store);
232
+
233
+ expect(brandPageClient.saveBrandPageAsync).toHaveBeenCalledOnce();
234
+
235
+ const [, saveRequest, saveToken] = vi.mocked(
236
+ brandPageClient.saveBrandPageAsync,
237
+ ).mock.calls[0];
238
+
239
+ expect(saveRequest).toMatchObject({ saveOnly: true });
240
+ expect(saveToken).toBeFalsy();
241
+
242
+ expect(endpoint).toHaveBeenCalledOnce();
243
+ expect(endpoint).toHaveBeenCalledWith(
244
+ expect.anything(),
245
+ 'draft-123',
246
+ expect.objectContaining(expectedPayload),
247
+ );
248
+
249
+ const saveOrder = vi.mocked(brandPageClient.saveBrandPageAsync).mock
250
+ .invocationCallOrder[0];
251
+ const writeOrder = endpoint.mock.invocationCallOrder[0];
252
+
253
+ expect(saveOrder).toBeLessThan(writeOrder);
254
+
255
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(1);
256
+ expect(brandPageMakerStore.notificationMessageQueue[0].type).toBe(
257
+ 'success',
258
+ );
259
+
260
+ // The save's own banner was disarmed — a later close must not raise it.
261
+ brandPageMakerStore.notificationMessageQueue.length = 0;
262
+
263
+ await store.closePublishModal();
264
+ await store.showSaveSuccessMessage();
265
+
266
+ expect(store.triggerSaveSuccessMessage).toBe(false);
267
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(0);
268
+ },
269
+ );
270
+
271
+ test.each([
272
+ [
273
+ 'save throws',
274
+ (): void => {
275
+ vi.mocked(brandPageClient.saveBrandPageAsync).mockRejectedValue(
276
+ new Error('network'),
277
+ );
278
+ },
279
+ false,
280
+ ],
281
+ [
282
+ 'save returns nothing',
283
+ (): void => {
284
+ vi.mocked(brandPageClient.saveBrandPageAsync).mockResolvedValue(
285
+ null as any,
286
+ );
287
+ },
288
+ false,
289
+ ],
290
+ [
291
+ 'save ok, write returns nothing',
292
+ (): void => {
293
+ vi.mocked(brandPageClient.saveBrandPageAsync).mockResolvedValue(
294
+ SAVED as any,
295
+ );
296
+ vi.mocked(
297
+ brandPageClient.updateSensitiveContentAsync,
298
+ ).mockResolvedValue(null as any);
299
+ },
300
+ true,
301
+ ],
302
+ ])(
303
+ 'failed first save (%s)',
304
+ async (_case: string, setup: () => void, writeReached: boolean) => {
305
+ setup();
306
+
307
+ await store.updateSensitiveContent({
308
+ sensitiveContent: SENSITIVE_CONTENT_TYPES.plus18,
309
+ });
310
+
311
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(1);
312
+ expect(brandPageMakerStore.notificationMessageQueue[0].type).toBe(
313
+ 'error',
314
+ );
315
+ expect(
316
+ brandPageMakerStore.showNextNotificationMessage,
317
+ ).toHaveBeenCalled();
318
+ expect(store.isPublishing).toBe(false);
319
+
320
+ if (!writeReached) {
321
+ expect(
322
+ brandPageClient.updateSensitiveContentAsync,
323
+ ).not.toHaveBeenCalled();
324
+ }
325
+ },
326
+ );
327
+ });
328
+
329
+ describe('pre-purchase, never saved', () => {
330
+ beforeEach(() => {
331
+ store.isPrePurchase = true;
332
+ store.designDraftToken = undefined;
333
+ });
334
+
335
+ // No listener started a save, so isSaving never went true on the dispatch —
336
+ // the click would otherwise go silent.
337
+ test.each(settingsActions)(
338
+ 'pre-purchase asks the host to save, no listener (%s)',
339
+ async (_name, callAction) => {
340
+ const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
341
+ const setIsPublishingSpy = vi.spyOn(store, 'setIsPublishing');
342
+
343
+ await callAction(store);
344
+
345
+ expect(dispatchSpy).toHaveBeenCalledOnce();
346
+ expect((dispatchSpy.mock.calls[0][0] as Event).type).toBe(
347
+ 'SAVE_PROGRESS_CLICKED',
348
+ );
349
+ expect(brandPageClient.saveBrandPageAsync).not.toHaveBeenCalled();
350
+ expect(
351
+ brandPageClient.updateSensitiveContentAsync,
352
+ ).not.toHaveBeenCalled();
353
+ expect(brandPageClient.updateBrandPageMetaAsync).not.toHaveBeenCalled();
354
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(1);
355
+ expect(brandPageMakerStore.notificationMessageQueue[0].type).toBe(
356
+ 'error',
357
+ );
358
+ expect(setIsPublishingSpy).not.toHaveBeenCalled();
359
+ },
360
+ );
361
+
362
+ // The host's saveDesign sets isSaving synchronously on the dispatch
363
+ // (MakerLeadCaptureModalContainer.vue), before its first await.
364
+ test.each(settingsActions)(
365
+ 'pre-purchase asks the host to save, a listener starts a save (%s)',
366
+ async (_name, callAction) => {
367
+ const dispatchSpy = vi
368
+ .spyOn(window, 'dispatchEvent')
369
+ .mockImplementation((event: Event) => {
370
+ if (event.type === 'SAVE_PROGRESS_CLICKED') {
371
+ store.setIsSaving({ isSaving: true });
372
+ }
373
+
374
+ return true;
375
+ });
376
+
377
+ await callAction(store);
378
+
379
+ expect(dispatchSpy).toHaveBeenCalledOnce();
380
+ expect(brandPageClient.saveBrandPageAsync).not.toHaveBeenCalled();
381
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(0);
382
+ },
383
+ );
384
+ });
385
+
386
+ describe('admin template editor, never saved', () => {
387
+ test.each(settingsActions)(
388
+ 'admin never saves (%s)',
389
+ async (_name, callAction) => {
390
+ store.isAdminMaker = true;
391
+ store.designDraftToken = undefined;
392
+
393
+ await callAction(store);
394
+
395
+ expect(brandPageClient.saveBrandPageAsync).not.toHaveBeenCalled();
396
+ expect(
397
+ brandPageClient.updateSensitiveContentAsync,
398
+ ).not.toHaveBeenCalled();
399
+ expect(brandPageClient.updateBrandPageMetaAsync).not.toHaveBeenCalled();
400
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(0);
401
+ },
402
+ );
403
+ });
404
+
405
+ test.each([
406
+ ['a save already running', { isSaving: true }],
407
+ ['a publish already running', { isPublishing: true }],
408
+ ])(
409
+ 'another save running (%s)',
410
+ async (_case: string, flags: Record<string, boolean>) => {
411
+ store.isPrePurchase = false;
412
+ store.designDraftToken = undefined;
413
+ Object.assign(store, flags);
414
+
415
+ const isPublishingBefore = store.isPublishing;
416
+ const setIsPublishingSpy = vi.spyOn(store, 'setIsPublishing');
417
+
418
+ await store.updateSensitiveContent({
419
+ sensitiveContent: SENSITIVE_CONTENT_TYPES.plus18,
420
+ });
421
+
422
+ expect(brandPageClient.saveBrandPageAsync).not.toHaveBeenCalled();
423
+ expect(
424
+ brandPageClient.updateSensitiveContentAsync,
425
+ ).not.toHaveBeenCalled();
426
+ expect(store.isPublishing).toBe(isPublishingBefore);
427
+ expect(setIsPublishingSpy).not.toHaveBeenCalled();
428
+ expect(brandPageMakerStore.notificationMessageQueue).toHaveLength(1);
429
+ expect(brandPageMakerStore.notificationMessageQueue[0].type).toBe(
430
+ 'error',
431
+ );
432
+ expect(
433
+ brandPageMakerStore.showNextNotificationMessage,
434
+ ).toHaveBeenCalled();
435
+ },
436
+ );
437
+ });