import { expect, test, type Page, type Route } from '@playwright/test'; type ProfileState = { department: string; getMeCount: number; putBodies: Array>; }; const PROFILE_DEPARTMENT_EDIT_X = 1170; const PROFILE_DEPARTMENT_EDIT_Y = 680; const PROFILE_DEPARTMENT_INPUT_X = 110; const PROFILE_DEPARTMENT_INPUT_Y = 685; const PROFILE_BLUR_X = 200; const PROFILE_BLUR_Y = 260; async function seedTokenLogin(page: Page): Promise { await page.addInitScript(() => { window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); window.localStorage.setItem('baron_auth_provider', 'ory'); window.localStorage.removeItem('baron_auth_cookie_mode'); window.localStorage.removeItem('baron_auth_pending_provider'); }); } 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 openDepartmentEditor(page: Page): Promise { await page.locator('flt-glass-pane').click({ position: { x: PROFILE_DEPARTMENT_EDIT_X, y: PROFILE_DEPARTMENT_EDIT_Y }, force: true, }); await page.waitForTimeout(200); } async function blurDepartmentEditor(page: Page): Promise { await page.locator('flt-glass-pane').click({ position: { x: PROFILE_BLUR_X, y: PROFILE_BLUR_Y }, force: true, }); await page.waitForTimeout(250); } async function submitDepartmentEditor(page: Page): Promise { await page.keyboard.press('Enter'); await page.waitForTimeout(250); } async function mockProfileApis(page: Page, state: ProfileState): Promise { await page.route('**/api/v1/**', async (route: Route) => { const request = route.request(); const requestUrl = new URL(request.url()); const path = requestUrl.pathname; const method = request.method().toUpperCase(); if (path.endsWith('/api/v1/user/me') && method === 'GET') { const authHeader = request.headers()['authorization'] ?? ''; if (!authHeader.startsWith('Bearer ')) { await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'unauthorized' }), }); return; } state.getMeCount += 1; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'e2e-user', email: 'e2e@example.com', name: 'E2E User', phone: '+821012341234', department: state.department, affiliationType: 'employee', companyCode: 'BARON', tenant: { id: 'tenant-1', name: 'Baron', slug: 'baron', description: 'E2E tenant', }, }), }); return; } if (path.endsWith('/api/v1/user/me') && method === 'PUT') { const body = (request.postDataJSON() ?? {}) as Record; state.putBodies.push(body); const nextDepartment = String(body.department ?? '').trim(); if (nextDepartment !== '') { state.department = nextDepartment; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ status: 'success', updatedAt: '2026-02-24T00:00:00Z', }), }); 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; } if (path.endsWith('/api/v1/client-log')) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }), }); }); } async function openProfilePage(page: Page): Promise { await page.goto('/ko/profile'); await expect(page).toHaveURL(/\/ko\/profile$/); await page.waitForTimeout(1200); } async function waitForInitialProfileLoad(state: ProfileState): Promise { await expect.poll(() => state.getMeCount).toBeGreaterThan(0); } test.describe('UserFront WASM profile department editing', () => { test.afterEach(async ({ page }) => { await page.unroute('**/api/v1/**'); }); test('소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({ page, }) => { const state: ProfileState = { department: 'QA', getMeCount: 0, putBodies: [], }; await seedTokenLogin(page); await mockProfileApis(page, state); await openProfilePage(page); await waitForInitialProfileLoad(state); await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Updated'); await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(1); expect(state.putBodies[0]?.department).toBe('QA-Updated'); expect(state.department).toBe('QA-Updated'); const getCountBeforeReload = state.getMeCount; await page.reload(); await expect(page).toHaveURL(/\/ko\/profile$/); await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload); }); test('소속 입력 후 즉시 새로고침해도 저장 요청이 중복 전송되지 않는다', async ({ page, }) => { const state: ProfileState = { department: 'QA', getMeCount: 0, putBodies: [], }; await seedTokenLogin(page); await mockProfileApis(page, state); await openProfilePage(page); await waitForInitialProfileLoad(state); await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Repro'); await page.reload(); await expect(page).toHaveURL(/\/ko\/profile$/); expect(state.putBodies.length).toBeLessThanOrEqual(1); if (state.putBodies.length > 0) { expect(state.putBodies[0]?.department).toBe('QA-Repro'); expect(state.department).toBe('QA-Repro'); return; } expect(state.department).toBe('QA'); }); test('소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다', async ({ page, }) => { const state: ProfileState = { department: 'QA', getMeCount: 0, putBodies: [], }; await seedTokenLogin(page); await mockProfileApis(page, state); await openProfilePage(page); await waitForInitialProfileLoad(state); await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA'); await blurDepartmentEditor(page); expect(state.putBodies).toHaveLength(0); }); test('소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다', async ({ page }) => { const state: ProfileState = { department: 'QA', getMeCount: 0, putBodies: [], }; await seedTokenLogin(page); await mockProfileApis(page, state); await openProfilePage(page); await waitForInitialProfileLoad(state); await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, ''); await blurDepartmentEditor(page); expect(state.putBodies).toHaveLength(0); expect(state.department).toBe('QA'); }); test('소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다', async ({ page }) => { const state: ProfileState = { department: 'QA', getMeCount: 0, putBodies: [], }; await seedTokenLogin(page); await mockProfileApis(page, state); await openProfilePage(page); await waitForInitialProfileLoad(state); await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-1'); await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(1); await page.reload(); await expect(page).toHaveURL(/\/ko\/profile$/); await page.waitForTimeout(1200); await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-2'); await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(2); expect(state.putBodies[0]?.department).toBe('QA-1'); expect(state.putBodies[1]?.department).toBe('QA-2'); expect(state.department).toBe('QA-2'); }); });