This commit is contained in:
Daniil
2026-04-04 14:51:40 +03:00
parent 10a1d28f77
commit 0523ef3d72
191 changed files with 12065 additions and 2658 deletions
@@ -0,0 +1,645 @@
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_path: processedFilePath,
file_url: processedFileUrl,
},
}),
})
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 })
})
})
})