@griddo/ax 11.14.1 → 11.14.2-rc.1

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 (142) hide show
  1. package/config/jest/reactEasyCropMock.js +15 -0
  2. package/config/jest/reactTimezoneMock.js +13 -0
  3. package/package.json +221 -219
  4. package/public/img/welcome.svg +127 -0
  5. package/src/__tests__/components/Browser/Browser.test.tsx +27 -51
  6. package/src/__tests__/components/CategoryCell/CategoryCell.test.tsx +10 -5
  7. package/src/__tests__/components/ElementsTooltip/ElementsTooltip.test.tsx +27 -14
  8. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +2 -0
  9. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +138 -1
  10. package/src/__tests__/components/ImageDragAndDrop/CropStep/CropStep.test.tsx +84 -0
  11. package/src/__tests__/components/ImageDragAndDrop/ImageDragAndDrop.test.tsx +169 -0
  12. package/src/__tests__/components/ProfileImage/ProfileImage.test.tsx +120 -0
  13. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +8 -0
  14. package/src/__tests__/components/UserRolesAndSites/RoleItem/RoleItem.test.tsx +190 -0
  15. package/src/__tests__/components/UserRolesAndSites/UserRolesAndSites.test.tsx +471 -0
  16. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +15 -2
  17. package/src/__tests__/modules/Sites/Sites.test.tsx +68 -224
  18. package/src/__tests__/modules/Sites/SitesList/ListView/BulkHeader/BulkHeader.test.tsx +21 -17
  19. package/src/__tests__/modules/Sites/SitesList/SitesList.test.tsx +65 -565
  20. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/DataStep/DataStep.test.tsx +109 -0
  21. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/FinalStep/FinalStep.test.tsx +157 -0
  22. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/CropView.test.tsx +51 -0
  23. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/ImageStep.test.tsx +70 -0
  24. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/UploadView.test.tsx +92 -0
  25. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/TimezoneStep/TimezoneStep.test.tsx +94 -0
  26. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeModal.test.tsx +78 -0
  27. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/WelcomeStep/WelcomeStep.test.tsx +39 -0
  28. package/src/__tests__/modules/Sites/SitesList/WelcomeModal/utils.test.ts +55 -0
  29. package/src/api/sites.tsx +4 -4
  30. package/src/components/Avatar/index.tsx +26 -5
  31. package/src/components/Avatar/style.tsx +20 -10
  32. package/src/components/Browser/index.tsx +7 -1
  33. package/src/components/ConfigPanel/index.tsx +5 -4
  34. package/src/components/ElementsTooltip/index.tsx +96 -34
  35. package/src/components/ElementsTooltip/style.tsx +12 -1
  36. package/src/components/Fields/FileField/index.tsx +16 -17
  37. package/src/components/Fields/HeadingField/index.tsx +1 -1
  38. package/src/components/Fields/ImageField/index.tsx +9 -38
  39. package/src/components/Fields/ImageField/style.tsx +12 -1
  40. package/src/components/Fields/ToggleField/index.tsx +1 -1
  41. package/src/components/Fields/Wysiwyg/index.tsx +25 -20
  42. package/src/components/FileGallery/GalleryPanel/index.tsx +15 -7
  43. package/src/components/FileGallery/index.tsx +33 -28
  44. package/src/components/Gallery/GalleryPanel/index.tsx +5 -16
  45. package/src/components/Gallery/index.tsx +0 -2
  46. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +11 -2
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +21 -3
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +2 -2
  49. package/src/components/HeadingsPreviewModal/index.tsx +13 -3
  50. package/src/components/HeadingsPreviewModal/style.tsx +18 -0
  51. package/src/components/HeadingsPreviewModal/utils.tsx +31 -3
  52. package/src/components/Image/index.tsx +2 -2
  53. package/src/components/ImageDragAndDrop/CropStep/index.tsx +95 -0
  54. package/src/components/ImageDragAndDrop/CropStep/style.tsx +101 -0
  55. package/src/{modules/MediaGallery → components}/ImageDragAndDrop/index.tsx +103 -40
  56. package/src/{modules/MediaGallery → components}/ImageDragAndDrop/style.tsx +14 -2
  57. package/src/components/ProfileImage/index.tsx +55 -0
  58. package/src/components/ProfileImage/style.tsx +58 -0
  59. package/src/components/ResizePanel/ResizeHandle/index.tsx +44 -6
  60. package/src/components/ResizePanel/ResizeHandle/style.tsx +7 -0
  61. package/src/components/ResizePanel/index.tsx +25 -4
  62. package/src/components/Tabs/style.tsx +1 -1
  63. package/src/components/Tag/index.tsx +0 -1
  64. package/src/components/UserRolesAndSites/RoleItem/index.tsx +42 -0
  65. package/src/components/UserRolesAndSites/RoleItem/style.tsx +29 -0
  66. package/src/components/UserRolesAndSites/index.tsx +102 -0
  67. package/src/components/UserRolesAndSites/style.tsx +67 -0
  68. package/src/components/index.tsx +6 -0
  69. package/src/constants/index.ts +13 -1
  70. package/src/containers/App/actions.tsx +8 -1
  71. package/src/containers/Sites/actions.tsx +26 -0
  72. package/src/containers/Sites/constants.tsx +1 -0
  73. package/src/containers/Sites/interfaces.tsx +6 -0
  74. package/src/containers/Sites/reducer.tsx +5 -1
  75. package/src/containers/Users/reducer.tsx +6 -5
  76. package/src/guards/routeLeaving/index.tsx +9 -11
  77. package/src/helpers/images.tsx +50 -3
  78. package/src/helpers/index.tsx +2 -1
  79. package/src/hooks/forms.tsx +45 -48
  80. package/src/hooks/modals.tsx +4 -3
  81. package/src/modules/ActivityLog/ItemLogUser/UserItem/index.tsx +1 -1
  82. package/src/modules/App/Routing/Logout/index.tsx +3 -5
  83. package/src/modules/App/Routing/NavMenu/NavItem/index.tsx +73 -52
  84. package/src/modules/App/Routing/NavMenu/NavItem/style.tsx +21 -7
  85. package/src/modules/App/Routing/NavMenu/index.tsx +59 -54
  86. package/src/modules/App/Routing/NavMenu/style.tsx +13 -11
  87. package/src/modules/CreatePass/index.tsx +1 -1
  88. package/src/modules/FileDrive/FileDragAndDrop/index.tsx +11 -8
  89. package/src/modules/FileDrive/FileModal/index.tsx +8 -9
  90. package/src/modules/FileDrive/index.tsx +1 -18
  91. package/src/modules/Forms/FormEditor/index.tsx +1 -1
  92. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +22 -11
  93. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +1 -1
  94. package/src/modules/MediaGallery/ImageModal/index.tsx +1 -5
  95. package/src/modules/MediaGallery/index.tsx +1 -3
  96. package/src/modules/Settings/Globals/constants.tsx +942 -106
  97. package/src/modules/Sites/SitesList/AllSitesHeader/index.tsx +33 -0
  98. package/src/modules/Sites/SitesList/AllSitesHeader/style.tsx +35 -0
  99. package/src/modules/Sites/SitesList/GridView/GridHeaderFilter/index.tsx +5 -5
  100. package/src/modules/Sites/SitesList/GridView/GridSiteItem/index.tsx +23 -119
  101. package/src/modules/Sites/SitesList/ListView/BulkHeader/TableHeader/index.tsx +4 -4
  102. package/src/modules/Sites/SitesList/ListView/BulkHeader/index.tsx +4 -3
  103. package/src/modules/Sites/SitesList/ListView/ListSiteItem/index.tsx +23 -120
  104. package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/index.tsx +4 -5
  105. package/src/modules/Sites/SitesList/RecentSites/index.tsx +49 -0
  106. package/src/modules/Sites/SitesList/RecentSites/style.tsx +92 -0
  107. package/src/modules/Sites/SitesList/SiteModal/index.tsx +8 -7
  108. package/src/modules/Sites/SitesList/WelcomeModal/DataStep/index.tsx +72 -0
  109. package/src/modules/Sites/SitesList/WelcomeModal/DataStep/style.tsx +59 -0
  110. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/constants.tsx +78 -0
  111. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/index.tsx +78 -0
  112. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/style.tsx +141 -0
  113. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/index.tsx +93 -0
  114. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView/style.tsx +77 -0
  115. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/index.tsx +100 -0
  116. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView/style.tsx +94 -0
  117. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/index.tsx +44 -0
  118. package/src/modules/Sites/SitesList/WelcomeModal/ImageStep/style.tsx +31 -0
  119. package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/index.tsx +51 -0
  120. package/src/modules/Sites/SitesList/WelcomeModal/TimezoneStep/style.tsx +52 -0
  121. package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/index.tsx +40 -0
  122. package/src/modules/Sites/SitesList/WelcomeModal/WelcomeStep/style.tsx +53 -0
  123. package/src/modules/Sites/SitesList/WelcomeModal/index.tsx +215 -0
  124. package/src/modules/Sites/SitesList/WelcomeModal/style.tsx +12 -0
  125. package/src/modules/Sites/SitesList/WelcomeModal/utils.ts +26 -0
  126. package/src/modules/Sites/SitesList/atoms.tsx +4 -4
  127. package/src/modules/Sites/SitesList/hooks.tsx +149 -16
  128. package/src/modules/Sites/SitesList/index.tsx +127 -125
  129. package/src/modules/Sites/SitesList/style.tsx +1 -117
  130. package/src/modules/Sites/SitesList/utils.tsx +9 -2
  131. package/src/modules/Sites/index.tsx +19 -8
  132. package/src/modules/Users/Profile/index.tsx +169 -31
  133. package/src/modules/Users/Profile/style.tsx +81 -1
  134. package/src/modules/Users/Roles/RoleItem/index.tsx +2 -2
  135. package/src/modules/Users/UserCreate/SiteItem/index.tsx +11 -14
  136. package/src/modules/Users/UserForm/atoms.tsx +3 -3
  137. package/src/modules/Users/UserForm/index.tsx +25 -29
  138. package/src/modules/Users/UserForm/style.tsx +15 -2
  139. package/src/modules/Users/UserList/UserItem/index.tsx +4 -4
  140. package/src/routes/index.tsx +1 -0
  141. package/src/types/index.tsx +2 -0
  142. /package/src/modules/Sites/SitesList/{RecentSiteItem → RecentSites/RecentSiteItem}/style.tsx +0 -0
@@ -0,0 +1,109 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ import { parseTheme } from "@ax/helpers";
4
+ import type { IUserFormState } from "@ax/modules/Sites/SitesList/WelcomeModal";
5
+ import DataStep from "@ax/modules/Sites/SitesList/WelcomeModal/DataStep";
6
+ import globalTheme from "@ax/themes/theme.json";
7
+
8
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
9
+ import { ThemeProvider } from "styled-components";
10
+
11
+ const defaultForm: IUserFormState = {
12
+ name: "John Doe",
13
+ username: "johndoe",
14
+ position: "Developer",
15
+ company: "Acme",
16
+ timezone: "Europe/Madrid",
17
+ };
18
+
19
+ const renderDataStep = (props = {}) =>
20
+ render(
21
+ <ThemeProvider theme={parseTheme(globalTheme)}>
22
+ <DataStep
23
+ form={defaultForm}
24
+ setForm={jest.fn()}
25
+ onClickNext={jest.fn()}
26
+ onClickBack={jest.fn()}
27
+ croppedImageUrl={null}
28
+ {...props}
29
+ />
30
+ </ThemeProvider>,
31
+ );
32
+
33
+ afterEach(cleanup);
34
+
35
+ describe("DataStep rendering", () => {
36
+ it("should render all three form fields", () => {
37
+ renderDataStep();
38
+ const fields = screen.getAllByTestId("fields-behavior-wrapper");
39
+ expect(fields.length).toEqual(3);
40
+ });
41
+
42
+ it("should render Back and Next step buttons", () => {
43
+ renderDataStep();
44
+ expect(screen.getByText("Back")).toBeTruthy();
45
+ expect(screen.getByText("Next step")).toBeTruthy();
46
+ });
47
+
48
+ it("should disable Next step button when username is empty", () => {
49
+ renderDataStep({ form: { ...defaultForm, username: "" } });
50
+ const nextButton = screen.getByText("Next step").closest("button");
51
+ expect(nextButton).toBeDisabled();
52
+ });
53
+
54
+ it("should enable Next step button when username has value", () => {
55
+ renderDataStep();
56
+ const nextButton = screen.getByText("Next step").closest("button");
57
+ expect(nextButton).not.toBeDisabled();
58
+ });
59
+
60
+ it("should disable Next step button when username is only whitespace", () => {
61
+ renderDataStep({ form: { ...defaultForm, username: " " } });
62
+ const nextButton = screen.getByText("Next step").closest("button");
63
+ expect(nextButton).toBeDisabled();
64
+ });
65
+ });
66
+
67
+ describe("DataStep events", () => {
68
+ it("should call onClickBack when Back button is clicked", () => {
69
+ const onClickBack = jest.fn();
70
+ renderDataStep({ onClickBack });
71
+ fireEvent.click(screen.getByText("Back"));
72
+ expect(onClickBack).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ it("should call onClickNext when Next step button is clicked", () => {
76
+ const onClickNext = jest.fn();
77
+ renderDataStep({ onClickNext });
78
+ fireEvent.click(screen.getByText("Next step"));
79
+ expect(onClickNext).toHaveBeenCalledTimes(1);
80
+ });
81
+
82
+ it("should not call onClickNext when username is empty", () => {
83
+ const onClickNext = jest.fn();
84
+ renderDataStep({ onClickNext, form: { ...defaultForm, username: "" } });
85
+ const nextButton = screen.getByText("Next step").closest("button");
86
+ nextButton && fireEvent.click(nextButton);
87
+ expect(onClickNext).not.toHaveBeenCalled();
88
+ });
89
+ });
90
+
91
+ describe("DataStep position field", () => {
92
+ it("should display position value from form", () => {
93
+ renderDataStep({ form: { ...defaultForm, position: "Senior Developer" } });
94
+ const positionInput = screen.getByDisplayValue("Senior Developer");
95
+ expect(positionInput).toBeTruthy();
96
+ });
97
+
98
+ it("should display empty position field when not set", () => {
99
+ renderDataStep({ form: { ...defaultForm, position: "" } });
100
+ // Check that Job title label exists
101
+ expect(screen.getByText("Job title")).toBeTruthy();
102
+ });
103
+
104
+ it("should have position field with correct placeholder", () => {
105
+ renderDataStep();
106
+ const positionInput = screen.getByPlaceholderText("Type a job title");
107
+ expect(positionInput).toBeTruthy();
108
+ });
109
+ });
@@ -0,0 +1,157 @@
1
+ import { parseTheme } from "@ax/helpers";
2
+ import FinalStep from "@ax/modules/Sites/SitesList/WelcomeModal/FinalStep";
3
+ import globalTheme from "@ax/themes/theme.json";
4
+
5
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
6
+ import { ThemeProvider } from "styled-components";
7
+
8
+ afterEach(cleanup);
9
+
10
+ const mockRoles = [
11
+ {
12
+ id: 1,
13
+ name: "Editor",
14
+ hex: "#aabbcc",
15
+ description: "Editor role",
16
+ permissions: {} as any,
17
+ active: true,
18
+ users: [],
19
+ },
20
+ {
21
+ id: 2,
22
+ name: "Viewer",
23
+ hex: "#ddeeff",
24
+ description: "Viewer role",
25
+ permissions: {} as any,
26
+ active: true,
27
+ users: [],
28
+ },
29
+ {
30
+ id: 3,
31
+ name: "Administrator",
32
+ hex: "#ff0000",
33
+ description: "Administrator role",
34
+ permissions: {} as any,
35
+ active: true,
36
+ users: [],
37
+ },
38
+ {
39
+ id: 4,
40
+ name: "Webmaster",
41
+ hex: "#00ff00",
42
+ description: "Webmaster role",
43
+ permissions: {} as any,
44
+ active: true,
45
+ users: [],
46
+ },
47
+ {
48
+ id: 5,
49
+ name: "SEO Validator",
50
+ hex: "#0000ff",
51
+ description: "SEO Validator role",
52
+ permissions: {} as any,
53
+ active: true,
54
+ users: [],
55
+ },
56
+ ];
57
+
58
+ const mockSites = [
59
+ { id: 1, name: "Site 1", smallAvatar: null } as any,
60
+ { id: 2, name: "Site 2", smallAvatar: null } as any,
61
+ ];
62
+
63
+ const renderFinalStep = (props = {}) =>
64
+ render(
65
+ <ThemeProvider theme={parseTheme(globalTheme)}>
66
+ <FinalStep
67
+ isSuperAdmin={false}
68
+ userRoles={[]}
69
+ roles={mockRoles}
70
+ sites={mockSites}
71
+ onClose={jest.fn()}
72
+ onViewProfile={jest.fn()}
73
+ {...props}
74
+ />
75
+ </ThemeProvider>,
76
+ );
77
+
78
+ describe("FinalStep rendering", () => {
79
+ it("should render View profile and Start Using Griddo buttons", () => {
80
+ renderFinalStep();
81
+ expect(screen.getByText("View profile")).toBeTruthy();
82
+ expect(screen.getByText("Start Using Griddo")).toBeTruthy();
83
+ });
84
+ });
85
+
86
+ describe("FinalStep learn items", () => {
87
+ it("should show Administrator learn items when isSuperAdmin is true", () => {
88
+ renderFinalStep({ isSuperAdmin: true });
89
+ expect(screen.getByText("Create sites")).toBeTruthy();
90
+ expect(screen.getByText("Create Add-ons")).toBeTruthy();
91
+ });
92
+
93
+ it("should show Editor learn items when user has Editor role", () => {
94
+ renderFinalStep({
95
+ isSuperAdmin: false,
96
+ userRoles: [{ siteId: 1, roles: [1] }],
97
+ });
98
+ expect(screen.getByText("Add and manage Images")).toBeTruthy();
99
+ expect(screen.getByText("Work with AI")).toBeTruthy();
100
+ });
101
+
102
+ it("should show Viewer learn items when user has Viewer role", () => {
103
+ renderFinalStep({
104
+ isSuperAdmin: false,
105
+ userRoles: [{ siteId: 1, roles: [2] }],
106
+ });
107
+ expect(screen.getByText("View pages")).toBeTruthy();
108
+ });
109
+
110
+ it("should fall back to Editor items when role has no matching learn items", () => {
111
+ // Create a role that has no matching LEARN_ITEMS (id=99)
112
+ const rolesWithUnmapped = [
113
+ ...mockRoles,
114
+ { id: 99, name: "UnmappedRole", hex: "#ffffff", description: "", permissions: {} as any, active: true, users: [] },
115
+ ];
116
+ renderFinalStep({
117
+ isSuperAdmin: false,
118
+ userRoles: [{ siteId: 1, roles: [99] }],
119
+ roles: rolesWithUnmapped,
120
+ });
121
+ // Should fall back to Editor items
122
+ expect(screen.getByText("Add and manage Images")).toBeTruthy();
123
+ });
124
+
125
+ it("should show Webmaster learn items when user has Webmaster role", () => {
126
+ renderFinalStep({
127
+ isSuperAdmin: false,
128
+ userRoles: [{ siteId: 1, roles: [4] }],
129
+ });
130
+ expect(screen.getByText("Work with Categories")).toBeTruthy();
131
+ });
132
+
133
+ it("should show SEO Validator learn items when user has SEO Validator role", () => {
134
+ renderFinalStep({
135
+ isSuperAdmin: false,
136
+ userRoles: [{ siteId: 1, roles: [5] }],
137
+ });
138
+ expect(screen.getByText("Manage SEO")).toBeTruthy();
139
+ expect(screen.getByText("Preview your SEO")).toBeTruthy();
140
+ });
141
+ });
142
+
143
+ describe("FinalStep events", () => {
144
+ it("should call onClose when Start Using Griddo is clicked", () => {
145
+ const onClose = jest.fn();
146
+ renderFinalStep({ onClose });
147
+ fireEvent.click(screen.getByText("Start Using Griddo"));
148
+ expect(onClose).toHaveBeenCalledTimes(1);
149
+ });
150
+
151
+ it("should call onViewProfile when View profile is clicked", () => {
152
+ const onViewProfile = jest.fn();
153
+ renderFinalStep({ onViewProfile });
154
+ fireEvent.click(screen.getByText("View profile"));
155
+ expect(onViewProfile).toHaveBeenCalledTimes(1);
156
+ });
157
+ });
@@ -0,0 +1,51 @@
1
+ import { parseTheme } from "@ax/helpers";
2
+ import CropView from "@ax/modules/Sites/SitesList/WelcomeModal/ImageStep/CropView";
3
+ import globalTheme from "@ax/themes/theme.json";
4
+
5
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
6
+ import { ThemeProvider } from "styled-components";
7
+
8
+ afterEach(cleanup);
9
+
10
+ const renderCropView = (props = {}) =>
11
+ render(
12
+ <ThemeProvider theme={parseTheme(globalTheme)}>
13
+ <CropView imageSrc="data:image/jpeg;base64,abc" onConfirm={jest.fn()} onUploadNew={jest.fn()} {...props} />
14
+ </ThemeProvider>,
15
+ );
16
+
17
+ describe("CropView rendering", () => {
18
+ it("should render the cropper", () => {
19
+ renderCropView();
20
+ expect(screen.getByTestId("cropper-mock")).toBeTruthy();
21
+ });
22
+
23
+ it("should render zoom controls", () => {
24
+ renderCropView();
25
+ expect(screen.getByLabelText("Zoom in")).toBeTruthy();
26
+ expect(screen.getByLabelText("Zoom out")).toBeTruthy();
27
+ expect(screen.getByLabelText("Zoom")).toBeTruthy();
28
+ });
29
+
30
+ it("should render Next step and Upload new image buttons", () => {
31
+ renderCropView();
32
+ expect(screen.getByText("Next step")).toBeTruthy();
33
+ expect(screen.getByText("Upload new image")).toBeTruthy();
34
+ });
35
+ });
36
+
37
+ describe("CropView events", () => {
38
+ it("should call onConfirm with cropped area when Next step is clicked", () => {
39
+ const onConfirm = jest.fn();
40
+ renderCropView({ onConfirm });
41
+ fireEvent.click(screen.getByText("Next step"));
42
+ expect(onConfirm).toHaveBeenCalledWith({ x: 0, y: 0, width: 100, height: 100 });
43
+ });
44
+
45
+ it("should call onUploadNew when Upload new image is clicked", () => {
46
+ const onUploadNew = jest.fn();
47
+ renderCropView({ onUploadNew });
48
+ fireEvent.click(screen.getByText("Upload new image"));
49
+ expect(onUploadNew).toHaveBeenCalledTimes(1);
50
+ });
51
+ });
@@ -0,0 +1,70 @@
1
+ import { parseTheme } from "@ax/helpers";
2
+ import ImageStep from "@ax/modules/Sites/SitesList/WelcomeModal/ImageStep";
3
+ import globalTheme from "@ax/themes/theme.json";
4
+
5
+ import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
6
+ import { ThemeProvider } from "styled-components";
7
+
8
+ afterEach(cleanup);
9
+
10
+ const renderImageStep = (props = {}) =>
11
+ render(
12
+ <ThemeProvider theme={parseTheme(globalTheme)}>
13
+ <ImageStep onCropConfirmed={jest.fn()} onSkip={jest.fn()} {...props} />
14
+ </ThemeProvider>,
15
+ );
16
+
17
+ describe("ImageStep rendering", () => {
18
+ it("should render UploadView initially", () => {
19
+ renderImageStep();
20
+ expect(screen.getByText("Select image")).toBeTruthy();
21
+ expect(screen.queryByTestId("cropper-mock")).toBeFalsy();
22
+ });
23
+
24
+ it("should render CropView after image is loaded", async () => {
25
+ renderImageStep();
26
+
27
+ const mockResult = "data:image/jpeg;base64,abc123";
28
+ const mockFileReader = {
29
+ onload: null as any,
30
+ readAsDataURL: jest.fn(function (this: any) {
31
+ this.onload({ target: { result: mockResult } });
32
+ }),
33
+ };
34
+ jest.spyOn(global, "FileReader" as any).mockImplementation(() => mockFileReader);
35
+
36
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
37
+ const validFile = new File(["content"], "photo.jpg", { type: "image/jpeg" });
38
+
39
+ await act(async () => {
40
+ fireEvent.change(fileInput, { target: { files: [validFile] } });
41
+ });
42
+
43
+ expect(screen.getByTestId("cropper-mock")).toBeTruthy();
44
+ expect(screen.queryByText("Select image")).toBeFalsy();
45
+ });
46
+
47
+ it("should go back to UploadView when Upload new image is clicked", async () => {
48
+ renderImageStep();
49
+
50
+ const mockResult = "data:image/jpeg;base64,abc123";
51
+ const mockFileReader = {
52
+ onload: null as any,
53
+ readAsDataURL: jest.fn(function (this: any) {
54
+ this.onload({ target: { result: mockResult } });
55
+ }),
56
+ };
57
+ jest.spyOn(global, "FileReader" as any).mockImplementation(() => mockFileReader);
58
+
59
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
60
+ await act(async () => {
61
+ fireEvent.change(fileInput, { target: { files: [new File([""], "photo.jpg", { type: "image/jpeg" })] } });
62
+ });
63
+
64
+ await act(async () => {
65
+ fireEvent.click(screen.getByText("Upload new image"));
66
+ });
67
+
68
+ expect(screen.getByText("Select image")).toBeTruthy();
69
+ });
70
+ });
@@ -0,0 +1,92 @@
1
+ import { parseTheme } from "@ax/helpers";
2
+ import UploadView from "@ax/modules/Sites/SitesList/WelcomeModal/ImageStep/UploadView";
3
+ import globalTheme from "@ax/themes/theme.json";
4
+
5
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
6
+ import { ThemeProvider } from "styled-components";
7
+
8
+ afterEach(cleanup);
9
+
10
+ const renderUploadView = (props = {}) =>
11
+ render(
12
+ <ThemeProvider theme={parseTheme(globalTheme)}>
13
+ <UploadView onImageLoaded={jest.fn()} onSkip={jest.fn()} {...props} />
14
+ </ThemeProvider>,
15
+ );
16
+
17
+ describe("UploadView rendering", () => {
18
+ it("should render the skip button", () => {
19
+ renderUploadView();
20
+ expect(screen.getByText("Skip profile picture")).toBeTruthy();
21
+ });
22
+
23
+ it("should render the select image button", () => {
24
+ renderUploadView();
25
+ expect(screen.getByText("Select image")).toBeTruthy();
26
+ });
27
+
28
+ it("should render the file input", () => {
29
+ renderUploadView();
30
+ const fileInput = document.querySelector('input[type="file"]');
31
+ expect(fileInput).toBeTruthy();
32
+ });
33
+
34
+ it("should not render error message initially", () => {
35
+ renderUploadView();
36
+ expect(screen.queryByTestId("upload-view-error")).toBeFalsy();
37
+ });
38
+
39
+ it("should show error for invalid file format", () => {
40
+ renderUploadView();
41
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
42
+ const invalidFile = new File(["content"], "document.pdf", { type: "application/pdf" });
43
+ fireEvent.change(fileInput, { target: { files: [invalidFile] } });
44
+ expect(screen.getByTestId("upload-view-error")).toBeTruthy();
45
+ });
46
+ });
47
+
48
+ describe("UploadView events", () => {
49
+ it("should call onSkip when skip button is clicked", () => {
50
+ const onSkip = jest.fn();
51
+ renderUploadView({ onSkip });
52
+ fireEvent.click(screen.getByText("Skip profile picture"));
53
+ expect(onSkip).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it("should call onImageLoaded with data URL for valid file", () => {
57
+ const onImageLoaded = jest.fn();
58
+ const mockResult = "data:image/jpeg;base64,abc123";
59
+
60
+ const mockFileReader = {
61
+ onload: null as any,
62
+ readAsDataURL: jest.fn(function (this: any) {
63
+ this.onload({ target: { result: mockResult } });
64
+ }),
65
+ };
66
+ jest.spyOn(global, "FileReader" as any).mockImplementation(() => mockFileReader);
67
+
68
+ renderUploadView({ onImageLoaded });
69
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
70
+ const validFile = new File(["content"], "photo.jpg", { type: "image/jpeg" });
71
+ fireEvent.change(fileInput, { target: { files: [validFile] } });
72
+
73
+ expect(onImageLoaded).toHaveBeenCalledWith(mockResult);
74
+ });
75
+
76
+ it("should not call onImageLoaded for invalid file format", () => {
77
+ const onImageLoaded = jest.fn();
78
+ renderUploadView({ onImageLoaded });
79
+ const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
80
+ const invalidFile = new File(["content"], "document.pdf", { type: "application/pdf" });
81
+ fireEvent.change(fileInput, { target: { files: [invalidFile] } });
82
+ expect(onImageLoaded).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it("should show drop title when dragging over", () => {
86
+ renderUploadView();
87
+ const dragWrapper = screen.getByTestId("drag-drop-wrapper");
88
+ fireEvent.dragEnter(dragWrapper);
89
+ fireEvent.dragOver(dragWrapper);
90
+ expect(screen.getByText("Drop your image")).toBeTruthy();
91
+ });
92
+ });
@@ -0,0 +1,94 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ import { parseTheme } from "@ax/helpers";
4
+ import type { IUserFormState } from "@ax/modules/Sites/SitesList/WelcomeModal";
5
+ import TimezoneStep from "@ax/modules/Sites/SitesList/WelcomeModal/TimezoneStep";
6
+ import globalTheme from "@ax/themes/theme.json";
7
+
8
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
9
+ import { ThemeProvider } from "styled-components";
10
+
11
+ // react-timezone-map-select is mocked globally via jest.config.ts moduleNameMapper
12
+
13
+ const defaultForm: IUserFormState = {
14
+ name: "John Doe",
15
+ username: "johndoe",
16
+ position: "Developer",
17
+ company: "Acme",
18
+ timezone: "Europe/Madrid",
19
+ };
20
+
21
+ const renderTimezoneStep = (props = {}) =>
22
+ render(
23
+ <ThemeProvider theme={parseTheme(globalTheme)}>
24
+ <TimezoneStep
25
+ form={defaultForm}
26
+ setForm={jest.fn()}
27
+ onClickNext={jest.fn()}
28
+ onClickBack={jest.fn()}
29
+ isSaving={false}
30
+ {...props}
31
+ />
32
+ </ThemeProvider>,
33
+ );
34
+
35
+ afterEach(cleanup);
36
+
37
+ describe("TimezoneStep rendering", () => {
38
+ it("should render Back and Save profile buttons", () => {
39
+ renderTimezoneStep();
40
+ expect(screen.getByText("Back")).toBeTruthy();
41
+ expect(screen.getByText("Save profile")).toBeTruthy();
42
+ });
43
+
44
+ it("should render the world map", () => {
45
+ renderTimezoneStep();
46
+ expect(screen.getByTestId("world-map-mock")).toBeTruthy();
47
+ });
48
+
49
+ it("should render a timezone select", () => {
50
+ renderTimezoneStep();
51
+ expect(screen.getByTestId("select-component")).toBeTruthy();
52
+ });
53
+
54
+ it("should disable Save profile button when isSaving is true", () => {
55
+ renderTimezoneStep({ isSaving: true });
56
+ const saveButton = screen.getByText("Saving...").closest("button");
57
+ expect(saveButton).toBeDisabled();
58
+ });
59
+
60
+ it("should show Saving... text when isSaving is true", () => {
61
+ renderTimezoneStep({ isSaving: true });
62
+ expect(screen.getByText("Saving...")).toBeTruthy();
63
+ });
64
+
65
+ it("should disable Save profile button when timezone is empty", () => {
66
+ renderTimezoneStep({ form: { ...defaultForm, timezone: "" } });
67
+ const saveButton = screen.getByText("Save profile").closest("button");
68
+ expect(saveButton).toBeDisabled();
69
+ });
70
+ });
71
+
72
+ describe("TimezoneStep events", () => {
73
+ it("should call onClickBack when Back button is clicked", () => {
74
+ const onClickBack = jest.fn();
75
+ renderTimezoneStep({ onClickBack });
76
+ fireEvent.click(screen.getByText("Back"));
77
+ expect(onClickBack).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it("should call onClickNext when Save profile button is clicked", () => {
81
+ const onClickNext = jest.fn();
82
+ renderTimezoneStep({ onClickNext });
83
+ fireEvent.click(screen.getByText("Save profile"));
84
+ expect(onClickNext).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it("should not call onClickNext when isSaving is true", () => {
88
+ const onClickNext = jest.fn();
89
+ renderTimezoneStep({ onClickNext, isSaving: true });
90
+ const saveButton = screen.getByText("Saving...").closest("button");
91
+ saveButton && fireEvent.click(saveButton);
92
+ expect(onClickNext).not.toHaveBeenCalled();
93
+ });
94
+ });
@@ -0,0 +1,78 @@
1
+ import { Provider } from "react-redux";
2
+
3
+ import { parseTheme } from "@ax/helpers";
4
+ import WelcomeModal from "@ax/modules/Sites/SitesList/WelcomeModal";
5
+ import globalTheme from "@ax/themes/theme.json";
6
+
7
+ import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
8
+ import configureStore from "redux-mock-store";
9
+ import thunk from "redux-thunk";
10
+ import { ThemeProvider } from "styled-components";
11
+
12
+ import { sitesDataMock, userDataMock } from "../../../../../__mocks__/store/SitesList";
13
+
14
+ afterEach(cleanup);
15
+
16
+ const mockStore = configureStore([thunk]);
17
+
18
+ const initialStore = {
19
+ users: userDataMock,
20
+ sites: { allSites: sitesDataMock.sites },
21
+ };
22
+
23
+ const renderWelcomeModal = (props = {}, storeData = initialStore) => {
24
+ const store = mockStore(storeData);
25
+ return render(
26
+ <Provider store={store}>
27
+ <ThemeProvider theme={parseTheme(globalTheme)}>
28
+ <WelcomeModal isOpen={true} toggleModal={jest.fn()} {...props} />
29
+ </ThemeProvider>
30
+ </Provider>,
31
+ );
32
+ };
33
+
34
+ describe("WelcomeModal rendering", () => {
35
+ it("should render the modal when open", () => {
36
+ renderWelcomeModal({ isOpen: true });
37
+ expect(screen.getByTestId("modal-wrapper")).toBeTruthy();
38
+ });
39
+
40
+ it("should not render the modal when closed", () => {
41
+ renderWelcomeModal({ isOpen: false });
42
+ expect(screen.queryByTestId("modal-wrapper")).toBeFalsy();
43
+ });
44
+
45
+ it("should display WelcomeStep with correct title and buttons on initial load", () => {
46
+ renderWelcomeModal();
47
+ expect(screen.getByText("Welcome")).toBeTruthy();
48
+ expect(screen.getByText("Complete profile")).toBeTruthy();
49
+ expect(screen.getByText("Skip, I\u2019ll do it later")).toBeTruthy();
50
+ });
51
+ });
52
+
53
+ describe("WelcomeModal step navigation", () => {
54
+ it("should navigate through all steps from welcome to timezone", async () => {
55
+ renderWelcomeModal();
56
+
57
+ // Step 1: Welcome
58
+ expect(screen.getByText("Welcome")).toBeTruthy();
59
+
60
+ // Step 2: Image
61
+ await act(async () => {
62
+ fireEvent.click(screen.getByText("Complete profile"));
63
+ });
64
+ expect(screen.getByText("Step 1 of 3: Profile Picture")).toBeTruthy();
65
+
66
+ // Step 3: Data
67
+ await act(async () => {
68
+ fireEvent.click(screen.getByText("Skip profile picture"));
69
+ });
70
+ expect(screen.getByText("Step 2 of 3: Profile data")).toBeTruthy();
71
+
72
+ // Step 4: Timezone
73
+ await act(async () => {
74
+ fireEvent.click(screen.getByText("Next step"));
75
+ });
76
+ expect(screen.getByText("Step 3 of 3: Define timezone")).toBeTruthy();
77
+ });
78
+ });
@@ -0,0 +1,39 @@
1
+ import { parseTheme } from "@ax/helpers";
2
+ import WelcomeStep from "@ax/modules/Sites/SitesList/WelcomeModal/WelcomeStep";
3
+ import globalTheme from "@ax/themes/theme.json";
4
+
5
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
6
+ import { ThemeProvider } from "styled-components";
7
+
8
+ afterEach(cleanup);
9
+
10
+ const renderWelcomeStep = (props = {}) =>
11
+ render(
12
+ <ThemeProvider theme={parseTheme(globalTheme)}>
13
+ <WelcomeStep onSkip={jest.fn()} onClickNext={jest.fn()} {...props} />
14
+ </ThemeProvider>,
15
+ );
16
+
17
+ describe("WelcomeStep rendering", () => {
18
+ it("should render skip and complete profile buttons", () => {
19
+ renderWelcomeStep();
20
+ expect(screen.getByText("Skip, I\u2019ll do it later")).toBeTruthy();
21
+ expect(screen.getByText("Complete profile")).toBeTruthy();
22
+ });
23
+ });
24
+
25
+ describe("WelcomeStep events", () => {
26
+ it("should call onSkip when skip button is clicked", () => {
27
+ const onSkip = jest.fn();
28
+ renderWelcomeStep({ onSkip });
29
+ fireEvent.click(screen.getByText("Skip, I\u2019ll do it later"));
30
+ expect(onSkip).toHaveBeenCalledTimes(1);
31
+ });
32
+
33
+ it("should call onClickNext when complete profile button is clicked", () => {
34
+ const onClickNext = jest.fn();
35
+ renderWelcomeStep({ onClickNext });
36
+ fireEvent.click(screen.getByText("Complete profile"));
37
+ expect(onClickNext).toHaveBeenCalledTimes(1);
38
+ });
39
+ });