import {chromium, devices} from 'playwright'; const url = process.argv[2] || 'http://localhost/s/stay'; const b = await chromium.launch({channel: 'chrome'}); const prep = async (page) => page.evaluate(() => { window.__rails = []; document.querySelectorAll('#root .slider-viewport').forEach((el) => window.__rails.push(el)); document.querySelectorAll('#root *').forEach((el) => { if (el.classList.contains('slider-viewport')) return; const f = getComputedStyle(el).overflowX; if ((f === 'auto' || f === 'scroll') && el.scrollWidth - el.clientWidth > 40) window.__rails.push(el); }); window.__pos = (i) => { const r = window.__rails[i]; const c = r.firstElementChild; let m = 0; try { m = new DOMMatrix(getComputedStyle(c).transform).m41; } catch {} return Math.round(r.scrollLeft + m); }; window.__info = (i) => { const r = window.__rails[i]; const b = r.getBoundingClientRect(); return {label: r.getAttribute('aria-label') || '?', vis: !!r.getClientRects().length, cx: Math.round(b.left + b.width / 2), cy: Math.round(b.top + b.height / 2)}; }; return window.__rails.length; }); // ── 데스크톱 ─────────────────────────────────────────────── const page = await b.newPage({viewport: {width: 1280, height: 900}}); await page.goto(url, {waitUntil: 'domcontentloaded'}); await page.waitForTimeout(6000); const n = await prep(page); const rows = []; for (let i = 0; i < n; i++) { let info = await page.evaluate((k) => window.__info(k), i); if (!info.vis) { rows.push({레일: info.label, 판정: '숨은 탭'}); continue; } await page.mouse.move(5, 5); await page.evaluate((k) => window.__rails[k].scrollIntoView({block: 'center'}), i); await page.waitForTimeout(9000); info = await page.evaluate((k) => window.__info(k), i); const y = Math.min(Math.max(info.cy, 100), 800); const p0 = await page.evaluate((k) => window.__pos(k), i); await page.waitForTimeout(10000); // ① 가만히 const idle = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, p0]); await page.mouse.move(info.cx, y); // ② 레일 위 세로 휠 await page.mouse.wheel(0, 150); await page.mouse.move(5, 5); // 커서는 뺀다(호버 정지는 별개) const p1 = await page.evaluate((k) => window.__pos(k), i); await page.waitForTimeout(4000); const during = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); await page.waitForTimeout(9000); const after = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); const i2 = await page.evaluate((k) => window.__info(k), i); // ③ 손으로 민다 const y2 = Math.min(Math.max(i2.cy, 100), 800); await page.mouse.move(i2.cx + 180, y2); await page.mouse.down(); for (let s = 1; s <= 10; s++) { await page.mouse.move(i2.cx + 180 - s * 24, y2); await page.waitForTimeout(16); } await page.mouse.up(); await page.mouse.move(5, 5); await page.waitForTimeout(1500); const dragged = await page.evaluate((k) => window.__pos(k), i); await page.waitForTimeout(13000); const frozen = await page.evaluate(([k, p]) => window.__pos(k) - p, [i, dragged]); rows.push({레일: info.label, '①가만히': idle, '②휠직후': during, '②13초뒤': after, '③민뒤13초': frozen, 판정: (idle !== 0 && during === 0 && after !== 0 && frozen === 0) ? 'OK' : '확인'}); } console.log('── 데스크톱 (1280) ──'); console.table(rows); await page.close(); // ── 모바일 ─────────────────────────────────────────────── const ctx = await b.newContext({...devices['iPhone 13'], hasTouch: true, isMobile: true}); const mp = await ctx.newPage(); await mp.goto(url, {waitUntil: 'domcontentloaded'}); await mp.waitForTimeout(6000); const mn = await prep(mp); const cdp = await ctx.newCDPSession(mp); const mrows = []; for (let i = 0; i < mn; i++) { let info = await mp.evaluate((k) => window.__info(k), i); if (!info.vis) { mrows.push({레일: info.label, 판정: '숨은 탭'}); continue; } await mp.evaluate((k) => window.__rails[k].scrollIntoView({block: 'center'}), i); await mp.waitForTimeout(9000); info = await mp.evaluate((k) => window.__info(k), i); const x = info.cx, y = Math.min(Math.max(info.cy, 120), 620); const pt = (type, yy, xx) => cdp.send('Input.dispatchTouchEvent', {type, touchPoints: type === 'touchEnd' ? [] : [{x: xx ?? x, y: yy ?? y}]}); const p0 = await mp.evaluate((k) => window.__pos(k), i); await mp.waitForTimeout(10000); // ① 가만히 const idle = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, p0]); const p1 = await mp.evaluate((k) => window.__pos(k), i); // ② 손가락 대고 11초 await pt('touchStart'); for (let s = 0; s < 11; s++) { await mp.waitForTimeout(1000); await pt('touchMove', y - s * 2); } const held = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); await pt('touchEnd'); await mp.waitForTimeout(11000); const released = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, p1]); const i2 = await mp.evaluate((k) => window.__info(k), i); // ③ 가로 스와이프 const y2 = Math.min(Math.max(i2.cy, 120), 620); await pt('touchStart', y2, i2.cx + 120); for (let s = 1; s <= 8; s++) { await pt('touchMove', y2, i2.cx + 120 - s * 22); await mp.waitForTimeout(20); } await pt('touchEnd'); await mp.waitForTimeout(1500); const dragged = await mp.evaluate((k) => window.__pos(k), i); await mp.waitForTimeout(13000); const frozen = await mp.evaluate(([k, p]) => window.__pos(k) - p, [i, dragged]); mrows.push({레일: info.label, '①가만히': idle, '②손댄채11초': held, '②뗀뒤11초': released, '③민뒤13초': frozen, 판정: (idle !== 0 && held === 0 && released !== 0 && frozen === 0) ? 'OK' : '확인'}); } console.log('── 모바일 (iPhone 13) ──'); console.table(mrows); await b.close();