@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.
- package/config/eslint/base.eslint.config.js +1 -1
- package/config/jest/jest.config.js +1 -0
- package/config/types.js +0 -2
- package/config/webpack/common-config/all/getStylesheetRule.js +1 -1
- package/config/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
- package/config/webpack/webpack.config.build.js +1 -11
- package/config/webpack/webpack.config.dev.js +5 -11
- package/config/webpack/webpack.config.dev.shell.js +5 -11
- package/package.json +11 -3
- package/runtime/__mocks__/file.js +1 -0
- package/runtime/__mocks__/svg.js +1 -0
- package/runtime/__mocks__/universal-cookie.js +6 -0
- package/runtime/analytics/interface.test.js +242 -0
- package/runtime/auth/AxiosJwtAuthService.test.jsx +1076 -0
- package/runtime/auth/interceptors/createRetryInterceptor.test.js +23 -0
- package/runtime/config/getExternalLinkUrl.test.js +76 -0
- package/runtime/config/index.ts +2 -3
- package/runtime/i18n/lib.test.js +230 -0
- package/runtime/index.ts +5 -0
- package/runtime/initialize.async.function.config.test.js +43 -0
- package/runtime/initialize.const.config.test.js +41 -0
- package/runtime/initialize.function.config.test.js +41 -0
- package/runtime/initialize.test.js +356 -0
- package/runtime/jest.config.js +1 -0
- package/runtime/logging/NewRelicLoggingService.test.js +214 -0
- package/runtime/react/AuthenticatedPageRoute.test.jsx +135 -0
- package/runtime/react/ErrorBoundary.test.jsx +83 -0
- package/runtime/react/SiteProvider.test.jsx +66 -0
- package/runtime/react/SiteProvider.tsx +26 -3
- package/runtime/react/constants.ts +3 -0
- package/runtime/react/hooks/index.ts +8 -0
- package/runtime/react/hooks/theme/index.ts +2 -0
- package/runtime/react/hooks/theme/useTheme.test.ts +221 -0
- package/runtime/react/hooks/theme/useTheme.ts +179 -0
- package/runtime/react/hooks/theme/useThemeConfig.test.ts +107 -0
- package/runtime/react/hooks/theme/useThemeConfig.ts +34 -0
- package/runtime/react/hooks/theme/useThemeCore.test.ts +65 -0
- package/runtime/react/hooks/theme/useThemeCore.ts +52 -0
- package/runtime/react/hooks/theme/useThemeVariants.test.ts +97 -0
- package/runtime/react/hooks/theme/useThemeVariants.ts +116 -0
- package/runtime/react/hooks/theme/useTrackColorSchemeChoice.test.ts +54 -0
- package/runtime/react/hooks/theme/useTrackColorSchemeChoice.ts +30 -0
- package/runtime/react/hooks/theme/utils.ts +11 -0
- package/runtime/react/hooks/useActiveRoles.ts +15 -0
- package/runtime/react/hooks/useActiveRouteRoleWatcher.ts +31 -0
- package/runtime/react/hooks/useAppConfig.ts +9 -0
- package/runtime/react/hooks/useAuthenticatedUser.test.tsx +41 -0
- package/runtime/react/hooks/useAuthenticatedUser.ts +9 -0
- package/runtime/react/hooks/useSiteConfig.test.tsx +13 -0
- package/runtime/react/hooks/useSiteConfig.ts +9 -0
- package/runtime/react/hooks/useSiteEvent.ts +24 -0
- package/runtime/react/reducers.ts +40 -0
- package/runtime/routing/utils.test.ts +7 -0
- package/runtime/scripts/GoogleAnalyticsLoader.test.ts +77 -0
- package/runtime/setupTest.js +0 -35
- package/runtime/site.config.test.tsx +33 -0
- package/runtime/slots/Slot.test.tsx +40 -0
- package/runtime/slots/layout/DefaultSlotLayout.test.tsx +31 -0
- package/runtime/slots/layout/hooks.test.tsx +178 -0
- package/runtime/slots/layout/utils.test.ts +67 -0
- package/runtime/slots/types.ts +1 -0
- package/runtime/slots/utils.test.ts +64 -0
- package/runtime/slots/utils.ts +28 -9
- package/runtime/slots/widget/iframe/hooks.ts +1 -1
- package/runtime/testing/initializeMockApp.test.ts +66 -0
- package/runtime/testing/initializeMockApp.ts +5 -0
- package/runtime/utils.test.js +116 -0
- package/shell/Logo.test.tsx +32 -0
- package/shell/__mocks__/file.js +1 -0
- package/shell/__mocks__/svg.js +1 -0
- package/shell/__mocks__/universal-cookie.js +6 -0
- package/shell/app.scss +2 -1
- package/shell/app.ts +14 -0
- package/shell/dev/devHome/app.ts +2 -2
- package/shell/dev/slotShowcase/app.tsx +9 -9
- package/shell/header/app.tsx +3 -3
- package/shell/jest.config.js +1 -0
- package/shell/router/createRouter.test.tsx +50 -0
- package/shell/router/getAppRoutes.test.tsx +59 -0
- package/shell/setupTest.js +0 -35
- package/shell/site.config.dev.tsx +3 -3
- package/shell/site.config.test.tsx +16 -0
- package/shell/site.tsx +1 -1
- package/tools/dist/cli/intl-imports.test.js +146 -0
- package/tools/dist/cli/openedx.js +1 -15
- package/tools/dist/cli/utils/printUsage.js +0 -9
- package/tools/dist/eslint/base.eslint.config.js +1 -1
- package/tools/dist/jest/jest.config.js +1 -0
- package/tools/dist/types.js +0 -2
- package/tools/dist/webpack/common-config/all/getStylesheetRule.js +1 -1
- package/tools/dist/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
- package/tools/dist/webpack/webpack.config.build.js +1 -11
- package/tools/dist/webpack/webpack.config.dev.js +5 -11
- package/tools/dist/webpack/webpack.config.dev.shell.js +5 -11
- package/types.ts +21 -1
- package/config/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js +0 -108
- package/config/webpack/plugins/paragon-webpack-plugin/index.js +0 -7
- package/config/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.js +0 -64
- package/config/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.js +0 -53
- package/config/webpack/plugins/paragon-webpack-plugin/utils/index.js +0 -9
- package/config/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.js +0 -114
- package/config/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.js +0 -146
- package/config/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.js +0 -126
- package/config/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.js +0 -57
- package/config/webpack/types.js +0 -2
- package/config/webpack/utils/paragonUtils.js +0 -138
- package/eslint.config.js +0 -18
- package/frontend-base.d.ts +0 -8
- package/jest.config.js +0 -7
- package/openedx-frontend-base.tgz +0 -0
- package/runtime/react/hooks.ts +0 -106
- package/test-site/app.d.ts +0 -15
- package/test-site/dist/176.436443549ebb858db483.js +0 -2
- package/test-site/dist/176.436443549ebb858db483.js.map +0 -1
- package/test-site/dist/362.536eff787d2380fe246c.js +0 -2
- package/test-site/dist/362.536eff787d2380fe246c.js.map +0 -1
- package/test-site/dist/653.486966b108d224551296.js +0 -2
- package/test-site/dist/653.486966b108d224551296.js.map +0 -1
- package/test-site/dist/74e025d3fe9a7b7f8503054e2563b353.jpg +0 -0
- package/test-site/dist/806.323cf6496ad0a7fe73a7.js +0 -3
- package/test-site/dist/806.323cf6496ad0a7fe73a7.js.LICENSE.txt +0 -106
- package/test-site/dist/806.323cf6496ad0a7fe73a7.js.map +0 -1
- package/test-site/dist/95ec738c0b7faac5b5c9126794446bbd.svg +0 -4
- package/test-site/dist/app.612058b36c74787759ac.css +0 -61
- package/test-site/dist/app.612058b36c74787759ac.css.map +0 -1
- package/test-site/dist/app.612058b36c74787759ac.js +0 -2
- package/test-site/dist/app.612058b36c74787759ac.js.map +0 -1
- package/test-site/dist/cb28cdb1468c915e27e5cec9af64f22f.svg +0 -1
- package/test-site/dist/index.html +0 -1
- package/test-site/dist/report.html +0 -39
- package/test-site/dist/runtime.c7aeaf7b967496cb076f.js +0 -2
- package/test-site/dist/runtime.c7aeaf7b967496cb076f.js.map +0 -1
- package/test-site/eslint.config.js +0 -12
- package/test-site/package-lock.json +0 -19226
- package/test-site/package.json +0 -29
- package/test-site/public/index.html +0 -10
- package/test-site/site.config.build.tsx +0 -27
- package/test-site/site.config.dev.tsx +0 -27
- package/test-site/src/authenticated-page/AuthenticatedPage.tsx +0 -18
- package/test-site/src/authenticated-page/i18n/index.ts +0 -27
- package/test-site/src/authenticated-page/index.tsx +0 -28
- package/test-site/src/example-page/ExamplePage.tsx +0 -79
- package/test-site/src/example-page/Image.tsx +0 -11
- package/test-site/src/example-page/ParagonPreview.jsx +0 -66
- package/test-site/src/example-page/apple.jpg +0 -0
- package/test-site/src/example-page/apple.svg +0 -1
- package/test-site/src/example-page/index.ts +0 -16
- package/test-site/src/i18n/README.md +0 -3
- package/test-site/src/i18n/messages/frontend-app-sample/ar.json +0 -4
- package/test-site/src/i18n/messages/frontend-app-sample/eo.json +0 -1
- package/test-site/src/i18n/messages/frontend-app-sample/es_419.json +0 -4
- package/test-site/src/i18n/messages/frontend-component-emptylangs/ar.json +0 -1
- package/test-site/src/i18n/messages/frontend-component-singlelang/ar.json +0 -3
- package/test-site/src/iframe-widget/IframeWidget.tsx +0 -14
- package/test-site/src/iframe-widget/index.ts +0 -16
- package/test-site/src/index.tsx +0 -3
- package/test-site/src/messages.js +0 -11
- package/test-site/src/site.scss +0 -11
- package/test-site/tsconfig.json +0 -14
- package/tools/babel/babel.config.js +0 -27
- package/tools/babel.config.js +0 -3
- package/tools/cli/README.md +0 -29
- package/tools/cli/commands/pack.ts +0 -9
- package/tools/cli/commands/release.ts +0 -27
- package/tools/cli/commands/serve.ts +0 -43
- package/tools/cli/intl-imports.ts +0 -274
- package/tools/cli/openedx.ts +0 -101
- package/tools/cli/transifex-utils.ts +0 -75
- package/tools/cli/utils/ensureConfigFilenameOption.ts +0 -40
- package/tools/cli/utils/formatter.ts +0 -10
- package/tools/cli/utils/getResolvedConfigPath.ts +0 -23
- package/tools/cli/utils/prettyPrintTitle.ts +0 -15
- package/tools/cli/utils/printUsage.ts +0 -53
- package/tools/config-helpers/createConfig.ts +0 -8
- package/tools/config-helpers/createLintConfig.ts +0 -14
- package/tools/config-helpers/getBaseConfig.ts +0 -11
- package/tools/defaultConfigPaths.ts +0 -30
- package/tools/dist/cli/commands/pack.js +0 -14
- package/tools/dist/cli/commands/release.js +0 -28
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js +0 -108
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/index.js +0 -7
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.js +0 -64
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.js +0 -53
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/index.js +0 -9
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.js +0 -114
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.js +0 -146
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.js +0 -126
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.js +0 -57
- package/tools/dist/webpack/types.js +0 -2
- package/tools/dist/webpack/utils/paragonUtils.js +0 -138
- package/tools/eslint/base.eslint.config.js +0 -124
- package/tools/eslint/modules.d.ts +0 -5
- package/tools/eslint.config.js +0 -15
- package/tools/index.ts +0 -3
- package/tools/jest/jest.config.js +0 -30
- package/tools/jest.config.js +0 -19
- package/tools/tsconfig.json +0 -24
- package/tools/types.ts +0 -21
- package/tools/typescript/tsconfig.json +0 -32
- package/tools/webpack/common-config/README.md +0 -15
- package/tools/webpack/common-config/all/getCodeRules.ts +0 -51
- package/tools/webpack/common-config/all/getFileLoaderRules.ts +0 -23
- package/tools/webpack/common-config/all/getIgnoreWarnings.ts +0 -13
- package/tools/webpack/common-config/all/getImageMinimizer.ts +0 -26
- package/tools/webpack/common-config/all/getStylesheetRule.ts +0 -111
- package/tools/webpack/common-config/dev/getDevServer.ts +0 -35
- package/tools/webpack/common-config/index.ts +0 -6
- package/tools/webpack/common-config/site/getHtmlWebpackPlugin.ts +0 -11
- package/tools/webpack/modules.d.ts +0 -6
- package/tools/webpack/plugins/html-webpack-new-relic-plugin/HtmlWebpackNewRelicPlugin.ts +0 -102
- package/tools/webpack/plugins/html-webpack-new-relic-plugin/LICENSE +0 -21
- package/tools/webpack/plugins/html-webpack-new-relic-plugin/README.md +0 -7
- package/tools/webpack/plugins/html-webpack-new-relic-plugin/index.js +0 -3
- package/tools/webpack/plugins/html-webpack-new-relic-plugin/test/fixtures/entry.js +0 -1
- package/tools/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.ts +0 -134
- package/tools/webpack/plugins/paragon-webpack-plugin/index.ts +0 -3
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.ts +0 -71
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.ts +0 -72
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/index.ts +0 -6
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.ts +0 -131
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.ts +0 -144
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.ts +0 -106
- package/tools/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.ts +0 -54
- package/tools/webpack/types.ts +0 -69
- package/tools/webpack/utils/getLocalAliases.ts +0 -65
- package/tools/webpack/utils/getPublicPath.ts +0 -3
- package/tools/webpack/utils/getResolvedSiteConfigPath.ts +0 -28
- package/tools/webpack/utils/paragonUtils.ts +0 -152
- package/tools/webpack/webpack.config.build.ts +0 -93
- package/tools/webpack/webpack.config.dev.shell.ts +0 -122
- package/tools/webpack/webpack.config.dev.ts +0 -90
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* eslint-disable react/jsx-no-constructed-context-values */
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
4
|
+
import { sendPageEvent } from '../analytics';
|
|
5
|
+
import { getAuthenticatedUser, getLoginRedirectUrl } from '../auth';
|
|
6
|
+
import { getSiteConfig } from '../config';
|
|
7
|
+
import SiteContext from './SiteContext';
|
|
8
|
+
import AuthenticatedPageRoute from './AuthenticatedPageRoute';
|
|
9
|
+
|
|
10
|
+
jest.mock('../subscriptions');
|
|
11
|
+
jest.mock('../analytics');
|
|
12
|
+
jest.mock('../auth');
|
|
13
|
+
|
|
14
|
+
describe('AuthenticatedPageRoute', () => {
|
|
15
|
+
const { location } = global;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
delete global.location;
|
|
19
|
+
global.location = {
|
|
20
|
+
assign: jest.fn(),
|
|
21
|
+
};
|
|
22
|
+
sendPageEvent.mockReset();
|
|
23
|
+
getLoginRedirectUrl.mockReset();
|
|
24
|
+
getAuthenticatedUser.mockReset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
global.location = location;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should redirect to login if not authenticated', () => {
|
|
32
|
+
getAuthenticatedUser.mockReturnValue(null);
|
|
33
|
+
getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
|
|
34
|
+
const component = (
|
|
35
|
+
<SiteContext.Provider
|
|
36
|
+
value={{
|
|
37
|
+
authenticatedUser: getAuthenticatedUser(),
|
|
38
|
+
siteConfig: getSiteConfig(),
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<MemoryRouter initialEntries={['/authenticated']}>
|
|
42
|
+
<Routes>
|
|
43
|
+
<Route path="/" element={() => <p>Anonymous</p>} />
|
|
44
|
+
<Route path="/authenticated" element={<AuthenticatedPageRoute><p>Authenticated</p></AuthenticatedPageRoute>} />
|
|
45
|
+
</Routes>
|
|
46
|
+
</MemoryRouter>
|
|
47
|
+
</SiteContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
global.location.href = 'http://localhost/authenticated';
|
|
50
|
+
render(component);
|
|
51
|
+
expect(getLoginRedirectUrl).toHaveBeenCalledWith('http://localhost/authenticated');
|
|
52
|
+
expect(sendPageEvent).not.toHaveBeenCalled();
|
|
53
|
+
expect(global.location.assign).toHaveBeenCalledWith('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should redirect to custom redirect URL if not authenticated', () => {
|
|
57
|
+
getAuthenticatedUser.mockReturnValue(null);
|
|
58
|
+
getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
|
|
59
|
+
const authenticatedElement = (
|
|
60
|
+
<AuthenticatedPageRoute redirectUrl="http://localhost/elsewhere">
|
|
61
|
+
<p>Authenticated</p>
|
|
62
|
+
</AuthenticatedPageRoute>
|
|
63
|
+
);
|
|
64
|
+
const component = (
|
|
65
|
+
<SiteContext.Provider
|
|
66
|
+
value={{
|
|
67
|
+
authenticatedUser: getAuthenticatedUser(),
|
|
68
|
+
siteConfig: getSiteConfig(),
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<MemoryRouter initialEntries={['/authenticated']}>
|
|
72
|
+
<Routes>
|
|
73
|
+
<Route path="/" component={() => <p>Anonymous</p>} />
|
|
74
|
+
<Route path="/authenticated" element={authenticatedElement} />
|
|
75
|
+
</Routes>
|
|
76
|
+
</MemoryRouter>
|
|
77
|
+
</SiteContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
render(component);
|
|
80
|
+
expect(getLoginRedirectUrl).not.toHaveBeenCalled();
|
|
81
|
+
expect(sendPageEvent).not.toHaveBeenCalled();
|
|
82
|
+
expect(global.location.assign).toHaveBeenCalledWith('http://localhost/elsewhere');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should not call login if not the current route', () => {
|
|
86
|
+
getAuthenticatedUser.mockReturnValue(null);
|
|
87
|
+
getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
|
|
88
|
+
const component = (
|
|
89
|
+
<SiteContext.Provider
|
|
90
|
+
value={{
|
|
91
|
+
authenticatedUser: getAuthenticatedUser(),
|
|
92
|
+
siteConfig: getSiteConfig(),
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<MemoryRouter>
|
|
96
|
+
<Routes>
|
|
97
|
+
<Route path="/" element={<p>Anonymous</p>} />
|
|
98
|
+
<Route path="/authenticated" element={<AuthenticatedPageRoute><p>Authenticated</p></AuthenticatedPageRoute>} />
|
|
99
|
+
</Routes>
|
|
100
|
+
</MemoryRouter>
|
|
101
|
+
</SiteContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
const wrapper = render(component);
|
|
104
|
+
|
|
105
|
+
expect(getLoginRedirectUrl).not.toHaveBeenCalled();
|
|
106
|
+
expect(global.location.assign).not.toHaveBeenCalled();
|
|
107
|
+
expect(sendPageEvent).not.toHaveBeenCalled();
|
|
108
|
+
const element = wrapper.container.querySelector('p');
|
|
109
|
+
expect(element.textContent).toEqual('Anonymous'); // This is just a sanity check on our setup.
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should render authenticated route if authenticated', () => {
|
|
113
|
+
const component = (
|
|
114
|
+
<SiteContext.Provider
|
|
115
|
+
value={{
|
|
116
|
+
authenticatedUser: { userId: 12345, username: 'edx' },
|
|
117
|
+
siteConfig: getSiteConfig(),
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<MemoryRouter initialEntries={['/authenticated']}>
|
|
121
|
+
<Routes>
|
|
122
|
+
<Route path="/" element={<p>Anonymous</p>} />
|
|
123
|
+
<Route path="/authenticated" element={<AuthenticatedPageRoute><p>Authenticated</p></AuthenticatedPageRoute>} />
|
|
124
|
+
</Routes>
|
|
125
|
+
</MemoryRouter>
|
|
126
|
+
</SiteContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
const wrapper = render(component);
|
|
129
|
+
expect(getLoginRedirectUrl).not.toHaveBeenCalled();
|
|
130
|
+
expect(global.location.assign).not.toHaveBeenCalled();
|
|
131
|
+
expect(sendPageEvent).toHaveBeenCalled();
|
|
132
|
+
const element = wrapper.container.querySelector('p');
|
|
133
|
+
expect(element.textContent).toEqual('Authenticated');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import ErrorBoundary from './ErrorBoundary';
|
|
4
|
+
import { initializeMockApp } from '..';
|
|
5
|
+
|
|
6
|
+
describe('ErrorBoundary', () => {
|
|
7
|
+
let logError = jest.fn();
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
// This is a gross hack to suppress error logs in the invalid parentSelector test
|
|
11
|
+
jest.spyOn(console, 'error');
|
|
12
|
+
global.console.error.mockImplementation(() => { });
|
|
13
|
+
|
|
14
|
+
const { loggingService } = initializeMockApp();
|
|
15
|
+
logError = loggingService.logError;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
global.console.error.mockRestore();
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should render children if no error', () => {
|
|
24
|
+
const component = (
|
|
25
|
+
<ErrorBoundary>
|
|
26
|
+
<div>Yay</div>
|
|
27
|
+
</ErrorBoundary>
|
|
28
|
+
);
|
|
29
|
+
const { container: wrapper } = render(component);
|
|
30
|
+
|
|
31
|
+
const element = wrapper.querySelector('div');
|
|
32
|
+
expect(element.textContent).toEqual('Yay');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should render ErrorPage if it has an error', () => {
|
|
36
|
+
const ExplodingComponent = () => {
|
|
37
|
+
throw new Error('booyah');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const component = (
|
|
41
|
+
<ErrorBoundary>
|
|
42
|
+
<ExplodingComponent />
|
|
43
|
+
</ErrorBoundary>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
render(component);
|
|
47
|
+
|
|
48
|
+
expect(logError).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(logError).toHaveBeenCalledWith(
|
|
50
|
+
new Error('booyah'),
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
stack: expect.stringContaining('ExplodingComponent'),
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
it('should render the fallback component when an error occurs', () => {
|
|
57
|
+
function FallbackComponent() {
|
|
58
|
+
return <div data-testid="fallback-component">Oops, something went wrong!</div>;
|
|
59
|
+
}
|
|
60
|
+
function ComponentError() {
|
|
61
|
+
throw new Error('An error occurred during the click event!');
|
|
62
|
+
}
|
|
63
|
+
const wrapper = render(
|
|
64
|
+
<ErrorBoundary fallbackComponent={<FallbackComponent />}>
|
|
65
|
+
<ComponentError />
|
|
66
|
+
</ErrorBoundary>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(wrapper.queryByTestId('fallback-component')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should render the ErrorPage fallbackComponent is null', () => {
|
|
73
|
+
function ComponentError() {
|
|
74
|
+
throw new Error('An error occurred during the click event!');
|
|
75
|
+
}
|
|
76
|
+
const wrapper = render(
|
|
77
|
+
<ErrorBoundary fallbackComponent={null}>
|
|
78
|
+
<ComponentError />
|
|
79
|
+
</ErrorBoundary>,
|
|
80
|
+
);
|
|
81
|
+
expect(wrapper.queryByTestId('error-page')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { initialize } from '../initialize';
|
|
3
|
+
import SiteProvider from './SiteProvider';
|
|
4
|
+
|
|
5
|
+
jest.mock('../auth', () => ({
|
|
6
|
+
configureAuth: () => { },
|
|
7
|
+
getAuthenticatedUser: () => null,
|
|
8
|
+
fetchAuthenticatedUser: () => null,
|
|
9
|
+
getAuthenticatedHttpClient: () => ({}),
|
|
10
|
+
AUTHENTICATED_USER_CHANGED: 'user_changed',
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('../analytics', () => ({
|
|
14
|
+
configureAnalytics: () => { },
|
|
15
|
+
identifyAnonymousUser: jest.fn(),
|
|
16
|
+
identifyAuthenticatedUser: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock('./hooks', () => ({
|
|
20
|
+
...jest.requireActual('./hooks'),
|
|
21
|
+
useTrackColorSchemeChoice: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('SiteProvider', () => {
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await initialize({
|
|
27
|
+
loggingService: jest.fn(() => ({
|
|
28
|
+
logError: jest.fn(),
|
|
29
|
+
logInfo: jest.fn(),
|
|
30
|
+
})),
|
|
31
|
+
messages: {
|
|
32
|
+
ar: {},
|
|
33
|
+
'es-419': {},
|
|
34
|
+
fr: {},
|
|
35
|
+
'zh-cn': {},
|
|
36
|
+
ca: {},
|
|
37
|
+
he: {},
|
|
38
|
+
id: {},
|
|
39
|
+
'ko-kr': {},
|
|
40
|
+
pl: {},
|
|
41
|
+
'pt-br': {},
|
|
42
|
+
ru: {},
|
|
43
|
+
th: {},
|
|
44
|
+
uk: {},
|
|
45
|
+
'fa-ir': {},
|
|
46
|
+
fa: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render its children with a router', () => {
|
|
52
|
+
const component = (
|
|
53
|
+
<SiteProvider>
|
|
54
|
+
<div className="child">Child One</div>
|
|
55
|
+
<div className="child">Child Two</div>
|
|
56
|
+
</SiteProvider>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const wrapper = render(component);
|
|
60
|
+
const list = wrapper.container.querySelectorAll('div.child');
|
|
61
|
+
|
|
62
|
+
expect(list.length).toEqual(2);
|
|
63
|
+
expect(list[0].textContent).toEqual('Child One');
|
|
64
|
+
expect(list[1].textContent).toEqual('Child Two');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
import CombinedAppProvider from './CombinedAppProvider';
|
|
14
14
|
import ErrorBoundary from './ErrorBoundary';
|
|
15
15
|
import SiteContext from './SiteContext';
|
|
16
|
-
import {
|
|
16
|
+
import { SELECTED_THEME_VARIANT_KEY } from './constants';
|
|
17
|
+
import {
|
|
18
|
+
useTheme,
|
|
19
|
+
useSiteEvent,
|
|
20
|
+
useTrackColorSchemeChoice
|
|
21
|
+
} from './hooks';
|
|
22
|
+
import { themeActions } from './reducers';
|
|
17
23
|
|
|
18
24
|
interface SiteProviderProps {
|
|
19
25
|
children: ReactNode,
|
|
@@ -37,6 +43,7 @@ interface SiteProviderProps {
|
|
|
37
43
|
* - An error boundary as described above.
|
|
38
44
|
* - An `SiteContext` provider for React context data.
|
|
39
45
|
* - IntlProvider for @edx/frontend-i18n internationalization
|
|
46
|
+
* - A theme manager for Paragon.
|
|
40
47
|
*
|
|
41
48
|
* @param {Object} props
|
|
42
49
|
* @memberof module:React
|
|
@@ -58,11 +65,27 @@ export default function SiteProvider({ children }: SiteProviderProps) {
|
|
|
58
65
|
setLocale(getLocale());
|
|
59
66
|
});
|
|
60
67
|
|
|
68
|
+
useTrackColorSchemeChoice();
|
|
69
|
+
const [themeState, themeDispatch] = useTheme();
|
|
70
|
+
|
|
61
71
|
const siteContextValue = useMemo(() => ({
|
|
62
72
|
authenticatedUser,
|
|
63
73
|
siteConfig,
|
|
64
|
-
locale
|
|
65
|
-
|
|
74
|
+
locale,
|
|
75
|
+
theme: {
|
|
76
|
+
state: themeState,
|
|
77
|
+
setThemeVariant: (themeVariant: string) => {
|
|
78
|
+
themeDispatch(themeActions.setThemeVariant(themeVariant));
|
|
79
|
+
|
|
80
|
+
// Persist selected theme variant to localStorage.
|
|
81
|
+
window.localStorage.setItem(SELECTED_THEME_VARIANT_KEY, themeVariant);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}), [authenticatedUser, siteConfig, locale, themeState, themeDispatch]);
|
|
85
|
+
|
|
86
|
+
if (!themeState?.isThemeLoaded) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
66
89
|
|
|
67
90
|
return (
|
|
68
91
|
<IntlProvider locale={locale} messages={getMessages()}>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as useAppConfig } from './useAppConfig';
|
|
2
|
+
export { default as useAuthenticatedUser } from './useAuthenticatedUser';
|
|
3
|
+
export { default as useActiveRouteRoleWatcher } from './useActiveRouteRoleWatcher';
|
|
4
|
+
export { default as useActiveRoles } from './useActiveRoles';
|
|
5
|
+
export { default as useSiteConfig } from './useSiteConfig';
|
|
6
|
+
export { default as useSiteEvent } from './useSiteEvent';
|
|
7
|
+
|
|
8
|
+
export * from './theme';
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { act, fireEvent, renderHook } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import useTheme from './useTheme';
|
|
4
|
+
import * as config from '../../../config';
|
|
5
|
+
import { logError } from '../../../logging';
|
|
6
|
+
|
|
7
|
+
jest.mock('../../../logging');
|
|
8
|
+
|
|
9
|
+
const baseSiteConfig = config.getSiteConfig();
|
|
10
|
+
|
|
11
|
+
const theme = {
|
|
12
|
+
core: {
|
|
13
|
+
url: 'https://cdn.jsdelivr.net/npm/@openedx/brand@1.0.0/dist/core.min.css',
|
|
14
|
+
},
|
|
15
|
+
defaults: {
|
|
16
|
+
light: 'light',
|
|
17
|
+
dark: 'dark',
|
|
18
|
+
},
|
|
19
|
+
variants: {
|
|
20
|
+
light: {
|
|
21
|
+
url: 'https://cdn.jsdelivr.net/npm/@openedx/brand@1.0.0/dist/light.min.css',
|
|
22
|
+
},
|
|
23
|
+
dark: {
|
|
24
|
+
url: 'https://cdn.jsdelivr.net/npm/@openedx/brand@1.0.0/dist/dark.min.css',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let mockMediaQueryListEvent;
|
|
30
|
+
const mockAddEventListener = jest.fn((_, fn) => fn(mockMediaQueryListEvent));
|
|
31
|
+
const mockRemoveEventListener = jest.fn();
|
|
32
|
+
|
|
33
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
34
|
+
value: jest.fn(() => ({
|
|
35
|
+
addEventListener: mockAddEventListener,
|
|
36
|
+
removeEventListener: mockRemoveEventListener,
|
|
37
|
+
matches: mockMediaQueryListEvent.matches,
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
Object.defineProperty(window, 'localStorage', {
|
|
42
|
+
value: {
|
|
43
|
+
getItem: jest.fn(),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('useTheme', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
document.head.innerHTML = '';
|
|
50
|
+
mockMediaQueryListEvent = { matches: true };
|
|
51
|
+
mockAddEventListener.mockClear();
|
|
52
|
+
mockRemoveEventListener.mockClear();
|
|
53
|
+
jest.mocked(window.localStorage.getItem).mockClear();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
jest.spyOn(config, 'getSiteConfig').mockRestore();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each([
|
|
61
|
+
['dark', 'stylesheet', 'alternate stylesheet', true], // preference is dark
|
|
62
|
+
['light', 'alternate stylesheet', 'stylesheet', false], // preference is light
|
|
63
|
+
])(
|
|
64
|
+
'should configure theme variant for system preference %s and handle theme change events',
|
|
65
|
+
(initialPreference, expectedDarkRel, expectedLightRel, isDarkMediaMatch) => {
|
|
66
|
+
jest.spyOn(config, 'getSiteConfig').mockReturnValue({
|
|
67
|
+
...baseSiteConfig,
|
|
68
|
+
theme
|
|
69
|
+
});
|
|
70
|
+
// Mock the matchMedia behavior to simulate system preference
|
|
71
|
+
mockMediaQueryListEvent = { matches: isDarkMediaMatch };
|
|
72
|
+
// Set up the hook and initial theme configuration
|
|
73
|
+
const { result, unmount } = renderHook(() => useTheme());
|
|
74
|
+
const themeLinks = document.head.querySelectorAll('link');
|
|
75
|
+
|
|
76
|
+
const checkThemeLinks = () => {
|
|
77
|
+
const darkLink: HTMLAnchorElement | null = document.head.querySelector('link[data-theme-variant="dark"]');
|
|
78
|
+
const lightLink: HTMLAnchorElement | null = document.head.querySelector('link[data-theme-variant="light"]');
|
|
79
|
+
expect(darkLink?.rel).toBe(expectedDarkRel);
|
|
80
|
+
expect(lightLink?.rel).toBe(expectedLightRel);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Simulate initial theme configuration based on system preference
|
|
84
|
+
act(() => {
|
|
85
|
+
themeLinks.forEach((link) => fireEvent.load(link));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Ensure matchMedia was called with the correct system preference
|
|
89
|
+
expect(window.matchMedia).toHaveBeenCalledWith('(prefers-color-scheme: dark)');
|
|
90
|
+
expect(mockAddEventListener).toHaveBeenCalled();
|
|
91
|
+
|
|
92
|
+
// Check initial theme setup
|
|
93
|
+
checkThemeLinks();
|
|
94
|
+
expect(result.current[0]).toEqual({
|
|
95
|
+
isThemeLoaded: true,
|
|
96
|
+
themeVariant: initialPreference,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
unmount();
|
|
100
|
+
expect(mockRemoveEventListener).toHaveBeenCalled();
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
it('should configure theme variants according with user preference if is defined (localStorage)', () => {
|
|
105
|
+
jest.spyOn(config, 'getSiteConfig').mockReturnValue({
|
|
106
|
+
...baseSiteConfig,
|
|
107
|
+
theme
|
|
108
|
+
});
|
|
109
|
+
jest.mocked(window.localStorage.getItem).mockReturnValue('light');
|
|
110
|
+
const { result, unmount } = renderHook(() => useTheme());
|
|
111
|
+
const themeLinks = document.head.querySelectorAll('link');
|
|
112
|
+
const darkLink: HTMLAnchorElement | null = document.head.querySelector('link[data-theme-variant="dark"]');
|
|
113
|
+
const lightLink: HTMLAnchorElement | null = document.head.querySelector('link[data-theme-variant="light"]');
|
|
114
|
+
|
|
115
|
+
act(() => {
|
|
116
|
+
themeLinks.forEach((link) => fireEvent.load(link));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(window.matchMedia).toHaveBeenCalledWith('(prefers-color-scheme: dark)');
|
|
120
|
+
expect(mockAddEventListener).toHaveBeenCalled();
|
|
121
|
+
|
|
122
|
+
expect(darkLink?.rel).toBe('alternate stylesheet');
|
|
123
|
+
expect(lightLink?.rel).toBe('stylesheet');
|
|
124
|
+
expect(result.current[0]).toEqual({ isThemeLoaded: true, themeVariant: 'light' });
|
|
125
|
+
|
|
126
|
+
unmount();
|
|
127
|
+
expect(mockRemoveEventListener).toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should define the theme variant as default if only 1 is configured', () => {
|
|
131
|
+
jest.spyOn(config, 'getSiteConfig').mockReturnValue({
|
|
132
|
+
...baseSiteConfig,
|
|
133
|
+
theme: { ...theme, variants: { light: theme.variants.light } }
|
|
134
|
+
});
|
|
135
|
+
jest.mocked(window.localStorage.getItem).mockReturnValue('light');
|
|
136
|
+
const { result, unmount } = renderHook(() => useTheme());
|
|
137
|
+
const themeLinks = document.head.querySelectorAll('link');
|
|
138
|
+
const themeVariantLinks: NodeListOf<HTMLAnchorElement> | null = document.head.querySelectorAll('link[data-theme-variant]');
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
themeLinks.forEach((link) => fireEvent.load(link));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(themeVariantLinks.length).toBe(1);
|
|
145
|
+
expect((themeVariantLinks[0]).rel).toBe('stylesheet');
|
|
146
|
+
expect(result.current[0]).toEqual({ isThemeLoaded: true, themeVariant: 'light' });
|
|
147
|
+
|
|
148
|
+
unmount();
|
|
149
|
+
expect(mockRemoveEventListener).toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should not configure any theme if theme is undefined', () => {
|
|
153
|
+
const { result, unmount } = renderHook(() => useTheme());
|
|
154
|
+
const themeLinks = document.head.querySelectorAll('link');
|
|
155
|
+
|
|
156
|
+
expect(result.current[0]).toEqual({ isThemeLoaded: true, themeVariant: undefined });
|
|
157
|
+
expect(themeLinks.length).toBe(0);
|
|
158
|
+
unmount();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return themeVariant undefined if a default variant cannot be configured', () => {
|
|
162
|
+
jest.spyOn(config, 'getSiteConfig').mockReturnValue({
|
|
163
|
+
...baseSiteConfig,
|
|
164
|
+
theme: {
|
|
165
|
+
...theme,
|
|
166
|
+
defaults: {
|
|
167
|
+
light: 'red'
|
|
168
|
+
},
|
|
169
|
+
variants: {
|
|
170
|
+
light: theme.variants.light,
|
|
171
|
+
green: { url: 'green-url' }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
jest.mocked(window.localStorage.getItem).mockReturnValue(null);
|
|
176
|
+
|
|
177
|
+
const { result, unmount } = renderHook(() => useTheme());
|
|
178
|
+
const themeLinks = document.head.querySelectorAll('link');
|
|
179
|
+
const themeVariantLinks = document.head.querySelectorAll('link[data-theme-variant]');
|
|
180
|
+
act(() => {
|
|
181
|
+
themeLinks.forEach((link) => fireEvent.load(link));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(result.current[0]).toEqual({ isThemeLoaded: true, themeVariant: undefined });
|
|
185
|
+
expect(themeLinks.length).toBe(3);
|
|
186
|
+
themeVariantLinks.forEach((link: HTMLAnchorElement) => expect(link.rel).toBe('alternate stylesheet'));
|
|
187
|
+
unmount();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should log an error if the preferred theme variant cannot be set', async () => {
|
|
191
|
+
jest.spyOn(config, 'getSiteConfig').mockReturnValue({
|
|
192
|
+
...baseSiteConfig,
|
|
193
|
+
theme: {
|
|
194
|
+
...theme,
|
|
195
|
+
defaults: {
|
|
196
|
+
light: 'light',
|
|
197
|
+
dark: 'dark'
|
|
198
|
+
},
|
|
199
|
+
variants: {
|
|
200
|
+
light: theme.variants.light,
|
|
201
|
+
green: { url: 'green-url' }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
jest.mocked(window.localStorage.getItem).mockReturnValue(null);
|
|
206
|
+
|
|
207
|
+
const { result, unmount } = renderHook(() => useTheme());
|
|
208
|
+
const themeLinks = document.head.querySelectorAll('link');
|
|
209
|
+
const themeVariantLinks = document.head.querySelectorAll('link[data-theme-variant]');
|
|
210
|
+
|
|
211
|
+
act(() => {
|
|
212
|
+
themeLinks.forEach((link) => fireEvent.load(link));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.current[0]).toEqual({ isThemeLoaded: true, themeVariant: 'dark' });
|
|
216
|
+
expect(logError).toHaveBeenCalled();
|
|
217
|
+
expect(themeVariantLinks.length).toBe(2);
|
|
218
|
+
themeVariantLinks.forEach((link: HTMLAnchorElement) => expect(link.rel).toBe('alternate stylesheet'));
|
|
219
|
+
unmount();
|
|
220
|
+
});
|
|
221
|
+
});
|