import { expect, type APIRequestContext, type Page } from '@playwright/test'; 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`); const usernameInput = page.locator('input[placeholder="사용자명"]'); const passwordInput = page.locator('input[placeholder="비밀번호"]'); const loginButton = page.locator('#admin-login-submit'); await usernameInput.fill(username); await passwordInput.fill(password); await expect(loginButton).toBeEnabled({ timeout: 30_000 }); await expect(loginButton).toContainText('로그인'); await loginButton.click(); await expect(page).toHaveURL(/\/taxbaik\/admin\/dashboard$/); await page.locator('#blazor-loading').waitFor({ state: 'hidden', timeout: 30_000 }).catch(() => {}); await expect(page.getByRole('link', { name: '로그아웃' })).toBeVisible({ timeout: 20_000 }); await expect(page.getByText('세무 운영 콘솔')).toBeVisible({ timeout: 20_000 }); } export async function navigateInBlazor(page: Page, targetUrl: string) { await page.evaluate(url => { const blazor = (window as typeof window & { Blazor?: { navigateTo: (target: string) => void } }).Blazor; if (blazor) { blazor.navigateTo(url); return; } window.location.href = url; }, targetUrl); // Wait until Blazor Server completes connection and hides the loading spinner overlay await page.locator('#blazor-loading').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {}); // Give the SPA router a brief window to unmount the previous page and mount the loading spinner await page.waitForTimeout(500); // Also wait for MudBlazor's dynamic loading spinners to disappear (ensuring the grid is interactive) const spinner = page.locator('.mud-progress-circular, .mud-progress-linear-bar'); try { if (await spinner.count() > 0) { await spinner.first().waitFor({ state: 'hidden', timeout: 10000 }); } } catch (e) { // Suppress timeout if the spinner was already gone or never showed up } } 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!; }