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
465 lines
18 KiB
JavaScript
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 };
|