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 | 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 | 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 }) }) }) })