From d818c66900e8bb5df03be95a6d3ccfc26e11be7e Mon Sep 17 00:00:00 2001 From: Daniel Fusimoto Pires <84727721+FusiDaniel@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:33:00 -0300 Subject: [PATCH] test: reviews page (#177) --- .../CommentsList/CommentList.test.ts | 81 + .../ConceptsHorizontalChart.test.ts | 39 + .../ConceptsHorizontalChart.vue | 7 +- .../ConceptsPieChart/ConceptsPieChart.test.ts | 21 + .../FeedbackAlert/FeedbackAlert.vue | 1 + .../PendingReviewEnrollment.test.ts | 39 + .../PendingReviewEnrollment.vue | 2 +- .../PendingReviewEnrollmentList.test.ts | 40 + .../ReviewDialog/ReviewDialog.test.ts | 262 +++ .../components/ReviewDialog/ReviewDialog.vue | 84 +- .../ReviewsWelcome/ReviewsWelcome.test.ts | 22 + .../components/SearchBar/SearchBar.test.ts | 125 ++ .../src/components/SearchBar/SearchBar.vue | 2 + .../SingleComment/SingleComment.test.ts | 197 ++ .../SingleComment/SingleComment.vue | 33 +- .../SubjectReview/SubjectReview.test.ts | 119 ++ .../SubjectReview/SubjectReview.vue | 6 +- .../TeacherReview/TeacherReview.test.ts | 103 + apps/container/src/mocks/enrollments.ts | 210 +- apps/container/src/mocks/handlers.ts | 17 +- apps/container/src/mocks/reviews.ts | 1765 +++++++++++++++++ apps/container/src/test-utils.ts | 10 +- .../src/views/Admin/AdminView.test.ts | 11 + .../views/Confirmation/ConfirmationView.vue | 19 +- .../src/views/History/HistoryView.vue | 1 + .../src/views/Planning/PlanningView.test.ts | 11 + .../src/views/Recovery/RecoveryView.test.ts | 6 +- .../src/views/Reviews/ReviewsView.test.ts | 95 + .../container/src/views/SignUp/SignUpView.vue | 18 +- packages/services/src/comments.ts | 23 +- packages/services/src/reviews.ts | 14 +- packages/types/src/comments.ts | 18 +- packages/types/src/enrollments.ts | 1 + packages/types/src/subjects.ts | 8 +- packages/types/src/teachers.ts | 9 +- 35 files changed, 3272 insertions(+), 147 deletions(-) create mode 100644 apps/container/src/components/CommentsList/CommentList.test.ts create mode 100644 apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.test.ts create mode 100644 apps/container/src/components/ConceptsPieChart/ConceptsPieChart.test.ts create mode 100644 apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.test.ts create mode 100644 apps/container/src/components/PendingReviewEnrollmentList/PendingReviewEnrollmentList.test.ts create mode 100644 apps/container/src/components/ReviewDialog/ReviewDialog.test.ts create mode 100644 apps/container/src/components/ReviewsWelcome/ReviewsWelcome.test.ts create mode 100644 apps/container/src/components/SearchBar/SearchBar.test.ts create mode 100644 apps/container/src/components/SingleComment/SingleComment.test.ts create mode 100644 apps/container/src/components/SubjectReview/SubjectReview.test.ts create mode 100644 apps/container/src/components/TeacherReview/TeacherReview.test.ts create mode 100644 apps/container/src/mocks/reviews.ts create mode 100644 apps/container/src/views/Admin/AdminView.test.ts create mode 100644 apps/container/src/views/Planning/PlanningView.test.ts create mode 100644 apps/container/src/views/Reviews/ReviewsView.test.ts diff --git a/apps/container/src/components/CommentsList/CommentList.test.ts b/apps/container/src/components/CommentsList/CommentList.test.ts new file mode 100644 index 00000000..3daca06c --- /dev/null +++ b/apps/container/src/components/CommentsList/CommentList.test.ts @@ -0,0 +1,81 @@ +import { render, screen } from '@/test-utils'; +import { CommentsList } from '.'; +import { comments } from '@/mocks/reviews'; +import { server } from '@/mocks/server'; +import { HttpResponse, http } from 'msw'; + +describe('', () => { + test('render Comments List', async () => { + render(CommentsList, { + props: { + teacherId: '111', + selectedSubject: 'Todas as matérias', + }, + }); + expect( + await screen.findByText(RegExp(comments.data[0].comment, 'i')), + ).toBeInTheDocument(); + expect( + screen.getAllByText(RegExp(comments.data[0].subject.name, 'i'))[0], + ).toBeInTheDocument(); + expect( + screen.getByText(RegExp(comments.data[1].comment, 'i')), + ).toBeInTheDocument(); + expect( + screen.getAllByText(RegExp(comments.data[1].subject.name, 'i'))[0], + ).toBeInTheDocument(); + }); + test('render empty Comments List', async () => { + server.use( + http.get(`*/comments/*`, () => + HttpResponse.json({ + data: [], + total: 0, + }), + ), + ); + render(CommentsList, { + props: { + teacherId: '111', + selectedSubject: 'Todas as matérias', + }, + }); + expect( + await screen.findByText( + /Infelizmente, nenhum comentário foi encontrado/i, + ), + ).toBeInTheDocument(); + }); + test('show toaster when Fetching Teacher Data Error', async () => { + server.use( + http.get(`*/reviews/teachers/*`, () => + HttpResponse.json(null, { status: 500 }), + ), + ); + + render(CommentsList, { + props: { + teacherId: '111', + selectedSubject: 'Todas as matérias', + }, + }); + expect( + await screen.findByText('Erro ao carregar o(a) professor(a)'), + ).toBeInTheDocument(); + }); + test('show toaster when Fetching Comments Error', async () => { + server.use( + http.get(`*/comments/*`, () => HttpResponse.json(null, { status: 500 })), + ); + render(CommentsList, { + props: { + teacherId: '111', + selectedSubject: 'Todas as matérias', + }, + }); + + expect( + await screen.findByText('Erro ao carregar comentários'), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.test.ts b/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.test.ts new file mode 100644 index 00000000..f75a3284 --- /dev/null +++ b/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.test.ts @@ -0,0 +1,39 @@ +import { render, screen } from '@/test-utils'; +import { ConceptsHorizontalChart } from '.'; +import { subjectInfo } from '@/mocks/reviews'; + +describe('', () => { + test('render Concepts Horizontal Chart with regular found teacher', async () => { + render(ConceptsHorizontalChart, { + props: { + gradeData: subjectInfo.specific[2], + }, + }); + + const tooltipTextPercentage = `${ + subjectInfo.specific[2].distribution[0].conceito + }: ${( + (100 * subjectInfo.specific[2].distribution[0].count) / + subjectInfo.specific[2].count + ).toFixed(1)}%`; + + const tooltipTextAmount = `${subjectInfo.specific[2].distribution[0].count} notas`; + expect( + await screen.findByText(RegExp(tooltipTextPercentage, 'i')), + ).toBeInTheDocument(); + expect( + await screen.getByText(RegExp(tooltipTextAmount, 'i')), + ).toBeInTheDocument(); + }); + + test('render Concepts Horizontal Chart with untrustable threshold ', async () => { + render(ConceptsHorizontalChart, { + props: { + gradeData: subjectInfo.specific[1], + }, + }); + expect( + await screen.findByText('Dados sem muitas amostras'), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.vue b/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.vue index 2e5a8c0e..b0fc2384 100644 --- a/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.vue +++ b/apps/container/src/components/ConceptsHorizontalChart/ConceptsHorizontalChart.vue @@ -6,9 +6,10 @@ :key="grade.conceito" :hide-after="0" :content="` - ${grade.conceito}: ${grades[grade.conceito].toFixed(1)}% (${ - grade.count - } notas)`" + ${grade.conceito}: ${( + (100 * grades[grade.conceito]) / + gradeData.count + ).toFixed(1)}% (${grade.count} notas)`" > acc + curr, 0); + +describe('', () => { + test('render Concepts Pie Highchart', async () => { + render(ConceptsPieChart, { + props: { + grades: concepts, + }, + }); + + Object.values(concepts).forEach((concept) => { + expect( + screen.getByText(RegExp(((100 * concept) / total).toFixed(1), 'i')), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/apps/container/src/components/FeedbackAlert/FeedbackAlert.vue b/apps/container/src/components/FeedbackAlert/FeedbackAlert.vue index 4aaed266..49f29268 100644 --- a/apps/container/src/components/FeedbackAlert/FeedbackAlert.vue +++ b/apps/container/src/components/FeedbackAlert/FeedbackAlert.vue @@ -30,6 +30,7 @@ const props = defineProps({ onMounted(() => { ElMessage({ + showClose: true, message: props.text, type: props.type, duration: props.duration, diff --git a/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.test.ts b/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.test.ts new file mode 100644 index 00000000..775f6ff2 --- /dev/null +++ b/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.test.ts @@ -0,0 +1,39 @@ +import { render, screen } from '@/test-utils'; +import { PendingReviewEnrollment } from '.'; +import { enrollments } from '@/mocks/enrollments'; + +describe('', () => { + test('render the Pending Review Enrollment with a theorical only subject', async () => { + render(PendingReviewEnrollment, { + props: { + enrollment: enrollments[1], + }, + }); + expect( + await screen.findByText(enrollments[1].disciplina), + ).toBeInTheDocument(); + expect(screen.getByText('teoria')).toBeInTheDocument(); + }); + test('render the Pending Review Enrollment with a pratical only subject', async () => { + render(PendingReviewEnrollment, { + props: { + enrollment: enrollments[30], + }, + }); + expect( + await screen.findByText(enrollments[30].disciplina), + ).toBeInTheDocument(); + expect(screen.getByText('prática')).toBeInTheDocument(); + }); + test('render the Pending Review Enrollment with a theorical and pratical subject', async () => { + render(PendingReviewEnrollment, { + props: { + enrollment: enrollments[0], + }, + }); + expect( + await screen.findByText(enrollments[0].disciplina), + ).toBeInTheDocument(); + expect(screen.getByText('teoria e prática')).toBeInTheDocument(); + }); +}); diff --git a/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.vue b/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.vue index 4c3f96b6..7e80e78a 100644 --- a/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.vue +++ b/apps/container/src/components/PendingReviewEnrollment/PendingReviewEnrollment.vue @@ -64,7 +64,7 @@ import { Enrollment } from 'types'; const showDialog = ref(false); const conceptStyle = computed(() => ({ - backgroundColor: conceptsColor[props.enrollment.conceito ?? ''], + backgroundColor: conceptsColor[props.enrollment.conceito], height: '54px', width: '54px', fontSize: '34px', diff --git a/apps/container/src/components/PendingReviewEnrollmentList/PendingReviewEnrollmentList.test.ts b/apps/container/src/components/PendingReviewEnrollmentList/PendingReviewEnrollmentList.test.ts new file mode 100644 index 00000000..aa17ca4c --- /dev/null +++ b/apps/container/src/components/PendingReviewEnrollmentList/PendingReviewEnrollmentList.test.ts @@ -0,0 +1,40 @@ +import { render, screen, userEvent } from '@/test-utils'; +import { PendingReviewEnrollmentList } from '.'; +import { enrollments } from '@/mocks/enrollments'; +import { server } from '@/mocks/server'; +import { HttpResponse, http } from 'msw'; + +describe('', () => { + test('render Pending Review Enrollments List with a reviewable subjects', async () => { + render(PendingReviewEnrollmentList); + expect( + await screen.findByText(/Seus professores para avaliar/i), + ).toBeInTheDocument(); + expect( + await screen.findByText(enrollments[26].subject.name), + ).toBeInTheDocument(); + expect(screen.getByText('teoria')).toBeInTheDocument(); + expect(screen.getByText(enrollments[27].subject.name)).toBeInTheDocument(); + expect(screen.getByText('prática')).toBeInTheDocument(); + expect(screen.getByText(enrollments[28].subject.name)).toBeInTheDocument(); + expect(screen.getByText('teoria e prática')).toBeInTheDocument(); + }); + test('render Pending Review Enrollments List with error', async () => { + server.use( + http.get(`*/enrollments`, () => HttpResponse.json(null, { status: 500 })), + ); + render(PendingReviewEnrollmentList); + expect( + await screen.findByText('Erro ao buscar suas disciplinas cursadas'), + ).toBeInTheDocument(); + }); + test('open Review Dialog when click on Enrollment and close when click on clos', async () => { + render(PendingReviewEnrollmentList); + await userEvent.click( + screen.getByRole('button', { + name: RegExp(enrollments[26].subject.name, 'i'), + }), + ); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + }); +}); diff --git a/apps/container/src/components/ReviewDialog/ReviewDialog.test.ts b/apps/container/src/components/ReviewDialog/ReviewDialog.test.ts new file mode 100644 index 00000000..0c8e92c9 --- /dev/null +++ b/apps/container/src/components/ReviewDialog/ReviewDialog.test.ts @@ -0,0 +1,262 @@ +import { + render, + screen, + userEvent, + waitFor, + expectToasterToHaveText, +} from '@/test-utils'; +import { ReviewDialog } from '.'; +import { userCreateComment, userUpdateComment } from '@/mocks/reviews'; +import { enrollment } from '@/mocks/enrollments'; +import { formatSeason } from 'utils'; +import { server } from '@/mocks/server'; +import { HttpResponse, http } from 'msw'; +import { ElMessage } from 'element-plus'; + +const commentAreaPlaceholder = + 'Faça aqui um comentário em relação ao docente e sua disciplina.'; + +describe('', () => { + beforeEach(() => { + vitest.resetAllMocks(); + server.resetHandlers(); + ElMessage(''); + }); + + test.each([ + { tagName: 'teoria', subjectType: 'teoria' }, + { tagName: 'prática', subjectType: 'pratica' }, + ])( + 'render Review Dialog and be able to comment (%s)%#', + async ({ tagName, subjectType }) => { + server.use( + http.get(`*/enrollments/*`, () => { + return HttpResponse.json({ + ...enrollment, + comments: [], + teoria: {}, + pratica: {}, + [subjectType]: { + ...enrollment[subjectType as 'teoria' | 'pratica'], + comment: undefined, + }, + }); + }), + ); + render(ReviewDialog, { + props: { + enrollment: enrollment, + showDialog: true, + tags: [ + tagName, + formatSeason(enrollment.year + ':' + enrollment.quad), + ], + }, + }); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + expect(await screen.findByText(tagName)).toBeInTheDocument(); + await userEvent.type( + screen.getByPlaceholderText(commentAreaPlaceholder), + userCreateComment.comment, + ); + await userEvent.click(screen.getByRole('button', { name: /Enviar/i })); + await expectToasterToHaveText('Comentário enviado com sucesso'); + }, + ); + test.each([ + { tagName: 'teoria', commentAvaliable: ['teoria'] }, + { tagName: 'prática', commentAvaliable: ['pratica'] }, + { tagName: 'teoria e prática', commentAvaliable: ['teoria', 'pratica'] }, + ])( + 'render Review Dialog with user comment and be able to edit comment %s %#', + async ({ tagName, commentAvaliable }) => { + server.use( + http.get(`*/enrollments/*`, () => { + return HttpResponse.json({ + ...enrollment, + comments: commentAvaliable, + teoria: {}, + pratica: {}, + [commentAvaliable[0]]: { + ...enrollment[commentAvaliable[0] as 'teoria' | 'pratica'], + comment: { + ...enrollment[commentAvaliable[0] as 'teoria' | 'pratica'] + ?.comment, + comment: userCreateComment.comment, + }, + }, + }); + }), + ); + + render(ReviewDialog, { + props: { + enrollment: enrollment, + showDialog: true, + tags: [ + tagName, + formatSeason(enrollment.year + ':' + enrollment.quad), + ], + }, + }); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + await waitFor(() => + expect( + screen.getByPlaceholderText(commentAreaPlaceholder), + ).toHaveProperty('value', userCreateComment.comment), + ); + + await userEvent.clear( + screen.getByPlaceholderText(commentAreaPlaceholder), + ); + await userEvent.type( + screen.getByPlaceholderText(commentAreaPlaceholder), + userUpdateComment.comment, + ); + await userEvent.click( + screen.getByRole('button', { name: /Atualizar comentário/i }), + ); + + await expectToasterToHaveText('Comentário editado com sucesso'); + }, + ); + test.each([ + { tagName: 'teoria', commentAvaliable: ['teoria'] }, + { tagName: 'prática', commentAvaliable: ['pratica'] }, + { tagName: 'teoria e prática', commentAvaliable: ['teoria', 'pratica'] }, + ])( + 'render Review Dialog with theorical subject type and missing atributes %s %#', + async ({ tagName, commentAvaliable }) => { + render(ReviewDialog, { + props: { + enrollment: { + ...enrollment, + _id: undefined, + teoria: {}, + pratica: {}, + ...commentAvaliable.reduce( + (acc, comment) => ({ + ...acc, + [comment]: { + ...enrollment[comment as 'teoria' | 'pratica'], + _id: undefined, + name: undefined, + }, + }), + {}, + ), + }, + showDialog: true, + tags: [tagName], + }, + }); + + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText(tagName)).toBeInTheDocument(); + await expectToasterToHaveText( + 'Erro ao carregar as informações do professor desta disciplina', + ); + }, + ); + test('show error alert when fetching Teacher Enrollment Error', async () => { + server.use( + http.get(`*/enrollments/*`, () => + HttpResponse.json(null, { status: 500 }), + ), + ); + render(ReviewDialog, { + props: { + enrollment: enrollment, + showDialog: true, + tags: [], + }, + }); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + + await expectToasterToHaveText( + 'Erro ao carregar as informações do professor desta disciplina', + ); + }); + test('show error alert when create comment error', async () => { + server.use( + http.get(`*/enrollments/*`, () => { + return HttpResponse.json({ + ...enrollment, + comments: [], + teoria: {}, + pratica: {}, + }); + }), + http.post(`*/comments/*`, () => HttpResponse.json(null, { status: 500 })), + ); + render(ReviewDialog, { + props: { + enrollment: enrollment, + showDialog: true, + tags: ['teoria', formatSeason(enrollment.year + ':' + enrollment.quad)], + }, + }); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + expect(await screen.findByText('teoria')).toBeInTheDocument(); + await userEvent.type( + screen.getByPlaceholderText(commentAreaPlaceholder), + userCreateComment.comment, + ); + await userEvent.click(screen.getByRole('button', { name: /Enviar/i })); + await expectToasterToHaveText('Ocorreu um erro ao enviar o comentário'); + }); + test('show error alert when update comment error', async () => { + server.use( + http.put(`*/comments/*`, () => HttpResponse.json(null, { status: 500 })), + http.get(`*/enrollments/*`, () => { + return HttpResponse.json({ + ...enrollment, + comments: ['teoria'], + + pratica: {}, + teoria: { + ...enrollment.teoria, + comment: { + ...enrollment.teoria?.comment, + comment: userCreateComment.comment, + }, + }, + }); + }), + ); + render(ReviewDialog, { + props: { + enrollment: enrollment, + showDialog: true, + tags: ['teoria', formatSeason(enrollment.year + ':' + enrollment.quad)], + }, + }); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + await waitFor(() => + expect( + screen.getByPlaceholderText(commentAreaPlaceholder), + ).toHaveProperty('value', userCreateComment.comment), + ); + + await userEvent.clear(screen.getByPlaceholderText(commentAreaPlaceholder)); + await userEvent.type( + screen.getByPlaceholderText(commentAreaPlaceholder), + userUpdateComment.comment, + ); + await userEvent.click( + screen.getByRole('button', { name: /Atualizar comentário/i }), + ); + + await expectToasterToHaveText('Ocorreu um erro ao editar o comentário'); + }); + test('not render Dialog if props is false', async () => { + render(ReviewDialog, { + props: { + enrollment: enrollment, + showDialog: false, + tags: ['teoria', formatSeason(enrollment.year + ':' + enrollment.quad)], + }, + }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/container/src/components/ReviewDialog/ReviewDialog.vue b/apps/container/src/components/ReviewDialog/ReviewDialog.vue index 76250665..52304eb4 100644 --- a/apps/container/src/components/ReviewDialog/ReviewDialog.vue +++ b/apps/container/src/components/ReviewDialog/ReviewDialog.vue @@ -1,7 +1,7 @@