Compare commits

..

12 Commits

Author SHA1 Message Date
Brian Smith
0c1eb6cae0 feat: import FooterSlot from component package instead of slot package (#226) 2025-04-24 12:10:52 -04:00
Brian Smith
16738335d0 fix(deps): update frontend-component-header to ^6.4.0 (#228) 2025-04-23 17:05:36 -04:00
edX requirements bot
658b70e455 chore: update browserslist DB (#227)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-04-21 00:13:06 +00:00
edX requirements bot
6cb174b146 chore: update browserslist DB (#225)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-04-14 00:13:07 +00:00
edX requirements bot
143f0dcd4b chore: update browserslist DB (#222)
Co-authored-by: arbrandes <759355+arbrandes@users.noreply.github.com>
2025-04-07 00:12:44 +00:00
Brian Smith
ad19426aee feat: upgrade to react 18 (#219) 2025-04-04 13:46:15 -04:00
Régis Behmo
5ab646b69c chore: remove husky 🪓🐶 (#220) 2025-04-04 05:13:54 -04:00
Brian Smith
1cd02a9dfb chore: update @openedx dependencies to versions that support React 18 (#218) 2025-03-27 16:15:55 -04:00
Feanil Patel
445a2f6cd3 Merge pull request #215 from salman2013/salman/update-catalog-info-file
Update catalog-info file for release data
2025-02-11 14:26:35 -05:00
salman2013
166b6fe7ae fix: remove openedx.yaml file 2025-02-11 13:56:57 +05:00
salman2013
a6711a59dc chore: update catalog-info file for release data 2025-01-31 17:11:48 +05:00
Muhammad Anas
454d3ddcdf test: Remove support for Node 18 (#213) 2024-10-31 14:44:39 -04:00
15 changed files with 4280 additions and 3070 deletions

View File

@@ -9,9 +9,6 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- name: Checkout
@@ -23,7 +20,7 @@ jobs:
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ node_modules
npm-debug.log
coverage
module.config.js
env.config.*
dist/
src/i18n/transifex_input.json

View File

@@ -12,6 +12,7 @@ metadata:
icon: "Article"
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:committers-frontend
type: "service"

View File

@@ -1,9 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
oeps: {}
openedx-release:
# The openedx-release key is described in OEP-10:
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
ref: master

6823
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,13 +16,9 @@
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/communications/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/edx/frontend-app-communications#readme",
@@ -34,8 +30,9 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "^8.0.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.4.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/tinymce-language-selector": "1.1.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
@@ -43,9 +40,8 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/frontend-plugin-framework": "^1.1.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.0.0",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/paragon": "^22.16.0",
"@tinymce/tinymce-react": "3.14.0",
"axios": "0.27.2",
"classnames": "2.3.2",
@@ -53,8 +49,8 @@
"jquery": "3.6.1",
"popper.js": "1.16.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
"react-router": "6.15.0",
@@ -65,13 +61,12 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
"@openedx/frontend-build": "14.0.3",
"@edx/reactifex": "^2.1.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"axios-mock-adapter": "1.21.2",
"glob": "7.2.3",
"husky": "7.0.4",
"jest": "29.7.0",
"prettier": "2.8.1",
"rosie": "2.1.0"

View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<UNDEFINED>
<ErrorPage
message="test-error-message"
/>
</UNDEFINED>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<UNDEFINED>
<AppProvider>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<link
href="favicon-url"
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<Routes>
<Route
element={
<AuthenticatedPageRoute>
<Page Container>
<Bulk Email Tool />
</Page Container>
</AuthenticatedPageRoute>
}
path="/courses/:courseId/bulk_email"
/>
</Routes>
</AppProvider>
</UNDEFINED>
`;

View File

@@ -3,7 +3,7 @@
*/
import React from 'react';
import {
render, screen, fireEvent, cleanup, act, initializeMockApp,
render, screen, fireEvent, cleanup, initializeMockApp,
} from '../../../../setupTest';
import { BulkEmailProvider } from '../../bulk-email-context';
import BulkEmailContentHistory from '../BulkEmailContentHistory';
@@ -41,107 +41,99 @@ describe('BulkEmailContentHistory component', () => {
});
test('renders a table when the button is pressed and data is returned', async () => {
await act(async () => {
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
render(renderBulkEmailContentHistory());
render(renderBulkEmailContentHistory());
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify component structure
const tableDescription = await screen.findByText(
'To read a sent email message, click the `View Message` button within the table.',
);
expect(tableDescription).toBeTruthy();
// verify component structure
const tableDescription = await screen.findByText(
'To read a sent email message, click the `View Message` button within the table.',
);
expect(tableDescription).toBeTruthy();
// verify table structure
expect(await screen.findByText('Subject')).toBeTruthy();
expect(await screen.findByText('Sent By')).toBeTruthy();
expect(await screen.findByText('Sent To')).toBeTruthy();
expect(await screen.findByText('Time Sent')).toBeTruthy();
expect(await screen.findByText('Number Sent')).toBeTruthy();
// verify table structure
expect(await screen.findByText('Subject')).toBeTruthy();
expect(await screen.findByText('Sent By')).toBeTruthy();
expect(await screen.findByText('Sent To')).toBeTruthy();
expect(await screen.findByText('Time Sent')).toBeTruthy();
expect(await screen.findByText('Number Sent')).toBeTruthy();
// verify table contents
const { emails } = emailHistoryData;
const email = emails[0];
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(email.number_sent)).toBeTruthy();
expect(await screen.findByText(email.requester)).toBeTruthy();
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
expect(await screen.findByText(email.email.subject)).toBeTruthy();
// verify screen reader only <span />
expect(await screen.findByText('0')).toHaveClass('sr-only');
expect(await screen.findAllByText('View Message')).toBeTruthy();
});
// verify table contents
const { emails } = emailHistoryData;
const email = emails[0];
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(email.number_sent)).toBeTruthy();
expect(await screen.findByText(email.requester)).toBeTruthy();
expect(await screen.findByText(email.sent_to.join(', '))).toBeTruthy();
expect(await screen.findByText(email.email.subject)).toBeTruthy();
// verify screen reader only <span />
expect(await screen.findByText('0')).toHaveClass('sr-only');
expect(await screen.findAllByText('View Message')).toBeTruthy();
});
test('renders a modal that will display the contents of the previously sent message to a user', async () => {
await act(async () => {
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
const emailHistoryData = buildEmailContentHistoryData(1);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
render(renderBulkEmailContentHistory());
render(renderBulkEmailContentHistory());
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
const viewMessageButton = await screen.findByText('View Message');
fireEvent.click(viewMessageButton);
const viewMessageButton = await screen.findByText('View Message');
fireEvent.click(viewMessageButton);
// verify modal components and behavior
const { emails } = emailHistoryData;
const email = emails[0];
const closeButton = await screen.findAllByText('Close');
// verify modal components and behavior
const { emails } = emailHistoryData;
const email = emails[0];
const closeButton = await screen.findAllByText('Close');
expect(closeButton).toBeTruthy();
expect(await screen.findByText('Subject:')).toBeTruthy();
expect(await screen.findByText('Sent by:')).toBeTruthy();
expect(await screen.findByText('Time sent:')).toBeTruthy();
expect(await screen.findByText('Sent to:')).toBeTruthy();
expect(await screen.findByText('Message:')).toBeTruthy();
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
expect(await screen.findAllByText(email.requester)).toBeTruthy();
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findAllByText(createdDate)).toBeTruthy();
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
// .replace() call strips the HTML tags from the string
expect(await screen.findByText(email.email.html_message.replace(/<[^>]*>?/gm, ''))).toBeTruthy();
});
expect(closeButton).toBeTruthy();
expect(await screen.findByText('Subject:')).toBeTruthy();
expect(await screen.findByText('Sent by:')).toBeTruthy();
expect(await screen.findByText('Time sent:')).toBeTruthy();
expect(await screen.findByText('Sent to:')).toBeTruthy();
expect(await screen.findByText('Message:')).toBeTruthy();
expect(await screen.findAllByText(email.email.subject)).toBeTruthy();
expect(await screen.findAllByText(email.requester)).toBeTruthy();
const createdDate = new Date(email.created).toLocaleString();
expect(await screen.findAllByText(createdDate)).toBeTruthy();
expect(await screen.findAllByText(email.sent_to.join(', '))).toBeTruthy();
// .replace() call strips the HTML tags from the string
expect(await screen.findByText(email.email.html_message.replace(/<[^>]*>?/gm, ''))).toBeTruthy();
});
test('renders a warning Alert when the button is pressed but there is no data to display', async () => {
await act(async () => {
const emailHistoryData = buildEmailContentHistoryData(0);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email history for this course.');
expect(alertMessage).toBeTruthy();
});
const emailHistoryData = buildEmailContentHistoryData(0);
getSentEmailHistory.mockImplementation(() => emailHistoryData);
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email history for this course.');
expect(alertMessage).toBeTruthy();
});
test('renders an error Alert when the button is pressed and an error occurs retrieving data', async () => {
await act(async () => {
getSentEmailHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'An error occurred retrieving email history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
getSentEmailHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(renderBulkEmailContentHistory());
// press the `show sent email history` button to initiate data retrieval
const showEmailContentHistoryButton = await screen.findByText('Show Sent Email History');
fireEvent.click(showEmailContentHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'An error occurred retrieving email history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
});
});

View File

@@ -3,7 +3,7 @@
*/
import React from 'react';
import {
render, screen, fireEvent, cleanup, act, initializeMockApp,
render, screen, fireEvent, cleanup, initializeMockApp,
} from '../../../../setupTest';
import BulkEmailTaskHistory from '../BulkEmailTaskHistory';
import { getEmailTaskHistory } from '../data/api';
@@ -32,72 +32,66 @@ describe('BulkEmailTaskHistory component', () => {
});
test('renders a table properly when the button is pressed and data is returned', async () => {
await act(async () => {
// build our mocked response
const taskHistoryData = buildEmailTaskHistoryData(1);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history button` to initiate data retrieval and rendering of the table in our component
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verification of table structure
expect(await screen.findByText('Task Type')).toBeTruthy();
expect(await screen.findByText('Task Inputs')).toBeTruthy();
expect(await screen.findByText('Task Id')).toBeTruthy();
expect(await screen.findByText('Requester')).toBeTruthy();
expect(await screen.findByText('Submitted')).toBeTruthy();
expect(await screen.findByText('Duration (seconds)')).toBeTruthy();
expect(await screen.findByText('State')).toBeTruthy();
expect(await screen.findByText('Status')).toBeTruthy();
expect(await screen.findByText('Task Progress')).toBeTruthy();
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
// verification of row contents
const { tasks } = taskHistoryData;
const task = tasks[0];
const createdDate = new Date(task.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
expect(await screen.findByText(task.requester)).toBeTruthy();
expect(await screen.findByText(task.status)).toBeTruthy();
expect(await screen.findByText(task.task_id)).toBeTruthy();
expect(await screen.findByText(task.task_input)).toBeTruthy();
expect(await screen.findByText(task.task_message)).toBeTruthy();
expect(await screen.findByText(task.task_state)).toBeTruthy();
expect(await screen.findByText(task.task_type)).toBeTruthy();
});
// build our mocked response
const taskHistoryData = buildEmailTaskHistoryData(1);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history button` to initiate data retrieval and rendering of the table in our component
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verification of table structure
expect(await screen.findByText('Task Type')).toBeTruthy();
expect(await screen.findByText('Task Inputs')).toBeTruthy();
expect(await screen.findByText('Task Id')).toBeTruthy();
expect(await screen.findByText('Requester')).toBeTruthy();
expect(await screen.findByText('Submitted')).toBeTruthy();
expect(await screen.findByText('Duration (seconds)')).toBeTruthy();
expect(await screen.findByText('State')).toBeTruthy();
expect(await screen.findByText('Status')).toBeTruthy();
expect(await screen.findByText('Task Progress')).toBeTruthy();
expect(await screen.findAllByText('Showing 1 - 1 of 1.')).toBeTruthy();
// verification of row contents
const { tasks } = taskHistoryData;
const task = tasks[0];
const createdDate = new Date(task.created).toLocaleString();
expect(await screen.findByText(createdDate)).toBeTruthy();
expect(await screen.findByText(task.duration_sec)).toBeTruthy();
expect(await screen.findByText(task.requester)).toBeTruthy();
expect(await screen.findByText(task.status)).toBeTruthy();
expect(await screen.findByText(task.task_id)).toBeTruthy();
expect(await screen.findByText(task.task_input)).toBeTruthy();
expect(await screen.findByText(task.task_message)).toBeTruthy();
expect(await screen.findByText(task.task_state)).toBeTruthy();
expect(await screen.findByText(task.task_type)).toBeTruthy();
});
test('renders a warning Alert when the button is pressed but there is no data to display', async () => {
await act(async () => {
const taskHistoryData = buildEmailTaskHistoryData(0);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email task history for this course.');
expect(alertMessage).toBeTruthy();
});
const taskHistoryData = buildEmailTaskHistoryData(0);
getEmailTaskHistory.mockImplementation(() => taskHistoryData);
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText('There is no email task history for this course.');
expect(alertMessage).toBeTruthy();
});
test('renders an error Alert when the button is pressed and an error occurs retrieving data', async () => {
await act(async () => {
getEmailTaskHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'Error fetching email task history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
getEmailTaskHistory.mockImplementation(() => {
throw new Error();
});
// render the component
render(<BulkEmailTaskHistory />);
// press the `show task history` button to initiate data retrieval
const showTaskHistoryButton = await screen.findByText('Show Email Task History');
fireEvent.click(showTaskHistoryButton);
// verify that an alert is displayed since the array of tasks is empty
const alertMessage = await screen.findByText(
'Error fetching email task history data for this course. Please try again later.',
);
expect(alertMessage).toBeTruthy();
});
});

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useParams } from 'react-router-dom';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import FooterSlot from '@openedx/frontend-slot-footer';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Spinner } from '@openedx/paragon';
import { getCohorts, getCourseHomeCourseMetadata } from './data/api';

View File

@@ -4,7 +4,7 @@
import React from 'react';
import { Factory } from 'rosie';
import {
act, cleanup, initializeMockApp, render, screen,
cleanup, initializeMockApp, render, screen,
} from '../../../setupTest';
import PageContainer from '../PageContainer';
@@ -32,36 +32,32 @@ describe('PageContainer', () => {
afterEach(cleanup);
test('PageContainer renders properly when given course metadata', async () => {
await act(async () => {
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
render(<PageContainer />);
render(<PageContainer />);
// Look for the org, title, and number of the course, which should be displayed in the Header.
expect(await screen.findByText(`${courseMetadata.org} ${courseMetadata.number}`)).toBeTruthy();
expect(await screen.findByText(courseMetadata.title)).toBeTruthy();
});
// Look for the org, title, and number of the course, which should be displayed in the Header.
expect(await screen.findByText(`${courseMetadata.org} ${courseMetadata.number}`)).toBeTruthy();
expect(await screen.findByText(courseMetadata.title)).toBeTruthy();
});
test('PageContainer renders children nested within it.', async () => {
await act(async () => {
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
const cohorts = { cohorts: [Factory.build('cohort'), Factory.build('cohort')] };
const courseMetadata = Factory.build('courseMetadata');
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
getCohorts.mockImplementation(() => cohorts);
getCourseHomeCourseMetadata.mockImplementation(() => courseMetadata);
render(
<PageContainer>
<span>Test Text</span>
</PageContainer>,
);
render(
<PageContainer>
<span>Test Text</span>
</PageContainer>,
);
expect(await screen.findByText('Test Text')).toBeTruthy();
});
expect(await screen.findByText('Test Text')).toBeTruthy();
});
});

View File

@@ -5,7 +5,8 @@ import {
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig, getConfig,
} from '@edx/frontend-platform';
import { AppProvider, AuthenticatedPageRoute, ErrorPage } from '@edx/frontend-platform/react';
import ReactDOM from 'react-dom';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Helmet } from 'react-helmet';
import { Routes, Route } from 'react-router-dom';
@@ -16,30 +17,39 @@ import BulkEmailTool from './components/bulk-email-tool';
import PageContainer from './components/page-container/PageContainer';
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<Routes>
<Route
path="/courses/:courseId/bulk_email"
element={(
<AuthenticatedPageRoute>
<PageContainer>
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<Routes>
<Route
path="/courses/:courseId/bulk_email"
element={(
<AuthenticatedPageRoute>
<PageContainer>
<BulkEmailTool />
</PageContainer>
</AuthenticatedPageRoute>
)}
/>
</Routes>
</AppProvider>,
document.getElementById('root'),
/>
</Routes>
</AppProvider>
</StrictMode>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
});
initialize({

67
src/index.test.jsx Normal file
View File

@@ -0,0 +1,67 @@
import {
APP_INIT_ERROR, APP_READY, subscribe,
} from '@edx/frontend-platform';
// Jest needs this for module resolution
import * as app from '.'; // eslint-disable-line no-unused-vars
// These need to be var not let so they get hoisted
// and can be used by jest.mock (which is also hoisted)
var mockRender; // eslint-disable-line no-var
var mockCreateRoot; // eslint-disable-line no-var
jest.mock('react-dom/client', () => {
mockRender = jest.fn();
mockCreateRoot = jest.fn(() => ({
render: mockRender,
}));
return ({
createRoot: mockCreateRoot,
});
});
jest.mock('@edx/frontend-platform', () => ({
APP_READY: 'app-is-ready-key',
APP_INIT_ERROR: 'app-init-error',
subscribe: jest.fn(),
initialize: jest.fn(),
mergeConfig: jest.fn(),
getConfig: () => ({
FAVICON_URL: 'favicon-url',
}),
ensureConfig: jest.fn(),
}));
jest.mock('./components/bulk-email-tool/BulkEmailTool', () => 'Bulk Email Tool');
jest.mock('./components/page-container/PageContainer', () => 'Page Container');
describe('app registry', () => {
let getElement;
beforeEach(() => {
mockCreateRoot.mockClear();
mockRender.mockClear();
getElement = window.document.getElementById;
window.document.getElementById = jest.fn(id => ({ id }));
});
afterAll(() => {
window.document.getElementById = getElement;
});
test('subscribe: APP_READY. links App to root element', () => {
const callArgs = subscribe.mock.calls[0];
expect(callArgs[0]).toEqual(APP_READY);
callArgs[1]();
const [rendered] = mockRender.mock.calls[0];
expect(rendered).toMatchSnapshot();
});
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
const callArgs = subscribe.mock.calls[1];
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
const error = { message: 'test-error-message' };
callArgs[1](error);
const [rendered] = mockRender.mock.calls[0];
expect(rendered).toMatchSnapshot();
});
});

View File

@@ -1,12 +1,15 @@
# Footer Slot
### Slot ID: `footer_slot`
### Slot ID: `org.openedx.frontend.layout.footer.v1`
### Slot ID Aliases
* `footer_slot`
## Description
This slot is used to replace/modify/hide the footer.
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
## Example
@@ -23,7 +26,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
footer_slot: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer

View File

@@ -1,3 +1,3 @@
# `frontend-app-communications` Plugin Slots
* [`footer_slot`](./FooterSlot/)
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)