From a8d58080d77fe405a2d933529a05a115067e2d20 Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 29 Sep 2022 22:45:34 +0400 Subject: [PATCH 1/5] Move mentor page file --- .../[lessonSlug]/mentor/index.test.js | 2 +- .../[lessonSlug]/mentor/addExercise/index.tsx | 221 ++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx diff --git a/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js b/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js index dc6573931..2846cad78 100644 --- a/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js +++ b/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js @@ -1,7 +1,7 @@ jest.mock('@sentry/nextjs') import React from 'react' -import MentorPage from '../../../../../pages/curriculum/[lessonSlug]/mentor/index' +import MentorPage from '../../../../../pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx' import userEvent from '@testing-library/user-event' import { render, screen, act } from '@testing-library/react' import { MockedProvider } from '@apollo/client/testing' diff --git a/pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx b/pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx new file mode 100644 index 000000000..9360624fd --- /dev/null +++ b/pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx @@ -0,0 +1,221 @@ +import React, { useMemo, useState } from 'react' +import { + AddExerciseMutation, + GetAppProps, + Module, + useAddExerciseMutation, + withGetApp +} from '../../../../../graphql' +import { useRouter } from 'next/router' +import { AdminLayout } from '../../../../../components/admin/AdminLayout' +import { DropdownMenu } from '../../../../../components/DropdownMenu' +import { FormCard, MD_INPUT } from '../../../../../components/FormCard' +import { formChange } from '../../../../../helpers/formChange' +import { exercisesValidation } from '../../../../../helpers/formValidation' +import ExercisePreview from '../../../../../components/ExercisePreview' +import styles from '../../../../../scss/mentorPage.module.scss' +import { get } from 'lodash' +import QueryInfo from '../../../../../components/QueryInfo' +import { errorCheckAllFields } from '../../../../../helpers/admin/adminHelpers' +import * as Sentry from '@sentry/nextjs' + +type DetachedModule = Omit + +type HeaderProps = { + lesson?: T + addExerciseData?: AddExerciseMutation | null + loading: boolean + error?: + | { + message: string + } + | string + setModule: (v: null | DetachedModule) => void + setErrorMsg: (v: string) => void +} +const Header = < + T extends { title: string; modules?: DetachedModule[] | null } +>({ + lesson, + addExerciseData, + loading, + error, + setModule, + setErrorMsg +}: HeaderProps) => { + const modules = get(lesson, 'modules') ?? [] + + return ( +
+
+

{get(lesson, 'title')}

+
+
+ Select a module + ({ + ...m, + title: m.name, + onClick: () => { + setModule({ ...m }) + setErrorMsg('') + } + }))} + /> +
+ +
+ ) +} + +type MainProps = { + onClick: () => void + formOptions: typeof initValues + handleChange: (value: string, propertyIndex: number) => Promise + exercise: { + description: string + answer: string + explanation: string + } +} +const Main = ({ onClick, formOptions, handleChange, exercise }: MainProps) => ( +
+
+
+ +
+ +
+
+) + +const initValues = [ + { + title: 'description', + type: MD_INPUT, + value: '', + error: '' + }, + { + title: 'answer', + value: '', + error: '' + }, + { + title: 'explanation', + type: MD_INPUT, + value: '', + error: '' + } +] + +const MentorPage = ({ data }: GetAppProps) => { + const router = useRouter() + const { lessonSlug } = router.query + + // Omitting author and lesson because data.lessons[i].modules[i] mismatching type + const [module, setModule] = useState(null) + const [errorMsg, setErrorMsg] = useState('') + + const { lessons } = data + const lesson = useMemo( + () => (lessons || []).find(lesson => lesson.slug === lessonSlug), + [lessons] + ) + + const [formOptions, setFormOptions] = useState(initValues) + const [description, answer, explanation] = formOptions + + const [addExercise, { data: addExerciseData, loading, error }] = + useAddExerciseMutation({ + variables: { + moduleId: get(module, 'id', -1), + description: description.value, + answer: answer.value, + explanation: explanation.value + } + }) + + const handleChange = async (value: string, propertyIndex: number) => { + await formChange( + value, + propertyIndex, + formOptions, + setFormOptions, + exercisesValidation + ) + } + + const onClick = async () => { + try { + const newProperties = [...formOptions] + const valid = await errorCheckAllFields( + newProperties, + exercisesValidation + ) + + if (!module) { + setErrorMsg('Please select a module') + return + } + + if (!valid) { + // Update the forms so the error messages appear + setFormOptions(newProperties) + return + } + + await addExercise() + } catch (err) { + Sentry.captureException(err) + } + } + + return ( + +
+
+ + ) +} + +export default withGetApp()(MentorPage) From f3c6d9f5634ecc5e1958ad3ac815049f39639dbe Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 29 Sep 2022 22:45:49 +0400 Subject: [PATCH 2/5] Add Mentor exercises path --- pages/exercises/[lessonSlug].tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/exercises/[lessonSlug].tsx b/pages/exercises/[lessonSlug].tsx index 038bff993..1b19de8b2 100644 --- a/pages/exercises/[lessonSlug].tsx +++ b/pages/exercises/[lessonSlug].tsx @@ -39,7 +39,11 @@ const Exercises: React.FC> = ({ ? [{ text: 'lessons', url: currentLesson.docUrl }] : []), { text: 'challenges', url: `/curriculum/${currentLesson.slug}` }, - { text: 'exercises', url: `/exercises/${currentLesson.slug}` } + { text: 'exercises', url: `/exercises/${currentLesson.slug}` }, + { + text: 'mentor exercises', + url: `/curriculum/${currentLesson.slug}/mentor/` + } ] const currentExercises = exercises From 7a0bc22449234e725138e8cb7d37e78bb706fe4f Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 29 Sep 2022 22:46:33 +0400 Subject: [PATCH 3/5] feat: Create mentor main page --- .../mentor/addExercise/index.test.js | 164 +++++++++ .../curriculum/[lessonSlug]/mentor/index.tsx | 319 +++++++----------- 2 files changed, 279 insertions(+), 204 deletions(-) create mode 100644 __tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js diff --git a/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js b/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js new file mode 100644 index 000000000..d5b4be9e0 --- /dev/null +++ b/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js @@ -0,0 +1,164 @@ +import React from 'react' +import { render, waitFor, screen, fireEvent } from '@testing-library/react' +import '@testing-library/jest-dom' +import Exercises from '../../../../../../pages/curriculum/[lessonSlug]/mentor' +import { useRouter } from 'next/router' +import { MockedProvider } from '@apollo/client/testing' +import getExercisesData from '../../../../../../__dummy__/getExercisesData' +import GET_EXERCISES from '../../../../../../graphql/queries/getExercises' + +describe('Exercises page', () => { + const { query, push } = useRouter() + query['lessonSlug'] = 'js0' + + test('Should render correctly', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData + } + } + ] + + render( + + + + ) + + await waitFor(() => + screen.getByRole('heading', { name: /Foundations of JavaScript/i }) + ) + + screen.getByRole('link', { name: 'CHALLENGES' }) + screen.getByRole('link', { name: 'EXERCISES' }) + screen.getByRole('link', { name: 'LESSON' }) + }) + + test('Should push to addExercise page', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData + } + } + ] + + const { getByRole, queryByRole, getByLabelText } = render( + + + + ) + + await waitFor(() => + getByRole('heading', { name: /Foundations of JavaScript/i }) + ) + + const solveExercisesButton = getByRole('button', { + name: 'ADD EXERCISE' + }) + + fireEvent.click(solveExercisesButton) + + expect(push).toBeCalledWith('/curriculum/js0/mentor/addExercise') + }) + + test('Should not render lessons nav card tab if lesson docUrl is null', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: { + ...getExercisesData, + lessons: getExercisesData.lessons.map(lesson => ({ + ...lesson, + docUrl: null + })) + } + } + } + ] + + render( + + + + ) + + await waitFor(() => + screen.getByRole('heading', { name: /Foundations of JavaScript/i }) + ) + + screen.getByRole('link', { name: 'CHALLENGES' }) + screen.getByRole('link', { name: 'EXERCISES' }) + expect(screen.queryByRole('link', { name: 'LESSONS' })).toBeNull() + }) + + test('Should render a 500 error page if the lesson data is null', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: { + ...getExercisesData, + lessons: null + } + } + } + ] + + render( + + + + ) + + await waitFor(() => screen.getByRole('heading', { name: /500 Error/i })) + }) + + test('Should render a 404 error page if the lesson is not found', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: { + ...getExercisesData, + lessons: [] + } + } + } + ] + + render( + + + + ) + + await waitFor(() => screen.getByRole('heading', { name: /404 Error/i })) + }) + + test('Should render a loading spinner if useRouter is not ready', async () => { + useRouter.mockImplementation(() => ({ + isReady: false + })) + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData + } + } + ] + + render( + + + + ) + + await waitFor(() => screen.getByText('Loading...')) + }) +}) diff --git a/pages/curriculum/[lessonSlug]/mentor/index.tsx b/pages/curriculum/[lessonSlug]/mentor/index.tsx index 33fdc7a77..3d099dbfb 100644 --- a/pages/curriculum/[lessonSlug]/mentor/index.tsx +++ b/pages/curriculum/[lessonSlug]/mentor/index.tsx @@ -1,221 +1,132 @@ -import React, { useMemo, useState } from 'react' -import { - AddExerciseMutation, - GetAppProps, - Module, - useAddExerciseMutation, - withGetApp -} from '../../../../graphql' import { useRouter } from 'next/router' -import { AdminLayout } from '../../../../components/admin/AdminLayout' -import { DropdownMenu } from '../../../../components/DropdownMenu' -import { FormCard, MD_INPUT } from '../../../../components/FormCard' -import { formChange } from '../../../../helpers/formChange' -import { exercisesValidation } from '../../../../helpers/formValidation' -import ExercisePreview from '../../../../components/ExercisePreview' -import styles from '../../../../scss/mentorPage.module.scss' -import { get } from 'lodash' -import QueryInfo from '../../../../components/QueryInfo' -import { errorCheckAllFields } from '../../../../helpers/admin/adminHelpers' -import * as Sentry from '@sentry/nextjs' - -type DetachedModule = Omit +import React from 'react' +import Layout from '../../../../components/Layout' +import withQueryLoader, { + QueryDataProps +} from '../../../../containers/withQueryLoader' +import { GetExercisesQuery } from '../../../../graphql' +import Error, { StatusCode } from '../../../../components/Error' +import LoadingSpinner from '../../../../components/LoadingSpinner' +import AlertsDisplay from '../../../../components/AlertsDisplay' +import NavCard from '../../../../..../../components/NavCard' +import ExercisePreviewCard from '../../../../..../../components/ExercisePreviewCard' +import { NewButton } from '../../../../..../../components/theme/Button' +import GET_EXERCISES from '../../../../..../../graphql/queries/getExercises' +import styles from '../../../../scss/exercises.module.scss' + +const AddExercises: React.FC> = ({ + queryData +}) => { + const { lessons, alerts, exercises } = queryData + const router = useRouter() -type HeaderProps = { - lesson?: T - addExerciseData?: AddExerciseMutation | null - loading: boolean - error?: - | { - message: string - } - | string - setModule: (v: null | DetachedModule) => void - setErrorMsg: (v: string) => void -} -const Header = < - T extends { title: string; modules?: DetachedModule[] | null } ->({ - lesson, - addExerciseData, - loading, - error, - setModule, - setErrorMsg -}: HeaderProps) => { - const modules = get(lesson, 'modules') ?? [] + if (!router.isReady) return + + const slug = router.query.lessonSlug + if (!lessons || !alerts || !exercises) + return + + const currentLesson = lessons.find(lesson => lesson.slug === slug) + if (!currentLesson) + return + + const tabs = [ + ...(currentLesson.docUrl + ? [{ text: 'lesson', url: currentLesson.docUrl }] + : []), + { text: 'challenges', url: `/curriculum/${currentLesson.slug}` }, + { text: 'exercises', url: `/exercises/${currentLesson.slug}` }, + { + text: 'mentor exercises', + url: `/curriculum/${currentLesson.slug}/mentor` + } + ] + + const currentExercises = exercises + .filter(exercise => exercise?.module.lesson.slug === slug) + .map(exercise => ({ + id: exercise.id, + moduleName: exercise.module.name, + problem: exercise.description, + answer: exercise.answer, + explanation: exercise.explanation || '' + })) return ( -
-
-

{get(lesson, 'title')}

-
-
- Select a module - ({ - ...m, - title: m.name, - onClick: () => { - setModule({ ...m }) - setErrorMsg('') - } - }))} - /> -
- + -
+ + {alerts && } + ) } -type MainProps = { - onClick: () => void - formOptions: typeof initValues - handleChange: (value: string, propertyIndex: number) => Promise - exercise: { - description: string +type ExerciseListProps = { + tabs: { text: string; url: string }[] + lessonTitle: string + exercises: { + moduleName: string + problem: string answer: string - explanation: string - } + }[] + lessonSlug: string } -const Main = ({ onClick, formOptions, handleChange, exercise }: MainProps) => ( -
-
-
- -
- -
-
-) - -const initValues = [ - { - title: 'description', - type: MD_INPUT, - value: '', - error: '' - }, - { - title: 'answer', - value: '', - error: '' - }, - { - title: 'explanation', - type: MD_INPUT, - value: '', - error: '' - } -] -const MentorPage = ({ data }: GetAppProps) => { +const ExerciseList = ({ + tabs, + lessonTitle, + exercises, + lessonSlug +}: ExerciseListProps) => { const router = useRouter() - const { lessonSlug } = router.query - - // Omitting author and lesson because data.lessons[i].modules[i] mismatching type - const [module, setModule] = useState(null) - const [errorMsg, setErrorMsg] = useState('') - - const { lessons } = data - const lesson = useMemo( - () => (lessons || []).find(lesson => lesson.slug === lessonSlug), - [lessons] - ) - - const [formOptions, setFormOptions] = useState(initValues) - const [description, answer, explanation] = formOptions - - const [addExercise, { data: addExerciseData, loading, error }] = - useAddExerciseMutation({ - variables: { - moduleId: get(module, 'id', -1), - description: description.value, - answer: answer.value, - explanation: explanation.value - } - }) - - const handleChange = async (value: string, propertyIndex: number) => { - await formChange( - value, - propertyIndex, - formOptions, - setFormOptions, - exercisesValidation - ) - } - - const onClick = async () => { - try { - const newProperties = [...formOptions] - const valid = await errorCheckAllFields( - newProperties, - exercisesValidation - ) - - if (!module) { - setErrorMsg('Please select a module') - return - } - - if (!valid) { - // Update the forms so the error messages appear - setFormOptions(newProperties) - return - } - - await addExercise() - } catch (err) { - Sentry.captureException(err) - } - } return ( - -
-
- + <> +
+ tab.text === 'mentor exercises')} + tabs={tabs} + /> +
+
+

{lessonTitle}

+
+ + router.push(`/curriculum/${lessonSlug}/mentor/addExercise`) + } + > + ADD EXERCISE + +
+
+
+ {exercises.map((exercise, i) => ( + + ))} +
+
+
+ ) } -export default withGetApp()(MentorPage) +export default withQueryLoader( + { + query: GET_EXERCISES + }, + AddExercises +) From a792fabd9e8894aa10b079023f7ace5bd3a1127d Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 29 Sep 2022 23:01:52 +0400 Subject: [PATCH 4/5] Replace files content --- .../mentor/addExercise/index.test.js | 370 ++++++++++++------ .../[lessonSlug]/mentor/index.test.js | 370 ++++++------------ 2 files changed, 370 insertions(+), 370 deletions(-) diff --git a/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js b/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js index d5b4be9e0..e8291aeb6 100644 --- a/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js +++ b/__tests__/pages/curriculum/[lessonSlug]/mentor/addExercise/index.test.js @@ -1,164 +1,306 @@ +jest.mock('@sentry/nextjs') + import React from 'react' -import { render, waitFor, screen, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import Exercises from '../../../../../../pages/curriculum/[lessonSlug]/mentor' -import { useRouter } from 'next/router' +import AddExercisePage from '../../../../../../pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx' +import userEvent from '@testing-library/user-event' +import { render, screen, act } from '@testing-library/react' import { MockedProvider } from '@apollo/client/testing' -import getExercisesData from '../../../../../../__dummy__/getExercisesData' -import GET_EXERCISES from '../../../../../../graphql/queries/getExercises' - -describe('Exercises page', () => { - const { query, push } = useRouter() - query['lessonSlug'] = 'js0' - - test('Should render correctly', async () => { - const mocks = [ - { - request: { query: GET_EXERCISES }, - result: { - data: getExercisesData +import GET_APP from '../../../../../../graphql/queries/getApp' +import ADD_EXERCISE from '../../../../../../graphql/queries/addExercise' + +import * as Sentry from '@sentry/nextjs' + +import dummyLessonData from '../../../../../../__dummy__/lessonData' +import dummySessionData from '../../../../../../__dummy__/sessionData' +import dummyAlertData from '../../../../../../__dummy__/alertData' + +// Imported to be able to use expect(...).toBeInTheDocument() +import '@testing-library/jest-dom' + +const getAppQueryMock = { + request: { query: GET_APP }, + result: { + data: { + session: dummySessionData, + lessons: [ + { + ...dummyLessonData[0], + id: 1 + }, + ...dummyLessonData + ], + alerts: dummyAlertData + } + } +} + +const getAppQueryMockWithNoModules = { + request: { query: GET_APP }, + result: { + data: { + session: dummySessionData, + lessons: [ + { + ...dummyLessonData[0], + modules: [], + id: 1 } - } - ] + ], + alerts: dummyAlertData + } + } +} + +const fakeExercise = { + moduleId: -1, + description: 'exercise desc', + answer: 'x', + explanation: 'because x is the answer' +} + +const addExerciseMutationMock = { + request: { + query: ADD_EXERCISE, + variables: { ...fakeExercise, moduleId: 1 } + }, + result: jest.fn(() => ({ + data: { + addExercise: { ...fakeExercise } + } + })) +} + +const addExerciseMutationMockSuccess = { + request: { + query: ADD_EXERCISE, + variables: { ...fakeExercise, moduleId: 1 } + }, + result: jest.fn(() => ({ + data: { + addExercise: { ...fakeExercise, moduleId: 1 } + } + })) +} + +const addExerciseMutationMockError = { + request: { + query: ADD_EXERCISE, + variables: { ...fakeExercise, moduleId: 1 } + }, + error: new Error('Error') +} + +const mocks = [getAppQueryMock, addExerciseMutationMock] +const mocksWithError = [getAppQueryMock, addExerciseMutationMockError] +const mocksWithSuccess = [getAppQueryMock, addExerciseMutationMockSuccess] +const mocksWithNoModules = [ + getAppQueryMockWithNoModules, + addExerciseMutationMock +] + +const useRouter = jest.spyOn(require('next/router'), 'useRouter') +const useRouterObj = { + asPath: 'c0d3.com/curriculum/js1/mentor', + query: { + lessonSlug: 'js1' + }, + push: jest.fn() +} + +useRouter.mockImplementation(() => useRouterObj) + +const fillOutExerciseForms = async () => { + const [description, explanation] = screen.getAllByTestId('textbox') + const answer = screen.getByTestId('input1') + + // the type event needs to be delayed so the Formik validations finish + await userEvent.type(description, fakeExercise.description, { delay: 1 }) + await userEvent.type(answer, fakeExercise.answer, { delay: 1 }) + await userEvent.type(explanation, fakeExercise.explanation, { delay: 1 }) +} + +describe('AddExercise page', () => { + it('should not submit when inputs are empty', async () => { + expect.assertions(1) render( - - + + ) - await waitFor(() => - screen.getByRole('heading', { name: /Foundations of JavaScript/i }) - ) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) - screen.getByRole('link', { name: 'CHALLENGES' }) - screen.getByRole('link', { name: 'EXERCISES' }) - screen.getByRole('link', { name: 'LESSON' }) - }) + const dropdownBtn = screen.getByTestId('dropdown-lesson') + await userEvent.click(dropdownBtn) - test('Should push to addExercise page', async () => { - const mocks = [ - { - request: { query: GET_EXERCISES }, - result: { - data: getExercisesData - } - } - ] + const dropdownItem = screen.getByText('module1') + await userEvent.click(dropdownItem) + + const submitButton = await screen.findByText('Save exercise') + await userEvent.click(submitButton) + + expect(screen.queryAllByText('Required')[0]).toBeInTheDocument() + }) - const { getByRole, queryByRole, getByLabelText } = render( - - + it('should render Mentor page', async () => { + render( + + ) - await waitFor(() => - getByRole('heading', { name: /Foundations of JavaScript/i }) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + expect(screen.queryAllByText('Select a module')[0]).toBeInTheDocument() + }) + + it('should fill out the inputs', async () => { + render( + + + ) - const solveExercisesButton = getByRole('button', { - name: 'ADD EXERCISE' - }) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + await fillOutExerciseForms() - fireEvent.click(solveExercisesButton) + const [description, explanation] = screen.getAllByTestId('textbox') + const answer = screen.getByTestId('input1') - expect(push).toBeCalledWith('/curriculum/js0/mentor/addExercise') + expect(description.value).toBe(fakeExercise.description) + expect(explanation.value).toBe(fakeExercise.explanation) + expect(answer.value).toBe(fakeExercise.answer) }) - test('Should not render lessons nav card tab if lesson docUrl is null', async () => { - const mocks = [ - { - request: { query: GET_EXERCISES }, - result: { - data: { - ...getExercisesData, - lessons: getExercisesData.lessons.map(lesson => ({ - ...lesson, - docUrl: null - })) - } - } - } - ] + it('should add exercise (submit)', async () => { + expect.assertions(1) render( - - + + ) - await waitFor(() => - screen.getByRole('heading', { name: /Foundations of JavaScript/i }) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + const dropdownBtn = screen.getByTestId('dropdown-lesson') + await userEvent.click(dropdownBtn) + + const dropdownItem = await screen.findByText('module1') + await userEvent.click(dropdownItem) + + await fillOutExerciseForms() + + const submitButton = screen.getByText('Save exercise') + await userEvent.click(submitButton) + + expect( + await screen.findByText('Added the exercise successfully!') + ).toBeInTheDocument() + }) + + it('should not add exercise (submit) if no module is selected', async () => { + expect.assertions(1) + + render( + + + ) - screen.getByRole('link', { name: 'CHALLENGES' }) - screen.getByRole('link', { name: 'EXERCISES' }) - expect(screen.queryByRole('link', { name: 'LESSONS' })).toBeNull() + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + await fillOutExerciseForms() + + const submitButton = screen.getByText('Save exercise') + await userEvent.click(submitButton) + + expect( + await screen.findByText('Please select a module') + ).toBeInTheDocument() }) - test('Should render a 500 error page if the lesson data is null', async () => { - const mocks = [ - { - request: { query: GET_EXERCISES }, - result: { - data: { - ...getExercisesData, - lessons: null - } - } - } - ] + it('should set error when adding an exercise', async () => { + expect.assertions(2) render( - - + + ) - await waitFor(() => screen.getByRole('heading', { name: /500 Error/i })) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + const dropdownBtn = screen.getByTestId('dropdown-lesson') + await userEvent.click(dropdownBtn) + + const dropdownItem = screen.queryByText('module1') + await userEvent.click(dropdownItem) + + await fillOutExerciseForms() + + const submitButton = screen.queryByText('Save exercise') + await userEvent.click(submitButton) + + expect( + await screen.findByText('An error occurred. Please try again.') + ).toBeInTheDocument() + expect(Sentry.captureException).toBeCalled() }) - test('Should render a 404 error page if the lesson is not found', async () => { - const mocks = [ - { - request: { query: GET_EXERCISES }, - result: { - data: { - ...getExercisesData, - lessons: [] - } - } - } - ] + it('should successfully add an exercise', async () => { + expect.assertions(1) render( - - + + ) - await waitFor(() => screen.getByRole('heading', { name: /404 Error/i })) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + const dropdownBtn = screen.getByTestId('dropdown-lesson') + await userEvent.click(dropdownBtn) + + const dropdownItem = await screen.findByText('module1') + await userEvent.click(dropdownItem) + + await fillOutExerciseForms() + + const submitButton = screen.queryByText('Save exercise') + await userEvent.click(submitButton) + + expect( + await screen.findByText('Added the exercise successfully!') + ).toBeInTheDocument() }) - test('Should render a loading spinner if useRouter is not ready', async () => { - useRouter.mockImplementation(() => ({ - isReady: false - })) - const mocks = [ - { - request: { query: GET_EXERCISES }, - result: { - data: getExercisesData - } - } - ] + it('should render no modules if there are none', async () => { + expect.assertions(1) render( - - + + ) - await waitFor(() => screen.getByText('Loading...')) + // Helps the data to resolve + await act(() => new Promise(res => setTimeout(res, 0))) + + const dropdownBtn = screen.getByTestId('dropdown-lesson') + await userEvent.click(dropdownBtn) + + const dropdownItem = screen.queryByText('module1') + + expect(dropdownItem).not.toBeInTheDocument() }) }) diff --git a/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js b/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js index 2846cad78..cbaa654bf 100644 --- a/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js +++ b/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js @@ -1,306 +1,164 @@ -jest.mock('@sentry/nextjs') - import React from 'react' -import MentorPage from '../../../../../pages/curriculum/[lessonSlug]/mentor/addExercise/index.tsx' -import userEvent from '@testing-library/user-event' -import { render, screen, act } from '@testing-library/react' -import { MockedProvider } from '@apollo/client/testing' -import GET_APP from '../../../../../graphql/queries/getApp' -import ADD_EXERCISE from '../../../../../graphql/queries/addExercise' - -import * as Sentry from '@sentry/nextjs' - -import dummyLessonData from '../../../../../__dummy__/lessonData' -import dummySessionData from '../../../../../__dummy__/sessionData' -import dummyAlertData from '../../../../../__dummy__/alertData' - -// Imported to be able to use expect(...).toBeInTheDocument() +import { render, waitFor, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' - -const getAppQueryMock = { - request: { query: GET_APP }, - result: { - data: { - session: dummySessionData, - lessons: [ - { - ...dummyLessonData[0], - id: 1 - }, - ...dummyLessonData - ], - alerts: dummyAlertData - } - } -} - -const getAppQueryMockWithNoModules = { - request: { query: GET_APP }, - result: { - data: { - session: dummySessionData, - lessons: [ - { - ...dummyLessonData[0], - modules: [], - id: 1 +import Exercises from '../../../../../pages/curriculum/[lessonSlug]/mentor' +import { useRouter } from 'next/router' +import { MockedProvider } from '@apollo/client/testing' +import getExercisesData from '../../../../../__dummy__/getExercisesData' +import GET_EXERCISES from '../../../../../graphql/queries/getExercises' + +describe('Exercises page', () => { + const { query, push } = useRouter() + query['lessonSlug'] = 'js0' + + test('Should render correctly', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData } - ], - alerts: dummyAlertData - } - } -} - -const fakeExercise = { - moduleId: -1, - description: 'exercise desc', - answer: 'x', - explanation: 'because x is the answer' -} - -const addExerciseMutationMock = { - request: { - query: ADD_EXERCISE, - variables: { ...fakeExercise, moduleId: 1 } - }, - result: jest.fn(() => ({ - data: { - addExercise: { ...fakeExercise } - } - })) -} - -const addExerciseMutationMockSuccess = { - request: { - query: ADD_EXERCISE, - variables: { ...fakeExercise, moduleId: 1 } - }, - result: jest.fn(() => ({ - data: { - addExercise: { ...fakeExercise, moduleId: 1 } - } - })) -} - -const addExerciseMutationMockError = { - request: { - query: ADD_EXERCISE, - variables: { ...fakeExercise, moduleId: 1 } - }, - error: new Error('Error') -} - -const mocks = [getAppQueryMock, addExerciseMutationMock] -const mocksWithError = [getAppQueryMock, addExerciseMutationMockError] -const mocksWithSuccess = [getAppQueryMock, addExerciseMutationMockSuccess] -const mocksWithNoModules = [ - getAppQueryMockWithNoModules, - addExerciseMutationMock -] - -const useRouter = jest.spyOn(require('next/router'), 'useRouter') -const useRouterObj = { - asPath: 'c0d3.com/curriculum/js1/mentor', - query: { - lessonSlug: 'js1' - }, - push: jest.fn() -} - -useRouter.mockImplementation(() => useRouterObj) - -const fillOutExerciseForms = async () => { - const [description, explanation] = screen.getAllByTestId('textbox') - const answer = screen.getByTestId('input1') - - // the type event needs to be delayed so the Formik validations finish - await userEvent.type(description, fakeExercise.description, { delay: 1 }) - await userEvent.type(answer, fakeExercise.answer, { delay: 1 }) - await userEvent.type(explanation, fakeExercise.explanation, { delay: 1 }) -} - -describe('Mentor page', () => { - it('should not submit when inputs are empty', async () => { - expect.assertions(1) + } + ] render( - - + + ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - const dropdownBtn = screen.getByTestId('dropdown-lesson') - await userEvent.click(dropdownBtn) - - const dropdownItem = screen.getByText('module1') - await userEvent.click(dropdownItem) - - const submitButton = await screen.findByText('Save exercise') - await userEvent.click(submitButton) - - expect(screen.queryAllByText('Required')[0]).toBeInTheDocument() - }) - - it('should render Mentor page', async () => { - render( - - - + await waitFor(() => + screen.getByRole('heading', { name: /Foundations of JavaScript/i }) ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - expect(screen.queryAllByText('Select a module')[0]).toBeInTheDocument() + screen.getByRole('link', { name: 'CHALLENGES' }) + screen.getByRole('link', { name: 'EXERCISES' }) + screen.getByRole('link', { name: 'LESSON' }) }) - it('should fill out the inputs', async () => { - render( - - - - ) - - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - await fillOutExerciseForms() - - const [description, explanation] = screen.getAllByTestId('textbox') - const answer = screen.getByTestId('input1') - - expect(description.value).toBe(fakeExercise.description) - expect(explanation.value).toBe(fakeExercise.explanation) - expect(answer.value).toBe(fakeExercise.answer) - }) - - it('should add exercise (submit)', async () => { - expect.assertions(1) + test('Should push to addExercise page', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData + } + } + ] - render( - - + const { getByRole, queryByRole, getByLabelText } = render( + + ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - const dropdownBtn = screen.getByTestId('dropdown-lesson') - await userEvent.click(dropdownBtn) - - const dropdownItem = await screen.findByText('module1') - await userEvent.click(dropdownItem) + await waitFor(() => + getByRole('heading', { name: /Foundations of JavaScript/i }) + ) - await fillOutExerciseForms() + const solveExercisesButton = getByRole('button', { + name: 'ADD EXERCISE' + }) - const submitButton = screen.getByText('Save exercise') - await userEvent.click(submitButton) + fireEvent.click(solveExercisesButton) - expect( - await screen.findByText('Added the exercise successfully!') - ).toBeInTheDocument() + expect(push).toBeCalledWith('/curriculum/js0/mentor/addExercise') }) - it('should not add exercise (submit) if no module is selected', async () => { - expect.assertions(1) + test('Should not render lessons nav card tab if lesson docUrl is null', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: { + ...getExercisesData, + lessons: getExercisesData.lessons.map(lesson => ({ + ...lesson, + docUrl: null + })) + } + } + } + ] render( - - + + ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - await fillOutExerciseForms() - - const submitButton = screen.getByText('Save exercise') - await userEvent.click(submitButton) + await waitFor(() => + screen.getByRole('heading', { name: /Foundations of JavaScript/i }) + ) - expect( - await screen.findByText('Please select a module') - ).toBeInTheDocument() + screen.getByRole('link', { name: 'CHALLENGES' }) + screen.getByRole('link', { name: 'EXERCISES' }) + expect(screen.queryByRole('link', { name: 'LESSONS' })).toBeNull() }) - it('should set error when adding an exercise', async () => { - expect.assertions(2) + test('Should render a 500 error page if the lesson data is null', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: { + ...getExercisesData, + lessons: null + } + } + } + ] render( - - + + ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - const dropdownBtn = screen.getByTestId('dropdown-lesson') - await userEvent.click(dropdownBtn) - - const dropdownItem = screen.queryByText('module1') - await userEvent.click(dropdownItem) - - await fillOutExerciseForms() - - const submitButton = screen.queryByText('Save exercise') - await userEvent.click(submitButton) - - expect( - await screen.findByText('An error occurred. Please try again.') - ).toBeInTheDocument() - expect(Sentry.captureException).toBeCalled() + await waitFor(() => screen.getByRole('heading', { name: /500 Error/i })) }) - it('should successfully add an exercise', async () => { - expect.assertions(1) + test('Should render a 404 error page if the lesson is not found', async () => { + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: { + ...getExercisesData, + lessons: [] + } + } + } + ] render( - - + + ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - const dropdownBtn = screen.getByTestId('dropdown-lesson') - await userEvent.click(dropdownBtn) - - const dropdownItem = await screen.findByText('module1') - await userEvent.click(dropdownItem) - - await fillOutExerciseForms() - - const submitButton = screen.queryByText('Save exercise') - await userEvent.click(submitButton) - - expect( - await screen.findByText('Added the exercise successfully!') - ).toBeInTheDocument() + await waitFor(() => screen.getByRole('heading', { name: /404 Error/i })) }) - it('should render no modules if there are none', async () => { - expect.assertions(1) + test('Should render a loading spinner if useRouter is not ready', async () => { + useRouter.mockImplementation(() => ({ + isReady: false + })) + const mocks = [ + { + request: { query: GET_EXERCISES }, + result: { + data: getExercisesData + } + } + ] render( - - + + ) - // Helps the data to resolve - await act(() => new Promise(res => setTimeout(res, 0))) - - const dropdownBtn = screen.getByTestId('dropdown-lesson') - await userEvent.click(dropdownBtn) - - const dropdownItem = screen.queryByText('module1') - - expect(dropdownItem).not.toBeInTheDocument() + await waitFor(() => screen.getByText('Loading...')) }) }) From 441745bf60d0383a9027f8a5007337e94cab3928 Mon Sep 17 00:00:00 2001 From: Flacial Date: Thu, 29 Sep 2022 23:05:40 +0400 Subject: [PATCH 5/5] style: Rename import --- .../curriculum/[lessonSlug]/mentor/index.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js b/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js index cbaa654bf..84ab39bb8 100644 --- a/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js +++ b/__tests__/pages/curriculum/[lessonSlug]/mentor/index.test.js @@ -1,13 +1,13 @@ import React from 'react' import { render, waitFor, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' -import Exercises from '../../../../../pages/curriculum/[lessonSlug]/mentor' +import AddExercises from '../../../../../pages/curriculum/[lessonSlug]/mentor' import { useRouter } from 'next/router' import { MockedProvider } from '@apollo/client/testing' import getExercisesData from '../../../../../__dummy__/getExercisesData' import GET_EXERCISES from '../../../../../graphql/queries/getExercises' -describe('Exercises page', () => { +describe('AddExercises page', () => { const { query, push } = useRouter() query['lessonSlug'] = 'js0' @@ -23,7 +23,7 @@ describe('Exercises page', () => { render( - + ) @@ -48,7 +48,7 @@ describe('Exercises page', () => { const { getByRole, queryByRole, getByLabelText } = render( - + ) @@ -83,7 +83,7 @@ describe('Exercises page', () => { render( - + ) @@ -111,7 +111,7 @@ describe('Exercises page', () => { render( - + ) @@ -133,7 +133,7 @@ describe('Exercises page', () => { render( - + ) @@ -155,7 +155,7 @@ describe('Exercises page', () => { render( - + )