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

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 (164) hide show
  1. package/config/types.js +0 -2
  2. package/config/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
  3. package/package.json +8 -1
  4. package/runtime/__mocks__/file.js +1 -0
  5. package/runtime/__mocks__/svg.js +1 -0
  6. package/runtime/__mocks__/universal-cookie.js +6 -0
  7. package/runtime/analytics/interface.test.js +242 -0
  8. package/runtime/auth/AxiosJwtAuthService.test.jsx +1076 -0
  9. package/runtime/auth/interceptors/createRetryInterceptor.test.js +23 -0
  10. package/runtime/config/getExternalLinkUrl.test.js +76 -0
  11. package/runtime/i18n/lib.test.js +230 -0
  12. package/runtime/initialize.async.function.config.test.js +43 -0
  13. package/runtime/initialize.const.config.test.js +41 -0
  14. package/runtime/initialize.function.config.test.js +41 -0
  15. package/runtime/initialize.test.js +356 -0
  16. package/runtime/logging/NewRelicLoggingService.test.js +214 -0
  17. package/runtime/react/AuthenticatedPageRoute.test.jsx +135 -0
  18. package/runtime/react/ErrorBoundary.test.jsx +83 -0
  19. package/runtime/react/SiteProvider.test.jsx +66 -0
  20. package/runtime/react/hooks.test.jsx +104 -0
  21. package/runtime/routing/utils.test.ts +7 -0
  22. package/runtime/scripts/GoogleAnalyticsLoader.test.ts +77 -0
  23. package/runtime/site.config.test.tsx +33 -0
  24. package/runtime/slots/Slot.test.tsx +40 -0
  25. package/runtime/slots/layout/DefaultSlotLayout.test.tsx +31 -0
  26. package/runtime/slots/layout/hooks.test.tsx +178 -0
  27. package/runtime/slots/layout/utils.test.ts +67 -0
  28. package/runtime/slots/types.ts +1 -0
  29. package/runtime/slots/utils.test.ts +64 -0
  30. package/runtime/slots/utils.ts +28 -9
  31. package/runtime/testing/initializeMockApp.test.ts +66 -0
  32. package/runtime/utils.test.js +116 -0
  33. package/shell/Logo.test.tsx +32 -0
  34. package/shell/__mocks__/file.js +1 -0
  35. package/shell/__mocks__/svg.js +1 -0
  36. package/shell/__mocks__/universal-cookie.js +6 -0
  37. package/shell/app.ts +14 -0
  38. package/shell/dev/devHome/app.ts +2 -2
  39. package/shell/dev/slotShowcase/app.tsx +9 -9
  40. package/shell/header/app.tsx +3 -3
  41. package/shell/router/createRouter.test.tsx +50 -0
  42. package/shell/router/getAppRoutes.test.tsx +59 -0
  43. package/shell/site.config.dev.tsx +3 -3
  44. package/shell/site.config.test.tsx +16 -0
  45. package/tools/dist/cli/intl-imports.test.js +146 -0
  46. package/tools/dist/cli/openedx.js +1 -15
  47. package/tools/dist/cli/utils/printUsage.js +0 -9
  48. package/tools/dist/types.js +0 -2
  49. package/tools/dist/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
  50. package/types.ts +1 -1
  51. package/eslint.config.js +0 -18
  52. package/frontend-base.d.ts +0 -8
  53. package/jest.config.js +0 -7
  54. package/openedx-frontend-base.tgz +0 -0
  55. package/test-site/app.d.ts +0 -15
  56. package/test-site/dist/176.436443549ebb858db483.js +0 -2
  57. package/test-site/dist/176.436443549ebb858db483.js.map +0 -1
  58. package/test-site/dist/362.536eff787d2380fe246c.js +0 -2
  59. package/test-site/dist/362.536eff787d2380fe246c.js.map +0 -1
  60. package/test-site/dist/653.486966b108d224551296.js +0 -2
  61. package/test-site/dist/653.486966b108d224551296.js.map +0 -1
  62. package/test-site/dist/74e025d3fe9a7b7f8503054e2563b353.jpg +0 -0
  63. package/test-site/dist/806.323cf6496ad0a7fe73a7.js +0 -3
  64. package/test-site/dist/806.323cf6496ad0a7fe73a7.js.LICENSE.txt +0 -106
  65. package/test-site/dist/806.323cf6496ad0a7fe73a7.js.map +0 -1
  66. package/test-site/dist/95ec738c0b7faac5b5c9126794446bbd.svg +0 -4
  67. package/test-site/dist/app.612058b36c74787759ac.css +0 -61
  68. package/test-site/dist/app.612058b36c74787759ac.css.map +0 -1
  69. package/test-site/dist/app.612058b36c74787759ac.js +0 -2
  70. package/test-site/dist/app.612058b36c74787759ac.js.map +0 -1
  71. package/test-site/dist/cb28cdb1468c915e27e5cec9af64f22f.svg +0 -1
  72. package/test-site/dist/index.html +0 -1
  73. package/test-site/dist/report.html +0 -39
  74. package/test-site/dist/runtime.c7aeaf7b967496cb076f.js +0 -2
  75. package/test-site/dist/runtime.c7aeaf7b967496cb076f.js.map +0 -1
  76. package/test-site/eslint.config.js +0 -12
  77. package/test-site/package-lock.json +0 -19226
  78. package/test-site/package.json +0 -29
  79. package/test-site/public/index.html +0 -10
  80. package/test-site/site.config.build.tsx +0 -27
  81. package/test-site/site.config.dev.tsx +0 -27
  82. package/test-site/src/authenticated-page/AuthenticatedPage.tsx +0 -18
  83. package/test-site/src/authenticated-page/i18n/index.ts +0 -27
  84. package/test-site/src/authenticated-page/index.tsx +0 -28
  85. package/test-site/src/example-page/ExamplePage.tsx +0 -79
  86. package/test-site/src/example-page/Image.tsx +0 -11
  87. package/test-site/src/example-page/ParagonPreview.jsx +0 -66
  88. package/test-site/src/example-page/apple.jpg +0 -0
  89. package/test-site/src/example-page/apple.svg +0 -1
  90. package/test-site/src/example-page/index.ts +0 -16
  91. package/test-site/src/i18n/README.md +0 -3
  92. package/test-site/src/i18n/messages/frontend-app-sample/ar.json +0 -4
  93. package/test-site/src/i18n/messages/frontend-app-sample/eo.json +0 -1
  94. package/test-site/src/i18n/messages/frontend-app-sample/es_419.json +0 -4
  95. package/test-site/src/i18n/messages/frontend-component-emptylangs/ar.json +0 -1
  96. package/test-site/src/i18n/messages/frontend-component-singlelang/ar.json +0 -3
  97. package/test-site/src/iframe-widget/IframeWidget.tsx +0 -14
  98. package/test-site/src/iframe-widget/index.ts +0 -16
  99. package/test-site/src/index.tsx +0 -3
  100. package/test-site/src/messages.js +0 -11
  101. package/test-site/src/site.scss +0 -11
  102. package/test-site/tsconfig.json +0 -14
  103. package/tools/babel/babel.config.js +0 -27
  104. package/tools/babel.config.js +0 -3
  105. package/tools/cli/README.md +0 -29
  106. package/tools/cli/commands/pack.ts +0 -9
  107. package/tools/cli/commands/release.ts +0 -27
  108. package/tools/cli/commands/serve.ts +0 -43
  109. package/tools/cli/intl-imports.ts +0 -274
  110. package/tools/cli/openedx.ts +0 -101
  111. package/tools/cli/transifex-utils.ts +0 -75
  112. package/tools/cli/utils/ensureConfigFilenameOption.ts +0 -40
  113. package/tools/cli/utils/formatter.ts +0 -10
  114. package/tools/cli/utils/getResolvedConfigPath.ts +0 -23
  115. package/tools/cli/utils/prettyPrintTitle.ts +0 -15
  116. package/tools/cli/utils/printUsage.ts +0 -53
  117. package/tools/config-helpers/createConfig.ts +0 -8
  118. package/tools/config-helpers/createLintConfig.ts +0 -14
  119. package/tools/config-helpers/getBaseConfig.ts +0 -11
  120. package/tools/defaultConfigPaths.ts +0 -30
  121. package/tools/dist/cli/commands/pack.js +0 -14
  122. package/tools/dist/cli/commands/release.js +0 -28
  123. package/tools/eslint/base.eslint.config.js +0 -124
  124. package/tools/eslint/modules.d.ts +0 -5
  125. package/tools/eslint.config.js +0 -15
  126. package/tools/index.ts +0 -3
  127. package/tools/jest/jest.config.js +0 -30
  128. package/tools/jest.config.js +0 -19
  129. package/tools/tsconfig.json +0 -24
  130. package/tools/types.ts +0 -21
  131. package/tools/typescript/tsconfig.json +0 -32
  132. package/tools/webpack/common-config/README.md +0 -15
  133. package/tools/webpack/common-config/all/getCodeRules.ts +0 -51
  134. package/tools/webpack/common-config/all/getFileLoaderRules.ts +0 -23
  135. package/tools/webpack/common-config/all/getIgnoreWarnings.ts +0 -13
  136. package/tools/webpack/common-config/all/getImageMinimizer.ts +0 -26
  137. package/tools/webpack/common-config/all/getStylesheetRule.ts +0 -111
  138. package/tools/webpack/common-config/dev/getDevServer.ts +0 -35
  139. package/tools/webpack/common-config/index.ts +0 -6
  140. package/tools/webpack/common-config/site/getHtmlWebpackPlugin.ts +0 -11
  141. package/tools/webpack/modules.d.ts +0 -6
  142. package/tools/webpack/plugins/html-webpack-new-relic-plugin/HtmlWebpackNewRelicPlugin.ts +0 -102
  143. package/tools/webpack/plugins/html-webpack-new-relic-plugin/LICENSE +0 -21
  144. package/tools/webpack/plugins/html-webpack-new-relic-plugin/README.md +0 -7
  145. package/tools/webpack/plugins/html-webpack-new-relic-plugin/index.js +0 -3
  146. package/tools/webpack/plugins/html-webpack-new-relic-plugin/test/fixtures/entry.js +0 -1
  147. package/tools/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.ts +0 -134
  148. package/tools/webpack/plugins/paragon-webpack-plugin/index.ts +0 -3
  149. package/tools/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.ts +0 -71
  150. package/tools/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.ts +0 -72
  151. package/tools/webpack/plugins/paragon-webpack-plugin/utils/index.ts +0 -6
  152. package/tools/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.ts +0 -131
  153. package/tools/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.ts +0 -144
  154. package/tools/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.ts +0 -106
  155. package/tools/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.ts +0 -54
  156. package/tools/webpack/types.ts +0 -69
  157. package/tools/webpack/utils/getLocalAliases.ts +0 -65
  158. package/tools/webpack/utils/getPublicPath.ts +0 -3
  159. package/tools/webpack/utils/getResolvedSiteConfigPath.ts +0 -28
  160. package/tools/webpack/utils/paragonUtils.ts +0 -152
  161. package/tools/webpack/webpack.config.build.ts +0 -93
  162. package/tools/webpack/webpack.config.dev.shell.ts +0 -122
  163. package/tools/webpack/webpack.config.dev.ts +0 -90
  164. package/tsconfig.json +0 -23
@@ -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
 
@@ -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
+ });
@@ -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.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,
@@ -43,13 +43,13 @@ function TakesPropsViaContext() {
43
43
  }
44
44
 
45
45
  const app: App = {
46
- appId: 'org.openedx.frontend.app.dev.slotShowcase',
46
+ appId: 'org.openedx.frontend.app.slotShowcase',
47
47
  routes: [{
48
- id: 'org.openedx.frontend.route.dev.slotShowcase',
48
+ id: 'org.openedx.frontend.route.slotShowcase',
49
49
  path: '/slots',
50
50
  Component: SlotShowcasePage,
51
51
  handle: {
52
- role: 'slotShowcase',
52
+ role: 'org.openedx.frontend.role.slotShowcase',
53
53
  }
54
54
  }],
55
55
  slots: [
@@ -314,7 +314,7 @@ const app: App = {
314
314
  title: 'Courses (modified)',
315
315
  },
316
316
  condition: {
317
- active: ['slotShowcase'],
317
+ active: ['org.openedx.frontend.role.slotShowcase'],
318
318
  }
319
319
  },
320
320
  {
@@ -324,7 +324,7 @@ const app: App = {
324
324
  relatedId: 'org.openedx.frontend.widget.slotShowcase.headerLink3',
325
325
  element: (<LinkMenuItem label="Link After 3" url="#" variant="navLink" />),
326
326
  condition: {
327
- active: ['slotShowcase'],
327
+ active: ['org.openedx.frontend.role.slotShowcase'],
328
328
  }
329
329
  },
330
330
  {
@@ -335,7 +335,7 @@ const app: App = {
335
335
  <NavDropdownMenuSlot id="org.openedx.frontend.slot.header.primaryLinksDropdown.v1" label="Resources" />
336
336
  ),
337
337
  condition: {
338
- active: ['slotShowcase']
338
+ active: ['org.openedx.frontend.role.slotShowcase']
339
339
  }
340
340
  },
341
341
  {
@@ -346,7 +346,7 @@ const app: App = {
346
346
  <LinkMenuItem label="Resource 1" url="#" variant="dropdownItem" />
347
347
  ),
348
348
  condition: {
349
- active: ['slotShowcase'],
349
+ active: ['org.openedx.frontend.role.slotShowcase'],
350
350
  }
351
351
  },
352
352
  {
@@ -355,7 +355,7 @@ const app: App = {
355
355
  op: WidgetOperationTypes.APPEND,
356
356
  element: (<LinkMenuItem label="Link 3" url="#" variant="navLink" />),
357
357
  condition: {
358
- active: ['slotShowcase'],
358
+ active: ['org.openedx.frontend.role.slotShowcase'],
359
359
  }
360
360
  },
361
361
  {
@@ -364,7 +364,7 @@ const app: App = {
364
364
  op: WidgetOperationTypes.APPEND,
365
365
  element: (<LinkMenuItem label="Link 4" url="#" variant="navLink" />),
366
366
  condition: {
367
- active: ['slotShowcase'],
367
+ active: ['org.openedx.frontend.role.slotShowcase'],
368
368
  }
369
369
  },
370
370
  ]
@@ -66,7 +66,7 @@ const config: App = {
66
66
  element: (
67
67
  <ProfileLinkMenuItem
68
68
  label={messages['header.user.menu.profile']}
69
- role="profile"
69
+ role="org.openedx.frontend.role.profile"
70
70
  variant="dropdownItem"
71
71
  />
72
72
  )
@@ -78,7 +78,7 @@ const config: App = {
78
78
  element: (
79
79
  <LinkMenuItem
80
80
  label={messages['header.user.menu.account']}
81
- role="account"
81
+ role="org.openedx.frontend.role.account"
82
82
  variant="dropdownItem"
83
83
  />
84
84
  )
@@ -90,7 +90,7 @@ const config: App = {
90
90
  element: (
91
91
  <LinkMenuItem
92
92
  label={messages['header.user.menu.logout']}
93
- role="logout"
93
+ role="org.openedx.frontend.role.logout"
94
94
  variant="dropdownItem"
95
95
  />
96
96
  )
@@ -0,0 +1,50 @@
1
+ import { createBrowserRouter } from 'react-router-dom';
2
+ import { getBasename } from '../../runtime/initialize';
3
+ import Shell from '../Shell';
4
+ import createRouter from './createRouter';
5
+ import getAppRoutes from './getAppRoutes';
6
+
7
+ jest.mock('react-router-dom', () => ({
8
+ createBrowserRouter: jest.fn(),
9
+ }));
10
+
11
+ jest.mock('../../runtime/initialize', () => ({
12
+ getBasename: jest.fn(),
13
+ }));
14
+
15
+ jest.mock('../Shell', () => jest.fn());
16
+ jest.mock('./getAppRoutes', () => jest.fn());
17
+
18
+ describe('createRouter', () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ it('should create a router with the correct configuration', () => {
24
+ const mockRoutes = [{ path: '/', element: <div>Home</div> }];
25
+ const mockBasename = '/base';
26
+
27
+ (getAppRoutes as jest.Mock).mockReturnValue(mockRoutes);
28
+ (getBasename as jest.Mock).mockReturnValue(mockBasename);
29
+ (createBrowserRouter as jest.Mock).mockReturnValue('fake router value');
30
+
31
+ const result = createRouter();
32
+
33
+ expect(getAppRoutes).toHaveBeenCalled();
34
+ expect(getBasename).toHaveBeenCalled();
35
+ expect(createBrowserRouter).toHaveBeenCalledWith(
36
+ [
37
+ {
38
+ Component: Shell,
39
+ children: mockRoutes,
40
+ },
41
+ ],
42
+ {
43
+ basename: mockBasename,
44
+ }
45
+ );
46
+
47
+ // Just proving we actually return the router from createBrowserRouter.
48
+ expect(result).toEqual('fake router value');
49
+ });
50
+ });
@@ -0,0 +1,59 @@
1
+ import { RouteObject } from 'react-router';
2
+ import { getSiteConfig } from '../../runtime';
3
+ import getAppRoutes from './getAppRoutes';
4
+
5
+ jest.mock('../../runtime', () => ({
6
+ getSiteConfig: jest.fn(),
7
+ }));
8
+
9
+ describe('getAppRoutes', () => {
10
+ it('should return an empty array when no apps are configured', () => {
11
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: [] });
12
+
13
+ const routes = getAppRoutes();
14
+
15
+ expect(routes).toEqual([]);
16
+ });
17
+
18
+ it('should flatten and return routes from configured apps', () => {
19
+ const mockApps = [
20
+ {
21
+ routes: [
22
+ { path: '/page1', element: <div>Page 1</div> },
23
+ { path: '/page2', element: <div>Page 2</div> }
24
+ ]
25
+ },
26
+ {
27
+ routes: [
28
+ { path: '/page3', element: <div>Page 3</div> }
29
+ ]
30
+ },
31
+ ];
32
+
33
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: mockApps });
34
+
35
+ const routes = getAppRoutes();
36
+
37
+ expect(routes).toEqual([
38
+ { path: '/page1', element: <div>Page 1</div> },
39
+ { path: '/page2', element: <div>Page 2</div> },
40
+ { path: '/page3', element: <div>Page 3</div> }
41
+ ]);
42
+ });
43
+
44
+ it('should ignore apps without routes', () => {
45
+ const mockRoutes: RouteObject[] = [
46
+ { path: '/page1', element: <div>Page 1</div> },
47
+ ];
48
+ const mockApps = [
49
+ { routes: mockRoutes },
50
+ { slots: [] },
51
+ ];
52
+
53
+ (getSiteConfig as jest.Mock).mockReturnValue({ apps: mockApps });
54
+
55
+ const routes = getAppRoutes();
56
+
57
+ expect(routes).toEqual(mockRoutes);
58
+ });
59
+ });
@@ -18,15 +18,15 @@ const siteConfig: SiteConfig = {
18
18
 
19
19
  externalRoutes: [
20
20
  {
21
- role: 'profile',
21
+ role: 'org.openedx.frontend.role.profile',
22
22
  url: 'http://apps.local.openedx.io:1995/profile/'
23
23
  },
24
24
  {
25
- role: 'account',
25
+ role: 'org.openedx.frontend.role.account',
26
26
  url: 'http://apps.local.openedx.io:1997/account/'
27
27
  },
28
28
  {
29
- role: 'logout',
29
+ role: 'org.openedx.frontend.role.logout',
30
30
  url: 'http://local.openedx.io:8000/logout'
31
31
  },
32
32
  ],
@@ -0,0 +1,16 @@
1
+ import { EnvironmentTypes, SiteConfig } from '../types';
2
+
3
+ const siteConfig: SiteConfig = {
4
+ siteId: 'test',
5
+ siteName: 'Open edX',
6
+ baseUrl: 'http://localhost:8080',
7
+ lmsBaseUrl: 'http://localhost:18000',
8
+ loginUrl: 'http://localhost:18000/login',
9
+ logoutUrl: 'http://localhost:18000/logout',
10
+
11
+ environment: EnvironmentTypes.TEST,
12
+ apps: [],
13
+ segmentKey: 'segment_whoa',
14
+ };
15
+
16
+ export default siteConfig;