Compare commits

..

41 Commits

Author SHA1 Message Date
David Joy
e91cdacfc9 feat: adding pact testing for APIs
This is WIP and shouldn’t make it to master.
2023-06-12 17:09:07 -04:00
renovate[bot]
6740ad3672 fix(deps): update dependency algoliasearch to v4.17.2 2023-06-12 16:25:54 +00:00
renovate[bot]
ca14d3d279 chore(deps): update dependency @edx/frontend-build to v12.8.51 2023-06-12 12:00:55 +00:00
Jenkins
9dbe58b10f chore(i18n): update translations 2023-06-11 16:40:53 -04:00
Jason Wesson
50d80ef614 fix: adjust header height and content when window is medium in width or less (#756)
* fix: adjust header height and content when window is medium in width or less

* feat: convert stepper header's contents to compact view for medium windows (or less)

---------

Co-authored-by: Jason Wesson <jwesson@2u.com>
2023-06-06 11:10:00 -07:00
renovate[bot]
fe6b76da7e fix(deps): update dependency @edx/frontend-platform to v4.5.1 2023-06-05 12:46:05 +00:00
renovate[bot]
7fff13137d chore(deps): update dependency @edx/frontend-build to v12.8.40 2023-06-05 10:26:23 +00:00
renovate[bot]
91282cff74 chore(deps): update commitlint monorepo to v17.6.5 2023-06-05 07:50:24 +00:00
renovate[bot]
45be830f18 fix(deps): update dependency core-js to v3.30.2 2023-05-29 12:15:11 +00:00
renovate[bot]
122affbb6d fix(deps): update dependency @edx/frontend-platform to v4.5.0 2023-05-29 11:07:30 +00:00
renovate[bot]
48a97b769f fix(deps): update dependency algoliasearch to v4.17.1 2023-05-29 08:24:04 +00:00
renovate[bot]
bdcc09f6ba chore(deps): update dependency @edx/frontend-build to v12.8.38 2023-05-29 06:05:47 +00:00
Maxwell Frank
ac4fb6a340 Merge pull request #757 from openedx/mfrank/fallback-img-typo
fix: fallback src typo for course cards
2023-05-25 13:01:59 -04:00
Maxwell Frank
409d365125 fix: fallback src typo for course cards 2023-05-25 16:15:20 +00:00
Jason Wesson
53985e94d8 fix: disable 'select a goal' from goal dropdown when a goal has been selected (#755)
* hide the second and third portion of skills form if currentGoal is false
2023-05-23 07:57:25 -07:00
renovate[bot]
0d9a39afd7 fix(deps): update dependency @edx/frontend-platform to v4.4.0 2023-05-23 04:18:26 +00:00
renovate[bot]
cbb860bb16 chore(deps): update dependency @edx/reactifex to v2.2.0 2023-05-23 01:34:22 +00:00
Justin Hynes
695df9aa0b fix: filter out non-English courses from recommendations (#752)
[APER-2444]

This PR updates the `getProductRecommendations` utiltiy function, adding a filter to only include English courses in our recommendations.
2023-05-22 13:13:43 -04:00
Maxwell Frank
603304b799 Merge pull request #751 from openedx/mfrank/fix-close-exit-buttons
fix: close and exit buttons
2023-05-18 13:58:42 -04:00
Maxwell Frank
d3e5931d05 fix: close and exit buttons 2023-05-18 17:52:40 +00:00
renovate[bot]
6804f7e127 fix(deps): update dependency algoliasearch to v4.17.0 2023-05-17 22:25:05 +00:00
Maxwell Frank
4b16673780 Merge pull request #750 from openedx/mfrank/reduce-event-payload
fix: reduce event payload
2023-05-17 11:12:10 -04:00
Maxwell Frank
6674025bd4 fix: reduce event payload 2023-05-17 14:38:15 +00:00
renovate[bot]
0dab2d03eb chore(deps): update commitlint monorepo to v17.6.3 2023-05-15 09:41:55 +00:00
Jenkins
df1a84feb7 chore(i18n): update translations 2023-05-14 16:40:48 -04:00
Bilal Qamar
334a9b090e feat: upgraded to node v18, added .nvmrc and updated workflows (#712)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* build: updated frontend-build, frontend-platform, component-footer & component-header packages

* refactor: updated snapshots

* refactor: updated snapshots
2023-05-12 14:09:48 -04:00
Maxwell Frank
5d06276838 Merge pull request #745 from openedx/mfrank/adding-configure-component
fix: remove filtering from current job select
2023-05-11 09:20:43 -04:00
Maxwell Frank
e391e427f1 fix: remove filtering from current job select 2023-05-11 13:05:04 +00:00
Maxwell Frank
b71328fd3f Merge pull request #744 from openedx/mfrank/adding-configure-component
fix: adding configure component to filter jobs
2023-05-10 16:40:11 -04:00
Maxwell Frank
3b9b3f8840 fix: adding configure component to filter jobs 2023-05-10 20:34:29 +00:00
Maxwell Frank
30e837306f Merge pull request #739 from openedx/mfrank/segement-events
APER-2333: Dispatch segment events from JS components
2023-05-10 15:45:35 -04:00
Maxwell Frank
c7a0c1d799 feat: adding analytics 2023-05-09 13:29:38 +00:00
renovate[bot]
337c97e3a0 chore(deps): update dependency @edx/frontend-build to v12.8.27 2023-05-08 09:32:26 +00:00
renovate[bot]
0de4496953 fix(deps): update dependency @edx/paragon to v20.32.3 2023-05-01 10:06:44 +00:00
renovate[bot]
359ae7f1fb chore(deps): update dependency @edx/frontend-build to v12.8.16 2023-05-01 07:10:23 +00:00
Omar Al-Ithawi
8d467f01dc feat: use atlas in make pull_translations (#732)
- Bump frontend-platform to bring intl-imports.js script
 - Move all i18n imports into `src/i18n/index.js` so intl-imports.js can override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL` environment variable is set.

This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).
2023-04-25 09:33:22 -04:00
renovate[bot]
20debcd79e fix(deps): update dependency reselect to v4.1.8 2023-04-24 13:13:42 +00:00
renovate[bot]
6a7cbf88df fix(deps): update dependency @edx/frontend-component-footer to v11.7.4 2023-04-24 09:34:43 +00:00
Jenkins
1b3880ee1b chore(i18n): update translations 2023-04-23 16:40:44 -04:00
renovate[bot]
79cebaf6df fix(deps): update dependency @edx/frontend-component-footer to v11.7.2 2023-04-17 15:00:56 +00:00
renovate[bot]
8686af563e chore(deps): update dependency @edx/frontend-build to v12.8.10 2023-04-17 10:24:54 +00:00
42 changed files with 3653 additions and 3425 deletions

1
.env
View File

@@ -25,6 +25,7 @@ FAVICON_URL=''
COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL=''
ENABLE_SKILLS_BUILDER=''
ENABLE_SKILLS_BUILDER_PROFILE=''
ALGOLIA_APP_ID=''

View File

@@ -26,6 +26,7 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL='http://localhost:18000/courses'
ENABLE_SKILLS_BUILDER='true'
ENABLE_SKILLS_BUILDER_PROFILE=''
ALGOLIA_APP_ID=''

View File

@@ -2,6 +2,7 @@ export TRANSIFEX_RESOURCE = frontend-app-profile
transifex_resource = frontend-app-profile
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
@@ -49,9 +50,23 @@ push_translations:
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-profile/src/i18n/messages:frontend-app-profile
$(intl_imports) frontend-component-header frontend-component-footer frontend-app-profile
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

6183
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,16 +30,17 @@
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.0",
"@edx/frontend-platform": "4.2.0",
"@edx/paragon": "^20.20.0",
"@edx/frontend-platform": "4.5.1",
"@edx/paragon": "^20.43.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"algoliasearch": "4.6.0",
"@pact-foundation/pact": "^11.0.2",
"algoliasearch": "4.17.2",
"classnames": "2.3.2",
"core-js": "3.27.2",
"core-js": "3.30.2",
"history": "4.10.1",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
@@ -59,15 +60,15 @@
"redux-saga": "1.2.3",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.13.11",
"reselect": "4.1.7",
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@commitlint/cli": "17.5.1",
"@commitlint/config-angular": "17.4.4",
"@commitlint/cli": "17.6.5",
"@commitlint/config-angular": "17.6.5",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.8.6",
"@edx/reactifex": "2.1.1",
"@edx/frontend-build": "12.8.51",
"@edx/reactifex": "2.2.0",
"@testing-library/react": "11.2.7",
"codecov": "3.8.3",
"enzyme": "3.11.0",

View File

@@ -1,3 +1,6 @@
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import arMessages from './messages/ar.json';
import frMessages from './messages/fr.json';
import es419Messages from './messages/es_419.json';
@@ -11,7 +14,7 @@ import hiMessages from './messages/hi.json';
import frCAMessages from './messages/fr_CA.json';
// no need to import en messages-- they are in the defaultMessage field
const messages = {
const appMessages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
@@ -25,4 +28,8 @@ const messages = {
uk: ukMessages,
};
export default messages;
export default [
headerMessages,
footerMessages,
appMessages,
];

View File

@@ -34,11 +34,11 @@
"profile.formcontrols.button.saved": "تم الحفظ",
"profile.visibility.who.just.me": "أنا فقط",
"profile.visibility.who.everyone": "جميع من على {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.learningGoal.learningGoal": "هدف التعلم",
"profile.learningGoal.options.start_career": "أريد أن أبدأ مسيرتي المهنية",
"profile.learningGoal.options.advance_career": "أريد أن ارتقي في مسيرتي المهنية",
"profile.learningGoal.options.learn_something_new": "أريد أن أتعلم شيئًا جديدًا",
"profile.learningGoal.options.something_else": "شيء آخر",
"profile.name.full.name": "الاسم الكامل",
"profile.name.details": "هذا هو الاسم الذي يظهر في حسابك وفي شهاداتك",
"profile.name.empty": "إضافة الاسم",
@@ -54,31 +54,32 @@
"profile.viewMyRecords": "عرض سجلّاتي",
"profile.loading": "يتم تحميل الملف الشخصي...",
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",
"select.preferences": "Select preferences",
"review.results": "Review results",
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
"learning.goal.prompt": "First, tell us what you want to achieve",
"select.learning.goal": "Select a goal",
"learning.goal.start_career": "I want to start my career",
"learning.goal.advance_career": "I want to advance my career",
"learning.goal.change_career": "I want to change careers",
"learning.goal.something.new": "I want to learn something new",
"learning.goal.something.else": "Something else",
"job.title.prompt": "Next, search and select your current job title",
"job.title.input.placeholder.text": "Search and select a job title",
"student.checkbox.prompt": "I'm a student",
"currently.looking.checkbox.prompt": "I'm currently looking for work",
"career.interest.prompt": "What careers are you interested in?",
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
"career.interest.remove.button.alt.text": "Remove career interest: ",
"matches.found.success.alert": "We found skills and courses that match your preferences!",
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
"related.skills.heading": "Related Skills",
"related.skills.selectable.box.label.text": "Related skills:",
"skills.builder.header.title": "باني المهارات",
"skills.builder.header.subheading": "دع (المنصة التعليمية أو edX) ان تكون دليلك",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "العودة إلى الخلف",
"next.step.button": "الخطوة التالية",
"exit.button": "خروج",
"select.preferences": "حدد التفضيلاتك",
"review.results": "مراجعة النتائج",
"skills.builder.description": "ابحث عن الدورات والبرامج المناسبة التي تساعدك في الوصول إلى أهدافك.",
"learning.goal.prompt": "أولاً، أخبرنا بما تريد تحقيقه",
"select.learning.goal": "اختر هدفًا",
"learning.goal.start_career": "أريد أن أبدأ مسيرتي المهنية",
"learning.goal.advance_career": "أريد أن ارتقي في مهنتي",
"learning.goal.change_career": "اريد تغيير المهنتي",
"learning.goal.something.new": "أريد أن أتعلم شيئًا جديدًا",
"learning.goal.something.else": "شيء آخر",
"job.title.prompt": "بعد ذلك، ابحث وحدد المسمى الوظيفي الحالي الخاص بك",
"job.title.input.placeholder.text": "أبحث واختار مسمى وظيفي",
"student.checkbox.prompt": "أنا طالب",
"currently.looking.checkbox.prompt": "أنا حاليا أبحث عن عمل",
"career.interest.prompt": "ما هي المهن التي تثير اهتمامك؟",
"career.interest.input.placeholder.text": "حدد ما يصل إلى ثلاث عناوين وظيفية جديدة",
"career.interest.remove.button.alt.text": "إزالة الاهتمام الوظيفي:",
"matches.found.success.alert": "وجدنا المهارات والدورات التي تناسب تفضيلاتك!",
"matches.not.found.danger.alert": "لم نتمكن من استرداد التوصيات في هذا الوقت. الرجاء معاودة المحاولة في وقت لاحق.",
"related.skills.heading": "مهارات ذات الصلة",
"related.skills.selectable.box.label.text": "مهارات ذات الصلة:",
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
}

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "La información del perfil solo la visualiza usted. Solo el nombre de usuario es visible para los demás en {siteName}.",
"skills.builder.header.title": "Constructor de habilidades",
"skills.builder.header.subheading": "Dejanos ser tu guía",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Volver Atrás",
"next.step.button": "Próximo paso",
"exit.button": "Salida",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}.",
"skills.builder.header.title": "Constructeur de compétences",
"skills.builder.header.subheading": "Laissez EDUlib être votre guide",
"skills.builder.header.title.is.medium": "Générateur de compétences edX",
"go.back.button": "Retour",
"next.step.button": "Prochaine étape",
"exit.button": "Sortie",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -8,7 +8,7 @@
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.my.certificates": "Мої сертифікати",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -56,6 +56,7 @@
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}.",
"skills.builder.header.title": "Skills Builder",
"skills.builder.header.subheading": "Let edX be your guide",
"skills.builder.header.title.is.medium": "edX Skills builder",
"go.back.button": "Go Back",
"next.step.button": "Next Step",
"exit.button": "Exit",

View File

@@ -16,10 +16,10 @@ import {
import React from 'react';
import ReactDOM from 'react-dom';
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
import Header from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
import appMessages from './i18n';
import messages from './i18n';
import configureStore from './data/configureStore';
import './index.scss';
@@ -46,11 +46,7 @@ subscribe(APP_INIT_ERROR, (error) => {
});
initialize({
messages: [
appMessages,
headerMessages,
footerMessages,
],
messages,
hydrateAuthenticatedUser: true,
handlers: {
config: () => {
@@ -62,6 +58,7 @@ initialize({
ALGOLIA_JOBS_INDEX_NAME: process.env.ALGOLIA_JOBS_INDEX_NAME || null,
ALGOLIA_PRODUCT_INDEX_NAME: process.env.ALGOLIA_PRODUCT_INDEX_NAME || null,
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || null,
MARKETING_SITE_SEARCH_URL: process.env.SEARCH_CATALOG_URL || null,
}, 'App loadConfig override handler');
},
},

View File

@@ -0,0 +1,94 @@
{
"consumer": {
"name": "frontend-app-profile"
},
"interactions": [
{
"description": "a request for a user's account information",
"providerStates": [
{
"name": "I have user account information"
}
],
"request": {
"headers": {
"Accept": "application/json"
},
"method": "GET",
"path": "/api/user/v1/accounts/edx"
},
"response": {
"body": {
"accomplishments_shared": false,
"account_privacy": "private",
"activation_key": null,
"bio": null,
"country": null,
"course_certificates": null,
"date_joined": "2021-07-30T20:01:46Z",
"email": "edx@example.com",
"extended_profile": [],
"gender": null,
"goals": null,
"id": 3,
"is_active": true,
"language_proficiencies": [],
"last_login": "2023-04-10T18:28:01.771896Z",
"level_of_education": null,
"mailing_address": null,
"name": "",
"pending_name_change": null,
"phone_number": null,
"profile_image": {
"has_image": false,
"image_url_full": "http://localhost:18000/static/images/profiles/default_500.png",
"image_url_large": "http://localhost:18000/static/images/profiles/default_120.png",
"image_url_medium": "http://localhost:18000/static/images/profiles/default_50.png",
"image_url_small": "http://localhost:18000/static/images/profiles/default_30.png"
},
"requires_parental_consent": true,
"secondary_email": null,
"secondary_email_enabled": null,
"social_links": [],
"state": null,
"time_zone": null,
"username": "edx",
"verified_name": null,
"year_of_birth": null
},
"headers": {
"Content-Type": "application/json"
},
"matchingRules": {
"body": {
"$": {
"combine": "AND",
"matchers": [
{
"match": "type"
}
]
}
},
"header": {}
},
"status": 200
}
}
],
"metadata": {
"pact-js": {
"version": "11.0.2"
},
"pactRust": {
"ffi": "0.4.0",
"models": "1.0.4"
},
"pactSpecification": {
"version": "3.0.0"
}
},
"provider": {
"name": "edx-platform"
}
}

View File

@@ -163,7 +163,7 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -241,7 +241,7 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -1745,7 +1745,7 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z"
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
fill="currentColor"
/>
</svg>
@@ -2387,7 +2387,7 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -2544,7 +2544,7 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -2622,7 +2622,7 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -3100,7 +3100,7 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z"
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
fill="currentColor"
/>
</svg>
@@ -3562,7 +3562,7 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -3719,7 +3719,7 @@ exports[`<ProfilePage /> Renders correctly in various states test preferreded la
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -3797,7 +3797,7 @@ exports[`<ProfilePage /> Renders correctly in various states test preferreded la
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -5060,7 +5060,7 @@ exports[`<ProfilePage /> Renders correctly in various states test preferreded la
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z"
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
fill="currentColor"
/>
</svg>
@@ -5612,7 +5612,7 @@ exports[`<ProfilePage /> Renders correctly in various states test preferreded la
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -5723,7 +5723,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24zM3.42 2.45L2.01 3.87l2.68 2.68A11.738 11.738 0 001 11.5C2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l3.43 3.43 1.41-1.41L3.42 2.45zM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5zm2.97-5.33a2.97 2.97 0 00-2.64-2.64l2.64 2.64z"
d="M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24ZM3.42 2.45 2.01 3.87l2.68 2.68A11.738 11.738 0 0 0 1 11.5C2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l3.43 3.43 1.41-1.41L3.42 2.45ZM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5Zm2.97-5.33a2.97 2.97 0 0 0-2.64-2.64l2.64 2.64Z"
fill="currentColor"
/>
</svg>
@@ -5784,7 +5784,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24zM3.42 2.45L2.01 3.87l2.68 2.68A11.738 11.738 0 001 11.5C2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l3.43 3.43 1.41-1.41L3.42 2.45zM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5zm2.97-5.33a2.97 2.97 0 00-2.64-2.64l2.64 2.64z"
d="M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24ZM3.42 2.45 2.01 3.87l2.68 2.68A11.738 11.738 0 0 0 1 11.5C2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l3.43 3.43 1.41-1.41L3.42 2.45ZM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5Zm2.97-5.33a2.97 2.97 0 0 0-2.64-2.64l2.64 2.64Z"
fill="currentColor"
/>
</svg>
@@ -6001,7 +6001,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -6079,7 +6079,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -6894,7 +6894,7 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -7051,7 +7051,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -7129,7 +7129,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -7818,7 +7818,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z"
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
fill="currentColor"
/>
</svg>
@@ -8011,7 +8011,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -8168,7 +8168,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -8246,7 +8246,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -8941,7 +8941,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z"
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
fill="currentColor"
/>
</svg>
@@ -9134,7 +9134,7 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
@@ -10094,7 +10094,7 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>

View File

View File

@@ -0,0 +1,90 @@
import path from 'path';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { getAccountRequest } from './services';
import { getConfig, initializeMockApp, setConfig } from '@edx/frontend-platform';
// Create a 'pact' between the two applications in the integration we are testing
const provider = new PactV3({
log: path.resolve(process.cwd(), 'src/pact-logs/pact.log'),
dir: path.resolve(process.cwd(), 'src/pacts'),
consumer: 'frontend-app-profile',
provider: 'edx-platform',
});
const getAccountBody = {
"account_privacy":"private",
"profile_image":{
"has_image":false,
"image_url_full":"http://localhost:18000/static/images/profiles/default_500.png",
"image_url_large":"http://localhost:18000/static/images/profiles/default_120.png",
"image_url_medium":"http://localhost:18000/static/images/profiles/default_50.png",
"image_url_small":"http://localhost:18000/static/images/profiles/default_30.png"
},"username":"edx",
"bio":null,
"course_certificates":null,
"country":null,
"date_joined":"2021-07-30T20:01:46Z",
"language_proficiencies":[],
"level_of_education":null,
"social_links":[],
"time_zone":null,
"accomplishments_shared":false,
"name":"",
"email":"edx@example.com",
"id":3,
"verified_name":null,
"extended_profile":[],
"gender":null,
"state":null,
"goals":null,
"is_active":true,
"last_login":"2023-04-10T18:28:01.771896Z",
"mailing_address":null,
"requires_parental_consent":true,
"secondary_email":null,
"secondary_email_enabled":null,
"year_of_birth":null,
"phone_number":null,
"activation_key":null,
"pending_name_change":null
}
const EXPECTED_GET_ACCOUNT_BODY = MatchersV3.like(getAccountBody);
describe('getAccount', () => {
beforeAll(async () => {
initializeMockApp();
});
it('returns an HTTP 200 and user account information', () => {
const username = 'edx';
provider
.given("I have user account information")
.uponReceiving(`a request for a user's account information`)
.withRequest({
method: 'GET',
path: `/api/user/v1/accounts/${username}`,
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: EXPECTED_GET_ACCOUNT_BODY
});
return provider.executeTest(async (mockserver) => {
const port = mockserver.port;
setConfig({
...getConfig(),
LMS_BASE_URL: `http://localhost:${port}`,
});
const response = await getAccountRequest(username);
expect(response.data).toEqual(getAccountBody);
})
});
});

View File

@@ -1,5 +1,5 @@
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
@@ -21,17 +21,24 @@ function processAndThrowError(error, errorDataProcessor) {
// GET ACCOUNT
export async function getAccount(username) {
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
const { data } = await getAccountRequest(username);
// Process response data
return processAccountData(data);
}
export async function getAccountRequest(username) {
return await getAuthenticatedHttpClient().get(
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`,
{ mockApiId: 'getAccount' }
);
}
// PATCH PROFILE
export async function patchProfile(username, params) {
const processedParams = snakeCaseObject(params);
const { data } = await getHttpClient()
const { data } = await getAuthenticatedHttpClient()
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
headers: {
'Content-Type': 'application/merge-patch+json',
@@ -49,7 +56,7 @@ export async function patchProfile(username, params) {
export async function postProfilePhoto(username, formData) {
// eslint-disable-next-line no-unused-vars
const { data } = await getHttpClient().post(
const { data } = await getAuthenticatedHttpClient().post(
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
formData,
{
@@ -74,7 +81,7 @@ export async function postProfilePhoto(username, formData) {
export async function deleteProfilePhoto(username) {
// eslint-disable-next-line no-unused-vars
const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
const { data } = await getAuthenticatedHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
// TODO: Someday in the future the POST photo endpoint
// will return the new values. At that time we should
@@ -87,7 +94,7 @@ export async function deleteProfilePhoto(username) {
// GET PREFERENCES
export async function getPreferences(username) {
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
const { data } = await getAuthenticatedHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, { mockApiId: 'getPreferences'});
return camelCaseObject(data);
}
@@ -107,7 +114,7 @@ export async function patchPreferences(username, params) {
visibility_time_zone: 'visibility.time_zone',
});
await getHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
await getAuthenticatedHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});
@@ -140,7 +147,7 @@ function transformCertificateData(data) {
export async function getCourseCertificates(username) {
const url = `${getConfig().LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
try {
const { data } = await getHttpClient().get(url);
const { data } = await getAuthenticatedHttpClient().get(url, { mockApiId: 'getCourseCertificates' });
return transformCertificateData(data);
} catch (e) {
logError(e);

View File

@@ -170,7 +170,7 @@ exports[`<SocialLinks /> calls social links with edit mode bio 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z"
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
fill="currentColor"
/>
</svg>

View File

@@ -1,11 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import edXLogo from '../images/edX-logo.svg';
import messages from './messages';
const SkillsBuilderHeader = () => {
const SkillsBuilderHeader = ({ isMedium }) => {
const { formatMessage } = useIntl();
if (isMedium) {
return (
<div className="d-flex">
<h1 className="h3 mb-0 text-white">
{formatMessage(messages.skillsBuilderHeaderTitleIsMedium)}
</h1>
</div>
);
}
return (
<div className="d-flex">
<img src={edXLogo} alt="edx-logo" className="mt-2 h-50" />
@@ -22,4 +32,8 @@ const SkillsBuilderHeader = () => {
);
};
SkillsBuilderHeader.propTypes = {
isMedium: PropTypes.func.isRequired,
};
export default SkillsBuilderHeader;

View File

@@ -11,6 +11,11 @@ const messages = defineMessages({
defaultMessage: 'Let edX be your guide',
description: 'Subheading to the Skills Builder title in the header component',
},
skillsBuilderHeaderTitleIsMedium: {
id: 'skills.builder.header.title.is.medium',
defaultMessage: 'edX Skills builder',
description: 'Title for the Skills Builder feature when screen size is medium or less',
},
});
export default messages;

View File

@@ -1,9 +1,10 @@
import React, { useState, useContext } from 'react';
import {
Button, Container, Stepper, ModalDialog,
Button, Container, Stepper, ModalDialog, Form, Hyperlink, useMediaQuery, breakpoints,
} from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useHistory } from 'react-router';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
STEP1, STEP2,
} from '../data/constants';
@@ -18,14 +19,36 @@ import headerImage from '../images/headerImage.png';
const SkillsBuilderModal = () => {
const { formatMessage } = useIntl();
const isMedium = useMediaQuery({ maxWidth: breakpoints.medium.maxWidth });
const { state } = useContext(SkillsBuilderContext);
const { careerInterests } = state;
const { currentGoal, currentJobTitle, careerInterests } = state;
const [currentStep, setCurrentStep] = useState(STEP1);
const history = useHistory();
const sendActionButtonEvent = (eventSuffix) => {
sendTrackEvent(
`edx.skills_builder.${eventSuffix}`,
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
current_goal: currentGoal,
current_job_title: currentJobTitle,
career_interests: careerInterests,
},
},
);
};
const onCloseHandle = () => {
history.goBack();
const nextStepHandle = () => {
setCurrentStep(STEP2);
sendActionButtonEvent('next_step');
};
const exitButtonHandle = () => {
sendActionButtonEvent('exit');
};
const closeButtonHandle = () => {
sendActionButtonEvent('close');
window.location.href = getConfig().MARKETING_SITE_SEARCH_URL;
};
return (
@@ -35,55 +58,52 @@ const SkillsBuilderModal = () => {
size="fullscreen"
className="skills-builder-modal bg-light-200"
isOpen
onClose={onCloseHandle}
onClose={closeButtonHandle}
>
<ModalDialog.Hero>
<ModalDialog.Hero className="med-min-height">
<ModalDialog.Hero.Background className="bg-primary-500">
<img src={headerImage} alt="" className="h-100" />
{ !isMedium && <img src={headerImage} alt="" className="h-100" /> }
</ModalDialog.Hero.Background>
<ModalDialog.Hero.Content>
<SkillsBuilderHeader />
<SkillsBuilderHeader isMedium={isMedium} />
</ModalDialog.Hero.Content>
</ModalDialog.Hero>
<Stepper.Header />
<Stepper.Header compactWidth="md" />
<ModalDialog.Body>
<Container size="md" className="p-4.5">
<Stepper.Step eventKey={STEP1} title={formatMessage(messages.selectPreferences)}>
<SelectPreferences />
</Stepper.Step>
<Form>
<Stepper.Step eventKey={STEP1} title={formatMessage(messages.selectPreferences)}>
<SelectPreferences />
</Stepper.Step>
<Stepper.Step eventKey={STEP2} title={formatMessage(messages.reviewResults)}>
<ViewResults />
</Stepper.Step>
<Stepper.Step eventKey={STEP2} title={formatMessage(messages.reviewResults)}>
<ViewResults />
</Stepper.Step>
</Form>
</Container>
</ModalDialog.Body>
<ModalDialog.Footer>
<Stepper.ActionRow eventKey={STEP1}>
<Button variant="outline-primary" onClick={onCloseHandle}>
{formatMessage(messages.goBackButton)}
</Button>
<Stepper.ActionRow.Spacer />
<Button
onClick={() => setCurrentStep(STEP2)}
onClick={nextStepHandle}
disabled={careerInterests.length === 0}
>
{formatMessage(messages.nextStepButton)}
</Button>
</Stepper.ActionRow>
<Stepper.ActionRow eventKey={STEP2}>
<Button
variant="outline-primary"
onClick={() => setCurrentStep(STEP1)}
>
<Button variant="outline-primary" onClick={() => setCurrentStep(STEP1)}>
{formatMessage(messages.goBackButton)}
</Button>
<Stepper.ActionRow.Spacer />
<Button onClick={onCloseHandle}>
{formatMessage(messages.exitButton)}
</Button>
<Hyperlink destination={getConfig().MARKETING_SITE_SEARCH_URL}>
<Button onClick={exitButtonHandle}>
{formatMessage(messages.exitButton)}
</Button>
</Hyperlink>
</Stepper.ActionRow>
</ModalDialog.Footer>
</ModalDialog>

View File

@@ -4,6 +4,7 @@ import {
IconButton, Icon,
} from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { Close } from '@edx/paragon/icons';
import { SkillsBuilderContext } from '../../skills-builder-context';
import { removeCareerInterest } from '../../data/actions';
@@ -13,6 +14,21 @@ const CareerInterestCard = ({ interest }) => {
const { formatMessage } = useIntl();
const { dispatch } = useContext(SkillsBuilderContext);
const handleRemoveCareerInterest = () => {
dispatch(removeCareerInterest(interest));
sendTrackEvent(
'edx.skills_builder.career_interest.removed',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
career_interest: interest,
},
},
);
};
return (
<div className="d-flex justify-content-between align-items-center pb-2 pr-2 pl-4 rounded shadow-sm">
<p className="pt-4">
@@ -22,7 +38,7 @@ const CareerInterestCard = ({ interest }) => {
iconAs={Icon}
src={Close}
alt={`${formatMessage(messages.removeCareerInterestButtonAltText)} ${interest}`}
onClick={() => dispatch(removeCareerInterest(interest))}
onClick={handleRemoveCareerInterest}
/>
</div>
);

View File

@@ -1,10 +1,11 @@
import React, { useContext } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
Stack, Row, Col, Form,
} from '@edx/paragon';
import { InstantSearch } from 'react-instantsearch-hooks-web';
import { Configure, InstantSearch } from 'react-instantsearch-hooks-web';
import JobTitleInstantSearch from './JobTitleInstantSearch';
import CareerInterestCard from './CareerInterestCard';
import { addCareerInterest } from '../../data/actions';
@@ -20,6 +21,17 @@ const CareerInterestSelect = () => {
const handleCareerInterestSelect = (value) => {
if (!careerInterests.includes(value) && careerInterests.length < 3) {
dispatch(addCareerInterest(value));
sendTrackEvent(
'edx.skills_builder.career_interest.added',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
career_interest: value,
},
},
);
}
};
@@ -30,9 +42,11 @@ const CareerInterestSelect = () => {
{formatMessage(messages.careerInterestPrompt)}
</h4>
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
<Configure filters="b2c_opt_in:true" />
<JobTitleInstantSearch
onSelected={handleCareerInterestSelect}
placeholder={formatMessage(messages.careerInterestInputPlaceholderText)}
data-testid="career-interest-select"
/>
</InstantSearch>
</Form.Label>

View File

@@ -3,6 +3,7 @@ import {
Form,
} from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { setGoal } from '../../data/actions';
import { SkillsBuilderContext } from '../../skills-builder-context';
import messages from './messages';
@@ -12,6 +13,22 @@ const GoalDropdown = () => {
const { state, dispatch } = useContext(SkillsBuilderContext);
const { currentGoal } = state;
const handleGoalSelect = (e) => {
const { value } = e.target;
dispatch(setGoal(value));
sendTrackEvent(
'edx.skills_builder.goal.select',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
current_goal: value,
},
},
);
};
return (
<Form.Group>
<Form.Label>
@@ -22,10 +39,10 @@ const GoalDropdown = () => {
<Form.Control
as="select"
value={currentGoal}
onChange={(e) => dispatch(setGoal(e.target.value))}
onChange={handleGoalSelect}
data-testid="goal-select-dropdown"
>
<option value="">{formatMessage(messages.selectLearningGoal)}</option>
<option value="" disabled={currentGoal}>{formatMessage(messages.selectLearningGoal)}</option>
<option>{formatMessage(messages.learningGoalStartCareer)}</option>
<option>{formatMessage(messages.learningGoalAdvanceCareer)}</option>
<option>{formatMessage(messages.learningGoalChangeCareer)}</option>

View File

@@ -4,6 +4,7 @@ import {
Form, Stack,
} from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { InstantSearch } from 'react-instantsearch-hooks-web';
import { setCurrentJobTitle } from '../../data/actions';
import { SkillsBuilderContext } from '../../skills-builder-context';
@@ -12,16 +13,37 @@ import messages from './messages';
const JobTitleSelect = () => {
const { formatMessage } = useIntl();
const { dispatch, algolia } = useContext(SkillsBuilderContext);
const { state, dispatch, algolia } = useContext(SkillsBuilderContext);
const { searchClient } = algolia;
const { currentJobTitle } = state;
const handleCurrentJobTitleSelect = (value) => {
dispatch(setCurrentJobTitle(value));
sendTrackEvent(
'edx.skills_builder.current_job.select',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
current_job_title: value,
},
},
);
};
// Below implementation sets the job title to "Student" or "Looking for work" — this overwrites any previous selection
// This will need to be revisited when we decide what to do with this data
const handleCheckboxChange = (e) => dispatch(setCurrentJobTitle(e.target.value));
const handleCheckboxChange = (e) => {
const { value } = e.target;
// only setCurrentJobTitle if the user hasn't selected a current job as we don't want to override their selection
if (!currentJobTitle) { dispatch(setCurrentJobTitle(value)); }
sendTrackEvent(
`edx.skills_builder.current_job.${value}`,
{
app_name: 'skills_builder',
category: 'skills_builder',
},
);
};
return (
<Stack>
@@ -33,6 +55,7 @@ const JobTitleSelect = () => {
<JobTitleInstantSearch
onSelected={handleCurrentJobTitleSelect}
placeholder={formatMessage(messages.jobTitleInputPlaceholderText)}
data-testid="job-title-select"
/>
</InstantSearch>
</Form.Label>
@@ -41,10 +64,10 @@ const JobTitleSelect = () => {
name="other-occupations"
onChange={handleCheckboxChange}
>
<Form.Checkbox value="Student">
<Form.Checkbox value="student">
{formatMessage(messages.studentCheckboxPrompt)}
</Form.Checkbox>
<Form.Checkbox value="Looking for work">
<Form.Checkbox value="looking_for_work">
{formatMessage(messages.currentlyLookingCheckboxPrompt)}
</Form.Checkbox>
</Form.CheckboxSet>

View File

@@ -27,7 +27,7 @@ const SelectPreferences = () => {
<JobTitleSelect />
)}
{currentJobTitle && (
{currentGoal && currentJobTitle && (
<CareerInterestSelect />
)}
</Stack>

View File

@@ -2,8 +2,13 @@ import {
screen, render, cleanup, fireEvent,
} from '@testing-library/react';
import { mergeConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { SkillsBuilderWrapperWithContext, dispatchMock, contextValue } from '../../../test/setupSkillsBuilder';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
describe('select-preferences', () => {
beforeAll(() => {
mergeConfig({
@@ -29,8 +34,13 @@ describe('select-preferences', () => {
payload: 'I want to advance my career',
type: 'SET_GOAL',
};
const expectedStudent = {
payload: 'student',
type: 'SET_CURRENT_JOB_TITLE',
};
const expectedJobTitle = {
payload: 'Student',
payload: 'Prospector',
type: 'SET_CURRENT_JOB_TITLE',
};
@@ -40,9 +50,41 @@ describe('select-preferences', () => {
const checkbox = screen.getByRole('checkbox', { name: 'I\'m a student' });
fireEvent.click(checkbox);
const jobTitleInput = screen.getByTestId('job-title-select');
fireEvent.change(jobTitleInput, { target: { value: 'Prospector' } });
fireEvent.click(screen.getByRole('button', { name: 'Prospector' }));
expect(screen.getByText('Next, search and select your current job title')).toBeTruthy();
expect(dispatchMock).toHaveBeenCalledWith(expectedGoal);
expect(dispatchMock).toHaveBeenCalledWith(expectedStudent);
expect(dispatchMock).toHaveBeenCalledWith(expectedJobTitle);
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.goal.select',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
current_goal: 'I want to advance my career',
},
},
);
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.current_job.student',
{
app_name: 'skills_builder',
category: 'skills_builder',
},
);
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.current_job.select',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
current_job_title: 'Prospector',
},
},
);
});
it('should render the third prompt if a current job title is selected', () => {
@@ -70,14 +112,33 @@ describe('select-preferences', () => {
...contextValue.state,
currentGoal: 'I want to start my career',
currentJobTitle: 'Goblin Lackey',
careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'],
careerInterests: ['Prospector'],
},
},
),
);
expect(screen.getByText('Prospector')).toBeTruthy();
expect(screen.getByText('Mirror Breaker')).toBeTruthy();
expect(screen.getByText('Bombardment')).toBeTruthy();
const careerInterestInput = screen.getByTestId('career-interest-select');
fireEvent.change(careerInterestInput, { target: { value: 'Mirror Breaker' } });
fireEvent.click(screen.getByRole('button', { name: 'Mirror Breaker' }));
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.career_interest.added',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
career_interest: 'Mirror Breaker',
},
},
);
expect(dispatchMock).toHaveBeenCalledWith(
{
payload: 'Mirror Breaker',
type: 'ADD_CAREER_INTEREST',
},
);
});
});
@@ -104,6 +165,16 @@ describe('select-preferences', () => {
fireEvent.click(screen.getByLabelText('Remove career interest: Prospector'));
expect(dispatchMock).toHaveBeenCalledWith(expected);
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.career_interest.removed',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
career_interest: 'Prospector',
},
},
);
});
});
});

View File

@@ -1,5 +1,16 @@
.skills-builder-modal {
button[aria-label="Close"][type="button"]{
color: #ffffff;
color: $white;
}
}
$breakpoint-medium: 992px;
@media (max-width: $breakpoint-medium) {
.med-min-height {
min-height: map-get($spacers, 6);
}
.pgn__modal-close-container {
top: 1rem;
}
}

View File

@@ -1,13 +1,18 @@
import React from 'react';
import { CardCarousel } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import RecommendationCard from './RecommendationCard';
import messages from './messages';
const CarouselStack = ({ selectedRecommendations }) => {
const { formatMessage } = useIntl();
const { name: jobName, recommendations } = selectedRecommendations;
const { id: jobId, name: jobName, recommendations } = selectedRecommendations;
const productTypeNames = Object.keys(recommendations);
const courseKeys = recommendations.course?.map(rec => ({
title: rec.title,
courserun_key: rec.active_run_key,
}));
const normalizeProductTypeName = (productType) => {
// If the productType is more than one word (i.e. boot_camp)
@@ -35,6 +40,24 @@ const CarouselStack = ({ selectedRecommendations }) => {
</h3>
);
const handleCourseCardClick = (courseKey, productType) => {
sendTrackEvent(
'edx.skills_builder.recommendation.click',
{
app_name: 'skills_builder',
category: 'skills_builder',
page: 'skills_builder',
courserun_key: courseKey,
product_type: productType,
selected_recommendations: {
job_id: jobId,
job_name: jobName,
courserun_keys: courseKeys,
},
},
);
};
return (
productTypeNames.map(productType => (
<CardCarousel
@@ -43,7 +66,12 @@ const CarouselStack = ({ selectedRecommendations }) => {
title={renderCarouselTitle(productType)}
>
{recommendations[productType].map(rec => (
<RecommendationCard rec={rec} key={rec.uuid} />
<RecommendationCard
key={rec.uuid}
handleCourseCardClick={handleCourseCardClick}
rec={rec}
productType={productType}
/>
))}
</CardCarousel>
)));

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { Card, Chip, Hyperlink } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import cardImageCapFallbackSrc from '../../images/card-imagecap-fallback.png';
const RecommendationCard = ({ rec }) => {
const RecommendationCard = ({ rec, productType, handleCourseCardClick }) => {
const {
card_image_url: cardImageUrl,
marketing_url: marketingUrl,
active_run_key: courseKey,
owners,
partner,
title,
@@ -17,12 +17,15 @@ const RecommendationCard = ({ rec }) => {
return (
<Hyperlink destination={marketingUrl} target="_blank" showLaunchIcon={false}>
<Card className={classNames('carousel-card')}>
<Card
className="carousel-card"
onClick={() => handleCourseCardClick(courseKey, productType)}
>
<Card.ImageCap
src={cardImageUrl}
logoSrc={logoImageUrl}
fallackSrc={cardImageCapFallbackSrc}
fallBackLogo={cardImageCapFallbackSrc}
fallbackSrc={cardImageCapFallbackSrc}
fallbackLogoSrc={cardImageCapFallbackSrc}
/>
<Card.Header title={title} />
<Card.Section>
@@ -48,7 +51,10 @@ RecommendationCard.propTypes = {
key: PropTypes.string,
logoImageUrl: PropTypes.string,
})),
active_run_key: PropTypes.string.isRequired,
}).isRequired,
productType: PropTypes.string.isRequired,
handleCourseCardClick: PropTypes.func.isRequired,
};
export default RecommendationCard;

View File

@@ -1,7 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import React, {
useContext, useEffect, useState,
} from 'react';
import {
Stack, Row, Alert, Spinner,
} from '@edx/paragon';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { CheckCircle, ErrorOutline } from '@edx/paragon/icons';
import { SkillsBuilderContext } from '../../skills-builder-context';
@@ -55,9 +58,24 @@ const ViewResults = () => {
}));
setJobSkillsList(jobInfo);
setSelectedJobTitle(jobInfo[0].name);
setSelectedJobTitle(results[0].name);
setProductRecommendations(results);
setIsLoading(false);
sendTrackEvent('edx.skills_builder.recommendation.shown', {
app_name: 'skills_builder',
category: 'skills_builder',
page: 'skills_builder',
selected_recommendations: {
job_id: results[0].id,
job_name: results[0].name,
/* We extract the title and course key into an array of objects */
courserun_keys: results[0].recommendations.course?.map(rec => ({
title: rec.title,
courserun_key: rec.active_run_key,
})),
},
is_default: true,
});
};
getRecommendations()
.catch(() => {
@@ -70,6 +88,35 @@ const ViewResults = () => {
setSelectedRecommendations(productRecommendations.find(rec => rec.name === selectedJobTitle));
}, [productRecommendations, selectedJobTitle]);
const handleJobTitleChange = (e) => {
const { value } = e.target;
setSelectedJobTitle(value);
const currentSelection = productRecommendations.find(rec => rec.name === value);
const { id: jobId, name: jobName, recommendations } = currentSelection;
const courseKeys = recommendations.course?.map(rec => ({
title: rec.title,
courserun_key: rec.active_run_key,
}));
/*
The is_default value will be set to false for any selections made by the user.
This code is intentionally duplicated from the event that fires in the useEffect for fetching recommendations.
This proved less clunky than refactoring to make things DRY as we have to ensure the first call fires only once.
The previous implementation wrapped the event in an additional useEffect that was looping unnecessarily.
We have plans to refactor all of the event code as part of APER-2392, where we will revisit this approach.
*/
sendTrackEvent('edx.skills_builder.recommendation.shown', {
app_name: 'skills_builder',
category: 'skills_builder',
page: 'skills_builder',
selected_recommendations: {
job_id: jobId,
job_name: jobName,
courserun_keys: courseKeys,
},
is_default: false,
});
};
if (fetchError) {
return (
<Alert
@@ -106,7 +153,7 @@ const ViewResults = () => {
<RelatedSkillsSelectableBoxSet
jobSkillsList={jobSkillsList}
selectedJobTitle={selectedJobTitle}
onChange={(e) => setSelectedJobTitle(e.target.value)}
onChange={handleJobTitleChange}
/>
<CarouselStack selectedRecommendations={selectedRecommendations} />

View File

@@ -2,9 +2,14 @@ import {
screen, render, cleanup, fireEvent, act,
} from '@testing-library/react';
import { mergeConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { SkillsBuilderWrapperWithContext, contextValue } from '../../../test/setupSkillsBuilder';
import { getProductRecommendations } from '../../../utils/search';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const renderSkillsBuilderWrapper = (
value = {
...contextValue,
@@ -37,22 +42,117 @@ describe('view-results', () => {
});
});
it('should render a <JobSillsSelectableBox> for each career interest the learner has submitted', async () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should render a <JobSillsSelectableBox> for each career interest the learner has submitted', () => {
expect(screen.getByText('Prospector')).toBeTruthy();
expect(screen.getByText('Mirror Breaker')).toBeTruthy();
const chipComponents = document.querySelectorAll('.pgn__chip');
expect(chipComponents[0].textContent).toEqual('finding shiny things');
expect(chipComponents[1].textContent).toEqual('mining');
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.recommendation.shown',
{
app_name: 'skills_builder',
category: 'skills_builder',
page: 'skills_builder',
selected_recommendations: {
job_id: 0,
job_name: 'Prospector',
courserun_keys: [
{
title: 'Mining with the Mons',
courserun_key: 'MONS101',
},
{
title: 'The Art of Warren Upkeep',
courserun_key: 'WAR101',
},
],
},
is_default: true,
},
);
// called once when "Next Step" button is clicked and then again for above event
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
});
it('renders a carousel of <Card> components', async () => {
it('renders a carousel of <Card> components', () => {
expect(screen.getByText('Course recommendations for Prospector')).toBeTruthy();
});
it('changes the recommendations based on the selected job title', () => {
fireEvent.click(screen.getByRole('radio', { name: 'Mirror Breaker' }));
expect(screen.getByText('Course recommendations for Mirror Breaker')).toBeTruthy();
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.recommendation.shown',
{
app_name: 'skills_builder',
category: 'skills_builder',
page: 'skills_builder',
selected_recommendations: {
job_id: 1,
job_name: 'Mirror Breaker',
courserun_keys: [
{
title: 'Mining with the Mons',
courserun_key: 'MONS101',
},
{
title: 'The Art of Warren Upkeep',
courserun_key: 'WAR101',
},
],
},
is_default: false,
},
);
});
it('sends an event when the "Next Step" button is clicked', () => {
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.next_step',
{
app_name: 'skills_builder',
category: 'skills_builder',
learner_data: {
current_goal: 'I want to start my career',
current_job_title: 'Goblin Lackey',
career_interests: ['Prospector', 'Mirror Breaker', 'Bombardment'],
},
},
);
});
it('fires an event when a product recommendation is clicked', () => {
fireEvent.click(screen.getByText('Mining with the Mons'));
expect(sendTrackEvent).toHaveBeenCalledWith(
'edx.skills_builder.recommendation.click',
{
app_name: 'skills_builder',
category: 'skills_builder',
page: 'skills_builder',
courserun_key: 'MONS101',
product_type: 'course',
selected_recommendations: {
job_id: 0,
job_name: 'Prospector',
courserun_keys: [
{
title: 'Mining with the Mons',
courserun_key: 'MONS101',
},
{
title: 'The Art of Warren Upkeep',
courserun_key: 'WAR101',
},
],
},
},
);
});
});

View File

@@ -2,11 +2,11 @@ export const mockData = {
hits: [
{
id: 0,
name: 'Text File Engineer'
name: 'Prospector'
},
{
id: 1,
name: 'Screen Viewer'
name: 'Mirror Breaker'
},
],
searchJobs: [
@@ -44,6 +44,7 @@ export const mockData = {
partner: ['edx'],
card_image_url: 'https://thisIsAUrl.ForAnImage.01.jpeg',
marketing_url: 'https://thisIsAUrl.ForTheRecommendedContent.01.com',
active_run_key: 'MONS101',
owners: [
{
logoImageUrl: 'https://thisIsAUrl.ForALogoImage.01.jpeg',
@@ -56,6 +57,7 @@ export const mockData = {
partner: ['edx'],
card_image_url: 'https://thisIsAUrl.ForAnImage.02.jpeg',
marketing_url: 'https://thisIsAUrl.ForTheRecommendedContent.02.com',
active_run_key: 'WAR101',
owners: [
{
logoImageUrl: 'https://thisIsAUrl.ForALogoImage.02.jpeg',

View File

@@ -11,6 +11,7 @@ jest.mock('@edx/frontend-platform/logging');
jest.mock('react-instantsearch-hooks-web', () => ({
// eslint-disable-next-line react/prop-types
InstantSearch: ({ children }) => (<div>{children}</div>),
Configure: jest.fn(() => (null)),
useSearchBox: jest.fn(() => ({ refine: jest.fn() })),
useHits: jest.fn(() => ({ hits: mockData.hits })),
}));

View File

@@ -96,7 +96,7 @@ export const getProductRecommendations = async (productIndex, productType, skill
const formattedSkillNames = formatFacetFilterData('skills.skill', skills);
try {
const { hits } = await productIndex.search('', {
filters: `product: "${productType}"`,
filters: `product: "${productType}" AND language: "English"`,
facetFilters: [
formattedSkillNames,
],

View File

@@ -55,7 +55,7 @@ describe('Algolias utility function', () => {
it('getProductRecommendations() queries Algolia with the expected search parameters', async () => {
const expectedSearchParameters = {
filters: 'product: "Course"',
filters: 'product: "Course" AND language: "English"',
facetFilters: [
['skills.skill:Sword Lobbing'],
],