@openedx/frontend-base 1.0.0-alpha.0 → 1.0.0-alpha.10

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 (232) hide show
  1. package/config/eslint/base.eslint.config.js +1 -1
  2. package/config/jest/jest.config.js +1 -0
  3. package/config/types.js +0 -2
  4. package/config/webpack/common-config/all/getStylesheetRule.js +1 -1
  5. package/config/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
  6. package/config/webpack/webpack.config.build.js +1 -11
  7. package/config/webpack/webpack.config.dev.js +5 -11
  8. package/config/webpack/webpack.config.dev.shell.js +5 -11
  9. package/package.json +11 -3
  10. package/runtime/__mocks__/file.js +1 -0
  11. package/runtime/__mocks__/svg.js +1 -0
  12. package/runtime/__mocks__/universal-cookie.js +6 -0
  13. package/runtime/analytics/interface.test.js +242 -0
  14. package/runtime/auth/AxiosJwtAuthService.test.jsx +1076 -0
  15. package/runtime/auth/interceptors/createRetryInterceptor.test.js +23 -0
  16. package/runtime/config/getExternalLinkUrl.test.js +76 -0
  17. package/runtime/config/index.ts +2 -3
  18. package/runtime/i18n/lib.test.js +230 -0
  19. package/runtime/index.ts +5 -0
  20. package/runtime/initialize.async.function.config.test.js +43 -0
  21. package/runtime/initialize.const.config.test.js +41 -0
  22. package/runtime/initialize.function.config.test.js +41 -0
  23. package/runtime/initialize.test.js +356 -0
  24. package/runtime/jest.config.js +1 -0
  25. package/runtime/logging/NewRelicLoggingService.test.js +214 -0
  26. package/runtime/react/AuthenticatedPageRoute.test.jsx +135 -0
  27. package/runtime/react/ErrorBoundary.test.jsx +83 -0
  28. package/runtime/react/SiteProvider.test.jsx +66 -0
  29. package/runtime/react/SiteProvider.tsx +26 -3
  30. package/runtime/react/constants.ts +3 -0
  31. package/runtime/react/hooks/index.ts +8 -0
  32. package/runtime/react/hooks/theme/index.ts +2 -0
  33. package/runtime/react/hooks/theme/useTheme.test.ts +221 -0
  34. package/runtime/react/hooks/theme/useTheme.ts +179 -0
  35. package/runtime/react/hooks/theme/useThemeConfig.test.ts +107 -0
  36. package/runtime/react/hooks/theme/useThemeConfig.ts +34 -0
  37. package/runtime/react/hooks/theme/useThemeCore.test.ts +65 -0
  38. package/runtime/react/hooks/theme/useThemeCore.ts +52 -0
  39. package/runtime/react/hooks/theme/useThemeVariants.test.ts +97 -0
  40. package/runtime/react/hooks/theme/useThemeVariants.ts +116 -0
  41. package/runtime/react/hooks/theme/useTrackColorSchemeChoice.test.ts +54 -0
  42. package/runtime/react/hooks/theme/useTrackColorSchemeChoice.ts +30 -0
  43. package/runtime/react/hooks/theme/utils.ts +11 -0
  44. package/runtime/react/hooks/useActiveRoles.ts +15 -0
  45. package/runtime/react/hooks/useActiveRouteRoleWatcher.ts +31 -0
  46. package/runtime/react/hooks/useAppConfig.ts +9 -0
  47. package/runtime/react/hooks/useAuthenticatedUser.test.tsx +41 -0
  48. package/runtime/react/hooks/useAuthenticatedUser.ts +9 -0
  49. package/runtime/react/hooks/useSiteConfig.test.tsx +13 -0
  50. package/runtime/react/hooks/useSiteConfig.ts +9 -0
  51. package/runtime/react/hooks/useSiteEvent.ts +24 -0
  52. package/runtime/react/reducers.ts +40 -0
  53. package/runtime/routing/utils.test.ts +7 -0
  54. package/runtime/scripts/GoogleAnalyticsLoader.test.ts +77 -0
  55. package/runtime/setupTest.js +0 -35
  56. package/runtime/site.config.test.tsx +33 -0
  57. package/runtime/slots/Slot.test.tsx +40 -0
  58. package/runtime/slots/layout/DefaultSlotLayout.test.tsx +31 -0
  59. package/runtime/slots/layout/hooks.test.tsx +178 -0
  60. package/runtime/slots/layout/utils.test.ts +67 -0
  61. package/runtime/slots/types.ts +1 -0
  62. package/runtime/slots/utils.test.ts +64 -0
  63. package/runtime/slots/utils.ts +28 -9
  64. package/runtime/slots/widget/iframe/hooks.ts +1 -1
  65. package/runtime/testing/initializeMockApp.test.ts +66 -0
  66. package/runtime/testing/initializeMockApp.ts +5 -0
  67. package/runtime/utils.test.js +116 -0
  68. package/shell/Logo.test.tsx +32 -0
  69. package/shell/__mocks__/file.js +1 -0
  70. package/shell/__mocks__/svg.js +1 -0
  71. package/shell/__mocks__/universal-cookie.js +6 -0
  72. package/shell/app.scss +2 -1
  73. package/shell/app.ts +14 -0
  74. package/shell/dev/devHome/app.ts +2 -2
  75. package/shell/dev/slotShowcase/app.tsx +9 -9
  76. package/shell/header/app.tsx +3 -3
  77. package/shell/jest.config.js +1 -0
  78. package/shell/router/createRouter.test.tsx +50 -0
  79. package/shell/router/getAppRoutes.test.tsx +59 -0
  80. package/shell/setupTest.js +0 -35
  81. package/shell/site.config.dev.tsx +3 -3
  82. package/shell/site.config.test.tsx +16 -0
  83. package/shell/site.tsx +1 -1
  84. package/tools/dist/cli/intl-imports.test.js +146 -0
  85. package/tools/dist/cli/openedx.js +1 -15
  86. package/tools/dist/cli/utils/printUsage.js +0 -9
  87. package/tools/dist/eslint/base.eslint.config.js +1 -1
  88. package/tools/dist/jest/jest.config.js +1 -0
  89. package/tools/dist/types.js +0 -2
  90. package/tools/dist/webpack/common-config/all/getStylesheetRule.js +1 -1
  91. package/tools/dist/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
  92. package/tools/dist/webpack/webpack.config.build.js +1 -11
  93. package/tools/dist/webpack/webpack.config.dev.js +5 -11
  94. package/tools/dist/webpack/webpack.config.dev.shell.js +5 -11
  95. package/types.ts +21 -1
  96. package/config/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js +0 -108
  97. package/config/webpack/plugins/paragon-webpack-plugin/index.js +0 -7
  98. package/config/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.js +0 -64
  99. package/config/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.js +0 -53
  100. package/config/webpack/plugins/paragon-webpack-plugin/utils/index.js +0 -9
  101. package/config/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.js +0 -114
  102. package/config/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.js +0 -146
  103. package/config/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.js +0 -126
  104. package/config/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.js +0 -57
  105. package/config/webpack/types.js +0 -2
  106. package/config/webpack/utils/paragonUtils.js +0 -138
  107. package/eslint.config.js +0 -18
  108. package/frontend-base.d.ts +0 -8
  109. package/jest.config.js +0 -7
  110. package/openedx-frontend-base.tgz +0 -0
  111. package/runtime/react/hooks.ts +0 -106
  112. package/test-site/app.d.ts +0 -15
  113. package/test-site/dist/176.436443549ebb858db483.js +0 -2
  114. package/test-site/dist/176.436443549ebb858db483.js.map +0 -1
  115. package/test-site/dist/362.536eff787d2380fe246c.js +0 -2
  116. package/test-site/dist/362.536eff787d2380fe246c.js.map +0 -1
  117. package/test-site/dist/653.486966b108d224551296.js +0 -2
  118. package/test-site/dist/653.486966b108d224551296.js.map +0 -1
  119. package/test-site/dist/74e025d3fe9a7b7f8503054e2563b353.jpg +0 -0
  120. package/test-site/dist/806.323cf6496ad0a7fe73a7.js +0 -3
  121. package/test-site/dist/806.323cf6496ad0a7fe73a7.js.LICENSE.txt +0 -106
  122. package/test-site/dist/806.323cf6496ad0a7fe73a7.js.map +0 -1
  123. package/test-site/dist/95ec738c0b7faac5b5c9126794446bbd.svg +0 -4
  124. package/test-site/dist/app.612058b36c74787759ac.css +0 -61
  125. package/test-site/dist/app.612058b36c74787759ac.css.map +0 -1
  126. package/test-site/dist/app.612058b36c74787759ac.js +0 -2
  127. package/test-site/dist/app.612058b36c74787759ac.js.map +0 -1
  128. package/test-site/dist/cb28cdb1468c915e27e5cec9af64f22f.svg +0 -1
  129. package/test-site/dist/index.html +0 -1
  130. package/test-site/dist/report.html +0 -39
  131. package/test-site/dist/runtime.c7aeaf7b967496cb076f.js +0 -2
  132. package/test-site/dist/runtime.c7aeaf7b967496cb076f.js.map +0 -1
  133. package/test-site/eslint.config.js +0 -12
  134. package/test-site/package-lock.json +0 -19226
  135. package/test-site/package.json +0 -29
  136. package/test-site/public/index.html +0 -10
  137. package/test-site/site.config.build.tsx +0 -27
  138. package/test-site/site.config.dev.tsx +0 -27
  139. package/test-site/src/authenticated-page/AuthenticatedPage.tsx +0 -18
  140. package/test-site/src/authenticated-page/i18n/index.ts +0 -27
  141. package/test-site/src/authenticated-page/index.tsx +0 -28
  142. package/test-site/src/example-page/ExamplePage.tsx +0 -79
  143. package/test-site/src/example-page/Image.tsx +0 -11
  144. package/test-site/src/example-page/ParagonPreview.jsx +0 -66
  145. package/test-site/src/example-page/apple.jpg +0 -0
  146. package/test-site/src/example-page/apple.svg +0 -1
  147. package/test-site/src/example-page/index.ts +0 -16
  148. package/test-site/src/i18n/README.md +0 -3
  149. package/test-site/src/i18n/messages/frontend-app-sample/ar.json +0 -4
  150. package/test-site/src/i18n/messages/frontend-app-sample/eo.json +0 -1
  151. package/test-site/src/i18n/messages/frontend-app-sample/es_419.json +0 -4
  152. package/test-site/src/i18n/messages/frontend-component-emptylangs/ar.json +0 -1
  153. package/test-site/src/i18n/messages/frontend-component-singlelang/ar.json +0 -3
  154. package/test-site/src/iframe-widget/IframeWidget.tsx +0 -14
  155. package/test-site/src/iframe-widget/index.ts +0 -16
  156. package/test-site/src/index.tsx +0 -3
  157. package/test-site/src/messages.js +0 -11
  158. package/test-site/src/site.scss +0 -11
  159. package/test-site/tsconfig.json +0 -14
  160. package/tools/babel/babel.config.js +0 -27
  161. package/tools/babel.config.js +0 -3
  162. package/tools/cli/README.md +0 -29
  163. package/tools/cli/commands/pack.ts +0 -9
  164. package/tools/cli/commands/release.ts +0 -27
  165. package/tools/cli/commands/serve.ts +0 -43
  166. package/tools/cli/intl-imports.ts +0 -274
  167. package/tools/cli/openedx.ts +0 -101
  168. package/tools/cli/transifex-utils.ts +0 -75
  169. package/tools/cli/utils/ensureConfigFilenameOption.ts +0 -40
  170. package/tools/cli/utils/formatter.ts +0 -10
  171. package/tools/cli/utils/getResolvedConfigPath.ts +0 -23
  172. package/tools/cli/utils/prettyPrintTitle.ts +0 -15
  173. package/tools/cli/utils/printUsage.ts +0 -53
  174. package/tools/config-helpers/createConfig.ts +0 -8
  175. package/tools/config-helpers/createLintConfig.ts +0 -14
  176. package/tools/config-helpers/getBaseConfig.ts +0 -11
  177. package/tools/defaultConfigPaths.ts +0 -30
  178. package/tools/dist/cli/commands/pack.js +0 -14
  179. package/tools/dist/cli/commands/release.js +0 -28
  180. package/tools/dist/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js +0 -108
  181. package/tools/dist/webpack/plugins/paragon-webpack-plugin/index.js +0 -7
  182. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.js +0 -64
  183. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.js +0 -53
  184. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/index.js +0 -9
  185. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.js +0 -114
  186. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.js +0 -146
  187. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.js +0 -126
  188. package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.js +0 -57
  189. package/tools/dist/webpack/types.js +0 -2
  190. package/tools/dist/webpack/utils/paragonUtils.js +0 -138
  191. package/tools/eslint/base.eslint.config.js +0 -124
  192. package/tools/eslint/modules.d.ts +0 -5
  193. package/tools/eslint.config.js +0 -15
  194. package/tools/index.ts +0 -3
  195. package/tools/jest/jest.config.js +0 -30
  196. package/tools/jest.config.js +0 -19
  197. package/tools/tsconfig.json +0 -24
  198. package/tools/types.ts +0 -21
  199. package/tools/typescript/tsconfig.json +0 -32
  200. package/tools/webpack/common-config/README.md +0 -15
  201. package/tools/webpack/common-config/all/getCodeRules.ts +0 -51
  202. package/tools/webpack/common-config/all/getFileLoaderRules.ts +0 -23
  203. package/tools/webpack/common-config/all/getIgnoreWarnings.ts +0 -13
  204. package/tools/webpack/common-config/all/getImageMinimizer.ts +0 -26
  205. package/tools/webpack/common-config/all/getStylesheetRule.ts +0 -111
  206. package/tools/webpack/common-config/dev/getDevServer.ts +0 -35
  207. package/tools/webpack/common-config/index.ts +0 -6
  208. package/tools/webpack/common-config/site/getHtmlWebpackPlugin.ts +0 -11
  209. package/tools/webpack/modules.d.ts +0 -6
  210. package/tools/webpack/plugins/html-webpack-new-relic-plugin/HtmlWebpackNewRelicPlugin.ts +0 -102
  211. package/tools/webpack/plugins/html-webpack-new-relic-plugin/LICENSE +0 -21
  212. package/tools/webpack/plugins/html-webpack-new-relic-plugin/README.md +0 -7
  213. package/tools/webpack/plugins/html-webpack-new-relic-plugin/index.js +0 -3
  214. package/tools/webpack/plugins/html-webpack-new-relic-plugin/test/fixtures/entry.js +0 -1
  215. package/tools/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.ts +0 -134
  216. package/tools/webpack/plugins/paragon-webpack-plugin/index.ts +0 -3
  217. package/tools/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.ts +0 -71
  218. package/tools/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.ts +0 -72
  219. package/tools/webpack/plugins/paragon-webpack-plugin/utils/index.ts +0 -6
  220. package/tools/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.ts +0 -131
  221. package/tools/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.ts +0 -144
  222. package/tools/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.ts +0 -106
  223. package/tools/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.ts +0 -54
  224. package/tools/webpack/types.ts +0 -69
  225. package/tools/webpack/utils/getLocalAliases.ts +0 -65
  226. package/tools/webpack/utils/getPublicPath.ts +0 -3
  227. package/tools/webpack/utils/getResolvedSiteConfigPath.ts +0 -28
  228. package/tools/webpack/utils/paragonUtils.ts +0 -152
  229. package/tools/webpack/webpack.config.build.ts +0 -93
  230. package/tools/webpack/webpack.config.dev.shell.ts +0 -122
  231. package/tools/webpack/webpack.config.dev.ts +0 -90
  232. package/tsconfig.json +0 -23
@@ -0,0 +1,178 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { SlotOperation } from '../types';
3
+ import * as slotHooks from '../hooks';
4
+ import * as slotUtils from '../utils';
5
+ import { WidgetAppendOperation, WidgetOperationTypes } from '../widget';
6
+ import { useLayoutForSlotId, useLayoutOptions, useLayoutOptionsForId } from './hooks';
7
+ import { LayoutOperationTypes, LayoutReplaceOperation } from './types';
8
+
9
+ jest.mock('../hooks'); // mocks the useSlotOperations hook
10
+
11
+ function MockLayout() {
12
+ return (
13
+ <div>Mock Component</div>
14
+ );
15
+ }
16
+
17
+ describe('useLayoutForSlotId', () => {
18
+ beforeEach(() => {
19
+ jest.restoreAllMocks();
20
+ });
21
+
22
+ it('should return null when no replace layout operations are provided', () => {
23
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([]);
24
+ const { result } = renderHook(() => useLayoutForSlotId('test-slot.ui'));
25
+ expect(result.current).toBeNull();
26
+ });
27
+
28
+ it('should return a layout component when replace layout operation is present', () => {
29
+ const operation: LayoutReplaceOperation = {
30
+ slotId: 'test-slot.ui',
31
+ op: LayoutOperationTypes.REPLACE,
32
+ component: MockLayout
33
+ };
34
+
35
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation]);
36
+ const { result } = renderHook(() => useLayoutForSlotId('test-slot.ui'));
37
+ expect(result.current).toEqual(MockLayout);
38
+ });
39
+
40
+ it('should return null if the operation condition is not satisfied', () => {
41
+ const operation: LayoutReplaceOperation = {
42
+ slotId: 'test-slot.ui',
43
+ op: LayoutOperationTypes.REPLACE,
44
+ component: MockLayout
45
+ };
46
+
47
+ jest.spyOn(slotUtils, 'isSlotOperationConditionSatisfied').mockReturnValue(false);
48
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation]);
49
+ const { result } = renderHook(() => useLayoutForSlotId('test-slot.ui'));
50
+ expect(result.current).toBeNull();
51
+ });
52
+
53
+ it('should return a layout element when replace layout operation is present', () => {
54
+ const operation: LayoutReplaceOperation = {
55
+ slotId: 'test-slot.ui',
56
+ op: LayoutOperationTypes.REPLACE,
57
+ element: <div>Mock layout</div>
58
+ };
59
+
60
+ // @ts-expect-error This is an intentionally malformed operation to test what happens when component/element are not present.
61
+ const malformedOperation: SlotOperation = {
62
+ slotId: 'test-slot.ui',
63
+ op: LayoutOperationTypes.REPLACE,
64
+ };
65
+
66
+ const unrelatedOperation: WidgetAppendOperation = {
67
+ slotId: 'test-slot.ui',
68
+ id: 'test-slot.widget1',
69
+ op: WidgetOperationTypes.APPEND,
70
+ element: <div>Widget</div>
71
+ };
72
+
73
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation, malformedOperation, unrelatedOperation]);
74
+ const { result } = renderHook(() => useLayoutForSlotId('test-slot.ui'));
75
+ expect(result.current).toEqual(<div>Mock layout</div>);
76
+ });
77
+
78
+ it('should return the last layout component in the operations list', () => {
79
+ const SecondMockLayout = () => <div>First Component</div>;
80
+
81
+ const operation1: LayoutReplaceOperation = {
82
+ slotId: 'test-slot.ui',
83
+ op: LayoutOperationTypes.REPLACE,
84
+ component: MockLayout
85
+ };
86
+
87
+ const operation2: LayoutReplaceOperation = {
88
+ slotId: 'test-slot.ui',
89
+ op: LayoutOperationTypes.REPLACE,
90
+ component: SecondMockLayout
91
+ };
92
+
93
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation1, operation2]);
94
+ const { result } = renderHook(() => useLayoutForSlotId('test-slot.ui'));
95
+ expect(result.current).toEqual(SecondMockLayout);
96
+ });
97
+ });
98
+
99
+ describe('useLayoutOptionsForId', () => {
100
+ beforeEach(() => {
101
+ jest.restoreAllMocks();
102
+ });
103
+
104
+ it('should return an empty object whent here are no layout options operations', () => {
105
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([]);
106
+ const { result } = renderHook(() => useLayoutOptionsForId('test-slot.ui'));
107
+ expect(result.current).toEqual({});
108
+ });
109
+
110
+ it('should return an object containing options from a single layout options operation', () => {
111
+ const mockOptions = { option1: 'value1' };
112
+ const operation = {
113
+ slotId: 'test-slot.ui',
114
+ op: LayoutOperationTypes.OPTIONS,
115
+ options: mockOptions
116
+ };
117
+
118
+ const otherOperation: LayoutReplaceOperation = {
119
+ slotId: 'test-slot.ui',
120
+ op: LayoutOperationTypes.REPLACE,
121
+ element: <div>Layout</div>
122
+ };
123
+
124
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation, otherOperation]);
125
+ const { result } = renderHook(() => useLayoutOptionsForId('test-slot.ui'));
126
+ expect(result.current).toEqual(mockOptions);
127
+ });
128
+
129
+ describe('when various failure conditions are true', () => {
130
+ const mockOptions = { option1: 'value1' };
131
+ const operation = {
132
+ slotId: 'test-slot.ui',
133
+ op: LayoutOperationTypes.OPTIONS,
134
+ options: mockOptions
135
+ };
136
+
137
+ it('should return an empty object if the operation condition is not satisfied.', () => {
138
+ jest.spyOn(slotUtils, 'isSlotOperationConditionSatisfied').mockReturnValue(false);
139
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation]);
140
+ const { result } = renderHook(() => useLayoutOptionsForId('test-slot.ui'));
141
+ expect(result.current).toEqual({});
142
+ });
143
+ });
144
+
145
+ it('should return merged layout options from multiple operations', () => {
146
+ const mockOptions1 = { option1: 'value1', optionOverride: 'foo', };
147
+ const mockOptions2 = { option2: 'value2', optionOverride: 'bar' };
148
+ const operation1 = {
149
+ slotId: 'test-slot.ui',
150
+ op: LayoutOperationTypes.OPTIONS,
151
+ options: mockOptions1
152
+ };
153
+ const operation2 = {
154
+ slotId: 'test-slot.ui',
155
+ op: LayoutOperationTypes.OPTIONS,
156
+ options: mockOptions2
157
+ };
158
+
159
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation1, operation2]);
160
+ const { result } = renderHook(() => useLayoutOptionsForId('test-slot.ui'));
161
+ expect(result.current).toEqual({ option1: 'value1', option2: 'value2', optionOverride: 'bar' });
162
+ });
163
+ });
164
+
165
+ describe('useLayoutOptions', () => {
166
+ it('should call useLayoutOptionsForId with the correct id ', () => {
167
+ const mockOptions1 = { option1: 'value1' };
168
+ const operation = {
169
+ slotId: 'test-slot.ui',
170
+ op: LayoutOperationTypes.OPTIONS,
171
+ options: mockOptions1
172
+ };
173
+ (slotHooks.useSlotOperations as jest.Mock).mockReturnValue([operation]);
174
+ jest.spyOn(slotHooks, 'useSlotContext').mockReturnValue({ id: 'test-slot.ui' });
175
+ const { result } = renderHook(() => useLayoutOptions());
176
+ expect(result.current).toEqual({ option1: 'value1' });
177
+ });
178
+ });
@@ -0,0 +1,67 @@
1
+ import { SlotOperation } from '../types';
2
+ import { WidgetOperationTypes } from '../widget';
3
+ import { LayoutOperationTypes } from './types';
4
+ import { isLayoutOperation, isLayoutOptionsOperation, isLayoutReplaceOperation } from './utils';
5
+
6
+ describe('UI Layout Operation utilities', () => {
7
+ describe('isLayoutOperation', () => {
8
+ it('should return true for valid LayoutOperation', () => {
9
+ const operation: SlotOperation = {
10
+ slotId: 'mock.slot.ui',
11
+ op: LayoutOperationTypes.OPTIONS,
12
+ options: {},
13
+ };
14
+ expect(isLayoutOperation(operation)).toBe(true);
15
+ });
16
+
17
+ it('should return false for invalid LayoutOperation', () => {
18
+ const operation: SlotOperation = {
19
+ slotId: 'mock.slot.ui',
20
+ op: WidgetOperationTypes.APPEND,
21
+ id: 'mock.slot.widget',
22
+ element: '',
23
+ };
24
+ expect(isLayoutOperation(operation)).toBe(false);
25
+ });
26
+ });
27
+
28
+ describe('isLayoutOptionsOperation', () => {
29
+ it('should return true for valid LayoutOptionsOperation', () => {
30
+ const operation: SlotOperation = {
31
+ slotId: 'mock.slot.ui',
32
+ op: LayoutOperationTypes.OPTIONS,
33
+ options: {},
34
+ };
35
+ expect(isLayoutOptionsOperation(operation)).toBe(true);
36
+ });
37
+
38
+ it('should return false for invalid LayoutOptionsOperation', () => {
39
+ const operation: SlotOperation = {
40
+ slotId: 'mock.slot.ui',
41
+ op: LayoutOperationTypes.REPLACE,
42
+ element: '',
43
+ };
44
+ expect(isLayoutOptionsOperation(operation)).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe('isLayoutReplaceOperation', () => {
49
+ it('should return true for valid LayoutReplaceOperation', () => {
50
+ const operation: SlotOperation = {
51
+ slotId: 'mock.slot.ui',
52
+ op: LayoutOperationTypes.REPLACE,
53
+ element: '',
54
+ };
55
+ expect(isLayoutReplaceOperation(operation)).toBe(true);
56
+ });
57
+
58
+ it('should return false for invalid LayoutReplaceOperation', () => {
59
+ const operation: SlotOperation = {
60
+ slotId: 'mock.slot.ui',
61
+ op: LayoutOperationTypes.OPTIONS,
62
+ options: {},
63
+ };
64
+ expect(isLayoutReplaceOperation(operation)).toBe(false);
65
+ });
66
+ });
67
+ });
@@ -9,6 +9,7 @@ import { WidgetOperation } from './widget/types';
9
9
 
10
10
  export interface SlotOperationCondition {
11
11
  active?: string[],
12
+ inactive?: string[],
12
13
  authenticated?: boolean,
13
14
  callback?: () => boolean,
14
15
  }
@@ -0,0 +1,64 @@
1
+ import { getSiteConfig } from '../config';
2
+ import { SlotOperation } from './types';
3
+ import { getSlotOperations } from './utils';
4
+ import { WidgetOperationTypes } from '.';
5
+
6
+ jest.mock('../config');
7
+
8
+ describe('getSlotOperations', () => {
9
+ it('should return an empty array if no apps are configured', () => {
10
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: [] });
11
+ const result = getSlotOperations('test-slot.ui');
12
+ expect(result).toEqual([]);
13
+ });
14
+
15
+ it('should return an empty array if no slots are present in apps', () => {
16
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: [{ slots: [] }] });
17
+ const result = getSlotOperations('test-slot.ui');
18
+ expect(result).toEqual([]);
19
+ });
20
+
21
+ it('should return an empty array if no matching slotId is found', () => {
22
+ const mockSlots: SlotOperation[] = [{ slotId: 'other-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget1', element: '' }];
23
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: [{ slots: mockSlots }] });
24
+ const result = getSlotOperations('test-slot.ui');
25
+ expect(result).toEqual([]);
26
+ });
27
+
28
+ it('should return the correct slot operations for a given slotId', () => {
29
+ const mockSlots: SlotOperation[] = [
30
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget1', element: '' },
31
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget2', element: '' },
32
+ { slotId: 'other-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget3', element: '' },
33
+ ];
34
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: [{ slots: mockSlots }] });
35
+ const result = getSlotOperations('test-slot.ui');
36
+ expect(result).toEqual([
37
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget1', element: '' },
38
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget2', element: '' },
39
+ ]);
40
+ });
41
+
42
+ it('should handle multiple apps with slots correctly', () => {
43
+ (getSiteConfig as jest.Mock).mockReturnValue({
44
+ apps: [
45
+ {
46
+ slots: [
47
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget1', element: '' },
48
+ ]
49
+ },
50
+ {
51
+ slots: [
52
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget2', element: '' },
53
+ { slotId: 'other-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget3', element: '' },
54
+ ]
55
+ }
56
+ ]
57
+ });
58
+ const result = getSlotOperations('test-slot.ui');
59
+ expect(result).toEqual([
60
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget1', element: '' },
61
+ { slotId: 'test-slot.ui', op: WidgetOperationTypes.APPEND, id: 'widget2', element: '' },
62
+ ]);
63
+ });
64
+ });
@@ -35,18 +35,37 @@ export function isSlotOperationConditionSatisfied(operation: SlotOperation) {
35
35
  }
36
36
  }
37
37
 
38
- if (condition?.active !== undefined) {
39
- let activeConditionRoleFound = false;
38
+ if (condition?.active !== undefined || condition?.inactive !== undefined) {
40
39
  const activeRoles: string[] = getActiveRoles();
41
- for (const conditionRole of condition.active) {
42
- if (activeRoles.includes(conditionRole)) {
43
- activeConditionRoleFound = true;
44
- break;
40
+
41
+ if (condition?.active !== undefined) {
42
+ let activeConditionRoleFound = false;
43
+ for (const conditionRole of condition.active) {
44
+ if (activeRoles.includes(conditionRole)) {
45
+ activeConditionRoleFound = true;
46
+ break;
47
+ }
48
+ }
49
+
50
+ // If we couldn't find an active role in our list, then we've failed this condition.
51
+ if (!activeConditionRoleFound) {
52
+ return false;
45
53
  }
46
54
  }
47
- // If we couldn't find an active role in our list, then we've failed this condition.
48
- if (!activeConditionRoleFound) {
49
- return false;
55
+
56
+ if (condition?.inactive !== undefined) {
57
+ let inactiveConditionRoleFound = false;
58
+ for (const conditionRole of condition.inactive) {
59
+ if (activeRoles.includes(conditionRole)) {
60
+ inactiveConditionRoleFound = true;
61
+ break;
62
+ }
63
+ }
64
+
65
+ // If we find an active role from our inactive list, then we've failed this condition.
66
+ if (inactiveConditionRoleFound) {
67
+ return false;
68
+ }
50
69
  }
51
70
  }
52
71
 
@@ -127,7 +127,7 @@ export function dispatchUnmountedEvent() {
127
127
  */
128
128
  export function useElementSize() {
129
129
  // Holds a reference to the ResizeObserver
130
- const observerRef = useRef<ResizeObserver>();
130
+ const observerRef = useRef<ResizeObserver | null>(null);
131
131
 
132
132
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
133
133
  const [offset, setOffset] = useState({ x: 0, y: 0 });
@@ -0,0 +1,66 @@
1
+ import {
2
+ getAnalyticsService,
3
+ MockAnalyticsService,
4
+ sendTrackEvent,
5
+ } from '../analytics';
6
+ import {
7
+ ensureAuthenticatedUser,
8
+ getAuthService,
9
+ getLoginRedirectUrl,
10
+ MockAuthService,
11
+ setAuthenticatedUser,
12
+ } from '../auth';
13
+ import {
14
+ getLoggingService,
15
+ logInfo,
16
+ MockLoggingService,
17
+ } from '../logging';
18
+ import initializeMockApp from './initializeMockApp';
19
+
20
+ describe('initializeMockApp', () => {
21
+ it('should create mock analytics, auth, and logging services, and a real i18n service', () => {
22
+ const {
23
+ analyticsService,
24
+ authService,
25
+ loggingService,
26
+ } = initializeMockApp();
27
+
28
+ expect(getAnalyticsService()).toBeInstanceOf(MockAnalyticsService);
29
+ expect(getAuthService()).toBeInstanceOf(MockAuthService);
30
+ expect(getLoggingService()).toBeInstanceOf(MockLoggingService);
31
+
32
+ const customAttributes = { custom: 'attribute' };
33
+
34
+ // Next, test a representative sample of functionality to prove mocking is working the way we hope.
35
+
36
+ // Analytics
37
+ sendTrackEvent('testEvent', customAttributes);
38
+ expect(analyticsService.sendTrackEvent).toHaveBeenCalledWith('testEvent', customAttributes);
39
+ // The mock analytics service calls checkIdentifyCalled when sendTrackEvent is called. Prove
40
+ // the jest.fn() works.
41
+ expect(analyticsService.checkIdentifyCalled).toHaveBeenCalledTimes(1);
42
+
43
+ // Auth
44
+ getLoginRedirectUrl('http://localhost/redirect/url');
45
+ setAuthenticatedUser({
46
+ userId: 'abc123',
47
+ username: 'Mock User',
48
+ roles: [],
49
+ administrator: false,
50
+ name: 'mock tester',
51
+ });
52
+ ensureAuthenticatedUser();
53
+
54
+ expect(authService.getLoginRedirectUrl).toHaveBeenCalledWith('http://localhost/redirect/url');
55
+ expect(authService.ensureAuthenticatedUser).toHaveBeenCalled();
56
+ // ensureAuthenticatedUser calls a few other things under the covers. Prove our mocking is
57
+ // working as expected across multiple levels.
58
+ expect(authService.fetchAuthenticatedUser).toHaveBeenCalled();
59
+ expect(authService.getAuthenticatedUser).toHaveBeenCalled();
60
+ expect(authService.redirectToLogin).not.toHaveBeenCalled();
61
+
62
+ // Logging
63
+ logInfo('logging info', customAttributes);
64
+ expect(loggingService.logInfo).toHaveBeenCalledWith('logging info', customAttributes);
65
+ });
66
+ });
@@ -1,5 +1,7 @@
1
1
  import siteConfig from 'site.config';
2
2
 
3
+ import { LocalizedMessages, User } from '../../types';
4
+
3
5
  import { configureAnalytics, MockAnalyticsService } from '../analytics';
4
6
  import { configureAuth, MockAuthService, setAuthenticatedUser } from '../auth';
5
7
  import { getSiteConfig, mergeSiteConfig } from '../config';
@@ -47,6 +49,9 @@ import mockMessages from './mockMessages';
47
49
  export default function initializeMockApp({
48
50
  messages = mockMessages,
49
51
  authenticatedUser = null,
52
+ }: {
53
+ messages?: LocalizedMessages,
54
+ authenticatedUser?: User | null,
50
55
  } = {}) {
51
56
  const config = siteConfig;
52
57
  mergeSiteConfig(config);
@@ -0,0 +1,116 @@
1
+ import { waitFor } from '@testing-library/react';
2
+ import {
3
+ camelCaseObject,
4
+ convertKeyNames,
5
+ getQueryParameters,
6
+ modifyObjectKeys,
7
+ snakeCaseObject,
8
+ } from '.';
9
+
10
+ describe('modifyObjectKeys', () => {
11
+ it('should use the provided modify function to change all keys in and object and its children', () => {
12
+ function meowKeys(key) {
13
+ return `${key}Meow`;
14
+ }
15
+
16
+ const result = modifyObjectKeys(
17
+ {
18
+ one: undefined,
19
+ two: null,
20
+ three: '',
21
+ four: 0,
22
+ five: NaN,
23
+ six: [1, 2, { seven: 'woof' }],
24
+ eight: { nine: { ten: 'bark' }, eleven: true },
25
+ },
26
+ meowKeys,
27
+ );
28
+
29
+ expect(result).toEqual({
30
+ oneMeow: undefined,
31
+ twoMeow: null,
32
+ threeMeow: '',
33
+ fourMeow: 0,
34
+ fiveMeow: NaN,
35
+ sixMeow: [1, 2, { sevenMeow: 'woof' }],
36
+ eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
37
+ });
38
+ });
39
+ });
40
+
41
+ describe('camelCaseObject', () => {
42
+ it('should make everything camelCase', () => {
43
+ const result = camelCaseObject({
44
+ what_now: 'brown cow',
45
+ but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
46
+ 'dot.dot.dot': 123,
47
+ });
48
+
49
+ expect(result).toEqual({
50
+ whatNow: 'brown cow',
51
+ butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
52
+ dotDotDot: 123,
53
+ });
54
+ });
55
+ });
56
+
57
+ describe('snakeCaseObject', () => {
58
+ it('should make everything snake_case', () => {
59
+ const result = snakeCaseObject({
60
+ whatNow: 'brown cow',
61
+ butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
62
+ 'dot.dot.dot': 123,
63
+ });
64
+
65
+ expect(result).toEqual({
66
+ what_now: 'brown cow',
67
+ but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
68
+ dot_dot_dot: 123,
69
+ });
70
+ });
71
+ });
72
+
73
+ describe('convertKeyNames', () => {
74
+ it('should replace the specified keynames', () => {
75
+ const result = convertKeyNames(
76
+ {
77
+ one: { two: { three: 'four' } },
78
+ five: 'six',
79
+ },
80
+ {
81
+ two: 'blue',
82
+ five: 'alive',
83
+ seven: 'heaven',
84
+ },
85
+ );
86
+
87
+ expect(result).toEqual({
88
+ one: { blue: { three: 'four' } },
89
+ alive: 'six',
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('getQueryParameters', () => {
95
+ it('should use window location by default', () => {
96
+ expect(global.location.search).toEqual('');
97
+ expect(getQueryParameters()).toEqual({});
98
+ });
99
+
100
+ it('should make an empty object with no query string', () => {
101
+ expect(getQueryParameters('')).toEqual({});
102
+ });
103
+
104
+ it('should make an object with one key value pair', () => {
105
+ expect(getQueryParameters('?foo=bar')).toEqual({
106
+ foo: 'bar',
107
+ });
108
+ });
109
+
110
+ it('should make an object with one key value pair', () => {
111
+ expect(getQueryParameters('?foo=bar&baz=1')).toEqual({
112
+ foo: 'bar',
113
+ baz: '1',
114
+ });
115
+ });
116
+ });
@@ -0,0 +1,32 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render } from '@testing-library/react';
3
+ import Logo from './Logo';
4
+
5
+ describe('Logo component', () => {
6
+ it('renders the image with default URL when no props are provided', async () => {
7
+ const { getByRole, queryByRole } = render(<Logo />);
8
+ const image = getByRole('img');
9
+ expect(image).toHaveAttribute('src', 'https://edx-cdn.org/v3/default/logo.svg');
10
+ const link = queryByRole('link');
11
+ expect(link).toBeNull();
12
+ });
13
+
14
+ it('renders the image with provided imageUrl', () => {
15
+ const testUrl = 'https://example.com/test-logo.svg';
16
+ const { getByRole, queryByRole } = render(<Logo imageUrl={testUrl} />);
17
+ const image = getByRole('img');
18
+ expect(image).toHaveAttribute('src', testUrl);
19
+ const link = queryByRole('link');
20
+ expect(link).toBeNull();
21
+ });
22
+
23
+ it('renders the image wrapped in a Hyperlink when destinationUrl is provided', () => {
24
+ const testDestinationUrl = 'https://example.com';
25
+ const { getByRole } = render(<Logo destinationUrl={testDestinationUrl} />);
26
+ const link = getByRole('link');
27
+ expect(link).toHaveAttribute('href', testDestinationUrl);
28
+ const image = getByRole('img');
29
+ expect(image).toBeInTheDocument();
30
+ expect(image).toHaveAttribute('src', 'https://edx-cdn.org/v3/default/logo.svg');
31
+ });
32
+ });
@@ -0,0 +1 @@
1
+ export default 'FileMock';
@@ -0,0 +1 @@
1
+ export default 'SvgURL';
@@ -0,0 +1,6 @@
1
+ const mockCookiesImplementation = {
2
+ get: jest.fn(),
3
+ remove: jest.fn(),
4
+ };
5
+
6
+ module.exports = () => mockCookiesImplementation;
package/shell/app.scss CHANGED
@@ -1,4 +1,5 @@
1
- @use "@openedx/paragon/scss/core/core.scss";
1
+ @use "@openedx/paragon/dist/core.min.css";
2
+ @use "@openedx/paragon/dist/light.min.css";
2
3
 
3
4
  .flex-basis-0 {
4
5
  flex-basis: 0 !important;
package/shell/app.ts CHANGED
@@ -3,6 +3,14 @@ import { App } from '../types';
3
3
  import { Footer } from './footer';
4
4
  import { Header } from './header';
5
5
 
6
+ const inactive = [
7
+ 'org.openedx.frontend.role.login',
8
+ 'org.openedx.frontend.role.register',
9
+ 'org.openedx.frontend.role.resetPassword',
10
+ 'org.openedx.frontend.role.confirmPassword',
11
+ 'org.openedx.frontend.role.welcome'
12
+ ];
13
+
6
14
  const app: App = {
7
15
  appId: 'org.openedx.frontend.app.shell',
8
16
  slots: [
@@ -11,12 +19,18 @@ const app: App = {
11
19
  id: 'org.openedx.frontend.widget.header.main.v1',
12
20
  op: WidgetOperationTypes.APPEND,
13
21
  component: Header,
22
+ condition: {
23
+ inactive,
24
+ }
14
25
  },
15
26
  {
16
27
  slotId: 'org.openedx.frontend.slot.footer.main.v1',
17
28
  id: 'org.openedx.frontend.widget.footer.main.v1',
18
29
  op: WidgetOperationTypes.APPEND,
19
30
  component: Footer,
31
+ condition: {
32
+ inactive,
33
+ }
20
34
  },
21
35
  ]
22
36
  };
@@ -6,10 +6,10 @@ const app: App = {
6
6
  appId: 'org.openedx.frontend.app.dev.home',
7
7
  routes: [{
8
8
  path: '/',
9
- id: 'dev.home',
9
+ id: 'org.openedx.frontend.route.dev.home',
10
10
  Component: HomePage,
11
11
  handle: {
12
- role: 'home'
12
+ role: 'org.openedx.frontend.role.devHome'
13
13
  }
14
14
  }],
15
15
  messages,