import { mkdir, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { chromium } from '@playwright/test'; const refRoot = process.env.REF_SAM_ROOT; const artifactRoot = process.env.KAKAO_OTP_ARTIFACT_DIR; if (!refRoot || !artifactRoot) { throw new Error('REF_SAM_ROOT and KAKAO_OTP_ARTIFACT_DIR are required.'); } const modalHtml = ` `; await mkdir(artifactRoot, { recursive: true }); const browser = await chromium.launch(); try { for (const viewport of [ { name: 'desktop', width: 1200, height: 900 }, { name: 'mobile', width: 390, height: 844 }, ]) { const page = await browser.newPage({ viewport }); await page.setContent(modalHtml); await page.addStyleTag({ path: resolve(refRoot, 'dist_js/gateway/common_ts.css') }); await page.addStyleTag({ path: resolve(refRoot, 'd_shared/common.css') }); await page.addStyleTag({ path: resolve(refRoot, 'dist_js/gateway/login.css') }); await page.addStyleTag({ content: '.modal { background: rgb(0 0 0 / 60%); } body { background: #000; }', }); const dialog = page.locator('#modalOTP .modal-content'); const input = page.locator('#otp_code'); const submit = page.getByRole('button', { name: '제출' }); const readSubmitStyle = () => submit.evaluate((element) => { const style = getComputedStyle(element); return { backgroundColor: style.backgroundColor, borderColor: style.borderColor, color: style.color, outline: style.outline, boxShadow: style.boxShadow, opacity: style.opacity, cursor: style.cursor, }; }); await page.waitForTimeout(250); const states = { normal: await readSubmitStyle() }; await submit.hover(); await page.waitForTimeout(250); states.hover = await readSubmitStyle(); await input.focus(); await input.press('Tab'); await page.keyboard.press('Tab'); await page.waitForTimeout(250); states.focusVisible = await readSubmitStyle(); const submitBox = await submit.boundingBox(); if (!submitBox) throw new Error('Ref OTP submit button has no geometry.'); await page.mouse.move(submitBox.x + submitBox.width / 2, submitBox.y + submitBox.height / 2); await page.mouse.down(); await page.waitForTimeout(250); states.active = await readSubmitStyle(); await page.mouse.move(1, 1); await page.mouse.up(); await submit.evaluate((element) => { element.disabled = true; }); await page.waitForTimeout(250); states.disabled = await readSubmitStyle(); const geometry = await dialog.evaluate((element) => { const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, backgroundColor: style.backgroundColor, border: style.border, fontSize: style.fontSize, }; }); await page.screenshot({ path: resolve(artifactRoot, `kakao-otp-ref-${viewport.name}.png`), fullPage: true }); await writeFile( resolve(artifactRoot, `kakao-otp-ref-${viewport.name}.json`), `${JSON.stringify({ ...geometry, submitStates: states }, null, 2)}\n`, 'utf8' ); await page.close(); } } finally { await browser.close(); }