Files
LMS/e2e/lms.spec.ts

246 lines
9.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test, type APIRequestContext, type Page } from "@playwright/test";
const apiBase = process.env.LDD_API_URL ?? "http://localhost:8080";
type Seeded = {
courseId: string;
courseNo: number;
courseTitle: string;
groupId: string;
labId: string;
groupName: string;
studentFullName: string;
studentLogin: string;
studentPassword: string;
teacherLogin: string;
teacherPassword: string;
adminLogin: string;
adminPassword: string;
};
let seeded: Seeded;
let api: APIRequestContext;
async function expectOk(response: Awaited<ReturnType<APIRequestContext["post"]>>): Promise<unknown> {
if (!response.ok()) {
throw new Error(`${response.url()} -> ${response.status()}: ${await response.text()}`);
}
return response.json();
}
async function setupAdmin(context: APIRequestContext): Promise<string> {
const health = await context.get("/api/v1/auth/health");
const healthBody = (await health.json()) as { adminExists: boolean };
if (healthBody.adminExists) {
const login = await context.post("/api/v1/auth/login", {
data: { login: "e2e-admin", password: "e2e-admin-123" },
});
return ((await expectOk(login)) as { token: string }).token;
}
const setup = await context.post("/api/v1/auth/setup", {
data: { login: "e2e-admin", password: "e2e-admin-123", password2: "e2e-admin-123" },
});
return ((await expectOk(setup)) as { token: string }).token;
}
async function createWithToken(
context: APIRequestContext,
token: string,
method: "post",
path: string,
data: unknown,
): Promise<Record<string, unknown>> {
const response = await context[method](path, {
data,
headers: { Authorization: `Bearer ${token}` },
});
return (await expectOk(response)) as Record<string, unknown>;
}
async function changePassword(context: APIRequestContext, login: string, oldPassword: string, newPassword: string) {
const loginResponse = await context.post("/api/v1/auth/login", {
data: { login, password: oldPassword },
});
const token = ((await expectOk(loginResponse)) as { token: string }).token;
await expectOk(
await context.post("/api/v1/auth/change-password", {
headers: { Authorization: `Bearer ${token}` },
data: { old: oldPassword, new: newPassword, new2: newPassword },
}),
);
}
async function login(page: Page, login: string, password: string, expectedPath: RegExp | string) {
await page.goto("/login");
await page.fill("#login", login);
await page.fill("#password", password);
await Promise.all([
page.waitForURL(expectedPath, { waitUntil: "domcontentloaded", timeout: 60_000 }),
page.click('button[type="submit"]'),
]);
}
test.beforeAll(async ({ playwright }) => {
api = await playwright.request.newContext({ baseURL: apiBase });
const suffix = Math.random().toString(36).slice(2, 8);
const adminToken = await setupAdmin(api);
const group = await createWithToken(api, adminToken, "post", "/api/v1/groups", {
stream: "E2E" + suffix,
course: 1,
number: 1,
degree: "БО",
enrollmentYear: 2026,
});
const course = await createWithToken(api, adminToken, "post", "/api/v1/courses", {
title: "E2E Course " + suffix,
});
const teacher = await createWithToken(api, adminToken, "post", "/api/v1/teachers", {
login: "e2e-teacher-" + suffix,
password: "e2e-teacher-123",
});
const teacherLogin = String(teacher.login);
await createWithToken(api, adminToken, "post", `/api/v1/courses/${course.id}/teachers`, {
accountId: teacher.id,
});
await createWithToken(api, adminToken, "post", `/api/v1/courses/${course.id}/groups`, {
groupId: group.id,
});
const lab = await createWithToken(api, adminToken, "post", "/api/v1/labs", {
number: 1,
title: "E2E Lab",
status: "published",
courseId: course.id,
levels: [{ code: "basic", title: "Базовый", position: 1 }],
});
const studentNo = "E2E-S-" + suffix;
const student = await createWithToken(api, adminToken, "post", `/api/v1/groups/${group.id}/students`, {
studentNo,
lastName: "Тестов",
firstName: "Студент",
basis: "бюджет",
});
const studentPassword = String(student.password);
await changePassword(api, teacherLogin, "e2e-teacher-123", "e2e-teacher-456");
await changePassword(api, studentNo, studentPassword, "e2e-student-456");
seeded = {
courseId: String(course.id),
courseNo: Number(course.courseNo),
courseTitle: String(course.title),
groupId: String(group.id),
groupName: String(group.name),
labId: String(lab.id),
studentFullName: String(student.fullName),
studentLogin: studentNo,
studentPassword: "e2e-student-456",
teacherLogin,
teacherPassword: "e2e-teacher-456",
adminLogin: "e2e-admin",
adminPassword: "e2e-admin-123",
};
});
test.afterAll(async () => {
await api.dispose();
});
test("admin opens courses and audit", async ({ page }) => {
await login(page, seeded.adminLogin, seeded.adminPassword, "**/admin");
await page.goto("/admin/courses");
await expect(page.locator("body")).toContainText(seeded.courseTitle);
await page.goto("/admin/audit");
await expect(page.locator("body")).toContainText("Создан курс");
});
test("teacher opens course, gradebook, student profile and authors a chapter", async ({ page }) => {
await login(page, seeded.teacherLogin, seeded.teacherPassword, "**/teacher");
await expect(page.locator("body")).toContainText(seeded.courseTitle);
await page.goto(`/teacher/courses/${seeded.courseNo}`);
await expect(page.locator("h1")).toContainText(seeded.courseTitle);
await expect(page.locator("body")).toContainText(seeded.groupName);
await expect(page.locator("body")).toContainText("E2E Lab");
await page.getByRole("link", { name: "Ведомость" }).click();
await expect(page).toHaveURL(new RegExp(`/admin/groups/${seeded.groupId}\\?courseNo=`));
await expect(page.locator("body")).toContainText(seeded.groupName);
await expect(page.locator("body")).toContainText(seeded.studentFullName);
await expect(page.getByRole("button", { name: "Отчислить" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Сбросить" })).toHaveCount(0);
await page.getByRole("link", { name: seeded.studentFullName }).first().click();
await expect(page).toHaveURL(new RegExp(`/teacher/courses/${seeded.courseNo}/students/`));
await expect(page.locator("body")).toContainText(seeded.studentFullName);
await page.goto(`/teacher/courses/${seeded.courseNo}/content`);
await page.fill("#chapterTitle", "E2E Chapter");
await page.getByRole("button", { name: "Создать главу" }).click();
await expect(page.locator("body")).toContainText("E2E Chapter");
});
test("anonymous and student cannot reach admin pages", async ({ page }) => {
await page.goto("/admin/audit");
await expect(page).toHaveURL(/\/login/);
await login(page, seeded.studentLogin, seeded.studentPassword, "**/student");
await page.goto("/admin/audit");
await expect(page).toHaveURL(/\/student/);
await page.goto(`/learn/course/${seeded.courseNo}`);
await expect(page.locator("body")).toContainText(seeded.courseTitle);
});
test("student submits a solution and mobile teacher page renders", async ({ page }) => {
await login(page, seeded.studentLogin, seeded.studentPassword, "**/student");
await page.goto(`/student/labs/${seeded.labId}`);
await page.setInputFiles('input[name="files"]', "fixtures/main.c");
await page.getByRole("button", { name: "Отправить решение" }).click();
await expect(page.locator("body")).toContainText("Решение отправлено");
await expect(page.locator("body")).toContainText("main.c");
await page.context().clearCookies();
await page.setViewportSize({ width: 390, height: 844 });
await login(page, seeded.teacherLogin, seeded.teacherPassword, "**/teacher");
await page.goto(`/teacher/courses/${seeded.courseNo}`);
await expect(page.locator("h1")).toContainText(seeded.courseTitle);
await expect(page.locator("body")).toContainText(seeded.groupName);
});
test("admin paginates audit with cursor and returns to start", async ({ page }) => {
await login(page, seeded.adminLogin, seeded.adminPassword, "**/admin");
await page.goto("/admin/audit?limit=1");
await expect(page.locator("body")).toContainText("Страница 1");
const nextLink = page.getByRole("link", { name: "Показать ещё" });
await expect(nextLink).toBeVisible();
await expect(nextLink).toHaveAttribute("href", /cursor=/);
await nextLink.click();
await expect(page).toHaveURL(/cursor=/);
await expect(page.locator("body")).toContainText("Продолжение");
await expect(page.getByRole("link", { name: "В начало" })).toBeVisible();
await page.getByRole("link", { name: "Показать ещё" }).click();
await expect(page).toHaveURL(/cursor=/);
await expect(page.locator("body")).toContainText("Продолжение");
await expect(page.getByRole("link", { name: "В начало" })).toBeVisible();
await page.getByRole("link", { name: "В начало" }).click();
await expect(page).not.toHaveURL(/cursor=/);
await expect(page.locator("body")).toContainText("Страница 1");
});
test("admin opens legacy assets inventory", async ({ page }) => {
await login(page, seeded.adminLogin, seeded.adminPassword, "**/admin");
await page.goto("/admin/legacy-assets");
await expect(page.locator("h1")).toContainText("Активы");
await expect(page.locator("body")).toContainText("только администраторам");
const assetRow = page.locator("table tbody tr td.font-mono").first();
const empty = page.getByText("Бесхозных файлов нет.");
await expect(assetRow.or(empty)).toBeVisible();
await expect(page.locator(".toast-error")).toHaveCount(0);
});