7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
500 lines
21 KiB
JavaScript
500 lines
21 KiB
JavaScript
/**
|
|
* 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}`);
|
|
}
|
|
});
|
|
});
|
|
});
|