@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.
- package/config/types.js +0 -2
- package/config/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
- package/package.json +8 -1
- 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/i18n/lib.test.js +230 -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/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/hooks.test.jsx +104 -0
- package/runtime/routing/utils.test.ts +7 -0
- package/runtime/scripts/GoogleAnalyticsLoader.test.ts +77 -0
- 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/testing/initializeMockApp.test.ts +66 -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.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/router/createRouter.test.tsx +50 -0
- package/shell/router/getAppRoutes.test.tsx +59 -0
- package/shell/site.config.dev.tsx +3 -3
- package/shell/site.config.test.tsx +16 -0
- 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/types.js +0 -2
- package/tools/dist/webpack/plugins/html-webpack-new-relic-plugin/test/HtmlWebpackNewRelicPlugin.test.js +66 -0
- package/types.ts +1 -1
- 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/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/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,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
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import siteConfig from 'site.config';
|
|
3
|
+
import { EnvironmentTypes } from '../../types';
|
|
4
|
+
import { sendTrackEvent } from '../analytics';
|
|
5
|
+
import { setAuthenticatedUser } from '../auth';
|
|
6
|
+
import { initializeMockApp } from '../testing';
|
|
7
|
+
import SiteProvider from './SiteProvider';
|
|
8
|
+
import { useAuthenticatedUser, useSiteConfig, useTrackColorSchemeChoice } from './hooks';
|
|
9
|
+
|
|
10
|
+
jest.mock('../analytics');
|
|
11
|
+
|
|
12
|
+
const mockAddEventListener = jest.fn();
|
|
13
|
+
const mockRemoveEventListener = jest.fn();
|
|
14
|
+
let matchesMock;
|
|
15
|
+
|
|
16
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
17
|
+
value: jest.fn(() => ({
|
|
18
|
+
addEventListener: mockAddEventListener,
|
|
19
|
+
removeEventListener: mockRemoveEventListener,
|
|
20
|
+
matches: matchesMock,
|
|
21
|
+
})),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('useTrackColorSchemeChoice hook', () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
mockAddEventListener.mockClear();
|
|
27
|
+
mockRemoveEventListener.mockClear();
|
|
28
|
+
sendTrackEvent.mockClear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('sends dark preferred color schema event if query matches', async () => {
|
|
32
|
+
matchesMock = true;
|
|
33
|
+
renderHook(() => useTrackColorSchemeChoice());
|
|
34
|
+
|
|
35
|
+
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(sendTrackEvent).toHaveBeenCalledWith(
|
|
37
|
+
'openedx.ui.frontend-base.prefers-color-scheme.selected',
|
|
38
|
+
{ preferredColorScheme: 'dark' },
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('sends light preferred color schema event if query does not match', async () => {
|
|
43
|
+
matchesMock = false;
|
|
44
|
+
renderHook(() => useTrackColorSchemeChoice());
|
|
45
|
+
|
|
46
|
+
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(sendTrackEvent).toHaveBeenCalledWith(
|
|
48
|
+
'openedx.ui.frontend-base.prefers-color-scheme.selected',
|
|
49
|
+
{ preferredColorScheme: 'light' },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('adds change event listener to matchMedia query', async () => {
|
|
54
|
+
renderHook(() => useTrackColorSchemeChoice());
|
|
55
|
+
|
|
56
|
+
expect(mockAddEventListener).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(mockAddEventListener).toHaveBeenCalledWith('change', expect.any(Function));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('useAuthenticatedUser', () => {
|
|
62
|
+
it('returns null when the user is anonymous', () => {
|
|
63
|
+
const { result } = renderHook(() => useAuthenticatedUser());
|
|
64
|
+
expect(result.current).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('with a user', () => {
|
|
68
|
+
const user = {
|
|
69
|
+
administrator: true,
|
|
70
|
+
email: 'admin@example.com',
|
|
71
|
+
name: 'Admin',
|
|
72
|
+
roles: ['admin'],
|
|
73
|
+
userId: 1,
|
|
74
|
+
username: 'admin-user',
|
|
75
|
+
avatar: 'http://localhost/admin.png',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
initializeMockApp({
|
|
80
|
+
authenticatedUser: user,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
act(() => {
|
|
86
|
+
setAuthenticatedUser(null);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns a User when the user exists', () => {
|
|
91
|
+
const { result } = renderHook(() => useAuthenticatedUser(), { wrapper: SiteProvider });
|
|
92
|
+
expect(result.current).toBe(user);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('useSiteConfig', () => {
|
|
98
|
+
it('returns the site config', () => {
|
|
99
|
+
const { result } = renderHook(() => useSiteConfig());
|
|
100
|
+
expect(result.current).toHaveProperty('apps', siteConfig.apps);
|
|
101
|
+
expect(result.current).toHaveProperty('environment', EnvironmentTypes.TEST);
|
|
102
|
+
expect(result.current).toHaveProperty('baseUrl', 'http://localhost:8080');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { GoogleAnalyticsLoader } from './index';
|
|
2
|
+
|
|
3
|
+
const googleAnalyticsId = 'test-key';
|
|
4
|
+
|
|
5
|
+
describe('GoogleAnalytics', () => {
|
|
6
|
+
let body;
|
|
7
|
+
let gaScriptSrc;
|
|
8
|
+
let gaScriptGtag;
|
|
9
|
+
let data;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// @ts-expect-error We're adding it, this is fine.
|
|
13
|
+
window.googleAnalytics = [];
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function loadGoogleAnalytics(scriptData) {
|
|
17
|
+
const script = new GoogleAnalyticsLoader(scriptData);
|
|
18
|
+
script.loadScript();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('with valid GOOGLE_ANALYTICS_4_ID', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
document.body.innerHTML = '<script id="stub" />';
|
|
24
|
+
data = {
|
|
25
|
+
config: {
|
|
26
|
+
GOOGLE_ANALYTICS_4_ID: googleAnalyticsId,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
loadGoogleAnalytics(data);
|
|
30
|
+
expect(global.googleAnalytics.invoked).toBe(true);
|
|
31
|
+
body = document.body.innerHTML;
|
|
32
|
+
gaScriptSrc = `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`;
|
|
33
|
+
gaScriptGtag = `gtag('config', '${googleAnalyticsId}');`;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should initialize google analytics', () => {
|
|
37
|
+
expect(body).toMatch(gaScriptSrc);
|
|
38
|
+
expect(body).toMatch(gaScriptGtag);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should not invoke snippet twice', () => {
|
|
42
|
+
loadGoogleAnalytics(data);
|
|
43
|
+
|
|
44
|
+
expect(global.googleAnalytics.invoked).toBe(true);
|
|
45
|
+
|
|
46
|
+
expect(body).toMatch(gaScriptSrc);
|
|
47
|
+
expect(body).toMatch(gaScriptGtag);
|
|
48
|
+
|
|
49
|
+
let count = (body.match(new RegExp(gaScriptSrc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
|
50
|
+
expect(count).toBe(1);
|
|
51
|
+
|
|
52
|
+
count = (body.match(new RegExp(gaScriptGtag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
|
53
|
+
expect(count).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('with invalid GOOGLE_ANALYTICS_ID', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
document.body.innerHTML = '<script id="stub" />';
|
|
60
|
+
data = {
|
|
61
|
+
config: {
|
|
62
|
+
GOOGLE_ANALYTICS_4_ID: '',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
loadGoogleAnalytics(data);
|
|
66
|
+
body = document.body.innerHTML;
|
|
67
|
+
gaScriptSrc = 'https://www.googletagmanager.com/gtag/js?id=';
|
|
68
|
+
gaScriptGtag = "gtag('config', '');";
|
|
69
|
+
expect(global.googleAnalytics.invoked).toBeFalsy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should not initialize google analytics', () => {
|
|
73
|
+
expect(body).not.toMatch(gaScriptSrc);
|
|
74
|
+
expect(body).not.toMatch(gaScriptGtag);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EnvironmentTypes, SiteConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
const siteConfig: SiteConfig = {
|
|
4
|
+
siteId: 'runtime',
|
|
5
|
+
siteName: '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
|
+
appId: 'test-app',
|
|
14
|
+
routes: [{
|
|
15
|
+
path: '/app1',
|
|
16
|
+
element: (
|
|
17
|
+
<div>Test App 1</div>
|
|
18
|
+
),
|
|
19
|
+
handle: {
|
|
20
|
+
role: 'test-app-1'
|
|
21
|
+
}
|
|
22
|
+
}]
|
|
23
|
+
}],
|
|
24
|
+
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
|
|
25
|
+
csrfTokenApiPath: '/csrf/api/v1/token',
|
|
26
|
+
languagePreferenceCookieName: 'openedx-language-preference',
|
|
27
|
+
refreshAccessTokenApiPath: '/login_refresh',
|
|
28
|
+
userInfoCookieName: 'edx-user-info',
|
|
29
|
+
ignoredErrorRegex: null,
|
|
30
|
+
segmentKey: 'segment_whoa',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default siteConfig;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { useContext } from 'react';
|
|
4
|
+
import { useLayoutForSlotId } from './layout/hooks';
|
|
5
|
+
import Slot from './Slot';
|
|
6
|
+
import SlotContext from './SlotContext';
|
|
7
|
+
|
|
8
|
+
jest.mock('./layout/hooks');
|
|
9
|
+
|
|
10
|
+
describe('Slot component', () => {
|
|
11
|
+
it('renders with default layout', () => {
|
|
12
|
+
(useLayoutForSlotId as jest.Mock).mockReturnValue(null);
|
|
13
|
+
const { container } = render(<MemoryRouter><Slot id="test-slot.ui" /></MemoryRouter>);
|
|
14
|
+
expect(container).toBeEmptyDOMElement();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('successfully passes the slot ID through a SlotContext', () => {
|
|
18
|
+
function IdPrintingLayout() {
|
|
19
|
+
const { id } = useContext(SlotContext);
|
|
20
|
+
return <div>Slot ID: {id}</div>;
|
|
21
|
+
}
|
|
22
|
+
(useLayoutForSlotId as jest.Mock).mockReturnValue(IdPrintingLayout);
|
|
23
|
+
const { getByText } = render(<Slot id="test-slot.ui" />);
|
|
24
|
+
expect(getByText('Slot ID: test-slot.ui')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders with a component override layout', () => {
|
|
28
|
+
const CustomLayoutComponent = () => <div>Custom Layout Component</div>;
|
|
29
|
+
(useLayoutForSlotId as jest.Mock).mockReturnValue(CustomLayoutComponent);
|
|
30
|
+
const { getByText } = render(<Slot id="test-slot.ui" />);
|
|
31
|
+
expect(getByText('Custom Layout Component')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders with an element override layout', () => {
|
|
35
|
+
const CustomLayoutElement = <div>Custom Layout Element</div>;
|
|
36
|
+
(useLayoutForSlotId as jest.Mock).mockReturnValue(null);
|
|
37
|
+
const { getByText } = render(<Slot id="test-slot.ui" layout={CustomLayoutElement} />);
|
|
38
|
+
expect(getByText('Custom Layout Element')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { useWidgets } from '../widget/hooks';
|
|
3
|
+
import DefaultSlotLayout from './DefaultSlotLayout';
|
|
4
|
+
|
|
5
|
+
// Mock the useWidgets hook
|
|
6
|
+
jest.mock('../widget/hooks', () => ({
|
|
7
|
+
useWidgets: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('DefaultSlotLayout', () => {
|
|
11
|
+
it('renders widgets returned by useWidgets hook', () => {
|
|
12
|
+
const mockWidgets = [
|
|
13
|
+
<div key="widget1">Widget One</div>,
|
|
14
|
+
<div key="widget2">Widget Two</div>,
|
|
15
|
+
];
|
|
16
|
+
(useWidgets as jest.Mock).mockReturnValue(mockWidgets);
|
|
17
|
+
|
|
18
|
+
const { getByText } = render(<DefaultSlotLayout />);
|
|
19
|
+
|
|
20
|
+
expect(getByText('Widget One')).toBeInTheDocument();
|
|
21
|
+
expect(getByText('Widget Two')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders empty when no widgets are returned', () => {
|
|
25
|
+
(useWidgets as jest.Mock).mockReturnValue([]);
|
|
26
|
+
|
|
27
|
+
const { container } = render(<DefaultSlotLayout />);
|
|
28
|
+
|
|
29
|
+
expect(container).toBeEmptyDOMElement();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -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
|
+
});
|