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

465 lines
18 KiB
JavaScript

#!/usr/bin/env node
/**
* Research Workflow UI Tests
*
* Tests for the research submission, progress tracking, and results viewing workflow.
*
* Run: node test_research_workflow_ci.js
*/
const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib');
// ============================================================================
// Research Form Tests
// ============================================================================
const ResearchFormTests = {
async researchFormStructure(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const queryInput = document.querySelector('#query, textarea[name="query"], input[name="query"]');
const submitBtn = document.querySelector('button[type="submit"], input[type="submit"], .ldr-research-submit');
const form = document.querySelector('form');
return {
hasQueryInput: !!queryInput,
queryInputType: queryInput?.tagName?.toLowerCase(),
hasSubmitBtn: !!submitBtn,
submitBtnText: submitBtn?.textContent?.trim(),
hasForm: !!form
};
});
const passed = result.hasQueryInput && result.hasSubmitBtn;
return {
passed,
message: passed
? `Research form complete (query: ${result.queryInputType}, submit: "${result.submitBtnText}")`
: `Missing: query=${result.hasQueryInput}, submit=${result.hasSubmitBtn}`
};
},
async advancedOptionsToggle(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Look for advanced options toggle button/link
const toggleBtn = document.querySelector(
'[data-bs-toggle="collapse"][data-bs-target*="advanced"], ' +
'.advanced-options-toggle, ' +
'button[onclick*="advanced"], ' +
'a[href*="advanced"], ' +
'.ldr-advanced-toggle'
);
if (!toggleBtn) return { hasToggle: false };
// Find the collapsible section
const advancedSection = document.querySelector(
'#advancedOptions, .advanced-options, .collapse, [id*="advanced"]'
);
return {
hasToggle: true,
toggleText: toggleBtn.textContent?.trim(),
hasSection: !!advancedSection,
sectionVisible: advancedSection ? window.getComputedStyle(advancedSection).display !== 'none' : null
};
});
if (!result.hasToggle) {
return { passed: null, skipped: true, message: 'No advanced options toggle found' };
}
return {
passed: result.hasSection,
message: result.hasSection
? `Advanced options toggle found ("${result.toggleText}"), section exists`
: 'Toggle found but section missing'
};
},
async modelProviderDropdown(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const providerSelect = document.querySelector(
'select[name*="provider"], ' +
'#llm_provider, ' +
'.ldr-provider-select, ' +
'[data-setting="llm.provider"]'
);
if (!providerSelect) return { exists: false };
const options = Array.from(providerSelect.options || providerSelect.querySelectorAll('option'));
return {
exists: true,
optionCount: options.length,
options: options.slice(0, 5).map(o => o.textContent?.trim() || o.value),
currentValue: providerSelect.value
};
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No provider dropdown on main page (may be in settings)' };
}
return {
passed: result.optionCount > 0,
message: `Provider dropdown has ${result.optionCount} options: ${result.options.join(', ')}`
};
},
async searchEngineDropdown(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const engineSelect = document.querySelector(
'select[name*="search"], ' +
'#search_tool, ' +
'.ldr-search-engine-select, ' +
'[data-setting*="search"]'
);
if (!engineSelect) return { exists: false };
const options = Array.from(engineSelect.options || engineSelect.querySelectorAll('option'));
return {
exists: true,
optionCount: options.length,
options: options.slice(0, 5).map(o => o.textContent?.trim() || o.value)
};
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No search engine dropdown on main page' };
}
return {
passed: result.optionCount > 0,
message: `Search engine dropdown has ${result.optionCount} options: ${result.options.join(', ')}`
};
},
async researchModeSelector(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Look for mode selector (radio buttons, toggle, or dropdown)
const modeRadios = document.querySelectorAll('input[name="mode"], input[name="research_mode"]');
const modeSelect = document.querySelector('select[name="mode"], select[name="research_mode"]');
const modeToggle = document.querySelector('.mode-toggle, .research-mode-toggle, [data-mode]');
if (modeRadios.length > 0) {
return {
exists: true,
type: 'radio',
options: Array.from(modeRadios).map(r => r.value)
};
}
if (modeSelect) {
return {
exists: true,
type: 'select',
options: Array.from(modeSelect.options).map(o => o.value)
};
}
if (modeToggle) {
return {
exists: true,
type: 'toggle'
};
}
return { exists: false };
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No research mode selector found' };
}
return {
passed: true,
message: `Research mode selector found (${result.type})${result.options ? ': ' + result.options.join(', ') : ''}`
};
}
};
// ============================================================================
// Progress Page Tests
// ============================================================================
const ProgressTests = {
async progressPageStructure(page, baseUrl) {
// The progress page (pages/progress.html) renders server-side for any
// research id — the route does not look the id up in the DB, so this is
// CI-safe with a fresh DB / no LLM. Assert the progress-page-specific
// container plus its concrete progress bar and status indicator, so the
// test fails if the wrong page (login/error/results) loaded instead.
await navigateTo(page, `${baseUrl}/progress/1`);
const result = await page.evaluate(() => {
const progressPage = document.querySelector('#research-progress');
const progressFill = document.querySelector('#progress-bar.ldr-progress-fill');
const progressBar = document.querySelector('.ldr-progress-bar[role="progressbar"]');
const statusText = document.querySelector('#status-text.ldr-status-indicator');
return {
onProgressPage: !!progressPage,
hasProgressFill: !!progressFill,
hasProgressBar: !!progressBar,
hasStatusText: !!statusText,
statusValue: statusText?.textContent?.trim() || null,
pageTitle: document.title
};
});
const passed = result.onProgressPage && result.hasProgressBar &&
result.hasProgressFill && result.hasStatusText;
return {
passed,
message: passed
? `Progress page structure OK (#research-progress, progress bar, status="${result.statusValue}")`
: `Progress page missing expected elements ` +
`(page=${result.onProgressPage}, bar=${result.hasProgressBar}, ` +
`fill=${result.hasProgressFill}, status=${result.hasStatusText}, title="${result.pageTitle}")`
};
}
};
// ============================================================================
// Results Page Tests
// ============================================================================
const ResultsTests = {
async resultsPageStructure(page, baseUrl) {
// The results page (pages/results.html) renders server-side for any
// research id — the route renders the template without a DB lookup, so
// this is CI-safe with a fresh DB / no LLM. Assert the results-page
// container, the exact title text, the report container and the
// metadata block, so the test fails if a login/error page loaded.
await navigateTo(page, `${baseUrl}/results/1`);
const result = await page.evaluate(() => {
const resultsPage = document.querySelector('#research-results');
const title = document.querySelector('#research-results h1.ldr-page-title');
// #results-content is the report container that results.js populates
const reportContainer = document.querySelector('#results-content.ldr-results-content');
const metadata = document.querySelector('#research-metadata.ldr-results-metadata');
return {
onResultsPage: !!resultsPage,
titleText: title?.textContent?.trim() || null,
hasReport: !!reportContainer,
hasMetadata: !!metadata,
pageTitle: document.title
};
});
const passed = result.onResultsPage &&
result.titleText === 'Research Results' &&
result.hasReport && result.hasMetadata;
return {
passed,
message: passed
? `Results page structure OK (#research-results, title="${result.titleText}", report container, metadata)`
: `Results page missing expected elements ` +
`(page=${result.onResultsPage}, title="${result.titleText}", ` +
`report=${result.hasReport}, metadata=${result.hasMetadata}, docTitle="${result.pageTitle}")`
};
},
async exportButtonsExist(page, baseUrl) {
// Export controls are rendered server-side in pages/results.html with
// stable ids, so navigate straight to the results page (no DB/LLM
// dependency) and assert the concrete controls by id instead of doing a
// substring match over arbitrary button text.
await navigateTo(page, `${baseUrl}/results/1`);
const result = await page.evaluate(() => {
return {
onResultsPage: !!document.querySelector('#research-results'),
hasPdfBtn: !!document.querySelector('#download-pdf-btn'),
hasExportDropdown: !!document.querySelector('#export-dropdown-btn[data-bs-toggle="dropdown"]'),
hasMarkdownBtn: !!document.querySelector('#export-markdown-btn'),
hasLatexBtn: !!document.querySelector('#export-latex-btn')
};
});
const passed = result.onResultsPage && result.hasPdfBtn &&
result.hasExportDropdown && result.hasMarkdownBtn;
return {
passed,
message: passed
? `Export controls present (#download-pdf-btn, #export-dropdown-btn, #export-markdown-btn, latex=${result.hasLatexBtn})`
: `Export controls missing ` +
`(page=${result.onResultsPage}, pdf=${result.hasPdfBtn}, ` +
`dropdown=${result.hasExportDropdown}, markdown=${result.hasMarkdownBtn})`
};
},
async starRatingExists(page, baseUrl) {
await navigateTo(page, `${baseUrl}/history`);
const researchId = await page.evaluate(() => {
const link = document.querySelector('a[href*="/results/"]');
if (link) {
const match = link.href.match(/\/results\/(\d+)/);
return match ? match[1] : null;
}
return null;
});
if (!researchId) {
return { passed: null, skipped: true, message: 'No completed research to test star rating' };
}
await navigateTo(page, `${baseUrl}/results/${researchId}`);
const result = await page.evaluate(() => {
const stars = document.querySelectorAll('.star-rating, .rating, [class*="star"], .fa-star, .bi-star');
const ratingContainer = document.querySelector('.rating-container, .star-container, [data-rating]');
return {
hasStars: stars.length > 0,
starCount: stars.length,
hasRatingContainer: !!ratingContainer
};
});
if (!result.hasStars && !result.hasRatingContainer) {
return { passed: null, skipped: true, message: 'No star rating system on results page' };
}
return {
passed: true,
message: `Star rating found (${result.starCount} star elements)`
};
}
};
// ============================================================================
// API Tests (checking API endpoints respond)
// ============================================================================
const ApiTests = {
async historyApiResponds(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(async (url) => {
try {
const response = await fetch(`${url}/api/history`);
return {
ok: response.ok,
status: response.status,
isJson: response.headers.get('content-type')?.includes('application/json')
};
} catch (e) {
return { ok: false, error: e.message };
}
}, baseUrl);
return {
passed: result.ok,
message: result.ok
? `History API responds (status ${result.status})`
: `History API failed: ${result.error || 'status ' + result.status}`
};
},
async settingsApiResponds(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(async (url) => {
try {
const response = await fetch(`${url}/settings/api`);
return {
ok: response.ok,
status: response.status
};
} catch (e) {
return { ok: false, error: e.message };
}
}, baseUrl);
return {
passed: result.ok,
message: result.ok
? `Settings API responds (status ${result.status})`
: `Settings API failed: ${result.error || 'status ' + result.status}`
};
}
};
// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
log.section('Research Workflow Tests');
const ctx = await setupTest({ authenticate: true });
const results = new TestResults('Research Workflow Tests');
const { page } = ctx;
const { baseUrl } = ctx.config;
const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
async function run(category, name, testFn) {
try {
const result = await withTimeout(
testFn(page, baseUrl),
subTestTimeout,
`${category}/${name}`
);
if (result.skipped) {
results.skip(category, name, result.message);
} else {
results.add(category, name, result.passed, result.message);
}
} catch (error) {
results.add(category, name, false, `Error: ${error.message}`);
}
}
try {
// Research Form Tests
log.section('Research Form');
await run('Form', 'Research Form Structure', (p, u) => ResearchFormTests.researchFormStructure(p, u));
await run('Form', 'Advanced Options Toggle', (p, u) => ResearchFormTests.advancedOptionsToggle(p, u));
await run('Form', 'Model Provider Dropdown', (p, u) => ResearchFormTests.modelProviderDropdown(p, u));
await run('Form', 'Search Engine Dropdown', (p, u) => ResearchFormTests.searchEngineDropdown(p, u));
await run('Form', 'Research Mode Selector', (p, u) => ResearchFormTests.researchModeSelector(p, u));
// Progress/Results Tests
log.section('Progress & Results');
await run('Results', 'Progress/Results Page Structure', (p, u) => ProgressTests.progressPageStructure(p, u));
await run('Results', 'Results Page Structure', (p, u) => ResultsTests.resultsPageStructure(p, u));
await run('Results', 'Export Buttons Exist', (p, u) => ResultsTests.exportButtonsExist(p, u));
await run('Results', 'Star Rating Exists', (p, u) => ResultsTests.starRatingExists(p, u));
// API Tests
log.section('API Endpoints');
await run('API', 'History API Responds', (p, u) => ApiTests.historyApiResponds(p, u));
await run('API', 'Settings API Responds', (p, u) => ApiTests.settingsApiResponds(p, u));
} catch (error) {
log.error(`Fatal error: ${error.message}`);
console.error(error.stack);
} finally {
results.print();
results.save();
await teardownTest(ctx);
process.exit(results.exitCode());
}
}
// Run if executed directly
if (require.main === module) {
main().catch(error => {
console.error('Test runner failed:', error);
process.exit(1);
});
}
module.exports = { ResearchFormTests, ProgressTests, ResultsTests, ApiTests };