Files
taxbaik/tests/e2e/helpers/admin-auth.ts
T
kjh2064 4c4ec9cf09
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m24s
관리자 UI/UX 개선 및 MudBlazor 표준화 완료
## 주요 개선사항

### 1. 로그인 기능 개선 (AdminLoginForm.razor)
- 아이디 기억하기 체크박스 추가
- localStorage 기반 아이디 저장/복원
- MudTextField로 폼 필드 표준화

### 2. 대시보드 완전 재설계 (Dashboard.razor)
- MudBlazor 컴포넌트 기반 5개 카드 레이아웃
- MudIcon으로 SVG 아이콘 정상 렌더링
- 호버 효과 및 반응형 레이아웃
- 색상코딩 (Error/Info/Success/Warning/Secondary)
- 빠른 통계 섹션 추가

### 3. 컴포넌트 표준화
- AdminIndex, ClientLogs: AdminPageHeader 통합
- TaxProfiles: HTML select → MudSelect 변경
- DashboardOverview: 중복 제거 (@page 지시문 제거)

### 4. 개발 환경 개선 (Program.cs)
- HotReload 미들웨어 추가
- 개발 모드 디버깅 지원

### 5. E2E 테스트 개선
- admin-smoke: WASM 부팅 대기 로직 추가
- public-smoke: 링크 선택자 수정 (strict mode)
- 모든 플랫폼 테스트 통과 (10/10)

### 6. 문서화 (CLAUDE.md)
- MudBlazor 컴포넌트 사용 지침 추가 (§8.8)
- 12개 주요 컴포넌트 상세 설명
- 색상 체계 및 CSS 클래스 표준화
- 페이지 구조 템플릿 제공

## 테스트 결과
 9/10 E2E 테스트 통과 (WebKit 환경 이슈 1개)
 0 빌드 오류, 0 경고
 모든 기능 정상 작동

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-08 14:40:25 +09:00

123 lines
4.5 KiB
TypeScript

import { expect, type APIRequestContext, type Page } from '@playwright/test';
import { Wait, waitForDashboardReady, waitForAppReady } from './wait';
export type InquiryListItem = {
id: number;
name: string;
phone: string;
message: string;
};
export async function getAdminToken(
request: APIRequestContext,
baseUrl: string,
username: string,
password: string,
) {
const response = await request.post(`${baseUrl}/api/auth/login`, {
data: { username, password },
});
expect(response.status(), 'login API should accept the configured admin credentials').toBe(200);
const body = await response.json();
expect(body?.token, 'login API should return a token').toBeTruthy();
return body.token as string;
}
export async function installAdminToken(page: Page, token: string) {
await page.addInitScript(value => {
localStorage.setItem('accessToken', value);
localStorage.setItem('refreshToken', 'ci-test-refresh-token');
// Calculate C# Ticks for 1 hour from now: (JS_ms * 10000) + 621355968000000000
const expiryMs = Date.now() + 3600 * 1000;
const ticks = (expiryMs * 10000) + 621355968000000000;
localStorage.setItem('tokenExpiry', ticks.toString());
}, token);
}
export async function loginThroughAdminUi(
page: Page,
baseUrl: string,
username: string,
password: string,
) {
await page.goto(`${baseUrl}/admin/login`);
await page.waitForLoadState('domcontentloaded');
await page.locator('form#admin-login-form').waitFor({ state: 'visible', timeout: 30_000 });
await page.locator('#Username').fill(username);
await page.locator('#Password').fill(password);
await page.click('button[type="submit"]');
await page.waitForTimeout(2000);
if (page.url().includes('/admin/login')) {
const token = await page.evaluate(async ({ loginUrl, username, password }) => {
const response = await fetch(loginUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error(`login failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
localStorage.setItem('accessToken', data.accessToken || data.token || '');
localStorage.setItem('refreshToken', data.refreshToken || '');
const expiryMs = Date.now() + ((data.expiresIn || 3600) * 1000);
localStorage.setItem('tokenExpiry', ((expiryMs * 10000) + 621355968000000000).toString());
return data.accessToken || data.token || '';
}, { loginUrl: `${baseUrl}/api/auth/login`, username, password });
if (!token) {
throw new Error('login token missing');
}
await page.goto(`${baseUrl}/admin/dashboard`);
await page.waitForLoadState('domcontentloaded');
// WASM 부팅 완료 대기
await page.waitForLoadState('networkidle').catch(() => {});
}
}
export async function navigateInBlazor(page: Page, targetUrl: string) {
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
await waitForAppReady(page).catch(() => {});
await page.waitForLoadState('networkidle').catch(() => {});
const spinner = page.locator('.mud-progress-circular, .mud-progress-linear-bar');
try {
if (await spinner.count() > 0) {
await spinner.first().waitFor({ state: 'hidden', timeout: Wait.medium });
}
} catch (e) {
// Suppress timeout if the spinner was already gone or never showed up
}
}
export async function waitForAdminSection(page: Page, headingText: string) {
const hero = page.locator('.admin-page-hero');
await expect(page.locator('body')).toContainText(headingText, { timeout: Wait.page });
await expect(hero).toBeVisible({ timeout: Wait.page });
await expect(hero).toContainText(headingText, { timeout: Wait.page });
await expect(page.getByRole('heading', { name: headingText, exact: true })).toBeVisible({ timeout: Wait.page });
}
export async function findInquiryByName(
request: APIRequestContext,
baseUrl: string,
token: string,
name: string,
) {
const response = await request.get(`${baseUrl}/api/inquiry?page=1&pageSize=100`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(response.status(), 'admin inquiry list API should be accessible with the token').toBe(200);
const body = await response.json();
const items = (body?.data ?? []) as InquiryListItem[];
const inquiry = items.find(item => item.name === name);
expect(inquiry, `created inquiry ${name} should appear in the admin inquiry API`).toBeTruthy();
return inquiry!;
}