Compare commits

...

11 Commits

Author SHA1 Message Date
Javier Ontiveros
d63d9b7b4f chore: update base route path to match old paths (#797) 2026-03-11 12:29:45 -03:00
Adolfo R. Brandes
0632525600 1.0.0-alpha.6 2026-03-04 11:01:27 -03:00
Adolfo R. Brandes
70576cf373 feat!: compile to JS before publishing
Configure the package to compile TypeScript and copy SCSS and image
assets (PNG, SVG) to dist/ before publishing, rather than publishing raw
source files. This allows us to use tsc-alias for @src imports.

Also use a more modern export map to decouple the internal file
structure from the package's API, and add a build step to CI.

BREAKING CHANGE: Consuming projects may need to update their imports or
SASS @use lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:29:46 -03:00
javier ontiveros
35283dfdd3 feat: added code spliting on main module 2026-02-11 09:58:55 -03:00
Adolfo R. Brandes
852444af42 1.0.0-alpha.5 2026-02-09 20:24:24 -03:00
Adolfo R. Brandes
4f117dfc96 test: fix minor issues with tests 2026-02-06 11:46:04 -03:00
Jesus Balderrama
21cb51861d fix: tests fixed after design tokens support change (#1) 2026-02-06 11:46:04 -03:00
Diana Villalvazo
48eeff44fe test: Remove react-unit-test-utils 2026-02-06 11:46:04 -03:00
Adolfo R. Brandes
648be5f579 fix: missing styles
The shell's SCSS must be explicitly loaded by site.config.dev.tsx.
2026-02-06 11:46:04 -03:00
Adolfo R. Brandes
0d96fff5f1 build: Update to Node 24 2026-02-06 11:46:04 -03:00
Adolfo R. Brandes
2c295e8ecc feat!: add design tokens support
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.
2026-02-06 11:46:04 -03:00
170 changed files with 6020 additions and 12525 deletions

2
.nvmrc
View File

@@ -1 +1 @@
20
24

View File

@@ -24,6 +24,19 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
requirements: ## install ci requirements
npm ci
clean:
rm -rf dist
build: clean
tsc --project tsconfig.build.json
tsc-alias -p tsconfig.build.json
find src -type f \( -name '*.scss' -o -name '*.png' -o -name '*.svg' \) -exec sh -c '\
for f in "$$@"; do \
d="dist/$${f#src/}"; \
mkdir -p "$$(dirname "$$d")"; \
cp "$$f" "$$d"; \
done' sh {} +
i18n.extract:
# Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp)

View File

@@ -1,3 +1,3 @@
const { createConfig } = require('@openedx/frontend-base/config');
const { createConfig } = require('@openedx/frontend-base/tools');
module.exports = createConfig('babel');

View File

@@ -1,6 +1,6 @@
// @ts-check
const { createLintConfig } = require('@openedx/frontend-base/config');
const { createLintConfig } = require('@openedx/frontend-base/tools');
module.exports = createLintConfig(
{

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@openedx/frontend-base/config');
const { createConfig } = require('@openedx/frontend-base/tools');
module.exports = createConfig('test', {
setupFilesAfterEnv: [
@@ -14,6 +14,7 @@ module.exports = createConfig('test', {
'src/__mocks__',
],
moduleNameMapper: {
// Asset mocks
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
},

10996
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,17 @@
{
"name": "@openedx/frontend-app-learner-dashboard",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.6",
"description": "",
"repository": {
"type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"main": "src/index.ts",
"exports": {
".": "./dist/index.js",
"./app.scss": "./dist/app.scss"
},
"files": [
"/src"
"/dist"
],
"browserslist": [
"extends @edx/browserslist-config"
@@ -18,11 +21,13 @@
"*.scss"
],
"scripts": {
"build": "make build",
"clean": "make clean",
"dev": "PORT=1996 PUBLIC_PATH=/learner-dashboard openedx dev",
"i18n_extract": "openedx formatjs extract",
"lint": "openedx lint .",
"lint:fix": "openedx lint --fix .",
"snapshot": "openedx test --updateSnapshot",
"prepack": "npm run build",
"test": "openedx test --coverage --passWithNoTests"
},
"author": "Open edX",
@@ -37,7 +42,6 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
@@ -59,6 +63,7 @@
"@edx/browserslist-config": "^1.5.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@@ -66,11 +71,12 @@
"jest-when": "^3.6.0",
"react-dev-utils": "^12.0.0",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
"redux-mock-store": "^1.5.4",
"tsc-alias": "^1.8.16"
},
"peerDependencies": {
"@openedx/frontend-base": "^1.0.0-alpha.6",
"@openedx/paragon": "^22",
"@openedx/frontend-base": "^1.0.0-alpha.13",
"@openedx/paragon": "^23",
"@tanstack/react-query": "^5",
"@types/react": "^18",
"@types/react-dom": "^18",

View File

@@ -13,7 +13,6 @@ const siteConfig: SiteConfig = {
logoutUrl: 'http://local.openedx.io:8000/logout',
environment: EnvironmentTypes.DEVELOPMENT,
basename: '/learner-dashboard',
apps: [
shellApp,
headerApp,

View File

@@ -11,7 +11,6 @@ const siteConfig: SiteConfig = {
logoutUrl: 'http://localhost:8000/logout',
environment: EnvironmentTypes.TEST,
basename: '/learner-dashboard',
apps: [{
appId,
config: {

View File

@@ -1,83 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component component initialize failure snapshot 1`] = `
<Fragment>
<div>
<AppWrapper>
<main
id="main"
>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
</div>
</Fragment>
`;
exports[`App router component component no network failure snapshot 1`] = `
<Fragment>
<div>
<AppWrapper>
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely project id snapshot 1`] = `
<Fragment>
<div>
<AppWrapper>
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
<Fragment>
<div>
<AppWrapper>
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
</div>
</Fragment>
`;
exports[`App router component component refresh failure snapshot 1`] = `
<Fragment>
<div>
<AppWrapper>
<main
id="main"
>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
</div>
</Fragment>
`;

View File

@@ -1,43 +0,0 @@
// 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>
<SiteProvider
store={
{
"redux": "store",
}
}
>
<NoticesWrapper>
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
path="/"
/>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</SiteProvider>
</UNDEFINED>
`;

View File

@@ -1,12 +1,9 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@use "@openedx/frontend-base/shell/app.scss";
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
#learnerDashboardRoot {
main {

View File

@@ -1,27 +1,27 @@
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import Banner from './Banner';
describe('Banner', () => {
const props = {
children: 'Hello, world!',
};
describe('snapshot', () => {
test('renders default banner', () => {
const wrapper = shallow(<Banner {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('renders with variants', () => {
const wrapper = shallow(<Banner {...props} variant="success" />);
expect(wrapper.snapshot).toMatchSnapshot();
describe('Banner component', () => {
it('renders children content', () => {
render(<Banner>Test content</Banner>);
expect(screen.getByText('Test content')).toBeInTheDocument();
});
expect(wrapper.instance.findByType(Alert)[0].props.variant).toEqual('success');
});
test('renders with custom class', () => {
const wrapper = shallow(<Banner {...props} className="custom-class" />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('uses default props correctly', () => {
render(<Banner>Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('mb-0');
});
it('accepts custom variant prop', () => {
render(<Banner variant="success">Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-success');
});
it('accepts custom className prop', () => {
render(<Banner className="custom-class">Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('custom-class');
});
});

View File

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Banner snapshot renders default banner 1`] = `
<Alert
className="mb-0"
icon={[MockFunction icons.Info]}
variant="info"
>
Hello, world!
</Alert>
`;
exports[`Banner snapshot renders with custom class 1`] = `
<Alert
className="custom-class"
icon={[MockFunction icons.Info]}
variant="info"
>
Hello, world!
</Alert>
`;
exports[`Banner snapshot renders with variants 1`] = `
<Alert
className="mb-0"
icon={[MockFunction icons.Info]}
variant="success"
>
Hello, world!
</Alert>
`;

View File

@@ -1,10 +1,8 @@
@import "@openedx/paragon/scss/core/core";
.course-card {
.card {
.pgn__card-wrapper-image-cap.vertical {
display: flex;
min-height: $card-image-vertical-max-height;
min-height: var(--pgn-size-card-image-vertical-max-height);
}
.pgn__card-image-cap {
border-bottom-left-radius: 0 !important;
@@ -53,11 +51,11 @@
> .alert {
border-radius: 0;
box-shadow: none;
padding: map-get($spacers, 3) map-get($spacers, 4);
padding: var(--pgn-spacing-spacer-3) var(--pgn-spacing-spacer-4);
&:last-of-type {
border-bottom-left-radius: $alert-border-radius;
border-bottom-right-radius: $alert-border-radius;
border-bottom-left-radius: var(--pgn-size-alert-border-radius);
border-bottom-right-radius: var(--pgn-size-alert-border-radius);
}
}

View File

@@ -1,111 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard component snapshot: collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="vertical"
>
<div
className="d-flex flex-column w-100"
>
<div>
<CourseCardImage
cardId="test-card-id"
orientation="horizontal"
/>
<Card.Body>
<Card.Header
actions={
<CourseCardMenu
cardId="test-card-id"
/>
}
title={
<CourseCardTitle
cardId="test-card-id"
/>
}
/>
<Card.Section
className="pt-0"
>
<CourseCardDetails
cardId="test-card-id"
/>
</Card.Section>
<Card.Footer
orientation="vertical"
>
<CourseCardActions
cardId="test-card-id"
/>
</Card.Footer>
</Card.Body>
</div>
<CourseCardBanners
cardId="test-card-id"
/>
</div>
</Card>
</div>
`;
exports[`CourseCard component snapshot: not collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="horizontal"
>
<div
className="d-flex flex-column w-100"
>
<div
className="d-flex"
>
<CourseCardImage
cardId="test-card-id"
orientation="horizontal"
/>
<Card.Body>
<Card.Header
actions={
<CourseCardMenu
cardId="test-card-id"
/>
}
title={
<CourseCardTitle
cardId="test-card-id"
/>
}
/>
<Card.Section
className="pt-0"
>
<CourseCardDetails
cardId="test-card-id"
/>
</Card.Section>
<Card.Footer
orientation="horizontal"
>
<CourseCardActions
cardId="test-card-id"
/>
</Card.Footer>
</Card.Body>
</div>
<CourseCardBanners
cardId="test-card-id"
/>
</div>
</Card>
</div>
`;

View File

@@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActionButton snapshot is collapsed 1`] = `
<Button
arbitary="props"
size="sm"
/>
`;
exports[`ActionButton snapshot is not collapsed 1`] = `
<Button
arbitary="props"
/>
`;

View File

@@ -1,6 +1,33 @@
import { useWindowSize, breakpoints } from '@openedx/paragon';
import useIsCollapsed from './hooks';
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
useWindowSize: jest.fn(),
breakpoints: {
extraSmall: {
minWidth: 0,
maxWidth: 575,
},
small: {
minWidth: 576,
maxWidth: 767,
},
medium: {
minWidth: 768,
maxWidth: 991,
},
large: {
minWidth: 992,
maxWidth: 1199,
},
extraLarge: {
minWidth: 1200,
maxWidth: 100000,
},
},
}));
describe('useIsCollapsed', () => {
it('returns true only when it is between medium and small', () => {
// make sure all three breakpoints gap is large enough for test

View File

@@ -1,5 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import ActionButton from '.';
import useIsCollapsed from './hooks';
@@ -8,18 +7,22 @@ jest.mock('./hooks', () => jest.fn());
describe('ActionButton', () => {
const props = {
arbitary: 'props',
className: 'custom-class',
children: 'Test',
};
describe('snapshot', () => {
test('is collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('is collapsed', async () => {
useIsCollapsed.mockReturnValue(true);
render(<ActionButton {...props} />);
const button = screen.getByRole('button', { name: 'Test' });
expect(button).toHaveClass('btn-sm', 'custom-class');
});
it('is not collapsed', () => {
useIsCollapsed.mockReturnValue(false);
render(<ActionButton {...props} />);
const button = screen.getByRole('button', { name: 'Test' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('size', 'sm');
});
});

View File

@@ -1,27 +1,29 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import useActionDisabledState from '../hooks';
import BeginCourseButton from './BeginCourseButton';
jest.mock('tracking', () => ({
jest.mock('@src/tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const homeUrl = 'home-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
@@ -30,56 +32,57 @@ reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
);
const props = {
cardId: 'cardId',
};
const renderComponent = () => render(<IntlProvider locale="en"><BeginCourseButton {...props} /></IntlProvider>);
describe('BeginCourseButton', () => {
const props = {
cardId: 'cardId',
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
describe('initiliaze hooks', () => {
it('initializes course run data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
renderComponent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
renderComponent();
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
});
it('loads disabled states for begin action from action hooks', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
renderComponent();
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
});
describe('snapshot', () => {
describe('behavior', () => {
describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true);
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
renderComponent();
const button = screen.getByRole('button', { name: 'Begin Course' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});
describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.instance.props.disabled).toEqual(false);
renderComponent();
const button = screen.getByRole('button', { name: 'Begin Course' });
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
});
it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
it('should track enter course clicked event on click, with exec ed param', async () => {
renderComponent();
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Begin Course' });
user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked,
props.cardId,
homeUrl + execEdPath(props.cardId),
));
);
});
});
});

View File

@@ -1,17 +1,19 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import useActionDisabledState from '../hooks';
import ResumeButton from './ResumeButton';
jest.mock('tracking', () => ({
jest.mock('@src/tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),
@@ -19,7 +21,8 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const resumeUrl = 'resume-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
@@ -29,55 +32,52 @@ reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
);
let wrapper;
describe('ResumeButton', () => {
const props = {
cardId: 'cardId',
};
describe('behavior', () => {
describe('initialize hooks', () => {
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
it('initializes course run data with cardId', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
});
it('loads disabled states for resume action from action hooks', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
});
describe('snapshot', () => {
describe('behavior', () => {
describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true);
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Resume' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});
describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.instance.props.disabled).toEqual(false);
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Resume' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
});
it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
it('should track enter course clicked event on click, with exec ed param', async () => {
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Resume' });
user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked,
props.cardId,
resumeUrl + execEdPath(props.cardId),
));
);
});
});
});

View File

@@ -1,34 +1,43 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import useActionDisabledState from '../hooks';
import SelectSessionButton from './SelectSessionButton';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
useUpdateSelectSessionModalCallback: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
describe('SelectSessionButton', () => {
const props = { cardId: 'cardId' };
it('default render', () => {
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick.getMockName()).toEqual(
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
);
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Select Session' });
expect(button).toBeInTheDocument();
});
test('disabled states', () => {
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
describe('if useActionDisabledState is false', () => {
it('should disabled Select Session', () => {
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Select Session' });
expect(button).toBeDisabled();
});
});
describe('on click', () => {
it('should call openSessionModal', async () => {
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Select Session' });
await user.click(button);
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(props.cardId);
});
});
});

View File

@@ -1,45 +1,55 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import track from '@src/tracking';
import { reduxHooks } from '@src/hooks';
import useActionDisabledState from '../hooks';
import ViewCourseButton from './ViewCourseButton';
jest.mock('tracking', () => ({
jest.mock('@src/tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const defaultProps = { cardId: 'cardId' };
const homeUrl = 'homeUrl';
describe('ViewCourseButton', () => {
test('learner can view course', () => {
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
it('learner can view course', async () => {
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'View Course' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
});
it('calls trackCourseEvent on click', async () => {
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'View Course' });
await user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
));
expect(wrapper.instance.props.disabled).toEqual(false);
);
});
test('learner cannot view course', () => {
it('learner cannot view course', () => {
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'View Course' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResumeButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;
exports[`ResumeButton snapshot enabled snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionButton default render 1`] = `
<ActionButton
disabled={false}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</ActionButton>
`;
exports[`SelectSessionButton disabled states 1`] = `
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</ActionButton>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewCourseButton learner can view course 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;
exports[`ViewCourseButton learner cannot view course 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;

View File

@@ -1,16 +1,9 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'slots/CourseCardActionSlot';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
import ViewCourseButton from './ViewCourseButton';
import { render, screen } from '@testing-library/react';
import { reduxHooks } from '@src/hooks';
import CourseCardActions from '.';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -19,16 +12,15 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('slots/CourseCardActionSlot', () => 'CustomActionButton');
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
jest.mock('./ResumeButton', () => 'ResumeButton');
jest.mock('@src/slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>));
jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>));
jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));
jest.mock('./ResumeButton', () => jest.fn(() => <div>ResumeButton</div>));
const cardId = 'test-card-id';
const props = { cardId };
let el;
describe('CourseCardActions', () => {
const mockHooks = ({
isEntitlement = false,
@@ -44,13 +36,11 @@ describe('CourseCardActions', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
};
const render = () => {
el = shallow(<CourseCardActions {...props} />);
};
describe('behavior', () => {
const renderComponent = () => render(<CourseCardActions {...props} />);
describe('hooks', () => {
it('initializes redux hooks', () => {
mockHooks();
render();
renderComponent();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
@@ -60,36 +50,44 @@ describe('CourseCardActions', () => {
describe('entitlement course', () => {
it('renders ViewCourseButton if fulfilled', () => {
mockHooks({ isEntitlement: true, isFulfilled: true });
render();
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
renderComponent();
const ViewCourseButton = screen.getByText('ViewCourseButton');
expect(ViewCourseButton).toBeInTheDocument();
});
it('renders SelectSessionButton if not fulfilled', () => {
mockHooks({ isEntitlement: true });
render();
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
renderComponent();
const SelectSessionButton = screen.getByText('SelectSessionButton');
expect(SelectSessionButton).toBeInTheDocument();
});
});
describe('not entitlement, verified, or exec ed', () => {
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
renderComponent();
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(CourseCardActionSlot).toBeInTheDocument();
const ViewCourseButton = screen.getByText('ViewCourseButton');
expect(ViewCourseButton).toBeInTheDocument();
});
describe('unstarted courses', () => {
it('renders CourseCardActionSlot and BeginCourseButton', () => {
mockHooks();
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
renderComponent();
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(CourseCardActionSlot).toBeInTheDocument();
const BeginCourseButton = screen.getByText('BeginCourseButton');
expect(BeginCourseButton).toBeInTheDocument();
});
});
describe('active courses (started, and not archived)', () => {
it('renders CourseCardActionSlot and ResumeButton', () => {
mockHooks({ hasStarted: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
renderComponent();
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(CourseCardActionSlot).toBeInTheDocument();
const ResumeButton = screen.getByText('ResumeButton');
expect(ResumeButton).toBeInTheDocument();
});
});
});

View File

@@ -1,10 +1,10 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import CertificateBanner from './CertificateBanner';
import messages from './messages';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
utilHooks: {
useFormatDate: jest.fn(() => date => date),
},
@@ -17,28 +17,28 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('components/Banner', () => 'Banner');
const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
};
const defaultEnrollment = {
isAudit: false,
isVerified: false,
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const props = { cardId: 'cardId' };
const supportEmail = 'suport@email.com';
const billingEmail = 'billing@email.com';
describe('CertificateBanner', () => {
const props = { cardId: 'cardId' };
reduxHooks.useCardCourseRunData.mockReturnValue({
minPassingGrade: 0.8,
progressUrl: 'progressUrl',
});
const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
};
const defaultEnrollment = {
isAudit: false,
isVerified: false,
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const createWrapper = ({
certificate = {},
enrollment = {},
@@ -51,177 +51,192 @@ describe('CertificateBanner', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
return shallow(<CertificateBanner {...props} />);
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
};
/** TODO: Update tests to validate snapshots **/
describe('snapshot', () => {
test('is restricted', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support and billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
enrollment: {
isAudit: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and has finished', () => {
const wrapper = createWrapper({
courseRun: { isArchived: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and not audit and not finished', () => {
const wrapper = createWrapper({});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarnedButUnavailable: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and not downloadable render empty', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
it('is restricted', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
it('is restricted', () => {
createWrapper({
certificate: {
isRestricted: true,
},
});
it('is restricted and verified', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRefundContactBilling.defaultMessage);
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const msg = screen.getByText((text) => text.includes('please let us know.'));
expect(msg).toBeInTheDocument();
expect(msg).not.toContain(supportEmail);
});
it('is restricted with support email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const msg = screen.getByText((text) => text.includes(supportEmail));
expect(msg).toBeInTheDocument();
});
it('is restricted with billing email but not verified', () => {
createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-danger');
const msg = screen.queryByText((text) => text.includes(billingEmail));
expect(msg).not.toBeInTheDocument();
});
it('is restricted and verified', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const restrictedMsg = screen.getByText((text) => text.includes('please let us know.'));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes('If you would like a refund'));
expect(refundMsg).toBeInTheDocument();
});
it('is restricted and verified with support email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail,
},
});
const restrictedMsg = screen.getByText((text) => text.includes(supportEmail));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes('If you would like a refund'));
expect(refundMsg).toBeInTheDocument();
expect(refundMsg.innerHTML).not.toContain(billingEmail);
});
it('is restricted and verified with billing email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail,
},
});
const restrictedMsg = screen.queryByText((text) => text.includes('please let us know.'));
expect(restrictedMsg).toBeInTheDocument();
expect(restrictedMsg.innerHTML).not.toContain(supportEmail);
const refundMsg = screen.getByText((text) => text.includes(billingEmail));
expect(refundMsg).toBeInTheDocument();
});
it('is restricted and verified with support and billing email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail,
billingEmail,
},
});
const restrictedMsg = screen.getByText((text) => text.includes(supportEmail));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes(billingEmail));
expect(refundMsg).toBeInTheDocument();
});
it('is passing and is downloadable', () => {
createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-success');
const readyMsg = screen.getByText((text) => text.includes('Congratulations.'));
expect(readyMsg).toBeInTheDocument();
});
it('not passing and is downloadable', () => {
createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-success');
const readyMsg = screen.getByText((text) => text.includes('Congratulations.'));
expect(readyMsg).toBeInTheDocument();
});
it('not passing and audit', () => {
createWrapper({
enrollment: {
isAudit: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-info');
const auditMsg = screen.getByText((text) => text.includes('Grade required to pass the course:'));
expect(auditMsg).toBeInTheDocument();
});
it('not passing and has finished', () => {
createWrapper({
courseRun: { isArchived: true },
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-warning');
const archivedMsg = screen.getByText('You are not eligible for a certificate.');
expect(archivedMsg).toBeInTheDocument();
});
it('not passing and not audit and not finished', () => {
createWrapper({});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-warning');
const msg = screen.getByText((text) => text.includes('Grade required for a certificate'));
expect(msg).toBeInTheDocument();
});
it('is passing and is earned but unavailable', () => {
createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarnedButUnavailable: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-info');
const earnedMsg = screen.getByText((text) => text.includes('Your grade and certificate will be ready after'));
expect(earnedMsg).toBeInTheDocument();
});
it('is passing and not downloadable render empty', () => {
createWrapper({
grade: {
isPassing: true,
},
});
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
});

View File

@@ -1,15 +1,13 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Hyperlink } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import { reduxHooks } from '@src/hooks';
import { formatMessage } from '@src/testUtils';
import { CourseBanner } from './CourseBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
},
@@ -19,9 +17,7 @@ jest.mock('hooks', () => ({
},
}));
const cardId = 'my-test-course-number';
let el;
const cardId = 'test-card-id';
const enrollmentData = {
isVerified: false,
@@ -38,7 +34,7 @@ const courseRunData = {
marketingUrl: 'marketing-url',
};
const render = (overrides = {}) => {
const renderCourseBanner = (overrides = {}) => {
const {
courseRun = {},
enrollment = {},
@@ -51,81 +47,58 @@ const render = (overrides = {}) => {
...enrollmentData,
...enrollment,
});
el = shallow(<CourseBanner cardId={cardId} />);
return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
};
describe('CourseBanner', () => {
test('initializes data with course number from enrollment, course and course run data', () => {
render();
it('initializes data with course number from enrollment, course and course run data', () => {
renderCourseBanner();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } });
expect(el.isEmptyRender()).toEqual(true);
it('no display if learner is verified', () => {
renderCourseBanner({ enrollment: { isVerified: true } });
expect(screen.queryByRole('alert')).toBeNull();
});
describe('audit access expired', () => {
beforeEach(() => {
render({ enrollment: { isAuditAccessExpired: true } });
});
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: auditAccessExpired', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
it('should display correct message and link', () => {
renderCourseBanner({ enrollment: { isAuditAccessExpired: true } });
const auditAccessText = screen.getByText(formatMessage(messages.auditAccessExpired));
expect(auditAccessText).toBeInTheDocument();
const auditAccessLink = screen.getByText(formatMessage(messages.findAnotherCourse));
expect(auditAccessLink).toBeInTheDocument();
});
});
describe('unmet prerequisites', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
});
test('snapshot: unmetPrerequisites', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: prerequisitesNotMet', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.prerequisitesNotMet.defaultMessage);
it('should display correct message', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
const preReqText = screen.getByText(formatMessage(messages.prerequisitesNotMet));
expect(preReqText).toBeInTheDocument();
});
});
describe('too early', () => {
describe('no start date', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
it('should not display banner', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages', () => expect(el.instance.children).toEqual([]));
});
describe('has start date', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } } });
});
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages: courseHasNotStarted', () => {
expect(el.instance.children[0].children[0].el).toContain(
it('should display messages courseHasNotStarted', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } } });
const earlyMsg = screen.getByText(
formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }),
);
expect(earlyMsg).toBeInTheDocument();
});
});
});
describe('staff', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isStaff: true } } });
it('should not display banner', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { isStaff: true } } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('snapshot: isStaff', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
test('snapshot: stacking banners', () => {
render({
enrollment: {
coursewareAccess: {
isStaff: true,
isTooEarly: true,
hasUnmetPrerequisites: true,
},
},
});
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,58 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditBanner component render with error state snapshot 1`] = `
<Banner
variant="danger"
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
<format-message-function
message={
{
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
{
"supportEmailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
</p>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;
exports[`CreditBanner component render with error state with no email snapshot 1`] = `
<Banner
variant="danger"
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
An error occurred with this transaction.
</p>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;
exports[`CreditBanner component render with no error state snapshot 1`] = `
<Banner>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;

View File

@@ -1,5 +1,5 @@
import { keyStore } from 'utils';
import { reduxHooks } from 'hooks';
import { keyStore } from '@src/utils';
import { reduxHooks } from '@src/hooks';
import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent';
@@ -9,7 +9,7 @@ import RejectedContent from './views/RejectedContent';
import * as hooks from './hooks';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
usePlatformSettingsData: jest.fn(),

View File

@@ -1,95 +1,65 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import { MailtoLink } from '@openedx/paragon';
import { screen, render } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import hooks from './hooks';
import messages from './messages';
import CreditBanner from '.';
jest.mock('components/Banner', () => 'Banner');
import { CreditBanner } from '.';
jest.mock('./hooks', () => ({
useCreditBannerData: jest.fn(),
}));
let el;
const cardId = 'test-card-id';
describe('CreditBanner', () => {
const mockCardId = 'test-card-id';
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
const mockSupportEmail = 'support@test.com';
const ContentComponent = () => 'ContentComponent';
const supportEmail = 'test-support-email';
const renderCreditBanner = () => render(
<IntlProvider locale="en">
<CreditBanner cardId={mockCardId} />
</IntlProvider>,
);
describe('CreditBanner component', () => {
describe('behavior', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue(null);
el = shallow(<CreditBanner cardId={cardId} />);
});
it('initializes hooks with cardId', () => {
expect(hooks.useCreditBannerData).toHaveBeenCalledWith(cardId);
});
it('returns null if hookData is null', () => {
expect(el.isEmptyRender()).toEqual(true);
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('render', () => {
describe('with error state', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: true,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('passes danger variant to Banner parent', () => {
expect(el.instance.findByType('Banner')[0].props.variant).toEqual('danger');
});
it('includes credit-error-msg with support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(shallow(formatMessage(messages.error, {
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
})));
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
});
});
describe('with error state with no email', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: true,
ContentComponent,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('includes credit-error-msg without support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(formatMessage(messages.errorNoEmail));
});
});
it('should return null if hook returns null', () => {
hooks.useCreditBannerData.mockReturnValue(null);
renderCreditBanner();
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
describe('with no error state', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: false,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
});
it('should render content component without error', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: false,
});
renderCreditBanner();
expect(screen.getByTestId('mock-content')).toBeInTheDocument();
expect(screen.queryByTestId('credit-error-msg')).not.toBeInTheDocument();
});
it('should render error message with support email', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: true,
supportEmail: mockSupportEmail,
});
renderCreditBanner();
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
expect(screen.getByText(mockSupportEmail)).toBeInTheDocument();
});
it('should render error message without support email', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: true,
});
renderCreditBanner();
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
expect(screen.queryByText(mockSupportEmail)).not.toBeInTheDocument();
});
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';

View File

@@ -1,63 +1,80 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import ApprovedContent from './ApprovedContent';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useMasqueradeData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
const cardId = 'test-card-id';
const credit = {
providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name',
};
reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const renderWithMasquerading = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<ApprovedContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>
);
describe('ApprovedContent component', () => {
beforeEach(() => {
el = shallow(<ApprovedContent cardId={cardId} />);
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
renderWithMasquerading();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => {
let component;
beforeAll(() => {
component = el.instance.findByType('CreditContent');
beforeEach(() => {
jest.clearAllMocks();
renderWithMasquerading();
});
test('action.href from credit.providerStatusUrl', () => {
expect(component[0].props.action.href).toEqual(credit.providerStatusUrl);
it('action.message is formatted viewCredit message', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toBeInTheDocument();
expect(actionButton).toHaveTextContent(formatMessage(messages.viewCredit));
});
test('action.message is formatted viewCredit message', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.viewCredit));
it('action.href from credit.providerStatusUrl', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('href', credit.providerStatusUrl);
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
it('action.disabled is false', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).not.toHaveAttribute('aria-disabled', 'true');
expect(actionButton).not.toHaveClass('disabled');
expect(actionButton).toBeEnabled();
});
test('message is formatted approved message', () => {
expect(component[0].props.message).toEqual(formatMessage(
messages.approved,
{
congratulations: (<b>{formatMessage(messages.congratulations)}</b>),
linkToProviderSite: <ProviderLink cardId={cardId} />,
providerName: credit.providerName,
},
));
it('message is formatted approved message', () => {
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.textContent).toContain(`${credit.providerName} has approved your request for course credit`);
});
});
describe('when masquerading', () => {
beforeEach(() => {
renderWithMasquerading(true);
});
it('disables the action button', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('aria-disabled', 'true');
expect(actionButton).toHaveClass('disabled');
});
it('still renders provider name and link correctly', () => {
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg.textContent).toContain(credit.providerName);
});
});
});

View File

@@ -1,29 +1,26 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import track from 'tracking';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import messages from './messages';
import EligibleContent from './EligibleContent';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useCardCourseRunData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('tracking', () => ({
jest.mock('@src/tracking', () => ({
credit: {
purchase: (...args) => ({ trackCredit: args }),
purchase: jest.fn(),
},
}));
let el;
let component;
const cardId = 'test-card-id';
const courseId = 'test-course-id';
const credit = {
@@ -32,50 +29,45 @@ const credit = {
reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
const render = () => {
el = shallow(<EligibleContent cardId={cardId} />);
};
const loadComponent = () => {
component = el.instance.findByType('CreditContent');
};
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
describe('EligibleContent component', () => {
beforeEach(() => {
render();
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
renderEligibleContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
it('initializes course run data with cardId', () => {
renderEligibleContent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('behavior', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
loadComponent();
it('action message is formatted getCredit message', () => {
renderEligibleContent();
const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
expect(button).toBeInTheDocument();
});
test('action.onClick sends credit purchase track event', () => {
expect(component[0].props.action.onClick).toEqual(
track.credit.purchase(courseId),
);
it('onClick sends credit purchase track event', async () => {
const user = userEvent.setup();
renderEligibleContent();
const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
await user.click(button);
expect(track.credit.purchase).toHaveBeenCalledWith(courseId);
});
test('action.message is formatted getCredit message', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.getCredit));
it('message is formatted eligible message if provider', () => {
renderEligibleContent();
const eligibleMessage = screen.getByTestId('credit-msg');
expect(eligibleMessage).toBeInTheDocument();
expect(eligibleMessage).toHaveTextContent(credit.providerName);
});
test('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValueOnce({});
render();
loadComponent();
expect(component[0].props.message).toEqual(formatMessage(
messages.eligible,
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
));
});
test('message is formatted eligible message if provider', () => {
expect(component[0].props.message).toEqual(
formatMessage(messages.eligibleFromProvider, { providerName: credit.providerName }),
);
it('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValue({});
renderEligibleContent();
const eligibleMessage = screen.getByTestId('credit-msg');
expect(eligibleMessage).toBeInTheDocument();
expect(eligibleMessage).toHaveTextContent(messages.getCredit.defaultMessage);
});
});
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';

View File

@@ -1,73 +1,103 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import messages from './messages';
import hooks from './hooks';
import ProviderLink from './components/ProviderLink';
import MustRequestContent from './MustRequestContent';
jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(),
}));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
let component;
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},
}));
const cardId = 'test-card-id';
const requestData = { test: 'requestData' };
const createCreditRequest = jest.fn().mockName('createCreditRequest');
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const render = () => {
el = shallow(<MustRequestContent cardId={cardId} />);
const requestData = {
url: 'test-request-data-url',
parameters: {
key1: 'val1',
key2: 'val2',
key3: 'val3',
},
};
const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url';
const createCreditRequest = jest.fn().mockName('createCreditRequest');
const renderMustRequestContent = (isMasquerading = false) => render(
<IntlProvider locale="en" messages={messages}>
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<MustRequestContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>,
);
describe('MustRequestContent component', () => {
beforeEach(() => {
render();
jest.clearAllMocks();
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit request data with cardId', () => {
renderMustRequestContent();
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => {
describe('behavior', () => {
describe('rendered content', () => {
beforeEach(() => {
component = el.instance.findByType('CreditContent');
renderMustRequestContent();
});
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
expect(component[0].props.action.onClick).toEqual(createCreditRequest);
it('calls createCreditRequest when request credit button is clicked', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: /request credit/i });
await user.click(button);
expect(createCreditRequest).toHaveBeenCalled();
});
test('action.message is formatted requestCredit message', () => {
expect(component[0].props.action.message).toEqual(
formatMessage(messages.requestCredit),
);
it('shows request credit button that is enabled', () => {
const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toBeEnabled();
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
it('displays must request message with provider link', () => {
expect(screen.getByTestId('credit-msg')).toHaveTextContent(/request credit/i);
});
test('message is formatted mustRequest message', () => {
expect(component[0].props.message).toEqual(
formatMessage(messages.mustRequest, {
linkToProviderSite: <ProviderLink cardId={cardId} />,
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>,
}),
);
it('renders credit request form with correct data', () => {
const { container } = renderMustRequestContent();
const form = container.querySelector('form');
expect(form).toBeInTheDocument();
expect(form).toHaveAttribute('action', requestData.url);
});
test('requestData drawn from useCreditRequestData hook', () => {
expect(component[0].props.requestData).toEqual(requestData);
});
describe('when masquerading', () => {
beforeEach(() => {
renderMustRequestContent(true);
});
it('disables the request credit button', () => {
const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveClass('disabled');
});
});
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';

View File

@@ -1,20 +1,15 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import messages from './messages';
import PendingContent from './PendingContent';
jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
jest.mock('@src/hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
let component;
const cardId = 'test-card-id';
const providerName = 'test-credit-provider-name';
@@ -23,40 +18,51 @@ reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const render = () => {
el = shallow(<PendingContent cardId={cardId} />);
};
const renderPendingContent = (isMasquerading = false) => render(
<IntlProvider messages={{}} locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<PendingContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>,
);
describe('PendingContent component', () => {
beforeEach(() => {
render();
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes card credit data with cardId', () => {
renderPendingContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('behavior', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
component = el.instance.findByType('CreditContent');
it('action message is formatted requestCredit message', () => {
renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toBeInTheDocument();
});
test('action.href will go to provider status site', () => {
expect(component[0].props.action.href).toEqual(providerStatusUrl);
it('action href will go to provider status site', () => {
renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveAttribute('href', providerStatusUrl);
});
test('action.message is formatted requestCredit message', () => {
expect(component[0].props.action.message).toEqual(
formatMessage(messages.viewDetails),
);
it('action.disabled is false', () => {
renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).not.toHaveClass('disabled');
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
it('message is formatted pending message with provider name', () => {
renderPendingContent();
const component = screen.getByTestId('credit-msg');
expect(component).toBeInTheDocument();
expect(component).toHaveTextContent(`${providerName} has received`);
});
test('message is formatted pending message', () => {
expect(component[0].props.message).toEqual(
formatMessage(messages.received, { providerName }),
);
describe('when masqueradeData is true', () => {
it('disables the view details button', () => {
renderPendingContent(true);
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveClass('disabled');
});
});
});
});

View File

@@ -1,19 +1,14 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import { reduxHooks } from '@src/hooks';
import RejectedContent from './RejectedContent';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
const cardId = 'test-card-id';
const credit = {
@@ -22,32 +17,27 @@ const credit = {
};
reduxHooks.useCardCreditData.mockReturnValue(credit);
let el;
let component;
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
const loadComponent = () => { component = el.instance.findByType('CreditContent'); };
const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
describe('RejectedContent component', () => {
beforeEach(render);
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
renderRejectedContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => {
beforeAll(loadComponent);
test('no action is passed', () => {
expect(component[0].props.action).toEqual(undefined);
it('no action is passed', () => {
renderRejectedContent();
const action = screen.queryByTestId('action-row-btn');
expect(action).not.toBeInTheDocument();
});
test('message is formatted rejected message', () => {
expect(component[0].props.message).toEqual(formatMessage(
messages.rejected,
{
linkToProviderSite: <ProviderLink cardId={cardId} />,
providerName: credit.providerName,
},
));
it('message is formatted rejected message', () => {
renderRejectedContent();
const message = screen.getByTestId('credit-msg');
expect(message).toBeInTheDocument();
expect(message).toHaveTextContent(`${credit.providerName} did not approve your request for course credit.`);
});
});
});

View File

@@ -1,9 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import CreditContent from './CreditContent';
let el;
const action = {
href: 'test-action-href',
onClick: jest.fn().mockName('test-action-onClick'),
@@ -15,45 +13,57 @@ const message = 'test-message';
const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } };
const props = { action, message, requestData };
const renderCreditContent = (data) => render(
<CreditContent {...data} />,
);
describe('CreditContent component', () => {
describe('render', () => {
describe('with action', () => {
beforeEach(() => {
el = shallow(<CreditContent {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads href, onClick, and message into action row button', () => {
const buttonEl = el.instance.findByTestId('action-row-btn')[0];
expect(buttonEl.props.href).toEqual(action.href);
expect(buttonEl.props.onClick).toEqual(action.onClick);
expect(buttonEl.props.disabled).toEqual(action.disabled);
expect(buttonEl.children[0].el).toEqual(action.message);
it('loads href and message into action row button', () => {
renderCreditContent(props);
const button = screen.getByRole('link', { name: action.message });
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('href', action.href);
expect(button).not.toHaveAttribute('disabled');
});
it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message);
renderCreditContent(props);
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
});
it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].props.requestData).toEqual(requestData);
const { container } = renderCreditContent(props);
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
});
test('disables action button when action.disabled is true', () => {
el = shallow(<CreditContent {...props} action={{ ...action, disabled: true }} />);
expect(el.instance.findByTestId('action-row-btn')[0].props.disabled).toEqual(true);
it('disables action button when action.disabled is true', () => {
renderCreditContent({ ...props, action: { ...action, disabled: true } });
const button = screen.getByRole('link', { name: action.message });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
});
describe('without action', () => {
test('snapshot', () => {
el = shallow(<CreditContent {...{ message, requestData }} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message);
renderCreditContent({ message, requestData });
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
});
it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].props.requestData).toEqual(requestData);
const { container } = renderCreditContent({ message, requestData });
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
});
it('does not render action row button', () => {
renderCreditContent({ message, requestData });
const button = screen.queryByRole('link', { name: action.message });
expect(button).not.toBeInTheDocument();
});
});
});

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditRequestForm component render output valid requestData snapshot 1`] = `
<Form
accept-method="UTF-8"
action="test-request-data-url"
className="hidden"
method="POST"
>
<FormControl
as="textarea"
key="key1"
name="key1"
value="val1"
/>
<FormControl
as="textarea"
key="key2"
name="key2"
value="val2"
/>
<FormControl
as="textarea"
key="key3"
name="key3"
value="val3"
/>
<Button
type="submit"
/>
</Form>
`;

View File

@@ -4,6 +4,12 @@ import useCreditRequestFormData from './hooks';
const requestData = 'test-request-data';
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}));
let out;
const ref = {
current: { click: jest.fn() },

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render } from '@testing-library/react';
import { keyStore } from 'utils';
import { keyStore } from '@src/utils';
import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.';
@@ -11,7 +10,8 @@ jest.mock('./hooks', () => ({
default: jest.fn(),
}));
const ref = 'test-ref';
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
const requestData = {
url: 'test-request-data-url',
parameters: {
@@ -25,40 +25,41 @@ const paramKeys = keyStore(requestData.parameters);
useCreditRequestFormData.mockReturnValue({ ref });
let el;
const shallowRender = (data) => { el = shallow(<CreditRequestForm requestData={data} />); };
const renderForm = (data) => render(<CreditRequestForm requestData={data} />);
describe('CreditRequestForm component', () => {
describe('behavior', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('hooks', () => {
it('initializes ref from hook with requestData', () => {
shallowRender(requestData);
renderForm(requestData);
expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData);
});
});
describe('render output', () => {
describe('null requestData', () => {
it('returns null', () => {
shallowRender(null);
expect(el.isEmptyRender()).toEqual(true);
const { container } = renderForm(null);
expect(container.firstChild).toBeNull();
});
});
describe('valid requestData', () => {
beforeEach(() => {
shallowRender(requestData);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads Form with requestData url', () => {
expect(el.instance.findByType('Form')[0].props.action).toEqual(requestData.url);
const { container } = renderForm(requestData);
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
});
it('loads a textarea form control for each requestData parameter', () => {
const controls = el.instance.findByType('FormControl');
expect(controls[0].props.name).toEqual(paramKeys.key1);
expect(controls[0].props.value).toEqual(requestData.parameters.key1);
expect(controls[1].props.name).toEqual(paramKeys.key2);
expect(controls[1].props.value).toEqual(requestData.parameters.key2);
expect(controls[2].props.name).toEqual(paramKeys.key3);
expect(controls[2].props.value).toEqual(requestData.parameters.key3);
const { container } = renderForm(requestData);
const controls = container.querySelectorAll('textarea');
expect(controls.length).toEqual(Object.keys(requestData.parameters).length);
expect(controls[0]).toHaveAttribute('name', paramKeys.key1);
expect(controls[0]).toHaveValue(requestData.parameters.key1);
expect(controls[1]).toHaveAttribute('name', paramKeys.key2);
expect(controls[1]).toHaveValue(requestData.parameters.key2);
expect(controls[2]).toHaveAttribute('name', paramKeys.key3);
expect(controls[2]).toHaveValue(requestData.parameters.key3);
});
});
});

View File

@@ -4,9 +4,6 @@ import { render } from '@testing-library/react';
import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.mock('./hooks', () => ({
__esModule: true,
default: jest.fn(),

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import { render, screen } from '@testing-library/react';
import { reduxHooks } from '@src/hooks';
import { IntlProvider } from '@openedx/frontend-base';
import ProviderLink from './ProviderLink';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
},
@@ -16,27 +15,30 @@ const credit = {
providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name',
};
let el;
const renderProviderLink = () => render(
<IntlProvider locale="en"><ProviderLink cardId={cardId} /></IntlProvider>,
);
describe('ProviderLink component', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useCardCreditData.mockReturnValue(credit);
el = shallow(<ProviderLink cardId={cardId} />);
renderProviderLink();
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit hook with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('passes credit.providerStatusUrl to the hyperlink href', () => {
expect(el.instance.findByType('Hyperlink')[0].props.href).toEqual(credit.providerStatusUrl);
const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
expect(providerLink).toBeInTheDocument();
});
it('passes providerName for the link message', () => {
expect(el.instance.findByType('Hyperlink')[0].children[0].el).toEqual(credit.providerName);
const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
expect(providerLink).toHaveTextContent(credit.providerName);
});
});
});

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditContent component render with action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<ActionRow
className="mt-4"
>
<Button
as="a"
className="border-gray-400"
data-testid="action-row-btn"
disabled={false}
href="test-action-href"
onClick={[MockFunction test-action-onClick]}
rel="noopener"
target="_blank"
variant="outline-primary"
>
test-action-message
</Button>
</ActionRow>
<CreditRequestForm
requestData={
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
}
}
/>
</Fragment>
`;
exports[`CreditContent component render without action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<CreditRequestForm
requestData={
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
}
}
/>
</Fragment>
`;

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProviderLink component render snapshot 1`] = `
<Hyperlink
href="test-credit-provider-status-url"
rel="noopener"
target="_blank"
>
test-credit-provider-name
</Hyperlink>
`;

View File

@@ -1,8 +1,8 @@
import { MockUseState } from 'testUtils';
import { apiHooks } from 'hooks';
import { MockUseState } from '@src/testUtils';
import { apiHooks } from '@src/hooks';
import * as hooks from './hooks';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
apiHooks: {
useCreateCreditRequest: jest.fn(),
},

View File

@@ -1,11 +1,12 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import EntitlementBanner from './EntitlementBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
},
@@ -18,9 +19,7 @@ jest.mock('hooks', () => ({
},
}));
const cardId = 'my-test-course-number';
let el;
const cardId = 'test-card-id';
const entitlementData = {
isEntitlement: true,
@@ -31,33 +30,43 @@ const entitlementData = {
};
const platformData = { supportEmail: 'test-support-email' };
const render = (overrides = {}) => {
const renderComponent = (overrides = {}) => {
const { entitlement = {} } = overrides;
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
el = shallow(<EntitlementBanner cardId={cardId} />);
return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
};
describe('EntitlementBanner', () => {
test('initializes data with course number from entitlement', () => {
render();
it('initializes data with course number from entitlement', () => {
renderComponent();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
});
test('no display if not an entitlement', () => {
render({ entitlement: { isEntitlement: false } });
expect(el.isEmptyRender()).toEqual(true);
it('no display if not an entitlement', () => {
renderComponent({ entitlement: { isEntitlement: false } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('snapshot: no sessions available', () => {
render({ entitlement: { isFulfilled: false, hasSessions: false } });
expect(el.snapshot).toMatchSnapshot();
it('renders when no sessions available', () => {
renderComponent({ entitlement: { isFulfilled: false, hasSessions: false } });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-warning');
expect(banner.innerHTML).toContain(platformData.supportEmail);
});
test('snapshot: expiration warning', () => {
render({ entitlement: { showExpirationWarning: true } });
expect(el.snapshot).toMatchSnapshot();
it('renders when expiration warning', () => {
renderComponent({ entitlement: { showExpirationWarning: true } });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-info');
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
expect(button).toBeInTheDocument();
});
test('no display if sessions available and not displaying warning', () => {
render();
expect(el.isEmptyRender()).toEqual(true);
it('renders expired banner', () => {
renderComponent({ entitlement: { isExpired: true } });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
});
});

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { ProgramsList } from './ProgramsList';
@@ -9,15 +9,23 @@ describe('ProgramsList', () => {
title: 'Example Program 1',
},
{
programUrl: 'http://example.com',
programUrl: 'http://example2.com',
title: 'Example Program 2',
},
];
it('renders correctly', () => {
const wrapper = shallow(<ProgramsList programs={programs} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(<ProgramsList programs={programs} />);
const list = screen.getByRole('list');
expect(list).toBeInTheDocument();
expect(list.children.length).toEqual(programs.length);
});
expect(wrapper.instance.findByType('li').length).toEqual(programs.length);
it('add the links correctly', () => {
render(<ProgramsList programs={programs} />);
programs.forEach(program => {
const link = screen.getByRole('link', { name: program.title });
expect(link).toHaveAttribute('href', program.url);
});
});
});

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProgramsList renders correctly 1`] = `
<ul
className="related-programs-list-container"
>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 1
</a>
</li>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 2
</a>
</li>
</ul>
`;

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBanner render with programs 1`] = `
<Banner
className="bg-white border-top border-bottom mb-0 related-programs-banner"
icon={[MockFunction icons.Program]}
variant="info"
>
<span
className="font-weight-bolder"
>
Related Programs:
</span>
<ProgramsList
programs={
[
{
"title": "Program 1",
"url": "http://example.com/program1",
},
{
"title": "Program 2",
"url": "http://example.com/program2",
},
]
}
/>
</Banner>
`;

View File

@@ -1,42 +1,49 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from '../../../../../hooks';
import { reduxHooks } from '@src/hooks';
import RelatedProgramsBanner from '.';
jest.mock('./ProgramsList', () => 'ProgramsList');
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardRelatedProgramsData: jest.fn(),
},
}));
const cardId = 'test-card-id';
const programData = {
list: [
{
title: 'Program 1',
url: 'http://example.com/program1',
},
{
title: 'Program 2',
url: 'http://example.com/program2',
},
],
length: 2,
};
describe('RelatedProgramsBanner', () => {
test('render empty', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
length: 0,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el.isEmptyRender()).toEqual(true);
it('render empty', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({});
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('render with programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
list: [
{
title: 'Program 1',
url: 'http://example.com/program1',
},
{
title: 'Program 2',
url: 'http://example.com/program2',
},
],
length: 2,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el.snapshot).toMatchSnapshot();
it('render with programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
const list = screen.getByRole('list');
expect(list.childElementCount).toBe(programData.list.length);
});
it('render related programs title', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
const title = screen.getByText('Related Programs:');
expect(title).toBeInTheDocument();
});
});

View File

@@ -1,205 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CertificateBanner snapshot is passing and is downloadable 1`] = `
<Banner
icon={[MockFunction icons.CheckCircle]}
variant="success"
>
Congratulations. Your certificate is ready.
</Banner>
`;
exports[`CertificateBanner snapshot is passing and is earned but unavailable 1`] = `
<Banner>
Your grade and certificate will be ready after 10/20/3030.
</Banner>
`;
exports[`CertificateBanner snapshot is passing and not downloadable render empty 1`] = `null`;
exports[`CertificateBanner snapshot is restricted 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
<format-message-function
message={
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
"billingEmail": <MailtoLink
to="billing@email"
>
billing@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
<format-message-function
message={
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
"billingEmail": <MailtoLink
to="billing@email"
>
billing@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and audit 1`] = `
<Banner>
Grade required to pass the course: 0.8%
</Banner>
`;
exports[`CertificateBanner snapshot not passing and has finished 1`] = `
<Banner
variant="warning"
>
You are not eligible for a certificate.
<Hyperlink
destination="progressUrl"
isInline={true}
>
View grades.
</Hyperlink>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and is downloadable 1`] = `
<Banner
icon={[MockFunction icons.CheckCircle]}
variant="success"
>
Congratulations. Your certificate is ready.
</Banner>
`;
exports[`CertificateBanner snapshot not passing and not audit and not finished 1`] = `
<Banner
variant="warning"
>
Grade required for a certificate: 0.8%
</Banner>
`;

View File

@@ -1,38 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseBanner audit access expired snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
<Fragment>
<Banner>
Your audit access to this course has expired.
<Hyperlink
destination=""
isInline={true}
>
Find another course
</Hyperlink>
</Banner>
</Fragment>
`;
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;
exports[`CourseBanner too early has start date snapshot 1`] = `
<Fragment>
<Banner>
You can't access this course just yet because the course hasn't started yet. The course will start on 11/11/3030.
</Banner>
</Fragment>
`;
exports[`CourseBanner too early no start date snapshot 1`] = `<Fragment />`;
exports[`CourseBanner unmet prerequisites snapshot: unmetPrerequisites 1`] = `
<Fragment>
<Banner>
You can't access this course just yet because you have not met the pre-requisites.
</Banner>
</Fragment>
`;

View File

@@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EntitlementBanner snapshot: expiration warning 1`] = `
<Banner>
<format-message-function
message={
{
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
"description": "Entitlement course message when the entitlement is expiring soon.",
"id": "learner-dash.courseCard.banners.entitlementExpiringSoon",
}
}
values={
{
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
onClick={[MockFunction updateSelectSessionModalCallback(my-test-course-number)]}
size="inline"
variant="link"
>
select a session
</Button>,
}
}
/>
</Banner>
`;
exports[`EntitlementBanner snapshot: no sessions available 1`] = `
<Banner
variant="warning"
>
<format-message-function
message={
{
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
"description": "Entitlement course message when no sessions are available",
"id": "learner-dash.courseCard.banners.entitlementUnavailable",
}
}
values={
{
"emailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
</Banner>
`;

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardBanners render with isEnrolled false 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBannerSlot
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
</div>
`;
exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBannerSlot
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
<CertificateBanner
cardId="test-card-id"
/>
<CreditBanner
cardId="test-card-id"
/>
</div>
`;

View File

@@ -1,16 +1,25 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { MemoryRouter } from 'react-router-dom';
import { reduxHooks } from '@src/hooks';
import CourseCardBanners from '.';
jest.mock('./CourseBanner', () => 'CourseBanner');
jest.mock('./CertificateBanner', () => 'CertificateBanner');
jest.mock('./CreditBanner', () => 'CreditBanner');
jest.mock('./EntitlementBanner', () => 'EntitlementBanner');
jest.mock('./RelatedProgramsBanner', () => 'RelatedProgramsBanner');
jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>));
jest.mock('./CertificateBanner', () => jest.fn(() => <div>CertificateBanner</div>));
jest.mock('./CreditBanner', () => jest.fn(() => <div>CreditBanner</div>));
jest.mock('./EntitlementBanner', () => jest.fn(() => <div>EntitlementBanner</div>));
jest.mock('./RelatedProgramsBanner', () => jest.fn(() => <div>RelatedProgramsBanner</div>));
jest.mock('hooks', () => ({
const mockedComponents = [
'CourseBanner',
'CertificateBanner',
'CreditBanner',
'EntitlementBanner',
'RelatedProgramsBanner',
];
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
},
@@ -20,13 +29,33 @@ describe('CourseCardBanners', () => {
const props = {
cardId: 'test-card-id',
};
test('renders default CourseCardBanners', () => {
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
it('renders default CourseCardBanners', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: true });
render(
<MemoryRouter>
<IntlProvider locale="en">
<CourseCardBanners {...props} />
</IntlProvider>
</MemoryRouter>
);
mockedComponents.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
});
});
test('render with isEnrolled false', () => {
it('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(
<MemoryRouter>
<IntlProvider locale="en">
<CourseCardBanners {...props} />
</IntlProvider>
</MemoryRouter>
);
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
mockedComponentsIfNotEnrolled.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
});
});
});

View File

@@ -1,56 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard Details component does not have change session button on regular course 1`] = `
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
</span>
</div>
`;
exports[`CourseCard Details component has change session button on entitlement course 1`] = `
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
<button
class="m-0 p-0"
variant="link"
>
change-or-leave-session-message
</button>
</span>
</div>
`;
exports[`CourseCard Details component has change session button on entitlement course but no access message 1`] = `
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
<button
class="m-0 p-0"
variant="link"
>
change-or-leave-session-message
</button>
</span>
</div>
`;

View File

@@ -1,12 +1,11 @@
import { useIntl } from '@openedx/frontend-base';
import { keyStore } from 'utils';
import { utilHooks, reduxHooks } from 'hooks';
import { keyStore } from '@src/utils';
import { utilHooks, reduxHooks } from '@src/hooks';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
utilHooks: {
useFormatDate: jest.fn(),
},
@@ -20,6 +19,16 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('@openedx/frontend-base', () => {
const { formatMessage } = jest.requireActual('@src/testUtils');
return {
...jest.requireActual('@openedx/frontend-base'),
useIntl: () => ({
formatMessage,
}),
};
});
const cardId = 'my-test-card-id';
const courseNumber = 'test-course-number';
const useAccessMessage = 'test-access-message';
@@ -36,9 +45,8 @@ describe('CourseCardDetails hooks', () => {
});
describe('useCardDetailsData', () => {
const providerData = {
name: 'my-provider-name',
};
const providerName = 'my-provider-name';
const providerData = {};
const entitlementData = {
isEntitlement: false,
disableViewCourse: false,
@@ -68,8 +76,10 @@ describe('CourseCardDetails hooks', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ cardId }));
});
it('forwards provider name if it exists, else formatted unknown provider name', () => {
expect(out.providerName).toEqual(providerData.name);
runHook({ provider: { name: '' } });
runHook({ provider: { name: providerName } });
expect(out.providerName).toEqual(providerName);
runHook({ provider: {} });
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
});
it('forward changeOrLeaveSessionMessage', () => {

View File

@@ -1,7 +1,3 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
a.course-card-title {
color: $black;
color: var(--pgn-color-black);
}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import CourseCardDetails from '.';
@@ -48,23 +47,36 @@ describe('CourseCard Details component', () => {
return separatorsCount;
};
test('has change session button on entitlement course', () => {
it('has change session button on entitlement course', () => {
const wrapper = createWrapper();
expect(wrapper.container).toMatchSnapshot();
const sessionButton = screen.getByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeInTheDocument();
const accessMessage = screen.getByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeInTheDocument();
// it has 3 separator, 4 column
expect(fetchSeparators(wrapper)).toBe(3);
});
test('has change session button on entitlement course but no access message', () => {
it('has change session button on entitlement course but no access message', () => {
const wrapper = createWrapper({ accessMessage: null });
expect(wrapper.container).toMatchSnapshot();
const sessionButton = screen.getByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeInTheDocument();
const accessMessage = screen.queryByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeNull();
// it has 2 separator, 3 column
expect(fetchSeparators(wrapper)).toBe(2);
});
test('does not have change session button on regular course', () => {
it('does not have change session button on regular course', () => {
const wrapper = createWrapper({ isEntitlement: false });
expect(wrapper.container).toMatchSnapshot();
const sessionButton = screen.queryByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeNull();
const accessMessage = screen.getByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeInTheDocument();
// it has 2 separator, 3 column
expect(fetchSeparators(wrapper)).toBe(2);
});

View File

@@ -1,61 +1,68 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { reduxHooks } from '@src/hooks';
import useActionDisabledState from './hooks';
import CourseCardImage from './CourseCardImage';
import { CourseCardImage } from './CourseCardImage';
import messages from '../messages';
const homeUrl = 'home-url';
const homeUrl = 'https://example.com';
const bannerImgSrc = 'banner-img-src.jpg';
jest.mock('tracking', () => ({
course: {
courseImageClicked: jest.fn().mockName('segment.courseImageClicked'),
},
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
useCardEnrollmentData: jest.fn(),
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
trackCourseEvent: { eventName, cardId, url },
})),
},
}));
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
jest.mock('./hooks', () => jest.fn());
describe('CourseCardImage', () => {
const props = {
cardId: 'cardId',
orientation: 'orientation',
cardId: 'test-card-id',
orientation: 'horizontal',
};
beforeEach(() => {
jest.clearAllMocks();
it('renders course image with correct attributes', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const image = screen.getByRole('img', { name: formatMessage(messages.bannerAlt) });
expect(image).toBeInTheDocument();
expect(image.src).toContain(bannerImgSrc);
expect(image.parentElement).toHaveClass('horizontal');
});
describe('snapshot', () => {
test('renders clickable link course Image', () => {
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('a');
expect(wrapper.instance.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(
track.course.courseImageClicked,
props.cardId,
homeUrl,
),
);
});
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('div');
});
it('isVerified, should render badge', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const badge = screen.getByText(formatMessage(messages.verifiedBanner));
expect(badge).toBeInTheDocument();
const badgeImg = screen.getByRole('img', { name: formatMessage(messages.verifiedBannerRibbonAlt) });
expect(badgeImg).toBeInTheDocument();
});
describe('behavior', () => {
it('renders link with correct href if disableCourseTitle is false', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: false });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', homeUrl);
});
describe('hooks', () => {
it('initializes', () => {
shallow(<CourseCardImage {...props} />);
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
props.cardId,

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { useContext } from 'react';
import PropTypes from 'prop-types';
import * as ReactShare from 'react-share';
import { StrictDict } from '@edx/react-unit-test-utils';
import { useIntl } from '@openedx/frontend-base';
import { Dropdown } from '@openedx/paragon';
@@ -12,9 +11,9 @@ import { reduxHooks } from '../../../../hooks';
import messages from './messages';
export const testIds = StrictDict({
export const testIds = {
emailSettingsModalToggle: 'emailSettingsModalToggle',
});
};
export const SocialShareMenu = ({ cardId, emailSettings }) => {
const { formatMessage } = useIntl();

View File

@@ -1,35 +1,20 @@
import { IntlProvider } from '@openedx/frontend-base';
import { render, screen } from '@testing-library/react';
import { when } from 'jest-when';
import * as ReactShare from 'react-share';
import { useIntl } from '@openedx/frontend-base';
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import track from '@src/tracking';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import { useEmailSettings } from './hooks';
import SocialShareMenu, { testIds } from './SocialShareMenu';
import SocialShareMenu from './SocialShareMenu';
import messages from './messages';
jest.mock('react-share', () => ({
FacebookShareButton: () => 'FacebookShareButton',
TwitterShareButton: () => 'TwitterShareButton',
}));
jest.mock('tracking', () => ({
jest.mock('@src/tracking', () => ({
socialShare: 'test-social-share-key',
}));
jest.mock('@openedx/frontend-base', () => ({
...jest.requireActual('@openedx/frontend-base'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardSocialSettingsData: jest.fn(),
@@ -47,9 +32,9 @@ const props = {
const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
when(fn).calledWith(props.cardId).mockReturnValue(returnValue);
} else {
when(fn).calledWith().mockReturnValueOnce(returnValue);
when(fn).calledWith().mockReturnValue(returnValue);
}
};
@@ -78,7 +63,6 @@ const mockHooks = (returnVals = {}) => {
{ isCardHook: true },
);
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
mockHook(
reduxHooks.useCardSocialSettingsData,
{
@@ -89,19 +73,19 @@ const mockHooks = (returnVals = {}) => {
);
};
let el;
const render = () => {
el = shallow(<SocialShareMenu {...props} />);
};
const renderComponent = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<SocialShareMenu {...props} />
</MasqueradeUserContext.Provider>
</IntlProvider>,
);
describe('SocialShareMenu', () => {
describe('behavior', () => {
beforeEach(() => {
mockHooks();
render();
});
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalledWith();
renderComponent();
});
it('initializes local hooks', () => {
when(useEmailSettings).expectCalledWith();
@@ -110,7 +94,6 @@ describe('SocialShareMenu', () => {
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
when(reduxHooks.useMasqueradeData).expectCalledWith();
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
});
@@ -118,53 +101,45 @@ describe('SocialShareMenu', () => {
describe('render', () => {
it('renders null if exec ed course', () => {
mockHooks({ isExecEd2UCourse: true });
render();
expect(el.isEmptyRender()).toEqual(true);
renderComponent();
const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
});
const testEmailSettingsDropdown = (isMasquerading = false) => {
describe('email settings dropdown', () => {
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
it('renders', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeInTheDocument();
});
if (isMasquerading) {
it('is disabled', () => {
expect(loadToggle().props.disabled).toEqual(true);
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeInTheDocument();
expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true');
expect(emailSettingsButton).toHaveClass('disabled');
});
} else {
it('is enabled', () => {
expect(loadToggle().props.disabled).toEqual(false);
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeEnabled();
});
}
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
});
};
const testFacebookShareButton = () => {
test('renders facebook share button with courseName and brand', () => {
const button = el.instance.findByType(ReactShare.FacebookShareButton)[0];
expect(button.props.url).toEqual(socialShare.facebook.shareUrl);
expect(button.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'facebook'),
);
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
courseName,
socialBrand: socialShare.facebook.socialBrand,
}));
it('renders facebook share button', () => {
const facebookShareButton = screen.getByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeInTheDocument();
});
};
const testTwitterShareButton = () => {
test('renders twitter share button with courseName and brand', () => {
const button = el.instance.findByType(ReactShare.TwitterShareButton)[0];
expect(button.props.url).toEqual(socialShare.twitter.shareUrl);
expect(button.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'twitter'),
);
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
courseName,
socialBrand: socialShare.twitter.socialBrand,
}));
it('renders twitter share button', () => {
const twitterShareButton = screen.getByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeInTheDocument();
});
};
describe('all enabled', () => {
@@ -174,19 +149,7 @@ describe('SocialShareMenu', () => {
twitter: { isEnabled: true },
isEmailEnabled: true,
});
render();
});
describe('email settings dropdown', () => {
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
it('renders', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
});
it('is enabled', () => {
expect(loadToggle().props.disabled).toEqual(false);
});
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
renderComponent();
});
testEmailSettingsDropdown();
testFacebookShareButton();
@@ -194,42 +157,49 @@ describe('SocialShareMenu', () => {
});
describe('only email enabled', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHooks({ isEmailEnabled: true });
render();
renderComponent();
});
testEmailSettingsDropdown();
it('does not render facebook or twitter controls', () => {
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
});
describe('masquerading', () => {
beforeEach(() => {
mockHooks({ isEmailEnabled: true, isMasquerading: true });
render();
});
testEmailSettingsDropdown(true);
});
describe('masquerading', () => {
beforeEach(() => {
mockHooks({ isEmailEnabled: true });
renderComponent(true);
});
testEmailSettingsDropdown(true);
});
describe('only facebook enabled', () => {
beforeEach(() => {
mockHooks({ facebook: { isEnabled: true } });
render();
renderComponent();
});
testFacebookShareButton();
it('does not render email or twitter controls', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
});
});
describe('only twitter enabled', () => {
beforeEach(() => {
mockHooks({ twitter: { isEnabled: true } });
render();
renderComponent();
});
testTwitterShareButton();
it('does not render email or facebook controls', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
});
});
});

View File

@@ -1,81 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardMenu render show dropdown hide unenroll item and disable email snapshot 1`] = `
<Fragment>
<Dropdown
onToggle={[MockFunction hooks.handleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
</Fragment>
`;
exports[`CourseCardMenu render show dropdown show unenroll and enable email snapshot 1`] = `
<Fragment>
<Dropdown
onToggle={[MockFunction hooks.handleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item
data-testid="unenrollModalToggle"
disabled={false}
onClick={[MockFunction unenrollShow]}
>
Unenroll
</Dropdown.Item>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
<EmailSettingsModal
cardId="test-card-id"
closeModal={[MockFunction emailSettingHide]}
show={false}
/>
</Fragment>
`;

View File

@@ -1,15 +1,16 @@
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
import { useState } from 'react';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
import track from '../../../../tracking';
import { StrictDict } from '../../../../utils';
export const stateKeys = StrictDict({
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible',
isEmailSettingsVisible: 'isEmailSettingsVisible',
export const state = StrictDict({
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
isEmailSettingsVisible: (val) => useState(val), // eslint-disable-line
});
export const useUnenrollData = () => {
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isUnenrollConfirmVisible, false);
const [isVisible, setIsVisible] = state.isUnenrollConfirmVisible(false);
return {
show: () => setIsVisible(true),
hide: () => setIsVisible(false),
@@ -18,7 +19,7 @@ export const useUnenrollData = () => {
};
export const useEmailSettings = () => {
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isEmailSettingsVisible, false);
const [isVisible, setIsVisible] = state.isEmailSettingsVisible(false);
return {
show: () => setIsVisible(true),
hide: () => setIsVisible(false),

View File

@@ -1,11 +1,10 @@
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { reduxHooks } from '@src/hooks';
import track from '@src/tracking';
import { MockUseState } from '@src/testUtils';
import * as hooks from './hooks';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCertificateData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -19,7 +18,7 @@ reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
const cardId = 'test-card-id';
let out;
const state = mockUseKeyedState(hooks.stateKeys);
const state = new MockUseState(hooks);
describe('CourseCardMenu hooks', () => {
beforeEach(() => {
@@ -28,7 +27,6 @@ describe('CourseCardMenu hooks', () => {
});
describe('useUnenrollData', () => {
beforeEach(() => {
state.mockVals({ isUnenrollConfirmVisible: true });
out = hooks.useUnenrollData();
});
describe('behavior', () => {
@@ -37,9 +35,6 @@ describe('CourseCardMenu hooks', () => {
});
});
describe('output', () => {
test('state is loaded from current state value', () => {
expect(out.isVisible).toEqual(true);
});
test('show sets state value to true', () => {
out.show();
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true);
@@ -53,7 +48,6 @@ describe('CourseCardMenu hooks', () => {
describe('useEmailSettings', () => {
beforeEach(() => {
state.mockVals({ isEmailSettingsVisible: true });
out = hooks.useEmailSettings();
});
describe('behavior', () => {
@@ -62,9 +56,6 @@ describe('CourseCardMenu hooks', () => {
});
});
describe('output', () => {
test('state is loaded from current state value', () => {
expect(out.isVisible).toEqual(state.values.isEmailSettingsVisible);
});
test('show sets state value to true', () => {
out.show();
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true);
@@ -77,7 +68,9 @@ describe('CourseCardMenu hooks', () => {
});
describe('useHandleToggleDropdown', () => {
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
beforeEach(() => {
out = hooks.useHandleToggleDropdown(cardId);
});
describe('behavior', () => {
it('initializes course event tracker with event name and card ID', () => {
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(

View File

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base';
import { Dropdown, Icon, IconButton } from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons';
import { StrictDict } from '@edx/react-unit-test-utils';
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
import EmailSettingsModal from '../../../../containers/EmailSettingsModal';
@@ -21,9 +20,9 @@ import {
import messages from './messages';
export const testIds = StrictDict({
export const testIds = {
unenrollModalToggle: 'unenrollModalToggle',
});
};
export const CourseCardMenu = ({ cardId }) => {
const { formatMessage } = useIntl();

View File

@@ -1,26 +1,21 @@
import { when } from 'jest-when';
import { Dropdown } from '@openedx/paragon';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@openedx/frontend-base';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import * as hooks from './hooks';
import CourseCardMenu, { testIds } from '.';
import CourseCardMenu from '.';
import messages from './messages';
jest.mock('@openedx/frontend-base', () => ({
...jest.requireActual('@openedx/frontend-base'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(),
},
}));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() },
}));
jest.mock('./SocialShareMenu', () => 'SocialShareMenu');
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
jest.mock('@src/containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
jest.mock('@src/containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(),
@@ -44,13 +39,11 @@ const unenrollData = {
hide: jest.fn().mockName('unenrollHide'),
};
let el;
const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
when(fn).calledWith(props.cardId).mockReturnValue(returnValue);
} else {
when(fn).calledWith().mockReturnValueOnce(returnValue);
when(fn).calledWith().mockReturnValue(returnValue);
}
};
@@ -74,7 +67,6 @@ const mockHooks = (returnVals = {}) => {
},
{ isCardHook: true },
);
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
mockHook(
reduxHooks.useCardEnrollmentData,
{ isEmailEnabled: !!returnVals.isEmailEnabled },
@@ -82,18 +74,19 @@ const mockHooks = (returnVals = {}) => {
);
};
const render = () => {
el = shallow(<CourseCardMenu {...props} />);
};
const renderComponent = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<CourseCardMenu {...props} />
</MasqueradeUserContext.Provider>
</IntlProvider>
);
describe('CourseCardMenu', () => {
describe('behavior', () => {
describe('hooks', () => {
beforeEach(() => {
mockHooks();
render();
});
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalledWith();
renderComponent();
});
it('initializes local hooks', () => {
when(hooks.useEmailSettings).expectCalledWith();
@@ -102,54 +95,36 @@ describe('CourseCardMenu', () => {
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
});
it('initializes redux hook data ', () => {
when(reduxHooks.useMasqueradeData).expectCalledWith();
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
});
});
describe('render', () => {
it('renders null if showDropdown is false', () => {
mockHooks();
render();
expect(el.isEmptyRender()).toEqual(true);
renderComponent();
const dropdown = screen.queryByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeNull();
});
const testHandleToggle = () => {
it('displays Dropdown with onToggle=handleToggleDropdown', () => {
expect(el.instance.findByType(Dropdown)[0].props.onToggle).toEqual(handleToggleDropdown);
});
};
const testUnenrollConfirmModal = () => {
it('displays UnenrollConfirmModal with cardId and unenrollModal data', () => {
const modal = el.instance.findByType(UnenrollConfirmModal)[0];
expect(modal.props.show).toEqual(unenrollData.isVisible);
expect(modal.props.closeModal).toEqual(unenrollData.hide);
expect(modal.props.cardId).toEqual(props.cardId);
});
};
const testSocialShareMenu = () => {
it('displays SocialShareMenu with cardID and emailSettings', () => {
const menu = el.instance.findByType(SocialShareMenu)[0];
expect(menu.props.cardId).toEqual(props.cardId);
expect(menu.props.emailSettings).toEqual(emailSettings);
});
};
describe('show dropdown', () => {
describe('hide unenroll item and disable email', () => {
beforeEach(() => {
mockHooks({ shouldShowDropdown: true });
render();
it('displays Dropdown and renders SocialShareMenu and UnenrollConfirmModal', async () => {
mockHooks({
shouldShowDropdown: true,
});
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.queryByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeNull();
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.queryByText('EmailSettingsModal');
expect(emailSettingsModal).toBeNull();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
it('does not render unenroll modal toggle', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(0);
});
it('does not render EmailSettingsModal', () => {
expect(el.instance.findByType(EmailSettingsModal).length).toEqual(0);
});
testUnenrollConfirmModal();
});
describe('show unenroll and enable email', () => {
const hookProps = {
@@ -157,57 +132,48 @@ describe('CourseCardMenu', () => {
isEmailEnabled: true,
shouldShowUnenrollItem: true,
};
beforeEach(() => {
mockHooks(hookProps);
render();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
describe('unenroll modal toggle', () => {
let toggle;
describe('not masquerading', () => {
beforeEach(() => {
it('renders all components', async () => {
mockHooks(hookProps);
render();
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
});
it('renders unenroll modal toggle', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
});
test('onClick from unenroll modal hook', () => {
expect(toggle.props.onClick).toEqual(unenrollData.show);
});
test('disabled', () => {
expect(toggle.props.disabled).toEqual(false);
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeInTheDocument();
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.getByText('EmailSettingsModal');
expect(emailSettingsModal).toBeInTheDocument();
});
});
describe('masquerading', () => {
beforeEach(() => {
mockHooks({ ...hookProps, isMasquerading: true });
render();
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
});
it('renders', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
});
test('onClick from unenroll modal hook', () => {
expect(toggle.props.onClick).toEqual(unenrollData.show);
});
test('disabled', () => {
expect(toggle.props.disabled).toEqual(true);
it('renders but unenroll is disabled', async () => {
mockHooks({ ...hookProps });
renderComponent(true);
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeInTheDocument();
expect(unenrollOption).toHaveAttribute('aria-disabled', 'true');
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.getByText('EmailSettingsModal');
expect(emailSettingsModal).toBeInTheDocument();
});
});
});
testUnenrollConfirmModal();
it('displays EmaiSettingsModal with cardId and emailSettingsModal data', () => {
const modal = el.instance.findByType(EmailSettingsModal)[0];
expect(modal.props.show).toEqual(emailSettings.isVisible);
expect(modal.props.closeModal).toEqual(emailSettings.hide);
expect(modal.props.cardId).toEqual(props.cardId);
});
});
});
});

View File

@@ -1,67 +1,73 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { reduxHooks } from '@src/hooks';
import useActionDisabledState from './hooks';
import CourseCardTitle from './CourseCardTitle';
import track from '@src/tracking';
const homeUrl = 'home-url';
jest.mock('tracking', () => ({
jest.mock('@src/tracking', () => ({
course: {
courseTitleClicked: jest.fn().mockName('segment.courseTitleClicked'),
},
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
trackCourseEvent: { eventName, cardId, url },
})),
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
describe('CourseCardTitle', () => {
const props = {
cardId: 'cardId',
cardId: 'test-card-id',
};
const courseName = 'Test Course';
const homeUrl = 'http://test.com';
const handleTitleClick = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
reduxHooks.useTrackCourseEvent.mockReturnValue(handleTitleClick);
});
describe('snapshot', () => {
test('renders clickable link course title', () => {
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('a');
expect(title[0].props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(
track.course.courseTitleClicked,
props.cardId,
homeUrl,
),
);
});
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('span');
expect(title[0].props.onClick).toBeUndefined();
});
it('renders course name as link when not disabled', async () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
render(<CourseCardTitle {...props} />);
const user = userEvent.setup();
const link = screen.getByRole('link', { name: courseName });
expect(link).toHaveAttribute('href', homeUrl);
await user.click(link);
expect(handleTitleClick).toHaveBeenCalled();
});
describe('behavior', () => {
it('initializes', () => {
shallow(<CourseCardTitle {...props} />);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
props.cardId,
);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
it('renders course name as span when disabled', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
render(<CourseCardTitle {...props} />);
const text = screen.getByText(courseName);
expect(text).toBeInTheDocument();
expect(text.tagName.toLowerCase()).toBe('span');
});
it('uses correct hooks with cardId', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
render(<CourseCardTitle {...props} />);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.courseTitleClicked,
props.cardId,
homeUrl,
);
});
});

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `
<Fragment>
<Button
className="pl-0 mr-0 justify-content-start align-self-start flex-shrink-1"
data-testid="RelatedProgramsBadge"
onClick={[MockFunction useRelatedProgramsBadge.openModal]}
size="sm"
variant="tertiary"
>
<Icon
className="mr-2 pr-0"
src={[MockFunction icons.Program]}
/>
useRelatedProgramsBadge.programsMessage
</Button>
<RelatedProgramsModal
cardId="test-course-number"
closeModal={[MockFunction useRelatedProgramsBadge.closeModal]}
isOpen={true}
/>
</Fragment>
`;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { useIntl } from '@openedx/frontend-base';
import { StrictDict } from 'utils';
import { reduxHooks } from 'hooks';
import { StrictDict } from '@src/utils';
import { reduxHooks } from '@src/hooks';
import messages from './messages';
import * as module from './hooks';

View File

@@ -1,21 +1,30 @@
import { useIntl } from '@openedx/frontend-base';
import { MockUseState } from 'testUtils';
import { reduxHooks } from 'hooks';
import { MockUseState } from '@src/testUtils';
import { reduxHooks } from '@src/hooks';
import * as hooks from './hooks';
import messages from './messages';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardRelatedProgramsData: jest.fn(),
},
}));
jest.mock('@openedx/frontend-base', () => {
const { formatMessage } = jest.requireActual('@src/testUtils');
return {
...jest.requireActual('@openedx/frontend-base'),
useIntl: () => ({
formatMessage,
}),
};
});
const cardId = 'test-card-id';
const state = new MockUseState(hooks);
const numPrograms = 27;
let numPrograms = 27;
describe('RelatedProgramsBadge hooks', () => {
const { formatMessage } = useIntl();
@@ -25,6 +34,9 @@ describe('RelatedProgramsBadge hooks', () => {
});
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({
length: numPrograms,
});
});
describe('useRelatedProgramsBadgeData', () => {
beforeEach(() => {
@@ -54,11 +66,13 @@ describe('RelatedProgramsBadge hooks', () => {
expect(out.numPrograms).toEqual(numPrograms);
});
test('returns empty programsMessage if no programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReset();
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
out = hooks.useRelatedProgramsBadgeData({ cardId });
expect(out.programsMessage).toEqual('');
});
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReset();
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
out = hooks.useRelatedProgramsBadgeData({ cardId });
expect(out.programsMessage).toEqual(formatMessage(

View File

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { Button, Icon } from '@openedx/paragon';
import { Program } from '@openedx/paragon/icons';
import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal';
import RelatedProgramsBadgeModal from '@src/containers/RelatedProgramsModal';
import useRelatedProgramsBadgeData from './hooks';
export const RelatedProgramsBadge = ({ cardId }) => {

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import RelatedProgramsBadge from '../RelatedProgramsBadge';
import useRelatedProgramsBadge from './hooks';
import RelatedProgramsBadge from '.';
jest.mock('containers/RelatedProgramsModal', () => 'RelatedProgramsModal');
jest.mock('@src/containers/RelatedProgramsModal', () => 'RelatedProgramsModal');
jest.mock('./hooks', () => jest.fn());
const hookProps = {
@@ -13,18 +12,23 @@ const hookProps = {
closeModal: jest.fn().mockName('useRelatedProgramsBadge.closeModal'),
numPrograms: 3,
programsMessage: 'useRelatedProgramsBadge.programsMessage',
};
}
const cardId = 'test-course-number';
const cardId = 'test-card-id';
describe('RelatedProgramsBadge component', () => {
test('empty render: no programs', () => {
it('should not render if no programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce({ ...hookProps, numPrograms: 0 });
const el = shallow(<RelatedProgramsBadge cardId={cardId} />);
expect(el.isEmptyRender()).toEqual(true);
render(<IntlProvider locale="en"><RelatedProgramsBadge cardId={cardId} /></IntlProvider>);
const button = screen.queryByRole('button', { name: hookProps.programsMessage });
expect(button).toBeNull();
const dialog = screen.queryByRole('dialog');
expect(dialog).toBeNull();
});
test('snapshot: 3 programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce(hookProps);
expect(shallow(<RelatedProgramsBadge cardId={cardId} />).snapshot).toMatchSnapshot();
it('3 programs closed', () => {
useRelatedProgramsBadge.mockReturnValue({ ...hookProps, isOpen: false });
render(<IntlProvider locale="en"><RelatedProgramsBadge cardId={cardId} /></IntlProvider>);
const button = screen.getByRole('button', { name: hookProps.programsMessage });
expect(button).toBeInTheDocument();
});
});

View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
<a
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"url": "home-url",
},
}
}
tabIndex="-1"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap w-100 show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</a>
`;
exports[`CourseCardImage snapshot renders disabled link 1`] = `
<div
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap w-100 show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</div>
`;

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
<h3>
<a
className="course-card-title"
data-testid="CourseCardTitle"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"url": "home-url",
},
}
}
>
course-name
</a>
</h3>
`;
exports[`CourseCardTitle snapshot renders disabled link 1`] = `
<h3>
<span
className="course-card-title"
data-testid="CourseCardTitle"
>
course-name
</span>
</h3>
`;

View File

@@ -1,8 +1,10 @@
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import * as hooks from './hooks';
import { renderHook } from '@testing-library/react';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardEnrollmentData: jest.fn(),
@@ -57,11 +59,20 @@ describe('useActionDisabledState', () => {
});
};
const runHook = () => hooks.useActionDisabledState(cardId);
const runHook = (masqueradeValue = { isMasquerading: false }) => {
const { result } = renderHook(() => hooks.useActionDisabledState(cardId), {
wrapper: ({ children }) => (
<MasqueradeUserContext.Provider value={masqueradeValue}>
{children}
</MasqueradeUserContext.Provider>
),
});
return result.current;
};
describe('disableBeginCourse', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableBeginCourse).toBe(expected);
expect(runHook({ isMasquerading: data.isMasquerading ?? false }).disableBeginCourse).toBe(expected);
};
it('disable when homeUrl is invalid', () => {
testDisabled({ homeUrl: null }, true);
@@ -82,7 +93,7 @@ describe('useActionDisabledState', () => {
describe('disableResumeCourse', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableResumeCourse).toBe(expected);
expect(runHook({ isMasquerading: data.isMasquerading ?? false }).disableResumeCourse).toBe(expected);
};
it('disable when resumeUrl is invalid', () => {
testDisabled({ resumeUrl: null }, true);
@@ -103,7 +114,7 @@ describe('useActionDisabledState', () => {
describe('disableViewCourse', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableViewCourse).toBe(expected);
expect(runHook({ isMasquerading: data.isMasquerading ?? false }).disableViewCourse).toBe(expected);
};
it('disable when hasAccess is false', () => {
testDisabled({ hasAccess: false }, true);
@@ -118,7 +129,7 @@ describe('useActionDisabledState', () => {
describe('disableSelectSession', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableSelectSession).toBe(expected);
expect(runHook({ isMasquerading: data.isMasquerading ?? false }).disableSelectSession).toBe(expected);
};
it('disable when isEntitlement is false', () => {
testDisabled({ isEntitlement: false }, true);
@@ -150,7 +161,7 @@ describe('useActionDisabledState', () => {
describe('disableCourseTitle', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableCourseTitle).toBe(expected);
expect(runHook({ isMasquerading: data.isMasquerading ?? false }).disableCourseTitle).toBe(expected);
};
it('disable when isEntitlement is true and isFulfilled is false', () => {
testDisabled({ isEntitlement: true, isFulfilled: false }, true);

View File

@@ -1,16 +1,26 @@
import { useIntl } from '@openedx/frontend-base';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import * as hooks from './hooks';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
}));
jest.mock('@openedx/frontend-base', () => {
const { formatMessage } = jest.requireActual('@src/testUtils');
return {
...jest.requireActual('@openedx/frontend-base'),
useIntl: () => ({
formatMessage,
}),
};
});
const cardId = 'my-test-course-number';
describe('CourseCard hooks', () => {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import CourseCard from '.';
import hooks from './hooks';
@@ -8,22 +8,43 @@ jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(),
}));
jest.mock('./components/CourseCardBanners', () => 'CourseCardBanners');
jest.mock('./components/CourseCardImage', () => 'CourseCardImage');
jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
jest.mock('./components/CourseCardTitle', () => 'CourseCardTitle');
const namesMockComponents = [
'CourseCardBanners',
'CourseCardImage',
'CourseCardMenu',
'CourseCardActions',
'CourseCardDetails',
'CourseCardTitle',
];
jest.mock('./components/CourseCardBanners', () => jest.fn(() => <div>CourseCardBanners</div>));
jest.mock('./components/CourseCardImage', () => jest.fn(() => <div>CourseCardImage</div>));
jest.mock('./components/CourseCardMenu', () => jest.fn(() => <div>CourseCardMenu</div>));
jest.mock('./components/CourseCardActions', () => jest.fn(() => <div>CourseCardActions</div>));
jest.mock('./components/CourseCardDetails', () => jest.fn(() => <div>CourseCardDetails</div>));
jest.mock('./components/CourseCardTitle', () => jest.fn(() => <div>CourseCardTitle</div>));
const cardId = 'test-card-id';
describe('CourseCard component', () => {
test('snapshot: collapsed', () => {
it('collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(true);
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
const cardImage = screen.getByText('CourseCardImage');
expect(cardImage.parentElement).not.toHaveClass('d-flex');
});
test('snapshot: not collapsed', () => {
it('not collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false);
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
const cardImage = screen.getByText('CourseCardImage');
expect(cardImage.parentElement).toHaveClass('d-flex');
});
it('renders courseCard child components', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false);
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
namesMockComponents.map((courseCardName) => {
const courseCardComponent = screen.getByText(courseCardName);
return expect(courseCardComponent).toBeInTheDocument();
});
});
});

View File

@@ -1,17 +1,28 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { FilterKeys } from 'data/constants/app';
import { FilterKeys } from '@src/data/constants/app';
import ActiveCourseFilters from './ActiveCourseFilters';
import messages from './messages';
const filters = Object.values(FilterKeys);
describe('ActiveCourseFilters', () => {
const props = {
filters: Object.values(FilterKeys),
filters,
handleRemoveFilter: jest.fn().mockName('handleRemoveFilter'),
};
describe('snapshot', () => {
test('renders', () => {
const wrapper = shallow(<ActiveCourseFilters {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
it('renders chips correctly', () => {
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
filters.map((key) => {
const chip = screen.getByText(formatMessage(messages[key]));
return expect(chip).toBeInTheDocument();
});
});
});
it('renders button correctly', () => {
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
expect(button).toBeInTheDocument();
});
});

View File

@@ -1,60 +1,75 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { formatMessage } from '@src/testUtils';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { reduxHooks } from '@src/hooks';
import { IntlProvider } from '@openedx/frontend-base';
import { FilterKeys, SortKeys } from '@src/data/constants/app';
import { reduxHooks } from 'hooks';
import messages from './messages';
import CourseFilterControls from './CourseFilterControls';
import useCourseFilterControlsData from './hooks';
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: { useHasCourses: jest.fn() },
}));
jest.mock('./hooks', () => jest.fn().mockName('useCourseFilterControlsData'));
jest.mock('./hooks', () => jest.fn());
jest.mock('./components/FilterForm', () => 'FilterForm');
jest.mock('./components/SortForm', () => 'SortForm');
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
useWindowSize: jest.fn(),
}));
reduxHooks.useHasCourses.mockReturnValue(true);
const filters = Object.values(FilterKeys);
const mockControlsData = {
isOpen: false,
open: jest.fn().mockName('open'),
close: jest.fn().mockName('close'),
target: 'target-test',
setTarget: jest.fn(),
handleFilterChange: jest.fn().mockName('handleFilterChange'),
handleSortChange: jest.fn().mockName('handleSortChange'),
};
describe('CourseFilterControls', () => {
const props = {
sortBy: 'test-sort-by',
sortBy: SortKeys.enrolled,
setSortBy: jest.fn().mockName('setSortBy'),
filters: ['test-filter'],
filters,
};
useCourseFilterControlsData.mockReturnValue({
isOpen: false,
open: jest.fn().mockName('open'),
close: jest.fn().mockName('close'),
target: 'test-target',
setTarget: jest.fn().mockName('setTarget'),
handleFilterChange: jest.fn().mockName('handleFilterChange'),
handleSortChange: jest.fn().mockName('handleSortChange'),
});
describe('no courses', () => {
test('snapshot', () => {
reduxHooks.useHasCourses.mockReturnValueOnce(false);
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
describe('mobile', () => {
test('snapshot', () => {
describe('mobile and open', () => {
it('should render sheet', () => {
reduxHooks.useHasCourses.mockReturnValue(true);
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
const sheet = screen.getByRole('presentation', { hidden: true });
expect(sheet).toBeInTheDocument();
expect(sheet.parentElement).toHaveClass('sheet-container');
});
});
describe('is not mobile', () => {
test('snapshot', () => {
it('should have button disabled', () => {
reduxHooks.useHasCourses.mockReturnValue(true);
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
const filterForm = screen.getByText(messages.courseStatus.defaultMessage);
const modal = filterForm.closest('div.pgn__modal-popup__tooltip');
expect(modal).toBeInTheDocument();
});
});
describe('no courses', () => {
it('should have button disabled', () => {
reduxHooks.useHasCourses.mockReturnValue(false);
useCourseFilterControlsData.mockReturnValue(mockControlsData);
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
});
});
});

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActiveCourseFilters snapshot renders 1`] = `
<div
id="course-list-active-filters"
>
<Chip
key="inProgress"
>
In-Progress
</Chip>
<Chip
key="notStarted"
>
Not Started
</Chip>
<Chip
key="done"
>
Done
</Chip>
<Chip
key="notEnrolled"
>
Not Enrolled
</Chip>
<Chip
key="upgraded"
>
Upgraded
</Chip>
<Button
onClick={[Function]}
variant="link"
>
Clear all
</Button>
</div>
`;

View File

@@ -1,169 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseFilterControls is not mobile snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={false}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<ModalPopup
isOpen={false}
onClose={[MockFunction close]}
placement="bottom-end"
positionRef="test-target"
>
<div
className="bg-white p-3 rounded shadow d-flex flex-row"
id="course-filter-controls-card"
>
<div
className="filter-form-col"
>
<FilterForm
filters={
[
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<hr
className="h-100 bg-primary-200 mx-3 my-0"
/>
<div
className="filter-form-col text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
</div>
</ModalPopup>
</Form>
</div>
`;
exports[`CourseFilterControls mobile snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={false}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<Sheet
className="w-75"
onClose={[MockFunction close]}
position="left"
show={false}
>
<div
className="p-1 mr-3"
>
<b>
Refine
</b>
</div>
<hr />
<div
className="filter-form-row"
>
<FilterForm
filters={
[
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<div
className="filter-form-row text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
<div
className="pgn__modal-close-container"
>
<ModalCloseButton
onClick={[MockFunction close]}
variant="tertiary"
>
<Icon
src={[MockFunction icons.Close]}
/>
</ModalCloseButton>
</div>
</Sheet>
</Form>
</div>
`;
exports[`CourseFilterControls no courses snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={true}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<ModalPopup
isOpen={false}
onClose={[MockFunction close]}
placement="bottom-end"
positionRef="test-target"
>
<div
className="bg-white p-3 rounded shadow d-flex flex-row"
id="course-filter-controls-card"
>
<div
className="filter-form-col"
>
<FilterForm
filters={
[
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<hr
className="h-100 bg-primary-200 mx-3 my-0"
/>
<div
className="filter-form-col text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
</div>
</ModalPopup>
</Form>
</div>
`;

View File

@@ -1,14 +1,17 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { formatMessage } from '@src/testUtils';
import { IntlProvider } from '@openedx/frontend-base';
import { FilterKeys } from 'data/constants/app';
import { FilterKeys } from '@src/data/constants/app';
import Checkbox from './Checkbox';
import messages from '../messages';
describe('Checkbox', () => {
describe('snapshot', () => {
describe('renders correctly', () => {
Object.keys(FilterKeys).forEach((filterKey) => {
it(`renders ${filterKey}`, () => {
const wrapper = shallow(<Checkbox filterKey={filterKey} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><Checkbox filterKey={filterKey} /></IntlProvider>);
expect(screen.getByText(formatMessage(messages[filterKey]))).toBeInTheDocument();
});
});
});

View File

@@ -1,29 +1,54 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { FilterKeys } from '@src/data/constants/app';
import { FilterForm, filterOrder } from './FilterForm';
import messages from '../messages';
import { FilterKeys } from 'data/constants/app';
import FilterForm, { filterOrder } from './FilterForm';
const mockHandleFilterChange = jest.fn();
jest.mock('./Checkbox', () => 'Checkbox');
const defaultProps = {
filters: [FilterKeys.inProgress],
handleFilterChange: mockHandleFilterChange,
};
const renderComponent = (props = defaultProps) => render(
<IntlProvider messages={{}}>
<FilterForm {...props} />
</IntlProvider>,
);
describe('FilterForm', () => {
const props = {
filters: ['test-filter'],
handleFilterChange: jest.fn().mockName('handleFilterChange'),
};
describe('snapshot', () => {
test('renders', () => {
const wrapper = shallow(<FilterForm {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders all filter checkboxes in correct order', () => {
renderComponent();
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes).toHaveLength(filterOrder.length);
checkboxes.forEach((checkbox, index) => {
expect(checkbox).toHaveAttribute('value', filterOrder[index]);
});
});
test('filterOrder', () => {
expect(filterOrder).toEqual([
FilterKeys.inProgress,
FilterKeys.notStarted,
FilterKeys.done,
FilterKeys.notEnrolled,
FilterKeys.upgraded,
]);
it('checks boxes based on filters prop', () => {
const filters = [FilterKeys.inProgress, FilterKeys.done];
renderComponent({ ...defaultProps, filters });
filters.forEach(filter => {
expect(screen.getByRole('checkbox', { name: formatMessage(messages[filter]) })).toBeChecked();
});
});
it('calls handleFilterChange when checkbox is clicked', () => {
renderComponent();
const checkbox = screen.getByRole('checkbox', { name: formatMessage(messages.notStarted) });
fireEvent.click(checkbox);
expect(mockHandleFilterChange).toHaveBeenCalled();
});
it('displays course status heading', () => {
renderComponent();
expect(screen.getByText(/course status/i)).toBeInTheDocument();
});
});

View File

@@ -1,19 +1,29 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { SortKeys } from 'data/constants/app';
import { SortKeys } from '@src/data/constants/app';
import SortForm from './SortForm';
jest.mock('./Checkbox', () => 'Checkbox');
import messages from '../messages';
describe('SortForm', () => {
const props = {
handleSortChange: jest.fn().mockName('handleSortChange'),
sortBy: SortKeys.enrolled,
};
describe('snapshot', () => {
test('renders', () => {
const wrapper = shallow(<SortForm {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('renders heading', () => {
render(<IntlProvider locale="en"><SortForm {...props} /></IntlProvider>);
const heading = screen.getByText(formatMessage(messages.sort));
expect(heading).toBeInTheDocument();
});
it('renders radio enrolled', () => {
render(<IntlProvider locale="en"><SortForm {...props} /></IntlProvider>);
const enrolled = screen.getByRole('radio', { name: formatMessage(messages.sortLastEnrolled) });
expect(enrolled).toBeInTheDocument();
});
it('renders radio title', () => {
render(<IntlProvider locale="en"><SortForm {...props} /></IntlProvider>);
const title = screen.getByRole('radio', { name: formatMessage(messages.sortTitle) });
expect(title).toBeInTheDocument();
});
});

View File

@@ -1,46 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Checkbox snapshot renders done 1`] = `
<Form.Checkbox
className="py-2"
value="done"
>
Done
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders inProgress 1`] = `
<Form.Checkbox
className="py-2"
value="inProgress"
>
In-Progress
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders notEnrolled 1`] = `
<Form.Checkbox
className="py-2"
value="notEnrolled"
>
Not Enrolled
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders notStarted 1`] = `
<Form.Checkbox
className="py-2"
value="notStarted"
>
Not Started
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders upgraded 1`] = `
<Form.Checkbox
className="py-2"
value="upgraded"
>
Upgraded
</Form.Checkbox>
`;

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterForm snapshot renders 1`] = `
<Form.Group>
<div
className="filter-form-heading mb-1"
>
Course Status
</div>
<Form.CheckboxSet
name="course-status-filters"
onChange={[MockFunction handleFilterChange]}
value={
[
"test-filter",
]
}
>
<Checkbox
filterKey="inProgress"
key="inProgress"
/>
<Checkbox
filterKey="notStarted"
key="notStarted"
/>
<Checkbox
filterKey="done"
key="done"
/>
<Checkbox
filterKey="notEnrolled"
key="notEnrolled"
/>
<Checkbox
filterKey="upgraded"
key="upgraded"
/>
</Form.CheckboxSet>
</Form.Group>
`;

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SortForm snapshot renders 1`] = `
<Fragment>
<div
className="filter-form-heading mb-1"
>
Sort
</div>
<Form.RadioSet
name="sort"
onChange={[MockFunction handleSortChange]}
value="enrolled"
>
<Form.Radio
className="py-2"
value="enrolled"
>
Last enrolled
</Form.Radio>
<Form.Radio
className="py-2"
value="title"
>
Title (A-Z)
</Form.Radio>
</Form.RadioSet>
</Fragment>
`;

View File

@@ -1,26 +1,35 @@
import { useToggle } from '@openedx/paragon';
import { MockUseState } from 'testUtils';
import { reduxHooks } from 'hooks';
import { MockUseState } from '@src/testUtils';
import track from 'tracking';
import track from '@src/tracking';
import { reduxHooks } from '@src/hooks';
import * as hooks from './hooks';
jest.mock('tracking', () => ({
filter: {
filterClicked: jest.fn(),
filterOptionSelected: jest.fn(),
},
}));
jest.mock('hooks', () => ({
jest.mock('@src/hooks', () => ({
reduxHooks: {
useAddFilter: jest.fn(),
useRemoveFilter: jest.fn(),
},
}));
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
useToggle: jest.fn().mockImplementation((val) => [
val,
jest.fn().mockName('useToggle.setTrue'),
jest.fn().mockName('useToggle.setFalse'),
]),
}));
jest.mock('@src/tracking', () => ({
filter: {
filterClicked: jest.fn(),
filterOptionSelected: jest.fn(),
},
}));
const state = new MockUseState(hooks);
describe('CourseFilterControls hooks', () => {
@@ -29,9 +38,12 @@ describe('CourseFilterControls hooks', () => {
const setSortBy = jest.fn();
const removeFilter = jest.fn();
reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
const addFilter = jest.fn();
reduxHooks.useAddFilter.mockReturnValue(addFilter);
beforeEach(() => {
reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
reduxHooks.useAddFilter.mockReturnValue(addFilter);
});
const toggleOpen = jest.fn();
const toggleClose = jest.fn();

View File

@@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseList collapsed with multiple courses and pages snapshot 1`] = `
<Fragment>
<div
id="course-list-active-filters-container"
>
<ActiveCourseFilters />
</div>
<div
className="d-flex flex-column flex-grow-1"
>
<CourseCard
cardId="foo"
key="foo"
/>
<CourseCard
cardId="bar"
key="bar"
/>
<CourseCard
cardId="baz"
key="baz"
/>
<Pagination
className="mx-auto mb-2"
pageCount={3}
paginationLabel="Course List"
variant="reduced"
/>
</div>
</Fragment>
`;
exports[`CourseList no courses or filters snapshot 1`] = `
<Fragment>
<div
className="d-flex flex-column flex-grow-1"
/>
</Fragment>
`;
exports[`CourseList with filters snapshot 1`] = `undefined`;
exports[`CourseList with multiple courses and pages snapshot 1`] = `
<Fragment>
<div
className="d-flex flex-column flex-grow-1"
>
<CourseCard
cardId="foo"
key="foo"
/>
<CourseCard
cardId="bar"
key="bar"
/>
<CourseCard
cardId="baz"
key="baz"
/>
<Pagination
className="mx-auto mb-2"
pageCount={3}
paginationLabel="Course List"
variant="secondary"
/>
</div>
</Fragment>
`;

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { useIsCollapsed } from './hooks';
import CourseList from '.';
@@ -7,9 +7,9 @@ jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(),
}));
jest.mock('containers/CourseCard', () => 'CourseCard');
jest.mock('containers/CourseFilterControls', () => ({
ActiveCourseFilters: 'ActiveCourseFilters',
jest.mock('../../CourseCard', () => jest.fn(() => <div>CourseCard</div>));
jest.mock('../../CourseFilterControls', () => ({
ActiveCourseFilters: jest.fn(() => <div>ActiveCourseFilters</div>),
}));
describe('CourseList', () => {
@@ -22,43 +22,60 @@ describe('CourseList', () => {
};
useIsCollapsed.mockReturnValue(false);
const createWrapper = (courseListData = defaultCourseListData) => (
shallow(<CourseList courseListData={courseListData} />)
const renderList = (courseListData = defaultCourseListData) => (
render(<CourseList courseListData={courseListData} />)
);
describe('no courses or filters', () => {
test('snapshot', () => {
const wrapper = createWrapper();
expect(wrapper.snapshot).toMatchSnapshot();
it('should not render related components', () => {
renderList();
const filterControls = screen.queryByText('ActiveCourseFilters');
const courseCard = screen.queryByText('CourseCard');
const prevButton = screen.queryByRole('button', { name: 'Previous' });
expect(filterControls).toBeNull();
expect(courseCard).toBeNull();
expect(prevButton).toBeNull();
});
});
describe('with filters', () => {
test('snapshot', () => {
const wrapper = createWrapper({
filterOptions: { abitary: 'filter' },
it('should render filter component', () => {
renderList({
...defaultCourseListData,
showFilters: true,
});
expect(wrapper.snapshot).toMatchSnapshot();
const filterControls = screen.getByText('ActiveCourseFilters');
expect(filterControls).toBeInTheDocument();
});
});
describe('with multiple courses and pages', () => {
test('snapshot', () => {
const wrapper = createWrapper({
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }],
numPages: 3,
it('render Course Cards and pagination', () => {
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
const numPages = 3;
renderList({
...defaultCourseListData,
visibleList,
numPages,
});
expect(wrapper.snapshot).toMatchSnapshot();
const courseCards = screen.getAllByText('CourseCard');
expect(courseCards.length).toEqual(visibleList.length);
const pageButtons = screen.getAllByRole('button', { name: /^Page/i });
expect(pageButtons.length).toBe(numPages);
});
});
describe('collapsed with multiple courses and pages', () => {
test('snapshot', () => {
it('should render correct components', () => {
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = createWrapper({
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }],
renderList({
...defaultCourseListData,
visibleList,
numPages: 3,
showFilters: true,
});
expect(wrapper.snapshot).toMatchSnapshot();
const courseCards = screen.getAllByText('CourseCard');
expect(courseCards.length).toEqual(visibleList.length);
const reducedPagination = screen.getByRole('button', { name: '1 of 3' });
expect(reducedPagination).toBeInTheDocument();
});
});
});

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoCoursesView snapshot 1`] = `
<div
className="d-flex align-items-center justify-content-center mb-4.5"
id="no-courses-content-view"
>
<Image
alt="No Courses view banner"
src="icon/mock/path"
/>
<h3
className="h1"
>
Looking for a new challenge?
</h3>
<p>
Explore our courses to add them to your dashboard.
</p>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
variant="brand"
>
Explore courses
</Button>
</div>
`;

View File

@@ -1,15 +1,13 @@
@import "@openedx/paragon/scss/core/core";
#no-courses-content-view {
border: 2px solid $light-400;
border: 2px solid var(--pgn-color-light-400);
flex-direction: column;
padding-bottom: map-get($spacers, 5);
padding-top: map-get($spacers, 5);
padding-bottom: var(--pgn-spacing-spacer-5);
padding-top: var(--pgn-spacing-spacer-5);
height: 100%;
& > * {
margin-top: map-get($spacers, 3);
margin-bottom: map-get($spacers, 3);
margin-top: var(--pgn-spacing-spacer-3);
margin-bottom: var(--pgn-spacing-spacer-3);
}
}

View File

@@ -1,18 +1,38 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base';
import { formatMessage } from '@src/testUtils';
import { baseAppUrl } from '@src/data/services/lms/urls';
import EmptyCourse from '.';
import messages from './messages';
jest.mock('hooks', () => ({
const courseSearchUrl = '/course-search-url';
jest.mock('@src/hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: '/course-search-url',
courseSearchUrl,
})),
},
}));
describe('NoCoursesView', () => {
test('snapshot', () => {
expect(shallow(<EmptyCourse />).snapshot).toMatchSnapshot();
it('should display image, heading and button', () => {
render(<IntlProvider locale="en"><EmptyCourse /></IntlProvider>);
const image = screen.getByRole('img', { alt: formatMessage(messages.bannerAlt) });
expect(image).toBeInTheDocument();
});
it('should display heading and prompt', () => {
render(<IntlProvider locale="en"><EmptyCourse /></IntlProvider>);
const heading = screen.getByText(formatMessage(messages.lookingForChallengePrompt));
const prompt = screen.getByText(formatMessage(messages.exploreCoursesPrompt));
expect(heading).toBeInTheDocument();
expect(prompt).toBeInTheDocument();
});
it('should display button', () => {
render(<IntlProvider locale="en"><EmptyCourse /></IntlProvider>);
const button = screen.getByRole('link', { name: formatMessage(messages.exploreCoursesButton) });
expect(button).toBeInTheDocument();
expect(button.href).toBe(baseAppUrl(courseSearchUrl));
});
});

View File

@@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoursesPanel no courses snapshot 1`] = `
<div
className="course-list-container"
>
<div
className="course-list-heading-container"
>
<h2
className="course-list-title"
>
My Courses
</h2>
<div
className="course-filter-controls-container"
>
<CourseFilterControls />
</div>
</div>
<NoCoursesViewSlot />
</div>
`;
exports[`CoursesPanel with courses snapshot 1`] = `
<div
className="course-list-container"
>
<div
className="course-list-heading-container"
>
<h2
className="course-list-title"
>
My Courses
</h2>
<div
className="course-filter-controls-container"
>
<CourseFilterControls />
</div>
</div>
<CourseListSlot
courseListData={
{
"filterOptions": {},
"numPages": 1,
"setPageNumber": [MockFunction setPageNumber],
"showFilters": false,
"visibleList": [],
}
}
/>
</div>
`;

Some files were not shown because too many files have changed in this diff Show More