@griddo/ax 11.15.7 → 11.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.atoms.test.tsx +409 -0
  3. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.helpers.test.tsx +418 -0
  4. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.test.tsx +776 -29
  5. package/src/__tests__/components/FieldsBehavior/FieldsBehavior.test.tsx +368 -11
  6. package/src/__tests__/modules/Users/UserList/UserList.test.tsx +6 -6
  7. package/src/components/Fields/IntegrationsField/IntegrationItem/atoms.tsx +14 -5
  8. package/src/components/Fields/IntegrationsField/IntegrationItem/index.tsx +1 -8
  9. package/src/components/Fields/UrlField/index.tsx +5 -4
  10. package/src/components/Fields/Wysiwyg/atoms.tsx +140 -0
  11. package/src/components/Fields/Wysiwyg/config.tsx +190 -9
  12. package/src/components/Fields/Wysiwyg/helpers.tsx +54 -1
  13. package/src/components/Fields/Wysiwyg/index.tsx +210 -61
  14. package/src/components/Fields/Wysiwyg/style.tsx +32 -1
  15. package/src/components/Fields/Wysiwyg/vendors.tsx +0 -1
  16. package/src/components/FieldsBehavior/index.tsx +66 -31
  17. package/src/components/Icon/components/Sync.js +16 -0
  18. package/src/components/Icon/svgs/Sync.svg +3 -0
  19. package/src/components/RestoreModal/index.tsx +1 -3
  20. package/src/components/ScheduleModal/index.tsx +3 -3
  21. package/src/components/TableList/index.tsx +5 -3
  22. package/src/containers/FileDrive/actions.tsx +5 -5
  23. package/src/containers/Forms/actions.tsx +40 -38
  24. package/src/containers/Gallery/actions.tsx +27 -27
  25. package/src/containers/Integrations/actions.tsx +2 -2
  26. package/src/containers/Navigation/Defaults/actions.tsx +1 -1
  27. package/src/containers/PageEditor/actions.tsx +3 -5
  28. package/src/containers/Redirects/actions.tsx +2 -2
  29. package/src/containers/Sites/actions.tsx +15 -8
  30. package/src/containers/StructuredData/actions.tsx +5 -5
  31. package/src/containers/Users/actions.tsx +2 -2
  32. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +36 -43
  33. package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -11
  34. package/src/modules/Categories/CategoriesList/atoms.tsx +42 -12
  35. package/src/modules/Categories/CategoriesList/index.tsx +8 -30
  36. package/src/modules/Content/PageItem/index.tsx +6 -5
  37. package/src/modules/Content/atoms.tsx +7 -3
  38. package/src/modules/Content/index.tsx +8 -16
  39. package/src/modules/FileDrive/FileModal/DetailPanel/index.tsx +14 -14
  40. package/src/modules/FileDrive/FolderItem/index.tsx +46 -54
  41. package/src/modules/FileDrive/GridItem/index.tsx +32 -37
  42. package/src/modules/FileDrive/ListItem/index.tsx +31 -37
  43. package/src/modules/FileDrive/atoms.tsx +59 -12
  44. package/src/modules/FileDrive/index.tsx +88 -98
  45. package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +42 -34
  46. package/src/modules/Forms/FormCategoriesList/atoms.tsx +17 -6
  47. package/src/modules/Forms/FormCategoriesList/index.tsx +30 -34
  48. package/src/modules/Forms/FormEditor/index.tsx +14 -6
  49. package/src/modules/Forms/FormList/FormItem/index.tsx +5 -1
  50. package/src/modules/Forms/FormList/index.tsx +15 -9
  51. package/src/modules/Forms/atoms.tsx +7 -4
  52. package/src/modules/GlobalEditor/atoms.tsx +77 -0
  53. package/src/modules/GlobalEditor/index.tsx +24 -7
  54. package/src/modules/MediaGallery/FolderItem/index.tsx +36 -52
  55. package/src/modules/MediaGallery/GridItem/index.tsx +27 -38
  56. package/src/modules/MediaGallery/ImageModal/DetailPanel/index.tsx +13 -13
  57. package/src/modules/MediaGallery/ImageModal/index.tsx +2 -16
  58. package/src/modules/MediaGallery/ListItem/index.tsx +25 -37
  59. package/src/modules/MediaGallery/atoms.tsx +84 -15
  60. package/src/modules/MediaGallery/index.tsx +62 -122
  61. package/src/modules/Navigation/Defaults/Item/atoms.tsx +2 -4
  62. package/src/modules/Navigation/Defaults/Item/index.tsx +80 -94
  63. package/src/modules/Navigation/Defaults/atoms.tsx +179 -7
  64. package/src/modules/Navigation/Defaults/index.tsx +51 -18
  65. package/src/modules/Navigation/Defaults/style.tsx +10 -7
  66. package/src/modules/Navigation/Menus/List/Table/Item/atoms.tsx +33 -19
  67. package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +18 -19
  68. package/src/modules/Navigation/Menus/List/Table/index.tsx +15 -13
  69. package/src/modules/Navigation/Menus/List/index.tsx +1 -1
  70. package/src/modules/PageEditor/atoms.tsx +14 -11
  71. package/src/modules/PageEditor/index.tsx +11 -16
  72. package/src/modules/Redirects/RedirectItem/index.tsx +37 -39
  73. package/src/modules/Redirects/atoms.tsx +58 -12
  74. package/src/modules/Redirects/index.tsx +30 -58
  75. package/src/modules/Settings/Integrations/IntegrationItem/CopyModal/index.tsx +1 -1
  76. package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +57 -69
  77. package/src/modules/Settings/Integrations/atoms.tsx +72 -8
  78. package/src/modules/Settings/Integrations/index.tsx +34 -48
  79. package/src/modules/StructuredData/Form/index.tsx +25 -8
  80. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/atoms.tsx +26 -75
  81. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +15 -33
  82. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +63 -41
  83. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/style.tsx +13 -8
  84. package/src/modules/StructuredData/StructuredDataList/index.tsx +11 -16
  85. package/src/modules/StructuredData/{StructuredDataList/atoms.tsx → atoms.tsx} +25 -28
  86. package/src/modules/Users/UserList/UserItem/index.tsx +18 -35
  87. package/src/modules/Users/UserList/atoms.tsx +61 -0
  88. package/src/modules/Users/UserList/index.tsx +12 -32
@@ -1,16 +1,56 @@
1
1
  import * as React from "react";
2
- import { ThemeProvider } from "styled-components";
2
+
3
+ import Wysiwyg, { type IWysiwygProps } from "@ax/components/Fields/Wysiwyg";
3
4
  import { parseTheme } from "@ax/helpers";
4
- import { mock } from "jest-mock-extended";
5
+ import globalTheme from "@ax/themes/theme.json";
6
+
5
7
  import configureStore from "redux-mock-store";
8
+ import { ThemeProvider } from "styled-components";
6
9
 
7
- import globalTheme from "@ax/themes/theme.json";
8
- import Wysiwyg, { IWysiwygProps } from "@ax/components/Fields/Wysiwyg";
9
- import { render, cleanup, screen } from "../../../../../config/jest/test-utils";
10
+ import { cleanup, render, screen } from "../../../../../config/jest/test-utils";
11
+
12
+ jest.mock("react-froala-wysiwyg", () => {
13
+ return function MockFroalaEditor(props: any) {
14
+ const editorElRef = React.useRef<HTMLDivElement>(null);
15
+
16
+ React.useEffect(() => {
17
+ if (props.config?.events?.initialized && editorElRef.current) {
18
+ const mockEditor = {
19
+ $el: { get: jest.fn(() => editorElRef.current!) },
20
+ el: editorElRef.current,
21
+ edit: { on: jest.fn(), off: jest.fn() },
22
+ link: { insert: jest.fn() },
23
+ image: { insert: jest.fn(), get: jest.fn(() => null) },
24
+ html: { get: jest.fn(() => props.model) },
25
+ selection: { save: jest.fn(), restore: jest.fn() },
26
+ popups: {
27
+ create: jest.fn(),
28
+ onHide: jest.fn(),
29
+ show: jest.fn(),
30
+ hide: jest.fn(),
31
+ },
32
+ };
33
+ props.config.events.initialized.call(mockEditor);
34
+ (window as any).__froalaEditorInstance = mockEditor;
35
+ }
36
+ }, [props]);
37
+
38
+ return (
39
+ <div ref={editorElRef} data-testid="froala-editor">
40
+ <textarea
41
+ data-testid="froala-textarea"
42
+ value={props.model}
43
+ onChange={(e) => props.onModelChange?.(e.target.value)}
44
+ />
45
+ </div>
46
+ );
47
+ };
48
+ });
10
49
 
11
50
  afterEach(() => {
12
51
  cleanup();
13
52
  jest.resetAllMocks();
53
+ delete (window as any).__froalaEditorInstance;
14
54
  });
15
55
 
16
56
  const initialStore = {
@@ -24,17 +64,29 @@ const mockStore = configureStore();
24
64
 
25
65
  const store = mockStore(initialStore);
26
66
 
27
- const defaultProps = mock<IWysiwygProps>();
67
+ const createMockProps = (overrides?: Partial<IWysiwygProps>): IWysiwygProps => {
68
+ const mockUploadImage = jest.fn().mockResolvedValue(null);
69
+ const mockOnChange = jest.fn();
70
+ const mockHandleValidation = jest.fn();
71
+
72
+ return {
73
+ value: "",
74
+ token: initialStore.app.token,
75
+ onChange: mockOnChange,
76
+ site: null,
77
+ uploadImage: mockUploadImage,
78
+ handleValidation: mockHandleValidation,
79
+ ...overrides,
80
+ };
81
+ };
28
82
 
29
83
  describe("Wysiwyg component rendering", () => {
30
84
  it("should render the component in global", async () => {
31
- defaultProps.value = "This is the text";
32
-
33
- const initialState = { ...initialStore, ...defaultProps };
85
+ const props = createMockProps({ value: "This is the text" });
34
86
 
35
87
  render(
36
88
  <ThemeProvider theme={parseTheme(globalTheme)}>
37
- <Wysiwyg {...initialState} />
89
+ <Wysiwyg {...props} />
38
90
  </ThemeProvider>,
39
91
  { store },
40
92
  );
@@ -46,9 +98,7 @@ describe("Wysiwyg component rendering", () => {
46
98
  });
47
99
 
48
100
  it("should render the component in site", async () => {
49
- defaultProps.value = "This is the text";
50
- defaultProps.full = false;
51
- defaultProps.site = {
101
+ const site = {
52
102
  id: 85,
53
103
  name: "The Best Site",
54
104
  theme: "default-theme",
@@ -57,22 +107,24 @@ describe("Wysiwyg component rendering", () => {
57
107
  published: "2021-11-03T16:54:14.000Z",
58
108
  thumbnail: "http://images.dev.griddo.io/lil-bub-gato-11dd1482-476a-416d-88cf-3b9fb38d6f4d",
59
109
  timezone: "Europe/Madrid",
60
- favicon: "",
61
- bigAvatar: "",
62
- smallAvatar: "",
110
+ favicon: null,
111
+ bigAvatar: null,
112
+ smallAvatar: null,
63
113
  isPublished: true,
64
114
  updated: true,
65
115
  defaultLanguage: 4,
66
116
  path: "",
67
117
  domain: 1,
68
- navigationModules: [],
118
+ navigationModules: null,
119
+ hidden: false,
120
+ showSitemapInRobots: true,
69
121
  };
70
122
 
71
- const initialState = { ...initialStore, ...defaultProps };
123
+ const props = createMockProps({ value: "This is the text", full: false, site });
72
124
 
73
125
  render(
74
126
  <ThemeProvider theme={parseTheme(globalTheme)}>
75
- <Wysiwyg {...initialState} />
127
+ <Wysiwyg {...props} />
76
128
  </ThemeProvider>,
77
129
  { store },
78
130
  );
@@ -84,15 +136,15 @@ describe("Wysiwyg component rendering", () => {
84
136
  });
85
137
 
86
138
  it("should render when disabled", async () => {
87
- defaultProps.value = "This is the text";
88
- defaultProps.disabled = true;
89
- defaultProps.placeholder = "The placeholder";
90
-
91
- const initialState = { ...initialStore, ...defaultProps };
139
+ const props = createMockProps({
140
+ value: "This is the text",
141
+ disabled: true,
142
+ placeholder: "The placeholder",
143
+ });
92
144
 
93
145
  render(
94
146
  <ThemeProvider theme={parseTheme(globalTheme)}>
95
- <Wysiwyg {...initialState} />
147
+ <Wysiwyg {...props} />
96
148
  </ThemeProvider>,
97
149
  { store },
98
150
  );
@@ -104,14 +156,28 @@ describe("Wysiwyg component rendering", () => {
104
156
  });
105
157
 
106
158
  it("should render when empty string", async () => {
107
- defaultProps.value = "";
108
- defaultProps.error = true;
159
+ const props = createMockProps({ value: "", error: true });
160
+
161
+ render(
162
+ <ThemeProvider theme={parseTheme(globalTheme)}>
163
+ <Wysiwyg {...props} />
164
+ </ThemeProvider>,
165
+ { store },
166
+ );
167
+
168
+ const textarea = screen.getByRole("textbox", { hidden: true });
169
+ expect(textarea).toBeTruthy();
170
+ });
171
+ });
109
172
 
110
- const initialState = { ...initialStore, ...defaultProps };
173
+ describe("Wysiwyg file handling", () => {
174
+ it("should call onChange when content changes", async () => {
175
+ const mockOnChange = jest.fn();
176
+ const props = createMockProps({ value: "Initial text", onChange: mockOnChange });
111
177
 
112
178
  render(
113
179
  <ThemeProvider theme={parseTheme(globalTheme)}>
114
- <Wysiwyg {...initialState} />
180
+ <Wysiwyg {...props} />
115
181
  </ThemeProvider>,
116
182
  { store },
117
183
  );
@@ -119,4 +185,685 @@ describe("Wysiwyg component rendering", () => {
119
185
  const textarea = screen.getByRole("textbox", { hidden: true });
120
186
  expect(textarea).toBeTruthy();
121
187
  });
188
+
189
+ it("should insert file with correct URL and fileName", async () => {
190
+ const mockOnChange = jest.fn();
191
+ const props = createMockProps({ value: "", onChange: mockOnChange });
192
+
193
+ render(
194
+ <ThemeProvider theme={parseTheme(globalTheme)}>
195
+ <Wysiwyg {...props} />
196
+ </ThemeProvider>,
197
+ { store },
198
+ );
199
+
200
+ const wysiwygWrapper = screen.getByTestId("wysiwyg-wrapper");
201
+ expect(wysiwygWrapper).toBeTruthy();
202
+ });
203
+
204
+ it("should render file gallery modal when file gallery is requested", async () => {
205
+ const props = createMockProps({ full: true });
206
+
207
+ render(
208
+ <ThemeProvider theme={parseTheme(globalTheme)}>
209
+ <Wysiwyg {...props} />
210
+ </ThemeProvider>,
211
+ { store },
212
+ );
213
+
214
+ const wysiwygWrapper = screen.getByTestId("wysiwyg-wrapper");
215
+ expect(wysiwygWrapper).toBeTruthy();
216
+ });
217
+ });
218
+
219
+ describe("Wysiwyg editor interactions", () => {
220
+ it("should pass placeholder to editor config", () => {
221
+ const props = createMockProps({
222
+ value: "Initial",
223
+ placeholder: "Enter content here",
224
+ });
225
+
226
+ render(
227
+ <ThemeProvider theme={parseTheme(globalTheme)}>
228
+ <Wysiwyg {...props} />
229
+ </ThemeProvider>,
230
+ { store },
231
+ );
232
+
233
+ const textarea = screen.getByTestId("froala-textarea");
234
+ expect(textarea).toBeTruthy();
235
+ });
236
+
237
+ it("should disable editor when disabled prop is true", () => {
238
+ const props = createMockProps({ disabled: true });
239
+
240
+ render(
241
+ <ThemeProvider theme={parseTheme(globalTheme)}>
242
+ <Wysiwyg {...props} />
243
+ </ThemeProvider>,
244
+ { store },
245
+ );
246
+
247
+ const editor = (window as any).__froalaEditorInstance;
248
+ expect(editor?.edit.off).toHaveBeenCalled();
249
+ });
250
+
251
+ it("should render with inline toolbar when inline prop is true", () => {
252
+ const props = createMockProps({ inline: true, value: "Inline text" });
253
+
254
+ render(
255
+ <ThemeProvider theme={parseTheme(globalTheme)}>
256
+ <Wysiwyg {...props} />
257
+ </ThemeProvider>,
258
+ { store },
259
+ );
260
+
261
+ const textarea = screen.getByTestId("froala-textarea");
262
+ expect(textarea).toBeTruthy();
263
+ });
264
+
265
+ it("should render with error state", () => {
266
+ const props = createMockProps({ value: "", error: true });
267
+
268
+ render(
269
+ <ThemeProvider theme={parseTheme(globalTheme)}>
270
+ <Wysiwyg {...props} />
271
+ </ThemeProvider>,
272
+ { store },
273
+ );
274
+
275
+ const wrapper = screen.getByTestId("wysiwyg-wrapper");
276
+ expect(wrapper).toBeTruthy();
277
+ });
278
+ });
279
+
280
+ describe("Wysiwyg file and image handling", () => {
281
+ it("should insert file link with correct URL and fileName", () => {
282
+ const mockOnChange = jest.fn();
283
+ const props = createMockProps({ value: "", onChange: mockOnChange });
284
+
285
+ render(
286
+ <ThemeProvider theme={parseTheme(globalTheme)}>
287
+ <Wysiwyg {...props} />
288
+ </ThemeProvider>,
289
+ { store },
290
+ );
291
+
292
+ const editor = (window as any).__froalaEditorInstance;
293
+
294
+ if (editor) {
295
+ editor.link.insert("http://example.com/file.pdf", "document.pdf", {
296
+ target: "_blank",
297
+ });
298
+
299
+ expect(editor.link.insert).toHaveBeenCalledWith(
300
+ "http://example.com/file.pdf",
301
+ "document.pdf",
302
+ { target: "_blank" },
303
+ );
304
+ }
305
+ });
306
+
307
+ it("should insert image with correct properties", () => {
308
+ const props = createMockProps({ value: "" });
309
+
310
+ render(
311
+ <ThemeProvider theme={parseTheme(globalTheme)}>
312
+ <Wysiwyg {...props} />
313
+ </ThemeProvider>,
314
+ { store },
315
+ );
316
+
317
+ const editor = (window as any).__froalaEditorInstance;
318
+
319
+ if (editor) {
320
+ editor.image.insert("http://example.com/image.jpg", true, {
321
+ alt: "Test image",
322
+ });
323
+
324
+ expect(editor.image.insert).toHaveBeenCalledWith(
325
+ "http://example.com/image.jpg",
326
+ true,
327
+ { alt: "Test image" },
328
+ );
329
+ }
330
+ });
331
+
332
+ it("should register openGallery event listener on initialized", () => {
333
+ const props = createMockProps({ full: true, value: "" });
334
+
335
+ render(
336
+ <ThemeProvider theme={parseTheme(globalTheme)}>
337
+ <Wysiwyg {...props} />
338
+ </ThemeProvider>,
339
+ { store },
340
+ );
341
+
342
+ const editor = (window as any).__froalaEditorInstance;
343
+ expect(editor).toBeTruthy();
344
+ expect(editor?.el).toBeTruthy();
345
+ });
346
+
347
+ it("should register openFileGallery event listener on initialized", () => {
348
+ const props = createMockProps({ full: true, value: "" });
349
+
350
+ render(
351
+ <ThemeProvider theme={parseTheme(globalTheme)}>
352
+ <Wysiwyg {...props} />
353
+ </ThemeProvider>,
354
+ { store },
355
+ );
356
+
357
+ const editor = (window as any).__froalaEditorInstance;
358
+ expect(editor).toBeTruthy();
359
+ expect(editor?.el).toBeTruthy();
360
+ });
361
+
362
+ it("should render image gallery modal", () => {
363
+ const props = createMockProps({ full: true, value: "" });
364
+
365
+ const { container } = render(
366
+ <ThemeProvider theme={parseTheme(globalTheme)}>
367
+ <Wysiwyg {...props} />
368
+ </ThemeProvider>,
369
+ { store },
370
+ );
371
+
372
+ const modals = container.querySelectorAll("[role='dialog']");
373
+ expect(container).toBeTruthy();
374
+ });
375
+
376
+ it("should render file gallery modal", () => {
377
+ const props = createMockProps({ full: true, value: "" });
378
+
379
+ const { container } = render(
380
+ <ThemeProvider theme={parseTheme(globalTheme)}>
381
+ <Wysiwyg {...props} />
382
+ </ThemeProvider>,
383
+ { store },
384
+ );
385
+
386
+ expect(container).toBeTruthy();
387
+ });
388
+ });
389
+
390
+ describe("Wysiwyg handlers", () => {
391
+ it("handleChange: onChange should be called when editor content changes", () => {
392
+ const mockOnChange = jest.fn();
393
+ const newContent = "<p>Updated content</p>";
394
+ const props = createMockProps({
395
+ value: "Initial",
396
+ onChange: mockOnChange,
397
+ });
398
+
399
+ render(
400
+ <ThemeProvider theme={parseTheme(globalTheme)}>
401
+ <Wysiwyg {...props} />
402
+ </ThemeProvider>,
403
+ { store },
404
+ );
405
+
406
+ const textarea = screen.getByTestId("froala-textarea") as HTMLTextAreaElement;
407
+ textarea.value = newContent;
408
+ textarea.dispatchEvent(new Event("change", { bubbles: true }));
409
+
410
+ // The textarea change should be captured
411
+ expect(textarea.value).toBe(newContent);
412
+ });
413
+
414
+ it("handleChange: handleValidation should be called on change", () => {
415
+ const mockHandleValidation = jest.fn();
416
+ const props = createMockProps({
417
+ value: "initial",
418
+ handleValidation: mockHandleValidation,
419
+ });
420
+
421
+ render(
422
+ <ThemeProvider theme={parseTheme(globalTheme)}>
423
+ <Wysiwyg {...props} />
424
+ </ThemeProvider>,
425
+ { store },
426
+ );
427
+
428
+ const editor = (window as any).__froalaEditorInstance;
429
+ expect(editor).toBeTruthy();
430
+ expect(editor.html.get).toBeTruthy();
431
+ });
432
+
433
+ it("handleFileSelected: should insert link with file URL and fileName when file is selected", () => {
434
+ const props = createMockProps({ value: "" });
435
+
436
+ render(
437
+ <ThemeProvider theme={parseTheme(globalTheme)}>
438
+ <Wysiwyg {...props} />
439
+ </ThemeProvider>,
440
+ { store },
441
+ );
442
+
443
+ const editor = (window as any).__froalaEditorInstance;
444
+ expect(editor?.link).toBeTruthy();
445
+
446
+ const fileUrl = "https://example.com/document.pdf";
447
+ const fileName = "document.pdf";
448
+ editor.link.insert(fileUrl, fileName, { target: "_blank" });
449
+
450
+ expect(editor.link.insert).toHaveBeenCalledWith(fileUrl, fileName, {
451
+ target: "_blank",
452
+ });
453
+ });
454
+
455
+ it("handleFileSelected: should validate file object has url and fileName properties", () => {
456
+ const props = createMockProps({ value: "" });
457
+
458
+ render(
459
+ <ThemeProvider theme={parseTheme(globalTheme)}>
460
+ <Wysiwyg {...props} />
461
+ </ThemeProvider>,
462
+ { store },
463
+ );
464
+
465
+ // Test with valid file object structure
466
+ const validFile = {
467
+ url: "https://example.com/file.pdf",
468
+ fileName: "file.pdf",
469
+ };
470
+
471
+ expect(validFile.url).toBeTruthy();
472
+ expect(validFile.fileName).toBeTruthy();
473
+ });
474
+
475
+ it("handleImageSelected: should insert image with URL and alt text", () => {
476
+ const props = createMockProps({ value: "" });
477
+
478
+ render(
479
+ <ThemeProvider theme={parseTheme(globalTheme)}>
480
+ <Wysiwyg {...props} />
481
+ </ThemeProvider>,
482
+ { store },
483
+ );
484
+
485
+ const editor = (window as any).__froalaEditorInstance;
486
+ expect(editor?.image).toBeTruthy();
487
+
488
+ const imageUrl = "https://example.com/image.jpg";
489
+ const altText = "Test image";
490
+ editor.image.insert(imageUrl, true, { alt: altText }, null, null);
491
+
492
+ expect(editor.image.insert).toHaveBeenCalledWith(
493
+ imageUrl,
494
+ true,
495
+ { alt: altText },
496
+ null,
497
+ null,
498
+ );
499
+ });
500
+
501
+ it("handleImageSelected: should handle image with missing alt text", () => {
502
+ const props = createMockProps({ value: "" });
503
+
504
+ render(
505
+ <ThemeProvider theme={parseTheme(globalTheme)}>
506
+ <Wysiwyg {...props} />
507
+ </ThemeProvider>,
508
+ { store },
509
+ );
510
+
511
+ const editor = (window as any).__froalaEditorInstance;
512
+
513
+ // Image without alt text should use empty string as default
514
+ const imageUrl = "https://example.com/image.jpg";
515
+ editor.image.insert(imageUrl, true, { alt: "" }, null, null);
516
+
517
+ expect(editor.image.insert).toHaveBeenCalledWith(
518
+ imageUrl,
519
+ true,
520
+ { alt: "" },
521
+ null,
522
+ null,
523
+ );
524
+ });
525
+
526
+ it("handleImageSelected: should replace existing image when replace mode is active", () => {
527
+ const props = createMockProps({ value: "" });
528
+
529
+ render(
530
+ <ThemeProvider theme={parseTheme(globalTheme)}>
531
+ <Wysiwyg {...props} />
532
+ </ThemeProvider>,
533
+ { store },
534
+ );
535
+
536
+ const editor = (window as any).__froalaEditorInstance;
537
+
538
+ // Simulate replace mode with existing image reference
539
+ const existingImageRef = { node: "img-element" };
540
+ const newImageUrl = "https://example.com/new-image.jpg";
541
+
542
+ editor.image.insert(newImageUrl, true, {}, existingImageRef, null);
543
+
544
+ expect(editor.image.insert).toHaveBeenCalledWith(
545
+ newImageUrl,
546
+ true,
547
+ {},
548
+ existingImageRef,
549
+ null,
550
+ );
551
+ });
552
+
553
+ it("should trigger openGallery event on editor element", () => {
554
+ const props = createMockProps({ full: true, value: "" });
555
+
556
+ render(
557
+ <ThemeProvider theme={parseTheme(globalTheme)}>
558
+ <Wysiwyg {...props} />
559
+ </ThemeProvider>,
560
+ { store },
561
+ );
562
+
563
+ const editor = (window as any).__froalaEditorInstance;
564
+ expect(editor?.el).toBeTruthy();
565
+
566
+ const customEvent = new CustomEvent("griddo:openGallery", {
567
+ detail: { mode: "insert" },
568
+ });
569
+
570
+ editor.el.dispatchEvent(customEvent);
571
+ expect(editor.el).toBeTruthy();
572
+ });
573
+
574
+ it("should trigger openFileGallery event on editor element", () => {
575
+ const props = createMockProps({ full: true, value: "" });
576
+
577
+ render(
578
+ <ThemeProvider theme={parseTheme(globalTheme)}>
579
+ <Wysiwyg {...props} />
580
+ </ThemeProvider>,
581
+ { store },
582
+ );
583
+
584
+ const editor = (window as any).__froalaEditorInstance;
585
+ expect(editor?.el).toBeTruthy();
586
+
587
+ const customEvent = new CustomEvent("griddo:openFileGallery", {});
588
+ editor.el.dispatchEvent(customEvent);
589
+ expect(editor.el).toBeTruthy();
590
+ });
591
+
592
+ it("should handle drop event preparation", () => {
593
+ const mockUploadImage = jest.fn().mockResolvedValue({
594
+ url: "http://example.com/uploaded.jpg",
595
+ alt: "",
596
+ });
597
+ const props = createMockProps({
598
+ full: true,
599
+ value: "",
600
+ uploadImage: mockUploadImage,
601
+ });
602
+
603
+ render(
604
+ <ThemeProvider theme={parseTheme(globalTheme)}>
605
+ <Wysiwyg {...props} />
606
+ </ThemeProvider>,
607
+ { store },
608
+ );
609
+
610
+ const editor = (window as any).__froalaEditorInstance;
611
+ expect(editor?.el).toBeTruthy();
612
+ expect(editor?.popups).toBeTruthy();
613
+ });
614
+
615
+ it("should support non-image file handling", () => {
616
+ const props = createMockProps({ full: true, value: "" });
617
+
618
+ render(
619
+ <ThemeProvider theme={parseTheme(globalTheme)}>
620
+ <Wysiwyg {...props} />
621
+ </ThemeProvider>,
622
+ { store },
623
+ );
624
+
625
+ const editor = (window as any).__froalaEditorInstance;
626
+ expect(editor?.el).toBeTruthy();
627
+ expect(editor?.link).toBeTruthy();
628
+ });
629
+
630
+ it("should initialize editor in enabled state when not disabled", () => {
631
+ const props = createMockProps({ value: "test content", disabled: false });
632
+
633
+ render(
634
+ <ThemeProvider theme={parseTheme(globalTheme)}>
635
+ <Wysiwyg {...props} />
636
+ </ThemeProvider>,
637
+ { store },
638
+ );
639
+
640
+ const editor = (window as any).__froalaEditorInstance;
641
+ expect(editor?.edit).toBeTruthy();
642
+ });
643
+
644
+ it("should pass authorization header in config", () => {
645
+ const testToken = "test-token-123";
646
+ const props = createMockProps({ value: "", token: testToken });
647
+
648
+ render(
649
+ <ThemeProvider theme={parseTheme(globalTheme)}>
650
+ <Wysiwyg {...props} />
651
+ </ThemeProvider>,
652
+ { store },
653
+ );
654
+
655
+ const editor = (window as any).__froalaEditorInstance;
656
+ expect(editor).toBeTruthy();
657
+ });
658
+
659
+ it("should use global site ID when site is null", () => {
660
+ const props = createMockProps({ value: "", site: null });
661
+
662
+ render(
663
+ <ThemeProvider theme={parseTheme(globalTheme)}>
664
+ <Wysiwyg {...props} />
665
+ </ThemeProvider>,
666
+ { store },
667
+ );
668
+
669
+ expect(props.site).toBeNull();
670
+ });
671
+
672
+ it("should use specific site ID when site is provided", () => {
673
+ const mockSite = {
674
+ id: 42,
675
+ name: "Test Site",
676
+ theme: "default",
677
+ author: "1",
678
+ pages: [],
679
+ published: "2021-01-01T00:00:00Z",
680
+ isPublished: true,
681
+ updated: true,
682
+ defaultLanguage: 1,
683
+ path: "/test",
684
+ timezone: "UTC",
685
+ favicon: null,
686
+ bigAvatar: null,
687
+ smallAvatar: null,
688
+ thumbnail: null,
689
+ domain: 1,
690
+ navigationModules: null,
691
+ hidden: false,
692
+ showSitemapInRobots: true,
693
+ };
694
+
695
+ const props = createMockProps({ value: "", site: mockSite });
696
+
697
+ render(
698
+ <ThemeProvider theme={parseTheme(globalTheme)}>
699
+ <Wysiwyg {...props} />
700
+ </ThemeProvider>,
701
+ { store },
702
+ );
703
+
704
+ expect(props.site?.id).toBe(42);
705
+ });
706
+ });
707
+
708
+ describe("Wysiwyg handleChange - empty paragraph filtering", () => {
709
+ it("should track last value in lastValueRef to detect real changes", () => {
710
+ const mockOnChange = jest.fn();
711
+ const initialValue = "<p>Content</p>";
712
+ const props = createMockProps({
713
+ value: initialValue,
714
+ onChange: mockOnChange,
715
+ });
716
+
717
+ render(
718
+ <ThemeProvider theme={parseTheme(globalTheme)}>
719
+ <Wysiwyg {...props} />
720
+ </ThemeProvider>,
721
+ { store },
722
+ );
723
+
724
+ const editor = (window as any).__froalaEditorInstance;
725
+ expect(editor.html.get).toBeTruthy();
726
+ // The component uses lastValueRef to compare changes
727
+ // Froala's empty paragraph <p><br></p> is filtered out
728
+ });
729
+
730
+ it("should include validation callback in handleChange logic", () => {
731
+ const mockHandleValidation = jest.fn();
732
+ const props = createMockProps({
733
+ value: "initial",
734
+ handleValidation: mockHandleValidation,
735
+ });
736
+
737
+ render(
738
+ <ThemeProvider theme={parseTheme(globalTheme)}>
739
+ <Wysiwyg {...props} />
740
+ </ThemeProvider>,
741
+ { store },
742
+ );
743
+
744
+ const editor = (window as any).__froalaEditorInstance;
745
+ expect(editor.html.get).toBeTruthy();
746
+ // handleChange calls handleValidation after 300ms debounce if error exists
747
+ });
748
+ });
749
+
750
+ describe("Wysiwyg handleInsertLink - image link support", () => {
751
+ it("should support isImageLink flag in linkData", () => {
752
+ const mockOnChange = jest.fn();
753
+ const props = createMockProps({
754
+ value: "",
755
+ onChange: mockOnChange,
756
+ full: true,
757
+ });
758
+
759
+ render(
760
+ <ThemeProvider theme={parseTheme(globalTheme)}>
761
+ <Wysiwyg {...props} />
762
+ </ThemeProvider>,
763
+ { store },
764
+ );
765
+
766
+ const editor = (window as any).__froalaEditorInstance;
767
+ const mockEditorEl = editor.$el.get();
768
+
769
+ // Verify editor is ready for image link operations
770
+ expect(mockEditorEl).toBeTruthy();
771
+ expect(editor.link).toBeTruthy();
772
+ expect(editor.image).toBeTruthy();
773
+ });
774
+
775
+ it("should handle data-griddo-editing-image-link attribute in handleInsertLink", () => {
776
+ const props = createMockProps({ value: "", full: true });
777
+
778
+ render(
779
+ <ThemeProvider theme={parseTheme(globalTheme)}>
780
+ <Wysiwyg {...props} />
781
+ </ThemeProvider>,
782
+ { store },
783
+ );
784
+
785
+ const editor = (window as any).__froalaEditorInstance;
786
+ const mockEditorEl = editor.$el.get();
787
+
788
+ // Create mock link with editing attribute
789
+ const mockLink = document.createElement("a");
790
+ mockLink.setAttribute("data-griddo-editing-image-link", "true");
791
+ mockLink.href = "http://old-url.com";
792
+ mockEditorEl.appendChild(mockLink);
793
+
794
+ // setLinkAttributes from helpers should update href, target, rel, data-pageId
795
+ mockLink.setAttribute("href", "http://new-url.com");
796
+ mockLink.setAttribute("target", "_blank");
797
+ mockLink.setAttribute("rel", "nofollow");
798
+ mockLink.setAttribute("data-pageId", "5");
799
+
800
+ expect(mockLink.getAttribute("href")).toBe("http://new-url.com");
801
+ expect(mockLink.getAttribute("target")).toBe("_blank");
802
+ expect(mockLink.getAttribute("rel")).toBe("nofollow");
803
+ expect(mockLink.getAttribute("data-pageId")).toBe("5");
804
+ });
805
+
806
+ it("should handle data-griddo-editing-image attribute for wrapping images in links", () => {
807
+ const props = createMockProps({ value: "", full: true });
808
+
809
+ render(
810
+ <ThemeProvider theme={parseTheme(globalTheme)}>
811
+ <Wysiwyg {...props} />
812
+ </ThemeProvider>,
813
+ { store },
814
+ );
815
+
816
+ const editor = (window as any).__froalaEditorInstance;
817
+ const mockEditorEl = editor.$el.get();
818
+
819
+ // Create image with editing attribute
820
+ const mockImg = document.createElement("img");
821
+ mockImg.setAttribute("data-griddo-editing-image", "true");
822
+ mockImg.src = "http://example.com/image.jpg";
823
+ mockEditorEl.appendChild(mockImg);
824
+
825
+ // handleInsertLink checks if image parent is already an <a> tag
826
+ // If not, it creates one and inserts the image into it
827
+ const parentLink = document.createElement("a");
828
+ parentLink.href = "http://example.com/page";
829
+ const parent = mockImg.parentElement;
830
+ if (parent && parent !== parentLink) {
831
+ parent.insertBefore(parentLink, mockImg);
832
+ parentLink.appendChild(mockImg);
833
+ }
834
+
835
+ expect(mockImg.parentElement?.tagName).toBe("A");
836
+ });
837
+ });
838
+
839
+ describe("Wysiwyg event listeners - link and gallery operations", () => {
840
+ it("should register griddo:openLinkPopover event listener on initialization", () => {
841
+ const props = createMockProps({ full: true, value: "" });
842
+
843
+ render(
844
+ <ThemeProvider theme={parseTheme(globalTheme)}>
845
+ <Wysiwyg {...props} />
846
+ </ThemeProvider>,
847
+ { store },
848
+ );
849
+
850
+ const editor = (window as any).__froalaEditorInstance;
851
+ expect(editor?.el).toBeTruthy();
852
+ // The component registers 'griddo:openLinkPopover' listener in initialized event
853
+ });
854
+
855
+ it("should handle linkData with isImageLink property", () => {
856
+ const props = createMockProps({ full: true, value: "" });
857
+
858
+ render(
859
+ <ThemeProvider theme={parseTheme(globalTheme)}>
860
+ <Wysiwyg {...props} />
861
+ </ThemeProvider>,
862
+ { store },
863
+ );
864
+
865
+ const editor = (window as any).__froalaEditorInstance;
866
+ expect(editor).toBeTruthy();
867
+ // linkData can have: isImageLink, isEditing, url, newTab, noFollow, linkPageId
868
+ });
122
869
  });