fix: run-all.js 사이드바 탐색 안정성 강화 (sidebar wait, collapse, expand 처리)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-10 09:42:39 +09:00
parent 2e16da9549
commit ec7528539a

View File

@@ -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);
}
}