Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
.venv/
__pycache__/
*.mp4
!clipcraft-web/e2e/fixtures/sample.mp4
clip_search/clips/
28 changes: 28 additions & 0 deletions clipcraft-web/dist/assets/index-DOoTTw61.js

Large diffs are not rendered by default.

28 changes: 0 additions & 28 deletions clipcraft-web/dist/assets/index-DW3LtBA3.js

This file was deleted.

1 change: 1 addition & 0 deletions clipcraft-web/dist/assets/index-DeWz9RhU.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion clipcraft-web/dist/assets/index-DgQ3_1pY.css

This file was deleted.

4 changes: 2 additions & 2 deletions clipcraft-web/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>ClipCraft</title>
<script type="module" crossorigin src="/assets/index-DW3LtBA3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DgQ3_1pY.css">
<script type="module" crossorigin src="/assets/index-DOoTTw61.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DeWz9RhU.css">
</head>
<body>
<div id="root"></div>
Expand Down
135 changes: 135 additions & 0 deletions clipcraft-web/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# ClipCraft E2E QA

Playwright 기반 핵심 사용자 플로우 테스트입니다.

## 사전 준비

```bash
cd clipcraft-web
npm install
npx playwright install chromium
```

CI 환경에서는 workflow가 `npx playwright install --with-deps`를 실행합니다.

## 실행 방법

전체 E2E 테스트:

```bash
npm run test:e2e
```

브라우저를 보면서 실행:

```bash
npm run test:e2e:headed
```

Playwright UI 모드:

```bash
npm run test:e2e:ui
```

특정 테스트만 실행:

```bash
npx playwright test e2e/auth.spec.ts
npx playwright test e2e/project.spec.ts
npx playwright test e2e/analysis-flow.spec.ts
npx playwright test e2e/editor.spec.ts
```

## 테스트 구성

- `auth.spec.ts`
- 랜딩 페이지 접속
- 로그인 페이지 이동
- 이메일/비밀번호 입력
- workspace 이동 확인

- `project.spec.ts`
- workspace에서 새 프로젝트 생성
- 프로젝트 이름 입력
- 기본 영상 비율 `세로 9:16` 확인
- 업로드 화면에 프로젝트 정보 반영 확인

- `analysis-flow.spec.ts`
- 샘플 영상 업로드
- 시나리오 3개 입력
- 분석 API route mocking
- 추천 결과 카드 3개 표시 확인
- start/end/score 정보 표시 확인

- `editor.spec.ts`
- 세그먼트 선택
- 세그먼트 시간 조정 핸들 확인
- 재생 버튼 동작 확인
- 채팅 자연어 명령 확인
- `2배속해줘`
- `음소거해줘`
- `보여줘`
- `삭제해줘`

## Mock 동작

분석 API는 실제 백엔드를 호출하지 않습니다.

`e2e/support/flows.ts`에서 Playwright `page.route()`로 아래 요청을 mock 처리합니다.

- `POST **/analyze/jobs`
- `GET **/analyze/jobs/mock-job`

mock 응답은 시나리오 입력 3개에 맞춰 추천 구간 3개를 반환합니다.

## 테스트 결과 위치

Playwright 기본 결과 위치:

- HTML 리포트: `clipcraft-web/playwright-report/`
- 실패 디버깅 파일: `clipcraft-web/test-results/`

현재 설정:

- 실패 시 video 저장: `video: 'retain-on-failure'`
- retry 시 trace 저장: `trace: 'on-first-retry'`

HTML 리포트 보기:

```bash
npx playwright show-report
```

## GitHub Actions

workflow:

```text
.github/workflows/clipcraft-web-e2e.yml
```

push 또는 PR에서 `clipcraft-web/**` 변경이 있으면 실행됩니다.

실행 순서:

1. `npm install`
2. `npm run build`
3. `npx playwright install --with-deps`
4. `npm run test:e2e`

## 참고

Playwright 설정 파일:

```text
clipcraft-web/playwright.config.ts
```

테스트용 샘플 영상:

```text
clipcraft-web/e2e/fixtures/sample.mp4
```

이 파일은 작고 고정된 fixture로, 실제 AI 분석 결과와 무관하게 안정적인 테스트를 위해 사용합니다.
19 changes: 19 additions & 0 deletions clipcraft-web/e2e/analysis-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, test } from '@playwright/test';
import { addScenarios, createProject, mockAnalysisApi, uploadSampleVideo } from './support/flows';

test('scenario input starts analysis and renders three recommendation cards', async ({ page }) => {
await mockAnalysisApi(page);
await createProject(page);
await uploadSampleVideo(page);
await addScenarios(page);

await page.getByTestId('analyze-start-button').click();
await expect(page.getByTestId('loading-page')).toBeVisible();
await expect(page).toHaveURL(/\/editor\/mock-job$/);

const cards = page.getByTestId('result-card');
await expect(cards).toHaveCount(3);
await expect(page.getByTestId('result-start-time')).toHaveCount(3);
await expect(page.getByTestId('result-end-time')).toHaveCount(3);
await expect(page.getByTestId('result-score')).toHaveCount(3);
});
18 changes: 18 additions & 0 deletions clipcraft-web/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from '@playwright/test';

test('landing to login to workspace flow works', async ({ page }) => {
await page.goto('/');

await expect(page.getByAltText('ClipCraft').first()).toBeVisible();
await expect(page.getByRole('button', { name: '지금 시작하기' }).first()).toBeVisible();

await page.getByTestId('login-link').click();
await expect(page).toHaveURL(/\/login$/);

await page.getByTestId('email-input').fill('qa@example.com');
await page.getByTestId('password-input').fill('Password123');
await page.getByTestId('login-submit-button').click();

await expect(page).toHaveURL(/\/workspace$/);
await expect(page.getByRole('heading', { name: '내 프로젝트' })).toBeVisible();
});
45 changes: 45 additions & 0 deletions clipcraft-web/e2e/editor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from '@playwright/test';
import { runAnalysisFlow } from './support/flows';

async function sendCommand(page: import('@playwright/test').Page, command: string) {
await page.getByTestId('chat-input').fill(command);
await page.getByTestId('chat-send-button').click();
}

test('editor supports segment selection, timing controls, playback and chat commands', async ({ page }) => {
await runAnalysisFlow(page);

const firstCard = page.getByTestId('result-card').first();
await firstCard.click();
await expect(firstCard).toHaveAttribute('data-active', 'true');

const secondCard = page.getByTestId('result-card').nth(1);
await secondCard.click();
await expect(secondCard).toHaveAttribute('data-active', 'true');

await page.getByTestId('segment-start-input').first().hover();
await page.mouse.down();
await page.mouse.move(260, 180);
await page.mouse.up();

await page.getByTestId('segment-end-input').first().hover();
await page.mouse.down();
await page.mouse.move(420, 180);
await page.mouse.up();

await page.getByTestId('play-button').click();
await expect(page.getByTestId('play-button')).toBeVisible();

await page.getByTestId('result-card').first().click();
await sendCommand(page, '2배속해줘');
await expect(page.getByTestId('playback-rate-indicator').first()).toBeVisible();

await sendCommand(page, '음소거해줘');
await expect(page.getByTestId('mute-indicator').first()).toBeVisible();

await sendCommand(page, '보여줘');
await expect(page.getByTestId('result-card').first()).toBeVisible();

await sendCommand(page, '삭제해줘');
await expect(page.getByTestId('hidden-segment-indicator')).toBeVisible();
});
Binary file added clipcraft-web/e2e/fixtures/sample.mp4
Binary file not shown.
10 changes: 10 additions & 0 deletions clipcraft-web/e2e/project.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test } from '@playwright/test';
import { createProject, projectName } from './support/flows';

test('workspace creates a project with default vertical aspect ratio', async ({ page }) => {
await createProject(page);

await expect(page.getByTestId('video-upload-area')).toBeVisible();
await expect(page.getByTestId('project-title')).toHaveText(projectName);
await expect(page.getByTestId('project-aspect-ratio')).toHaveText('세로 9:16');
});
97 changes: 97 additions & 0 deletions clipcraft-web/e2e/support/flows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { expect, type Page, type Route } from '@playwright/test';
import path from 'node:path';

export const projectName = '김치찌개 브이로그 쇼츠';
export const scenarios = ['재료를 손질하는 장면', '김치찌개가 끓는 장면', '완성된 음식을 담는 장면'];
export const sampleVideoPath = path.join(process.cwd(), 'e2e/fixtures/sample.mp4');

export async function login(page: Page) {
await page.goto('/login');
await page.getByTestId('email-input').fill('qa@example.com');
await page.getByTestId('password-input').fill('Password123');
await page.getByTestId('login-submit-button').click();
await expect(page).toHaveURL(/\/workspace$/);
}

export async function createProject(page: Page, name = projectName) {
await page.goto('/workspace');
await page.getByTestId('new-project-button').click();
await page.getByTestId('project-name-input').fill(name);
await expect(page.getByTestId('aspect-ratio-select').first()).toContainText('세로 9:16');
await page.getByTestId('start-editing-button').click();
await expect(page).toHaveURL(/\/upload$/);
await expect(page.getByTestId('project-title')).toHaveText(name);
await expect(page.getByTestId('project-aspect-ratio')).toHaveText('세로 9:16');
}

export async function uploadSampleVideo(page: Page) {
await expect(page.getByTestId('video-upload-area')).toBeVisible();
await page.locator('input[type="file"]').setInputFiles(sampleVideoPath);
await expect(page.getByText('sample.mp4')).toBeVisible();
}

export async function addScenarios(page: Page) {
const input = page.getByTestId('scenario-input');
for (const scenario of scenarios) {
await input.fill(scenario);
await page.getByTestId('add-scenario-button').click();
await expect(page.getByText(scenario)).toBeVisible();
}
}

export async function mockAnalysisApi(page: Page) {
await page.route('**/analyze/jobs', async (route: Route) => {
if (route.request().method() !== 'POST') {
await route.fallback();
return;
}

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'success', job_id: 'mock-job' }),
});
});

await page.route('**/analyze/jobs/mock-job', async (route: Route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
progress: 100,
step_id: 5,
step_label: '하이라이트 구간 확정',
message: 'mock analysis complete',
logs: ['mock: received video', 'mock: matched 3 scenarios'],
project: projectName,
results: scenarios.map((scenario, index) => ({
project_name: projectName,
id: index + 1,
scenario,
title: scenario,
start: 12 + index * 20,
end: 20 + index * 20,
score: Number((0.94 - index * 0.04).toFixed(2)),
audio: {
duration: 90,
barCount: 88,
amplitudes: Array.from({ length: 88 }, (_, barIndex) => Number((0.25 + Math.abs(Math.sin(barIndex * 0.35)) * 0.7).toFixed(3))),
},
})),
error: null,
}),
});
});
}

export async function runAnalysisFlow(page: Page) {
await mockAnalysisApi(page);
await createProject(page);
await uploadSampleVideo(page);
await addScenarios(page);
await page.getByTestId('analyze-start-button').click();
await expect(page.getByTestId('loading-page')).toBeVisible();
await expect(page).toHaveURL(/\/editor\/mock-job$/);
await expect(page.getByTestId('result-card')).toHaveCount(3);
}
1 change: 1 addition & 0 deletions clipcraft-web/node_modules/.bin/playwright

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clipcraft-web/node_modules/.bin/playwright-core

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading