From 0ed34c9756d8f83f3239c4a47c49a280fd39c57f Mon Sep 17 00:00:00 2001 From: Cedric Ong <67156011+cedricongjh@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:37:19 +0800 Subject: [PATCH] [#12588] Add tests for student list component (#12854) * add student to generic builder * add tests to student list * remove unused getAriaSort from student list * fix lint --------- Co-authored-by: Wei Qing <48304907+weiquu@users.noreply.github.com> --- .../student-list.component.spec.ts | 159 +++++++++++++++++- .../student-list/student-list.component.ts | 7 - src/web/test-helpers/generic-builder.ts | 10 +- 3 files changed, 167 insertions(+), 9 deletions(-) diff --git a/src/web/app/components/student-list/student-list.component.spec.ts b/src/web/app/components/student-list/student-list.component.spec.ts index adc9a68ad84..ff6ad68f3fd 100644 --- a/src/web/app/components/student-list/student-list.component.spec.ts +++ b/src/web/app/components/student-list/student-list.component.spec.ts @@ -1,18 +1,58 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { StudentListComponent } from './student-list.component'; +import { of, throwError } from 'rxjs'; +import { StudentListComponent, StudentListRowModel } from './student-list.component'; import { StudentListModule } from './student-list.module'; +import { CourseService } from '../../../services/course.service'; +import { SimpleModalService } from '../../../services/simple-modal.service'; +import { StatusMessageService } from '../../../services/status-message.service'; +import { createBuilder, studentBuilder } from '../../../test-helpers/generic-builder'; +import { createMockNgbModalRef } from '../../../test-helpers/mock-ngb-modal-ref'; import { JoinState } from '../../../types/api-output'; import { Pipes } from '../../pipes/pipes.module'; +import { SimpleModalType } from '../simple-modal/simple-modal-type'; import { TeammatesCommonModule } from '../teammates-common/teammates-common.module'; import { TeammatesRouterModule } from '../teammates-router/teammates-router.module'; describe('StudentListComponent', () => { let component: StudentListComponent; let fixture: ComponentFixture; + let simpleModalService: SimpleModalService; + let courseService: CourseService; + let statusMessageService: StatusMessageService; + + const studentListRowModelBuilder = createBuilder({ + student: studentBuilder.build(), + isAllowedToModifyStudent: true, + isAllowedToViewStudentInSection: true, + }); + + const getButtonGroupByStudentEmail = (email: string): DebugElement | null => { + const studentListDebugElement = fixture.debugElement; + if (studentListDebugElement) { + const studentRows = studentListDebugElement.queryAll(By.css('tbody tr')); + for (const row of studentRows) { + const emailSpan = row.query(By.css('td:nth-child(5) span')); + if (emailSpan && emailSpan.nativeElement.textContent.trim() === email) { + return row.query(By.css('tm-group-buttons')); + } + } + } + return null; + }; + + const getButtonByText = (buttonGroup: DebugElement | null, text: string): DebugElement | null => { + if (buttonGroup) { + const buttons = buttonGroup.queryAll(By.css('.btn')); + return buttons.find((button) => button.nativeElement.textContent.includes(text)) ?? null; + } + + return null; + }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -32,6 +72,9 @@ describe('StudentListComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(StudentListComponent); + simpleModalService = TestBed.inject(SimpleModalService); + courseService = TestBed.inject(CourseService); + statusMessageService = TestBed.inject(StatusMessageService); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -383,4 +426,118 @@ describe('StudentListComponent', () => { const sendInviteButton = buttons.find((button : any) => button.nativeElement.textContent.includes('Send Invite')); expect(sendInviteButton).toBeTruthy(); }); + + it('hasSection: should return true when there are sections in the course', () => { + const studentOne = studentBuilder.sectionName('None').build(); + const studentTwo = studentBuilder.sectionName('section-one').build(); + component.studentModels = [ + studentListRowModelBuilder.student(studentOne).build(), + studentListRowModelBuilder.student(studentTwo).build(), + ]; + + expect(component.hasSection()).toBe(true); + }); + + it('hasSection: should return false when there are no sections in the course', () => { + const studentOne = studentBuilder.sectionName('None').build(); + const studentTwo = studentBuilder.sectionName('None').build(); + component.studentModels = [ + studentListRowModelBuilder.student(studentOne).build(), + studentListRowModelBuilder.student(studentTwo).build(), + ]; + + expect(component.hasSection()).toBe(false); + }); + + it('openReminderModal: should display warning when reminding student to join course', async () => { + const promise: Promise = Promise.resolve(); + const mockModalRef = createMockNgbModalRef({}, promise); + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockReturnValue(mockModalRef); + + const reminderStudentFromCourseSpy = jest.spyOn(component, 'remindStudentFromCourse'); + + const student = studentBuilder.build(); + student.joinState = JoinState.NOT_JOINED; + const studentModel = studentListRowModelBuilder.student(student).build(); + component.enableRemindButton = true; + component.studentModels = [studentModel]; + + fixture.detectChanges(); + + const buttonGroup = getButtonGroupByStudentEmail(studentModel.student.email); + const sendInviteButton = getButtonByText(buttonGroup, 'Send Invite'); + + sendInviteButton?.nativeElement.click(); + + await promise; + + const expectedModalContent: string = `Usually, there is no need to use this feature because + TEAMMATES sends an automatic invite to students at the opening time of each session. + Send a join request to ${studentModel.student.email} anyway?`; + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenLastCalledWith('Send join request?', + SimpleModalType.INFO, expectedModalContent); + + expect(reminderStudentFromCourseSpy).toHaveBeenCalledWith(studentModel.student.email); + }); + + it('openDeleteModal: should display warning when deleting student from course', async () => { + const promise: Promise = Promise.resolve(); + const mockModalRef = createMockNgbModalRef({}, promise); + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockReturnValue(mockModalRef); + + const removeStudentFromCourseSpy = jest.spyOn(component, 'removeStudentFromCourse'); + + const studentModel = studentListRowModelBuilder.build(); + component.studentModels = [studentModel]; + + fixture.detectChanges(); + + const buttonGroup = getButtonGroupByStudentEmail(studentModel.student.email); + const deleteButton = getButtonByText(buttonGroup, 'Delete'); + + deleteButton?.nativeElement.click(); + + await promise; + + const expectedModalHeader = `Delete student ${studentModel.student.name}?`; + const expectedModalContent: string = 'Are you sure you want to remove ' + + `${studentModel.student.name} ` + + `from the course ${component.courseId}?`; + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenLastCalledWith(expectedModalHeader, + SimpleModalType.DANGER, expectedModalContent); + + expect(removeStudentFromCourseSpy).toHaveBeenCalledWith(studentModel.student.email); + expect(component.students).not.toContain(studentModel.student.email); + }); + + it('remindStudentFromCourse: should call statusMessageService.showSuccessToast with' + + 'correct message upon success', () => { + const successMessage = 'success'; + jest.spyOn(courseService, 'remindStudentForJoin') + .mockReturnValue(of({ message: successMessage })); + const studentEmail = 'testemail@gmail.com'; + + const statusMessageServiceSpy = jest.spyOn(statusMessageService, 'showSuccessToast'); + + component.remindStudentFromCourse(studentEmail); + + expect(statusMessageServiceSpy).toHaveBeenLastCalledWith(successMessage); + }); + + it('remindStudentFromCourse: should call statusMessageService.showErrorToast with correct message upon error', () => { + const errorMessage = 'error'; + jest.spyOn(courseService, 'remindStudentForJoin') + .mockReturnValue(throwError(() => ({ + error: { message: errorMessage }, + }))); + const studentEmail = 'testemail@gmail.com'; + + const statusMessageServiceSpy = jest.spyOn(statusMessageService, 'showErrorToast'); + + component.remindStudentFromCourse(studentEmail); + + expect(statusMessageServiceSpy).toHaveBeenLastCalledWith(errorMessage); + }); }); diff --git a/src/web/app/components/student-list/student-list.component.ts b/src/web/app/components/student-list/student-list.component.ts index f5ec316c51b..7c295957d88 100644 --- a/src/web/app/components/student-list/student-list.component.ts +++ b/src/web/app/components/student-list/student-list.component.ts @@ -254,11 +254,4 @@ export class StudentListComponent implements OnInit { sortStudentListEventHandler(event: { sortBy: SortBy, sortOrder: SortOrder }): void { this.sortStudentListEvent.emit(event); } - - getAriaSort(by: SortBy): string { - if (by !== this.tableSortBy) { - return 'none'; - } - return this.tableSortOrder === SortOrder.ASC ? 'ascending' : 'descending'; - } } diff --git a/src/web/test-helpers/generic-builder.ts b/src/web/test-helpers/generic-builder.ts index d4af11955bc..2f5e2863f39 100644 --- a/src/web/test-helpers/generic-builder.ts +++ b/src/web/test-helpers/generic-builder.ts @@ -1,4 +1,4 @@ -import { Course, Instructor, JoinState } from '../types/api-output'; +import { Course, Instructor, JoinState, Student } from '../types/api-output'; type GenericBuilder = { [K in keyof T]: (value: T[K]) => GenericBuilder; @@ -59,3 +59,11 @@ export const instructorBuilder = createBuilder({ name: '', joinState: JoinState.JOINED, }); + +export const studentBuilder = createBuilder({ + courseId: 'exampleId', + email: 'examplestudent@gmail.com', + name: 'test-student', + teamName: 'test-team-name', + sectionName: 'test-section-name', +});