646 lines
17 KiB
TypeScript
646 lines
17 KiB
TypeScript
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 })
|
||
})
|
||
})
|
||
})
|