@griddo/ax 11.11.8-rc.1 → 11.12.0

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 (99) hide show
  1. package/config/jest/componentsMock.js +5 -7
  2. package/package.json +2 -2
  3. package/src/__tests__/components/Browser/Browser.test.tsx +87 -438
  4. package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +3 -1
  5. package/src/__tests__/components/Fields/Button/Button.test.tsx +27 -29
  6. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
  7. package/src/components/Browser/index.tsx +149 -294
  8. package/src/components/Browser/style.tsx +6 -75
  9. package/src/components/Button/index.tsx +1 -2
  10. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +4 -2
  11. package/src/components/Fields/AsyncSelect/style.tsx +0 -13
  12. package/src/components/Fields/FieldGroup/index.tsx +2 -5
  13. package/src/components/Fields/FieldGroup/style.tsx +7 -32
  14. package/src/components/Fields/HeadingField/index.tsx +2 -2
  15. package/src/components/Fields/HiddenField/style.tsx +1 -1
  16. package/src/components/Fields/NumberField/index.tsx +16 -15
  17. package/src/components/Fields/NumberField/style.tsx +0 -2
  18. package/src/components/Fields/ReferenceField/index.tsx +1 -1
  19. package/src/components/Fields/Select/index.tsx +1 -5
  20. package/src/components/Fields/Select/style.tsx +0 -56
  21. package/src/components/Fields/SummaryButton/index.tsx +9 -18
  22. package/src/components/Fields/SummaryButton/style.tsx +2 -1
  23. package/src/components/Fields/TagsField/index.tsx +9 -8
  24. package/src/components/Fields/UrlField/index.tsx +27 -26
  25. package/src/components/Fields/index.tsx +0 -2
  26. package/src/components/FloatingPanel/index.tsx +2 -5
  27. package/src/components/FloatingPanel/style.tsx +1 -2
  28. package/src/components/IconAction/index.tsx +1 -1
  29. package/src/components/MainWrapper/AppBar/index.tsx +1 -8
  30. package/src/components/MainWrapper/index.tsx +1 -7
  31. package/src/components/Notification/index.tsx +2 -2
  32. package/src/components/PageFinder/index.tsx +1 -1
  33. package/src/components/ResizePanel/index.tsx +3 -4
  34. package/src/components/ResizePanel/style.tsx +1 -1
  35. package/src/components/SearchField/style.tsx +2 -2
  36. package/src/components/SideModal/index.tsx +1 -2
  37. package/src/components/Tabs/index.tsx +4 -13
  38. package/src/components/Tabs/style.tsx +8 -7
  39. package/src/components/Toast/index.tsx +2 -4
  40. package/src/components/Tooltip/index.tsx +3 -4
  41. package/src/components/index.tsx +0 -8
  42. package/src/forms/fields.tsx +68 -70
  43. package/src/hooks/forms.tsx +1 -22
  44. package/src/hooks/index.tsx +3 -13
  45. package/src/hooks/modals.tsx +15 -103
  46. package/src/hooks/users.tsx +8 -25
  47. package/src/modules/Forms/atoms.tsx +2 -2
  48. package/src/modules/FramePreview/index.tsx +16 -55
  49. package/src/modules/FramePreview/style.tsx +2 -34
  50. package/src/modules/GlobalEditor/Editor/index.tsx +3 -37
  51. package/src/modules/GlobalEditor/PageBrowser/index.tsx +2 -19
  52. package/src/modules/GlobalEditor/Preview/index.tsx +2 -0
  53. package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
  54. package/src/modules/GlobalEditor/index.tsx +57 -119
  55. package/src/modules/PageEditor/Editor/index.tsx +2 -33
  56. package/src/modules/PageEditor/PageBrowser/index.tsx +2 -20
  57. package/src/modules/PageEditor/Preview/index.tsx +2 -0
  58. package/src/modules/PageEditor/Preview/style.tsx +1 -1
  59. package/src/modules/PageEditor/atoms.tsx +1 -1
  60. package/src/modules/PageEditor/index.tsx +66 -130
  61. package/src/modules/PublicPreview/index.tsx +2 -5
  62. package/src/schemas/pages/GlobalPage.ts +70 -87
  63. package/src/schemas/pages/Page.ts +70 -87
  64. package/src/types/index.tsx +0 -12
  65. package/src/__tests__/components/Browser/Browser.utils.test.ts +0 -55
  66. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +0 -158
  67. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +0 -90
  68. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +0 -178
  69. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +0 -150
  70. package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +0 -91
  71. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +0 -122
  72. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +0 -15
  73. package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +0 -101
  74. package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +0 -318
  75. package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +0 -242
  76. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +0 -185
  77. package/src/components/Browser/utils.tsx +0 -13
  78. package/src/components/Fields/SEOPreview/index.tsx +0 -36
  79. package/src/components/Fields/SEOPreview/style.tsx +0 -24
  80. package/src/components/FloatingNote/index.tsx +0 -35
  81. package/src/components/FloatingNote/style.tsx +0 -26
  82. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +0 -85
  83. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +0 -80
  84. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +0 -57
  85. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +0 -82
  86. package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +0 -71
  87. package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +0 -77
  88. package/src/components/HeadingsPreviewModal/index.tsx +0 -146
  89. package/src/components/HeadingsPreviewModal/style.tsx +0 -82
  90. package/src/components/HeadingsPreviewModal/utils.tsx +0 -257
  91. package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +0 -46
  92. package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +0 -64
  93. package/src/components/KeywordsPreviewModal/atoms.tsx +0 -96
  94. package/src/components/KeywordsPreviewModal/index.tsx +0 -99
  95. package/src/components/KeywordsPreviewModal/style.tsx +0 -87
  96. package/src/components/KeywordsPreviewModal/utils.tsx +0 -22
  97. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +0 -113
  98. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +0 -24
  99. package/src/modules/FramePreview/utils.tsx +0 -140
@@ -1,318 +0,0 @@
1
- import { parseTheme } from "@ax/helpers";
2
- import * as hooks from "@ax/hooks";
3
- import FramePreview from "@ax/modules/FramePreview";
4
- import * as frameUtils from "@ax/modules/FramePreview/utils";
5
- import globalTheme from "@ax/themes/theme.json";
6
-
7
- import "@testing-library/jest-dom";
8
- import configureStore from "redux-mock-store";
9
- import thunk from "redux-thunk";
10
- import { ThemeProvider } from "styled-components";
11
-
12
- import { act, cleanup, render, screen } from "../../../../config/jest/test-utils";
13
-
14
- // Capture BrowserContent props so individual callback functions can be tested
15
- const capturedBrowserContentProps: { current: Record<string, any> | null } = { current: null };
16
-
17
- jest.mock("@ax/hooks", () => ({
18
- ...jest.requireActual("@ax/hooks"),
19
- useURLSearchParam: jest.fn().mockReturnValue(null),
20
- useOnMessageReceivedFromOutside: jest.fn(),
21
- useToast: jest.fn().mockReturnValue({
22
- isVisible: false,
23
- toggleToast: jest.fn(),
24
- setIsVisible: jest.fn(),
25
- state: "",
26
- }),
27
- }));
28
-
29
- jest.mock("@ax/components", () => {
30
- const React = require("react");
31
- return {
32
- BrowserContent: (props: any) => {
33
- capturedBrowserContentProps.current = props;
34
- return React.createElement("div", { "data-testid": "browser-content" });
35
- },
36
- Loading: () => React.createElement("div", { "data-testid": "loading-wrapper" }),
37
- Toast: () => React.createElement("div", { "data-testid": "toast-wrapper" }),
38
- };
39
- });
40
-
41
- jest.mock("@ax/modules/FramePreview/utils", () => ({
42
- observeAndAddIdsToHeadings: jest.fn().mockReturnValue(jest.fn()),
43
- observeAndHighlightKeywords: jest.fn().mockReturnValue(jest.fn()),
44
- }));
45
-
46
- jest.mock("@ax/forms", () => ({
47
- findByEditorID: jest.fn().mockReturnValue({
48
- element: { component: "Page", editorID: 5 },
49
- parent: { parentEditorID: 0 },
50
- }),
51
- }));
52
-
53
- // Mock action creators as plain functions to avoid thunk side-effects
54
- jest.mock("@ax/containers/PageEditor", () => ({
55
- pageEditorActions: {
56
- setSelectedContent: (id: number) => ({ type: "SET_SELECTED_CONTENT", payload: id }),
57
- setEditorContent: (content: any) => ({ type: "SET_EDITOR_CONTENT", payload: content }),
58
- deleteModule: (ids: number[]) => ({ type: "DELETE_MODULE", payload: ids }),
59
- duplicateModule: (ids: number[]) => ({ type: "DUPLICATE_MODULE", payload: ids }),
60
- copyModule: (ids: number[]) => ({ type: "COPY_MODULE", payload: ids }),
61
- },
62
- }));
63
-
64
- jest.mock("@ax/containers/Forms", () => ({
65
- formsActions: {
66
- setSelectedContent: (id: number) => ({ type: "SET_FORM_SELECTED_CONTENT", payload: id }),
67
- setFormContent: (content: any) => ({ type: "SET_FORM_CONTENT", payload: content }),
68
- },
69
- }));
70
-
71
- const middlewares: any = [thunk];
72
- const mockStore = configureStore(middlewares);
73
-
74
- const buildStore = (appOverrides = {}) =>
75
- mockStore({
76
- pageEditor: { editorContent: {} },
77
- forms: { formContent: {} },
78
- social: {},
79
- app: {
80
- globalSettings: { cloudinaryName: null },
81
- globalLangs: [],
82
- isLoading: false,
83
- ...appOverrides,
84
- },
85
- sites: { currentSiteInfo: null, currentSiteLanguages: [] },
86
- });
87
-
88
- const renderFramePreview = async (store = buildStore()) => {
89
- await act(async () => {
90
- render(
91
- <ThemeProvider theme={parseTheme(globalTheme)}>
92
- <FramePreview />
93
- </ThemeProvider>,
94
- { store },
95
- );
96
- });
97
- };
98
-
99
- afterEach(() => {
100
- cleanup();
101
- capturedBrowserContentProps.current = null;
102
- jest.restoreAllMocks();
103
- });
104
-
105
- describe("FramePreview rendering", () => {
106
- it("should render Loading when isLoading is true from Redux state", async () => {
107
- await renderFramePreview(buildStore({ isLoading: true }));
108
-
109
- expect(screen.getByTestId("loading-wrapper")).toBeTruthy();
110
- });
111
-
112
- it("should render BrowserContent when not loading", async () => {
113
- await renderFramePreview();
114
-
115
- expect(screen.getByTestId("browser-content")).toBeTruthy();
116
- });
117
-
118
- it("should not render HeadingsOverlay when type is not 'headings'", async () => {
119
- await renderFramePreview();
120
-
121
- expect(screen.queryByTestId("headings-overlay")).toBeNull();
122
- });
123
-
124
- it("should render HeadingsOverlay when type is 'headings'", async () => {
125
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
126
- if (param === "type") return "headings";
127
- return null;
128
- });
129
-
130
- await renderFramePreview();
131
-
132
- expect(screen.getByTestId("headings-overlay")).toBeTruthy();
133
- });
134
- });
135
-
136
- describe("FramePreview effects", () => {
137
- it("should post iframe-mousemove message to parent on window mousemove", async () => {
138
- const postMessageSpy = jest.spyOn(window, "postMessage");
139
-
140
- await renderFramePreview();
141
-
142
- act(() => {
143
- window.dispatchEvent(new MouseEvent("mousemove", { clientX: 150 }));
144
- });
145
-
146
- expect(postMessageSpy).toHaveBeenCalledWith(
147
- expect.objectContaining({ type: "iframe-mousemove", clientX: 150 }),
148
- "*",
149
- );
150
- });
151
-
152
- it("should set selectedID to '0' in localStorage when disabled param is true", async () => {
153
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
154
- if (param === "disabled") return "true";
155
- return null;
156
- });
157
-
158
- await renderFramePreview();
159
-
160
- expect(localStorage.getItem("selectedID")).toBe("0");
161
- });
162
-
163
- it("should call observeAndAddIdsToHeadings when type is 'headings'", async () => {
164
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
165
- if (param === "type") return "headings";
166
- return null;
167
- });
168
-
169
- await renderFramePreview();
170
-
171
- expect(frameUtils.observeAndAddIdsToHeadings).toHaveBeenCalledWith(document.body, null);
172
- });
173
-
174
- it("should call observeAndHighlightKeywords when type is 'keywords'", async () => {
175
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
176
- if (param === "type") return "keywords";
177
- return null;
178
- });
179
-
180
- await renderFramePreview();
181
-
182
- expect(frameUtils.observeAndHighlightKeywords).toHaveBeenCalledWith(document.body, []);
183
- });
184
- });
185
-
186
- describe("FramePreview module actions", () => {
187
- it("should post module-delete and dispatch deleteModule when deleteModuleAction is called", async () => {
188
- const postMessageSpy = jest.spyOn(window, "postMessage");
189
-
190
- await renderFramePreview();
191
-
192
- capturedBrowserContentProps.current!.moduleActions.deleteModuleAction(1);
193
-
194
- expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-delete", message: 1 }, "*");
195
- });
196
-
197
- it("should post module-duplicate when duplicateModuleAction is called", async () => {
198
- const postMessageSpy = jest.spyOn(window, "postMessage");
199
-
200
- await renderFramePreview();
201
-
202
- capturedBrowserContentProps.current!.moduleActions.duplicateModuleAction(2);
203
-
204
- expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-duplicate", message: 2 }, "*");
205
- });
206
-
207
- it("should post module-copy and call toggleToast when copyModuleAction is called", async () => {
208
- const toggleToastMock = jest.fn();
209
- (hooks.useToast as jest.Mock).mockReturnValue({
210
- isVisible: false,
211
- toggleToast: toggleToastMock,
212
- setIsVisible: jest.fn(),
213
- state: "",
214
- });
215
- const postMessageSpy = jest.spyOn(window, "postMessage");
216
-
217
- await renderFramePreview();
218
-
219
- capturedBrowserContentProps.current!.moduleActions.copyModuleAction(3);
220
-
221
- expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-copy", message: 3 }, "*");
222
- expect(toggleToastMock).toHaveBeenCalledWith("1 module copied to clipboard");
223
- });
224
- });
225
-
226
- describe("FramePreview editor ID selection", () => {
227
- it("should post module-click when selectEditorID is called", async () => {
228
- const postMessageSpy = jest.spyOn(window, "postMessage");
229
-
230
- await renderFramePreview();
231
-
232
- const { selectEditorID } = capturedBrowserContentProps.current!;
233
- const mockEvent = { stopPropagation: jest.fn() };
234
- selectEditorID({ editorID: 1, component: "Page", type: "content", parentEditorID: 0 }, null, mockEvent);
235
-
236
- expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-click", message: 1 }, "*");
237
- });
238
-
239
- it("should post module-scroll when selectHoverEditorID is called and parent meets condition", async () => {
240
- const postMessageSpy = jest.spyOn(window, "postMessage");
241
-
242
- await renderFramePreview();
243
-
244
- const { selectHoverEditorID } = capturedBrowserContentProps.current!;
245
- selectHoverEditorID(5, 0);
246
-
247
- expect(postMessageSpy).toHaveBeenCalledWith({ type: "module-scroll", message: 5 }, "*");
248
- });
249
-
250
- it("should not post module-click when preview mode is active", async () => {
251
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
252
- if (param === "preview") return "true";
253
- return null;
254
- });
255
- const postMessageSpy = jest.spyOn(window, "postMessage");
256
-
257
- await renderFramePreview();
258
-
259
- const { selectEditorID } = capturedBrowserContentProps.current!;
260
- const mockEvent = { stopPropagation: jest.fn() };
261
- selectEditorID({ editorID: 1, component: "Page", type: "content", parentEditorID: 0 }, null, mockEvent);
262
-
263
- expect(postMessageSpy).not.toHaveBeenCalledWith(expect.objectContaining({ type: "module-click" }), "*");
264
- });
265
- });
266
-
267
- describe("FramePreview content modes", () => {
268
- it("should render Toast when isVisible is true", async () => {
269
- (hooks.useToast as jest.Mock).mockReturnValueOnce({
270
- isVisible: true,
271
- toggleToast: jest.fn(),
272
- setIsVisible: jest.fn(),
273
- state: "1 module copied to clipboard",
274
- });
275
-
276
- await renderFramePreview();
277
-
278
- expect(screen.getByTestId("toast-wrapper")).toBeTruthy();
279
- });
280
-
281
- it("should use 'preview' renderer when preview param is true", async () => {
282
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
283
- if (param === "preview") return "true";
284
- return null;
285
- });
286
-
287
- await renderFramePreview();
288
-
289
- expect(capturedBrowserContentProps.current?.renderer).toBe("preview");
290
- });
291
-
292
- it("should pass filtered keywords to observeAndHighlightKeywords when keywordFilter param is set", async () => {
293
- (hooks.useURLSearchParam as jest.Mock).mockImplementation((param: string) => {
294
- if (param === "type") return "keywords";
295
- if (param === "keywordFilter") return "seo";
296
- return null;
297
- });
298
-
299
- await renderFramePreview();
300
-
301
- expect(frameUtils.observeAndHighlightKeywords).toHaveBeenCalledWith(document.body, ["seo"]);
302
- });
303
-
304
- it("should use currentSiteInfo theme and langs when currentSiteInfo is provided", async () => {
305
- const store = mockStore({
306
- pageEditor: { editorContent: {} },
307
- forms: { formContent: {} },
308
- social: {},
309
- app: { globalSettings: { cloudinaryName: null }, globalLangs: [], isLoading: false },
310
- sites: { currentSiteInfo: { id: 42, theme: "custom-theme" }, currentSiteLanguages: [{ id: 1 }] },
311
- });
312
-
313
- await renderFramePreview(store);
314
-
315
- expect(capturedBrowserContentProps.current?.theme).toBe("custom-theme");
316
- expect(capturedBrowserContentProps.current?.siteID).toBe(42);
317
- });
318
- });
@@ -1,242 +0,0 @@
1
- import {
2
- addIdsToHeadings,
3
- highlightKeywords,
4
- removeKeywordHighlights,
5
- observeAndAddIdsToHeadings,
6
- observeAndHighlightKeywords,
7
- KEYWORD_HIGHLIGHT_CLASS,
8
- } from "@ax/modules/FramePreview/utils";
9
-
10
- describe("addIdsToHeadings", () => {
11
- it("should add data-griddoid to all heading elements when no filter is given", () => {
12
- const container = document.createElement("div");
13
- container.innerHTML = "<h1>Title</h1><h2>Subtitle</h2><h3>Section</h3>";
14
-
15
- addIdsToHeadings(container, null);
16
-
17
- const h1 = container.querySelector("h1") as HTMLElement;
18
- const h2 = container.querySelector("h2") as HTMLElement;
19
- const h3 = container.querySelector("h3") as HTMLElement;
20
-
21
- expect(h1.dataset.griddoid).toBe("heading-1");
22
- expect(h2.dataset.griddoid).toBe("heading-2");
23
- expect(h3.dataset.griddoid).toBe("heading-3");
24
- });
25
-
26
- it("should add data-griddoid only to filtered heading type", () => {
27
- const container = document.createElement("div");
28
- container.innerHTML = "<h1>Title</h1><h2>Subtitle</h2>";
29
-
30
- addIdsToHeadings(container, "h1");
31
-
32
- const h1 = container.querySelector("h1") as HTMLElement;
33
- const h2 = container.querySelector("h2") as HTMLElement;
34
-
35
- expect(h1.dataset.griddoid).toBe("heading-1");
36
- expect(h2.dataset.griddoid).toBeUndefined();
37
- });
38
-
39
- it("should do nothing if no elements match the selector", () => {
40
- const container = document.createElement("div");
41
- container.innerHTML = "<p>No headings here</p>";
42
-
43
- addIdsToHeadings(container, null);
44
-
45
- const p = container.querySelector("p") as HTMLElement;
46
- expect(p.dataset.griddoid).toBeUndefined();
47
- });
48
- });
49
-
50
- describe("removeKeywordHighlights", () => {
51
- it("should replace mark.gdd-keyword elements with their text content", () => {
52
- const container = document.createElement("div");
53
- container.innerHTML = `<p>Hello <mark class="${KEYWORD_HIGHLIGHT_CLASS}">world</mark>!</p>`;
54
-
55
- removeKeywordHighlights(container);
56
-
57
- expect(container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`)).toBeNull();
58
- expect(container.textContent).toBe("Hello world!");
59
- });
60
-
61
- it("should do nothing when there are no highlight marks", () => {
62
- const container = document.createElement("div");
63
- container.innerHTML = "<p>No highlights here</p>";
64
-
65
- removeKeywordHighlights(container);
66
-
67
- expect(container.textContent).toBe("No highlights here");
68
- });
69
-
70
- it("should remove multiple marks", () => {
71
- const container = document.createElement("div");
72
- container.innerHTML = `<p><mark class="${KEYWORD_HIGHLIGHT_CLASS}">seo</mark> and <mark class="${KEYWORD_HIGHLIGHT_CLASS}">react</mark></p>`;
73
-
74
- removeKeywordHighlights(container);
75
-
76
- expect(container.querySelectorAll(`mark.${KEYWORD_HIGHLIGHT_CLASS}`).length).toBe(0);
77
- expect(container.textContent).toBe("seo and react");
78
- });
79
- });
80
-
81
- describe("highlightKeywords", () => {
82
- it("should wrap matching keyword in a mark element", () => {
83
- const container = document.createElement("div");
84
- container.innerHTML = "<p>Hello world</p>";
85
-
86
- highlightKeywords(container, ["world"]);
87
-
88
- const mark = container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
89
- expect(mark).not.toBeNull();
90
- expect(mark?.textContent).toBe("world");
91
- });
92
-
93
- it("should do nothing when keywords array is empty", () => {
94
- const container = document.createElement("div");
95
- container.innerHTML = "<p>Hello world</p>";
96
-
97
- highlightKeywords(container, []);
98
-
99
- expect(container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`)).toBeNull();
100
- });
101
-
102
- it("should do nothing when keywords contain only whitespace", () => {
103
- const container = document.createElement("div");
104
- container.innerHTML = "<p>Hello world</p>";
105
-
106
- highlightKeywords(container, [" "]);
107
-
108
- expect(container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`)).toBeNull();
109
- });
110
-
111
- it("should be case-insensitive", () => {
112
- const container = document.createElement("div");
113
- container.innerHTML = "<p>Hello WORLD</p>";
114
-
115
- highlightKeywords(container, ["world"]);
116
-
117
- const mark = container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
118
- expect(mark).not.toBeNull();
119
- expect(mark?.textContent).toBe("WORLD");
120
- });
121
-
122
- it("should highlight multiple keywords", () => {
123
- const container = document.createElement("div");
124
- container.innerHTML = "<p>Hello world and react</p>";
125
-
126
- highlightKeywords(container, ["world", "react"]);
127
-
128
- const marks = container.querySelectorAll(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
129
- expect(marks.length).toBe(2);
130
- });
131
-
132
- it("should remove previous highlights before adding new ones", () => {
133
- const container = document.createElement("div");
134
- container.innerHTML = "<p>Hello world react</p>";
135
-
136
- highlightKeywords(container, ["world"]);
137
- expect(container.querySelectorAll(`mark.${KEYWORD_HIGHLIGHT_CLASS}`).length).toBe(1);
138
-
139
- highlightKeywords(container, ["react"]);
140
-
141
- const marks = container.querySelectorAll(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
142
- expect(marks.length).toBe(1);
143
- expect(marks[0].textContent).toBe("react");
144
- });
145
-
146
- it("should not highlight text inside SCRIPT elements", () => {
147
- const container = document.createElement("div");
148
- container.innerHTML = "<script>var world = 'test';</script><p>Hello world</p>";
149
-
150
- highlightKeywords(container, ["world"]);
151
-
152
- const marks = container.querySelectorAll(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
153
- expect(marks.length).toBe(1);
154
- expect(marks[0].textContent).toBe("world");
155
- });
156
- });
157
-
158
- describe("observeAndAddIdsToHeadings", () => {
159
- it("should add IDs to headings immediately on call", () => {
160
- const container = document.createElement("div");
161
- container.innerHTML = "<h1>Title</h1><h2>Subtitle</h2>";
162
-
163
- observeAndAddIdsToHeadings(container, null);
164
-
165
- const h1 = container.querySelector("h1") as HTMLElement;
166
- const h2 = container.querySelector("h2") as HTMLElement;
167
- expect(h1.dataset.griddoid).toBe("heading-1");
168
- expect(h2.dataset.griddoid).toBe("heading-2");
169
- });
170
-
171
- it("should return a cleanup function that disconnects the observer", () => {
172
- const container = document.createElement("div");
173
- container.innerHTML = "<h1>Title</h1>";
174
-
175
- const cleanup = observeAndAddIdsToHeadings(container, null);
176
-
177
- expect(typeof cleanup).toBe("function");
178
- expect(() => cleanup()).not.toThrow();
179
- });
180
-
181
- it("should re-apply IDs when new headings are added to the observed container", async () => {
182
- const container = document.createElement("div");
183
- container.innerHTML = "<h1>Title</h1>";
184
-
185
- const cleanup = observeAndAddIdsToHeadings(container, null);
186
-
187
- const h2 = document.createElement("h2");
188
- h2.textContent = "Subtitle";
189
- container.appendChild(h2);
190
-
191
- await new Promise((resolve) => setTimeout(resolve, 0));
192
-
193
- expect(h2.dataset.griddoid).toBe("heading-2");
194
-
195
- cleanup();
196
- });
197
- });
198
-
199
- describe("observeAndHighlightKeywords", () => {
200
- it("should highlight keywords immediately on call", () => {
201
- const container = document.createElement("div");
202
- container.innerHTML = "<p>Hello world</p>";
203
-
204
- const cleanup = observeAndHighlightKeywords(container, ["world"]);
205
-
206
- expect(container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`)).not.toBeNull();
207
- expect(typeof cleanup).toBe("function");
208
-
209
- cleanup();
210
- });
211
-
212
- it("should remove highlights when the cleanup function is called", () => {
213
- const container = document.createElement("div");
214
- container.innerHTML = "<p>Hello world</p>";
215
-
216
- const cleanup = observeAndHighlightKeywords(container, ["world"]);
217
- expect(container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`)).not.toBeNull();
218
-
219
- cleanup();
220
-
221
- expect(container.querySelector(`mark.${KEYWORD_HIGHLIGHT_CLASS}`)).toBeNull();
222
- expect(container.textContent).toBe("Hello world");
223
- });
224
-
225
- it("should re-highlight keywords after DOM mutations in the observed container", async () => {
226
- const container = document.createElement("div");
227
- container.innerHTML = "<p>Hello world</p>";
228
-
229
- const cleanup = observeAndHighlightKeywords(container, ["world"]);
230
-
231
- const newP = document.createElement("p");
232
- newP.textContent = "Another world mention";
233
- container.appendChild(newP);
234
-
235
- await new Promise((resolve) => setTimeout(resolve, 0));
236
-
237
- const marks = container.querySelectorAll(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
238
- expect(marks.length).toBeGreaterThanOrEqual(1);
239
-
240
- cleanup();
241
- });
242
- });