@designcrowd/library.brand-page 5.17.25-undo-21 → 5.17.25
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-Bze5WUip.js → CreatedOnBrandCrowd-de-de-9YvB5ocm.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-Dw5uSnX8.js → CreatedOnBrandCrowd-en-us-B8OpZpMP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Cz-oPdjt.js → CreatedOnBrandCrowd-es-es-D_d6SwvE.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-zldLXw3G.js → CreatedOnBrandCrowd-fr-ca-CPqNeevd.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-zldLXw3G.js → CreatedOnBrandCrowd-fr-fr-CPqNeevd.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-64QJpEaD.js → CreatedOnBrandCrowd-pt-br-D0DpxtjY.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-64QJpEaD.js → CreatedOnBrandCrowd-pt-pt-D0DpxtjY.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-8SXOvjEH.js → CreatedOnDcom-de-de-BLKiD1pl.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-ClZBYY_t.js → CreatedOnDcom-en-us-DGNjFV5B.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-xg4ZwvEb.js → CreatedOnDcom-es-es-C2IDMZZe.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-4zo2gxc1.js → CreatedOnDcom-fr-ca-ZRxz3QKj.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-Dw9Fuy7U.js → CreatedOnDcom-fr-fr-5bE7oSdF.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-Dbl0EWcU.js → CreatedOnDcom-pt-br-WCumEx63.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-Bps73oQE.js → CreatedOnDcom-pt-pt-C8Hinnmg.js} +1 -1
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +0 -10
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +0 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +2 -23
- package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +0 -3
- package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.js +0 -11
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +2 -7
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +0 -9
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +1 -17
- package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +0 -20
- package/lib/src/brand-page-maker/composables/useEventTracker.js +0 -17
- package/lib/src/brand-page-maker/stores/ai-thread/ai-website-builder-thread-store.d.ts +1 -1
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +2 -41
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +5 -247
- package/lib/src/brand-page-maker/stores/logo-store/logo-store.d.ts +0 -1
- package/lib/src/brand-page-maker/stores/user/user-store.js +0 -1
- package/lib/src/bundled-translations-Bu_5FO26.js +173 -0
- package/lib/src/bundles/bundled-translations.json +1 -3
- package/lib/src/constants/ui.d.ts +0 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +17 -22
- package/lib/src/css/library.brand-page-brandPage.min.css +18 -22
- package/lib/src/css/library.brand-page-designCom.min.css +17 -22
- package/lib/src/css/library.brand-page-preview.min.css +17 -22
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-BgvZxeSl.js → lib-BF9ONIgx.js} +23292 -23913
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +2 -23
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.stories.js +0 -1
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +2 -23
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +0 -16
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +2 -7
- package/lib/src/shared/content-settings/components/draggable-item-list/DraggableItemList.mixin.d.ts +0 -3
- package/lib/src/shared/content-settings/components/draggable-item-list/DraggableItemList.mixin.js +2 -19
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.js +8 -18
- package/lib/src/shared/content-settings/components/radio-field/RadioField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +0 -18
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.js +0 -11
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +2 -9
- package/lib/src/shared/logo-settings/store/editable-logo-store.d.ts +1 -9
- package/lib/src/shared/logo-settings/store/editable-logo-store.js +0 -33
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -24
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -9
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +2 -23
- package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +0 -16
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +2 -23
- package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +0 -16
- package/package.json +1 -1
- package/src/brand-page/components/video/Video.vue +0 -2
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +0 -11
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +3 -2
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +0 -1
- package/src/brand-page-maker/components/cookie-collection/CookieSettings.vue +17 -18
- package/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.ts +0 -13
- package/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.vue +4 -14
- package/src/brand-page-maker/components/quick-link-bar/i18n/quick-link-bar.json +1 -3
- package/src/brand-page-maker/components/save-contact/SaveContact.vue +19 -12
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +0 -10
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +1 -20
- package/src/brand-page-maker/composables/useEventTracker.ts +0 -29
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +0 -166
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +4 -287
- package/src/brand-page-maker/stores/logo-store/logo-store.ts +0 -5
- package/src/brand-page-maker/stores/user/user-store.ts +0 -1
- package/src/bundles/bundled-translations.json +1 -3
- package/src/constants/ui.ts +0 -1
- package/src/shared/content-settings/components/content-settings/ContentSettings.stories.ts +0 -1
- package/src/shared/content-settings/components/draggable-item-list/DraggableItemList.mixin.ts +2 -19
- package/src/shared/content-settings/components/draggable-item-list/DraggableItemList.vue +1 -1
- package/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.ts +10 -26
- package/src/shared/content-settings/components/logo-selection-field/LogoSelection.vue +48 -63
- package/src/shared/content-settings/components/text-field/TextField.vue +0 -1
- package/src/shared/content-settings/components/text-image-field/TextImageField.vue +4 -33
- package/src/shared/logo-settings/components/EditLogoSettings.mixin.ts +0 -11
- package/src/shared/logo-settings/store/editable-logo-store.ts +1 -45
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +0 -16
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +0 -22
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +0 -104
- package/lib/src/brand-page-maker/stores/history/history-store.js +0 -836
- package/lib/src/bundled-translations-CWHKs3in.js +0 -173
- package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
- package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
- package/src/brand-page-maker/components/quick-link-bar/RedoButton.test.ts +0 -166
- package/src/brand-page-maker/components/quick-link-bar/RedoButton.vue +0 -66
- package/src/brand-page-maker/components/quick-link-bar/UndoButton.test.ts +0 -166
- package/src/brand-page-maker/components/quick-link-bar/UndoButton.vue +0 -66
- package/src/brand-page-maker/composables/useUndoRedoFlag.ts +0 -26
- package/src/brand-page-maker/stores/history/history-store.test.ts +0 -1246
- package/src/brand-page-maker/stores/history/history-store.ts +0 -1041
- package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
|
@@ -1,1246 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
-
import { useHistoryStore } from './history-store';
|
|
4
|
-
import { useEventTracker } from '../../composables/useEventTracker';
|
|
5
|
-
import type { BrandPageModel } from '@brand-page';
|
|
6
|
-
|
|
7
|
-
// Shared mock maker store — methods are vi.fn() so we can assert calls.
|
|
8
|
-
const mockMakerStore = {
|
|
9
|
-
brandPageData: {
|
|
10
|
-
v: 1,
|
|
11
|
-
pages: [],
|
|
12
|
-
styles: {},
|
|
13
|
-
} as unknown as BrandPageModel,
|
|
14
|
-
isBrandPageLoaded: false,
|
|
15
|
-
applyHistorySnapshot: vi.fn(async () => undefined),
|
|
16
|
-
setIsBrandPageLoaded: vi.fn(),
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const mockPageNavigationStore = {
|
|
20
|
-
activePageId: '',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
vi.mock('../brandpage-maker/brandpage-maker-store', () => ({
|
|
24
|
-
useBrandPageMakerStore: vi.fn(() => mockMakerStore),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
vi.mock('../ai-thread/ai-website-builder-thread-store', () => ({
|
|
28
|
-
useAiWebsiteBuilderThreadStore: vi.fn(() => ({
|
|
29
|
-
isProcessing: false,
|
|
30
|
-
})),
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
vi.mock('@shared/page-navigation/store/page-navigation-store', () => ({
|
|
34
|
-
usePageNavigationStore: vi.fn(() => mockPageNavigationStore),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
// Mutate the mock between captures so each capture produces a distinct
|
|
38
|
-
// snapshot. The store now drops byte-identical captures (BP-5506: prevents
|
|
39
|
-
// undo/redo steps that do nothing visible), so tests that previously relied
|
|
40
|
-
// on capture-without-mutation lining up entries need to bump a field first.
|
|
41
|
-
let mutationCounter = 0;
|
|
42
|
-
|
|
43
|
-
function mutateMock(): void {
|
|
44
|
-
mutationCounter += 1;
|
|
45
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
-
(mockMakerStore.brandPageData as any).v = 1000 + mutationCounter;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function captureMutated(
|
|
50
|
-
store: ReturnType<typeof useHistoryStore>,
|
|
51
|
-
args: Parameters<typeof store.capture>[0] = {
|
|
52
|
-
source: 'manual_edit',
|
|
53
|
-
actor: 'user',
|
|
54
|
-
},
|
|
55
|
-
) {
|
|
56
|
-
mutateMock();
|
|
57
|
-
|
|
58
|
-
return store.capture(args);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe('history-store (single stack + JSON-string snapshots)', () => {
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
setActivePinia(createPinia());
|
|
64
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
65
|
-
mutationCounter = 0;
|
|
66
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
-
(mockMakerStore.brandPageData as any).v = 1;
|
|
68
|
-
mockPageNavigationStore.activePageId = '';
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('captures baseline and initialises the pointer to 0', async () => {
|
|
72
|
-
const store = useHistoryStore();
|
|
73
|
-
|
|
74
|
-
expect(store.entries.length).toBe(0);
|
|
75
|
-
expect(store.currentIndex).toBe(-1);
|
|
76
|
-
expect(store.baselineCaptured).toBe(false);
|
|
77
|
-
|
|
78
|
-
await store.captureBaseline();
|
|
79
|
-
|
|
80
|
-
expect(store.baselineCaptured).toBe(true);
|
|
81
|
-
expect(store.entries.length).toBe(1);
|
|
82
|
-
expect(store.entries[0].label).toBe('baseline');
|
|
83
|
-
expect(store.currentIndex).toBe(0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('stores snapshots as JSON strings, not objects', async () => {
|
|
87
|
-
const store = useHistoryStore();
|
|
88
|
-
|
|
89
|
-
await store.captureBaseline();
|
|
90
|
-
|
|
91
|
-
const entry = store.entries[0];
|
|
92
|
-
|
|
93
|
-
expect(typeof entry.snapshot).toBe('string');
|
|
94
|
-
// Round-trips to the same model via JSON.parse.
|
|
95
|
-
expect(JSON.parse(entry.snapshot)).toMatchObject(
|
|
96
|
-
mockMakerStore.brandPageData,
|
|
97
|
-
);
|
|
98
|
-
// sizeBytes matches string length
|
|
99
|
-
expect(entry.sizeBytes).toBe(entry.snapshot.length);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('does not capture baseline twice', async () => {
|
|
103
|
-
const store = useHistoryStore();
|
|
104
|
-
|
|
105
|
-
await store.captureBaseline();
|
|
106
|
-
const firstLength = store.entries.length;
|
|
107
|
-
|
|
108
|
-
await store.captureBaseline();
|
|
109
|
-
|
|
110
|
-
expect(store.entries.length).toBe(firstLength);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('capture pushes a new entry and advances the pointer', async () => {
|
|
114
|
-
const store = useHistoryStore();
|
|
115
|
-
|
|
116
|
-
await store.captureBaseline();
|
|
117
|
-
captureMutated(store);
|
|
118
|
-
|
|
119
|
-
expect(store.entries.length).toBe(2);
|
|
120
|
-
expect(store.currentIndex).toBe(1);
|
|
121
|
-
|
|
122
|
-
captureMutated(store);
|
|
123
|
-
|
|
124
|
-
expect(store.entries.length).toBe(3);
|
|
125
|
-
expect(store.currentIndex).toBe(2);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('capture truncates any redo tail (FR-11)', async () => {
|
|
129
|
-
const store = useHistoryStore();
|
|
130
|
-
|
|
131
|
-
await store.captureBaseline();
|
|
132
|
-
captureMutated(store); // index 1
|
|
133
|
-
captureMutated(store); // index 2
|
|
134
|
-
await store.undo(); // currentIndex = 1, entries still length 3
|
|
135
|
-
await store.undo(); // currentIndex = 0, entries still length 3
|
|
136
|
-
|
|
137
|
-
expect(store.entries.length).toBe(3);
|
|
138
|
-
expect(store.canRedo).toBe(true);
|
|
139
|
-
|
|
140
|
-
// New capture should truncate entries[1..] and replace with the new one.
|
|
141
|
-
captureMutated(store);
|
|
142
|
-
|
|
143
|
-
expect(store.entries.length).toBe(2); // baseline + new
|
|
144
|
-
expect(store.currentIndex).toBe(1);
|
|
145
|
-
expect(store.canRedo).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('drops byte-identical captures so undo/redo never replays a no-op', async () => {
|
|
149
|
-
const store = useHistoryStore();
|
|
150
|
-
|
|
151
|
-
await store.captureBaseline();
|
|
152
|
-
|
|
153
|
-
// First capture mutates and lands.
|
|
154
|
-
const r1 = captureMutated(store);
|
|
155
|
-
|
|
156
|
-
expect(r1).toBe('committed');
|
|
157
|
-
expect(store.entries.length).toBe(2);
|
|
158
|
-
|
|
159
|
-
// Second capture without a mutation should be detected as a no-op
|
|
160
|
-
// and skipped — the stack stays at length 2.
|
|
161
|
-
const r2 = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
162
|
-
|
|
163
|
-
expect(r2).toBe('noop');
|
|
164
|
-
expect(store.entries.length).toBe(2);
|
|
165
|
-
expect(store.currentIndex).toBe(1);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// BP-5506 phantom-undo dedup: opening a content settings panel can
|
|
169
|
-
// mutate `brandPageData` in ways the user can't see (RTE empty-paragraph
|
|
170
|
-
// init, lazy textStyle hydration, isModifiedAfterTemplateApplied flip).
|
|
171
|
-
// The dedup gate normalizes these out so capture returns 'noop'.
|
|
172
|
-
describe('phantom-edit dedup (sidebar-open cascades)', () => {
|
|
173
|
-
it('treats RTE empty-paragraph init as a no-op', async () => {
|
|
174
|
-
const store = useHistoryStore();
|
|
175
|
-
|
|
176
|
-
// Baseline has description = "" (cold start).
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
179
|
-
{
|
|
180
|
-
contents: [{ type: 'contact-form', description: '' }],
|
|
181
|
-
},
|
|
182
|
-
];
|
|
183
|
-
await store.captureBaseline();
|
|
184
|
-
|
|
185
|
-
// Sidebar open writes back TipTap's empty-paragraph default.
|
|
186
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
-
(mockMakerStore.brandPageData as any).pages[0].contents[0].description =
|
|
188
|
-
'<p style="text-align: center;"></p>';
|
|
189
|
-
|
|
190
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
191
|
-
|
|
192
|
-
expect(result).toBe('noop');
|
|
193
|
-
expect(store.entries.length).toBe(1);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('treats lazy textStyle hydration of every resolver-written key as a no-op', async () => {
|
|
197
|
-
const store = useHistoryStore();
|
|
198
|
-
|
|
199
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
201
|
-
{
|
|
202
|
-
contents: [
|
|
203
|
-
{
|
|
204
|
-
type: 'contact-form',
|
|
205
|
-
titleStyle: { textStyleType: 'titles' },
|
|
206
|
-
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
},
|
|
210
|
-
];
|
|
211
|
-
await store.captureBaseline();
|
|
212
|
-
|
|
213
|
-
// Settings panel — or canvas section selection — resolves
|
|
214
|
-
// textStyleType into the full set of resolver-written fields. The
|
|
215
|
-
// dedup keeps `textStyleType` (the authoring intent) and drops
|
|
216
|
-
// every resolved key, regardless of which exact keys it was.
|
|
217
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
218
|
-
const cf = (mockMakerStore.brandPageData as any).pages[0].contents[0];
|
|
219
|
-
|
|
220
|
-
cf.titleStyle.fontSize = '2.25em';
|
|
221
|
-
cf.titleStyle.fontFamily = 'Gloock';
|
|
222
|
-
cf.titleStyle.fontWeight = 'bold';
|
|
223
|
-
cf.titleStyle.fontStyle = 'italic';
|
|
224
|
-
cf.titleStyle.letterSpacing = '0';
|
|
225
|
-
cf.titleStyle.lineHeight = '1.3';
|
|
226
|
-
cf.titleStyle.color = 'rgb(255, 253, 233)';
|
|
227
|
-
cf.titleStyle.textDecoration = 'underline';
|
|
228
|
-
cf.descriptionStyle.fontSize = '1em';
|
|
229
|
-
cf.descriptionStyle.fontFamily = 'Lato';
|
|
230
|
-
cf.descriptionStyle.fontWeight = 'normal';
|
|
231
|
-
cf.descriptionStyle.letterSpacing = '0';
|
|
232
|
-
cf.descriptionStyle.lineHeight = '1.5';
|
|
233
|
-
cf.descriptionStyle.color = 'rgb(0, 0, 0)';
|
|
234
|
-
|
|
235
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
236
|
-
|
|
237
|
-
expect(result).toBe('noop');
|
|
238
|
-
expect(store.entries.length).toBe(1);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('preserves textAlign on a hydrated *Style as user authoring intent', async () => {
|
|
242
|
-
// textAlign is the user's per-instance alignment override — the
|
|
243
|
-
// resolver carries it through unchanged, so it must NOT be dropped.
|
|
244
|
-
const store = useHistoryStore();
|
|
245
|
-
|
|
246
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
248
|
-
{
|
|
249
|
-
contents: [
|
|
250
|
-
{
|
|
251
|
-
type: 'banner',
|
|
252
|
-
titleStyle: { textStyleType: 'titles', textAlign: 'left' },
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
];
|
|
257
|
-
await store.captureBaseline();
|
|
258
|
-
|
|
259
|
-
// User changes alignment from left to center — that's a real edit
|
|
260
|
-
// even though every other field is a hydration shape.
|
|
261
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
|
-
const banner = (mockMakerStore.brandPageData as any).pages[0].contents[0];
|
|
263
|
-
|
|
264
|
-
banner.titleStyle.textAlign = 'center';
|
|
265
|
-
banner.titleStyle.fontSize = '2.25em';
|
|
266
|
-
banner.titleStyle.fontFamily = 'Gloock';
|
|
267
|
-
banner.titleStyle.lineHeight = '1.3';
|
|
268
|
-
|
|
269
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
270
|
-
|
|
271
|
-
expect(result).toBe('committed');
|
|
272
|
-
expect(store.entries.length).toBe(2);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('still commits per-content style overrides (no textStyleType)', async () => {
|
|
276
|
-
// When the user edits a style via the inline RTE toolbar,
|
|
277
|
-
// `useRichTextEditor.ts` clears `textStyleType` (sets it to undefined)
|
|
278
|
-
// and writes the full set of fields directly to the per-content
|
|
279
|
-
// *Style. Those fields ARE the user's authoring intent, so they
|
|
280
|
-
// must be preserved by the dedup.
|
|
281
|
-
const store = useHistoryStore();
|
|
282
|
-
|
|
283
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
284
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
285
|
-
{
|
|
286
|
-
contents: [
|
|
287
|
-
{
|
|
288
|
-
type: 'banner',
|
|
289
|
-
titleStyle: { fontSize: '2.25em', fontFamily: 'Gloock' },
|
|
290
|
-
},
|
|
291
|
-
],
|
|
292
|
-
},
|
|
293
|
-
];
|
|
294
|
-
await store.captureBaseline();
|
|
295
|
-
|
|
296
|
-
// User bumps the font size — no textStyleType in the snapshot, so
|
|
297
|
-
// the field is treated as a real edit.
|
|
298
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
299
|
-
(
|
|
300
|
-
mockMakerStore.brandPageData as any
|
|
301
|
-
).pages[0].contents[0].titleStyle.fontSize = '5em';
|
|
302
|
-
|
|
303
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
304
|
-
|
|
305
|
-
expect(result).toBe('committed');
|
|
306
|
-
expect(store.entries.length).toBe(2);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// BP-5506 follow-up: editing logo text via the Logo sidebar mutates
|
|
310
|
-
// `editableLogoStore.logoEdits` (not in brandPageData), then re-renders
|
|
311
|
-
// the result into `logoImageBase64Url` on every header/footer +
|
|
312
|
-
// per-page logo content. If history captured that base64, undo would
|
|
313
|
-
// revert the canvas without touching the sidebar input — the user
|
|
314
|
-
// sees the canvas snap back but the sidebar still shows their typed
|
|
315
|
-
// value. V1 excludes logo property edits from undo, so dedup must
|
|
316
|
-
// collapse base64-only diffs.
|
|
317
|
-
it('treats isolated logoImageBase64Url changes as a no-op', async () => {
|
|
318
|
-
const store = useHistoryStore();
|
|
319
|
-
|
|
320
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
-
(mockMakerStore.brandPageData as any).pageHeaderDesktop = {
|
|
322
|
-
logoImageBase64Url: 'data:image/png;base64,AAAA',
|
|
323
|
-
};
|
|
324
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
325
|
-
(mockMakerStore.brandPageData as any).pageHeaderMobile = {
|
|
326
|
-
logoImageBase64Url: 'data:image/png;base64,AAAA',
|
|
327
|
-
};
|
|
328
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
329
|
-
(mockMakerStore.brandPageData as any).pageFooterDesktop = {
|
|
330
|
-
logoImageBase64Url: 'data:image/png;base64,AAAA',
|
|
331
|
-
};
|
|
332
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
333
|
-
(mockMakerStore.brandPageData as any).pageFooterMobile = {
|
|
334
|
-
logoImageBase64Url: 'data:image/png;base64,AAAA',
|
|
335
|
-
};
|
|
336
|
-
await store.captureBaseline();
|
|
337
|
-
|
|
338
|
-
// Logo text/font/color edit re-renders all four base64 fields.
|
|
339
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
340
|
-
const data = mockMakerStore.brandPageData as any;
|
|
341
|
-
|
|
342
|
-
data.pageHeaderDesktop.logoImageBase64Url = 'data:image/png;base64,BBBB';
|
|
343
|
-
data.pageHeaderMobile.logoImageBase64Url = 'data:image/png;base64,BBBB';
|
|
344
|
-
data.pageFooterDesktop.logoImageBase64Url = 'data:image/png;base64,BBBB';
|
|
345
|
-
data.pageFooterMobile.logoImageBase64Url = 'data:image/png;base64,BBBB';
|
|
346
|
-
|
|
347
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
348
|
-
|
|
349
|
-
expect(result).toBe('noop');
|
|
350
|
-
expect(store.entries.length).toBe(1);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// BYO-upload undo MUST still work — that diff is in
|
|
354
|
-
// `logoImage.logoDraftToken`, not `logoImageBase64Url`. Stripping the
|
|
355
|
-
// base64 must not collapse a draft-token change.
|
|
356
|
-
it('still commits when logoImage.logoDraftToken changes (BYO upload)', async () => {
|
|
357
|
-
const store = useHistoryStore();
|
|
358
|
-
|
|
359
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
360
|
-
(mockMakerStore.brandPageData as any).pageHeaderDesktop = {
|
|
361
|
-
logoImage: { logoDraftToken: 'token-original', logoVariation: 0 },
|
|
362
|
-
logoImageBase64Url: 'data:image/png;base64,AAAA',
|
|
363
|
-
};
|
|
364
|
-
await store.captureBaseline();
|
|
365
|
-
|
|
366
|
-
// BYO upload swaps both the draft token AND the base64.
|
|
367
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
368
|
-
const header = (mockMakerStore.brandPageData as any).pageHeaderDesktop;
|
|
369
|
-
|
|
370
|
-
header.logoImage.logoDraftToken = 'token-uploaded';
|
|
371
|
-
header.logoImageBase64Url = 'data:image/png;base64,BBBB';
|
|
372
|
-
|
|
373
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
374
|
-
|
|
375
|
-
expect(result).toBe('committed');
|
|
376
|
-
expect(store.entries.length).toBe(2);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('treats isModifiedAfterTemplateApplied flip as a no-op', async () => {
|
|
380
|
-
const store = useHistoryStore();
|
|
381
|
-
|
|
382
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
383
|
-
(mockMakerStore.brandPageData as any).isModifiedAfterTemplateApplied =
|
|
384
|
-
false;
|
|
385
|
-
await store.captureBaseline();
|
|
386
|
-
|
|
387
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
388
|
-
(mockMakerStore.brandPageData as any).isModifiedAfterTemplateApplied =
|
|
389
|
-
true;
|
|
390
|
-
|
|
391
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
392
|
-
|
|
393
|
-
expect(result).toBe('noop');
|
|
394
|
-
expect(store.entries.length).toBe(1);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('treats RTE empty-paragraph init as a no-op when the prior key was absent', async () => {
|
|
398
|
-
const store = useHistoryStore();
|
|
399
|
-
|
|
400
|
-
// Baseline content has NO `description` key at all (the cold-start
|
|
401
|
-
// shape for sections like testimonial-section before any editor
|
|
402
|
-
// mounts). JSON.stringify drops `undefined`, so the captured snapshot
|
|
403
|
-
// simply lacks the key.
|
|
404
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
405
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
406
|
-
{
|
|
407
|
-
contents: [{ type: 'testimonial-section', title: 'A' }],
|
|
408
|
-
},
|
|
409
|
-
];
|
|
410
|
-
await store.captureBaseline();
|
|
411
|
-
|
|
412
|
-
// Sidebar open writes back TipTap's empty-paragraph default, creating
|
|
413
|
-
// the key that was previously absent.
|
|
414
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
415
|
-
(mockMakerStore.brandPageData as any).pages[0].contents[0].description =
|
|
416
|
-
'<p style="text-align: center;"></p>';
|
|
417
|
-
|
|
418
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
419
|
-
|
|
420
|
-
expect(result).toBe('noop');
|
|
421
|
-
expect(store.entries.length).toBe(1);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('treats RTE default-hydration wrapper around plain text as a no-op', async () => {
|
|
425
|
-
const store = useHistoryStore();
|
|
426
|
-
|
|
427
|
-
// Cold-start: titles/descriptions are plain strings (no <p> wrapper).
|
|
428
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
429
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
430
|
-
{
|
|
431
|
-
contents: [
|
|
432
|
-
{
|
|
433
|
-
type: 'banner',
|
|
434
|
-
title: 'Makeup Artist ✦ Eleanor Reeves',
|
|
435
|
-
description: 'Creating Flawless Looks',
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
type: 'about-section',
|
|
439
|
-
title: 'Passionate About Makeup',
|
|
440
|
-
paragraph:
|
|
441
|
-
'Makeup is my art form, and I believe that everyone deserves to feel confident.',
|
|
442
|
-
},
|
|
443
|
-
],
|
|
444
|
-
},
|
|
445
|
-
];
|
|
446
|
-
await store.captureBaseline();
|
|
447
|
-
|
|
448
|
-
// Opening any content sidebar mounts every RTE editor on the page; each
|
|
449
|
-
// one writes back its plain-text input wrapped in TipTap's default
|
|
450
|
-
// <p>/<span>/<mark> hydration HTML. Same visible text, no real edit.
|
|
451
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
452
|
-
const banner = (mockMakerStore.brandPageData as any).pages[0].contents[0];
|
|
453
|
-
|
|
454
|
-
banner.title =
|
|
455
|
-
'<p style="text-align: center;"><span style="font-family: Belleza; font-size: 1.5625em; letter-spacing: 0.02em; line-height: 1.5; color: rgb(209, 128, 47);"><mark data-color="#fffde9" style="background-color: rgb(255, 253, 233); color: inherit;">Makeup Artist ✦ Eleanor Reeves</mark></span></p>';
|
|
456
|
-
banner.description =
|
|
457
|
-
'<p style="text-align: center;"><span style="font-family: Gloock; font-size: 4.6875em; letter-spacing: 0.05em; line-height: 1.4; color: rgb(209, 128, 47);"><mark data-color="#fffde9" style="background-color: rgb(255, 253, 233); color: inherit;">Creating Flawless Looks</mark></span></p>';
|
|
458
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
459
|
-
const about = (mockMakerStore.brandPageData as any).pages[0].contents[1];
|
|
460
|
-
|
|
461
|
-
about.title =
|
|
462
|
-
'<p style="text-align: left;"><span style="font-family: Gloock; font-size: 3.125em; letter-spacing: 0.05em; line-height: 1.3; color: rgb(255, 253, 233);">Passionate About Makeup</span></p>';
|
|
463
|
-
about.paragraph =
|
|
464
|
-
'<p style="text-align: left;"><span style="font-family: Belleza; font-size: 1.375em; letter-spacing: 0px; line-height: 1.5; color: rgb(255, 253, 233);">Makeup is my art form, and I believe that everyone deserves to feel confident.</span></p>';
|
|
465
|
-
|
|
466
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
467
|
-
|
|
468
|
-
expect(result).toBe('noop');
|
|
469
|
-
expect(store.entries.length).toBe(1);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('treats bare <p>plain text</p> hydration as a no-op', async () => {
|
|
473
|
-
const store = useHistoryStore();
|
|
474
|
-
|
|
475
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
476
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
477
|
-
{
|
|
478
|
-
contents: [
|
|
479
|
-
{
|
|
480
|
-
type: 'contact-form',
|
|
481
|
-
successMessage:
|
|
482
|
-
"Thank you! We'll get in touch with you as soon as possible.",
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
},
|
|
486
|
-
];
|
|
487
|
-
await store.captureBaseline();
|
|
488
|
-
|
|
489
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
490
|
-
(
|
|
491
|
-
mockMakerStore.brandPageData as any
|
|
492
|
-
).pages[0].contents[0].successMessage =
|
|
493
|
-
"<p>Thank you! We'll get in touch with you as soon as possible.</p>";
|
|
494
|
-
|
|
495
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
496
|
-
|
|
497
|
-
expect(result).toBe('noop');
|
|
498
|
-
expect(store.entries.length).toBe(1);
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it('still commits when RTE content has partial-text inline formatting', async () => {
|
|
502
|
-
// Partial bold (`Hello <strong>world</strong>`) is a real edit — only
|
|
503
|
-
// *part* of the text is bold, which can't be expressed via the global
|
|
504
|
-
// textStyle. The dedup must NOT collapse this.
|
|
505
|
-
const store = useHistoryStore();
|
|
506
|
-
|
|
507
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
508
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
509
|
-
{
|
|
510
|
-
contents: [
|
|
511
|
-
{
|
|
512
|
-
type: 'banner',
|
|
513
|
-
title: 'Hello world',
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
},
|
|
517
|
-
];
|
|
518
|
-
await store.captureBaseline();
|
|
519
|
-
|
|
520
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
521
|
-
(mockMakerStore.brandPageData as any).pages[0].contents[0].title =
|
|
522
|
-
'<p>Hello <strong>world</strong></p>';
|
|
523
|
-
|
|
524
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
525
|
-
|
|
526
|
-
expect(result).toBe('committed');
|
|
527
|
-
expect(store.entries.length).toBe(2);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// BP-5506 follow-up: TipTap re-emits a whole-content <strong>/<em>/<u>/<s>
|
|
531
|
-
// wrapper on first hydration to mirror the textStyle's fontWeight /
|
|
532
|
-
// fontStyle / textDecoration. The whole-content case is hydration noise
|
|
533
|
-
// (textStyle already encodes it), so it must dedup. The earlier RTE
|
|
534
|
-
// canonicalizer rejected anything containing `<` or `>` inside the inner
|
|
535
|
-
// text, so this case slipped through and produced phantom undo entries
|
|
536
|
-
// when the user clicked any content section that had bolded text.
|
|
537
|
-
it('treats whole-content <strong>/<em>/<u>/<s> hydration wrappers as a no-op', async () => {
|
|
538
|
-
const store = useHistoryStore();
|
|
539
|
-
|
|
540
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
541
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
542
|
-
{
|
|
543
|
-
contents: [
|
|
544
|
-
{
|
|
545
|
-
type: 'shapes-section',
|
|
546
|
-
textItems: [
|
|
547
|
-
{ text: 'This test is for text' },
|
|
548
|
-
{ text: 'Italic line' },
|
|
549
|
-
{ text: 'Underline line' },
|
|
550
|
-
{ text: 'Strikethrough line' },
|
|
551
|
-
{ text: 'Bold italic combo' },
|
|
552
|
-
],
|
|
553
|
-
},
|
|
554
|
-
],
|
|
555
|
-
},
|
|
556
|
-
];
|
|
557
|
-
await store.captureBaseline();
|
|
558
|
-
|
|
559
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
560
|
-
const items = (mockMakerStore.brandPageData as any).pages[0].contents[0]
|
|
561
|
-
.textItems;
|
|
562
|
-
|
|
563
|
-
// Each item gets hydrated to its TipTap default-shape with a
|
|
564
|
-
// whole-content format tag mirroring the textStyle.
|
|
565
|
-
items[0].text =
|
|
566
|
-
'<p style="text-align: left;"><span style="font-family: Lemonada; font-size: 1.875em;"><strong>This test is for text</strong></span></p>';
|
|
567
|
-
items[1].text =
|
|
568
|
-
'<p><span style="font-family: Lato;"><em>Italic line</em></span></p>';
|
|
569
|
-
items[2].text = '<p><u>Underline line</u></p>';
|
|
570
|
-
items[3].text = '<p><s>Strikethrough line</s></p>';
|
|
571
|
-
items[4].text = '<p><em><strong>Bold italic combo</strong></em></p>';
|
|
572
|
-
|
|
573
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
574
|
-
|
|
575
|
-
expect(result).toBe('noop');
|
|
576
|
-
expect(store.entries.length).toBe(1);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// Anything beyond the hydration whitelist must NOT be collapsed.
|
|
580
|
-
// <a> is the canonical case — a TipTap-rendered link is real authoring
|
|
581
|
-
// intent and must survive dedup.
|
|
582
|
-
it('still commits when RTE content adds a link (real authoring intent)', async () => {
|
|
583
|
-
const store = useHistoryStore();
|
|
584
|
-
|
|
585
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
586
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
587
|
-
{
|
|
588
|
-
contents: [{ type: 'banner', title: 'click here' }],
|
|
589
|
-
},
|
|
590
|
-
];
|
|
591
|
-
await store.captureBaseline();
|
|
592
|
-
|
|
593
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
594
|
-
(mockMakerStore.brandPageData as any).pages[0].contents[0].title =
|
|
595
|
-
'<p>click <a href="https://example.com">here</a></p>';
|
|
596
|
-
|
|
597
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
598
|
-
|
|
599
|
-
expect(result).toBe('committed');
|
|
600
|
-
expect(store.entries.length).toBe(2);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// A <span> carrying a style property outside the hydration whitelist
|
|
604
|
-
// (e.g. `text-shadow`, `text-transform`) is a real edit. TipTap's
|
|
605
|
-
// hydration span only emits `font-family / font-size / letter-spacing /
|
|
606
|
-
// line-height / color / background-color`.
|
|
607
|
-
it('still commits when RTE content adds a non-hydration style property', async () => {
|
|
608
|
-
const store = useHistoryStore();
|
|
609
|
-
|
|
610
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
611
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
612
|
-
{
|
|
613
|
-
contents: [{ type: 'banner', title: 'shadowed' }],
|
|
614
|
-
},
|
|
615
|
-
];
|
|
616
|
-
await store.captureBaseline();
|
|
617
|
-
|
|
618
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
619
|
-
(mockMakerStore.brandPageData as any).pages[0].contents[0].title =
|
|
620
|
-
'<p><span style="text-shadow: 1px 1px 2px black;">shadowed</span></p>';
|
|
621
|
-
|
|
622
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
623
|
-
|
|
624
|
-
expect(result).toBe('committed');
|
|
625
|
-
expect(store.entries.length).toBe(2);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it('treats canvas section-click cascade as a no-op (BP-5506 repro)', async () => {
|
|
629
|
-
// Captured live from UAT: clicking any section on the canvas mounts
|
|
630
|
-
// every RTE editor on the page at once + resolves textStyleType for
|
|
631
|
-
// the section's style objects + flips isModifiedAfterTemplateApplied.
|
|
632
|
-
// None of that is a user-visible edit, so dedup must collapse it.
|
|
633
|
-
const store = useHistoryStore();
|
|
634
|
-
|
|
635
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
636
|
-
(mockMakerStore.brandPageData as any).isModifiedAfterTemplateApplied =
|
|
637
|
-
false;
|
|
638
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
639
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
640
|
-
{
|
|
641
|
-
contents: [
|
|
642
|
-
{
|
|
643
|
-
type: 'banner',
|
|
644
|
-
title: 'Makeup Artist ✦ Eleanor Reeves',
|
|
645
|
-
description: 'Creating Flawless Looks',
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
type: 'text-media-section',
|
|
649
|
-
title: 'My Specialties',
|
|
650
|
-
description: '',
|
|
651
|
-
descriptionStyle: { textStyleType: 'paragraphs' },
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
},
|
|
655
|
-
];
|
|
656
|
-
await store.captureBaseline();
|
|
657
|
-
|
|
658
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
659
|
-
const data = mockMakerStore.brandPageData as any;
|
|
660
|
-
const banner = data.pages[0].contents[0];
|
|
661
|
-
const tms = data.pages[0].contents[1];
|
|
662
|
-
|
|
663
|
-
banner.title =
|
|
664
|
-
'<p style="text-align: center;"><span style="font-family: Belleza;"><mark data-color="#fffde9">Makeup Artist ✦ Eleanor Reeves</mark></span></p>';
|
|
665
|
-
banner.description =
|
|
666
|
-
'<p style="text-align: center;"><span style="font-family: Gloock;"><mark>Creating Flawless Looks</mark></span></p>';
|
|
667
|
-
tms.title =
|
|
668
|
-
'<p style="text-align: center;"><span style="font-family: Gloock;">My Specialties</span></p>';
|
|
669
|
-
tms.description = '<p style="text-align: center;"></p>';
|
|
670
|
-
tms.descriptionStyle.fontSize = '1em';
|
|
671
|
-
tms.descriptionStyle.fontFamily = 'Lato';
|
|
672
|
-
tms.descriptionStyle.lineHeight = '1.5';
|
|
673
|
-
tms.descriptionStyle.letterSpacing = '0';
|
|
674
|
-
data.isModifiedAfterTemplateApplied = true;
|
|
675
|
-
|
|
676
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
677
|
-
|
|
678
|
-
expect(result).toBe('noop');
|
|
679
|
-
expect(store.entries.length).toBe(1);
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
it('still commits when a real edit accompanies a phantom field', async () => {
|
|
683
|
-
const store = useHistoryStore();
|
|
684
|
-
|
|
685
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
686
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
687
|
-
{
|
|
688
|
-
contents: [
|
|
689
|
-
{
|
|
690
|
-
type: 'contact-form',
|
|
691
|
-
buttonText: 'Send',
|
|
692
|
-
description: '',
|
|
693
|
-
},
|
|
694
|
-
],
|
|
695
|
-
},
|
|
696
|
-
];
|
|
697
|
-
await store.captureBaseline();
|
|
698
|
-
|
|
699
|
-
// Real edit + phantom together — must commit.
|
|
700
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
701
|
-
const cf = (mockMakerStore.brandPageData as any).pages[0].contents[0];
|
|
702
|
-
|
|
703
|
-
cf.buttonText = 'Submit';
|
|
704
|
-
cf.description = '<p style="text-align: center;"></p>';
|
|
705
|
-
|
|
706
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
707
|
-
|
|
708
|
-
expect(result).toBe('committed');
|
|
709
|
-
expect(store.entries.length).toBe(2);
|
|
710
|
-
});
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
it('reentrancy guard: capture no-ops when isApplyingSnapshot is true', async () => {
|
|
714
|
-
const store = useHistoryStore();
|
|
715
|
-
|
|
716
|
-
await store.captureBaseline();
|
|
717
|
-
const initialLength = store.entries.length;
|
|
718
|
-
|
|
719
|
-
store.isApplyingSnapshot = true;
|
|
720
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
721
|
-
|
|
722
|
-
expect(store.entries.length).toBe(initialLength);
|
|
723
|
-
store.isApplyingSnapshot = false;
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it('canUndo requires currentIndex > 0', async () => {
|
|
727
|
-
const store = useHistoryStore();
|
|
728
|
-
|
|
729
|
-
expect(store.canUndo).toBe(false);
|
|
730
|
-
|
|
731
|
-
await store.captureBaseline();
|
|
732
|
-
expect(store.canUndo).toBe(false); // only baseline
|
|
733
|
-
|
|
734
|
-
captureMutated(store);
|
|
735
|
-
expect(store.canUndo).toBe(true);
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
it('canRedo requires currentIndex < entries.length - 1', async () => {
|
|
739
|
-
const store = useHistoryStore();
|
|
740
|
-
|
|
741
|
-
expect(store.canRedo).toBe(false);
|
|
742
|
-
|
|
743
|
-
await store.captureBaseline();
|
|
744
|
-
captureMutated(store);
|
|
745
|
-
expect(store.canRedo).toBe(false);
|
|
746
|
-
|
|
747
|
-
await store.undo();
|
|
748
|
-
expect(store.canRedo).toBe(true);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it('undo decrements pointer, keeps stack intact, calls applyHistorySnapshot', async () => {
|
|
752
|
-
const store = useHistoryStore();
|
|
753
|
-
|
|
754
|
-
await store.captureBaseline();
|
|
755
|
-
const baselineBrandPageData = JSON.parse(store.entries[0].snapshot);
|
|
756
|
-
|
|
757
|
-
captureMutated(store);
|
|
758
|
-
|
|
759
|
-
expect(store.currentIndex).toBe(1);
|
|
760
|
-
expect(store.entries.length).toBe(2);
|
|
761
|
-
|
|
762
|
-
await store.undo();
|
|
763
|
-
|
|
764
|
-
expect(store.currentIndex).toBe(0);
|
|
765
|
-
expect(store.entries.length).toBe(2); // stack is unchanged, pointer moved
|
|
766
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledTimes(1);
|
|
767
|
-
// Undo restores the baseline snapshot — the parsed BrandPageModel must
|
|
768
|
-
// round-trip back through applyHistorySnapshot. Second arg is the
|
|
769
|
-
// resolved activePageId (undefined here because no entry has one).
|
|
770
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
771
|
-
expect.objectContaining(baselineBrandPageData),
|
|
772
|
-
undefined,
|
|
773
|
-
);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it('redo increments pointer, keeps stack intact, calls applyHistorySnapshot', async () => {
|
|
777
|
-
const store = useHistoryStore();
|
|
778
|
-
|
|
779
|
-
await store.captureBaseline();
|
|
780
|
-
captureMutated(store);
|
|
781
|
-
await store.undo();
|
|
782
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
783
|
-
|
|
784
|
-
expect(store.currentIndex).toBe(0);
|
|
785
|
-
expect(store.canRedo).toBe(true);
|
|
786
|
-
|
|
787
|
-
await store.redo();
|
|
788
|
-
|
|
789
|
-
expect(store.currentIndex).toBe(1);
|
|
790
|
-
expect(store.entries.length).toBe(2);
|
|
791
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledTimes(1);
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
it('baseline invariant: undo refuses to go below index 0', async () => {
|
|
795
|
-
const store = useHistoryStore();
|
|
796
|
-
|
|
797
|
-
await store.captureBaseline();
|
|
798
|
-
|
|
799
|
-
expect(store.canUndo).toBe(false);
|
|
800
|
-
await store.undo();
|
|
801
|
-
|
|
802
|
-
expect(store.currentIndex).toBe(0);
|
|
803
|
-
expect(mockMakerStore.applyHistorySnapshot).not.toHaveBeenCalled();
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
it('getCurrentSizeBytes sums byte sizes across all entries', async () => {
|
|
807
|
-
const store = useHistoryStore();
|
|
808
|
-
|
|
809
|
-
expect(store.getCurrentSizeBytes()).toBe(0);
|
|
810
|
-
|
|
811
|
-
await store.captureBaseline();
|
|
812
|
-
const baselineSize = store.getCurrentSizeBytes();
|
|
813
|
-
|
|
814
|
-
expect(baselineSize).toBe(store.entries[0].sizeBytes);
|
|
815
|
-
expect(baselineSize).toBeGreaterThan(0);
|
|
816
|
-
|
|
817
|
-
captureMutated(store);
|
|
818
|
-
expect(store.getCurrentSizeBytes()).toBe(
|
|
819
|
-
store.entries[0].sizeBytes + store.entries[1].sizeBytes,
|
|
820
|
-
);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
it('getCurrentEntryCount reflects the stack length', async () => {
|
|
824
|
-
const store = useHistoryStore();
|
|
825
|
-
|
|
826
|
-
expect(store.getCurrentEntryCount()).toBe(0);
|
|
827
|
-
|
|
828
|
-
await store.captureBaseline();
|
|
829
|
-
expect(store.getCurrentEntryCount()).toBe(1);
|
|
830
|
-
|
|
831
|
-
captureMutated(store);
|
|
832
|
-
expect(store.getCurrentEntryCount()).toBe(2);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
// --- U1 finalize (FR-12 / FR-13 / FR-13c) ---
|
|
836
|
-
|
|
837
|
-
it('FR-12: caps the stack at 40 entries, dropping oldest non-baseline', async () => {
|
|
838
|
-
const store = useHistoryStore();
|
|
839
|
-
|
|
840
|
-
await store.captureBaseline();
|
|
841
|
-
const baselineSnapshot = store.entries[0].snapshot;
|
|
842
|
-
|
|
843
|
-
// Baseline + 50 edits — stack should cap at 40.
|
|
844
|
-
for (let i = 0; i < 50; i++) {
|
|
845
|
-
// Mutate the mock so each snapshot is distinguishable.
|
|
846
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
847
|
-
(mockMakerStore.brandPageData as any).v = i + 100;
|
|
848
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
expect(store.entries.length).toBe(40);
|
|
852
|
-
// Baseline is preserved at index 0.
|
|
853
|
-
expect(store.entries[0].snapshot).toBe(baselineSnapshot);
|
|
854
|
-
// Pointer is at the new end.
|
|
855
|
-
expect(store.currentIndex).toBe(39);
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
it('FR-13c: skips oversize captures (> 25 MB) and leaves the stack alone', async () => {
|
|
859
|
-
const store = useHistoryStore();
|
|
860
|
-
|
|
861
|
-
await store.captureBaseline();
|
|
862
|
-
const lengthBefore = store.entries.length;
|
|
863
|
-
|
|
864
|
-
// Swap in a mock that produces a > 25 MB snapshot on serialize.
|
|
865
|
-
const originalData = mockMakerStore.brandPageData;
|
|
866
|
-
|
|
867
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
868
|
-
(mockMakerStore as any).brandPageData = {
|
|
869
|
-
v: 1,
|
|
870
|
-
pages: [],
|
|
871
|
-
styles: {},
|
|
872
|
-
_padding: 'x'.repeat(26 * 1024 * 1024),
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
876
|
-
|
|
877
|
-
expect(result).toBe('oversized');
|
|
878
|
-
expect(store.entries.length).toBe(lengthBefore);
|
|
879
|
-
|
|
880
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
881
|
-
(mockMakerStore as any).brandPageData = originalData;
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
it('FR-13: evicts oldest non-baseline when total memory exceeds 100 MB', async () => {
|
|
885
|
-
const store = useHistoryStore();
|
|
886
|
-
|
|
887
|
-
await store.captureBaseline();
|
|
888
|
-
const baselineSnapshot = store.entries[0].snapshot;
|
|
889
|
-
|
|
890
|
-
// Each capture is ~4 MB, under the 5 MB per-entry cap. Push 30
|
|
891
|
-
// of them — total would be ~120 MB, so memory eviction should kick in.
|
|
892
|
-
const originalData = mockMakerStore.brandPageData;
|
|
893
|
-
|
|
894
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
895
|
-
(mockMakerStore as any).brandPageData = {
|
|
896
|
-
v: 1,
|
|
897
|
-
pages: [],
|
|
898
|
-
styles: {},
|
|
899
|
-
_padding: 'x'.repeat(4 * 1024 * 1024),
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
for (let i = 0; i < 30; i++) {
|
|
903
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
expect(store.getCurrentSizeBytes()).toBeLessThanOrEqual(100 * 1024 * 1024);
|
|
907
|
-
// Baseline must survive memory eviction.
|
|
908
|
-
expect(store.entries[0].snapshot).toBe(baselineSnapshot);
|
|
909
|
-
|
|
910
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
911
|
-
(mockMakerStore as any).brandPageData = originalData;
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// --- U5 analytics ---
|
|
915
|
-
|
|
916
|
-
it('emits undo / redo events with entries_count + actor of the affected entry', async () => {
|
|
917
|
-
const received: { name: string; props: unknown }[] = [];
|
|
918
|
-
|
|
919
|
-
useEventTracker().subscribe((event) =>
|
|
920
|
-
received.push({ name: event.eventName, props: event.eventProps }),
|
|
921
|
-
);
|
|
922
|
-
|
|
923
|
-
const store = useHistoryStore();
|
|
924
|
-
|
|
925
|
-
await store.captureBaseline();
|
|
926
|
-
captureMutated(store, { source: 'ai_changeset', actor: 'ai_builder' });
|
|
927
|
-
await store.undo();
|
|
928
|
-
await store.redo();
|
|
929
|
-
|
|
930
|
-
const undoEvent = received.find((e) => e.name === 'website_maker_undo');
|
|
931
|
-
const redoEvent = received.find((e) => e.name === 'website_maker_redo');
|
|
932
|
-
|
|
933
|
-
expect(undoEvent).toBeDefined();
|
|
934
|
-
expect(redoEvent).toBeDefined();
|
|
935
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
936
|
-
expect((undoEvent!.props as any).entries_count).toBe(2);
|
|
937
|
-
// Undo reports the actor of the entry being undone — the ai_builder change.
|
|
938
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
939
|
-
expect((undoEvent!.props as any).actor).toBe('ai_builder');
|
|
940
|
-
// Redo reports the actor of the entry being re-applied — same ai_builder change.
|
|
941
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
942
|
-
expect((redoEvent!.props as any).actor).toBe('ai_builder');
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
it('emits oversized event when a capture exceeds 25 MB', async () => {
|
|
946
|
-
const received: { name: string; props: unknown }[] = [];
|
|
947
|
-
|
|
948
|
-
useEventTracker().subscribe((event) =>
|
|
949
|
-
received.push({ name: event.eventName, props: event.eventProps }),
|
|
950
|
-
);
|
|
951
|
-
|
|
952
|
-
const store = useHistoryStore();
|
|
953
|
-
|
|
954
|
-
await store.captureBaseline();
|
|
955
|
-
|
|
956
|
-
const originalData = mockMakerStore.brandPageData;
|
|
957
|
-
|
|
958
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
959
|
-
(mockMakerStore as any).brandPageData = {
|
|
960
|
-
v: 1,
|
|
961
|
-
pages: [],
|
|
962
|
-
styles: {},
|
|
963
|
-
_padding: 'x'.repeat(26 * 1024 * 1024),
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
967
|
-
|
|
968
|
-
const oversized = received.find(
|
|
969
|
-
(e) => e.name === 'website_maker_history_oversized',
|
|
970
|
-
);
|
|
971
|
-
|
|
972
|
-
expect(oversized).toBeDefined();
|
|
973
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
974
|
-
expect((oversized!.props as any).attempted_bytes).toBeGreaterThan(
|
|
975
|
-
25 * 1024 * 1024,
|
|
976
|
-
);
|
|
977
|
-
|
|
978
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
979
|
-
(mockMakerStore as any).brandPageData = originalData;
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
// --- BP-5506 (page-tab highlight + active-page tracking) ---
|
|
983
|
-
|
|
984
|
-
describe('activePageId capture and restore', () => {
|
|
985
|
-
it('captures the current activePageId on baseline', async () => {
|
|
986
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
987
|
-
|
|
988
|
-
const store = useHistoryStore();
|
|
989
|
-
|
|
990
|
-
await store.captureBaseline();
|
|
991
|
-
|
|
992
|
-
expect(store.entries[0].activePageId).toBe('page-home');
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
it('captures the current activePageId on each force-commit', async () => {
|
|
996
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
997
|
-
const store = useHistoryStore();
|
|
998
|
-
|
|
999
|
-
await store.captureBaseline();
|
|
1000
|
-
|
|
1001
|
-
mockPageNavigationStore.activePageId = 'page-2';
|
|
1002
|
-
captureMutated(store);
|
|
1003
|
-
|
|
1004
|
-
expect(store.entries[1].activePageId).toBe('page-2');
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
it('passes the captured activePageId to applyHistorySnapshot when the page still exists', async () => {
|
|
1008
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1009
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1010
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1011
|
-
{ id: 'page-home', contents: [] },
|
|
1012
|
-
];
|
|
1013
|
-
|
|
1014
|
-
const store = useHistoryStore();
|
|
1015
|
-
|
|
1016
|
-
await store.captureBaseline();
|
|
1017
|
-
|
|
1018
|
-
mockPageNavigationStore.activePageId = 'page-2';
|
|
1019
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1020
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1021
|
-
{ id: 'page-home', contents: [] },
|
|
1022
|
-
{ id: 'page-2', contents: [] },
|
|
1023
|
-
];
|
|
1024
|
-
captureMutated(store);
|
|
1025
|
-
|
|
1026
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1027
|
-
await store.undo();
|
|
1028
|
-
|
|
1029
|
-
// Baseline restored — its captured page was 'page-home', and that
|
|
1030
|
-
// page exists in the restored snapshot, so it round-trips to apply.
|
|
1031
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
1032
|
-
expect.anything(),
|
|
1033
|
-
'page-home',
|
|
1034
|
-
);
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
it('walks back when the captured activePageId no longer exists in the restored snapshot (page-add undo)', async () => {
|
|
1038
|
-
// Baseline: only page-home, user is on page-home.
|
|
1039
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1040
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1041
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1042
|
-
{ id: 'page-home', contents: [] },
|
|
1043
|
-
];
|
|
1044
|
-
|
|
1045
|
-
const store = useHistoryStore();
|
|
1046
|
-
|
|
1047
|
-
await store.captureBaseline();
|
|
1048
|
-
|
|
1049
|
-
// User adds a new page. Snapshot now has both pages, user is on the
|
|
1050
|
-
// new page.
|
|
1051
|
-
mockPageNavigationStore.activePageId = 'page-new';
|
|
1052
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1053
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1054
|
-
{ id: 'page-home', contents: [] },
|
|
1055
|
-
{ id: 'page-new', contents: [] },
|
|
1056
|
-
];
|
|
1057
|
-
captureMutated(store);
|
|
1058
|
-
|
|
1059
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1060
|
-
await store.undo();
|
|
1061
|
-
|
|
1062
|
-
// Baseline only has 'page-home'. The entry being applied has
|
|
1063
|
-
// activePageId='page-home', so we resolve to that.
|
|
1064
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
1065
|
-
expect.anything(),
|
|
1066
|
-
'page-home',
|
|
1067
|
-
);
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
it('navigates to the restored page when undoing a delete (page present in target but not source)', async () => {
|
|
1071
|
-
// Baseline: home + about, user is on about (the page that will be deleted).
|
|
1072
|
-
mockPageNavigationStore.activePageId = 'page-about';
|
|
1073
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1074
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1075
|
-
{ id: 'page-home', contents: [] },
|
|
1076
|
-
{ id: 'page-about', contents: [] },
|
|
1077
|
-
];
|
|
1078
|
-
|
|
1079
|
-
const store = useHistoryStore();
|
|
1080
|
-
|
|
1081
|
-
await store.captureBaseline();
|
|
1082
|
-
|
|
1083
|
-
// User deletes about. The delete handler navigates them to home,
|
|
1084
|
-
// then captures. Snapshot has only home; activePageId on the entry
|
|
1085
|
-
// is page-home (not page-about — the user was already off it by
|
|
1086
|
-
// the time the watcher fired).
|
|
1087
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1088
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1089
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1090
|
-
{ id: 'page-home', contents: [] },
|
|
1091
|
-
];
|
|
1092
|
-
captureMutated(store);
|
|
1093
|
-
|
|
1094
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1095
|
-
await store.undo();
|
|
1096
|
-
|
|
1097
|
-
// Diff resolution: target snapshot (baseline) has page-about,
|
|
1098
|
-
// source snapshot (post-delete entry 1) does not. So page-about
|
|
1099
|
-
// is being restored — navigate there. Without diff resolution
|
|
1100
|
-
// the walk-back would land on page-home (the activePageId on
|
|
1101
|
-
// entry 1 and entry 0 both happen to be page-home in this
|
|
1102
|
-
// scenario), leaving the user staring at home after un-deleting
|
|
1103
|
-
// about.
|
|
1104
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
1105
|
-
expect.anything(),
|
|
1106
|
-
'page-about',
|
|
1107
|
-
);
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
it('navigates to the newly-added page when redoing an add (page present in target but not source)', async () => {
|
|
1111
|
-
// Baseline: only home.
|
|
1112
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1113
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1114
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1115
|
-
{ id: 'page-home', contents: [] },
|
|
1116
|
-
];
|
|
1117
|
-
|
|
1118
|
-
const store = useHistoryStore();
|
|
1119
|
-
|
|
1120
|
-
await store.captureBaseline();
|
|
1121
|
-
|
|
1122
|
-
// User adds page-new and is navigated there.
|
|
1123
|
-
mockPageNavigationStore.activePageId = 'page-new';
|
|
1124
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1125
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1126
|
-
{ id: 'page-home', contents: [] },
|
|
1127
|
-
{ id: 'page-new', contents: [] },
|
|
1128
|
-
];
|
|
1129
|
-
captureMutated(store);
|
|
1130
|
-
|
|
1131
|
-
// Undo the add — back to baseline.
|
|
1132
|
-
await store.undo();
|
|
1133
|
-
|
|
1134
|
-
// Redo the add. Source (baseline) has only home; target (entry 1)
|
|
1135
|
-
// has both home and new. The diff identifies page-new as the
|
|
1136
|
-
// page being restored by this transition — navigate there.
|
|
1137
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1138
|
-
await store.redo();
|
|
1139
|
-
|
|
1140
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
1141
|
-
expect.anything(),
|
|
1142
|
-
'page-new',
|
|
1143
|
-
);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
it('falls through to undefined when no entry walking back has a viable id', async () => {
|
|
1147
|
-
// Baseline page exists, captured id was 'page-home'.
|
|
1148
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1149
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1150
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1151
|
-
{ id: 'page-home', contents: [] },
|
|
1152
|
-
];
|
|
1153
|
-
|
|
1154
|
-
const store = useHistoryStore();
|
|
1155
|
-
|
|
1156
|
-
await store.captureBaseline();
|
|
1157
|
-
|
|
1158
|
-
// Snapshot contains a totally different page (e.g. template switch
|
|
1159
|
-
// replaced everything) and the user is on its new page.
|
|
1160
|
-
mockPageNavigationStore.activePageId = 'page-x';
|
|
1161
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1162
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
1163
|
-
{ id: 'page-x', contents: [] },
|
|
1164
|
-
];
|
|
1165
|
-
captureMutated(store);
|
|
1166
|
-
|
|
1167
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1168
|
-
await store.undo();
|
|
1169
|
-
|
|
1170
|
-
// Baseline restored — its activePageId 'page-home' exists in its own
|
|
1171
|
-
// snapshot, so resolution finds it on the first walk-back hop.
|
|
1172
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
1173
|
-
expect.anything(),
|
|
1174
|
-
'page-home',
|
|
1175
|
-
);
|
|
1176
|
-
|
|
1177
|
-
// Now redo. Pointer moves forward to entry 1 (page-x captured),
|
|
1178
|
-
// page-x exists in entry-1's snapshot, so resolves to 'page-x'.
|
|
1179
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1180
|
-
await store.redo();
|
|
1181
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
1182
|
-
expect.anything(),
|
|
1183
|
-
'page-x',
|
|
1184
|
-
);
|
|
1185
|
-
});
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
describe('reset()', () => {
|
|
1189
|
-
it('clears entries and re-baselines against the current state', async () => {
|
|
1190
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1191
|
-
|
|
1192
|
-
const store = useHistoryStore();
|
|
1193
|
-
|
|
1194
|
-
await store.captureBaseline();
|
|
1195
|
-
captureMutated(store);
|
|
1196
|
-
captureMutated(store);
|
|
1197
|
-
|
|
1198
|
-
expect(store.entries.length).toBe(3);
|
|
1199
|
-
|
|
1200
|
-
// Mutate the mock so the new baseline reflects the just-saved state.
|
|
1201
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1202
|
-
(mockMakerStore.brandPageData as any).v = 9999;
|
|
1203
|
-
|
|
1204
|
-
store.reset();
|
|
1205
|
-
|
|
1206
|
-
expect(store.entries.length).toBe(1);
|
|
1207
|
-
expect(store.currentIndex).toBe(0);
|
|
1208
|
-
expect(store.baselineCaptured).toBe(true);
|
|
1209
|
-
expect(JSON.parse(store.entries[0].snapshot).v).toBe(9999);
|
|
1210
|
-
expect(store.canUndo).toBe(false);
|
|
1211
|
-
expect(store.canRedo).toBe(false);
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
it('captures activePageId on the new baseline after reset', async () => {
|
|
1215
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
1216
|
-
|
|
1217
|
-
const store = useHistoryStore();
|
|
1218
|
-
|
|
1219
|
-
await store.captureBaseline();
|
|
1220
|
-
captureMutated(store);
|
|
1221
|
-
|
|
1222
|
-
mockPageNavigationStore.activePageId = 'page-2';
|
|
1223
|
-
store.reset();
|
|
1224
|
-
|
|
1225
|
-
expect(store.entries[0].activePageId).toBe('page-2');
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
it('no-ops when no baseline has ever been captured (unflagged session)', () => {
|
|
1229
|
-
// Simulates a session where the undo/redo feature flag is off, so
|
|
1230
|
-
// captureBaseline is never called. saveBrandPage calls reset()
|
|
1231
|
-
// unconditionally — it must not install the watcher or capture a
|
|
1232
|
-
// baseline as a side effect, otherwise unflagged sessions start
|
|
1233
|
-
// accumulating snapshots in memory.
|
|
1234
|
-
const store = useHistoryStore();
|
|
1235
|
-
|
|
1236
|
-
expect(store.baselineCaptured).toBe(false);
|
|
1237
|
-
expect(store.entries.length).toBe(0);
|
|
1238
|
-
|
|
1239
|
-
store.reset();
|
|
1240
|
-
|
|
1241
|
-
expect(store.baselineCaptured).toBe(false);
|
|
1242
|
-
expect(store.entries.length).toBe(0);
|
|
1243
|
-
expect(store.currentIndex).toBe(-1);
|
|
1244
|
-
});
|
|
1245
|
-
});
|
|
1246
|
-
});
|