/** * Comprehensive UI Functionality Tests * * Tests the main user workflows: * 1. Research workflow (start, monitor progress, view results) * 2. Settings changes and persistence * 3. Library functionality (view, search, collections) * 4. News page (search, subscriptions, filters) * 5. Navigation and core UI elements */ const puppeteer = require('puppeteer'); const { expect } = require('chai'); // Import shared helpers const { BASE_URL, HEADLESS, SLOW_MO, SCREENSHOT_DIR, delay, takeScreenshot, ensureLoggedIn, getLaunchOptions } = require('./helpers'); // Test configuration - static username for UI tests const TEST_USERNAME = 'ui_test_user'; const TEST_PASSWORD = 'Test_password_123'; // Helper to log page content (unique to this test file) async function logPageInfo(page, label = '') { const url = page.url(); const title = await page.title(); console.log(`\n--- ${label} ---`); console.log(`URL: ${url}`); console.log(`Title: ${title}`); } describe('UI Functionality Tests', function() { this.timeout(300000); // 5 minute timeout for full suite let browser; let page; before(async () => { console.log(`\nStarting browser (headless: ${HEADLESS}, slowMo: ${SLOW_MO})`); console.log(`Screenshots will be saved to: ${SCREENSHOT_DIR}`); browser = await puppeteer.launch(getLaunchOptions()); page = await browser.newPage(); await page.setViewport({ width: 1400, height: 900 }); page.setDefaultNavigationTimeout(30000); // Log browser console messages page.on('console', msg => { if (msg.type() === 'error') { console.log('Browser ERROR:', msg.text()); } }); }); after(async () => { if (browser) { await browser.close(); } }); describe('Authentication', () => { it('should be logged in (login or register as needed)', async () => { await ensureLoggedIn(page, TEST_USERNAME, TEST_PASSWORD); await takeScreenshot(page, 'after-auth'); const url = page.url(); console.log(` -> Final URL: ${url}`); expect(url).to.not.include('/login'); expect(url).to.not.include('/register'); }); }); describe('Research Workflow', () => { it('should load the research page with form elements', async () => { await logPageInfo(page, 'Research Page'); await page.goto(`${BASE_URL}/`, { waitUntil: 'domcontentloaded' }); await takeScreenshot(page, 'research-page'); // Verify key elements exist const queryInput = await page.$('#query, textarea[name="query"]'); expect(queryInput).to.not.be.null; console.log(' ✓ Query input found'); const submitBtn = await page.$('#start-research-btn, button[type="submit"]'); expect(submitBtn).to.not.be.null; console.log(' ✓ Submit button found'); // Check for research mode options const quickMode = await page.$('#mode-quick, [data-mode="quick"]'); const detailedMode = await page.$('#mode-detailed, [data-mode="detailed"]'); expect(quickMode).to.not.be.null; expect(detailedMode).to.not.be.null; console.log(' ✓ Research mode options found'); }); it('should expand advanced options and show model/search settings', async () => { // Ensure advanced options are expanded (may already be open by default) const toggle = await page.$('.ldr-advanced-options-toggle'); if (toggle) { const panel = await page.$('.ldr-advanced-options-panel'); const isVisible = panel ? await page.evaluate(el => { const style = window.getComputedStyle(el); return style.visibility !== 'hidden' && style.opacity !== '0'; }, panel) : false; if (!isVisible) { await toggle.click(); await delay(500); } await takeScreenshot(page, 'advanced-options-expanded'); // Check for advanced options content const modelProvider = await page.$('#model_provider, select[name="model_provider"]'); const searchEngine = await page.$('#search_engine, [id*="search-engine"]'); console.log(` Model provider visible: ${modelProvider !== null}`); console.log(` Search engine visible: ${searchEngine !== null}`); } }); it('should type a research query without errors', async () => { const testQuery = 'What is the capital of France?'; const queryInput = await page.$('#query, textarea[name="query"]'); await queryInput.click({ clickCount: 3 }); // Select all await page.keyboard.press('Backspace'); // Clear await page.type('#query', testQuery); await takeScreenshot(page, 'research-query-entered'); const value = await page.$eval('#query', el => el.value); expect(value).to.equal(testQuery); console.log(` ✓ Query entered: "${testQuery}"`); }); it('should pass form validation when hidden context_window has a non-step value (#3909 regression)', async () => { // Regression for #3909 / PR #4051: the context_window input lives // inside a display:none container that is only shown for local // providers. Previously the input had step="512", so any stored // value not aligned to that grid (e.g. 25000) failed HTML5 // validation. Because the field is hidden, the browser cannot // focus it to surface the error, so the Start Research click // silently no-ops with no log line. We assert via checkValidity() // (no submit) so the test does not consume LLM credits. await page.goto(`${BASE_URL}/`, { waitUntil: 'domcontentloaded' }); await delay(500); const check = await page.evaluate(() => { const input = document.getElementById('context_window'); const container = document.getElementById('context_window_container'); const query = document.getElementById('query'); const form = document.getElementById('research-form'); if (!input || !container || !query || !form) { return { ok: false, reason: 'expected form elements not found' }; } // Populate the textarea so query (required-by-JS) is not the // reason validation might fail. query.value = 'regression check for #3909'; // The exact stored value from the reporter's issue. 25000 is // not a multiple of 512 from min=512. input.value = '25000'; return { ok: true, hiddenForCloudProvider: window.getComputedStyle(container).display === 'none', formValid: form.checkValidity(), inputValid: input.validity.valid, stepMismatch: input.validity.stepMismatch, rangeUnderflow: input.validity.rangeUnderflow, rangeOverflow: input.validity.rangeOverflow, }; }); expect(check.ok, check.reason).to.be.true; expect( check.hiddenForCloudProvider, '#context_window_container should be hidden by default (cloud provider) — the regression scenario' ).to.be.true; expect( check.formValid, 'research-form.checkValidity() must return true with context_window=25000 in a hidden container. ' + `If this fails, the step="512" constraint has been re-added to #context_window — see PR #4051. ` + `Validity flags: ${JSON.stringify({ inputValid: check.inputValid, stepMismatch: check.stepMismatch, rangeUnderflow: check.rangeUnderflow, rangeOverflow: check.rangeOverflow, })}` ).to.be.true; console.log(' ✓ Hidden context_window with non-step value does not block submit'); }); it('should handle research form submission without crashing', async () => { // Note: We don't actually run a full research (would take too long) // Just verify the form can be submitted and the UI responds // Get current URL before submit const urlBefore = page.url(); // Check if form is valid before submitting const query = await page.$eval('#query', el => el.value); console.log(` Query before submit: "${query}"`); if (query && query.length > 0) { // Submit the form - may trigger navigation to progress page const navigationPromise = page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => null); await page.click('#start-research-btn'); await navigationPromise; await takeScreenshot(page, 'after-research-submit'); // Check URL changed to progress page or research started const urlAfter = page.url(); console.log(` URL after submit: ${urlAfter}`); // Verify page didn't crash (should have some content) const bodyText = await page.$eval('body', el => el.textContent.substring(0, 500)); expect(bodyText.length).to.be.greaterThan(0); console.log(' ✓ Page responded to form submission'); } }); }); describe('Settings Page', () => { it('should load settings page with all sections', async () => { await page.goto(`${BASE_URL}/settings`, { waitUntil: 'domcontentloaded' }); await logPageInfo(page, 'Settings Page'); await takeScreenshot(page, 'settings-page'); // Check for settings container const settingsContainer = await page.$('.ldr-settings-container, #settings'); expect(settingsContainer).to.not.be.null; console.log(' ✓ Settings container found'); // Check for tabs const tabs = await page.$$('.ldr-settings-tab'); console.log(` Found ${tabs.length} setting tabs`); expect(tabs.length).to.be.greaterThan(0); }); it('should have working settings tabs navigation', async () => { // Get all tabs const tabs = await page.$$('.ldr-settings-tab'); for (let i = 0; i < Math.min(tabs.length, 4); i++) { const tabText = await tabs[i].evaluate(el => el.textContent.trim()); await tabs[i].click(); await delay(500); console.log(` Clicked tab: "${tabText}"`); } await takeScreenshot(page, 'settings-tabs-navigated'); }); it('should display LLM provider options', async () => { // Wait for settings to load await delay(1000); // Look for LLM-related settings const llmTab = await page.$('[data-tab="llm"]'); if (llmTab) { await llmTab.click(); await delay(500); } await takeScreenshot(page, 'settings-llm-section'); // Check for provider dropdown or options const bodyText = await page.$eval('body', el => el.textContent.toLowerCase()); const hasProviderOptions = bodyText.includes('ollama') || bodyText.includes('openai') || bodyText.includes('provider'); console.log(` Has provider options: ${hasProviderOptions}`); }); it('should have search input for filtering settings', async () => { const searchInput = await page.$('#settings-search'); expect(searchInput).to.not.be.null; await searchInput.type('ollama'); await delay(500); await takeScreenshot(page, 'settings-search-filter'); console.log(' ✓ Settings search works'); }); }); describe('News Page', () => { it('should load news page with all components', async () => { await page.goto(`${BASE_URL}/news/`, { waitUntil: 'domcontentloaded' }); await logPageInfo(page, 'News Page'); await takeScreenshot(page, 'news-page'); // Check for news container const newsContainer = await page.$('.ldr-news-page-wrapper, .ldr-news-container'); expect(newsContainer).to.not.be.null; console.log(' ✓ News container found'); }); it('should have search functionality', async () => { const searchInput = await page.$('#news-search'); expect(searchInput).to.not.be.null; await searchInput.type('technology'); await takeScreenshot(page, 'news-search-entered'); const searchBtn = await page.$('#search-btn'); if (searchBtn) { await searchBtn.click(); await delay(1000); await takeScreenshot(page, 'news-search-results'); } console.log(' ✓ News search input works'); }); it('should have filter controls', async () => { // Check for time filter buttons const filterBtns = await page.$$('.ldr-filter-btn'); console.log(` Found ${filterBtns.length} filter buttons`); expect(filterBtns.length).to.be.greaterThan(0); // Click on different time filters for (const btn of filterBtns.slice(0, 3)) { const text = await btn.evaluate(el => el.textContent.trim()); await btn.click(); console.log(` Clicked filter: "${text}"`); await delay(300); } await takeScreenshot(page, 'news-filters'); }); it('should have subscription links', async () => { const createSubLink = await page.$('a[href="/news/subscriptions/new"]'); const manageSubLink = await page.$('a[href="/news/subscriptions"]'); console.log(` Create subscription link: ${createSubLink !== null}`); console.log(` Manage subscriptions link: ${manageSubLink !== null}`); }); it('should load subscriptions page', async () => { await page.goto(`${BASE_URL}/news/subscriptions`, { waitUntil: 'domcontentloaded' }); await takeScreenshot(page, 'subscriptions-page'); const url = page.url(); expect(url).to.include('/subscriptions'); console.log(' ✓ Subscriptions page loaded'); }); }); describe('Library Page', () => { it('should load library page with filters', async () => { await page.goto(`${BASE_URL}/library/`, { waitUntil: 'domcontentloaded' }); await logPageInfo(page, 'Library Page'); await takeScreenshot(page, 'library-page'); // Check for library container const libraryContainer = await page.$('.ldr-library-container'); expect(libraryContainer).to.not.be.null; console.log(' ✓ Library container found'); }); it('should have collection filter', async () => { const collectionFilter = await page.$('#filter-collection'); expect(collectionFilter).to.not.be.null; console.log(' ✓ Collection filter found'); }); it('should have domain filter', async () => { const domainFilter = await page.$('#filter-domain'); expect(domainFilter).to.not.be.null; console.log(' ✓ Domain filter found'); }); it('should have search functionality', async () => { const searchInput = await page.$('#search-documents'); expect(searchInput).to.not.be.null; await searchInput.type('test search'); await takeScreenshot(page, 'library-search'); console.log(' ✓ Library search input works'); }); it('should have action buttons', async () => { const syncBtn = await page.$('button[onclick="showSyncModal()"]'); console.log(` Sync button: ${syncBtn !== null}`); const getAllPdfsBtn = await page.$('button[onclick="downloadAllNew()"]'); console.log(` Get All PDFs button: ${getAllPdfsBtn !== null}`); await takeScreenshot(page, 'library-actions'); }); it('should navigate to collections page', async () => { await page.goto(`${BASE_URL}/library/collections`, { waitUntil: 'domcontentloaded' }); await takeScreenshot(page, 'collections-page'); const url = page.url(); expect(url).to.include('/collections'); console.log(' ✓ Collections page loaded'); }); }); describe('History Page', () => { it('should load history page', async () => { await page.goto(`${BASE_URL}/history`, { waitUntil: 'domcontentloaded' }); await logPageInfo(page, 'History Page'); await takeScreenshot(page, 'history-page'); const url = page.url(); // History might redirect to home with history modal or have its own page console.log(` History page URL: ${url}`); }); }); describe('Embedding Settings Page', () => { it('should load embedding settings page', async () => { await page.goto(`${BASE_URL}/library/embedding-settings`, { waitUntil: 'domcontentloaded' }); await logPageInfo(page, 'Embedding Settings Page'); await takeScreenshot(page, 'embedding-settings'); const url = page.url(); expect(url).to.include('/embedding-settings'); console.log(' ✓ Embedding settings page loaded'); }); }); describe('Navigation', () => { it('should have working sidebar navigation', async () => { await page.goto(`${BASE_URL}/`, { waitUntil: 'domcontentloaded' }); // Check sidebar exists const sidebar = await page.$('.ldr-sidebar, aside, nav'); expect(sidebar).to.not.be.null; console.log(' ✓ Sidebar found'); await takeScreenshot(page, 'sidebar-navigation'); }); it('should navigate to all main sections without errors', async () => { const routes = [ { path: '/', name: 'Home', minContent: 100 }, { path: '/settings', name: 'Settings', minContent: 100 }, { path: '/news/', name: 'News', minContent: 100 }, { path: '/library/', name: 'Library', minContent: 100 }, { path: '/library/embedding-settings', name: 'Embeddings', minContent: 10 } // May have minimal content ]; for (const route of routes) { await page.goto(`${BASE_URL}${route.path}`, { waitUntil: 'domcontentloaded' }); const title = await page.title(); console.log(` ${route.name}: ${title}`); // Verify page loaded (has some content - not a complete crash) const bodyText = await page.$eval('body', el => el.textContent.length); expect(bodyText).to.be.greaterThan(route.minContent); } console.log(' ✓ All main routes load successfully'); }); }); describe('Error Handling', () => { it('should handle 404 pages gracefully', async () => { await page.goto(`${BASE_URL}/nonexistent-page-xyz`, { waitUntil: 'domcontentloaded' }); await takeScreenshot(page, 'error-404'); // Should not crash, should have some content const bodyText = await page.$eval('body', el => el.textContent); expect(bodyText.length).to.be.greaterThan(0); console.log(' ✓ 404 page handled gracefully'); }); }); describe('Logout', () => { it('should be able to logout', async () => { // Find and click logout link await page.goto(`${BASE_URL}/settings`, { waitUntil: 'domcontentloaded' }); const logoutLink = await page.$('a[href*="logout"]'); if (logoutLink) { await logoutLink.click(); await delay(1000); await takeScreenshot(page, 'after-logout'); const url = page.url(); console.log(` After logout URL: ${url}`); } }); }); });