- teacher/admin course pages, content, gradebook, profile, downloads - courseNo -> UUID только на границе REST API - обновлены e2e, шаблоны, тесты и docs - LDD-UI-002; baseline 59 claims / 385 тестов
243 lines
9.6 KiB
TypeScript
243 lines
9.6 KiB
TypeScript
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), 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);
|
||
});
|