@designcrowd/library.brand-page 5.18.3-undo-40 → 5.18.3
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-D_w411MF.js → CreatedOnBrandCrowd-de-de-CkuAAwKQ.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DvJhv23Q.js → CreatedOnBrandCrowd-en-us-Coln5kSU.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BvW6EBKl.js → CreatedOnBrandCrowd-es-es-raGyXChm.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Bc4iL236.js → CreatedOnBrandCrowd-fr-ca-BT_WYYxW.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Bc4iL236.js → CreatedOnBrandCrowd-fr-fr-BT_WYYxW.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-C74D9eIB.js → CreatedOnBrandCrowd-pt-br-BRbD_kiI.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-C74D9eIB.js → CreatedOnBrandCrowd-pt-pt-BRbD_kiI.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-BVL3vDSq.js → CreatedOnDcom-de-de-DetCIFxa.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-B9Dow5pk.js → CreatedOnDcom-en-us-D0LeF_V2.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-H2Dv6N8F.js → CreatedOnDcom-es-es-CHa4stWS.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-sw4LRlLw.js → CreatedOnDcom-fr-ca-ntW7A335.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-DnOriI4M.js → CreatedOnDcom-fr-fr-C6rQcclc.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-BXs8zbRg.js → CreatedOnDcom-pt-br-Cn07ZK_m.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-DmfOvW_0.js → CreatedOnDcom-pt-pt-CewmqYVY.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 -8
- 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 -87
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +0 -5
- 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 -20
- 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/brandpage-maker/brandpage-maker-store.d.ts +2 -41
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +5 -307
- 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-B5Fkt3qd.js +175 -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 +15 -20
- package/lib/src/css/library.brand-page-brandPage.min.css +16 -20
- package/lib/src/css/library.brand-page-designCom.min.css +15 -20
- package/lib/src/css/library.brand-page-preview.min.css +15 -20
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-Dg2Zs9nH.js → lib-CGfmcIBb.js} +29373 -29970
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +2 -87
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.js +0 -5
- 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/form-fields-field/FormFieldsField.mixin.js +8 -18
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -64
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +0 -6
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +0 -19
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.js +0 -11
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +2 -10
- package/lib/src/shared/logo-settings/store/editable-logo-store.d.ts +1 -5
- package/lib/src/shared/logo-settings/store/editable-logo-store.js +0 -31
- 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 -10
- 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-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +0 -9
- 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/brandpage-maker-page/BrandPageMakerPage.mixin.ts +0 -5
- 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 +3 -26
- 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 -24
- package/src/brand-page-maker/composables/useEventTracker.ts +0 -29
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +0 -335
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +4 -371
- 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.mixin.ts +0 -5
- 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/page-dropdown-field/PageDropdownField.vue +0 -4
- package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +0 -7
- 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 -42
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +0 -17
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +0 -25
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +0 -83
- package/lib/src/brand-page-maker/stores/history/history-store.js +0 -495
- package/lib/src/bundled-translations-D1q3rSEz.js +0 -175
- 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.vue +0 -66
- package/src/brand-page-maker/components/quick-link-bar/UndoButton.vue +0 -66
- package/src/brand-page-maker/components/quick-link-bar/undo-redo-buttons.test.ts +0 -116
- package/src/brand-page-maker/composables/useUndoRedoFlag.ts +0 -30
- package/src/brand-page-maker/stores/history/history-store.test.ts +0 -1120
- package/src/brand-page-maker/stores/history/history-store.ts +0 -660
- package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
|
@@ -1,1120 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { reactive } from 'vue';
|
|
3
|
-
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
-
import { useHistoryStore } from './history-store';
|
|
5
|
-
import { useEventTracker } from '../../composables/useEventTracker';
|
|
6
|
-
import { useUiEditableLogoStore } from '@shared/logo-settings/store/editable-logo-store';
|
|
7
|
-
import type { BrandPageModel } from '@brand-page';
|
|
8
|
-
|
|
9
|
-
// Shared mock maker store — methods are vi.fn() so we can assert calls.
|
|
10
|
-
const mockMakerStore = {
|
|
11
|
-
brandPageData: {
|
|
12
|
-
v: 1,
|
|
13
|
-
pages: [],
|
|
14
|
-
styles: {},
|
|
15
|
-
} as unknown as BrandPageModel,
|
|
16
|
-
isBrandPageLoaded: false,
|
|
17
|
-
applyHistorySnapshot: vi.fn(async () => undefined),
|
|
18
|
-
setIsBrandPageLoaded: vi.fn(),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const mockPageNavigationStore = {
|
|
22
|
-
activePageId: '',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
vi.mock('../brandpage-maker/brandpage-maker-store', () => ({
|
|
26
|
-
useBrandPageMakerStore: vi.fn(() => mockMakerStore),
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
vi.mock('../ai-thread/ai-website-builder-thread-store', () => ({
|
|
30
|
-
useAiWebsiteBuilderThreadStore: vi.fn(() => ({
|
|
31
|
-
isProcessing: false,
|
|
32
|
-
})),
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
vi.mock('@shared/page-navigation/store/page-navigation-store', () => ({
|
|
36
|
-
usePageNavigationStore: vi.fn(() => mockPageNavigationStore),
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
// Mutate the mock between captures so each capture produces a distinct
|
|
40
|
-
// snapshot. The store now drops byte-identical captures (BP-5506: prevents
|
|
41
|
-
// undo/redo steps that do nothing visible), so tests that previously relied
|
|
42
|
-
// on capture-without-mutation lining up entries need to bump a field first.
|
|
43
|
-
let mutationCounter = 0;
|
|
44
|
-
|
|
45
|
-
function mutateMock(): void {
|
|
46
|
-
mutationCounter += 1;
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
-
(mockMakerStore.brandPageData as any).v = 1000 + mutationCounter;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function captureMutated(
|
|
52
|
-
store: ReturnType<typeof useHistoryStore>,
|
|
53
|
-
args: Parameters<typeof store.capture>[0] = {
|
|
54
|
-
source: 'manual_edit',
|
|
55
|
-
actor: 'user',
|
|
56
|
-
},
|
|
57
|
-
) {
|
|
58
|
-
mutateMock();
|
|
59
|
-
|
|
60
|
-
return store.capture(args);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe('history-store (single stack + JSON-string snapshots)', () => {
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
setActivePinia(createPinia());
|
|
66
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
67
|
-
mutationCounter = 0;
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
-
(mockMakerStore.brandPageData as any).v = 1;
|
|
70
|
-
mockPageNavigationStore.activePageId = '';
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('captures baseline and initialises the pointer to 0', async () => {
|
|
74
|
-
const store = useHistoryStore();
|
|
75
|
-
|
|
76
|
-
expect(store.entries.length).toBe(0);
|
|
77
|
-
expect(store.currentIndex).toBe(-1);
|
|
78
|
-
expect(store.baselineCaptured).toBe(false);
|
|
79
|
-
|
|
80
|
-
await store.captureBaseline();
|
|
81
|
-
|
|
82
|
-
expect(store.baselineCaptured).toBe(true);
|
|
83
|
-
expect(store.entries.length).toBe(1);
|
|
84
|
-
expect(store.entries[0].label).toBe('baseline');
|
|
85
|
-
expect(store.currentIndex).toBe(0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('stores snapshots as JSON strings, not objects', async () => {
|
|
89
|
-
const store = useHistoryStore();
|
|
90
|
-
|
|
91
|
-
await store.captureBaseline();
|
|
92
|
-
|
|
93
|
-
const entry = store.entries[0];
|
|
94
|
-
|
|
95
|
-
expect(typeof entry.snapshot).toBe('string');
|
|
96
|
-
// Round-trips to the same model via JSON.parse.
|
|
97
|
-
expect(JSON.parse(entry.snapshot)).toMatchObject(
|
|
98
|
-
mockMakerStore.brandPageData,
|
|
99
|
-
);
|
|
100
|
-
// sizeBytes matches string length
|
|
101
|
-
expect(entry.sizeBytes).toBe(entry.snapshot.length);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('does not capture baseline twice', async () => {
|
|
105
|
-
const store = useHistoryStore();
|
|
106
|
-
|
|
107
|
-
await store.captureBaseline();
|
|
108
|
-
const firstLength = store.entries.length;
|
|
109
|
-
|
|
110
|
-
await store.captureBaseline();
|
|
111
|
-
|
|
112
|
-
expect(store.entries.length).toBe(firstLength);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('capture pushes a new entry and advances the pointer', async () => {
|
|
116
|
-
const store = useHistoryStore();
|
|
117
|
-
|
|
118
|
-
await store.captureBaseline();
|
|
119
|
-
captureMutated(store);
|
|
120
|
-
|
|
121
|
-
expect(store.entries.length).toBe(2);
|
|
122
|
-
expect(store.currentIndex).toBe(1);
|
|
123
|
-
|
|
124
|
-
captureMutated(store);
|
|
125
|
-
|
|
126
|
-
expect(store.entries.length).toBe(3);
|
|
127
|
-
expect(store.currentIndex).toBe(2);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('capture truncates any redo tail (FR-11)', async () => {
|
|
131
|
-
const store = useHistoryStore();
|
|
132
|
-
|
|
133
|
-
await store.captureBaseline();
|
|
134
|
-
captureMutated(store); // index 1
|
|
135
|
-
captureMutated(store); // index 2
|
|
136
|
-
await store.undo(); // currentIndex = 1, entries still length 3
|
|
137
|
-
await store.undo(); // currentIndex = 0, entries still length 3
|
|
138
|
-
|
|
139
|
-
expect(store.entries.length).toBe(3);
|
|
140
|
-
expect(store.canRedo).toBe(true);
|
|
141
|
-
|
|
142
|
-
// New capture should truncate entries[1..] and replace with the new one.
|
|
143
|
-
captureMutated(store);
|
|
144
|
-
|
|
145
|
-
expect(store.entries.length).toBe(2); // baseline + new
|
|
146
|
-
expect(store.currentIndex).toBe(1);
|
|
147
|
-
expect(store.canRedo).toBe(false);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('drops byte-identical captures so undo/redo never replays a no-op', async () => {
|
|
151
|
-
const store = useHistoryStore();
|
|
152
|
-
|
|
153
|
-
await store.captureBaseline();
|
|
154
|
-
|
|
155
|
-
// First capture mutates and lands.
|
|
156
|
-
const r1 = captureMutated(store);
|
|
157
|
-
|
|
158
|
-
expect(r1).toBe('committed');
|
|
159
|
-
expect(store.entries.length).toBe(2);
|
|
160
|
-
|
|
161
|
-
// Second capture without a mutation should be detected as a no-op
|
|
162
|
-
// and skipped — the stack stays at length 2.
|
|
163
|
-
const r2 = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
164
|
-
|
|
165
|
-
expect(r2).toBe('noop');
|
|
166
|
-
expect(store.entries.length).toBe(2);
|
|
167
|
-
expect(store.currentIndex).toBe(1);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('captures pre-purchase logo edits even when brandPageData is byte-identical', async () => {
|
|
171
|
-
// Pre-purchase logo-tab edits (text/color/font) live on
|
|
172
|
-
// `editableLogoStore.logoEdits` (sidecar) and the rendered base64 is
|
|
173
|
-
// stripped from snapshots to stay under the 25 MB cap. Two
|
|
174
|
-
// consecutive logo edits therefore produce identical snapshot bytes
|
|
175
|
-
// — without comparing the sidecar, every logo edit would dedup as
|
|
176
|
-
// a no-op and history would silently stop growing.
|
|
177
|
-
const editableLogoStore = useUiEditableLogoStore();
|
|
178
|
-
|
|
179
|
-
editableLogoStore.logoEdits.textObjects = [
|
|
180
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
-
{ id: 't1', role: 'primary-text', text: 'Acme' } as any,
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
const store = useHistoryStore();
|
|
185
|
-
|
|
186
|
-
await store.captureBaseline();
|
|
187
|
-
expect(store.entries.length).toBe(1);
|
|
188
|
-
|
|
189
|
-
// User changes the logo text — only the sidecar mutates. Snapshot
|
|
190
|
-
// bytes are identical (no brandPageData diff). Must still commit.
|
|
191
|
-
editableLogoStore.logoEdits.textObjects[0].text = 'AcmeCo';
|
|
192
|
-
|
|
193
|
-
const r = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
194
|
-
|
|
195
|
-
expect(r).toBe('committed');
|
|
196
|
-
expect(store.entries.length).toBe(2);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// BP-5506 phantom-undo prevention via the `mergeNextMutation()` flag.
|
|
200
|
-
// Hydration write sites (TipTap mount, lazy paymentConfig fill,
|
|
201
|
-
// stale-page-ref clear) call this immediately before the imperative
|
|
202
|
-
// write they know is a hydration. The watcher's flush consumes the flag
|
|
203
|
-
// and folds the resulting snapshot into the current head entry instead
|
|
204
|
-
// of pushing a new one — so opening a sidebar or clicking a section
|
|
205
|
-
// doesn't grow the undo stack.
|
|
206
|
-
describe('mergeNextMutation flag (hydration writes fold into head)', () => {
|
|
207
|
-
// Internal constants from the watcher: BASELINE_SETTLE_MS and
|
|
208
|
-
// CAPTURE_DEBOUNCE_MS. Tests can't import them directly without
|
|
209
|
-
// exporting from the store; we use vi.useFakeTimers + advance past
|
|
210
|
-
// the debounce to drive the watcher's flush.
|
|
211
|
-
const CAPTURE_DEBOUNCE_MS = 1700;
|
|
212
|
-
const BASELINE_SETTLE_MS = 2000;
|
|
213
|
-
|
|
214
|
-
let liveData: Record<string, unknown>;
|
|
215
|
-
|
|
216
|
-
// Vue's deep watcher only fires on a reactive proxy. The shared mock
|
|
217
|
-
// is a plain object — for this group we swap in a `reactive()` proxy
|
|
218
|
-
// and mutate it directly so the watcher (installed on store baseline
|
|
219
|
-
// capture) sees the change.
|
|
220
|
-
beforeEach(() => {
|
|
221
|
-
vi.useFakeTimers();
|
|
222
|
-
liveData = reactive({ v: 1, pages: [], styles: {} });
|
|
223
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
-
(mockMakerStore as any).brandPageData = liveData;
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
afterEach(() => {
|
|
228
|
-
vi.useRealTimers();
|
|
229
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
-
(mockMakerStore as any).brandPageData = {
|
|
231
|
-
v: 1,
|
|
232
|
-
pages: [],
|
|
233
|
-
styles: {},
|
|
234
|
-
};
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Drive the watcher: flush Vue's reactive queue (so the watch
|
|
238
|
-
// callback runs and arms the timer), then advance past
|
|
239
|
-
// CAPTURE_DEBOUNCE_MS so the queued flush fires.
|
|
240
|
-
async function flushWatcher(): Promise<void> {
|
|
241
|
-
await Promise.resolve();
|
|
242
|
-
await vi.advanceTimersByTimeAsync(CAPTURE_DEBOUNCE_MS + 50);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function elapseSettleWindow(): Promise<void> {
|
|
246
|
-
await vi.advanceTimersByTimeAsync(BASELINE_SETTLE_MS + 50);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
it('mergeNextMutation folds the next watcher flush into the current head (no new entry)', async () => {
|
|
250
|
-
const store = useHistoryStore();
|
|
251
|
-
|
|
252
|
-
await store.captureBaseline();
|
|
253
|
-
await elapseSettleWindow();
|
|
254
|
-
|
|
255
|
-
// First real edit lands as a new entry.
|
|
256
|
-
liveData.v = 100;
|
|
257
|
-
await flushWatcher();
|
|
258
|
-
|
|
259
|
-
expect(store.entries.length).toBe(2);
|
|
260
|
-
expect(store.currentIndex).toBe(1);
|
|
261
|
-
const headBefore = store.entries[1].snapshot;
|
|
262
|
-
|
|
263
|
-
// Now a hydration site flags the next mutation as merge-into-head.
|
|
264
|
-
store.mergeNextMutation();
|
|
265
|
-
liveData.hydratedField = '<p>hydrated</p>';
|
|
266
|
-
await flushWatcher();
|
|
267
|
-
|
|
268
|
-
// No new entry — the head was replaced in place.
|
|
269
|
-
expect(store.entries.length).toBe(2);
|
|
270
|
-
expect(store.currentIndex).toBe(1);
|
|
271
|
-
expect(store.entries[1].snapshot).not.toBe(headBefore);
|
|
272
|
-
expect(JSON.parse(store.entries[1].snapshot).hydratedField).toBe(
|
|
273
|
-
'<p>hydrated</p>',
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('mergeNextMutation is one-shot — the flag clears after a single flush', async () => {
|
|
278
|
-
const store = useHistoryStore();
|
|
279
|
-
|
|
280
|
-
await store.captureBaseline();
|
|
281
|
-
await elapseSettleWindow();
|
|
282
|
-
|
|
283
|
-
// Land a real entry first so the head isn't the baseline (where
|
|
284
|
-
// the post-settle merge-into-baseline rule no longer applies but
|
|
285
|
-
// the merge branch is still doing its job).
|
|
286
|
-
liveData.v = 100;
|
|
287
|
-
await flushWatcher();
|
|
288
|
-
|
|
289
|
-
expect(store.entries.length).toBe(2);
|
|
290
|
-
|
|
291
|
-
// First post-baseline mutation: flagged as merge — folds into head.
|
|
292
|
-
store.mergeNextMutation();
|
|
293
|
-
liveData.v = 150;
|
|
294
|
-
await flushWatcher();
|
|
295
|
-
|
|
296
|
-
expect(store.entries.length).toBe(2);
|
|
297
|
-
|
|
298
|
-
// Second mutation: no flag — must push a new entry.
|
|
299
|
-
liveData.v = 200;
|
|
300
|
-
await flushWatcher();
|
|
301
|
-
|
|
302
|
-
expect(store.entries.length).toBe(3);
|
|
303
|
-
expect(store.currentIndex).toBe(2);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('multiple mergeNextMutation calls in the same burst still produce only one merge', async () => {
|
|
307
|
-
// Idempotent flag — every editor on a page mounting at once calls
|
|
308
|
-
// it, but the watcher debounces all the mutations into a single
|
|
309
|
-
// flush, which consumes the flag once.
|
|
310
|
-
const store = useHistoryStore();
|
|
311
|
-
|
|
312
|
-
await store.captureBaseline();
|
|
313
|
-
await elapseSettleWindow();
|
|
314
|
-
|
|
315
|
-
// Three hydration writes from three editor mounts in the same tick.
|
|
316
|
-
store.mergeNextMutation();
|
|
317
|
-
liveData.a = '1';
|
|
318
|
-
store.mergeNextMutation();
|
|
319
|
-
liveData.b = '2';
|
|
320
|
-
store.mergeNextMutation();
|
|
321
|
-
liveData.c = '3';
|
|
322
|
-
await flushWatcher();
|
|
323
|
-
|
|
324
|
-
// All three writes folded into the same merge — still one entry.
|
|
325
|
-
expect(store.entries.length).toBe(1);
|
|
326
|
-
const merged = JSON.parse(store.entries[0].snapshot);
|
|
327
|
-
|
|
328
|
-
expect(merged.a).toBe('1');
|
|
329
|
-
expect(merged.b).toBe('2');
|
|
330
|
-
expect(merged.c).toBe('3');
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('a follow-up real edit after a merged hydration still pushes a new entry', async () => {
|
|
334
|
-
const store = useHistoryStore();
|
|
335
|
-
|
|
336
|
-
await store.captureBaseline();
|
|
337
|
-
await elapseSettleWindow();
|
|
338
|
-
|
|
339
|
-
// Land a real entry first.
|
|
340
|
-
liveData.v = 100;
|
|
341
|
-
await flushWatcher();
|
|
342
|
-
|
|
343
|
-
expect(store.entries.length).toBe(2);
|
|
344
|
-
|
|
345
|
-
// Hydration burst.
|
|
346
|
-
store.mergeNextMutation();
|
|
347
|
-
liveData.hydrated = 'yes';
|
|
348
|
-
await flushWatcher();
|
|
349
|
-
|
|
350
|
-
expect(store.entries.length).toBe(2);
|
|
351
|
-
|
|
352
|
-
// User makes a real edit — must push.
|
|
353
|
-
liveData.realEdit = 'yes';
|
|
354
|
-
await flushWatcher();
|
|
355
|
-
|
|
356
|
-
expect(store.entries.length).toBe(3);
|
|
357
|
-
expect(store.currentIndex).toBe(2);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('drops a stale merge arm if no mutation follows in the same tick', async () => {
|
|
361
|
-
// Repro for the "first pre-purchase logo edit not captured" bug:
|
|
362
|
-
// a hydration site (e.g. ContentSettings.mounted on tab switch)
|
|
363
|
-
// armed the flag, but the user navigated away before the flagged
|
|
364
|
-
// write happened. A LATER unrelated mutation must NOT inherit
|
|
365
|
-
// the stale arm and merge — it must commit normally.
|
|
366
|
-
const store = useHistoryStore();
|
|
367
|
-
|
|
368
|
-
await store.captureBaseline();
|
|
369
|
-
await elapseSettleWindow();
|
|
370
|
-
|
|
371
|
-
// Land a real entry first.
|
|
372
|
-
liveData.v = 100;
|
|
373
|
-
await flushWatcher();
|
|
374
|
-
|
|
375
|
-
expect(store.entries.length).toBe(2);
|
|
376
|
-
|
|
377
|
-
// Hydration site arms the flag, but no mutation follows in the
|
|
378
|
-
// same tick — caller navigated away.
|
|
379
|
-
store.mergeNextMutation();
|
|
380
|
-
// Advance past the macrotask cleanup, no mutation in between.
|
|
381
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
382
|
-
|
|
383
|
-
// Some time later, a real edit happens.
|
|
384
|
-
liveData.unrelated = 'edit';
|
|
385
|
-
await flushWatcher();
|
|
386
|
-
|
|
387
|
-
// Must push, not merge.
|
|
388
|
-
expect(store.entries.length).toBe(3);
|
|
389
|
-
expect(store.currentIndex).toBe(2);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('clears any armed merge flag during snapshot apply (undo/redo)', async () => {
|
|
393
|
-
const store = useHistoryStore();
|
|
394
|
-
|
|
395
|
-
await store.captureBaseline();
|
|
396
|
-
await elapseSettleWindow();
|
|
397
|
-
|
|
398
|
-
// Land two real entries.
|
|
399
|
-
liveData.v = 100;
|
|
400
|
-
await flushWatcher();
|
|
401
|
-
liveData.v = 200;
|
|
402
|
-
await flushWatcher();
|
|
403
|
-
|
|
404
|
-
expect(store.entries.length).toBe(3);
|
|
405
|
-
|
|
406
|
-
// Hydration site arms the flag right before the user clicks Undo.
|
|
407
|
-
// The flag must NOT survive the apply — otherwise the post-apply
|
|
408
|
-
// watcher tick would silently fold the just-restored snapshot into
|
|
409
|
-
// entries[currentIndex.value], erasing the user's redo.
|
|
410
|
-
store.mergeNextMutation();
|
|
411
|
-
await store.undo();
|
|
412
|
-
|
|
413
|
-
// Drive any pending watcher tick.
|
|
414
|
-
liveData.postApply = 'edit';
|
|
415
|
-
await flushWatcher();
|
|
416
|
-
|
|
417
|
-
// The post-apply edit pushed a new entry — proves the merge flag
|
|
418
|
-
// was cleared, not consumed by the apply tick.
|
|
419
|
-
// (Pointer was at 1 after undo, post-apply edit pushes to 2, length 3.)
|
|
420
|
-
expect(store.entries.length).toBe(3);
|
|
421
|
-
expect(store.currentIndex).toBe(2);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('reset() clears any armed merge flag', async () => {
|
|
425
|
-
const store = useHistoryStore();
|
|
426
|
-
|
|
427
|
-
await store.captureBaseline();
|
|
428
|
-
await elapseSettleWindow();
|
|
429
|
-
|
|
430
|
-
// Land a real entry first so reset has something to reset.
|
|
431
|
-
liveData.v = 100;
|
|
432
|
-
await flushWatcher();
|
|
433
|
-
|
|
434
|
-
store.mergeNextMutation();
|
|
435
|
-
store.reset();
|
|
436
|
-
|
|
437
|
-
// First real mutation after reset must push, not merge.
|
|
438
|
-
await elapseSettleWindow();
|
|
439
|
-
liveData.postReset = 'edit';
|
|
440
|
-
await flushWatcher();
|
|
441
|
-
|
|
442
|
-
expect(store.entries.length).toBe(2);
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('reentrancy guard: capture no-ops when isApplyingSnapshot is true', async () => {
|
|
447
|
-
const store = useHistoryStore();
|
|
448
|
-
|
|
449
|
-
await store.captureBaseline();
|
|
450
|
-
const initialLength = store.entries.length;
|
|
451
|
-
|
|
452
|
-
store.isApplyingSnapshot = true;
|
|
453
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
454
|
-
|
|
455
|
-
expect(store.entries.length).toBe(initialLength);
|
|
456
|
-
store.isApplyingSnapshot = false;
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('canUndo requires currentIndex > 0', async () => {
|
|
460
|
-
const store = useHistoryStore();
|
|
461
|
-
|
|
462
|
-
expect(store.canUndo).toBe(false);
|
|
463
|
-
|
|
464
|
-
await store.captureBaseline();
|
|
465
|
-
expect(store.canUndo).toBe(false); // only baseline
|
|
466
|
-
|
|
467
|
-
captureMutated(store);
|
|
468
|
-
expect(store.canUndo).toBe(true);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it('canRedo requires currentIndex < entries.length - 1', async () => {
|
|
472
|
-
const store = useHistoryStore();
|
|
473
|
-
|
|
474
|
-
expect(store.canRedo).toBe(false);
|
|
475
|
-
|
|
476
|
-
await store.captureBaseline();
|
|
477
|
-
captureMutated(store);
|
|
478
|
-
expect(store.canRedo).toBe(false);
|
|
479
|
-
|
|
480
|
-
await store.undo();
|
|
481
|
-
expect(store.canRedo).toBe(true);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it('undo decrements pointer, keeps stack intact, calls applyHistorySnapshot', async () => {
|
|
485
|
-
const store = useHistoryStore();
|
|
486
|
-
|
|
487
|
-
await store.captureBaseline();
|
|
488
|
-
const baselineBrandPageData = JSON.parse(store.entries[0].snapshot);
|
|
489
|
-
|
|
490
|
-
captureMutated(store);
|
|
491
|
-
|
|
492
|
-
expect(store.currentIndex).toBe(1);
|
|
493
|
-
expect(store.entries.length).toBe(2);
|
|
494
|
-
|
|
495
|
-
await store.undo();
|
|
496
|
-
|
|
497
|
-
expect(store.currentIndex).toBe(0);
|
|
498
|
-
expect(store.entries.length).toBe(2); // stack is unchanged, pointer moved
|
|
499
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledTimes(1);
|
|
500
|
-
// Undo restores the baseline snapshot — the parsed BrandPageModel must
|
|
501
|
-
// round-trip back through applyHistorySnapshot. Second arg is the
|
|
502
|
-
// resolved activePageId (undefined here because no entry has one).
|
|
503
|
-
// Third arg is the captured logoEdits JSON sidecar.
|
|
504
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
505
|
-
expect.objectContaining(baselineBrandPageData),
|
|
506
|
-
undefined,
|
|
507
|
-
expect.any(String),
|
|
508
|
-
);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
it('redo increments pointer, keeps stack intact, calls applyHistorySnapshot', async () => {
|
|
512
|
-
const store = useHistoryStore();
|
|
513
|
-
|
|
514
|
-
await store.captureBaseline();
|
|
515
|
-
captureMutated(store);
|
|
516
|
-
await store.undo();
|
|
517
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
518
|
-
|
|
519
|
-
expect(store.currentIndex).toBe(0);
|
|
520
|
-
expect(store.canRedo).toBe(true);
|
|
521
|
-
|
|
522
|
-
await store.redo();
|
|
523
|
-
|
|
524
|
-
expect(store.currentIndex).toBe(1);
|
|
525
|
-
expect(store.entries.length).toBe(2);
|
|
526
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledTimes(1);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it('baseline invariant: undo refuses to go below index 0', async () => {
|
|
530
|
-
const store = useHistoryStore();
|
|
531
|
-
|
|
532
|
-
await store.captureBaseline();
|
|
533
|
-
|
|
534
|
-
expect(store.canUndo).toBe(false);
|
|
535
|
-
await store.undo();
|
|
536
|
-
|
|
537
|
-
expect(store.currentIndex).toBe(0);
|
|
538
|
-
expect(mockMakerStore.applyHistorySnapshot).not.toHaveBeenCalled();
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it('getCurrentSizeBytes sums byte sizes across all entries', async () => {
|
|
542
|
-
const store = useHistoryStore();
|
|
543
|
-
|
|
544
|
-
expect(store.getCurrentSizeBytes()).toBe(0);
|
|
545
|
-
|
|
546
|
-
await store.captureBaseline();
|
|
547
|
-
const baselineSize = store.getCurrentSizeBytes();
|
|
548
|
-
|
|
549
|
-
expect(baselineSize).toBe(store.entries[0].sizeBytes);
|
|
550
|
-
expect(baselineSize).toBeGreaterThan(0);
|
|
551
|
-
|
|
552
|
-
captureMutated(store);
|
|
553
|
-
expect(store.getCurrentSizeBytes()).toBe(
|
|
554
|
-
store.entries[0].sizeBytes + store.entries[1].sizeBytes,
|
|
555
|
-
);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it('getCurrentEntryCount reflects the stack length', async () => {
|
|
559
|
-
const store = useHistoryStore();
|
|
560
|
-
|
|
561
|
-
expect(store.getCurrentEntryCount()).toBe(0);
|
|
562
|
-
|
|
563
|
-
await store.captureBaseline();
|
|
564
|
-
expect(store.getCurrentEntryCount()).toBe(1);
|
|
565
|
-
|
|
566
|
-
captureMutated(store);
|
|
567
|
-
expect(store.getCurrentEntryCount()).toBe(2);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
// --- U1 finalize (FR-12 / FR-13 / FR-13c) ---
|
|
571
|
-
|
|
572
|
-
it('FR-12: caps the stack at 40 entries, dropping oldest non-baseline', async () => {
|
|
573
|
-
const store = useHistoryStore();
|
|
574
|
-
|
|
575
|
-
await store.captureBaseline();
|
|
576
|
-
const baselineSnapshot = store.entries[0].snapshot;
|
|
577
|
-
|
|
578
|
-
// Baseline + 50 edits — stack should cap at 40.
|
|
579
|
-
for (let i = 0; i < 50; i++) {
|
|
580
|
-
// Mutate the mock so each snapshot is distinguishable.
|
|
581
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
582
|
-
(mockMakerStore.brandPageData as any).v = i + 100;
|
|
583
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
expect(store.entries.length).toBe(40);
|
|
587
|
-
// Baseline is preserved at index 0.
|
|
588
|
-
expect(store.entries[0].snapshot).toBe(baselineSnapshot);
|
|
589
|
-
// Pointer is at the new end.
|
|
590
|
-
expect(store.currentIndex).toBe(39);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
it('FR-13c: skips oversize captures (> 25 MB) and leaves the stack alone', async () => {
|
|
594
|
-
const store = useHistoryStore();
|
|
595
|
-
|
|
596
|
-
await store.captureBaseline();
|
|
597
|
-
const lengthBefore = store.entries.length;
|
|
598
|
-
|
|
599
|
-
// Swap in a mock that produces a > 25 MB snapshot on serialize.
|
|
600
|
-
const originalData = mockMakerStore.brandPageData;
|
|
601
|
-
|
|
602
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
603
|
-
(mockMakerStore as any).brandPageData = {
|
|
604
|
-
v: 1,
|
|
605
|
-
pages: [],
|
|
606
|
-
styles: {},
|
|
607
|
-
_padding: 'x'.repeat(26 * 1024 * 1024),
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
const result = store.capture({ source: 'manual_edit', actor: 'user' });
|
|
611
|
-
|
|
612
|
-
expect(result).toBe('oversized');
|
|
613
|
-
expect(store.entries.length).toBe(lengthBefore);
|
|
614
|
-
|
|
615
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
616
|
-
(mockMakerStore as any).brandPageData = originalData;
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('FR-13: evicts oldest non-baseline when total memory exceeds 100 MB', async () => {
|
|
620
|
-
const store = useHistoryStore();
|
|
621
|
-
|
|
622
|
-
await store.captureBaseline();
|
|
623
|
-
const baselineSnapshot = store.entries[0].snapshot;
|
|
624
|
-
|
|
625
|
-
// Each capture is ~4 MB, under the 5 MB per-entry cap. Push 30
|
|
626
|
-
// of them — total would be ~120 MB, so memory eviction should kick in.
|
|
627
|
-
const originalData = mockMakerStore.brandPageData;
|
|
628
|
-
|
|
629
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
630
|
-
(mockMakerStore as any).brandPageData = {
|
|
631
|
-
v: 1,
|
|
632
|
-
pages: [],
|
|
633
|
-
styles: {},
|
|
634
|
-
_padding: 'x'.repeat(4 * 1024 * 1024),
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
for (let i = 0; i < 30; i++) {
|
|
638
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
expect(store.getCurrentSizeBytes()).toBeLessThanOrEqual(100 * 1024 * 1024);
|
|
642
|
-
// Baseline must survive memory eviction.
|
|
643
|
-
expect(store.entries[0].snapshot).toBe(baselineSnapshot);
|
|
644
|
-
|
|
645
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
646
|
-
(mockMakerStore as any).brandPageData = originalData;
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// --- U5 analytics ---
|
|
650
|
-
|
|
651
|
-
it('emits undo / redo events with entries_count + actor of the affected entry', async () => {
|
|
652
|
-
const received: { name: string; props: unknown }[] = [];
|
|
653
|
-
|
|
654
|
-
useEventTracker().subscribe((event) =>
|
|
655
|
-
received.push({ name: event.eventName, props: event.eventProps }),
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
const store = useHistoryStore();
|
|
659
|
-
|
|
660
|
-
await store.captureBaseline();
|
|
661
|
-
captureMutated(store, { source: 'ai_changeset', actor: 'ai_builder' });
|
|
662
|
-
await store.undo();
|
|
663
|
-
await store.redo();
|
|
664
|
-
|
|
665
|
-
const undoEvent = received.find((e) => e.name === 'website_maker_undo');
|
|
666
|
-
const redoEvent = received.find((e) => e.name === 'website_maker_redo');
|
|
667
|
-
|
|
668
|
-
expect(undoEvent).toBeDefined();
|
|
669
|
-
expect(redoEvent).toBeDefined();
|
|
670
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
671
|
-
expect((undoEvent!.props as any).entries_count).toBe(2);
|
|
672
|
-
// Undo reports the actor of the entry being undone — the ai_builder change.
|
|
673
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
674
|
-
expect((undoEvent!.props as any).actor).toBe('ai_builder');
|
|
675
|
-
// Redo reports the actor of the entry being re-applied — same ai_builder change.
|
|
676
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
677
|
-
expect((redoEvent!.props as any).actor).toBe('ai_builder');
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
it('emits oversized event when a capture exceeds 25 MB', async () => {
|
|
681
|
-
const received: { name: string; props: unknown }[] = [];
|
|
682
|
-
|
|
683
|
-
useEventTracker().subscribe((event) =>
|
|
684
|
-
received.push({ name: event.eventName, props: event.eventProps }),
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
const store = useHistoryStore();
|
|
688
|
-
|
|
689
|
-
await store.captureBaseline();
|
|
690
|
-
|
|
691
|
-
const originalData = mockMakerStore.brandPageData;
|
|
692
|
-
|
|
693
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
694
|
-
(mockMakerStore as any).brandPageData = {
|
|
695
|
-
v: 1,
|
|
696
|
-
pages: [],
|
|
697
|
-
styles: {},
|
|
698
|
-
_padding: 'x'.repeat(26 * 1024 * 1024),
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
store.capture({ source: 'manual_edit', actor: 'user' });
|
|
702
|
-
|
|
703
|
-
const oversized = received.find(
|
|
704
|
-
(e) => e.name === 'website_maker_history_oversized',
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
expect(oversized).toBeDefined();
|
|
708
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
709
|
-
expect((oversized!.props as any).attempted_bytes).toBeGreaterThan(
|
|
710
|
-
25 * 1024 * 1024,
|
|
711
|
-
);
|
|
712
|
-
|
|
713
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
714
|
-
(mockMakerStore as any).brandPageData = originalData;
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// --- BP-5506 (page-tab highlight + active-page tracking) ---
|
|
718
|
-
|
|
719
|
-
describe('activePageId capture and restore', () => {
|
|
720
|
-
it('captures the current activePageId on baseline', async () => {
|
|
721
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
722
|
-
|
|
723
|
-
const store = useHistoryStore();
|
|
724
|
-
|
|
725
|
-
await store.captureBaseline();
|
|
726
|
-
|
|
727
|
-
expect(store.entries[0].activePageId).toBe('page-home');
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
it('captures the current activePageId on each force-commit', async () => {
|
|
731
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
732
|
-
const store = useHistoryStore();
|
|
733
|
-
|
|
734
|
-
await store.captureBaseline();
|
|
735
|
-
|
|
736
|
-
mockPageNavigationStore.activePageId = 'page-2';
|
|
737
|
-
captureMutated(store);
|
|
738
|
-
|
|
739
|
-
expect(store.entries[1].activePageId).toBe('page-2');
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
it('passes the captured activePageId to applyHistorySnapshot when the page still exists', async () => {
|
|
743
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
744
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
745
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
746
|
-
{ id: 'page-home', contents: [] },
|
|
747
|
-
];
|
|
748
|
-
|
|
749
|
-
const store = useHistoryStore();
|
|
750
|
-
|
|
751
|
-
await store.captureBaseline();
|
|
752
|
-
|
|
753
|
-
mockPageNavigationStore.activePageId = 'page-2';
|
|
754
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
755
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
756
|
-
{ id: 'page-home', contents: [] },
|
|
757
|
-
{ id: 'page-2', contents: [] },
|
|
758
|
-
];
|
|
759
|
-
captureMutated(store);
|
|
760
|
-
|
|
761
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
762
|
-
await store.undo();
|
|
763
|
-
|
|
764
|
-
// Baseline restored — its captured page was 'page-home', and that
|
|
765
|
-
// page exists in the restored snapshot, so it round-trips to apply.
|
|
766
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
767
|
-
expect.anything(),
|
|
768
|
-
'page-home',
|
|
769
|
-
expect.any(String),
|
|
770
|
-
);
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
it('walks back when the captured activePageId no longer exists in the restored snapshot (page-add undo)', async () => {
|
|
774
|
-
// Baseline: only page-home, user is on page-home.
|
|
775
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
776
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
777
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
778
|
-
{ id: 'page-home', contents: [] },
|
|
779
|
-
];
|
|
780
|
-
|
|
781
|
-
const store = useHistoryStore();
|
|
782
|
-
|
|
783
|
-
await store.captureBaseline();
|
|
784
|
-
|
|
785
|
-
// User adds a new page. Snapshot now has both pages, user is on the
|
|
786
|
-
// new page.
|
|
787
|
-
mockPageNavigationStore.activePageId = 'page-new';
|
|
788
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
789
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
790
|
-
{ id: 'page-home', contents: [] },
|
|
791
|
-
{ id: 'page-new', contents: [] },
|
|
792
|
-
];
|
|
793
|
-
captureMutated(store);
|
|
794
|
-
|
|
795
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
796
|
-
await store.undo();
|
|
797
|
-
|
|
798
|
-
// Baseline only has 'page-home'. The entry being applied has
|
|
799
|
-
// activePageId='page-home', so we resolve to that.
|
|
800
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
801
|
-
expect.anything(),
|
|
802
|
-
'page-home',
|
|
803
|
-
expect.any(String),
|
|
804
|
-
);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it('navigates to the restored page when undoing a delete (page present in target but not source)', async () => {
|
|
808
|
-
// Baseline: home + about, user is on about (the page that will be deleted).
|
|
809
|
-
mockPageNavigationStore.activePageId = 'page-about';
|
|
810
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
811
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
812
|
-
{ id: 'page-home', contents: [] },
|
|
813
|
-
{ id: 'page-about', contents: [] },
|
|
814
|
-
];
|
|
815
|
-
|
|
816
|
-
const store = useHistoryStore();
|
|
817
|
-
|
|
818
|
-
await store.captureBaseline();
|
|
819
|
-
|
|
820
|
-
// User deletes about. The delete handler navigates them to home,
|
|
821
|
-
// then captures. Snapshot has only home; activePageId on the entry
|
|
822
|
-
// is page-home (not page-about — the user was already off it by
|
|
823
|
-
// the time the watcher fired).
|
|
824
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
825
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
826
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
827
|
-
{ id: 'page-home', contents: [] },
|
|
828
|
-
];
|
|
829
|
-
captureMutated(store);
|
|
830
|
-
|
|
831
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
832
|
-
await store.undo();
|
|
833
|
-
|
|
834
|
-
// Diff resolution: target snapshot (baseline) has page-about,
|
|
835
|
-
// source snapshot (post-delete entry 1) does not. So page-about
|
|
836
|
-
// is being restored — navigate there. Without diff resolution
|
|
837
|
-
// the walk-back would land on page-home (the activePageId on
|
|
838
|
-
// entry 1 and entry 0 both happen to be page-home in this
|
|
839
|
-
// scenario), leaving the user staring at home after un-deleting
|
|
840
|
-
// about.
|
|
841
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
842
|
-
expect.anything(),
|
|
843
|
-
'page-about',
|
|
844
|
-
expect.any(String),
|
|
845
|
-
);
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it('navigates to the newly-added page when redoing an add (page present in target but not source)', async () => {
|
|
849
|
-
// Baseline: only home.
|
|
850
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
851
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
852
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
853
|
-
{ id: 'page-home', contents: [] },
|
|
854
|
-
];
|
|
855
|
-
|
|
856
|
-
const store = useHistoryStore();
|
|
857
|
-
|
|
858
|
-
await store.captureBaseline();
|
|
859
|
-
|
|
860
|
-
// User adds page-new and is navigated there.
|
|
861
|
-
mockPageNavigationStore.activePageId = 'page-new';
|
|
862
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
863
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
864
|
-
{ id: 'page-home', contents: [] },
|
|
865
|
-
{ id: 'page-new', contents: [] },
|
|
866
|
-
];
|
|
867
|
-
captureMutated(store);
|
|
868
|
-
|
|
869
|
-
// Undo the add — back to baseline.
|
|
870
|
-
await store.undo();
|
|
871
|
-
|
|
872
|
-
// Redo the add. Source (baseline) has only home; target (entry 1)
|
|
873
|
-
// has both home and new. The diff identifies page-new as the
|
|
874
|
-
// page being restored by this transition — navigate there.
|
|
875
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
876
|
-
await store.redo();
|
|
877
|
-
|
|
878
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
879
|
-
expect.anything(),
|
|
880
|
-
'page-new',
|
|
881
|
-
expect.any(String),
|
|
882
|
-
);
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
it('falls through to undefined when no entry walking back has a viable id', async () => {
|
|
886
|
-
// Baseline page exists, captured id was 'page-home'.
|
|
887
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
888
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
889
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
890
|
-
{ id: 'page-home', contents: [] },
|
|
891
|
-
];
|
|
892
|
-
|
|
893
|
-
const store = useHistoryStore();
|
|
894
|
-
|
|
895
|
-
await store.captureBaseline();
|
|
896
|
-
|
|
897
|
-
// Snapshot contains a totally different page (e.g. template switch
|
|
898
|
-
// replaced everything) and the user is on its new page.
|
|
899
|
-
mockPageNavigationStore.activePageId = 'page-x';
|
|
900
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
901
|
-
(mockMakerStore.brandPageData as any).pages = [
|
|
902
|
-
{ id: 'page-x', contents: [] },
|
|
903
|
-
];
|
|
904
|
-
captureMutated(store);
|
|
905
|
-
|
|
906
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
907
|
-
await store.undo();
|
|
908
|
-
|
|
909
|
-
// Baseline restored — its activePageId 'page-home' exists in its own
|
|
910
|
-
// snapshot, so resolution finds it on the first walk-back hop.
|
|
911
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
912
|
-
expect.anything(),
|
|
913
|
-
'page-home',
|
|
914
|
-
expect.any(String),
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
// Now redo. Pointer moves forward to entry 1 (page-x captured),
|
|
918
|
-
// page-x exists in entry-1's snapshot, so resolves to 'page-x'.
|
|
919
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
920
|
-
await store.redo();
|
|
921
|
-
expect(mockMakerStore.applyHistorySnapshot).toHaveBeenCalledWith(
|
|
922
|
-
expect.anything(),
|
|
923
|
-
'page-x',
|
|
924
|
-
expect.any(String),
|
|
925
|
-
);
|
|
926
|
-
});
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
describe('reset()', () => {
|
|
930
|
-
it('clears entries and re-baselines against the current state', async () => {
|
|
931
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
932
|
-
|
|
933
|
-
const store = useHistoryStore();
|
|
934
|
-
|
|
935
|
-
await store.captureBaseline();
|
|
936
|
-
captureMutated(store);
|
|
937
|
-
captureMutated(store);
|
|
938
|
-
|
|
939
|
-
expect(store.entries.length).toBe(3);
|
|
940
|
-
|
|
941
|
-
// Mutate the mock so the new baseline reflects the just-saved state.
|
|
942
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
943
|
-
(mockMakerStore.brandPageData as any).v = 9999;
|
|
944
|
-
|
|
945
|
-
store.reset();
|
|
946
|
-
|
|
947
|
-
expect(store.entries.length).toBe(1);
|
|
948
|
-
expect(store.currentIndex).toBe(0);
|
|
949
|
-
expect(store.baselineCaptured).toBe(true);
|
|
950
|
-
expect(JSON.parse(store.entries[0].snapshot).v).toBe(9999);
|
|
951
|
-
expect(store.canUndo).toBe(false);
|
|
952
|
-
expect(store.canRedo).toBe(false);
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it('captures activePageId on the new baseline after reset', async () => {
|
|
956
|
-
mockPageNavigationStore.activePageId = 'page-home';
|
|
957
|
-
|
|
958
|
-
const store = useHistoryStore();
|
|
959
|
-
|
|
960
|
-
await store.captureBaseline();
|
|
961
|
-
captureMutated(store);
|
|
962
|
-
|
|
963
|
-
mockPageNavigationStore.activePageId = 'page-2';
|
|
964
|
-
store.reset();
|
|
965
|
-
|
|
966
|
-
expect(store.entries[0].activePageId).toBe('page-2');
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
it('no-ops when no baseline has ever been captured (unflagged session)', () => {
|
|
970
|
-
// Simulates a session where the undo/redo feature flag is off, so
|
|
971
|
-
// captureBaseline is never called. saveBrandPage calls reset()
|
|
972
|
-
// unconditionally — it must not install the watcher or capture a
|
|
973
|
-
// baseline as a side effect, otherwise unflagged sessions start
|
|
974
|
-
// accumulating snapshots in memory.
|
|
975
|
-
const store = useHistoryStore();
|
|
976
|
-
|
|
977
|
-
expect(store.baselineCaptured).toBe(false);
|
|
978
|
-
expect(store.entries.length).toBe(0);
|
|
979
|
-
|
|
980
|
-
store.reset();
|
|
981
|
-
|
|
982
|
-
expect(store.baselineCaptured).toBe(false);
|
|
983
|
-
expect(store.entries.length).toBe(0);
|
|
984
|
-
expect(store.currentIndex).toBe(-1);
|
|
985
|
-
});
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// BP-5506 follow-up: pre-purchase logo-tab edits (template switch, logo
|
|
989
|
-
// colors, logo text, support text) author through `editableLogoStore.logoEdits`,
|
|
990
|
-
// a sidecar that's NOT in `brandPageData`. The history entry carries a
|
|
991
|
-
// `logoEdits` JSON sidecar — captured at the same instant as the snapshot,
|
|
992
|
-
// applied on undo via `_syncLogoSettingsFromSnapshot`. Without this,
|
|
993
|
-
// save-after-undo would re-derive metadata from the post-edit `logoEdits`
|
|
994
|
-
// and silently reapply the change the user undid.
|
|
995
|
-
describe('logoEdits sidecar capture', () => {
|
|
996
|
-
it('stashes editableLogoStore.logoEdits as a JSON string on baseline', async () => {
|
|
997
|
-
const editableLogoStore = useUiEditableLogoStore();
|
|
998
|
-
|
|
999
|
-
editableLogoStore.logoEdits.textObjects = [
|
|
1000
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1001
|
-
{
|
|
1002
|
-
id: 't1',
|
|
1003
|
-
role: 'primary-text',
|
|
1004
|
-
text: 'Acme',
|
|
1005
|
-
textEnabled: true,
|
|
1006
|
-
} as any,
|
|
1007
|
-
];
|
|
1008
|
-
|
|
1009
|
-
const store = useHistoryStore();
|
|
1010
|
-
|
|
1011
|
-
await store.captureBaseline();
|
|
1012
|
-
|
|
1013
|
-
const stashed = store.entries[0].logoEdits;
|
|
1014
|
-
|
|
1015
|
-
expect(typeof stashed).toBe('string');
|
|
1016
|
-
|
|
1017
|
-
const parsed = JSON.parse(stashed!);
|
|
1018
|
-
|
|
1019
|
-
expect(parsed.textObjects).toEqual([
|
|
1020
|
-
{ id: 't1', role: 'primary-text', text: 'Acme', textEnabled: true },
|
|
1021
|
-
]);
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
it('captures distinct logoEdits per entry as the user edits', async () => {
|
|
1025
|
-
const editableLogoStore = useUiEditableLogoStore();
|
|
1026
|
-
|
|
1027
|
-
editableLogoStore.logoEdits.textObjects = [
|
|
1028
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1029
|
-
{
|
|
1030
|
-
id: 't1',
|
|
1031
|
-
role: 'primary-text',
|
|
1032
|
-
text: 'Acme',
|
|
1033
|
-
textEnabled: true,
|
|
1034
|
-
} as any,
|
|
1035
|
-
];
|
|
1036
|
-
|
|
1037
|
-
const store = useHistoryStore();
|
|
1038
|
-
|
|
1039
|
-
await store.captureBaseline();
|
|
1040
|
-
|
|
1041
|
-
// User types into the logo-text input — `logoEdits.textObjects[0].text`
|
|
1042
|
-
// mutates and `updateEditableLogoImageUrlAsync` writes new base64 into
|
|
1043
|
-
// brandPageData. The base64 mutation is what triggers the watcher; we
|
|
1044
|
-
// simulate by also bumping `v` so the snapshot bytes change.
|
|
1045
|
-
editableLogoStore.logoEdits.textObjects[0].text = 'AcmeCo';
|
|
1046
|
-
captureMutated(store);
|
|
1047
|
-
|
|
1048
|
-
const baselineLogoEdits = JSON.parse(store.entries[0].logoEdits!);
|
|
1049
|
-
const editLogoEdits = JSON.parse(store.entries[1].logoEdits!);
|
|
1050
|
-
|
|
1051
|
-
expect(baselineLogoEdits.textObjects[0].text).toBe('Acme');
|
|
1052
|
-
expect(editLogoEdits.textObjects[0].text).toBe('AcmeCo');
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
it('forwards captured logoEdits to applyHistorySnapshot on undo', async () => {
|
|
1056
|
-
const editableLogoStore = useUiEditableLogoStore();
|
|
1057
|
-
|
|
1058
|
-
editableLogoStore.logoEdits.textObjects = [
|
|
1059
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1060
|
-
{
|
|
1061
|
-
id: 't1',
|
|
1062
|
-
role: 'primary-text',
|
|
1063
|
-
text: 'Acme',
|
|
1064
|
-
textEnabled: true,
|
|
1065
|
-
} as any,
|
|
1066
|
-
];
|
|
1067
|
-
|
|
1068
|
-
const store = useHistoryStore();
|
|
1069
|
-
|
|
1070
|
-
await store.captureBaseline();
|
|
1071
|
-
|
|
1072
|
-
editableLogoStore.logoEdits.textObjects[0].text = 'AcmeCo';
|
|
1073
|
-
captureMutated(store);
|
|
1074
|
-
|
|
1075
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1076
|
-
await store.undo();
|
|
1077
|
-
|
|
1078
|
-
// Third arg is the entry's `logoEdits` — undo should hand the
|
|
1079
|
-
// baseline's pre-edit blob back to the maker store.
|
|
1080
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1081
|
-
const callArgs = mockMakerStore.applyHistorySnapshot.mock
|
|
1082
|
-
.calls[0] as any[];
|
|
1083
|
-
const passedLogoEdits = JSON.parse(callArgs[2]);
|
|
1084
|
-
|
|
1085
|
-
expect(passedLogoEdits.textObjects[0].text).toBe('Acme');
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
it('forwards the post-edit logoEdits on redo', async () => {
|
|
1089
|
-
const editableLogoStore = useUiEditableLogoStore();
|
|
1090
|
-
|
|
1091
|
-
editableLogoStore.logoEdits.textObjects = [
|
|
1092
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1093
|
-
{
|
|
1094
|
-
id: 't1',
|
|
1095
|
-
role: 'primary-text',
|
|
1096
|
-
text: 'Acme',
|
|
1097
|
-
textEnabled: true,
|
|
1098
|
-
} as any,
|
|
1099
|
-
];
|
|
1100
|
-
|
|
1101
|
-
const store = useHistoryStore();
|
|
1102
|
-
|
|
1103
|
-
await store.captureBaseline();
|
|
1104
|
-
|
|
1105
|
-
editableLogoStore.logoEdits.textObjects[0].text = 'AcmeCo';
|
|
1106
|
-
captureMutated(store);
|
|
1107
|
-
|
|
1108
|
-
await store.undo();
|
|
1109
|
-
mockMakerStore.applyHistorySnapshot.mockClear();
|
|
1110
|
-
await store.redo();
|
|
1111
|
-
|
|
1112
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1113
|
-
const callArgs = mockMakerStore.applyHistorySnapshot.mock
|
|
1114
|
-
.calls[0] as any[];
|
|
1115
|
-
const passedLogoEdits = JSON.parse(callArgs[2]);
|
|
1116
|
-
|
|
1117
|
-
expect(passedLogoEdits.textObjects[0].text).toBe('AcmeCo');
|
|
1118
|
-
});
|
|
1119
|
-
});
|
|
1120
|
-
});
|