iter 2
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { test as base, type Page, type Route } from "@playwright/test"
|
||||
|
||||
const DEFAULT_USER = {
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
phone_number: null,
|
||||
avatar: null,
|
||||
email_verified: true,
|
||||
phone_verified: false,
|
||||
is_active: true,
|
||||
is_staff: false,
|
||||
is_superuser: false,
|
||||
date_joined: "2025-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
const DEFAULT_TOKENS = {
|
||||
access: "fake-access-jwt",
|
||||
refresh: "fake-refresh-jwt",
|
||||
}
|
||||
|
||||
interface LoginPage {
|
||||
page: Page
|
||||
login(username: string, password: string): Promise<void>
|
||||
mockLoginSuccess(userData?: Record<string, unknown>): Promise<void>
|
||||
mockLoginError(status: number, body?: Record<string, unknown>): Promise<void>
|
||||
mockLoginNetworkError(): Promise<void>
|
||||
mockLoginDelayed(delayMs?: number): Promise<void>
|
||||
}
|
||||
|
||||
export const test = base.extend<{ loginPage: LoginPage }>({
|
||||
loginPage: async ({ page }, use) => {
|
||||
await page.goto("/login")
|
||||
await page.getByRole("heading", { name: "Вход" }).waitFor()
|
||||
|
||||
const loginPage: LoginPage = {
|
||||
page,
|
||||
|
||||
async login(username: string, password: string) {
|
||||
await page.getByRole("textbox", { name: "Логин" }).fill(username)
|
||||
await page.getByRole("textbox", { name: "Пароль" }).fill(password)
|
||||
await page.getByRole("button", { name: "Войти" }).click()
|
||||
},
|
||||
|
||||
async mockLoginSuccess(userData?: Record<string, unknown>) {
|
||||
const mockUser = { ...DEFAULT_USER, ...userData }
|
||||
|
||||
await page.route("**/auth/login", async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
user: mockUser,
|
||||
...DEFAULT_TOKENS,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/users/me/", async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mockUser),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route("**/api/projects/*", async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify([]),
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async mockLoginError(
|
||||
status: number,
|
||||
body?: Record<string, unknown>,
|
||||
) {
|
||||
await page.route("**/auth/login", async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
body ?? { detail: "Invalid credentials" },
|
||||
),
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async mockLoginNetworkError() {
|
||||
await page.route("**/auth/login", async (route: Route) => {
|
||||
await route.abort()
|
||||
})
|
||||
},
|
||||
|
||||
async mockLoginDelayed(delayMs = 2000) {
|
||||
await page.route("**/auth/login", async (route: Route) => {
|
||||
await new Promise((r) => setTimeout(r, delayMs))
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
user: DEFAULT_USER,
|
||||
...DEFAULT_TOKENS,
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
await use(loginPage)
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,159 @@
|
||||
import path from "node:path"
|
||||
import { test as base, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
import {
|
||||
createProjectViaApi,
|
||||
deleteProjectViaApi,
|
||||
loginAsAdmin,
|
||||
} from "#tests/e2e/support/auth-api"
|
||||
|
||||
export 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 },
|
||||
]
|
||||
export const MOCK_DURATION_MS = 60000
|
||||
export const MOCK_TOTAL_REMOVED_MS = MOCK_SEGMENTS.reduce(
|
||||
(sum, s) => sum + (s.end_ms - s.start_ms),
|
||||
0,
|
||||
)
|
||||
|
||||
interface FragmentsPage {
|
||||
page: Page
|
||||
projectId: string
|
||||
fragmentsStep: Locator
|
||||
jobId: string
|
||||
}
|
||||
|
||||
export const test = base.extend<{ fragmentsPage: FragmentsPage }>({
|
||||
fragmentsPage: async ({ page }, use) => {
|
||||
const tokens = await loginAsAdmin()
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: tokens.accessToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: tokens.refreshToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
const suffix = Date.now().toString(36)
|
||||
const projectId = await createProjectViaApi(
|
||||
tokens.accessToken,
|
||||
`fragments-test-${suffix}`,
|
||||
)
|
||||
|
||||
// Navigate to project wizard
|
||||
await page.goto(`/projects/${projectId}`)
|
||||
await page.locator("[data-testid='ProjectWizard']").waitFor()
|
||||
|
||||
// Upload test video file
|
||||
const testVideoPath = path.resolve(
|
||||
__dirname,
|
||||
"../assets/test-video.mp4",
|
||||
)
|
||||
const fileInput = page
|
||||
.locator("[data-testid='UploadStep']")
|
||||
.locator("input[type='file']")
|
||||
await fileInput.setInputFiles(testVideoPath)
|
||||
|
||||
// Wait for wizard to advance to Verify step
|
||||
await page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
|
||||
// Wait for file processing to complete
|
||||
await page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.getByText("Готово к обработке")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
|
||||
// Advance to Silence Settings step
|
||||
await page
|
||||
.getByRole("button", { name: "Далее: Настройки тишины" })
|
||||
.click()
|
||||
|
||||
const silenceStep = page.locator("[data-testid='SilenceSettingsStep']")
|
||||
await silenceStep.waitFor({ timeout: 10_000 })
|
||||
|
||||
// Intercept task status polling — initially return RUNNING
|
||||
let taskStatusResponse = {
|
||||
status: "RUNNING",
|
||||
progress_pct: 0,
|
||||
output_data: null as Record<string, unknown> | null,
|
||||
}
|
||||
|
||||
await page.route("**/api/tasks/status/**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(taskStatusResponse),
|
||||
})
|
||||
})
|
||||
|
||||
// Capture job_id from the silence-detect submission
|
||||
let capturedJobId = ""
|
||||
page.on("response", async (res) => {
|
||||
if (
|
||||
res.url().includes("/api/tasks/silence-detect/") &&
|
||||
res.request().method() === "POST" &&
|
||||
res.status() === 200
|
||||
) {
|
||||
try {
|
||||
const body = await res.json()
|
||||
if (body?.job_id) capturedJobId = body.job_id
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Click "Далее" to submit silence detection
|
||||
await silenceStep.getByRole("button", { name: "Далее" }).click()
|
||||
|
||||
// Wait for ProcessingStep to appear
|
||||
await page
|
||||
.locator("[data-testid='ProcessingStep']")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
|
||||
// Now switch the task status to DONE with mock data to trigger auto-advance
|
||||
taskStatusResponse = {
|
||||
status: "DONE",
|
||||
progress_pct: 100,
|
||||
output_data: {
|
||||
silent_segments: MOCK_SEGMENTS,
|
||||
duration_ms: MOCK_DURATION_MS,
|
||||
},
|
||||
}
|
||||
|
||||
// Wait for auto-advance to FragmentsStep (polls every 2s)
|
||||
const fragmentsStep = page.locator("[data-testid='FragmentsStep']")
|
||||
await fragmentsStep.waitFor({ timeout: 15_000 })
|
||||
|
||||
const fragmentsPage: FragmentsPage = {
|
||||
page,
|
||||
projectId,
|
||||
fragmentsStep,
|
||||
jobId: capturedJobId,
|
||||
}
|
||||
|
||||
await use(fragmentsPage)
|
||||
|
||||
// Cleanup: delete project
|
||||
try {
|
||||
await deleteProjectViaApi(tokens.accessToken, projectId)
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,138 @@
|
||||
import path from "node:path"
|
||||
import { test as base, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
import {
|
||||
createProjectViaApi,
|
||||
deleteProjectViaApi,
|
||||
loginAsAdmin,
|
||||
} from "#tests/e2e/support/auth-api"
|
||||
|
||||
interface ProcessingPage {
|
||||
page: Page
|
||||
projectId: string
|
||||
processingStep: Locator
|
||||
jobId: string
|
||||
}
|
||||
|
||||
export const test = base.extend<{ processingPage: ProcessingPage }>({
|
||||
processingPage: async ({ page }, use) => {
|
||||
const tokens = await loginAsAdmin()
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: tokens.accessToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: tokens.refreshToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
const suffix = Date.now().toString(36)
|
||||
const projectId = await createProjectViaApi(
|
||||
tokens.accessToken,
|
||||
`processing-test-${suffix}`,
|
||||
)
|
||||
|
||||
// Navigate to project wizard
|
||||
await page.goto(`/projects/${projectId}`)
|
||||
await page.locator("[data-testid='ProjectWizard']").waitFor()
|
||||
|
||||
// Upload test video file
|
||||
const testVideoPath = path.resolve(
|
||||
__dirname,
|
||||
"../assets/test-video.mp4",
|
||||
)
|
||||
const fileInput = page
|
||||
.locator("[data-testid='UploadStep']")
|
||||
.locator("input[type='file']")
|
||||
await fileInput.setInputFiles(testVideoPath)
|
||||
|
||||
// Wait for wizard to advance to Verify step
|
||||
await page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
|
||||
// Wait for file processing to complete
|
||||
await page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.getByText("Готово к обработке")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
|
||||
// Advance to Silence Settings step
|
||||
await page
|
||||
.getByRole("button", { name: "Далее: Настройки тишины" })
|
||||
.click()
|
||||
|
||||
const silenceStep = page.locator("[data-testid='SilenceSettingsStep']")
|
||||
await silenceStep.waitFor({ timeout: 10_000 })
|
||||
|
||||
// Intercept task status polling to keep returning RUNNING (prevent auto-advance)
|
||||
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,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
// Capture job_id from the silence-detect submission
|
||||
let capturedJobId = ""
|
||||
page.on("request", (req) => {
|
||||
if (
|
||||
req.url().includes("/api/tasks/silence-detect/") &&
|
||||
req.method() === "POST"
|
||||
) {
|
||||
// We'll get the job_id from the response
|
||||
}
|
||||
})
|
||||
page.on("response", async (res) => {
|
||||
if (
|
||||
res.url().includes("/api/tasks/silence-detect/") &&
|
||||
res.request().method() === "POST" &&
|
||||
res.status() === 200
|
||||
) {
|
||||
try {
|
||||
const body = await res.json()
|
||||
if (body?.job_id) capturedJobId = body.job_id
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Click "Далее" to submit silence detection
|
||||
await silenceStep.getByRole("button", { name: "Далее" }).click()
|
||||
|
||||
// Wait for ProcessingStep to appear
|
||||
const processingStep = page.locator("[data-testid='ProcessingStep']")
|
||||
await processingStep.waitFor({ timeout: 10_000 })
|
||||
|
||||
const processingPage: ProcessingPage = {
|
||||
page,
|
||||
projectId,
|
||||
processingStep,
|
||||
jobId: capturedJobId,
|
||||
}
|
||||
|
||||
await use(processingPage)
|
||||
|
||||
// Cleanup: delete project
|
||||
try {
|
||||
await deleteProjectViaApi(tokens.accessToken, projectId)
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,179 @@
|
||||
import { test as base, type Locator, type Page, type Route } from "@playwright/test"
|
||||
|
||||
const DEFAULT_USER = {
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
username: "testuser",
|
||||
email: "test@example.com",
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
phone_number: null,
|
||||
avatar: null,
|
||||
email_verified: true,
|
||||
phone_verified: false,
|
||||
is_active: true,
|
||||
is_staff: false,
|
||||
is_superuser: false,
|
||||
date_joined: "2025-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
const DEFAULT_PROJECT = {
|
||||
id: "00000000-0000-0000-0000-000000000002",
|
||||
owner_id: "00000000-0000-0000-0000-000000000001",
|
||||
name: "Тестовый проект",
|
||||
description: null,
|
||||
language: "auto",
|
||||
folder: null,
|
||||
status: "DRAFT",
|
||||
workspace_state: null,
|
||||
is_active: true,
|
||||
created_at: "2025-06-01T00:00:00Z",
|
||||
updated_at: "2025-06-01T00:00:00Z",
|
||||
}
|
||||
|
||||
interface ProjectsPage {
|
||||
page: Page
|
||||
/** Locator for the modal dialog (aria-modal prevents getByRole from working) */
|
||||
modal: Locator
|
||||
openCreateModal(): Promise<void>
|
||||
mockCreateSuccess(overrides?: Record<string, unknown>): Promise<void>
|
||||
mockCreateError(status: number, body?: Record<string, unknown>): Promise<void>
|
||||
mockCreateNetworkError(): Promise<void>
|
||||
mockCreateDelayed(ms?: number): Promise<void>
|
||||
}
|
||||
|
||||
export const test = base.extend<{ projectsPage: ProjectsPage }>({
|
||||
projectsPage: async ({ page }, use) => {
|
||||
let projectsList: Record<string, unknown>[] = []
|
||||
|
||||
// Set auth cookies
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: "fake-access-jwt",
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: "fake-refresh-jwt",
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
// Mock GET /api/users/me/
|
||||
await page.route("**/api/users/me/", async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(DEFAULT_USER),
|
||||
})
|
||||
})
|
||||
|
||||
// Mock GET /api/projects/ (stateful)
|
||||
await page.route("**/api/projects/*", async (route: Route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(projectsList),
|
||||
})
|
||||
} else {
|
||||
await route.fallback()
|
||||
}
|
||||
})
|
||||
|
||||
await page.route("**/api/projects/", async (route: Route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(projectsList),
|
||||
})
|
||||
} else {
|
||||
await route.fallback()
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto("/projects")
|
||||
await page.getByRole("heading", { name: "Мои проекты" }).waitFor()
|
||||
|
||||
const modal = page.locator("[role=dialog]")
|
||||
|
||||
const projectsPage: ProjectsPage = {
|
||||
page,
|
||||
modal,
|
||||
|
||||
async openCreateModal() {
|
||||
await page.getByRole("button", { name: /Создать проект/ }).click()
|
||||
await modal.waitFor({ state: "visible" })
|
||||
},
|
||||
|
||||
async mockCreateSuccess(overrides?: Record<string, unknown>) {
|
||||
const project = { ...DEFAULT_PROJECT, ...overrides }
|
||||
|
||||
await page.route("**/api/projects/", async (route: Route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
projectsList.push(project)
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(project),
|
||||
})
|
||||
} else {
|
||||
await route.fallback()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async mockCreateError(
|
||||
status: number,
|
||||
body?: Record<string, unknown>,
|
||||
) {
|
||||
await page.route("**/api/projects/", async (route: Route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body ?? { detail: "Server error" }),
|
||||
})
|
||||
} else {
|
||||
await route.fallback()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async mockCreateNetworkError() {
|
||||
await page.route("**/api/projects/", async (route: Route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await route.abort()
|
||||
} else {
|
||||
await route.fallback()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async mockCreateDelayed(ms = 2000) {
|
||||
const project = { ...DEFAULT_PROJECT }
|
||||
|
||||
await page.route("**/api/projects/", async (route: Route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await new Promise((r) => setTimeout(r, ms))
|
||||
projectsList.push(project)
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(project),
|
||||
})
|
||||
} else {
|
||||
await route.fallback()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
await use(projectsPage)
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,46 @@
|
||||
import { test as base, type Page } from "@playwright/test"
|
||||
|
||||
import { registerTestUser, type TestUser } from "#tests/e2e/support/auth-api"
|
||||
|
||||
interface RealLoginPage {
|
||||
page: Page
|
||||
user: TestUser
|
||||
login(password?: string): Promise<void>
|
||||
submitWithEnter(password?: string): Promise<void>
|
||||
getCookie(name: string): Promise<string | undefined>
|
||||
}
|
||||
|
||||
export const test = base.extend<{ realLoginPage: RealLoginPage }>({
|
||||
realLoginPage: async ({ page }, use) => {
|
||||
const user = await registerTestUser()
|
||||
|
||||
await page.goto("/login")
|
||||
await page.getByRole("heading", { name: "Вход" }).waitFor()
|
||||
|
||||
const realLoginPage: RealLoginPage = {
|
||||
page,
|
||||
user,
|
||||
|
||||
async login(password = user.password) {
|
||||
await page.getByRole("textbox", { name: "Логин" }).fill(user.username)
|
||||
await page.getByLabel("Пароль").fill(password)
|
||||
await page.getByRole("button", { name: "Войти" }).click()
|
||||
},
|
||||
|
||||
async submitWithEnter(password = user.password) {
|
||||
await page.getByRole("textbox", { name: "Логин" }).fill(user.username)
|
||||
await page.getByLabel("Пароль").fill(password)
|
||||
await page.getByLabel("Пароль").press("Enter")
|
||||
},
|
||||
|
||||
async getCookie(name: string) {
|
||||
const cookies = await page.context().cookies()
|
||||
return cookies.find((cookie) => cookie.name === name)?.value
|
||||
},
|
||||
}
|
||||
|
||||
await use(realLoginPage)
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,91 @@
|
||||
import { test as base, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
import {
|
||||
E2E_API_URL,
|
||||
registerTestUser,
|
||||
type TestUser,
|
||||
} from "#tests/e2e/support/auth-api"
|
||||
|
||||
interface RealProjectsPage {
|
||||
page: Page
|
||||
modal: Locator
|
||||
user: TestUser
|
||||
openCreateModal(): Promise<void>
|
||||
/** Delete a project by ID via API (for cleanup) */
|
||||
deleteProject(projectId: string): Promise<void>
|
||||
/** Get all projects for the test user via API */
|
||||
getProjects(): Promise<{ id: string; name: string }[]>
|
||||
}
|
||||
|
||||
export const test = base.extend<{ realProjectsPage: RealProjectsPage }>({
|
||||
realProjectsPage: async ({ page }, use) => {
|
||||
// Register a fresh user for each test
|
||||
const user = await registerTestUser()
|
||||
|
||||
// Set auth cookies
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: user.accessToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: user.refreshToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto("/projects")
|
||||
await page.getByRole("heading", { name: "Мои проекты" }).waitFor()
|
||||
|
||||
const modal = page.locator("[role=dialog]")
|
||||
|
||||
const realProjectsPage: RealProjectsPage = {
|
||||
page,
|
||||
modal,
|
||||
user,
|
||||
|
||||
async openCreateModal() {
|
||||
await page.getByRole("button", { name: /Создать проект/ }).click()
|
||||
await modal.waitFor({ state: "visible" })
|
||||
},
|
||||
|
||||
async deleteProject(projectId: string) {
|
||||
const res = await fetch(`${E2E_API_URL}/api/projects/${projectId}/`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${user.accessToken}` },
|
||||
})
|
||||
if (!res.ok && res.status !== 404) {
|
||||
throw new Error(`Delete project failed: ${res.status}`)
|
||||
}
|
||||
},
|
||||
|
||||
async getProjects() {
|
||||
const res = await fetch(`${E2E_API_URL}/api/projects/`, {
|
||||
headers: { Authorization: `Bearer ${user.accessToken}` },
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`Get projects failed: ${res.status}`)
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
}
|
||||
|
||||
await use(realProjectsPage)
|
||||
|
||||
// Cleanup: delete all projects created by this test user
|
||||
try {
|
||||
const projects = await realProjectsPage.getProjects()
|
||||
for (const project of projects) {
|
||||
await realProjectsPage.deleteProject(project.id)
|
||||
}
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,91 @@
|
||||
import path from "node:path"
|
||||
import { test as base, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
import {
|
||||
createProjectViaApi,
|
||||
deleteProjectViaApi,
|
||||
loginAsAdmin,
|
||||
} from "#tests/e2e/support/auth-api"
|
||||
|
||||
interface SilencePage {
|
||||
page: Page
|
||||
projectId: string
|
||||
silenceStep: Locator
|
||||
}
|
||||
|
||||
export const test = base.extend<{ silencePage: SilencePage }>({
|
||||
silencePage: async ({ page }, use) => {
|
||||
const tokens = await loginAsAdmin()
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: tokens.accessToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: tokens.refreshToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
const suffix = Date.now().toString(36)
|
||||
const projectId = await createProjectViaApi(
|
||||
tokens.accessToken,
|
||||
`silence-test-${suffix}`,
|
||||
)
|
||||
|
||||
// Navigate to project wizard
|
||||
await page.goto(`/projects/${projectId}`)
|
||||
await page.locator("[data-testid='ProjectWizard']").waitFor()
|
||||
|
||||
// Upload test video file
|
||||
const testVideoPath = path.resolve(
|
||||
__dirname,
|
||||
"../assets/test-video.mp4",
|
||||
)
|
||||
const fileInput = page
|
||||
.locator("[data-testid='UploadStep']")
|
||||
.locator("input[type='file']")
|
||||
await fileInput.setInputFiles(testVideoPath)
|
||||
|
||||
// Wait for wizard to advance to Verify step
|
||||
await page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.waitFor({ timeout: 30_000 })
|
||||
|
||||
// Wait for file processing to complete
|
||||
await page
|
||||
.locator("[data-testid='VerifyStep']")
|
||||
.getByText("Готово к обработке")
|
||||
.waitFor({ timeout: 10_000 })
|
||||
|
||||
// Advance to Silence Settings step
|
||||
await page
|
||||
.getByRole("button", { name: "Далее: Настройки тишины" })
|
||||
.click()
|
||||
|
||||
const silenceStep = page.locator("[data-testid='SilenceSettingsStep']")
|
||||
await silenceStep.waitFor({ timeout: 10_000 })
|
||||
|
||||
const silencePage: SilencePage = {
|
||||
page,
|
||||
projectId,
|
||||
silenceStep,
|
||||
}
|
||||
|
||||
await use(silencePage)
|
||||
|
||||
// Cleanup: delete project
|
||||
try {
|
||||
await deleteProjectViaApi(tokens.accessToken, projectId)
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
@@ -0,0 +1,90 @@
|
||||
import path from "node:path"
|
||||
import { test as base, type Locator, type Page } from "@playwright/test"
|
||||
|
||||
import {
|
||||
createProjectViaApi,
|
||||
deleteProjectViaApi,
|
||||
loginAsAdmin,
|
||||
} from "#tests/e2e/support/auth-api"
|
||||
|
||||
interface UploadPage {
|
||||
page: Page
|
||||
projectId: string
|
||||
dropZone: Locator
|
||||
fileInput: Locator
|
||||
/** Path to the minimal test MP4 file on disk */
|
||||
testVideoPath: string
|
||||
/** Upload a file by setting the hidden input (bypasses accept filter) */
|
||||
uploadFile(filePath: string): Promise<void>
|
||||
/** Upload with a synthetic buffer (name + MIME type + content) */
|
||||
uploadBuffer(name: string, mimeType: string, content: Buffer): Promise<void>
|
||||
}
|
||||
|
||||
export const test = base.extend<{ uploadPage: UploadPage }>({
|
||||
uploadPage: async ({ page }, use) => {
|
||||
const tokens = await loginAsAdmin()
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "access_token",
|
||||
value: tokens.accessToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "refresh_token",
|
||||
value: tokens.refreshToken,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
])
|
||||
|
||||
const suffix = Date.now().toString(36)
|
||||
const projectId = await createProjectViaApi(
|
||||
tokens.accessToken,
|
||||
`upload-test-${suffix}`,
|
||||
)
|
||||
|
||||
await page.goto(`/projects/${projectId}`)
|
||||
await page.locator("[data-testid='ProjectWizard']").waitFor()
|
||||
|
||||
const testVideoPath = path.resolve(
|
||||
__dirname,
|
||||
"../assets/test-video.mp4",
|
||||
)
|
||||
|
||||
const dropZone = page.locator("[data-testid='UploadStep']")
|
||||
const fileInput = dropZone.locator("input[type='file']")
|
||||
|
||||
const uploadPage: UploadPage = {
|
||||
page,
|
||||
projectId,
|
||||
dropZone,
|
||||
fileInput,
|
||||
testVideoPath,
|
||||
|
||||
async uploadFile(filePath: string) {
|
||||
await fileInput.setInputFiles(filePath)
|
||||
},
|
||||
|
||||
async uploadBuffer(name: string, mimeType: string, content: Buffer) {
|
||||
await fileInput.setInputFiles({
|
||||
name,
|
||||
mimeType,
|
||||
buffer: content,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
await use(uploadPage)
|
||||
|
||||
// Cleanup
|
||||
try {
|
||||
await deleteProjectViaApi(tokens.accessToken, projectId)
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { expect } from "@playwright/test"
|
||||
Reference in New Issue
Block a user