feat(ldd): property-based, mutation и браузерный E2E

This commit is contained in:
2026-10-28 00:01:56 +03:00
parent 80ccf26b61
commit 905feb440b
18 changed files with 1432 additions and 2 deletions

188
e2e/lms.spec.ts Normal file
View File

@@ -0,0 +1,188 @@
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;
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), 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,
});
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),
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.courseId}`);
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}\\?courseId=`));
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.courseId}/students/`));
await expect(page.locator("body")).toContainText(seeded.studentFullName);
await page.goto(`/teacher/courses/${seeded.courseId}/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);
});