#!/usr/bin/env node /** * Settings Pages UI Tests * * Tests for the settings dashboard, tabs, and configuration options. * * Run: node test_settings_pages_ci.js */ const { setupTest, teardownTest, TestResults, log, delay, waitForVisible, navigateTo, withTimeout } = require('./test_lib'); // Open /settings/ on a clean "All Settings" tab. navigateTo() no-ops when the // page is already on /settings/, so a prior test's active tab leaks in (e.g. // the 'llm' tab, which hides search settings). Force the 'all' tab so every // setting renders before the Input assertions read the DOM. async function openSettingsAllTab(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 }); // Require the 'all' tab to exist so a missing tab fails here with context, // rather than silently proceeding to an opaque downstream timeout. await page.waitForSelector('.ldr-settings-tab[data-tab="all"]', { timeout: 5000 }); const isActive = await page.$eval('.ldr-settings-tab[data-tab="all"]', el => el.classList.contains('active')); if (!isActive) { await page.click('.ldr-settings-tab[data-tab="all"]'); await page.waitForFunction( () => document.querySelector('.ldr-settings-tab[data-tab="all"]')?.classList.contains('active') === true, { timeout: 5000 } ).catch(() => {}); } } // llm.provider and search.tool both render as custom dropdowns (custom_dropdown.html): // a hidden input[name=] carries the form value inside a .ldr-custom-dropdown, // and the dropdown JS resolves the selection asynchronously after load. Assert a // real value ends up selected. The .catch on the value-wait lets the assertion // below report a specific `value=""` message rather than a raw timeout. async function assertCustomDropdownSelected(page, name, label) { await page.waitForSelector(`#settings-content [name="${name}"]`, { timeout: 15000 }); await page.waitForFunction( (n) => (document.querySelector(`[name="${n}"]`)?.value || '').length > 0, { timeout: 10000 }, name ).catch(() => {}); const result = await page.evaluate((n) => { const hidden = document.querySelector(`[name="${n}"]`); return { value: hidden?.value || '', hasDropdown: !!hidden?.closest('.ldr-custom-dropdown') }; }, name); const passed = result.value.length > 0 && result.hasDropdown; return { passed, message: passed ? `${label} renders a real selection ("${result.value}") in a custom dropdown` : `${label} incomplete (value="${result.value}", customDropdown=${result.hasDropdown})` }; } // ============================================================================ // Settings Page Structure Tests // ============================================================================ const SettingsPageTests = { async settingsPageLoads(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(() => { return { hasSettingsContent: !!document.querySelector('.settings-container, .ldr-settings, #settings, .settings-dashboard'), hasForm: !!document.querySelector('form'), hasTabs: !!document.querySelector('.nav-tabs, .tab-list, [role="tablist"], .settings-tabs'), pageTitle: document.title, hasAnyInputs: document.querySelectorAll('input, select, textarea').length > 0 }; }); const passed = result.hasSettingsContent || result.hasAnyInputs; return { passed, message: passed ? `Settings page loaded (content=${result.hasSettingsContent}, form=${result.hasForm}, tabs=${result.hasTabs})` : 'Settings page failed to load expected content' }; }, async settingsTabsExist(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const result = await page.evaluate(() => { const tabs = document.querySelectorAll('.nav-tabs .nav-link, .tab-list button, [role="tab"], .settings-tab'); const tabTexts = Array.from(tabs).map(t => t.textContent?.trim()); // Also check for category sections if tabs aren't used const categories = document.querySelectorAll('.settings-category, .setting-section, h2, h3'); const categoryTexts = Array.from(categories).map(c => c.textContent?.trim()).filter(t => t && t.length < 50); return { tabCount: tabs.length, tabNames: tabTexts.slice(0, 10), categoryCount: categories.length, categoryNames: categoryTexts.slice(0, 10) }; }); const hasTabs = result.tabCount > 0; const hasCategories = result.categoryCount > 0; return { passed: hasTabs || hasCategories, message: hasTabs ? `Found ${result.tabCount} tabs: ${result.tabNames.join(', ')}` : hasCategories ? `Found ${result.categoryCount} setting categories` : 'No tabs or categories found' }; }, async settingsTabNavigation(page, baseUrl) { await navigateTo(page, `${baseUrl}/settings/`); const tabs = await page.$$('.nav-tabs .nav-link, [role="tab"]'); if (tabs.length < 2) { return { passed: null, skipped: true, message: 'Not enough tabs to test navigation' }; } try { // Click second tab using evaluate to avoid stale element issues await page.evaluate(() => { const tabElements = document.querySelectorAll('.nav-tabs .nav-link, [role="tab"]'); if (tabElements[1]) { tabElements[1].click(); } }); // Wait for page to stabilize await delay(1000); // Wait for network to settle in case of navigation await page.waitForFunction(() => document.readyState === 'complete', { timeout: 5000 }).catch(() => {}); const result = await page.evaluate(() => { const activeTab = document.querySelector('.nav-link.active, [role="tab"][aria-selected="true"]'); const visiblePanel = document.querySelector('.tab-pane.active, .tab-pane.show, [role="tabpanel"]:not([hidden])'); return { hasActiveTab: !!activeTab, activeTabText: activeTab?.textContent?.trim(), hasVisiblePanel: !!visiblePanel }; }); return { passed: result.hasActiveTab, message: result.hasActiveTab ? `Tab navigation works (active: "${result.activeTabText}")` : 'Tab navigation not working properly' }; } catch (err) { // Handle navigation or context destruction gracefully if (err.message && (err.message.includes('context') || err.message.includes('navigation') || err.message.includes('Target closed'))) { return { passed: null, skipped: true, message: 'Tab click caused page navigation - skipping' }; } return { passed: null, skipped: true, message: `Tab click failed: ${err.message}` }; } } }; // ============================================================================ // Settings Input Tests // ============================================================================ const SettingsInputTests = { async modelProviderSetting(page, baseUrl) { // llm.provider renders as a custom dropdown; assert a *real* provider // ends up selected (the old test returned passed:true for any input). await openSettingsAllTab(page, baseUrl); return assertCustomDropdownSelected(page, 'llm.provider', 'Provider setting'); }, async searchEngineSetting(page, baseUrl) { // search.tool also renders as a custom dropdown. The old test's // select[name*=search] matched an unrelated country