Skip to content

Commit

Permalink
[#12588] Add tests for student list component (#12854)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
cedricongjh and weiquu committed Mar 2, 2024
1 parent 30ac90b commit 0ed34c9
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 9 deletions.
159 changes: 158 additions & 1 deletion src/web/app/components/student-list/student-list.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<StudentListComponent>;
let simpleModalService: SimpleModalService;
let courseService: CourseService;
let statusMessageService: StatusMessageService;

const studentListRowModelBuilder = createBuilder<StudentListRowModel>({
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({
Expand All @@ -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();
});
Expand Down Expand Up @@ -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<void> = 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 <strong>${studentModel.student.email}</strong> 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<void> = 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 <strong>${studentModel.student.name}</strong>?`;
const expectedModalContent: string = 'Are you sure you want to remove '
+ `<strong>${studentModel.student.name}</strong> `
+ `from the course <strong>${component.courseId}?</strong>`;
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);
});
});
7 changes: 0 additions & 7 deletions src/web/app/components/student-list/student-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
10 changes: 9 additions & 1 deletion src/web/test-helpers/generic-builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Course, Instructor, JoinState } from '../types/api-output';
import { Course, Instructor, JoinState, Student } from '../types/api-output';

type GenericBuilder<T> = {
[K in keyof T]: (value: T[K]) => GenericBuilder<T>;
Expand Down Expand Up @@ -59,3 +59,11 @@ export const instructorBuilder = createBuilder<Instructor>({
name: '',
joinState: JoinState.JOINED,
});

export const studentBuilder = createBuilder<Student>({
courseId: 'exampleId',
email: 'examplestudent@gmail.com',
name: 'test-student',
teamName: 'test-team-name',
sectionName: 'test-section-name',
});

0 comments on commit 0ed34c9

Please sign in to comment.