feat: add E2E tests for backend and frontend

Backend (pytest):
- Auth flow tests (login, token, protected routes)
- Portfolio CRUD and transaction tests
- Strategy endpoint tests
- Backtest flow tests
- Snapshot and returns tests

Frontend (Playwright):
- Auth page tests
- Portfolio navigation tests
- Strategy page tests
- Backtest page tests
- Playwright configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zephyrdark 2026-02-03 12:30:13 +09:00
parent e3b9ec1071
commit efcfc0e090
13 changed files with 1022 additions and 0 deletions

View File

@ -0,0 +1,3 @@
"""
Test package for Galaxy-PO backend.
"""

108
backend/tests/conftest.py Normal file
View File

@ -0,0 +1,108 @@
"""
Pytest configuration and fixtures for E2E tests.
"""
import os
import pytest
from typing import Generator
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import StaticPool
from app.main import app
from app.core.database import Base, get_db
from app.models.user import User
from app.core.auth import get_password_hash, create_access_token
# Use in-memory SQLite for tests
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def override_get_db():
"""Override database dependency for tests."""
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
@pytest.fixture(scope="function")
def db() -> Generator[Session, None, None]:
"""Create a fresh database for each test."""
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db: Session) -> Generator[TestClient, None, None]:
"""Create a test client with database override."""
app.dependency_overrides[get_db] = override_get_db
# Create tables
Base.metadata.create_all(bind=engine)
with TestClient(app) as test_client:
yield test_client
# Cleanup
Base.metadata.drop_all(bind=engine)
app.dependency_overrides.clear()
@pytest.fixture(scope="function")
def test_user(db: Session) -> User:
"""Create a test user."""
user = User(
username="testuser",
email="test@example.com",
hashed_password=get_password_hash("testpassword"),
is_admin=False,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture(scope="function")
def admin_user(db: Session) -> User:
"""Create an admin test user."""
user = User(
username="admin",
email="admin@example.com",
hashed_password=get_password_hash("adminpassword"),
is_admin=True,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture(scope="function")
def auth_headers(test_user: User) -> dict:
"""Create authorization headers for test user."""
token = create_access_token(data={"sub": test_user.username})
return {"Authorization": f"Bearer {token}"}
@pytest.fixture(scope="function")
def admin_auth_headers(admin_user: User) -> dict:
"""Create authorization headers for admin user."""
token = create_access_token(data={"sub": admin_user.username})
return {"Authorization": f"Bearer {token}"}

View File

@ -0,0 +1,3 @@
"""
E2E tests for Galaxy-PO API.
"""

View File

@ -0,0 +1,75 @@
"""
E2E tests for authentication flow.
"""
import pytest
from fastapi.testclient import TestClient
def test_health_check(client: TestClient):
"""Test health check endpoint."""
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
def test_login_success(client: TestClient, test_user):
"""Test successful login."""
response = client.post(
"/api/auth/login",
data={
"username": "testuser",
"password": "testpassword",
},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
def test_login_wrong_password(client: TestClient, test_user):
"""Test login with wrong password."""
response = client.post(
"/api/auth/login",
data={
"username": "testuser",
"password": "wrongpassword",
},
)
assert response.status_code == 401
def test_login_nonexistent_user(client: TestClient):
"""Test login with nonexistent user."""
response = client.post(
"/api/auth/login",
data={
"username": "nonexistent",
"password": "password",
},
)
assert response.status_code == 401
def test_get_current_user(client: TestClient, auth_headers):
"""Test getting current user info."""
response = client.get("/api/auth/me", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["username"] == "testuser"
assert data["email"] == "test@example.com"
def test_get_current_user_no_token(client: TestClient):
"""Test getting current user without token."""
response = client.get("/api/auth/me")
assert response.status_code == 401
def test_get_current_user_invalid_token(client: TestClient):
"""Test getting current user with invalid token."""
response = client.get(
"/api/auth/me",
headers={"Authorization": "Bearer invalid_token"},
)
assert response.status_code == 401

View File

@ -0,0 +1,140 @@
"""
E2E tests for backtest flow.
"""
import pytest
from fastapi.testclient import TestClient
def test_create_backtest(client: TestClient, auth_headers):
"""Test creating a backtest."""
response = client.post(
"/api/backtest",
json={
"strategy_type": "multi_factor",
"strategy_params": {
"weights": {"value": 0.3, "quality": 0.3, "momentum": 0.2, "f_score": 0.2}
},
"start_date": "2023-01-01",
"end_date": "2023-12-31",
"rebalance_period": "quarterly",
"initial_capital": 100000000,
"commission_rate": 0.00015,
"slippage_rate": 0.001,
"benchmark": "KODEX200",
"top_n": 20,
},
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert "id" in data
assert data["status"] == "pending"
def test_list_backtests(client: TestClient, auth_headers):
"""Test listing backtests."""
# Create a backtest first
client.post(
"/api/backtest",
json={
"strategy_type": "quality",
"strategy_params": {"min_f_score": 6},
"start_date": "2023-01-01",
"end_date": "2023-06-30",
"rebalance_period": "monthly",
"initial_capital": 50000000,
"commission_rate": 0.00015,
"slippage_rate": 0.001,
"benchmark": "KODEX200",
"top_n": 10,
},
headers=auth_headers,
)
# List backtests
response = client.get("/api/backtest", headers=auth_headers)
assert response.status_code == 200
backtests = response.json()
assert len(backtests) >= 1
def test_get_backtest(client: TestClient, auth_headers):
"""Test getting backtest details."""
# Create a backtest
create_response = client.post(
"/api/backtest",
json={
"strategy_type": "value_momentum",
"strategy_params": {"value_weight": 0.6, "momentum_weight": 0.4},
"start_date": "2023-01-01",
"end_date": "2023-03-31",
"rebalance_period": "monthly",
"initial_capital": 10000000,
"commission_rate": 0.00015,
"slippage_rate": 0.001,
"benchmark": "KODEX200",
"top_n": 5,
},
headers=auth_headers,
)
backtest_id = create_response.json()["id"]
# Get backtest
response = client.get(f"/api/backtest/{backtest_id}", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["id"] == backtest_id
assert data["strategy_type"] == "value_momentum"
def test_delete_backtest(client: TestClient, auth_headers):
"""Test deleting a backtest."""
# Create a backtest
create_response = client.post(
"/api/backtest",
json={
"strategy_type": "multi_factor",
"strategy_params": {},
"start_date": "2023-01-01",
"end_date": "2023-01-31",
"rebalance_period": "monthly",
"initial_capital": 1000000,
"commission_rate": 0.00015,
"slippage_rate": 0.001,
"benchmark": "KODEX200",
"top_n": 3,
},
headers=auth_headers,
)
backtest_id = create_response.json()["id"]
# Delete backtest
response = client.delete(f"/api/backtest/{backtest_id}", headers=auth_headers)
assert response.status_code == 200
# Verify deleted
response = client.get(f"/api/backtest/{backtest_id}", headers=auth_headers)
assert response.status_code == 404
def test_backtest_requires_auth(client: TestClient):
"""Test that backtest endpoints require authentication."""
response = client.get("/api/backtest")
assert response.status_code == 401
response = client.post(
"/api/backtest",
json={
"strategy_type": "multi_factor",
"strategy_params": {},
"start_date": "2023-01-01",
"end_date": "2023-12-31",
"rebalance_period": "quarterly",
"initial_capital": 100000000,
"commission_rate": 0.00015,
"slippage_rate": 0.001,
"benchmark": "KODEX200",
"top_n": 20,
},
)
assert response.status_code == 401

View File

@ -0,0 +1,217 @@
"""
E2E tests for portfolio management flow.
"""
import pytest
from fastapi.testclient import TestClient
def test_portfolio_crud_flow(client: TestClient, auth_headers):
"""Test complete portfolio CRUD flow."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={
"name": "Test Portfolio",
"portfolio_type": "pension",
},
headers=auth_headers,
)
assert response.status_code == 201
portfolio = response.json()
portfolio_id = portfolio["id"]
assert portfolio["name"] == "Test Portfolio"
assert portfolio["portfolio_type"] == "pension"
# Get portfolio list
response = client.get("/api/portfolios", headers=auth_headers)
assert response.status_code == 200
portfolios = response.json()
assert len(portfolios) == 1
assert portfolios[0]["id"] == portfolio_id
# Get single portfolio
response = client.get(f"/api/portfolios/{portfolio_id}", headers=auth_headers)
assert response.status_code == 200
assert response.json()["id"] == portfolio_id
# Update portfolio
response = client.put(
f"/api/portfolios/{portfolio_id}",
json={"name": "Updated Portfolio"},
headers=auth_headers,
)
assert response.status_code == 200
assert response.json()["name"] == "Updated Portfolio"
# Delete portfolio
response = client.delete(f"/api/portfolios/{portfolio_id}", headers=auth_headers)
assert response.status_code == 204
# Verify deleted
response = client.get(f"/api/portfolios/{portfolio_id}", headers=auth_headers)
assert response.status_code == 404
def test_targets_flow(client: TestClient, auth_headers):
"""Test portfolio target allocation flow."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Target Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Set targets
targets = [
{"ticker": "005930", "target_ratio": 40},
{"ticker": "000660", "target_ratio": 30},
{"ticker": "035420", "target_ratio": 30},
]
response = client.put(
f"/api/portfolios/{portfolio_id}/targets",
json=targets,
headers=auth_headers,
)
assert response.status_code == 200
result = response.json()
assert len(result) == 3
assert sum(t["target_ratio"] for t in result) == 100
# Get targets
response = client.get(
f"/api/portfolios/{portfolio_id}/targets",
headers=auth_headers,
)
assert response.status_code == 200
assert len(response.json()) == 3
# Test invalid targets (not 100%)
invalid_targets = [
{"ticker": "005930", "target_ratio": 50},
{"ticker": "000660", "target_ratio": 30},
]
response = client.put(
f"/api/portfolios/{portfolio_id}/targets",
json=invalid_targets,
headers=auth_headers,
)
assert response.status_code == 400
def test_holdings_flow(client: TestClient, auth_headers):
"""Test portfolio holdings flow."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Holdings Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Set holdings
holdings = [
{"ticker": "005930", "quantity": 10, "avg_price": 70000},
{"ticker": "000660", "quantity": 5, "avg_price": 120000},
]
response = client.put(
f"/api/portfolios/{portfolio_id}/holdings",
json=holdings,
headers=auth_headers,
)
assert response.status_code == 200
result = response.json()
assert len(result) == 2
# Get holdings
response = client.get(
f"/api/portfolios/{portfolio_id}/holdings",
headers=auth_headers,
)
assert response.status_code == 200
assert len(response.json()) == 2
def test_transaction_flow(client: TestClient, auth_headers):
"""Test transaction recording flow."""
# Create portfolio with initial holdings
response = client.post(
"/api/portfolios",
json={"name": "Transaction Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Buy transaction
response = client.post(
f"/api/portfolios/{portfolio_id}/transactions",
json={
"ticker": "005930",
"tx_type": "buy",
"quantity": 10,
"price": 70000,
"executed_at": "2024-01-15T10:00:00",
"memo": "Initial purchase",
},
headers=auth_headers,
)
assert response.status_code == 201
tx = response.json()
assert tx["ticker"] == "005930"
assert tx["tx_type"] == "buy"
# Verify holdings updated
response = client.get(
f"/api/portfolios/{portfolio_id}/holdings",
headers=auth_headers,
)
holdings = response.json()
assert len(holdings) == 1
assert holdings[0]["ticker"] == "005930"
assert holdings[0]["quantity"] == 10
# Sell transaction
response = client.post(
f"/api/portfolios/{portfolio_id}/transactions",
json={
"ticker": "005930",
"tx_type": "sell",
"quantity": 5,
"price": 75000,
"executed_at": "2024-01-16T10:00:00",
},
headers=auth_headers,
)
assert response.status_code == 201
# Verify holdings updated
response = client.get(
f"/api/portfolios/{portfolio_id}/holdings",
headers=auth_headers,
)
holdings = response.json()
assert holdings[0]["quantity"] == 5
# Get transactions
response = client.get(
f"/api/portfolios/{portfolio_id}/transactions",
headers=auth_headers,
)
assert response.status_code == 200
txs = response.json()
assert len(txs) == 2
def test_unauthorized_access(client: TestClient, auth_headers):
"""Test that users can't access other users' portfolios."""
# Create portfolio with test user
response = client.post(
"/api/portfolios",
json={"name": "Private Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Try to access without auth
response = client.get(f"/api/portfolios/{portfolio_id}")
assert response.status_code == 401

View File

@ -0,0 +1,122 @@
"""
E2E tests for snapshot and returns flow.
"""
import pytest
from fastapi.testclient import TestClient
def test_snapshot_requires_holdings(client: TestClient, auth_headers):
"""Test that snapshot creation requires holdings."""
# Create portfolio without holdings
response = client.post(
"/api/portfolios",
json={"name": "Empty Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Try to create snapshot
response = client.post(
f"/api/portfolios/{portfolio_id}/snapshots",
headers=auth_headers,
)
assert response.status_code == 400
assert "empty" in response.json()["detail"].lower()
def test_snapshot_list_empty(client: TestClient, auth_headers):
"""Test listing snapshots for portfolio with no snapshots."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Snapshot Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# List snapshots
response = client.get(
f"/api/portfolios/{portfolio_id}/snapshots",
headers=auth_headers,
)
assert response.status_code == 200
assert response.json() == []
def test_returns_empty(client: TestClient, auth_headers):
"""Test returns for portfolio with no snapshots."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Returns Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Get returns
response = client.get(
f"/api/portfolios/{portfolio_id}/returns",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["portfolio_id"] == portfolio_id
assert data["total_return"] is None
assert data["data"] == []
def test_snapshot_not_found(client: TestClient, auth_headers):
"""Test getting non-existent snapshot."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Not Found Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Get non-existent snapshot
response = client.get(
f"/api/portfolios/{portfolio_id}/snapshots/99999",
headers=auth_headers,
)
assert response.status_code == 404
def test_snapshot_delete_not_found(client: TestClient, auth_headers):
"""Test deleting non-existent snapshot."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Delete Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Delete non-existent snapshot
response = client.delete(
f"/api/portfolios/{portfolio_id}/snapshots/99999",
headers=auth_headers,
)
assert response.status_code == 404
def test_snapshot_requires_auth(client: TestClient, auth_headers):
"""Test that snapshot endpoints require authentication."""
# Create portfolio
response = client.post(
"/api/portfolios",
json={"name": "Auth Test Portfolio", "portfolio_type": "general"},
headers=auth_headers,
)
portfolio_id = response.json()["id"]
# Try without auth
response = client.get(f"/api/portfolios/{portfolio_id}/snapshots")
assert response.status_code == 401
response = client.post(f"/api/portfolios/{portfolio_id}/snapshots")
assert response.status_code == 401
response = client.get(f"/api/portfolios/{portfolio_id}/returns")
assert response.status_code == 401

View File

@ -0,0 +1,86 @@
"""
E2E tests for quant strategy flow.
"""
import pytest
from fastapi.testclient import TestClient
def test_multi_factor_strategy(client: TestClient, auth_headers):
"""Test multi-factor strategy endpoint."""
response = client.post(
"/api/strategy/multi-factor",
json={
"market": "KOSPI",
"min_market_cap": 100000000000,
"top_n": 20,
"weights": {
"value": 0.3,
"quality": 0.3,
"momentum": 0.2,
"f_score": 0.2,
},
},
headers=auth_headers,
)
# May fail if no stock data, just check it returns a proper response
assert response.status_code in [200, 400, 500]
if response.status_code == 200:
data = response.json()
assert "stocks" in data
assert "strategy_type" in data
assert data["strategy_type"] == "multi_factor"
def test_quality_strategy(client: TestClient, auth_headers):
"""Test quality strategy endpoint."""
response = client.post(
"/api/strategy/quality",
json={
"market": "KOSPI",
"min_market_cap": 100000000000,
"top_n": 20,
"min_f_score": 6,
},
headers=auth_headers,
)
assert response.status_code in [200, 400, 500]
if response.status_code == 200:
data = response.json()
assert "stocks" in data
assert data["strategy_type"] == "quality"
def test_value_momentum_strategy(client: TestClient, auth_headers):
"""Test value-momentum strategy endpoint."""
response = client.post(
"/api/strategy/value-momentum",
json={
"market": "KOSPI",
"min_market_cap": 100000000000,
"top_n": 20,
"value_weight": 0.5,
"momentum_weight": 0.5,
},
headers=auth_headers,
)
assert response.status_code in [200, 400, 500]
if response.status_code == 200:
data = response.json()
assert "stocks" in data
assert data["strategy_type"] == "value_momentum"
def test_strategy_requires_auth(client: TestClient):
"""Test that strategy endpoints require authentication."""
response = client.post(
"/api/strategy/multi-factor",
json={
"market": "KOSPI",
"min_market_cap": 100000000000,
"top_n": 20,
},
)
assert response.status_code == 401

34
frontend/e2e/auth.spec.ts Normal file
View File

@ -0,0 +1,34 @@
import { test, expect } from "@playwright/test";
test.describe("Authentication", () => {
test("should show login page", async ({ page }) => {
await page.goto("/login");
await expect(page.locator("h1")).toContainText("로그인");
await expect(page.locator('input[name="username"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toBeVisible();
});
test("should show error on invalid login", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="username"]', "invaliduser");
await page.fill('input[name="password"]', "invalidpassword");
await page.click('button[type="submit"]');
// Wait for error message
await expect(page.locator(".text-red-700, .text-red-600")).toBeVisible({
timeout: 5000,
});
});
test("should redirect to login when accessing protected page", async ({
page,
}) => {
await page.goto("/portfolio");
// Should redirect to login
await expect(page).toHaveURL(/\/login/);
});
});

View File

@ -0,0 +1,77 @@
import { test, expect } from "@playwright/test";
test.describe("Backtest", () => {
test.beforeEach(async ({ page }) => {
// Mock login
await page.addInitScript(() => {
localStorage.setItem("token", "test-token");
});
});
test("should show backtest page", async ({ page }) => {
await page.goto("/backtest");
// Check page title
await expect(page.locator("h1")).toContainText("백테스트");
});
test("should show backtest form", async ({ page }) => {
await page.goto("/backtest");
// Check for strategy selection
await expect(
page.locator('select, [role="combobox"], input[type="radio"]').first()
).toBeVisible();
});
test("should show date inputs", async ({ page }) => {
await page.goto("/backtest");
// Check for date inputs
await expect(
page.locator('input[type="date"], input[placeholder*="날짜"]').first()
).toBeVisible();
});
test("should show initial capital input", async ({ page }) => {
await page.goto("/backtest");
// Check for capital input
await expect(
page
.locator('input[name*="capital"], input[placeholder*="자본"]')
.first()
).toBeVisible();
});
test("should show rebalance period selection", async ({ page }) => {
await page.goto("/backtest");
// Check for period selection
const periodText = await page.locator(
'text=월별, text=분기별, text=리밸런싱, select'
);
await expect(periodText.first()).toBeVisible();
});
test("should have submit button", async ({ page }) => {
await page.goto("/backtest");
// Check for submit button
await expect(
page
.locator(
'button[type="submit"], button:has-text("실행"), button:has-text("시작")'
)
.first()
).toBeVisible();
});
test("should navigate to backtest result page", async ({ page }) => {
// Assuming backtest with ID 1 exists
await page.goto("/backtest/1");
// Should show result page or error
await expect(page.locator("body")).toBeVisible();
});
});

View File

@ -0,0 +1,66 @@
import { test, expect } from "@playwright/test";
// Helper to login before tests
async function login(page: import("@playwright/test").Page) {
await page.goto("/login");
await page.fill('input[name="username"]', "testuser");
await page.fill('input[name="password"]', "testpassword");
await page.click('button[type="submit"]');
// Wait for redirect to dashboard or portfolio page
await page.waitForURL(/\/(portfolio)?$/);
}
test.describe("Portfolio", () => {
test.beforeEach(async ({ page }) => {
// Mock login - in real tests, use proper authentication
await page.addInitScript(() => {
localStorage.setItem("token", "test-token");
});
});
test("should show portfolio list page", async ({ page }) => {
await page.goto("/portfolio");
// Check page title
await expect(page.locator("h1")).toContainText("포트폴리오");
});
test("should have create portfolio button", async ({ page }) => {
await page.goto("/portfolio");
// Look for create button or link
const createButton = page.locator('a[href="/portfolio/new"], button:has-text("생성"), button:has-text("새")');
await expect(createButton.first()).toBeVisible();
});
test("should navigate to new portfolio page", async ({ page }) => {
await page.goto("/portfolio/new");
// Check for form elements
await expect(page.locator('input[name="name"], input[placeholder*="이름"]').first()).toBeVisible();
});
test("should navigate to portfolio detail page", async ({ page }) => {
// Assuming portfolio with ID 1 exists
await page.goto("/portfolio/1");
// Should show portfolio content or redirect
// The exact behavior depends on whether the portfolio exists
await expect(page.locator("body")).toBeVisible();
});
test("should navigate to history page", async ({ page }) => {
await page.goto("/portfolio/1/history");
// Should show history page or error
await expect(page.locator("body")).toBeVisible();
});
test("should navigate to rebalance page", async ({ page }) => {
await page.goto("/portfolio/1/rebalance");
// Should show rebalance page or error
await expect(page.locator("body")).toBeVisible();
});
});

View File

@ -0,0 +1,65 @@
import { test, expect } from "@playwright/test";
test.describe("Strategy", () => {
test.beforeEach(async ({ page }) => {
// Mock login
await page.addInitScript(() => {
localStorage.setItem("token", "test-token");
});
});
test("should show strategy list page", async ({ page }) => {
await page.goto("/strategy");
// Check page title
await expect(page.locator("h1")).toContainText("전략");
});
test("should show multi-factor strategy option", async ({ page }) => {
await page.goto("/strategy");
// Look for multi-factor strategy card or link
await expect(
page.locator('text=멀티 팩터, a[href*="multi-factor"]').first()
).toBeVisible();
});
test("should show quality strategy option", async ({ page }) => {
await page.goto("/strategy");
// Look for quality strategy card or link
await expect(
page.locator('text=퀄리티, a[href*="quality"]').first()
).toBeVisible();
});
test("should show value-momentum strategy option", async ({ page }) => {
await page.goto("/strategy");
// Look for value-momentum strategy card or link
await expect(
page.locator('text=밸류 모멘텀, a[href*="value-momentum"]').first()
).toBeVisible();
});
test("should navigate to multi-factor strategy page", async ({ page }) => {
await page.goto("/strategy/multi-factor");
// Check for form elements
await expect(page.locator("form, [role='form']").first()).toBeVisible();
});
test("should navigate to quality strategy page", async ({ page }) => {
await page.goto("/strategy/quality");
// Check for form elements
await expect(page.locator("form, [role='form']").first()).toBeVisible();
});
test("should navigate to value-momentum strategy page", async ({ page }) => {
await page.goto("/strategy/value-momentum");
// Check for form elements
await expect(page.locator("form, [role='form']").first()).toBeVisible();
});
});

View File

@ -0,0 +1,26 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});