fix: run-all.js 사이드바 탐색 안정성 강화 (sidebar wait, collapse, expand 처리)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user