Skip to content

Commit

Permalink
Development: Add e2e tests for programming exercise teams (ls1intum#8532
Browse files Browse the repository at this point in the history
)
  • Loading branch information
muradium authored May 5, 2024
1 parent c06c626 commit c730642
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Course } from 'app/entities/course.model';
import { ProgrammingExercise } from 'app/entities/programming-exercise.model';

import { admin } from '../../../support/users';
import { admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor } from '../../../support/users';
import { test } from '../../../support/fixtures';
import { generateUUID } from '../../../support/utils';
import { Exercise } from 'app/entities/exercise.model';
import { expect } from '@playwright/test';
import { Exercise, ExerciseMode } from '../../../support/constants';

test.describe('Programming Exercise Management', () => {
let course: Course;
Expand Down Expand Up @@ -52,6 +52,54 @@ test.describe('Programming Exercise Management', () => {
});
});

test.describe('Programming exercise team creation', () => {
let exercise: ProgrammingExercise;

test.beforeEach('Setup team programming exercise', async ({ login, exerciseAPIRequests }) => {
await login(admin);
const teamAssignmentConfig = { minTeamSize: 2, maxTeamSize: 3 };
exercise = await exerciseAPIRequests.createProgrammingExercise({
course,
mode: ExerciseMode.TEAM,
teamAssignmentConfig,
});
});

test('Create an exercise team', async ({ login, page, navigationBar, courseManagement, courseManagementExercises, exerciseTeams, programmingExerciseOverview }) => {
await login(instructor, '/');
await navigationBar.openCourseManagement();
await courseManagement.openExercisesOfCourse(course.id!);
await courseManagementExercises.openExerciseTeams(exercise.id!);
await page.getByRole('table').waitFor({ state: 'visible' });
await exerciseTeams.createTeam();

const teamId = generateUUID();
const teamName = `Team ${teamId}`;
const teamShortName = `team${teamId}`;
await exerciseTeams.enterTeamName(teamName);
await exerciseTeams.enterTeamShortName(teamShortName);
await exerciseTeams.setTeamTutor(tutor.username);

await exerciseTeams.addStudentToTeam(studentOne.username);
await expect(exerciseTeams.getIgnoreTeamSizeRecommendationCheckbox()).toBeVisible();
await expect(exerciseTeams.getSaveButton()).toBeDisabled();
await exerciseTeams.getIgnoreTeamSizeRecommendationCheckbox().check();
await expect(exerciseTeams.getSaveButton()).toBeEnabled();
await exerciseTeams.getIgnoreTeamSizeRecommendationCheckbox().uncheck();

await exerciseTeams.addStudentToTeam(studentTwo.username);
await exerciseTeams.addStudentToTeam(studentThree.username);
await expect(exerciseTeams.getSaveButton()).toBeEnabled();
await exerciseTeams.getSaveButton().click();
await exerciseTeams.checkTeamOnList(teamShortName);

await login(studentOne, `/courses/${course.id}/exercises/${exercise.id}`);
await expect(programmingExerciseOverview.getExerciseDetails().locator('.view-team')).toBeVisible();
await login(studentFour, `/courses/${course.id}/exercises/${exercise.id}`);
await expect(programmingExerciseOverview.getExerciseDetails()).toHaveText(/No team yet/);
});
});

test.afterEach('Delete course', async ({ courseManagementAPIRequests }) => {
await courseManagementAPIRequests.deleteCourse(course, admin);
});
Expand Down
6 changes: 6 additions & 0 deletions src/test/playwright/support/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export type Exercise = {
exerciseGroup?: ExerciseGroup;
};

// ExerciseMode
export enum ExerciseMode {
INDIVIDUAL = 'INDIVIDUAL',
TEAM = 'TEAM',
}

// Exercise commit entity displayed in commit history
export type ExerciseCommit = {
message: string;
Expand Down
9 changes: 7 additions & 2 deletions src/test/playwright/support/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { ProgrammingExerciseOverviewPage } from './pageobjects/exercises/program
import { RepositoryPage } from './pageobjects/exercises/programming/RepositoryPage';
import { ExamGradingPage } from './pageobjects/exam/ExamGradingPage';
import { ExamScoresPage } from './pageobjects/exam/ExamScoresPage';
import { ExerciseTeamsPage } from './pageobjects/exercises/ExerciseTeamsPage';

/*
* Define custom types for fixtures
Expand Down Expand Up @@ -123,6 +124,7 @@ export type ArtemisPageObjects = {
textExerciseExampleSubmissionCreation: TextExerciseExampleSubmissionCreationPage;
textExerciseFeedback: TextExerciseFeedbackPage;
exerciseResult: ExerciseResultPage;
exerciseTeams: ExerciseTeamsPage;
};

export type ArtemisRequests = {
Expand Down Expand Up @@ -271,8 +273,8 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe
programmingExerciseCreation: async ({ page }, use) => {
await use(new ProgrammingExerciseCreationPage(page));
},
programmingExerciseEditor: async ({ page, courseList, courseOverview }, use) => {
await use(new OnlineEditorPage(page, courseList, courseOverview));
programmingExerciseEditor: async ({ page }, use) => {
await use(new OnlineEditorPage(page));
},
programmingExerciseFeedback: async ({ page }, use) => {
await use(new ProgrammingExerciseFeedbackPage(page));
Expand Down Expand Up @@ -319,6 +321,9 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe
exerciseResult: async ({ page }, use) => {
await use(new ExerciseResultPage(page));
},
exerciseTeams: async ({ page }, use) => {
await use(new ExerciseTeamsPage(page));
},
courseManagementAPIRequests: async ({ page }, use) => {
await use(new CourseManagementAPIRequests(page));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,10 @@ export class CourseManagementExercisesPage {
getModelingExerciseMaxPoints(exerciseID: number) {
return this.page.locator(`#exercise-card-${exerciseID}`).locator(`#modeling-exercise-${exerciseID}-maxPoints`);
}

async openExerciseTeams(exerciseId: number) {
const exerciseElement = this.getExercise(exerciseId);
const teamsButton = exerciseElement.locator('.btn', { hasText: 'Teams' });
await teamsButton.click();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,11 @@ export class CourseOverviewPage {
async openExam(examId: number): Promise<void> {
await this.page.locator(`#exam-${examId} .clickable`).click();
}

/**
* Opens the team info for the exercise.
*/
async openTeam() {
await this.page.locator('.view-team').click();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Page } from 'playwright';
import { expect } from '@playwright/test';

/**
* Page object for the exercise teams page.
*/
export class ExerciseTeamsPage {
private readonly page: Page;

constructor(page: Page) {
this.page = page;
}

/**
* Clicks the "Create team" button.
*/
async createTeam() {
await this.page.locator('button', { hasText: 'Create team' }).click();
}

/**
* Enters the team name.
* @param teamName - the team name.
*/
async enterTeamName(teamName: string) {
await this.page.locator('#teamName').fill(teamName);
}

/**
* Enters the team short name.
* @param teamShortName - the team short name.
*/
async enterTeamShortName(teamShortName: string) {
await this.page.locator('#teamShortName').fill(teamShortName);
}

/**
* Sets the team owner/tutor.
* @param username - the tutor username.
*/
async setTeamTutor(username: string) {
const tutorSearchInput = this.page.locator('#owner-search-input');
await tutorSearchInput.fill(username);
await this.page.getByRole('listbox').waitFor({ state: 'visible' });
await tutorSearchInput.press('Enter');
}

/**
* Adds a student to the team.
* @param username - the student username.
*/
async addStudentToTeam(username: string) {
const studentSearchInput = this.page.locator('#student-search-input');
await studentSearchInput.fill(username);
await this.page.getByRole('listbox').waitFor({ state: 'visible' });
await studentSearchInput.press('Enter');
}

/**
* Checks if the team is on the list of teams.
* @param teamShortName - the team short name.
*/
async checkTeamOnList(teamShortName: string) {
await expect(this.page.getByRole('table').getByRole('row').getByText(teamShortName)).toBeVisible();
}

/**
* Retrieves the Locator to ignore team size recommendation checkbox.
*/
getIgnoreTeamSizeRecommendationCheckbox() {
return this.page.locator('#ignoreTeamSizeRecommendation');
}

/**
* Retrieves the Locator for the save button.
*/
getSaveButton() {
return this.page.locator('button', { hasText: 'Save' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,11 @@ export class ProgrammingExerciseOverviewPage {
await this.page.locator('a', { hasText: 'Open repository' }).click();
return await repositoryPage;
}

/**
* Retrieves the Locator for the exercise details bar.
*/
getExerciseDetails() {
return this.page.locator('.tab-bar-exercise-details');
}
}
14 changes: 11 additions & 3 deletions src/test/playwright/support/requests/ExerciseAPIRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
BASE_API,
COURSE_BASE,
EXERCISE_BASE,
Exercise,
ExerciseMode,
ExerciseType,
MODELING_EXERCISE_BASE,
PROGRAMMING_EXERCISE_BASE,
Expand All @@ -35,9 +37,9 @@ import { ModelingExercise } from 'app/entities/modeling-exercise.model';
import { ProgrammingExercise } from 'app/entities/programming-exercise.model';
import { FileUploadExercise } from 'app/entities/file-upload-exercise.model';
import { Participation } from 'app/entities/participation/participation.model';
import { Exercise } from 'app/entities/exercise.model';
import { Exam } from 'app/entities/exam.model';
import { StudentParticipation } from 'app/entities/participation/student-participation.model';
import { TeamAssignmentConfig } from 'app/entities/team-assignment-config.model';

export class ExerciseAPIRequests {
private readonly page: Page;
Expand Down Expand Up @@ -67,7 +69,7 @@ export class ExerciseAPIRequests {
async createProgrammingExercise(options: {
course?: Course;
exerciseGroup?: ExerciseGroup;
scaMaxPenalty?: number | null;
scaMaxPenalty?: number | undefined;
recordTestwiseCoverage?: boolean;
releaseDate?: dayjs.Dayjs;
dueDate?: dayjs.Dayjs;
Expand All @@ -77,11 +79,13 @@ export class ExerciseAPIRequests {
packageName?: string;
assessmentDate?: dayjs.Dayjs;
assessmentType?: ProgrammingExerciseAssessmentType;
mode?: ExerciseMode;
teamAssignmentConfig?: TeamAssignmentConfig;
}): Promise<ProgrammingExercise> {
const {
course,
exerciseGroup,
scaMaxPenalty = null,
scaMaxPenalty = undefined,
recordTestwiseCoverage = false,
releaseDate = dayjs(),
dueDate = dayjs().add(1, 'day'),
Expand All @@ -91,6 +95,8 @@ export class ExerciseAPIRequests {
packageName = 'de.test',
assessmentDate = dayjs().add(2, 'days'),
assessmentType = ProgrammingExerciseAssessmentType.AUTOMATIC,
mode = ExerciseMode.INDIVIDUAL,
teamAssignmentConfig,
} = options;

let programmingExerciseTemplate = {};
Expand Down Expand Up @@ -127,6 +133,8 @@ export class ExerciseAPIRequests {

exercise.programmingLanguage = programmingLanguage;
exercise.testwiseCoverageEnabled = recordTestwiseCoverage;
exercise.mode = mode;
exercise.teamAssignmentConfig = teamAssignmentConfig;

const response = await this.page.request.post(`${PROGRAMMING_EXERCISE_BASE}/setup`, { data: exercise });
return response.json();
Expand Down

0 comments on commit c730642

Please sign in to comment.