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>): Promise { if (!response.ok()) { throw new Error(`${response.url()} -> ${response.status()}: ${await response.text()}`); } return response.json(); } async function setupAdmin(context: APIRequestContext): Promise { 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> { const response = await context[method](path, { data, headers: { Authorization: `Bearer ${token}` }, }); return (await expectOk(response)) as Record; } 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.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); }); 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.courseId}`); 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); });