iter 2
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import { test, expect } from "#tests/e2e/fixtures/upload"
|
||||
|
||||
test.describe("File Type and Extension Validation (Integration)", () => {
|
||||
test.describe("Input Accept Attribute", () => {
|
||||
test("should restrict file input to video/* MIME types", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
await expect(uploadPage.fileInput).toHaveAttribute(
|
||||
"accept",
|
||||
"video/*",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Valid Video Files", () => {
|
||||
test("should accept and upload an MP4 file", async ({ uploadPage }) => {
|
||||
const { page, testVideoPath } = uploadPage
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
await expect(
|
||||
page.locator("[data-testid='VerifyStep']"),
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
await expect(
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.getByText("Готово к обработке"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
|
||||
test("should attempt upload for a WebM file", async ({ uploadPage }) => {
|
||||
const { page, dropZone } = uploadPage
|
||||
|
||||
// Create a minimal buffer pretending to be WebM
|
||||
const webmBuffer = Buffer.from([
|
||||
0x1a, 0x45, 0xdf, 0xa3, // EBML header magic
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f,
|
||||
])
|
||||
|
||||
await uploadPage.uploadBuffer("test.webm", "video/webm", webmBuffer)
|
||||
|
||||
// Upload should begin (progress or error — both prove the file was sent)
|
||||
const uploadStarted = await Promise.race([
|
||||
dropZone
|
||||
.getByText("Загрузка файла...")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
])
|
||||
|
||||
expect(uploadStarted).toBe(true)
|
||||
})
|
||||
|
||||
test("should attempt upload for an MOV file", async ({ uploadPage }) => {
|
||||
const { page, dropZone } = uploadPage
|
||||
|
||||
// Minimal ftyp atom for MOV
|
||||
const movBuffer = Buffer.from([
|
||||
0x00, 0x00, 0x00, 0x14, // size: 20
|
||||
0x66, 0x74, 0x79, 0x70, // ftyp
|
||||
0x71, 0x74, 0x20, 0x20, // qt (QuickTime)
|
||||
0x00, 0x00, 0x00, 0x00, // minor version
|
||||
0x71, 0x74, 0x20, 0x20, // compatible brand
|
||||
])
|
||||
|
||||
await uploadPage.uploadBuffer(
|
||||
"test.mov",
|
||||
"video/quicktime",
|
||||
movBuffer,
|
||||
)
|
||||
|
||||
const uploadStarted = await Promise.race([
|
||||
dropZone
|
||||
.getByText("Загрузка файла...")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
])
|
||||
|
||||
expect(uploadStarted).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Non-Video Files (Bypass via setInputFiles)", () => {
|
||||
test("should attempt to upload a non-video file (no client-side validation)", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone } = uploadPage
|
||||
|
||||
// setInputFiles bypasses the accept attribute — proves no JS validation
|
||||
const txtBuffer = Buffer.from("Hello, this is a text file")
|
||||
await uploadPage.uploadBuffer(
|
||||
"document.txt",
|
||||
"text/plain",
|
||||
txtBuffer,
|
||||
)
|
||||
|
||||
// Upload should begin regardless — component has no file type check
|
||||
const uploadStarted = await Promise.race([
|
||||
dropZone
|
||||
.getByText("Загрузка файла...")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
])
|
||||
|
||||
expect(uploadStarted).toBe(true)
|
||||
})
|
||||
|
||||
test("should attempt to upload a PDF file", async ({ uploadPage }) => {
|
||||
const { page, dropZone } = uploadPage
|
||||
|
||||
const pdfBuffer = Buffer.from("%PDF-1.4 fake content")
|
||||
await uploadPage.uploadBuffer(
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
pdfBuffer,
|
||||
)
|
||||
|
||||
const uploadStarted = await Promise.race([
|
||||
dropZone
|
||||
.getByText("Загрузка файла...")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
])
|
||||
|
||||
expect(uploadStarted).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Edge Cases — File Names", () => {
|
||||
test("should handle a file with Unicode characters in the name", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
const fs = await import("node:fs")
|
||||
|
||||
const videoContent = fs.readFileSync(testVideoPath)
|
||||
|
||||
await uploadPage.uploadBuffer(
|
||||
"видео_тест_2026.mp4",
|
||||
"video/mp4",
|
||||
videoContent,
|
||||
)
|
||||
|
||||
// Should upload successfully
|
||||
const result = await Promise.race([
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "verify" as const),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "error" as const),
|
||||
])
|
||||
|
||||
// Both outcomes are valid — we verify no crash
|
||||
expect(["verify", "error"]).toContain(result)
|
||||
})
|
||||
|
||||
test("should handle a file with special characters in the name", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
const fs = await import("node:fs")
|
||||
|
||||
const videoContent = fs.readFileSync(testVideoPath)
|
||||
|
||||
await uploadPage.uploadBuffer(
|
||||
"test file (1) [final].mp4",
|
||||
"video/mp4",
|
||||
videoContent,
|
||||
)
|
||||
|
||||
const result = await Promise.race([
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "verify" as const),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "error" as const),
|
||||
])
|
||||
|
||||
expect(["verify", "error"]).toContain(result)
|
||||
})
|
||||
|
||||
test("should handle a zero-byte video file", async ({ uploadPage }) => {
|
||||
const { page, dropZone } = uploadPage
|
||||
|
||||
const emptyBuffer = Buffer.alloc(0)
|
||||
await uploadPage.uploadBuffer("empty.mp4", "video/mp4", emptyBuffer)
|
||||
|
||||
// Should attempt upload, likely fail server-side
|
||||
const result = await Promise.race([
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 15_000 })
|
||||
.then(() => "error" as const),
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 15_000 })
|
||||
.then(() => "verify" as const),
|
||||
])
|
||||
|
||||
expect(["error", "verify"]).toContain(result)
|
||||
})
|
||||
|
||||
test("should handle a file with no extension", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
const fs = await import("node:fs")
|
||||
|
||||
const videoContent = fs.readFileSync(testVideoPath)
|
||||
|
||||
await uploadPage.uploadBuffer(
|
||||
"videofile",
|
||||
"video/mp4",
|
||||
videoContent,
|
||||
)
|
||||
|
||||
const result = await Promise.race([
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "verify" as const),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "error" as const),
|
||||
])
|
||||
|
||||
expect(["verify", "error"]).toContain(result)
|
||||
})
|
||||
|
||||
test("should handle a file with double extension", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
const fs = await import("node:fs")
|
||||
|
||||
const videoContent = fs.readFileSync(testVideoPath)
|
||||
|
||||
await uploadPage.uploadBuffer(
|
||||
"video.mp4.mp4",
|
||||
"video/mp4",
|
||||
videoContent,
|
||||
)
|
||||
|
||||
const result = await Promise.race([
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "verify" as const),
|
||||
dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "error" as const),
|
||||
])
|
||||
|
||||
expect(["verify", "error"]).toContain(result)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("FormData Payload Verification", () => {
|
||||
test("should send file in FormData and include correct folder path", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, projectId, testVideoPath } = uploadPage
|
||||
|
||||
let requestFired = false
|
||||
let hasAuthHeader = false
|
||||
let requestUrl = ""
|
||||
|
||||
page.on("request", (req) => {
|
||||
if (req.url().includes("/api/files/upload")) {
|
||||
requestFired = true
|
||||
requestUrl = req.url()
|
||||
hasAuthHeader = !!req.headers()["authorization"]
|
||||
}
|
||||
})
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
// Wait for the request to fire
|
||||
await expect(async () => {
|
||||
expect(requestFired).toBe(true)
|
||||
}).toPass({ timeout: 10_000 })
|
||||
|
||||
expect(requestUrl).toContain("/api/files/upload")
|
||||
expect(hasAuthHeader).toBe(true)
|
||||
|
||||
// Wait for upload to complete (verify or error)
|
||||
await Promise.race([
|
||||
page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 }),
|
||||
uploadPage.dropZone
|
||||
.getByText("Не удалось загрузить файл")
|
||||
.waitFor({ timeout: 30_000 }),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,272 @@
|
||||
import { test, expect } from "#tests/e2e/fixtures/upload"
|
||||
|
||||
test.describe("File Upload (Integration)", () => {
|
||||
test.describe("Initial State", () => {
|
||||
test("should display the upload drop zone with correct instructions", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { dropZone } = uploadPage
|
||||
|
||||
await expect(
|
||||
dropZone.getByText("Перетащите видеофайл сюда"),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
dropZone.getByText("или нажмите для выбора файла"),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
dropZone.locator("button", { hasText: "Выбрать файл" }),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test("should have a file input that accepts only video types", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { fileInput } = uploadPage
|
||||
|
||||
await expect(fileInput).toHaveAttribute("accept", "video/*")
|
||||
await expect(fileInput).not.toBeDisabled()
|
||||
})
|
||||
|
||||
test("should not show progress bar or error in initial state", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { dropZone } = uploadPage
|
||||
|
||||
await expect(
|
||||
dropZone.getByText("Загрузка файла..."),
|
||||
).not.toBeVisible()
|
||||
await expect(
|
||||
dropZone.getByText("Не удалось загрузить файл"),
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Successful Upload", () => {
|
||||
test("should upload a valid video file and advance to the Verify step", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, testVideoPath } = uploadPage
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
// Wait for wizard to advance to Verify step
|
||||
await expect(
|
||||
page.locator("[data-testid='VerifyStep']"),
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
})
|
||||
|
||||
test("should show upload progress during file upload", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { dropZone, testVideoPath } = uploadPage
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
// Progress UI should appear (may be brief for small files)
|
||||
// We check that either progress appeared or the step already advanced
|
||||
const progressOrVerify = await Promise.race([
|
||||
dropZone
|
||||
.getByText("Загрузка файла...")
|
||||
.waitFor({ timeout: 5_000 })
|
||||
.then(() => "progress" as const)
|
||||
.catch(() => null),
|
||||
uploadPage.page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
.then(() => "verify" as const),
|
||||
])
|
||||
|
||||
expect(["progress", "verify"]).toContain(progressOrVerify)
|
||||
})
|
||||
|
||||
test("should show media info on Verify step after upload", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, testVideoPath } = uploadPage
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
const verifyStep = page.locator("[data-testid='VerifyStep']")
|
||||
await expect(verifyStep).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
// Badge should show "Готово к обработке" for MP4
|
||||
await expect(
|
||||
verifyStep.getByText("Готово к обработке"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// File info card should show the filename
|
||||
await expect(verifyStep.getByText("Файл")).toBeVisible()
|
||||
await expect(verifyStep.getByText("Размер и формат")).toBeVisible()
|
||||
})
|
||||
|
||||
test("should persist wizard state after upload completes", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, testVideoPath } = uploadPage
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
await expect(
|
||||
page.locator("[data-testid='VerifyStep']"),
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
// Wait for debounced state save (1000ms debounce + network)
|
||||
await page.waitForTimeout(2500)
|
||||
|
||||
await page.reload()
|
||||
await page.locator("[data-testid='ProjectWizard']").waitFor()
|
||||
|
||||
// Should remain on Verify step after reload
|
||||
await expect(
|
||||
page.locator("[data-testid='VerifyStep']"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Error States", () => {
|
||||
test("should show error message when upload fails due to network error", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
|
||||
// Intercept the upload XHR endpoint to abort
|
||||
await page.route("**/api/files/upload/**", (route) => route.abort())
|
||||
await page.route("**/api/files/upload/", (route) => route.abort())
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
await expect(
|
||||
dropZone.getByText("Не удалось загрузить файл"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Wizard stays on upload step
|
||||
await expect(dropZone).toBeVisible()
|
||||
await expect(
|
||||
page.locator("[data-testid='VerifyStep']"),
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test("should show error message when server returns 500", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
|
||||
await page.route("**/api/files/upload/**", (route) =>
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "Internal Server Error" }),
|
||||
}),
|
||||
)
|
||||
await page.route("**/api/files/upload/", (route) =>
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "Internal Server Error" }),
|
||||
}),
|
||||
)
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
await expect(
|
||||
dropZone.getByText("Не удалось загрузить файл"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Stays on upload step
|
||||
await expect(dropZone).toBeVisible()
|
||||
})
|
||||
|
||||
test("should allow retrying upload after a failure", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
|
||||
// First attempt: network error
|
||||
await page.route("**/api/files/upload/**", (route) => route.abort())
|
||||
await page.route("**/api/files/upload/", (route) => route.abort())
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
await expect(
|
||||
dropZone.getByText("Не удалось загрузить файл"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Remove intercepts and retry
|
||||
await page.unrouteAll({ behavior: "ignoreErrors" })
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
// Should succeed now and advance to Verify
|
||||
await expect(
|
||||
page.locator("[data-testid='VerifyStep']"),
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
})
|
||||
|
||||
test("should show error message when server returns 413", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, testVideoPath } = uploadPage
|
||||
|
||||
await page.route("**/api/files/upload/**", (route) =>
|
||||
route.fulfill({
|
||||
status: 413,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "File too large" }),
|
||||
}),
|
||||
)
|
||||
await page.route("**/api/files/upload/", (route) =>
|
||||
route.fulfill({
|
||||
status: 413,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: "File too large" }),
|
||||
}),
|
||||
)
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
await expect(
|
||||
dropZone.getByText("Не удалось загрузить файл"),
|
||||
).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("Edge Cases", () => {
|
||||
test("should disable file input during active upload", async ({
|
||||
uploadPage,
|
||||
}) => {
|
||||
const { page, dropZone, fileInput, testVideoPath } = uploadPage
|
||||
|
||||
// Delay the upload response to observe the uploading state
|
||||
await page.route("**/api/files/upload/**", async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
file_path: "projects/test/video.mp4",
|
||||
file_url: "http://localhost:9000/projects/test/video.mp4",
|
||||
}),
|
||||
})
|
||||
})
|
||||
await page.route("**/api/files/upload/", async (route) => {
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
file_path: "projects/test/video.mp4",
|
||||
file_url: "http://localhost:9000/projects/test/video.mp4",
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
await uploadPage.uploadFile(testVideoPath)
|
||||
|
||||
// During upload, the file input should be disabled
|
||||
await expect(
|
||||
dropZone.getByText("Загрузка файла..."),
|
||||
).toBeVisible({ timeout: 5_000 })
|
||||
await expect(fileInput).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user