Files
main_frontend/tests/e2e/specs/silence/silence-fragments.integration.spec.ts
T
Daniil 46f34bdcac rev 4
2026-04-07 13:42:23 +03:00

646 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
test,
expect,
MOCK_SEGMENTS,
MOCK_DURATION_MS,
MOCK_TOTAL_REMOVED_MS,
} from "#tests/e2e/fixtures/fragments"
test.describe("Fragments Step (Integration)", () => {
test.describe("Initial State", () => {
test("should display the fragments step", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
await expect(fragmentsStep).toBeVisible()
})
test("should display the correct fragment count", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
await expect(
fragmentsStep.getByText(`Фрагментов: ${MOCK_SEGMENTS.length}`),
).toBeVisible()
})
test("should display the correct total removal duration", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const totalSec = Math.floor(MOCK_TOTAL_REMOVED_MS / 1000)
await expect(
fragmentsStep.getByText(`Будет удалено: ${totalSec}с`),
).toBeVisible()
})
test("should display the video player", async ({ fragmentsPage }) => {
const { fragmentsStep } = fragmentsPage
await expect(
fragmentsStep.locator("media-player"),
).toBeVisible({ timeout: 10_000 })
})
test("should display zoom controls", async ({ fragmentsPage }) => {
const { fragmentsStep } = fragmentsPage
await expect(fragmentsStep.getByText("Масштаб")).toBeVisible()
await expect(
fragmentsStep.locator("button").filter({ hasText: "-" }),
).toBeVisible()
await expect(
fragmentsStep.locator("button").filter({ hasText: "+" }),
).toBeVisible()
})
test("should display the apply button when regions exist", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
await expect(
fragmentsStep.getByRole("button", { name: "Применить" }),
).toBeVisible()
})
test("should display the cancel button", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const cancelButton = fragmentsStep.getByRole("button", {
name: "Отмена",
})
await expect(cancelButton).toBeVisible()
await expect(cancelButton).toBeEnabled()
})
})
test.describe("Zoom Controls", () => {
test("should increase timeline width when + is clicked", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const timelineInner = fragmentsStep
.locator("[class*='timelineInner']")
.first()
// Wait for initial render
await timelineInner.waitFor({ timeout: 5_000 })
const initialWidth = await timelineInner.evaluate(
(el) => el.clientWidth,
)
// Click zoom in multiple times
const zoomIn = fragmentsStep
.locator("button")
.filter({ hasText: "+" })
await zoomIn.click()
await zoomIn.click()
await zoomIn.click()
const newWidth = await timelineInner.evaluate(
(el) => el.clientWidth,
)
expect(newWidth).toBeGreaterThan(initialWidth)
})
test("should decrease timeline width when - is clicked", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const timelineInner = fragmentsStep
.locator("[class*='timelineInner']")
.first()
// Zoom in first to have room to zoom out
const zoomIn = fragmentsStep
.locator("button")
.filter({ hasText: "+" })
await zoomIn.click()
await zoomIn.click()
await zoomIn.click()
await timelineInner.waitFor({ timeout: 5_000 })
const widthAfterZoomIn = await timelineInner.evaluate(
(el) => el.clientWidth,
)
// Zoom out
const zoomOut = fragmentsStep
.locator("button")
.filter({ hasText: "-" })
await zoomOut.click()
await zoomOut.click()
const widthAfterZoomOut = await timelineInner.evaluate(
(el) => el.clientWidth,
)
expect(widthAfterZoomOut).toBeLessThan(widthAfterZoomIn)
})
})
test.describe("Context Menu — Add Region", () => {
test("should show context menu with add option on right-click on timeline", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const timeline = fragmentsStep
.locator("[class*='timelineContainer']")
.first()
await timeline.click({ button: "right", position: { x: 50, y: 50 } })
await expect(
fragmentsStep.getByText("Добавить новый"),
).toBeVisible()
})
test("should add a new region via context menu", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const timeline = fragmentsStep
.locator("[class*='timelineContainer']")
.first()
await timeline.click({ button: "right", position: { x: 50, y: 50 } })
await fragmentsStep.getByText("Добавить новый").click()
await expect(
fragmentsStep.getByText(
`Фрагментов: ${MOCK_SEGMENTS.length + 1}`,
),
).toBeVisible()
})
})
test.describe("Context Menu — Delete Region", () => {
test("should show delete and add options when right-clicking a cut region", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const region = fragmentsStep
.locator("[data-testid='cut-region']")
.first()
await region.waitFor({ timeout: 5_000 })
await region.click({ button: "right", force: true })
await expect(
fragmentsStep.getByText("Удалить"),
).toBeVisible()
await expect(
fragmentsStep.getByText("Добавить новый"),
).toBeVisible()
})
test("should delete a region via context menu", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
const region = fragmentsStep
.locator("[data-testid='cut-region']")
.first()
await region.waitFor({ timeout: 5_000 })
await region.click({ button: "right", force: true })
await fragmentsStep.getByText("Удалить").click()
await expect(
fragmentsStep.getByText(
`Фрагментов: ${MOCK_SEGMENTS.length - 1}`,
),
).toBeVisible()
})
test("should update info bar correctly after multiple operations", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
// Delete first region
let region = fragmentsStep
.locator("[data-testid='cut-region']")
.first()
await region.waitFor({ timeout: 5_000 })
await region.click({ button: "right", force: true })
await fragmentsStep.getByText("Удалить").click()
await expect(
fragmentsStep.getByText(
`Фрагментов: ${MOCK_SEGMENTS.length - 1}`,
),
).toBeVisible()
// Delete second region
region = fragmentsStep
.locator("[data-testid='cut-region']")
.first()
await region.click({ button: "right", force: true })
await fragmentsStep.getByText("Удалить").click()
await expect(
fragmentsStep.getByText(
`Фрагментов: ${MOCK_SEGMENTS.length - 2}`,
),
).toBeVisible()
// Add a new one
const timeline = fragmentsStep
.locator("[class*='timelineContainer']")
.first()
await timeline.click({ button: "right", position: { x: 100, y: 50 } })
await fragmentsStep.getByText("Добавить новый").click()
// 4 - 2 + 1 = 3
await expect(
fragmentsStep.getByText(
`Фрагментов: ${MOCK_SEGMENTS.length - 2 + 1}`,
),
).toBeVisible()
})
})
test.describe("Skip When No Regions", () => {
test("should show skip button when all regions are removed", async ({
fragmentsPage,
}) => {
const { fragmentsStep } = fragmentsPage
// Remove all regions one by one
for (let i = 0; i < MOCK_SEGMENTS.length; i++) {
const region = fragmentsStep
.locator("[data-testid='cut-region']")
.first()
await region.waitFor({ timeout: 5_000 })
await region.click({ button: "right", force: true })
await fragmentsStep.getByText("Удалить").click()
}
await expect(
fragmentsStep.getByText("Фрагментов: 0"),
).toBeVisible()
await expect(
fragmentsStep.getByText("Будет удалено: 0с"),
).toBeVisible()
await expect(
fragmentsStep.getByRole("button", { name: "Пропустить" }),
).toBeVisible()
})
test("should advance to transcription settings when skip is clicked", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
// Remove all regions
for (let i = 0; i < MOCK_SEGMENTS.length; i++) {
const region = fragmentsStep
.locator("[data-testid='cut-region']")
.first()
await region.waitFor({ timeout: 5_000 })
await region.click({ button: "right", force: true })
await fragmentsStep.getByText("Удалить").click()
}
await fragmentsStep
.getByRole("button", { name: "Пропустить" })
.click()
await expect(
page.locator("[data-testid='TranscriptionSettingsStep']"),
).toBeVisible({ timeout: 10_000 })
})
})
test.describe("Apply with Regions", () => {
test("should send correct request body when apply is clicked", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep, projectId } = fragmentsPage
let postBody: Record<string, unknown> | null = null
page.on("request", (req) => {
if (
req.url().includes("/api/tasks/silence-apply/") &&
req.method() === "POST"
) {
postBody = req.postDataJSON()
}
})
await fragmentsStep
.getByRole("button", { name: "Применить" })
.click()
// Wait for request to be sent
await expect
.poll(() => postBody, { timeout: 5_000 })
.not.toBeNull()
expect(postBody!.project_id).toBe(projectId)
expect(postBody!.file_key).toBeTruthy()
expect((postBody!.output_name as string)).toContain("Без тишины")
expect((postBody!.cuts as unknown[]).length).toBe(
MOCK_SEGMENTS.length,
)
})
test("should disable buttons while apply is pending", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
// Delay the apply response
await page.route(
"**/api/tasks/silence-apply/**",
async (route) => {
await new Promise((r) => setTimeout(r, 3000))
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: "fake-apply-job" }),
})
},
)
await page.route(
"**/api/tasks/silence-apply/",
async (route) => {
await new Promise((r) => setTimeout(r, 3000))
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: "fake-apply-job" }),
})
},
)
await fragmentsStep
.getByRole("button", { name: "Применить" })
.click()
await expect(
fragmentsStep.getByRole("button", { name: "Применить" }),
).toBeDisabled({ timeout: 2_000 })
await expect(
fragmentsStep.getByRole("button", { name: "Отмена" }),
).toBeDisabled()
})
test("should show processing step after successful apply submission", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
// Mock successful apply response
await page.route("**/api/tasks/silence-apply/**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: "apply-job-123" }),
})
})
await page.route("**/api/tasks/silence-apply/", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: "apply-job-123" }),
})
})
await fragmentsStep
.getByRole("button", { name: "Применить" })
.click()
await expect(
page.locator("[data-testid='ProcessingStep']"),
).toBeVisible({ timeout: 10_000 })
})
test("should advance to transcription settings and submit processed file after apply completes", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
const applyJobId = "apply-job-123"
const processedFilePath =
"users/test-user/output_files/silent/Без тишины test-video.mp4"
const processedFileUrl = "https://example.com/processed-video.mp4"
let transcriptionBody: Record<string, unknown> | null = null
await page.route("**/api/tasks/silence-apply/**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: applyJobId }),
})
})
await page.route("**/api/tasks/silence-apply/", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: applyJobId }),
})
})
await page.unroute("**/api/tasks/status/**")
await page.route("**/api/tasks/status/**", async (route) => {
const url = route.request().url()
if (url.includes(applyJobId)) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
status: "DONE",
job_type: "SILENCE_APPLY",
progress_pct: 100,
output_data: {
file_id: "00000000-0000-0000-0000-000000000071",
file_path: processedFilePath,
},
}),
})
return
}
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: MOCK_DURATION_MS,
},
}),
})
})
page.on("request", (req) => {
if (
req.url().includes("/api/tasks/transcription-generate/") &&
req.method() === "POST"
) {
transcriptionBody = req.postDataJSON()
}
})
await page.route(
"**/api/tasks/transcription-generate/**",
async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: "transcription-job-123" }),
})
},
)
await page.route(
"**/api/tasks/transcription-generate/",
async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ job_id: "transcription-job-123" }),
})
},
)
await fragmentsStep
.getByRole("button", { name: "Применить" })
.click()
await expect(
page.locator("[data-testid='ProcessingStep']"),
).toBeVisible({ timeout: 10_000 })
await expect(
page.locator("[data-testid='TranscriptionSettingsStep']"),
).toBeVisible({ timeout: 10_000 })
await page
.locator("[data-testid='TranscriptionSettingsStep']")
.getByRole("button", { name: "Сгенерировать субтитры" })
.click()
await expect
.poll(() => transcriptionBody, { timeout: 5_000 })
.not.toBeNull()
expect(
(transcriptionBody as { file_key?: string } | null)?.file_key,
).toBe(processedFilePath)
})
test("should stay on fragments step when apply fails", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
// Mock failed apply response
await page.route("**/api/tasks/silence-apply/**", async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ detail: "Internal Server Error" }),
})
})
await page.route("**/api/tasks/silence-apply/", async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ detail: "Internal Server Error" }),
})
})
await fragmentsStep
.getByRole("button", { name: "Применить" })
.click()
// Should stay on fragments step
await page.waitForTimeout(2000)
await expect(fragmentsStep).toBeVisible()
// Buttons should be re-enabled
await expect(
fragmentsStep.getByRole("button", { name: "Применить" }),
).toBeEnabled({ timeout: 5_000 })
await expect(
fragmentsStep.getByRole("button", { name: "Отмена" }),
).toBeEnabled()
})
})
test.describe("Navigation", () => {
test("should navigate back when cancel button is clicked", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
await fragmentsStep
.getByRole("button", { name: "Отмена" })
.click()
// Should go back to the previous step
await expect(fragmentsStep).not.toBeVisible({ timeout: 5_000 })
// The previous step in the wizard should be visible
const processingOrSettings = page
.locator(
"[data-testid='ProcessingStep'], [data-testid='SilenceSettingsStep']",
)
.first()
await expect(processingOrSettings).toBeVisible({ timeout: 10_000 })
})
})
test.describe("State Persistence", () => {
test("should restore fragments step after page reload", async ({
fragmentsPage,
}) => {
const { page, fragmentsStep } = fragmentsPage
// Wait for debounced save
await page.waitForTimeout(2500)
// Reload page — re-add route interceptor for task status
await page.reload()
await page.route("**/api/tasks/status/**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
status: "DONE",
progress_pct: 100,
output_data: {
silent_segments: MOCK_SEGMENTS,
duration_ms: MOCK_DURATION_MS,
},
}),
})
})
await page
.locator("[data-testid='ProjectWizard']")
.waitFor({ timeout: 10_000 })
// Should restore to fragments step
await expect(
page.locator("[data-testid='FragmentsStep']"),
).toBeVisible({ timeout: 15_000 })
// Regions should re-load from mock data
await expect(
page.getByText(`Фрагментов: ${MOCK_SEGMENTS.length}`),
).toBeVisible({ timeout: 10_000 })
})
})
})