import { test, expect } from "#tests/e2e/fixtures/processing" const MOCK_SEGMENTS = [ { start_ms: 5000, end_ms: 8000 }, { start_ms: 15000, end_ms: 19000 }, { start_ms: 32000, end_ms: 35000 }, { start_ms: 45000, end_ms: 50000 }, ] const MOCK_DURATION_MS = 60000 function buildNotification( jobId: string, overrides: Record = {}, ) { return { event: "task_update", notification_id: null, job_id: jobId, project_id: null, job_type: "SILENCE_DETECT", status: "RUNNING", progress_pct: 0, message: null, title: null, created_at: new Date().toISOString(), is_read: false, ...overrides, } } test.describe("Processing Step (Integration)", () => { test.describe("Initial State", () => { test("should display the processing step with progress and status label", async ({ processingPage, }) => { const { processingStep } = processingPage await expect(processingStep).toBeVisible() await expect(processingStep.getByText("0%")).toBeVisible() await expect(processingStep.getByText("АНАЛИЗ")).toBeVisible() }) test("should display the default status message", async ({ processingPage, }) => { const { processingStep } = processingPage await expect( processingStep.getByText("Подождите, идёт обработка..."), ).toBeVisible() }) test("should display the info card about server processing", async ({ processingPage, }) => { const { processingStep } = processingPage await expect( processingStep.getByText( "Обработка выполняется на сервере. Вы можете покинуть страницу — прогресс сохранится.", ), ).toBeVisible() }) test("should display the cancel button", async ({ processingPage, }) => { const { processingStep } = processingPage const cancelButton = processingStep.getByRole("button", { name: "Отменить обработку", }) await expect(cancelButton).toBeVisible() await expect(cancelButton).toBeEnabled() }) }) test.describe("Progress Updates", () => { test("should display updated progress percentage from notifications", async ({ processingPage, }) => { const { page, processingStep, jobId } = processingPage await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { progress_pct: 45, message: "Анализируем аудио...", }), ) await expect(processingStep.getByText("45%")).toBeVisible() await expect( processingStep.getByText("Анализируем аудио..."), ).toBeVisible() await expect(processingStep.getByText("АНАЛИЗ")).toBeVisible() }) test("should update progress when notification changes", async ({ processingPage, }) => { const { page, processingStep, jobId } = processingPage // First update: 25% await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { progress_pct: 25, message: "Обработка...", }), ) await expect(processingStep.getByText("25%")).toBeVisible() // Second update: 75% await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { progress_pct: 75, message: "Почти готово...", }), ) await expect(processingStep.getByText("75%")).toBeVisible() await expect( processingStep.getByText("Почти готово..."), ).toBeVisible() }) }) test.describe("Auto-Advance on Completion", () => { test("should auto-advance to fragments step when task status returns DONE", async ({ processingPage, }) => { const { page } = processingPage // Replace the route to return DONE await page.unrouteAll({ behavior: "ignoreErrors" }) 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, }, }), }) }) // Wait for auto-advance (polls every 2s) await expect( page.locator("[data-testid='FragmentsStep']"), ).toBeVisible({ timeout: 10_000 }) await expect( page.locator("[data-testid='ProcessingStep']"), ).not.toBeVisible() }) test("should auto-advance when notification reports DONE", async ({ processingPage, }) => { const { page, jobId } = processingPage // Dispatch DONE notification (auto-advance also checks notifications) await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { status: "DONE", progress_pct: 100, }), ) // The WizardContext auto-advance also needs the polling to confirm, // so update the route too await page.unrouteAll({ behavior: "ignoreErrors" }) 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 expect( page.locator("[data-testid='FragmentsStep']"), ).toBeVisible({ timeout: 10_000 }) }) }) test.describe("Failure State", () => { test("should display error state when notification status is FAILED", async ({ processingPage, }) => { const { page, processingStep, jobId } = processingPage await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { status: "FAILED", message: "Файл повреждён", }), ) await expect(processingStep.getByText("ОШИБКА")).toBeVisible() await expect( processingStep.getByText("Файл повреждён"), ).toBeVisible() // Button should change from "Отменить обработку" to "Назад" await expect( processingStep.getByRole("button", { name: "Назад" }), ).toBeVisible() await expect( processingStep.getByRole("button", { name: "Отменить обработку", }), ).not.toBeVisible() }) test("should show default error message when FAILED notification has no message", async ({ processingPage, }) => { const { page, processingStep, jobId } = processingPage await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { status: "FAILED", message: null, }), ) await expect( processingStep.getByText("Произошла ошибка при обработке"), ).toBeVisible() }) test("should navigate back to silence settings when clicking back in error state", async ({ processingPage, }) => { const { page, processingStep, jobId } = processingPage await page.evaluate( (payload) => { ;(window as any).__REDUX_STORE__?.dispatch({ type: "notifications/addNotification", payload, }) }, buildNotification(jobId, { status: "FAILED", message: "Ошибка обработки", }), ) await processingStep .getByRole("button", { name: "Назад" }) .click() await expect( page.locator("[data-testid='SilenceSettingsStep']"), ).toBeVisible({ timeout: 10_000 }) await expect(processingStep).not.toBeVisible() }) }) test.describe("Cancel", () => { test("should navigate back to silence settings when cancel button is clicked", async ({ processingPage, }) => { const { page, processingStep } = processingPage await processingStep .getByRole("button", { name: "Отменить обработку" }) .click() await expect( page.locator("[data-testid='SilenceSettingsStep']"), ).toBeVisible({ timeout: 10_000 }) await expect(processingStep).not.toBeVisible() }) test("should start a new job when clicking Далее again after cancel", async ({ processingPage, }) => { const { page, processingStep } = processingPage // Cancel await processingStep .getByRole("button", { name: "Отменить обработку" }) .click() const silenceStep = page.locator( "[data-testid='SilenceSettingsStep']", ) await silenceStep.waitFor({ timeout: 10_000 }) // Capture new request let newPostMade = false page.on("request", (req) => { if ( req.url().includes("/api/tasks/silence-detect/") && req.method() === "POST" ) { newPostMade = true } }) // Click "Далее" again await silenceStep .getByRole("button", { name: "Далее" }) .click() await expect( page.locator("[data-testid='ProcessingStep']"), ).toBeVisible({ timeout: 10_000 }) expect(newPostMade).toBe(true) }) }) test.describe("State Persistence", () => { test("should restore processing step after page reload", async ({ processingPage, }) => { const { page } = processingPage // Wait for debounced save await page.waitForTimeout(2500) // Reload — route intercepts are lost, re-add before reload completes await page.reload() 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 .locator("[data-testid='ProjectWizard']") .waitFor({ timeout: 10_000 }) await expect( page.locator("[data-testid='ProcessingStep']"), ).toBeVisible({ timeout: 10_000 }) }) test("should resume polling and auto-advance after page reload", async ({ processingPage, }) => { const { page } = processingPage // Wait for debounced save await page.waitForTimeout(2500) // Reload and set up route to return DONE 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 auto-advance to fragments after polling picks up DONE await expect( page.locator("[data-testid='FragmentsStep']"), ).toBeVisible({ timeout: 15_000 }) }) }) })