Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#12588] Add tests for student list component #12854

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
});
Loading