Skip to content

Commit bba5534

Browse files
feat: Grade student (#263)
* feat: Add form to allow teachers to leave a comment in students' grades * feat: Allow teachers to select criteria from rubric to grade students * feat(ui): Create component to be shown when no rubric have been chosen * feat: Add button to allow teachers to go back to the grades table * feat: Show students' submissions to teachers * feat: Allow teachers to download students' code * chore(deps): Remove extra font A mono-space font was being loaded to display the `stdout` of the tests but it wasn't necessary at all. * feat: Allow students to download their code * test: Add test to ensure students can submit to tests blocks * test: Add test to ensure teachers can grade students * test: Add test to ensure teachers can download students' code * chore(deps): Bump dependencies
1 parent a501b11 commit bba5534

33 files changed

+1700
-135
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ dist-ssr
2929
# Test data
3030
e2e/laboratories/data/java-downloaded.zip
3131
e2e/laboratories/data/Test block name.zip
32+
e2e/grades/data/downloaded-java-tests.zip

e2e/grades/data/java-tests.zip

5.06 KB
Binary file not shown.

e2e/grades/grades-workflow.spec.ts

Lines changed: 346 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ import {
66
getRandomName,
77
getRandomUniversityID
88
} from "e2e/Utils";
9+
import { dirname, join } from "path";
10+
import { fileURLToPath } from "url";
11+
12+
const __filename = fileURLToPath(import.meta.url);
13+
const __dirname = dirname(__filename);
914

1015
test.describe.serial("Grades workflow", () => {
1116
const teacherEmail = getRandomEmail();
1217

1318
const courseName = "Test course";
19+
const rubricName = "Test rubric";
1420
const laboratoryName = "Test laboratory";
21+
const testBlockName = "Test block name";
1522

1623
const studentFullName = getRandomName();
1724
const studentEmail = getRandomEmail();
@@ -43,6 +50,20 @@ test.describe.serial("Grades workflow", () => {
4350
await page.getByRole("button", { name: "Submit" }).click();
4451
});
4552

53+
test("Create test rubric", async ({ page }) => {
54+
// Login as a teacher
55+
await page.goto("/login");
56+
await page.getByLabel("Email").fill(teacherEmail);
57+
await page.getByLabel("Password").fill(getDefaultPassword());
58+
await page.getByRole("button", { name: "Submit" }).click();
59+
60+
// Create a rubric
61+
await page.getByRole("link", { name: "Rubrics", exact: true }).click();
62+
await page.getByRole("button", { name: "Create rubric" }).click();
63+
await page.getByLabel("Name").fill("Test rubric");
64+
await page.getByRole("button", { name: "Create" }).click();
65+
});
66+
4667
test("Create test course", async ({ page }) => {
4768
// Login as a teacher
4869
await page.goto("/login");
@@ -78,6 +99,89 @@ test.describe.serial("Grades workflow", () => {
7899
await page.getByLabel("Opening date").fill("2023-12-01T00:00");
79100
await page.getByLabel("Closing date").fill("2032-12-01T23:59");
80101
await page.getByRole("button", { name: "Create" }).click();
102+
103+
// Select the rubric
104+
const editLaboratoryButton = page.getByRole("link", {
105+
name: `Edit ${laboratoryName} laboratory`
106+
});
107+
await expect(editLaboratoryButton).toBeVisible();
108+
await editLaboratoryButton.click();
109+
110+
const rubricSelect = page.getByRole("combobox", { name: "Rubric" });
111+
await expect(rubricSelect).toBeVisible();
112+
await rubricSelect.click();
113+
114+
const rubricOption = page.getByLabel(rubricName);
115+
await expect(rubricOption).toBeVisible();
116+
await rubricOption.click();
117+
118+
// Save the changes
119+
const saveButton = page.getByRole("button", { name: "Save changes" });
120+
await expect(saveButton).toBeVisible();
121+
await saveButton.click();
122+
123+
// Assert a toast is shown
124+
await expect(
125+
page.getByText("Laboratory details updated successfully")
126+
).toBeVisible();
127+
});
128+
129+
test("Create test block", async ({ page }) => {
130+
// Login as a teacher
131+
await page.goto("/login");
132+
await page.getByLabel("Email").fill(teacherEmail);
133+
await page.getByLabel("Password").fill(getDefaultPassword());
134+
await page.getByRole("button", { name: "Submit" }).click();
135+
136+
// Go to the course page
137+
await page.getByRole("link", { name: courseName }).click();
138+
139+
// Enter to the edit page of the laboratory
140+
const editLaboratoryButton = page.getByRole("link", {
141+
name: `Edit ${laboratoryName} laboratory`
142+
});
143+
await expect(editLaboratoryButton).toBeVisible();
144+
await editLaboratoryButton.click();
145+
146+
// Add the test block
147+
const addTestBlockButton = page.getByRole("button", {
148+
name: "Add test block"
149+
});
150+
await addTestBlockButton.click();
151+
152+
// set the name
153+
const addTestBlockModal = page.getByRole("dialog");
154+
await addTestBlockModal.getByLabel("Name").fill(testBlockName);
155+
156+
// select the language
157+
const languageSelect = addTestBlockModal.getByRole("combobox", {
158+
name: "Language"
159+
});
160+
await expect(languageSelect).toBeVisible();
161+
languageSelect.click();
162+
163+
const javaOption = page.getByLabel("Java");
164+
await expect(javaOption).toBeVisible();
165+
await javaOption.click();
166+
167+
// Upload tests file
168+
const zipFile = join(__dirname, "data", "java-tests.zip");
169+
const zipFileInput = addTestBlockModal.getByLabel("Test file");
170+
171+
const fileChooserPromise = page.waitForEvent("filechooser");
172+
await zipFileInput.click();
173+
const fileChooser = await fileChooserPromise;
174+
await fileChooser.setFiles(zipFile);
175+
176+
// Submit the form
177+
const submitButton = page.getByRole("button", { name: "Add" });
178+
await expect(submitButton).toBeVisible();
179+
await submitButton.click();
180+
181+
// Assert the test block is shown
182+
const nameInput = page.getByLabel("Block name");
183+
await expect(nameInput).toBeVisible();
184+
await expect(nameInput).toHaveValue(testBlockName);
81185
});
82186

83187
test("Enroll student in test course", async ({ page }) => {
@@ -110,6 +214,81 @@ test.describe.serial("Grades workflow", () => {
110214
await studentButton.click();
111215
});
112216

217+
test("Student submits a solution", async ({ page }) => {
218+
// Login as a student
219+
await page.goto("/login");
220+
await page.getByLabel("Email").fill(studentEmail);
221+
await page.getByLabel("Password").fill(getDefaultPassword());
222+
await page.getByRole("button", { name: "Submit" }).click();
223+
224+
// Go to the course page
225+
await page.getByRole("link", { name: courseName }).click();
226+
227+
// Go to the laboratory page
228+
await page
229+
.getByRole("link", { name: `Complete ${laboratoryName} laboratory` })
230+
.click();
231+
232+
// Assert the test block is shown
233+
const testBlockNameInput = page.getByLabel("Test name");
234+
await expect(testBlockNameInput).toBeVisible();
235+
await expect(testBlockNameInput).toHaveValue(testBlockName);
236+
237+
// Upload the solution
238+
const solutionFile = join(__dirname, "data", "java-tests.zip");
239+
const solutionFileInput = page.getByLabel("Submission file");
240+
241+
const fileChooserPromise = page.waitForEvent("filechooser");
242+
await solutionFileInput.click();
243+
const fileChooser = await fileChooserPromise;
244+
await fileChooser.setFiles(solutionFile);
245+
246+
// Submit the form
247+
const submitButton = page.getByRole("button", { name: "Submit" });
248+
await expect(submitButton).toBeVisible();
249+
await submitButton.click();
250+
251+
// update the timeout
252+
const ONE_MINUTE_IN_MS = 60000;
253+
page.setDefaultTimeout(ONE_MINUTE_IN_MS);
254+
255+
// Assert the status phases are shown
256+
// Pending phase
257+
const pendingPhaseElm = page.getByTestId("pending-phase");
258+
await expect(pendingPhaseElm).toBeVisible();
259+
await expect(pendingPhaseElm).toHaveAttribute("data-reached", "true");
260+
261+
// Running phase
262+
const runningPhaseElm = page.getByTestId("running-phase");
263+
await expect(runningPhaseElm).toBeVisible();
264+
await expect(runningPhaseElm).toHaveAttribute("data-reached", "true");
265+
266+
// Ready phase
267+
const readyPhaseElm = page.getByTestId("ready-phase");
268+
await expect(readyPhaseElm).toBeVisible();
269+
await expect(readyPhaseElm).toHaveAttribute("data-reached", "true");
270+
271+
// TODO: Assert the student can download the submitted file
272+
/*
273+
const formTab = page.getByRole("tab", {
274+
name: "Test block 1 submission form",
275+
exact: true
276+
});
277+
await formTab.click();
278+
279+
const downloadButton = page.getByRole("button", {
280+
name: "Download current submission file for block number 1"
281+
});
282+
await expect(downloadButton).toBeVisible();
283+
284+
const downloadPromise = page.waitForEvent("download");
285+
286+
await downloadButton.click();
287+
const download = await downloadPromise;
288+
await download.saveAs(join(__dirname, "data", "downloaded-java-tests.zip"));
289+
*/
290+
});
291+
113292
test("Student appears in the grades list", async ({ page }) => {
114293
// Login as a teacher
115294
await page.goto("/login");
@@ -141,12 +320,177 @@ test.describe.serial("Grades workflow", () => {
141320
await expect(studentRow).toBeVisible();
142321

143322
await expect(
144-
studentRow.getByRole("cell", { name: studentFullName })
323+
studentRow.getByRole("cell", { name: studentFullName, exact: true })
145324
).toBeVisible();
146325
await expect(
147326
studentRow.getByRole("cell", {
148-
name: "N/A"
327+
name: "N/A",
328+
exact: true
329+
})
330+
).toBeVisible();
331+
await expect(
332+
studentRow.getByRole("link", {
333+
name: `Edit grade for student ${studentFullName}`,
334+
exact: true
149335
})
150336
).toBeVisible();
151337
});
338+
339+
test("Teacher can edit the grade", async ({ page }) => {
340+
// Login as a teacher
341+
await page.goto("/login");
342+
await page.getByLabel("Email").fill(teacherEmail);
343+
await page.getByLabel("Password").fill(getDefaultPassword());
344+
await page.getByRole("button", { name: "Submit" }).click();
345+
346+
// Go to the course page
347+
await page.getByRole("link", { name: courseName }).click();
348+
349+
// Go to the grades view of the laboratory
350+
const laboratoryRow = page.getByRole("row", {
351+
name: new RegExp(`^\\s*${laboratoryName}`),
352+
exact: true
353+
});
354+
await expect(laboratoryRow).toBeVisible();
355+
356+
const laboratoryGradesButton = laboratoryRow.getByRole("link", {
357+
name: `Go to the grades of ${laboratoryName}`,
358+
exact: true
359+
});
360+
await laboratoryGradesButton.click();
361+
362+
// Edit the grade
363+
const studentRow = page.getByRole("row", {
364+
name: new RegExp(`^\\s*${studentFullName}`),
365+
exact: true
366+
});
367+
await expect(studentRow).toBeVisible();
368+
369+
const editGradeButton = studentRow.getByRole("link", {
370+
name: `Edit grade for student ${studentFullName}`,
371+
exact: true
372+
});
373+
await editGradeButton.click();
374+
375+
// ## Select a criteria from the rubric
376+
let criteriaCard = page.getByRole("button", {
377+
name: "Select criteria 1 of objective 1",
378+
exact: true
379+
});
380+
await criteriaCard.click();
381+
382+
const criteriaWeightElm = page.getByLabel(
383+
"Criteria 1 of objective 1 weight"
384+
);
385+
expect(criteriaWeightElm).not.toBeNull();
386+
387+
const criteriaWeight = await criteriaWeightElm.getAttribute("value");
388+
expect(criteriaWeight).not.toBeNull();
389+
390+
// Assert a toast is shown
391+
await expect(page.getByText("Criteria has been selected")).toBeVisible();
392+
393+
// Assert the weight was added to the student grade
394+
const studentGradeInput = page.getByLabel("Grade", { exact: true });
395+
await expect(studentGradeInput).toBeVisible();
396+
await expect(studentGradeInput).toHaveValue(criteriaWeight!);
397+
398+
// ## Deselect the criteria
399+
criteriaCard = page.getByRole("button", {
400+
name: "De-select criteria 1 of objective 1",
401+
exact: true
402+
});
403+
await criteriaCard.click();
404+
405+
// Assert a toast is shown
406+
await expect(page.getByText("Criteria has been de-selected")).toBeVisible();
407+
408+
// Assert the weight was removed from the student grade
409+
await expect(studentGradeInput).toBeVisible();
410+
await expect(studentGradeInput).toHaveValue("0");
411+
412+
// ## Add a comment
413+
const commentInput = page.getByLabel("Comment");
414+
await expect(commentInput).toBeVisible();
415+
await commentInput.fill("This is a comment left by the teacher");
416+
417+
const updateCommentButton = page.getByRole("button", {
418+
name: "Update comment",
419+
exact: true
420+
});
421+
await updateCommentButton.click();
422+
423+
// Assert a toast is shown
424+
await expect(
425+
page.getByText("The comment has been set successfully")
426+
).toBeVisible();
427+
428+
// Select the criteria again
429+
criteriaCard = page.getByRole("button", {
430+
name: "Select criteria 1 of objective 1",
431+
exact: true
432+
});
433+
await criteriaCard.click();
434+
});
435+
436+
test("Teacher can download student submissions", async ({ page }) => {
437+
// Login as a teacher
438+
await page.goto("/login");
439+
await page.getByLabel("Email").fill(teacherEmail);
440+
await page.getByLabel("Password").fill(getDefaultPassword());
441+
await page.getByRole("button", { name: "Submit" }).click();
442+
443+
// Go to the course page
444+
await page.getByRole("link", { name: courseName }).click();
445+
446+
// Go to the grades view of the laboratory
447+
const laboratoryRow = page.getByRole("row", {
448+
name: new RegExp(`^\\s*${laboratoryName}`),
449+
exact: true
450+
});
451+
452+
const laboratoryGradesButton = laboratoryRow.getByRole("link", {
453+
name: `Go to the grades of ${laboratoryName}`,
454+
exact: true
455+
});
456+
await laboratoryGradesButton.click();
457+
458+
// Go to the grade of the student
459+
const studentRow = page.getByRole("row", {
460+
name: new RegExp(`^\\s*${studentFullName}`),
461+
exact: true
462+
});
463+
464+
const editGradeButton = studentRow.getByRole("link", {
465+
name: `Edit grade for student ${studentFullName}`,
466+
exact: true
467+
});
468+
469+
await editGradeButton.click();
470+
471+
// Click on the Submission tab
472+
const submissionTab = page.getByRole("tab", {
473+
name: "Submissions",
474+
exact: true
475+
});
476+
await submissionTab.click();
477+
478+
// Assert the teacher can download the submission
479+
const downloadButton = page.getByRole("button", {
480+
name: `Download code sent by the student for the ${testBlockName} test block`,
481+
exact: true
482+
});
483+
await expect(downloadButton).toBeVisible();
484+
485+
const downloadPromise = page.waitForEvent("download");
486+
487+
await downloadButton.click();
488+
const download = await downloadPromise;
489+
await download.saveAs(join(__dirname, "data", "downloaded-java-tests.zip"));
490+
491+
// Assert a toast is shown
492+
await expect(
493+
page.getByText("The submission archive has been downloaded successfully")
494+
).toBeVisible();
495+
});
152496
});

0 commit comments

Comments
 (0)