richie-education 3.4.1-dev13 → 3.4.1-dev17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/api/joanie.ts +10 -4
- package/js/api/lms/index.spec.ts +33 -0
- package/js/api/lms/index.ts +1 -1
- package/js/api/lms/openedx-hawthorn.spec.ts +49 -0
- package/js/api/lms/openedx-hawthorn.ts +5 -2
- package/js/components/DownloadBatchOrderSeatsButton/index.tsx +36 -19
- package/js/components/Icon/index.tsx +1 -0
- package/js/components/SaleTunnel/_styles.scss +4 -1
- package/js/hooks/useDownloadBatchOrderSeats/index.spec.tsx +28 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.tsx +15 -5
- package/js/types/api.ts +1 -0
- package/js/types/commonDataProps.ts +2 -0
- package/js/utils/errors/HttpError.ts +1 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.tsx +22 -18
- package/js/widgets/Slider/components/SlidePanel.tsx +13 -1
- package/js/widgets/Slider/index.spec.tsx +3 -5
- package/package.json +1 -1
- package/scss/components/templates/richie/slider/_slider.scss +10 -0
- package/scss/objects/_dashboard.scss +8 -1
package/js/api/joanie.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { JOANIE_API_VERSION } from 'settings';
|
|
|
16
16
|
import { ResourcesQuery } from 'hooks/useResources';
|
|
17
17
|
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
18
18
|
import { Maybe, Nullable } from 'types/utils';
|
|
19
|
+
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
19
20
|
import { checkStatus, getFileFromResponse } from './utils';
|
|
20
21
|
|
|
21
22
|
/*
|
|
@@ -366,10 +367,15 @@ const API = (): Joanie.API => {
|
|
|
366
367
|
);
|
|
367
368
|
},
|
|
368
369
|
},
|
|
369
|
-
seats_export: async (id: string): Promise<File> =>
|
|
370
|
-
fetchWithJWT(
|
|
371
|
-
.
|
|
372
|
-
|
|
370
|
+
seats_export: async (id: string): Promise<File> => {
|
|
371
|
+
const response = await fetchWithJWT(
|
|
372
|
+
ROUTES.user.batchOrders.seats_export.replace(':id', id),
|
|
373
|
+
);
|
|
374
|
+
if (response.status === HttpStatusCode.UNPROCESSABLE_ENTITY) {
|
|
375
|
+
throw new HttpError(response.status, response.statusText);
|
|
376
|
+
}
|
|
377
|
+
return checkStatus(response).then(getFileFromResponse);
|
|
378
|
+
},
|
|
373
379
|
},
|
|
374
380
|
enrollments: {
|
|
375
381
|
create: async (payload) =>
|
package/js/api/lms/index.spec.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
2
2
|
import { handle } from 'utils/errors/handle';
|
|
3
|
+
import { location } from 'utils/indirection/window';
|
|
3
4
|
import LMSHandler from '.';
|
|
4
5
|
|
|
6
|
+
jest.mock('utils/indirection/window', () => ({
|
|
7
|
+
location: {
|
|
8
|
+
pathname: '/courses/a-test-course/',
|
|
9
|
+
assign: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
5
12
|
jest.mock('utils/context', () => ({
|
|
6
13
|
__esModule: true,
|
|
7
14
|
default: mockRichieContextFactory({
|
|
@@ -16,6 +23,12 @@ jest.mock('utils/context', () => ({
|
|
|
16
23
|
endpoint: 'https://edx.endpoint/api',
|
|
17
24
|
course_regexp: '.*edx.org/.*',
|
|
18
25
|
},
|
|
26
|
+
{
|
|
27
|
+
backend: 'openedx-hawthorn',
|
|
28
|
+
endpoint: 'https://nau.endpoint/api',
|
|
29
|
+
course_regexp: '.*nau.org/.*',
|
|
30
|
+
next_url: 'richie-nau',
|
|
31
|
+
},
|
|
19
32
|
],
|
|
20
33
|
}).one(),
|
|
21
34
|
}));
|
|
@@ -24,6 +37,10 @@ const mockHandle: jest.Mock<typeof handle> = handle as any;
|
|
|
24
37
|
jest.mock('utils/errors/handle');
|
|
25
38
|
|
|
26
39
|
describe('API LMS', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
27
44
|
it('returns OpenEdX API if url that match edx selector is provided', () => {
|
|
28
45
|
const api = LMSHandler('https://edx.org/courses/a-test-course');
|
|
29
46
|
expect(api).toBeDefined();
|
|
@@ -42,4 +59,20 @@ describe('API LMS', () => {
|
|
|
42
59
|
new Error('No LMS Backend found for https://unknown.org/course/a-test-course.'),
|
|
43
60
|
);
|
|
44
61
|
});
|
|
62
|
+
|
|
63
|
+
it('uses default "richie" next prefix for openedx-hawthorn without next_url configured', () => {
|
|
64
|
+
const api = LMSHandler('https://edx.org/courses/a-test-course');
|
|
65
|
+
api.user.login();
|
|
66
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
67
|
+
`https://edx.endpoint/api/login?next=richie${location.pathname}`,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses configured next_url prefix for openedx-hawthorn with next_url set', () => {
|
|
72
|
+
const api = LMSHandler('https://nau.org/courses/a-test-course');
|
|
73
|
+
api.user.login();
|
|
74
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
75
|
+
`https://nau.endpoint/api/login?next=richie-nau${location.pathname}`,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
45
78
|
});
|
package/js/api/lms/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ const LmsAPIHandler = (url: string): APILms => {
|
|
|
15
15
|
case APIBackend.OPENEDX_DOGWOOD:
|
|
16
16
|
return OpenEdxDogwoodApiInterface(api);
|
|
17
17
|
case APIBackend.OPENEDX_HAWTHORN:
|
|
18
|
-
return OpenEdxHawthornApiInterface(api);
|
|
18
|
+
return OpenEdxHawthornApiInterface(api, { routes: {}, nextURL: api.next_url });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const error = new Error(`No LMS Backend found for ${url}.`);
|
|
@@ -3,10 +3,17 @@ import { faker } from '@faker-js/faker';
|
|
|
3
3
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
4
4
|
import { handle } from 'utils/errors/handle';
|
|
5
5
|
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
6
|
+
import { location } from 'utils/indirection/window';
|
|
6
7
|
import context from 'utils/context';
|
|
7
8
|
import API from './openedx-hawthorn';
|
|
8
9
|
|
|
9
10
|
jest.mock('utils/errors/handle');
|
|
11
|
+
jest.mock('utils/indirection/window', () => ({
|
|
12
|
+
location: {
|
|
13
|
+
pathname: '/courses/a-test-course/',
|
|
14
|
+
assign: jest.fn(),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
10
17
|
jest.mock('utils/context', () => ({
|
|
11
18
|
__esModule: true,
|
|
12
19
|
default: mockRichieContextFactory({
|
|
@@ -45,6 +52,48 @@ describe('OpenEdX Hawthorn API', () => {
|
|
|
45
52
|
});
|
|
46
53
|
});
|
|
47
54
|
|
|
55
|
+
describe('user', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('login', () => {
|
|
61
|
+
it('redirects to login with default "richie" next prefix when nextURL is not set', () => {
|
|
62
|
+
const api = API(LMSConf);
|
|
63
|
+
api.user.login();
|
|
64
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
65
|
+
`${EDX_ENDPOINT}/login?next=richie${location.pathname}`,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('redirects to login with custom next prefix when nextURL option is provided', () => {
|
|
70
|
+
const api = API(LMSConf, { routes: {}, nextURL: 'richie-nau' });
|
|
71
|
+
api.user.login();
|
|
72
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
73
|
+
`${EDX_ENDPOINT}/login?next=richie-nau${location.pathname}`,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('register', () => {
|
|
79
|
+
it('redirects to register with default "richie" next prefix when nextURL is not set', () => {
|
|
80
|
+
const api = API(LMSConf);
|
|
81
|
+
api.user.register();
|
|
82
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
83
|
+
`${EDX_ENDPOINT}/register?next=richie${location.pathname}`,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('redirects to register with custom next prefix when nextURL option is provided', () => {
|
|
88
|
+
const api = API(LMSConf, { routes: {}, nextURL: 'richie-ap' });
|
|
89
|
+
api.user.register();
|
|
90
|
+
expect(location.assign).toHaveBeenCalledWith(
|
|
91
|
+
`${EDX_ENDPOINT}/register?next=richie-ap${location.pathname}`,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
48
97
|
describe('enrollment', () => {
|
|
49
98
|
beforeEach(() => {
|
|
50
99
|
courseId = faker.string.uuid();
|
|
@@ -20,6 +20,8 @@ import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
const API = (APIConf: AuthenticationBackend | LMSBackend, options?: APIOptions): APILms => {
|
|
23
|
+
const nextURL = options?.nextURL ?? 'richie';
|
|
24
|
+
|
|
23
25
|
const extractCourseIdFromUrl = (url: string): Maybe<Nullable<string>> => {
|
|
24
26
|
const matches = url.match((APIConf as LMSBackend).course_regexp);
|
|
25
27
|
return matches && matches[1] ? matches[1] : null;
|
|
@@ -61,8 +63,9 @@ const API = (APIConf: AuthenticationBackend | LMSBackend, options?: APIOptions):
|
|
|
61
63
|
/ ! \ Prefix next param with richie.
|
|
62
64
|
In this way, OpenEdX Nginx conf knows that we want to go back to richie app after login/redirect
|
|
63
65
|
*/
|
|
64
|
-
login: () => location.assign(`${ROUTES.user.login}?next
|
|
65
|
-
register: () =>
|
|
66
|
+
login: () => location.assign(`${ROUTES.user.login}?next=${nextURL}${location.pathname}`),
|
|
67
|
+
register: () =>
|
|
68
|
+
location.assign(`${ROUTES.user.register}?next=${nextURL}${location.pathname}`),
|
|
66
69
|
logout: async () => {
|
|
67
70
|
await fetch(ROUTES.user.logout, {
|
|
68
71
|
mode: 'no-cors',
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useId } from 'react';
|
|
2
|
-
import { Button } from '@openfun/cunningham-react';
|
|
2
|
+
import { Alert, Button, VariantType } from '@openfun/cunningham-react';
|
|
3
3
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import { Spinner } from 'components/Spinner';
|
|
5
|
+
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
6
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
5
7
|
import { useDownloadBatchOrderSeats } from 'hooks/useDownloadBatchOrderSeats';
|
|
6
8
|
|
|
7
9
|
const messages = defineMessages({
|
|
@@ -20,6 +22,12 @@ const messages = defineMessages({
|
|
|
20
22
|
description: 'Text displayed for seats value in batch order',
|
|
21
23
|
id: 'batchOrder.seats',
|
|
22
24
|
},
|
|
25
|
+
noSeatsOwned: {
|
|
26
|
+
defaultMessage:
|
|
27
|
+
'No participants have claimed their seat yet. The export will be available once at least one participant has enrolled.',
|
|
28
|
+
description: 'Error message displayed when trying to export seats but no seats are owned yet.',
|
|
29
|
+
id: 'components.DownloadBatchOrderSeatsButton.noSeatsOwned',
|
|
30
|
+
},
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
export const sanitizeForFilename = (str: string) =>
|
|
@@ -46,7 +54,7 @@ const DownloadBatchOrderSeatsButton = ({
|
|
|
46
54
|
batchOrderId,
|
|
47
55
|
productTitle,
|
|
48
56
|
}: DownloadBatchOrderSeatsButtonProps) => {
|
|
49
|
-
const { download, loading } = useDownloadBatchOrderSeats();
|
|
57
|
+
const { download, loading, error } = useDownloadBatchOrderSeats();
|
|
50
58
|
const labelId = useId();
|
|
51
59
|
const intl = useIntl();
|
|
52
60
|
|
|
@@ -56,24 +64,33 @@ const DownloadBatchOrderSeatsButton = ({
|
|
|
56
64
|
};
|
|
57
65
|
|
|
58
66
|
return (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
<>
|
|
68
|
+
<Button
|
|
69
|
+
size="small"
|
|
70
|
+
color="brand"
|
|
71
|
+
variant="primary"
|
|
72
|
+
className="dashboard-item__action-button"
|
|
73
|
+
icon={<Icon name={IconTypeEnum.DOWNLOAD} size="small" />}
|
|
74
|
+
iconPosition="left"
|
|
75
|
+
disabled={loading}
|
|
76
|
+
onClick={handleClick}
|
|
77
|
+
>
|
|
78
|
+
{loading ? (
|
|
79
|
+
<Spinner theme="primary" aria-labelledby={labelId}>
|
|
80
|
+
<span id={labelId}>
|
|
81
|
+
<FormattedMessage {...messages.generating} />
|
|
82
|
+
</span>
|
|
83
|
+
</Spinner>
|
|
84
|
+
) : (
|
|
85
|
+
<FormattedMessage {...messages.download} />
|
|
86
|
+
)}
|
|
87
|
+
</Button>
|
|
88
|
+
{error?.code === HttpStatusCode.UNPROCESSABLE_ENTITY && (
|
|
89
|
+
<Alert type={VariantType.ERROR} className="mt-s">
|
|
90
|
+
<FormattedMessage {...messages.noSeatsOwned} />
|
|
91
|
+
</Alert>
|
|
75
92
|
)}
|
|
76
|
-
|
|
93
|
+
</>
|
|
77
94
|
);
|
|
78
95
|
};
|
|
79
96
|
|
|
@@ -129,4 +129,32 @@ describe('useDownloadBatchOrderSeats', () => {
|
|
|
129
129
|
expect(result.current.loading).toBe(false);
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
it('exposes a 422 HttpError when seats export fails because no seats are owned', async () => {
|
|
134
|
+
const batchOrder = BatchOrderReadFactory().one();
|
|
135
|
+
const DOWNLOAD_URL = `https://joanie.test/api/v1.0/batch-orders/${batchOrder.id}/seats-export/`;
|
|
136
|
+
fetchMock.get(DOWNLOAD_URL, {
|
|
137
|
+
status: HttpStatusCode.UNPROCESSABLE_ENTITY,
|
|
138
|
+
body: { detail: 'Batch order has no seats owned, cannot export seats.' },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(() => useDownloadBatchOrderSeats(), {
|
|
142
|
+
wrapper: Wrapper,
|
|
143
|
+
});
|
|
144
|
+
await waitFor(() => expect(result.current).not.toBeNull());
|
|
145
|
+
|
|
146
|
+
expect(result.current.error).toBeUndefined();
|
|
147
|
+
|
|
148
|
+
act(() => {
|
|
149
|
+
result.current.download(batchOrder.id);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
|
|
154
|
+
expect(result.current.error?.code).toBe(HttpStatusCode.UNPROCESSABLE_ENTITY);
|
|
155
|
+
// eslint-disable-next-line compat/compat
|
|
156
|
+
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
157
|
+
expect(result.current.loading).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
132
160
|
});
|
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
3
|
import { browserDownloadFromBlob } from 'utils/download';
|
|
4
|
+
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
4
5
|
|
|
5
6
|
export const useDownloadBatchOrderSeats = () => {
|
|
6
7
|
const [loading, setLoading] = useState(false);
|
|
8
|
+
const [error, setError] = useState<HttpError | undefined>();
|
|
7
9
|
const API = useJoanieApi();
|
|
8
10
|
|
|
9
11
|
return {
|
|
10
12
|
download: async (batchOrderId: string, filename?: string) => {
|
|
11
13
|
setLoading(true);
|
|
14
|
+
setError(undefined);
|
|
12
15
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const downloadFn = async () => {
|
|
17
|
+
try {
|
|
18
|
+
return await API.user.batchOrders.seats_export(batchOrderId);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (err instanceof HttpError && err.code === HttpStatusCode.UNPROCESSABLE_ENTITY) {
|
|
21
|
+
setError(err);
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await browserDownloadFromBlob(downloadFn, false, filename);
|
|
18
27
|
} finally {
|
|
19
28
|
setLoading(false);
|
|
20
29
|
}
|
|
21
30
|
},
|
|
22
31
|
loading,
|
|
32
|
+
error,
|
|
23
33
|
};
|
|
24
34
|
};
|
package/js/types/api.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface LMSBackend {
|
|
|
9
9
|
backend: string;
|
|
10
10
|
course_regexp: RegExp | string;
|
|
11
11
|
endpoint: string;
|
|
12
|
+
next_url?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface AuthenticationBackend {
|
|
@@ -26,6 +27,7 @@ export interface AuthenticationBackend {
|
|
|
26
27
|
username: string;
|
|
27
28
|
email: string;
|
|
28
29
|
};
|
|
30
|
+
next_url?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
enum FEATURES {
|
|
@@ -105,27 +105,31 @@ export const BatchOrderSeatInfo = ({ batchOrder }: BatchOrderSeatInfoProps) => {
|
|
|
105
105
|
<li key={seat.id}>{seat.owner_name ?? seat.voucher}</li>
|
|
106
106
|
))}
|
|
107
107
|
</ul>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
108
|
+
<div className="enrollment-actions">
|
|
109
|
+
{remainingCount > 0 && (
|
|
110
|
+
<Button
|
|
111
|
+
className="enrollment-load-more"
|
|
112
|
+
color="brand"
|
|
113
|
+
variant="secondary"
|
|
114
|
+
size="small"
|
|
115
|
+
onClick={() => setPage((p) => p + 1)}
|
|
116
|
+
disabled={states.fetching}
|
|
117
|
+
>
|
|
118
|
+
{intl.formatMessage(batchOrderSeatInfoMessages.loadMore, {
|
|
119
|
+
count: remainingCount,
|
|
120
|
+
})}
|
|
121
|
+
</Button>
|
|
122
|
+
)}
|
|
123
|
+
<div>
|
|
124
|
+
<DownloadBatchOrderSeatsButton
|
|
125
|
+
batchOrderId={batchOrder.id}
|
|
126
|
+
productTitle={batchOrder.offering?.product.title ?? ''}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
122
130
|
</>
|
|
123
131
|
)}
|
|
124
132
|
</div>
|
|
125
|
-
<DownloadBatchOrderSeatsButton
|
|
126
|
-
batchOrderId={batchOrder.id}
|
|
127
|
-
productTitle={batchOrder.offering?.product.title ?? ''}
|
|
128
|
-
/>
|
|
129
133
|
</div>
|
|
130
134
|
}
|
|
131
135
|
/>
|
|
@@ -53,7 +53,19 @@ const SlidePanel = ({
|
|
|
53
53
|
})}
|
|
54
54
|
>
|
|
55
55
|
<strong className="slide__title">
|
|
56
|
-
<span>
|
|
56
|
+
<span>
|
|
57
|
+
{slides[activeSlideIndex].link_url ? (
|
|
58
|
+
<a
|
|
59
|
+
href={slides[activeSlideIndex].link_url}
|
|
60
|
+
target={slides[activeSlideIndex].link_open_blank ? '_blank' : '_self'}
|
|
61
|
+
rel="noopener noreferrer"
|
|
62
|
+
>
|
|
63
|
+
{slides[activeSlideIndex].title}
|
|
64
|
+
</a>
|
|
65
|
+
) : (
|
|
66
|
+
slides[activeSlideIndex].title
|
|
67
|
+
)}
|
|
68
|
+
</span>
|
|
57
69
|
</strong>
|
|
58
70
|
{hasSlideContent && (
|
|
59
71
|
<div
|
|
@@ -71,13 +71,11 @@ describe('<Slider />', () => {
|
|
|
71
71
|
// Check if all slides are rendered
|
|
72
72
|
mockSlides.forEach((slide) => {
|
|
73
73
|
expect(screen.getByRole('img', { name: slide.title })).toBeInTheDocument();
|
|
74
|
-
// Check if the link is rendered
|
|
75
|
-
const link = screen.queryByRole('link', { name: slide.title });
|
|
76
74
|
if (slide.link_url) {
|
|
77
|
-
|
|
78
|
-
expect(link).
|
|
75
|
+
const links = screen.getAllByRole('link', { name: slide.title });
|
|
76
|
+
links.forEach((link) => expect(link).toHaveAttribute('href', slide.link_url));
|
|
79
77
|
} else {
|
|
80
|
-
expect(link).not.toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByRole('link', { name: slide.title })).not.toBeInTheDocument();
|
|
81
79
|
}
|
|
82
80
|
});
|
|
83
81
|
|
package/package.json
CHANGED
|
@@ -201,6 +201,16 @@ $r-slider-content-line-clamp: 4 !default;
|
|
|
201
201
|
font-weight: $r-slider-title-fontweight;
|
|
202
202
|
font-family: $r-slider-title-fontfamily;
|
|
203
203
|
|
|
204
|
+
a {
|
|
205
|
+
color: inherit;
|
|
206
|
+
text-decoration: none;
|
|
207
|
+
|
|
208
|
+
&:hover,
|
|
209
|
+
&:focus {
|
|
210
|
+
opacity: 0.8;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
204
214
|
& > span {
|
|
205
215
|
display: inline-block;
|
|
206
216
|
transform: translateY(0%);
|
|
@@ -152,9 +152,16 @@
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
.enrollment-actions {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: flex-start;
|
|
158
|
+
justify-content: space-between;
|
|
159
|
+
gap: rem-calc(8px);
|
|
160
|
+
margin-top: rem-calc(8px);
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
.enrollment-load-more {
|
|
156
164
|
width: fit-content;
|
|
157
|
-
margin-top: rem-calc(8px);
|
|
158
165
|
padding: rem-calc(4px) rem-calc(12px);
|
|
159
166
|
}
|
|
160
167
|
|