Skip to content

Commit c53a2b8

Browse files
test: add unit tests for error handling and field state validation in visual builder
1 parent a2d4397 commit c53a2b8

5 files changed

Lines changed: 481 additions & 2 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { hasPostMessageError } from '../errorHandling';
2+
3+
describe('hasPostMessageError', () => {
4+
it('should return true if obj.error is true', () => {
5+
const obj = { error: true };
6+
expect(hasPostMessageError(obj)).toBe(true);
7+
});
8+
9+
it('should return false if obj.error is false', () => {
10+
const obj = { error: false };
11+
expect(hasPostMessageError(obj)).toBe(false);
12+
});
13+
14+
it('should return false if obj.error is undefined', () => {
15+
const obj = {};
16+
expect(hasPostMessageError(obj)).toBe(false);
17+
});
18+
19+
it('should return false if obj is null', () => {
20+
const obj = null;
21+
expect(hasPostMessageError(obj)).toBe(false);
22+
});
23+
24+
it('should return false if obj is not an object', () => {
25+
const obj = 'not an object';
26+
expect(hasPostMessageError(obj)).toBe(false);
27+
});
28+
});

src/visualBuilder/utils/__test__/handleFieldMouseDown.test.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { MockInstance } from "vitest";
22
import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "../constants";
3-
import { handleFieldKeyDown } from "../handleFieldMouseDown";
3+
import { handleFieldInput, handleFieldKeyDown } from "../handleFieldMouseDown";
44
import * as insertSpaceAtCursor from "../insertSpaceAtCursor";
5-
5+
import * as generateOverlay from "../../generators/generateOverlay";
6+
import { VisualBuilderPostMessageEvents } from "../types/postMessage.types";
7+
import { FieldDataType } from "../types/index.types";
8+
9+
vi.mock("lodash-es", async () => ({
10+
...(await import("lodash-es")),
11+
throttle: vi.fn((fn) => fn),
12+
}))
613
describe("handle numeric field key down", () => {
714
let h1: HTMLHeadingElement;
815
let spiedPreventDefault: MockInstance<(e: []) => void> | undefined;
@@ -152,3 +159,85 @@ describe("handle keydown in button contenteditable", () => {
152159
expect(spiedInsertSpaceAtCursor).toHaveBeenCalledWith(button);
153160
});
154161
});
162+
163+
describe("handle single line field key down", () => {
164+
let h1: HTMLHeadingElement;
165+
let spiedPreventDefault: MockInstance<(e: []) => void> | undefined;
166+
167+
beforeEach(() => {
168+
h1 = document.createElement("h1");
169+
h1.innerHTML = "2.2";
170+
h1.setAttribute("contenteditable", "true");
171+
h1.setAttribute(VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY, FieldDataType.SINGLELINE);
172+
173+
h1.addEventListener("keydown", (e) => {
174+
spiedPreventDefault = vi.spyOn(e, "preventDefault");
175+
handleFieldKeyDown(e);
176+
});
177+
178+
document.body.appendChild(h1);
179+
});
180+
181+
afterEach(() => {
182+
document.body.innerHTML = "";
183+
vi.clearAllMocks();
184+
});
185+
186+
afterAll(() => {
187+
vi.restoreAllMocks();
188+
});
189+
190+
test("should prevent default on enter key", () => {
191+
const keyDownEvent = new KeyboardEvent("keydown", {
192+
bubbles: true,
193+
key: "Enter",
194+
code: "Enter",
195+
});
196+
h1.dispatchEvent(keyDownEvent);
197+
198+
expect(spiedPreventDefault).toHaveBeenCalledTimes(1);
199+
});
200+
})
201+
202+
describe("`handleFieldInput`", () => {
203+
let h1: HTMLHeadingElement;
204+
let visualBuilderContainer: HTMLElement;
205+
beforeEach(() => {
206+
h1 = document.createElement("h1");
207+
h1.innerHTML = "2.2";
208+
h1.setAttribute("contenteditable", "true");
209+
h1.setAttribute(VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY, "number");
210+
211+
h1.addEventListener("input", handleFieldInput);
212+
213+
visualBuilderContainer = document.createElement("div");
214+
visualBuilderContainer.classList.add("visual-builder__container");
215+
document.body.appendChild(visualBuilderContainer);
216+
document.body.appendChild(h1);
217+
});
218+
219+
afterEach(() => {
220+
document.body.innerHTML = "";
221+
vi.clearAllMocks();
222+
});
223+
224+
afterAll(() => {
225+
vi.restoreAllMocks();
226+
});
227+
228+
test("should call `sendFieldEvent` on input event", () => {
229+
const spiedSendFieldEvent = vi.spyOn(generateOverlay, "sendFieldEvent")
230+
const consoleError = vi.spyOn(console, "error");
231+
spiedSendFieldEvent.mockImplementation(() => {
232+
throw new Error("sendFieldEvent not implemented")
233+
});
234+
const inputEvent = new InputEvent("input", {
235+
bubbles: true,
236+
});
237+
h1.dispatchEvent(inputEvent);
238+
239+
expect(spiedSendFieldEvent).toHaveBeenCalledWith({ visualBuilderContainer, eventType: VisualBuilderPostMessageEvents.SYNC_FIELD });
240+
expect(consoleError).toHaveBeenCalled();
241+
});
242+
243+
})
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { isFieldDisabled } from '../isFieldDisabled';
3+
import { ISchemaFieldMap } from '../types/index.types';
4+
import { FieldDetails } from '../../components/FieldToolbar';
5+
import Config from '../../../configManager/configManager';
6+
import { VisualBuilder } from '../..';
7+
8+
describe('isFieldDisabled', () => {
9+
it('should return disabled state due to read-only role', () => {
10+
const fieldSchemaMap: ISchemaFieldMap = {
11+
field_metadata: {
12+
updateRestrict: true,
13+
},
14+
};
15+
const eventFieldDetails: FieldDetails = {
16+
editableElement: document.createElement('div'),
17+
fieldMetadata: {
18+
locale: 'en-us',
19+
},
20+
};
21+
22+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
23+
expect(result.isDisabled).toBe(true);
24+
expect(result.reason).toBe('You have only read access to this field');
25+
});
26+
27+
it('should return disabled state due to non-localizable fields', () => {
28+
Config.get = () => ({
29+
stackDetails: {
30+
masterLocale: 'en-us',
31+
},
32+
});
33+
34+
const fieldSchemaMap: ISchemaFieldMap = {
35+
non_localizable: true,
36+
};
37+
const eventFieldDetails: FieldDetails = {
38+
editableElement: document.createElement('div'),
39+
fieldMetadata: {
40+
locale: 'fr-fr',
41+
},
42+
};
43+
44+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
45+
expect(result.isDisabled).toBe(true);
46+
expect(result.reason).toBe('Editing this field is restricted in localized entries');
47+
});
48+
49+
it('should return disabled state due to unlinked variant', () => {
50+
const fieldSchemaMap: ISchemaFieldMap = {
51+
field_metadata: {
52+
isUnlinkedVariant: true,
53+
},
54+
};
55+
const eventFieldDetails: FieldDetails = {
56+
editableElement: document.createElement('div'),
57+
fieldMetadata: {
58+
locale: 'en-us',
59+
},
60+
};
61+
62+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
63+
expect(result.isDisabled).toBe(true);
64+
expect(result.reason).toBe('This field is not editable as it is not linked to the selected variant');
65+
});
66+
67+
it('should return disabled state due to unlocalized variant', () => {
68+
VisualBuilder.VisualBuilderGlobalState = {
69+
value: {
70+
locale: 'en-us',
71+
variant: true,
72+
},
73+
};
74+
75+
const fieldSchemaMap: ISchemaFieldMap = {};
76+
const eventFieldDetails: FieldDetails = {
77+
editableElement: document.createElement('div'),
78+
fieldMetadata: {
79+
locale: 'fr-fr',
80+
},
81+
};
82+
83+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
84+
expect(result.isDisabled).toBe(true);
85+
expect(result.reason).toBe('This field is not editable as it is not localized');
86+
});
87+
88+
it('should return disabled state due to audience mode', () => {
89+
VisualBuilder.VisualBuilderGlobalState = {
90+
value: {
91+
audienceMode: true,
92+
},
93+
};
94+
95+
const fieldSchemaMap: ISchemaFieldMap = {};
96+
const eventFieldDetails: FieldDetails = {
97+
editableElement: document.createElement('div'),
98+
fieldMetadata: {
99+
locale: 'en-us',
100+
},
101+
};
102+
103+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
104+
expect(result.isDisabled).toBe(true);
105+
expect(result.reason).toBe('Open an Experience from Audience widget to start editing');
106+
});
107+
108+
it('should return disabled state due to disabled variant', () => {
109+
VisualBuilder.VisualBuilderGlobalState = {
110+
value: {
111+
audienceMode: true,
112+
},
113+
};
114+
115+
const fieldSchemaMap: ISchemaFieldMap = {};
116+
const eventFieldDetails: FieldDetails = {
117+
editableElement: document.createElement('div'),
118+
fieldMetadata: {
119+
locale: 'en-us',
120+
},
121+
};
122+
eventFieldDetails.editableElement.classList.add('visual-builder__disabled-variant-field');
123+
124+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
125+
expect(result.isDisabled).toBe(true);
126+
expect(result.reason).toBe('This field is not editable as it doesn\'t match the selected variant');
127+
VisualBuilder.VisualBuilderGlobalState = {
128+
value: {
129+
audienceMode: false,
130+
},
131+
};
132+
});
133+
134+
it('should return enabled state when no restrictions apply', () => {
135+
const fieldSchemaMap: ISchemaFieldMap = {};
136+
const eventFieldDetails: FieldDetails = {
137+
editableElement: document.createElement('div'),
138+
fieldMetadata: {
139+
locale: 'en-us',
140+
},
141+
};
142+
143+
const result = isFieldDisabled(fieldSchemaMap, eventFieldDetails);
144+
expect(result.isDisabled).toBe(false);
145+
expect(result.reason).toBe('');
146+
});
147+
});

0 commit comments

Comments
 (0)