Skip to content

Commit d854138

Browse files
Merge pull request #329 from contentstack/VE-4802
test: add unit tests for error handling and field state validation in visual builder
2 parents 8fcdcc5 + ba0062e commit d854138

6 files changed

Lines changed: 493 additions & 57 deletions

File tree

src/visualBuilder/__test__/visualBuilderInput.test.tsx

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -661,59 +661,4 @@ describe("When an inline element is edited in visual builder mode", () => {
661661
});
662662
});
663663
});
664-
describe.skip("number field", () => {
665-
let numberField: HTMLParagraphElement;
666-
let visualBuilder: VisualBuilder;
667-
let overlayWrapper: HTMLDivElement;
668-
669-
beforeAll(() => {
670-
(visualBuilderPostMessage?.send as Mock).mockImplementation(
671-
(eventName: string, args) => {
672-
if (
673-
eventName ===
674-
VisualBuilderPostMessageEvents.GET_FIELD_DATA
675-
) {
676-
return Promise.resolve({
677-
fieldData: "87",
678-
});
679-
}
680-
return Promise.resolve({});
681-
}
682-
);
683-
});
684-
685-
beforeEach(() => {
686-
numberField = document.createElement("p");
687-
numberField.setAttribute(
688-
"data-cslp",
689-
"all_fields.bltapikey.en-us.number"
690-
);
691-
numberField.textContent = "87";
692-
document.body.appendChild(numberField);
693-
visualBuilder = new VisualBuilder();
694-
overlayWrapper = document.querySelector(
695-
".visual-builder__overlay__wrapper"
696-
) as HTMLDivElement;
697-
});
698-
699-
afterEach(() => {
700-
visualBuilder.destroy();
701-
});
702-
703-
test("should only accept characters like a number input", async () => {
704-
await userEvent.click(numberField);
705-
await waitFor(() => {
706-
expect(numberField).toHaveAttribute("contenteditable");
707-
});
708-
await userEvent.keyboard("ab56c78e-h10");
709-
710-
await waitFor(() => {
711-
// TODO cursor is placed at the start, instead of the end
712-
// so typed content appears at the start
713-
expect(numberField).toHaveTextContent(`5678e-1087`);
714-
});
715-
});
716-
}, {
717-
timeout: 20 * 1000
718-
});
719664
});
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: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
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+
import userEvent from "@testing-library/user-event";
9+
import { waitFor, screen } from "@testing-library/preact";
10+
11+
vi.mock("lodash-es", async () => ({
12+
...(await import("lodash-es")),
13+
throttle: vi.fn((fn) => fn),
14+
}))
615
describe("handle numeric field key down", () => {
716
let h1: HTMLHeadingElement;
817
let spiedPreventDefault: MockInstance<(e: []) => void> | undefined;
@@ -106,6 +115,16 @@ describe("handle numeric field key down", () => {
106115
h1.dispatchEvent(keyDownEvent);
107116
expect(spiedPreventDefault).toHaveBeenCalledTimes(1);
108117
});
118+
119+
test("should only accept characters like a number input", async () => {
120+
h1.innerHTML = '';
121+
await userEvent.click(h1);
122+
await userEvent.keyboard("ab56c78e-h10");
123+
124+
await waitFor(() => {
125+
expect(h1).toHaveTextContent(`5678e-10`);
126+
});
127+
});
109128
});
110129

111130
describe("handle keydown in button contenteditable", () => {
@@ -152,3 +171,85 @@ describe("handle keydown in button contenteditable", () => {
152171
expect(spiedInsertSpaceAtCursor).toHaveBeenCalledWith(button);
153172
});
154173
});
174+
175+
describe("handle single line field key down", () => {
176+
let h1: HTMLHeadingElement;
177+
let spiedPreventDefault: MockInstance<(e: []) => void> | undefined;
178+
179+
beforeEach(() => {
180+
h1 = document.createElement("h1");
181+
h1.innerHTML = "2.2";
182+
h1.setAttribute("contenteditable", "true");
183+
h1.setAttribute(VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY, FieldDataType.SINGLELINE);
184+
185+
h1.addEventListener("keydown", (e) => {
186+
spiedPreventDefault = vi.spyOn(e, "preventDefault");
187+
handleFieldKeyDown(e);
188+
});
189+
190+
document.body.appendChild(h1);
191+
});
192+
193+
afterEach(() => {
194+
document.body.innerHTML = "";
195+
vi.clearAllMocks();
196+
});
197+
198+
afterAll(() => {
199+
vi.restoreAllMocks();
200+
});
201+
202+
test("should prevent default on enter key", () => {
203+
const keyDownEvent = new KeyboardEvent("keydown", {
204+
bubbles: true,
205+
key: "Enter",
206+
code: "Enter",
207+
});
208+
h1.dispatchEvent(keyDownEvent);
209+
210+
expect(spiedPreventDefault).toHaveBeenCalledTimes(1);
211+
});
212+
})
213+
214+
describe("`handleFieldInput`", () => {
215+
let h1: HTMLHeadingElement;
216+
let visualBuilderContainer: HTMLElement;
217+
beforeEach(() => {
218+
h1 = document.createElement("h1");
219+
h1.innerHTML = "2.2";
220+
h1.setAttribute("contenteditable", "true");
221+
h1.setAttribute(VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY, "number");
222+
223+
h1.addEventListener("input", handleFieldInput);
224+
225+
visualBuilderContainer = document.createElement("div");
226+
visualBuilderContainer.classList.add("visual-builder__container");
227+
document.body.appendChild(visualBuilderContainer);
228+
document.body.appendChild(h1);
229+
});
230+
231+
afterEach(() => {
232+
document.body.innerHTML = "";
233+
vi.clearAllMocks();
234+
});
235+
236+
afterAll(() => {
237+
vi.restoreAllMocks();
238+
});
239+
240+
test("should call `sendFieldEvent` on input event", () => {
241+
const spiedSendFieldEvent = vi.spyOn(generateOverlay, "sendFieldEvent")
242+
const consoleError = vi.spyOn(console, "error");
243+
spiedSendFieldEvent.mockImplementation(() => {
244+
throw new Error("sendFieldEvent not implemented")
245+
});
246+
const inputEvent = new InputEvent("input", {
247+
bubbles: true,
248+
});
249+
h1.dispatchEvent(inputEvent);
250+
251+
expect(spiedSendFieldEvent).toHaveBeenCalledWith({ visualBuilderContainer, eventType: VisualBuilderPostMessageEvents.SYNC_FIELD });
252+
expect(consoleError).toHaveBeenCalled();
253+
});
254+
255+
})
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)