feat(ldd): property-based, mutation и браузерный E2E
This commit is contained in:
188
e2e/lms.spec.ts
Normal file
188
e2e/lms.spec.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user