Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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