From ec7528539a2b877738027ab5d502f2870ff64af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 10 Feb 2026 09:42:39 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20run-all.js=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=ED=83=90=EC=83=89=20=EC=95=88=EC=A0=95=EC=84=B1=20?= =?UTF-8?q?=EA=B0=95=ED=99=94=20(sidebar=20wait,=20collapse,=20expand=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- e2e/runner/run-all.js | 199 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 15 deletions(-) diff --git a/e2e/runner/run-all.js b/e2e/runner/run-all.js index 7be7775..c90b681 100644 --- a/e2e/runner/run-all.js +++ b/e2e/runner/run-all.js @@ -28,7 +28,7 @@ const RESULTS_DIR = path.join(SAM_ROOT, 'e2e', 'results', 'hotfix'); const SUCCESS_DIR = path.join(RESULTS_DIR, 'success'); const SCREENSHOTS_DIR = path.join(RESULTS_DIR, 'screenshots'); const EXECUTOR_PATH = path.join(SAM_ROOT, 'e2e', 'runner', 'step-executor.js'); -const DASHBOARD_URL = `${BASE_URL}/ko/dashboard`; +const DASHBOARD_URL = `${BASE_URL}/dashboard`; // CLI args const args = process.argv.slice(2); @@ -79,6 +79,48 @@ async function injectExecutor(page) { // ─── Menu Navigation ──────────────────────────────────────── async function navigateViaMenu(page, level1, level2) { + // Wait for sidebar to be present AND have rendered menu items + try { + await page.waitForFunction( + () => { + const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"]'); + if (!sidebar) return false; + const items = sidebar.querySelectorAll('a, button, [role="button"], [role="menuitem"]'); + return Array.from(items).filter(el => (el.textContent || '').trim().length > 1).length >= 3; + }, + null, + { timeout: 8000 } + ); + } catch (e) { + // Try reloading the page + console.log(C.yellow(` [DEBUG] sidebar check failed, reloading. URL: ${page.url()}`)); + try { + await page.reload({ waitUntil: 'load', timeout: 10000 }); + await sleep(2000); + await page.waitForFunction( + () => { + const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"]'); + if (!sidebar) return false; + const items = sidebar.querySelectorAll('a, button, [role="button"], [role="menuitem"]'); + return Array.from(items).filter(el => (el.textContent || '').trim().length > 1).length >= 3; + }, + null, + { timeout: 6000 } + ); + } catch (e2) { + console.log(C.red(` [DEBUG] sidebar still not rendered after reload! URL: ${page.url()}`)); + return false; + } + } + + // Collapse all open menus first to ensure clean state + await page.evaluate(() => { + const collapseBtn = Array.from(document.querySelectorAll('button, [role="button"]')) + .find(el => el.innerText?.trim() === '모두 접기'); + if (collapseBtn) collapseBtn.click(); + }); + await sleep(300); + // Scroll sidebar to top first await page.evaluate(() => { const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"], nav'); @@ -86,6 +128,57 @@ async function navigateViaMenu(page, level1, level2) { }); await sleep(300); + // Ensure sidebar is expanded (not icon-only collapsed mode) + const sidebarExpanded = await page.evaluate(() => { + try { + const raw = localStorage.getItem('sam-menu'); + if (raw) { + const data = JSON.parse(raw); + if (data.state && data.state.sidebarCollapsed) { + data.state.sidebarCollapsed = false; + localStorage.setItem('sam-menu', JSON.stringify(data)); + return false; // was collapsed, need reload + } + } + } catch (e) {} + return true; // already expanded + }); + if (!sidebarExpanded) { + await page.reload({ waitUntil: 'load', timeout: 10000 }); + await sleep(1500); + } + + // Wait specifically for the target L1 menu item to exist in DOM (textContent for collapsed text) + let l1Ready = false; + try { + l1Ready = await page.waitForFunction( + (l1Text) => { + const items = document.querySelectorAll('a, button, [role="button"], [role="menuitem"], [role="treeitem"]'); + return Array.from(items).some(el => { + const text = (el.textContent || el.innerText || '').trim(); + return text && (text === l1Text || text.startsWith(l1Text)); + }); + }, + level1, + { timeout: 8000 } + ); + } catch (e) { + // L1 target not found after waiting - collect debug info and fail + const debugInfo = await page.evaluate(() => { + const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"], nav'); + const items = document.querySelectorAll('a, button, [role="button"], [role="menuitem"], [role="treeitem"]'); + const texts = []; + items.forEach(el => { + const t = (el.textContent || el.innerText || '').trim(); + if (t && t.length > 1 && t.length < 25 && !texts.includes(t)) texts.push(t); + }); + return { hasSidebar: !!sidebar, url: window.location.href, menuTexts: texts.slice(0, 25) }; + }); + console.log(C.red(` [NAV-FAIL] L1 "${level1}" not found after wait. URL: ${debugInfo.url}, sidebar: ${debugInfo.hasSidebar}`)); + console.log(C.dim(` [NAV-FAIL] Visible items: ${debugInfo.menuTexts.join(', ')}`)); + return false; + } + // Find and click level1 menu with scroll-based search const l1Found = await page.evaluate( async ({ l1Text }) => { @@ -96,20 +189,19 @@ async function navigateViaMenu(page, level1, level2) { const items = Array.from( document.querySelectorAll('a, button, [role="button"], [role="menuitem"], [role="treeitem"]') ); + const match = items.find((el) => { - const text = el.innerText?.trim(); + const text = (el.textContent || el.innerText || '').trim(); return text && (text === l1Text || text.startsWith(l1Text)); }); if (match) { - // Scroll element into view and click match.scrollIntoView({ behavior: 'instant', block: 'center' }); await new Promise((r) => setTimeout(r, 100)); match.click(); - return { found: true }; + return { found: true, attempt: i }; } - // Scroll down to find more items if (sidebar) { sidebar.scrollBy({ top: 150, behavior: 'instant' }); await new Promise((r) => setTimeout(r, 100)); @@ -120,7 +212,9 @@ async function navigateViaMenu(page, level1, level2) { { l1Text: level1 } ); - if (!l1Found.found) return false; + if (!l1Found.found) { + return false; + } await sleep(500); // Wait for submenu to expand // Find and click level2 menu with scroll-based search @@ -135,12 +229,12 @@ async function navigateViaMenu(page, level1, level2) { document.querySelectorAll('a, button, [role="button"], [role="menuitem"], [role="treeitem"]') ); - // Try exact match first - let match = items.find((el) => el.innerText?.trim() === l2Text); + // Try exact match first (textContent for collapsed sidebar support) + let match = items.find((el) => (el.textContent || el.innerText || '').trim() === l2Text); // Try partial match if exact match fails if (!match) { - match = items.find((el) => el.innerText?.trim().includes(l2Text)); + match = items.find((el) => (el.textContent || el.innerText || '').trim().includes(l2Text)); } if (match) { @@ -182,17 +276,92 @@ async function ensureLoggedIn(page) { } async function goToDashboard(page) { + // Helper: expand sidebar if collapsed, then check menu items rendered + const waitForSidebar = async (timeout = 8000) => { + // Force sidebar expanded via sam-menu localStorage + await page.evaluate(() => { + try { + const raw = localStorage.getItem('sam-menu'); + if (raw) { + const data = JSON.parse(raw); + if (data.state && data.state.sidebarCollapsed) { + data.state.sidebarCollapsed = false; + localStorage.setItem('sam-menu', JSON.stringify(data)); + // Reload needed to apply the change + window.location.reload(); + } + } + } catch (e) {} + }); + await sleep(1500); + + // Wait for sidebar menu items to have visible text (innerText works when expanded) + await page.waitForFunction(() => { + const sidebar = document.querySelector('.sidebar-scroll, [data-sidebar="content"]'); + if (!sidebar) return false; + const items = sidebar.querySelectorAll('a, button, [role="button"], [role="menuitem"]'); + const withText = Array.from(items).filter(el => { + const t = (el.innerText || el.textContent || '').trim(); + return t.length > 1; + }); + return withText.length >= 5; + }, null, { timeout }); + }; + + // Force sidebar expanded state BEFORE navigation so page loads with full menu try { - // Always navigate to dashboard (even if already there) to reset state - await page.goto(DASHBOARD_URL, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.evaluate(() => { + try { + // The sidebar state is stored in 'sam-menu' localStorage key + const raw = localStorage.getItem('sam-menu'); + if (raw) { + const data = JSON.parse(raw); + if (data.state) { + data.state.sidebarCollapsed = false; + localStorage.setItem('sam-menu', JSON.stringify(data)); + } + } + } catch (e) {} + }); + } catch (e) { /* page may not be ready yet */ } + + // Attempt 1: Navigate to dashboard + try { + await page.goto(DASHBOARD_URL, { waitUntil: 'load', timeout: 15000 }); await sleep(1000); - // Check if redirected to login await ensureLoggedIn(page); + await waitForSidebar(); + return; } catch (e) { - // If navigation fails, try again - await page.goto(DASHBOARD_URL, { waitUntil: 'domcontentloaded', timeout: 15000 }); - await sleep(1000); + const dbgUrl = page.url(); + console.log(C.yellow(` [DASH] attempt 1 failed. URL: ${dbgUrl}, err: ${e.message?.substring(0, 80)}`)); + } + + // Attempt 2: Reload to force fresh render + try { + await page.reload({ waitUntil: 'load', timeout: 10000 }); + await sleep(1500); await ensureLoggedIn(page); + await waitForSidebar(); + return; + } catch (e) { + const dbgUrl = page.url(); + console.log(C.yellow(` [DASH] reload failed. URL: ${dbgUrl}, err: ${e.message?.substring(0, 80)}`)); + } + + // Attempt 3: Full re-login + try { + await page.goto(`${BASE_URL}/ko/login`, { waitUntil: 'load', timeout: 10000 }); + await sleep(500); + try { + await page.fill('#userId', AUTH.username); + await page.fill('#password', AUTH.password); + await page.click("button[type='submit']"); + await sleep(3000); + } catch (loginErr) { /* may already be on dashboard */ } + await waitForSidebar(); + } catch (e) { + await sleep(2000); } }