import { expect, test, type Page, type Route } from '@playwright/test'; type RequestCapture = { loginBody?: Record; resetBody?: Record; resetToken?: string | null; clientLogs: string[]; }; const SIGNIN_PASSWORD_TAB_X = 522; const SIGNIN_TAB_Y = 158; const SIGNIN_LOGIN_ID_X = 640; const SIGNIN_LOGIN_ID_Y = 245; const SIGNIN_PASSWORD_X = 640; const SIGNIN_PASSWORD_Y = 311; const SIGNIN_SUBMIT_X = 640; const SIGNIN_SUBMIT_Y = 381; const RESET_NEW_PASSWORD_X = 640; const RESET_NEW_PASSWORD_Y = 382; const RESET_CONFIRM_PASSWORD_X = 640; const RESET_CONFIRM_PASSWORD_Y = 464; const RESET_SUBMIT_X = 640; const RESET_SUBMIT_Y = 534; async function clickPasswordTab(page: Page): Promise { await page.waitForTimeout(900); const pane = page.locator('flt-glass-pane'); await pane.click({ position: { x: SIGNIN_PASSWORD_TAB_X, y: SIGNIN_TAB_Y }, force: true, }); await page.waitForTimeout(120); await pane.click({ position: { x: SIGNIN_PASSWORD_TAB_X, y: SIGNIN_TAB_Y }, force: true, }); await page.waitForTimeout(200); } async function fillAt(page: Page, x: number, y: number, value: string): Promise { const pane = page.locator('flt-glass-pane'); await pane.click({ position: { x, y }, force: true }); await page.waitForTimeout(100); await page.keyboard.press('Control+A'); await page.keyboard.press('Backspace'); await page.keyboard.type(value); } async function mockAuthApis(page: Page, capture: RequestCapture): Promise { await page.route('**/api/v1/**', async (route: Route) => { const requestUrl = new URL(route.request().url()); const path = requestUrl.pathname; if (path.endsWith('/api/v1/auth/password/login')) { capture.loginBody = (route.request().postDataJSON() ?? {}) as Record< string, unknown >; const loginId = String(capture.loginBody.loginId ?? ''); const password = String(capture.loginBody.password ?? ''); if (loginId === 'e2e@example.com' && password === 'ValidPass1!') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ sessionJwt: 'e30.e30.e30', provider: 'ory', }), }); return; } await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'password_or_email_mismatch' }), }); return; } if (path.endsWith('/api/v1/auth/password/policy')) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ minLength: 12, minCharacterTypes: 3, lowercase: true, uppercase: true, number: true, nonAlphanumeric: true, }), }); return; } if (path.endsWith('/api/v1/auth/password/reset/complete')) { capture.resetBody = (route.request().postDataJSON() ?? {}) as Record< string, unknown >; capture.resetToken = requestUrl.searchParams.get('token'); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'ok' }), }); return; } if (path.endsWith('/api/v1/client-log')) { const payload = (route.request().postDataJSON() ?? {}) as { message?: string; }; if (payload.message != null) { capture.clientLogs.push(payload.message); } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }), }); return; } if (path.endsWith('/api/v1/user/me')) { const authHeader = route.request().headers()['authorization'] ?? ''; if (!authHeader.startsWith('Bearer ')) { await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'unauthorized' }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'e2e-user', email: 'e2e@example.com', name: 'E2E User', phone: '+821012341234', department: 'QA', affiliationType: 'employee', companyCode: 'BARON', tenant: { id: 'tenant-1', name: 'Baron', slug: 'baron', description: 'E2E tenant', }, }), }); return; } if (path.endsWith('/api/v1/user/rp/linked')) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [] }), }); return; } if (path.endsWith('/api/v1/audit/auth/timeline')) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [], next_cursor: '' }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}), }); }); } test.describe('UserFront WASM password login and reset', () => { test('비밀번호 로그인 성공 시 dashboard로 이동하고 토큰을 저장한다', async ({ page }) => { const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); await page.goto('/ko/signin'); await clickPasswordTab(page); await fillAt(page, SIGNIN_LOGIN_ID_X, SIGNIN_LOGIN_ID_Y, 'e2e@example.com'); await fillAt(page, SIGNIN_PASSWORD_X, SIGNIN_PASSWORD_Y, 'ValidPass1!'); await page.locator('flt-glass-pane').click({ position: { x: SIGNIN_SUBMIT_X, y: SIGNIN_SUBMIT_Y }, force: true, }); await expect(page).toHaveURL(/\/ko\/dashboard$/); expect(capture.loginBody?.loginId).toBe('e2e@example.com'); expect(capture.loginBody?.password).toBe('ValidPass1!'); const storedToken = await page.evaluate(() => window.localStorage.getItem('baron_auth_token'), ); expect(storedToken).toBe('e30.e30.e30'); }); test('비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다', async ({ page }) => { const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); await page.goto('/ko/signin'); await clickPasswordTab(page); await fillAt(page, SIGNIN_LOGIN_ID_X, SIGNIN_LOGIN_ID_Y, 'e2e@example.com'); await fillAt(page, SIGNIN_PASSWORD_X, SIGNIN_PASSWORD_Y, 'WrongPass1!'); await page.locator('flt-glass-pane').click({ position: { x: SIGNIN_SUBMIT_X, y: SIGNIN_SUBMIT_Y }, force: true, }); await expect(page).toHaveURL(/\/ko\/signin$/); await expect .poll( () => capture.clientLogs.some((message) => message.includes('password_or_email_mismatch'), ), { timeout: 10000 }, ) .toBe(true); }); test('reset-password에서 변경 성공 시 signin으로 이동한다', async ({ page }) => { const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); const policyLoaded = page.waitForResponse( (response) => response.url().includes('/api/v1/auth/password/policy') && response.status() === 200, ); await page.goto('/ko/reset-password?token=reset-token-e2e'); await policyLoaded; await page.waitForTimeout(900); await fillAt(page, RESET_NEW_PASSWORD_X, RESET_NEW_PASSWORD_Y, 'ValidPass1!A'); await fillAt( page, RESET_CONFIRM_PASSWORD_X, RESET_CONFIRM_PASSWORD_Y, 'ValidPass1!A', ); await page.locator('flt-glass-pane').click({ position: { x: RESET_SUBMIT_X, y: RESET_SUBMIT_Y }, force: true, }); await expect .poll( () => capture.resetBody?.newPassword as string | undefined, { timeout: 10000 }, ) .toBe('ValidPass1!A'); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/, { timeout: 10_000 }); expect(capture.resetToken).toBe('reset-token-e2e'); expect(capture.resetBody?.newPassword).toBe('ValidPass1!A'); }); });