import { expect, test, type BrowserContext, type Page } from '@playwright/test'; const USERFRONT_BASE_URL = process.env.USERFRONT_BASE_URL ?? 'https://sso-test.hmac.kr'; const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? 'http://localhost:5173'; const LOGIN_ID = process.env.E2E_LOGIN_ID ?? ''; const PASSWORD = process.env.E2E_PASSWORD ?? ''; type SessionApiResponse = { items?: Array<{ session_id?: string; client_id?: string; app_name?: string; is_current?: boolean; user_agent?: string; ip_address?: string; }>; }; function ensureCredentials(): void { if (!LOGIN_ID || !PASSWORD) { test.skip(true, 'E2E credentials are required'); } } async function enableFlutterAccessibility(page: Page): Promise { await page.waitForTimeout(300); const button = page.getByRole('button', { name: 'Enable accessibility' }); if (await button.count()) { try { await button.click({ force: true }); } catch { return; } const placeholder = page.locator('flt-semantics-placeholder'); if (await placeholder.count()) { await placeholder.first().click({ force: true }); } await page.waitForTimeout(800); } } async function clickPasswordTab(page: Page): Promise { await page.waitForTimeout(900); const pane = page.locator('flt-glass-pane'); await pane.click({ position: { x: 522, y: 158 }, force: true, }); await page.waitForTimeout(120); await pane.click({ position: { x: 522, y: 158 }, 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 loginViaUserFront(page: Page): Promise { await page.waitForURL(/\/ko\/(signin|login)/, { timeout: 30_000 }); const loginIdInput = page.getByPlaceholder(/이메일 또는 휴대폰 번호|email|phone/i); const passwordInput = page.getByPlaceholder(/비밀번호|password/i); const submitButton = page.getByRole('button', { name: /로그인|Login/i }); if ((await loginIdInput.count()) >= 1 && (await passwordInput.count()) >= 1) { await loginIdInput.first().fill(LOGIN_ID); await passwordInput.first().fill(PASSWORD); await submitButton.click(); return; } await clickPasswordTab(page); await fillAt(page, 640, 245, LOGIN_ID); await fillAt(page, 640, 311, PASSWORD); await page.locator('flt-glass-pane').click({ position: { x: 640, y: 381 }, force: true, }); } async function ensureConsentIfNeeded(page: Page): Promise { if (!/\/ko\/consent/.test(page.url())) { return; } const allowButton = page .getByRole('button') .filter({ hasText: /허용|동의|Accept|Allow/i }) .first(); if (await allowButton.count()) { await allowButton.click({ force: true }); } } async function captureUserSessionsOnReload(page: Page): Promise { const responsePromise = page.waitForResponse( (response) => response.request().method() === 'GET' && response.url().includes('/api/v1/user/sessions'), { timeout: 30_000 }, ); await page.reload({ waitUntil: 'domcontentloaded' }); const response = await responsePromise; return (await response.json()) as SessionApiResponse; } async function loginUserFront(context: BrowserContext): Promise { const page = await context.newPage(); await page.goto(`${USERFRONT_BASE_URL}/ko/signin`, { waitUntil: 'domcontentloaded', }); await loginViaUserFront(page); await expect(page).toHaveURL(/\/ko\/dashboard/, { timeout: 60_000 }); return page; } async function loginAdminFront(context: BrowserContext): Promise { const page = await context.newPage(); await page.goto(ADMINFRONT_URL, { waitUntil: 'domcontentloaded' }); const ssoButton = page.getByRole('button', { name: /SSO 계정으로 로그인|SSO/i }); if (await ssoButton.count()) { await ssoButton.click({ force: true }); await page.waitForTimeout(1500); } if (/\/login$/.test(page.url())) { const authorizeUrl = await page.evaluate(() => { const origin = window.location.origin; const authority = 'https://sso-test.hmac.kr/oidc'; const params = new URLSearchParams({ client_id: 'adminfront', redirect_uri: `${origin}/auth/callback`, response_type: 'code', scope: 'openid offline_access profile email', state: `pw-${Date.now()}`, nonce: `pw-${Date.now()}`, code_challenge: 'test-code-challenge-test-code-challenge-test', code_challenge_method: 'plain', }); return `${authority}/oauth2/auth?${params.toString()}`; }); await page.goto(authorizeUrl, { waitUntil: 'domcontentloaded' }); } await loginViaUserFront(page); await ensureConsentIfNeeded(page); await page.waitForURL(/localhost:5173|\/auth\/callback|\/dashboard|\/tenants/, { timeout: 60_000, }); return page; } test.describe('cross-browser session debug', () => { test('userfront session card should map adminfront session metadata across contexts', async ({ browser, }, testInfo) => { ensureCredentials(); const userfrontContext = await browser.newContext({ locale: 'ko-KR' }); const adminfrontContext = await browser.newContext({ locale: 'ko-KR' }); const userfrontPage = await loginUserFront(userfrontContext); const adminfrontPage = await loginAdminFront(adminfrontContext); const sessionsPayload = await captureUserSessionsOnReload(userfrontPage); const items = sessionsPayload.items ?? []; const adminfrontItems = items.filter((item) => (item.client_id ?? '').toLowerCase().includes('adminfront'), ); const unknownCards = await userfrontPage.locator('text=세션 정보').allTextContents(); const adminFrontCards = await userfrontPage.locator('text=AdminFront').allTextContents(); await testInfo.attach('user-sessions.json', { body: JSON.stringify(sessionsPayload, null, 2), contentType: 'application/json', }); await testInfo.attach('card-summary.json', { body: JSON.stringify( { unknownCards, adminFrontCards, currentUrl: userfrontPage.url(), adminfrontUrl: adminfrontPage.url(), }, null, 2, ), contentType: 'application/json', }); expect(adminfrontItems.length).toBeGreaterThan(0); }); });