Files
taxbaik/tests/e2e/admin-responsive.spec.ts
T
kjh2064 700cdaed4f
TaxBaik CI/CD / build-and-deploy (push) Successful in 47s
test: fix E2E base URL for green-blue deployment and use test account
Green-Blue 배포에서 E2E 테스트가 항상 새 버전을 테스트하도록 개선:

Changes:
- E2E_BASE_URL default: http://localhost/taxbaik (Nginx 라우팅 → active 포트)
- 이전: http://localhost:5001/taxbaik (하드코드, 구 버전 테스트 위험)
- CI/E2E 워크플로우: test_admin 계정으로 변경 (실 admin 분리)
- Playwright config 주석 명확화 (Green-Blue 배포 지원)
- 로컬 테스트: Nginx 거쳐서 또는 명시적 포트 설정

Architecture:
┌─────────────────────────┐
│  E2E Test Runner        │
│  (test_admin account)   │
└────────────┬────────────┘
             │
    E2E_BASE_URL (env var)
             │
    ┌────────┴────────┐
    │                 │
 http://localhost/   http://localhost:5001/
  taxbaik (Nginx)    taxbaik (direct)
    │                 │
 ┌──▼──┐             │
 │Nginx├─────────────┘
 └──┬──┘
    │ (active port: 5001 or 5002)
    │
 ┌──▼──────────────┐
 │Active TaxBaik   │
 │(5001 or 5002)   │
 └─────────────────┘

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:32:23 +09:00

232 lines
8.4 KiB
TypeScript

import { expect, test, devices } from '@playwright/test';
import { loginThroughAdminUi } from './helpers/admin-auth';
// 테스트 계정 (실 admin 계정과 분리)
const TEST_USERNAME = 'test_admin';
const TEST_PASSWORD = 'test123456';
/**
* Green-Blue 배포 지원:
* - 로컬: Nginx를 거쳐 http://localhost/taxbaik (포트 무관, 항상 active 버전)
* - 원격: 프로덕션 도메인 (Nginx 라우팅)
*
* E2E_BASE_URL 환경변수 우선 사용, 없으면:
* - 운영(localhost): http://localhost/taxbaik (Nginx 라우팅 → active 포트)
* - 로컬 직접 테스트: http://127.0.0.1:5001/taxbaik (개발 포트)
*/
const baseUrl = (process.env.E2E_BASE_URL ?? 'http://localhost/taxbaik').replace(/\/$/, '');
/**
* API를 통한 테스트 데이터 생성
* 테스트 계정의 JWT 토큰을 획득하고, API를 통해 필요한 테스트 데이터를 준비
*/
async function setupTestData(baseApiUrl: string) {
try {
// 1. 테스트 계정 로그인 (JWT 토큰 획득)
const loginResponse = await fetch(`${baseApiUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: TEST_USERNAME, password: TEST_PASSWORD })
});
if (!loginResponse.ok) {
console.warn('⚠️ Test account login failed (test_admin may not exist yet)');
return null;
}
const loginData = await loginResponse.json();
const accessToken = loginData.accessToken;
if (!accessToken) {
console.warn('⚠️ No access token received');
return null;
}
// 2. API를 통해 테스트 데이터 확인/생성 (선택사항)
// 예: FAQ, Announcement 등 필요한 테스트 데이터 미리 생성
console.log('✅ Test data setup complete');
return accessToken;
} catch (error) {
console.warn('⚠️ Test data setup failed:', error);
return null;
}
}
// 디바이스별 반응형 테스트
test.describe('admin responsive design (test_admin account)', () => {
const deviceTests = [
{ name: 'Desktop (1920px)', viewport: { width: 1920, height: 1080 }, minElements: 4 },
{ name: 'Desktop (1440px)', viewport: { width: 1440, height: 900 }, minElements: 4 },
{ name: 'Laptop (1024px)', viewport: { width: 1024, height: 768 }, minElements: 4 },
{ name: 'Tablet L (960px)', viewport: { width: 960, height: 600 }, minElements: 3 },
{ name: 'Tablet M (768px)', viewport: { width: 768, height: 1024 }, minElements: 2 },
{ name: 'Tablet S (600px)', viewport: { width: 600, height: 800 }, minElements: 1 },
{ name: 'Mobile L (480px)', viewport: { width: 480, height: 853 }, minElements: 1 },
{ name: 'Mobile S (375px)', viewport: { width: 375, height: 667 }, minElements: 1 }
];
deviceTests.forEach(device => {
test(`dashboard loads correctly on ${device.name}`, async ({ browser }) => {
const context = await browser.newContext({
viewport: device.viewport,
deviceScaleFactor: 1
});
const page = await context.newPage();
try {
// 테스트 계정으로 로그인
await loginThroughAdminUi(page, baseUrl, TEST_USERNAME, TEST_PASSWORD);
await page.goto(`${baseUrl}/admin/dashboard`);
// 대시보드 요소 확인
await expect(page.locator('.admin-page-hero')).toBeVisible();
await expect(page.locator('.admin-page-title')).toContainText(/대시보드|Dashboard/i);
// 메트릭 카드 존재 확인
const metricCards = page.locator('.admin-metric-card');
const count = await metricCards.count();
expect(count, `Expected at least 1 metric card on ${device.name}`).toBeGreaterThanOrEqual(1);
// 메트릭 카드 가시성 확인
for (let i = 0; i < Math.min(count, device.minElements); i++) {
const card = metricCards.nth(i);
await expect(card).toBeInViewport();
}
// 테이블 체크 (있으면)
const tables = page.locator('.admin-table');
if (await tables.count() > 0) {
for (let i = 0; i < await tables.count(); i++) {
const table = tables.nth(i);
const headerCells = table.locator('thead th');
expect(await headerCells.count()).toBeGreaterThan(0);
}
}
// 오버플로우 체크
const bodyBounds = await page.evaluate(() => {
const docWidth = document.documentElement.scrollWidth;
const winWidth = window.innerWidth;
return docWidth <= winWidth + 1; // 1px 토러런스
});
expect(bodyBounds, `No horizontal scroll on ${device.name}`).toBe(true);
// 텍스트 가독성 (폰트 크기가 너무 작지 않은지)
const textElements = page.locator('p, span, .mud-typography');
for (let i = 0; i < Math.min(5, await textElements.count()); i++) {
const fontSize = await textElements.nth(i).evaluate((el) => {
return window.getComputedStyle(el).fontSize;
});
const size = parseFloat(fontSize);
expect(size).toBeGreaterThanOrEqual(11); // 최소 11px
}
console.log(`✅ ${device.name} - PASS`);
} finally {
await context.close();
}
});
});
// 드로어 반응형 테스트
test('drawer responsiveness on mobile', async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 375, height: 667 }
});
const page = await context.newPage();
try {
// 테스트 계정으로 로그인
await loginThroughAdminUi(page, baseUrl, TEST_USERNAME, TEST_PASSWORD);
await page.goto(`${baseUrl}/admin/dashboard`);
// 모바일에서 드로어가 존재하거나 숨겨져 있어야 함
const drawer = page.locator('.admin-drawer');
expect(await drawer.count()).toBeGreaterThan(0);
// 메뉴 버튼이 있어야 함
const menuButton = page.locator('.admin-menu-button');
await expect(menuButton).toBeVisible();
console.log('✅ Mobile drawer - PASS');
} finally {
await context.close();
}
});
// 폼 요소 반응형 테스트 (각 페이지)
test('form inputs are accessible on mobile', async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 480, height: 853 }
});
const page = await context.newPage();
try {
// 테스트 계정으로 로그인
await loginThroughAdminUi(page, baseUrl, TEST_USERNAME, TEST_PASSWORD);
// FAQ 페이지 (폼이 있음)
await page.goto(`${baseUrl}/admin/faqs/create`);
// 폼 필드들
const inputs = page.locator('input[type="text"], textarea, .mud-input-base, .mud-field');
const inputCount = await inputs.count();
if (inputCount > 0) {
// 첫 번째 입력창의 너비 확인
const width = await inputs.first().evaluate((el) => {
return el.getBoundingClientRect().width;
});
// 모바일 너비(480px)에서 충분한 공간을 차지해야 함
expect(width).toBeGreaterThan(200);
// 입력 필드가 접근 가능해야 함
await inputs.first().scrollIntoViewIfNeeded();
await expect(inputs.first()).toBeInViewport();
}
console.log('✅ Mobile forms - PASS');
} finally {
await context.close();
}
});
// 버튼 접근성 테스트
test('buttons are clickable on all viewports', async ({ browser }) => {
const viewports = [
{ width: 1920, height: 1080 },
{ width: 768, height: 1024 },
{ width: 375, height: 667 }
];
for (const viewport of viewports) {
const context = await browser.newContext({ viewport });
const page = await context.newPage();
try {
// 테스트 계정으로 로그인
await loginThroughAdminUi(page, baseUrl, TEST_USERNAME, TEST_PASSWORD);
await page.goto(`${baseUrl}/admin/dashboard`);
// 로그아웃 버튼 찾기
const logoutButton = page.locator('a:has-text("로그아웃"), button:has-text("로그아웃")').first();
if (await logoutButton.count() > 0) {
// 버튼이 클릭 가능한 영역에 있는지 확인
const box = await logoutButton.boundingBox();
expect(box).not.toBeNull();
expect(box?.width).toBeGreaterThan(20);
expect(box?.height).toBeGreaterThan(20);
}
} finally {
await context.close();
}
}
console.log('✅ Button accessibility - PASS');
});
});