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

Development: Fix flaky playwright e2e tests involving automatic exercise assessments #8740

Merged
merged 8 commits into from
Jun 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ <h5 class="mb-0" [id]="'exercise-group-title-' + exercise.id">
<fa-icon [ngClass]="exerciseInfo?.colorClass" [icon]="exerciseInfo?.resultIconClass!" size="lg" />
}
@if (exerciseInfo?.achievedPercentage !== undefined) {
<span [class]="exerciseInfo!.colorClass"> {{ exerciseInfo!.achievedPercentage }}% </span>
<span data-testid="achieved-percentage" [class]="exerciseInfo!.colorClass"> {{ exerciseInfo!.achievedPercentage }}% </span>
}
</div>
</div>
Expand Down
323 changes: 155 additions & 168 deletions src/test/playwright/e2e/exam/ExamResults.spec.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Fixtures } from '../../../fixtures/fixtures';
import { createFileWithContent } from '../../../support/utils';
import { ProgrammingExerciseSubmission } from '../../../support/pageobjects/exercises/programming/OnlineEditorPage';
import cAllSuccessful from '../../../fixtures/exercise/programming/c/all_successful/submission.json';
import { UserCredentials, admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor } from '../../../support/users';
import { UserCredentials, admin, instructor, studentFour, studentOne, studentTwo, tutor } from '../../../support/users';
import { Team } from 'app/entities/team.model';
import { ProgrammingExerciseOverviewPage } from '../../../support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage';
import { Participation } from 'app/entities/participation/participation.model';
Expand All @@ -27,6 +27,7 @@ test.describe('Programming exercise participation', () => {
await login(admin, '/');
course = await courseManagementAPIRequests.createCourse({ customizeGroups: true });
await courseManagementAPIRequests.addStudentToCourse(course, studentOne);
await courseManagementAPIRequests.addStudentToCourse(course, studentTwo);
});

const testCases = [
Expand Down Expand Up @@ -90,7 +91,6 @@ test.describe('Programming exercise participation', () => {
const submissions = [
{ student: studentOne, submission: javaBuildErrorSubmission, commitMessage: 'Initial commit' },
{ student: studentTwo, submission: javaPartiallySuccessfulSubmission, commitMessage: 'Initial implementation' },
{ student: studentThree, submission: javaAllSuccessfulSubmission, commitMessage: 'Implemented all tasks' },
];

test.beforeEach('Create team programming exercise', async ({ login, exerciseAPIRequests }) => {
Expand All @@ -107,7 +107,7 @@ test.describe('Programming exercise participation', () => {
test.beforeEach('Create an exercise team', async ({ login, userManagementAPIRequests, exerciseAPIRequests }) => {
await login(admin);
const students = await Promise.all(
[studentOne, studentTwo, studentThree].map(async (student) => {
[studentOne, studentTwo].map(async (student) => {
const response = await userManagementAPIRequests.getUser(student.username);
return response.json();
}),
Expand Down Expand Up @@ -182,7 +182,7 @@ test.describe('Programming exercise participation', () => {
});

test.describe('Check team participation', () => {
test.beforeEach('Each team member makes a submission', async ({ login, exerciseAPIRequests }) => {
test.beforeEach('Each team member makes a submission', async ({ login, waitForExerciseBuildToFinish, exerciseAPIRequests }) => {
for (const { student, submission } of submissions) {
await login(student);
const response = await exerciseAPIRequests.startExerciseParticipation(exercise.id!);
Expand All @@ -192,6 +192,7 @@ test.describe('Programming exercise participation', () => {
await exerciseAPIRequests.createProgrammingExerciseFile(participation.id!, filename);
}
await exerciseAPIRequests.makeProgrammingExerciseSubmission(participation.id!, submission);
await waitForExerciseBuildToFinish(exercise.id!);
}
});

Expand All @@ -203,8 +204,6 @@ test.describe('Programming exercise participation', () => {
programmingExerciseRepository,
programmingExerciseParticipations,
}) => {
// Marked test as slow as there are 3 builds being awaited
test.slow();
await login(instructor);
await navigationBar.openCourseManagement();
await courseManagement.openExercisesOfCourse(course.id!);
Expand Down
31 changes: 31 additions & 0 deletions src/test/playwright/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { UserCredentials } from './users';
import { BASE_API } from './constants';
import { Locator, Page, expect } from '@playwright/test';
import { StudentParticipation } from 'app/entities/participation/student-participation.model';
import { ExerciseAPIRequests } from './requests/ExerciseAPIRequests';

/**
* A class that encapsulates static helper command methods.
Expand Down Expand Up @@ -62,4 +64,33 @@ export class Commands {

throw new Error(`Timed out finding an element matching the "${locator}" locator`);
};

/**
* Waits for the build of an exercise to finish.
* Throws an error if the build does not finish within the timeout.
* @param page - Playwright page object.
* @param exerciseAPIRequests - ExerciseAPIRequests object.
* @param exerciseId - ID of the exercise to wait for.
* @param interval - Interval in milliseconds between checks for the build to finish.
* @param timeout - Timeout in milliseconds to wait for the build to finish.
*/
static waitForExerciseBuildToFinish = async (page: Page, exerciseAPIRequests: ExerciseAPIRequests, exerciseId: number, interval: number = 2000, timeout: number = 60000) => {
let exerciseParticipation: StudentParticipation;
const startTime = Date.now();

const numberOfBuildResults = (await exerciseAPIRequests.getExerciseParticipation(exerciseId)).results?.length;
console.log('Waiting for build of an exercise to finish...');

while (Date.now() - startTime < timeout) {
exerciseParticipation = await exerciseAPIRequests.getExerciseParticipation(exerciseId);

if (exerciseParticipation.results && exerciseParticipation.results.length > (numberOfBuildResults ?? 0)) {
return exerciseParticipation;
}

await new Promise((resolve) => setTimeout(resolve, interval));
}

throw new Error('Timed out while waiting for build to finish.');
};
}
6 changes: 6 additions & 0 deletions src/test/playwright/support/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { ExamParticipationActions } from './pageobjects/exam/ExamParticipationAc
*/
export type ArtemisCommands = {
login: (credentials: UserCredentials, url?: string) => Promise<void>;
waitForExerciseBuildToFinish: (exerciseId: number, interval?: number, timeout?: number) => Promise<void>;
};

export type ArtemisPageObjects = {
Expand Down Expand Up @@ -159,6 +160,11 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe
await Commands.login(page, credentials, url);
});
},
waitForExerciseBuildToFinish: async ({ page, exerciseAPIRequests }, use) => {
await use(async (exerciseId: number, interval?, timeout?) => {
await Commands.waitForExerciseBuildToFinish(page, exerciseAPIRequests, exerciseId, interval, timeout);
});
},
navigationBar: async ({ page }, use) => {
await use(new NavigationBar(page));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export class ExamParticipationActions {

async getResultScore(exerciseID?: number) {
const parentComponent = exerciseID ? getExercise(this.page, exerciseID) : this.page;
const resultScoreLocator = parentComponent.locator('#exercise-result-score');
await Commands.reloadUntilFound(this.page, resultScoreLocator);
const resultScoreLocator = parentComponent.getByTestId('achieved-percentage');
await Commands.reloadUntilFound(this.page, resultScoreLocator, 4000, 60000);
return resultScoreLocator;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ export class ExamResultsPage {
const achievedPoints = Math.floor(exerciseResult.achievedPoints).toString();
const achievablePoints = Math.floor(exerciseResult.maxScore).toString();
const achievedPercentage = exerciseResult.achievedScore.toString();
const bonusPoints = Math.floor(exercise.bonusPoints).toString();

await expect(exerciseRow.locator('td').nth(1).getByText(achievedPoints)).toBeVisible();
await expect(exerciseRow.locator('td').nth(2).getByText(achievablePoints)).toBeVisible();
await expect(exerciseRow.locator('td').nth(3).getByText(`${achievedPercentage} %`)).toBeVisible();
await expect(exerciseRow.locator('td').nth(4).getByText(bonusPoints)).toBeVisible();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class ExerciseResultPage {
}

async shouldShowScore(percentage: number) {
await Commands.reloadUntilFound(this.page, this.page.locator('jhi-course-exercise-details #submission-result-graded'));
await Commands.reloadUntilFound(this.page, this.page.locator('jhi-course-exercise-details #submission-result-graded'), 4000, 60000);
await expect(this.page.locator('.tab-bar-exercise-details').getByText(`${percentage}%`)).toBeVisible();
}

Expand Down
Loading