iter 2
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
|
||||
const USER_ID = "00000000-0000-0000-0000-000000000001"
|
||||
const PROJECT_ID = "65df675b-013b-4b1f-ab2d-075dadbcd0d9"
|
||||
const CAPTION_PRESET_ID = "00000000-0000-0000-0000-000000000010"
|
||||
const TRANSCRIPTION_ARTIFACT_ID =
|
||||
"00000000-0000-0000-0000-000000000020"
|
||||
const TRANSCRIPTION_ID = "00000000-0000-0000-0000-000000000030"
|
||||
const CAPTION_JOB_ID = "00000000-0000-0000-0000-000000000040"
|
||||
const PRIMARY_FILE_KEY = "projects/test/video.mp4"
|
||||
|
||||
const DEFAULT_USER = {
|
||||
id: USER_ID,
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
phone_number: null,
|
||||
avatar: null,
|
||||
email_verified: true,
|
||||
phone_verified: false,
|
||||
is_active: true,
|
||||
is_staff: false,
|
||||
is_superuser: false,
|
||||
date_joined: "2025-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
test.describe("Caption Settings Step", () => {
|
||||
test("should recover a missing transcription artifact from project data", async ({
|
||||
page,
|
||||
}) => {
|
||||
let project: Record<string, unknown> = {
|
||||
id: PROJECT_ID,
|
||||
owner_id: USER_ID,
|
||||
name: "Тестовый проект",
|
||||
description: null,
|
||||
language: "auto",
|
||||
folder: null,
|
||||
status: "DRAFT",
|
||||
workspace_state: {
|
||||
wizard: {
|
||||
current_step: "caption-settings",
|
||||
completed_steps: [
|
||||
"upload",
|
||||
"verify",
|
||||
"silence-settings",
|
||||
"processing",
|
||||
"fragments",
|
||||
"transcription-settings",
|
||||
"transcription-processing",
|
||||
"subtitle-revision",
|
||||
],
|
||||
primary_file_key: PRIMARY_FILE_KEY,
|
||||
video_url: "http://localhost:9000/projects/test/video.mp4",
|
||||
silence_settings: {
|
||||
min_silence_duration_ms: 200,
|
||||
silence_threshold_db: 16,
|
||||
padding_ms: 100,
|
||||
},
|
||||
active_job_id: null,
|
||||
active_job_type: null,
|
||||
silence_job_id: null,
|
||||
transcription_artifact_id: null,
|
||||
caption_preset_id: CAPTION_PRESET_ID,
|
||||
caption_style_config: null,
|
||||
captioned_video_path: null,
|
||||
},
|
||||
},
|
||||
is_active: true,
|
||||
created_at: "2025-06-01T00:00:00Z",
|
||||
updated_at: "2025-06-01T00:00:00Z",
|
||||
}
|
||||
let savedWizardState: Record<string, unknown> | null = null
|
||||
let generateRequestBody: Record<string, unknown> | null = null
|
||||
let generateRequestCount = 0
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: "fake-access-jwt",
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: "fake-refresh-jwt",
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
await page.route("**/api/users/me/", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(DEFAULT_USER),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route(`**/api/projects/${PROJECT_ID}/`, async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(project),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (route.request().method() === "PATCH") {
|
||||
const body = route.request().postDataJSON() as {
|
||||
workspace_state?: { wizard?: Record<string, unknown> }
|
||||
}
|
||||
|
||||
savedWizardState = body.workspace_state?.wizard ?? null
|
||||
project = {
|
||||
...project,
|
||||
workspace_state: body.workspace_state ?? project.workspace_state,
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(project),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fallback()
|
||||
})
|
||||
|
||||
await page.route("**/api/media/artifacts/", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: TRANSCRIPTION_ARTIFACT_ID,
|
||||
project_id: PROJECT_ID,
|
||||
file_id: null,
|
||||
media_file_id: null,
|
||||
artifact_type: "TRANSCRIPTION_JSON",
|
||||
is_deleted: false,
|
||||
is_active: true,
|
||||
created_at: "2025-06-01T00:00:00Z",
|
||||
updated_at: "2025-06-01T00:00:00Z",
|
||||
},
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route(
|
||||
`**/api/transcribe/transcriptions/by-artifact/${TRANSCRIPTION_ARTIFACT_ID}/`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id: TRANSCRIPTION_ID,
|
||||
artifact_id: TRANSCRIPTION_ARTIFACT_ID,
|
||||
}),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
await page.route("**/api/captions/presets/", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([
|
||||
{
|
||||
id: CAPTION_PRESET_ID,
|
||||
user_id: null,
|
||||
name: "Системный пресет",
|
||||
description: null,
|
||||
is_system: true,
|
||||
style_config: {},
|
||||
preview_url: null,
|
||||
created_at: "2025-06-01T00:00:00Z",
|
||||
updated_at: "2025-06-01T00:00:00Z",
|
||||
},
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/tasks/captions-generate/", async (route) => {
|
||||
generateRequestCount += 1
|
||||
generateRequestBody = route.request().postDataJSON() as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ job_id: CAPTION_JOB_ID }),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/tasks/status/**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
status: "RUNNING",
|
||||
progress_pct: 0,
|
||||
output_data: null,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto(`/projects/${PROJECT_ID}`)
|
||||
|
||||
const captionStep = page.locator("[data-testid='CaptionSettingsStep']")
|
||||
const generateButton = captionStep.getByRole("button", {
|
||||
name: "Генерировать",
|
||||
})
|
||||
|
||||
await expect(captionStep).toBeVisible()
|
||||
await expect(captionStep.getByText("Системный пресет")).toBeVisible()
|
||||
await expect(generateButton).toBeEnabled()
|
||||
|
||||
await expect
|
||||
.poll(() => savedWizardState?.transcription_artifact_id ?? null)
|
||||
.toBe(TRANSCRIPTION_ARTIFACT_ID)
|
||||
|
||||
await generateButton.click()
|
||||
|
||||
expect(generateRequestBody).toMatchObject({
|
||||
video_s3_path: PRIMARY_FILE_KEY,
|
||||
transcription_id: TRANSCRIPTION_ID,
|
||||
project_id: PROJECT_ID,
|
||||
preset_id: CAPTION_PRESET_ID,
|
||||
})
|
||||
expect(generateRequestCount).toBe(1)
|
||||
|
||||
await expect
|
||||
.poll(() => savedWizardState?.current_step ?? null)
|
||||
.toBe("caption-processing")
|
||||
await expect
|
||||
.poll(() => savedWizardState?.active_job_id ?? null)
|
||||
.toBe(CAPTION_JOB_ID)
|
||||
|
||||
await expect(page.locator("[data-testid='ProcessingStep']")).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import { test, expect } from "#tests/e2e/fixtures/real-backend"
|
||||
|
||||
// These tests run against the real backend (localhost:8000).
|
||||
// Each test gets a fresh user with real JWT tokens.
|
||||
// Created projects are cleaned up automatically after each test.
|
||||
|
||||
test.describe("Create Project (Integration)", () => {
|
||||
test("should create project and see it in the list", async ({
|
||||
realProjectsPage,
|
||||
}) => {
|
||||
const { page, modal } = realProjectsPage
|
||||
|
||||
// Empty state
|
||||
await expect(page.getByText("У вас пока нет проектов")).toBeVisible()
|
||||
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("#project_name").fill("Интеграционный тест")
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
|
||||
await expect(modal).toBeHidden()
|
||||
await expect(page.getByText("Интеграционный тест")).toBeVisible()
|
||||
})
|
||||
|
||||
test("should create project with description", async ({
|
||||
realProjectsPage,
|
||||
}) => {
|
||||
const { page, modal } = realProjectsPage
|
||||
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("#project_name").fill("Проект с описанием")
|
||||
await modal
|
||||
.locator("#project_description")
|
||||
.fill("Описание для интеграционного теста")
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
|
||||
await expect(modal).toBeHidden()
|
||||
await expect(page.getByText("Проект с описанием")).toBeVisible()
|
||||
|
||||
// Verify via API that description was saved
|
||||
const projects = await realProjectsPage.getProjects()
|
||||
const created = projects.find(
|
||||
(p: { name: string }) => p.name === "Проект с описанием",
|
||||
)
|
||||
expect(created).toBeTruthy()
|
||||
})
|
||||
|
||||
test("should create project with Russian language", async ({
|
||||
realProjectsPage,
|
||||
}) => {
|
||||
const { page, modal } = realProjectsPage
|
||||
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("#project_name").fill("Русский проект")
|
||||
|
||||
await modal.locator("button").filter({ hasText: "Авто" }).click()
|
||||
await page.locator("[role=option]", { hasText: "Русский" }).click()
|
||||
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
await expect(modal).toBeHidden()
|
||||
|
||||
// Verify language via API
|
||||
const projects = await realProjectsPage.getProjects()
|
||||
const created = projects.find(
|
||||
(p: { name: string }) => p.name === "Русский проект",
|
||||
) as { language: string } | undefined
|
||||
expect(created?.language).toBe("ru")
|
||||
})
|
||||
|
||||
test("should create multiple projects", async ({ realProjectsPage }) => {
|
||||
const { page, modal } = realProjectsPage
|
||||
|
||||
// Create first project
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("#project_name").fill("Первый проект")
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
await expect(modal).toBeHidden()
|
||||
await expect(page.getByText("Первый проект")).toBeVisible()
|
||||
|
||||
// Create second project
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("#project_name").fill("Второй проект")
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
await expect(modal).toBeHidden()
|
||||
await expect(page.getByText("Второй проект")).toBeVisible()
|
||||
|
||||
// Both visible
|
||||
await expect(page.getByText("Первый проект")).toBeVisible()
|
||||
await expect(page.getByText("Второй проект")).toBeVisible()
|
||||
|
||||
// Verify via API
|
||||
const projects = await realProjectsPage.getProjects()
|
||||
expect(projects.length).toBe(2)
|
||||
})
|
||||
|
||||
test("should persist project after page reload", async ({
|
||||
realProjectsPage,
|
||||
}) => {
|
||||
const { page, modal } = realProjectsPage
|
||||
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("#project_name").fill("Персистентный проект")
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
await expect(modal).toBeHidden()
|
||||
await expect(page.getByText("Персистентный проект")).toBeVisible()
|
||||
|
||||
// Reload and verify project is still there
|
||||
await page.reload()
|
||||
await page.getByRole("heading", { name: "Мои проекты" }).waitFor()
|
||||
await expect(page.getByText("Персистентный проект")).toBeVisible()
|
||||
})
|
||||
|
||||
test("should show validation error for empty name (client-side)", async ({
|
||||
realProjectsPage,
|
||||
}) => {
|
||||
const { modal } = realProjectsPage
|
||||
|
||||
await realProjectsPage.openCreateModal()
|
||||
await modal.locator("button", { hasText: "Создать" }).click()
|
||||
|
||||
await expect(
|
||||
modal.getByText("Введите название проекта"),
|
||||
).toBeVisible()
|
||||
await expect(modal).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,327 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,260 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
|
||||
const USER_ID = "00000000-0000-0000-0000-000000000001"
|
||||
const PROJECT_ID = "75df675b-013b-4b1f-ab2d-075dadbcd0d9"
|
||||
const DETECT_JOB_ID = "00000000-0000-0000-0000-000000000050"
|
||||
const APPLY_JOB_ID = "00000000-0000-0000-0000-000000000051"
|
||||
const TRANSCRIPTION_JOB_ID = "00000000-0000-0000-0000-000000000052"
|
||||
const ORIGINAL_FILE_KEY = "projects/test/original-video.mp4"
|
||||
const ORIGINAL_FILE_URL = "http://localhost:4444/files/original-video.mp4"
|
||||
const CUT_FILE_KEY = "projects/test/cut-video.mp4"
|
||||
const CUT_FILE_URL = "http://localhost:4444/files/cut-video.mp4"
|
||||
|
||||
const DEFAULT_USER = {
|
||||
id: USER_ID,
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
phone_number: null,
|
||||
avatar: null,
|
||||
email_verified: true,
|
||||
phone_verified: false,
|
||||
is_active: true,
|
||||
is_staff: false,
|
||||
is_superuser: false,
|
||||
date_joined: "2025-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
const MOCK_SEGMENTS = [
|
||||
{ start_ms: 5000, end_ms: 8000 },
|
||||
{ start_ms: 15000, end_ms: 19000 },
|
||||
]
|
||||
|
||||
test.describe("Silence Apply Flow", () => {
|
||||
test("should show processing for cut application and transcribe the processed video", async ({
|
||||
page,
|
||||
}) => {
|
||||
let project: Record<string, unknown> = {
|
||||
id: PROJECT_ID,
|
||||
owner_id: USER_ID,
|
||||
name: "Тестовый проект",
|
||||
description: null,
|
||||
language: "auto",
|
||||
folder: null,
|
||||
status: "DRAFT",
|
||||
workspace_state: {
|
||||
wizard: {
|
||||
current_step: "fragments",
|
||||
completed_steps: [
|
||||
"upload",
|
||||
"verify",
|
||||
"silence-settings",
|
||||
"processing",
|
||||
],
|
||||
primary_file_key: ORIGINAL_FILE_KEY,
|
||||
video_url: ORIGINAL_FILE_URL,
|
||||
original_file_name: "original-video.mp4",
|
||||
silence_settings: {
|
||||
min_silence_duration_ms: 200,
|
||||
silence_threshold_db: 16,
|
||||
padding_ms: 100,
|
||||
},
|
||||
active_job_id: null,
|
||||
active_job_type: null,
|
||||
silence_job_id: DETECT_JOB_ID,
|
||||
transcription_artifact_id: null,
|
||||
caption_preset_id: null,
|
||||
caption_style_config: null,
|
||||
captioned_video_path: null,
|
||||
captioned_video_file_id: null,
|
||||
},
|
||||
},
|
||||
is_active: true,
|
||||
created_at: "2025-06-01T00:00:00Z",
|
||||
updated_at: "2025-06-01T00:00:00Z",
|
||||
}
|
||||
let savedWizardState: Record<string, unknown> | null = null
|
||||
let applyStatus = "RUNNING"
|
||||
let transcriptionRequestBody: Record<string, unknown> | null = null
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: "fake-access-jwt",
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: "fake-refresh-jwt",
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
await page.route("**/api/users/me/", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(DEFAULT_USER),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route(`**/api/projects/${PROJECT_ID}/`, async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(project),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (route.request().method() === "PATCH") {
|
||||
const body = route.request().postDataJSON() as {
|
||||
workspace_state?: { wizard?: Record<string, unknown> }
|
||||
}
|
||||
|
||||
savedWizardState = body.workspace_state?.wizard ?? null
|
||||
project = {
|
||||
...project,
|
||||
workspace_state: body.workspace_state ?? project.workspace_state,
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(project),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fallback()
|
||||
})
|
||||
|
||||
await page.route("**/api/files/get_file/**", async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const filePath = url.searchParams.get("file_path")
|
||||
|
||||
const fileUrl =
|
||||
filePath === CUT_FILE_KEY ? CUT_FILE_URL : ORIGINAL_FILE_URL
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
file_url: fileUrl,
|
||||
file_path: filePath,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/tasks/status/**", async (route) => {
|
||||
const url = route.request().url()
|
||||
|
||||
if (url.includes(DETECT_JOB_ID)) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
status: "DONE",
|
||||
job_type: "SILENCE_DETECT",
|
||||
progress_pct: 100,
|
||||
output_data: {
|
||||
silent_segments: MOCK_SEGMENTS,
|
||||
duration_ms: 30000,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.includes(APPLY_JOB_ID)) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
status: applyStatus,
|
||||
job_type: "SILENCE_APPLY",
|
||||
progress_pct: applyStatus === "DONE" ? 100 : 30,
|
||||
output_data:
|
||||
applyStatus === "DONE"
|
||||
? {
|
||||
file_path: CUT_FILE_KEY,
|
||||
file_url: CUT_FILE_URL,
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
status: "RUNNING",
|
||||
progress_pct: 0,
|
||||
output_data: null,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/tasks/silence-apply/", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 202,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ job_id: APPLY_JOB_ID }),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/tasks/transcription-generate/", async (route) => {
|
||||
transcriptionRequestBody = route.request().postDataJSON() as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
|
||||
await route.fulfill({
|
||||
status: 202,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ job_id: TRANSCRIPTION_JOB_ID }),
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto(`/projects/${PROJECT_ID}`)
|
||||
|
||||
const fragmentsStep = page.locator("[data-testid='FragmentsStep']")
|
||||
await expect(fragmentsStep).toBeVisible()
|
||||
|
||||
await fragmentsStep.getByRole("button", { name: "Применить" }).click()
|
||||
|
||||
await expect(page.locator("[data-testid='ProcessingStep']")).toBeVisible()
|
||||
await expect
|
||||
.poll(() => savedWizardState?.active_job_type ?? null)
|
||||
.toBe("SILENCE_APPLY")
|
||||
await expect
|
||||
.poll(() => savedWizardState?.current_step ?? null)
|
||||
.toBe("processing")
|
||||
|
||||
applyStatus = "DONE"
|
||||
|
||||
const transcriptionStep = page.locator(
|
||||
"[data-testid='TranscriptionSettingsStep']",
|
||||
)
|
||||
await expect(transcriptionStep).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await expect
|
||||
.poll(() => savedWizardState?.primary_file_key ?? null)
|
||||
.toBe(CUT_FILE_KEY)
|
||||
|
||||
await transcriptionStep
|
||||
.getByRole("button", { name: "Сгенерировать субтитры" })
|
||||
.click()
|
||||
|
||||
expect(transcriptionRequestBody).toMatchObject({
|
||||
file_key: CUT_FILE_KEY,
|
||||
project_id: PROJECT_ID,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user