Files
main_frontend/tests/e2e/specs/project/create-project.spec.ts
T
2026-04-04 14:51:40 +03:00

328 lines
10 KiB
TypeScript

import { expect, test } from "#tests/e2e/fixtures/projects"
interface CreateProjectRequestBody {
description?: string
language?: string
name?: string
}
function requirePostBody(
postBody: CreateProjectRequestBody | null,
): CreateProjectRequestBody {
if (!postBody) {
throw new Error("Expected create project request body to be captured")
}
return postBody
}
// Note: ReactModal sets aria-modal="true" on the dialog, which makes
// Playwright's getByRole() unable to find elements inside or outside the modal.
// All modal content is accessed via CSS locators scoped through `modal`.
test.describe("Create Project Modal", () => {
test.describe("Rendering & UI", () => {
test("should display create project modal with all fields", async ({
projectsPage,
}) => {
const { modal } = projectsPage
await projectsPage.openCreateModal()
await expect(modal.locator("h2")).toHaveText("Создать проект")
await expect(modal.locator("p").first()).toHaveText(
"Заполните основные поля проекта",
)
await expect(modal.locator("#project_name")).toHaveValue("")
await expect(modal.locator("#project_description")).toHaveValue("")
await expect(modal.locator("button", { hasText: "Отмена" })).toBeVisible()
await expect(
modal.locator("button", { hasText: "Создать" }),
).toBeVisible()
})
test("should show default language as Авто", async ({ projectsPage }) => {
const { modal } = projectsPage
await projectsPage.openCreateModal()
const selectTrigger = modal.locator("button").filter({ hasText: "Авто" })
await expect(selectTrigger).toBeVisible()
})
})
test.describe("Happy Path", () => {
test("should create project with name only", async ({ projectsPage }) => {
const { page, modal } = projectsPage
let postBody: CreateProjectRequestBody | null = null
await projectsPage.mockCreateSuccess({ name: "Мой проект" })
page.on("request", (req) => {
if (req.url().includes("/api/projects/") && req.method() === "POST") {
postBody = req.postDataJSON()
}
})
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Мой проект")
await modal.locator("button", { hasText: "Создать" }).click()
// Modal should close
await expect(modal).toBeHidden()
const requestBody = requirePostBody(postBody)
expect(requestBody.name).toBe("Мой проект")
expect(requestBody.language).toBe("auto")
})
test("should create project with name and description", async ({
projectsPage,
}) => {
const { page, modal } = projectsPage
let postBody: CreateProjectRequestBody | null = null
await projectsPage.mockCreateSuccess({
name: "Мой проект",
description: "Описание проекта",
})
page.on("request", (req) => {
if (req.url().includes("/api/projects/") && req.method() === "POST") {
postBody = req.postDataJSON()
}
})
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Мой проект")
await modal.locator("#project_description").fill("Описание проекта")
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal).toBeHidden()
const requestBody = requirePostBody(postBody)
expect(requestBody.description).toBe("Описание проекта")
})
test("should create project with different language", async ({
projectsPage,
}) => {
const { page, modal } = projectsPage
let postBody: CreateProjectRequestBody | null = null
await projectsPage.mockCreateSuccess({
name: "Мой проект",
language: "ru",
})
page.on("request", (req) => {
if (req.url().includes("/api/projects/") && req.method() === "POST") {
postBody = req.postDataJSON()
}
})
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Мой проект")
// Open language select and pick Russian
await modal.locator("button").filter({ hasText: "Авто" }).click()
await page.locator("[role=option]", { hasText: "Русский" }).click()
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal).toBeHidden()
const requestBody = requirePostBody(postBody)
expect(requestBody.language).toBe("ru")
})
test("should refresh projects list after creation", async ({
projectsPage,
}) => {
const { page, modal } = projectsPage
// Verify empty state
await expect(page.getByText("У вас пока нет проектов")).toBeVisible()
await projectsPage.mockCreateSuccess({ name: "Новый проект" })
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Новый проект")
await modal.locator("button", { hasText: "Создать" }).click()
// Modal closes and project appears in list
await expect(modal).toBeHidden()
await expect(page.getByText("У вас пока нет проектов")).toBeHidden()
await expect(page.getByText("Новый проект")).toBeVisible()
})
})
test.describe("Validation", () => {
test("should show validation error for empty name", async ({
projectsPage,
}) => {
const { page, modal } = projectsPage
let postFired = false
page.on("request", (req) => {
if (req.url().includes("/api/projects/") && req.method() === "POST") {
postFired = true
}
})
await projectsPage.openCreateModal()
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal.getByText("Введите название проекта")).toBeVisible()
expect(postFired).toBe(false)
// Modal stays open
await expect(modal).toBeVisible()
})
test("should show validation error for whitespace-only name", async ({
projectsPage,
}) => {
const { modal } = projectsPage
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill(" ")
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal.getByText("Введите название проекта")).toBeVisible()
})
test("should clear validation error after correcting name", async ({
projectsPage,
}) => {
const { modal } = projectsPage
await projectsPage.mockCreateSuccess({ name: "Проект" })
await projectsPage.openCreateModal()
// Trigger validation error
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal.getByText("Введите название проекта")).toBeVisible()
// Fix and resubmit
await modal.locator("#project_name").fill("Проект")
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal).toBeHidden()
})
})
test.describe("Error States", () => {
test("should keep modal open on server error (500)", async ({
projectsPage,
}) => {
const { page, modal } = projectsPage
const consoleErrors: string[] = []
page.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text())
}
})
await projectsPage.mockCreateError(500)
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Проект")
await modal.locator("button", { hasText: "Создать" }).click()
// Wait for error to be processed
await page.waitForTimeout(1000)
// Modal stays open, buttons re-enable
await expect(modal).toBeVisible()
await expect(
modal.locator("button", { hasText: "Создать" }),
).toBeEnabled()
await expect(modal.locator("button", { hasText: "Отмена" })).toBeEnabled()
expect(
consoleErrors.some((e) => e.includes("Create project failed")),
).toBe(true)
})
test("should keep modal open on network error", async ({
projectsPage,
}) => {
const { page, modal } = projectsPage
await projectsPage.mockCreateNetworkError()
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Проект")
await modal.locator("button", { hasText: "Создать" }).click()
await page.waitForTimeout(1000)
await expect(modal).toBeVisible()
await expect(
modal.locator("button", { hasText: "Создать" }),
).toBeEnabled()
await expect(modal.locator("button", { hasText: "Отмена" })).toBeEnabled()
})
test("should allow retry after failure", async ({ projectsPage }) => {
const { page, modal } = projectsPage
// First attempt: 500 error
await projectsPage.mockCreateError(500)
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Проект")
await modal.locator("button", { hasText: "Создать" }).click()
await page.waitForTimeout(1000)
await expect(
modal.locator("button", { hasText: "Создать" }),
).toBeEnabled()
// Second attempt: success (re-mock the route)
await projectsPage.mockCreateSuccess({ name: "Проект" })
await modal.locator("button", { hasText: "Создать" }).click()
await expect(modal).toBeHidden()
})
})
test.describe("Form Behavior", () => {
test("should reset form when modal is closed and reopened", async ({
projectsPage,
}) => {
const { modal } = projectsPage
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Черновик")
await modal.locator("#project_description").fill("Описание черновика")
// Close with Cancel
await modal.locator("button", { hasText: "Отмена" }).click()
await expect(modal).toBeHidden()
// Reopen
await projectsPage.openCreateModal()
await expect(modal.locator("#project_name")).toHaveValue("")
await expect(modal.locator("#project_description")).toHaveValue("")
await expect(
modal.locator("button").filter({ hasText: "Авто" }),
).toBeVisible()
})
test("should disable buttons while request is pending", async ({
projectsPage,
}) => {
const { modal } = projectsPage
await projectsPage.mockCreateDelayed(2000)
await projectsPage.openCreateModal()
await modal.locator("#project_name").fill("Проект")
await modal.locator("button", { hasText: "Создать" }).click()
await expect(
modal.locator("button", { hasText: "Создать" }),
).toBeDisabled()
await expect(
modal.locator("button", { hasText: "Отмена" }),
).toBeDisabled()
})
})
})