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

767 lines
29 KiB
JavaScript

#!/usr/bin/env node
/**
* Research Form UI Tests
*
* Tests for the research form including advanced options, mode selection,
* dropdowns, and form submission.
*
* Run: node test_research_form_ci.js
*/
const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib');
// ============================================================================
// Research Form Basic Tests
// ============================================================================
const ResearchFormBasicTests = {
async formElementsPresent(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const queryInput = document.querySelector('textarea[name="query"], input[name="query"], #query, #research-query');
const submitBtn = document.querySelector('button[type="submit"], .start-research, #start-research');
const form = document.querySelector('form');
return {
hasQueryInput: !!queryInput,
hasSubmitBtn: !!submitBtn,
hasForm: !!form,
queryType: queryInput?.tagName?.toLowerCase(),
submitText: submitBtn?.textContent?.trim()
};
});
const passed = result.hasQueryInput && result.hasSubmitBtn;
return {
passed,
message: `Form elements: query=${result.hasQueryInput} (${result.queryType}), submit="${result.submitText}"`
};
},
async queryInputAcceptsText(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const testQuery = 'Test research query about artificial intelligence';
const result = await page.evaluate((query) => {
const queryInput = document.querySelector('textarea[name="query"], input[name="query"], #query, #research-query');
if (!queryInput) return { hasInput: false };
queryInput.value = query;
queryInput.dispatchEvent(new Event('input', { bubbles: true }));
return {
hasInput: true,
value: queryInput.value,
matches: queryInput.value === query
};
}, testQuery);
return {
passed: result.matches,
message: result.matches
? 'Query input accepts and displays text correctly'
: `Query input issue (has input: ${result.hasInput}, value length: ${result.value?.length || 0})`
};
},
async queryInputPlaceholder(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const queryInput = document.querySelector('textarea[name="query"], input[name="query"], #query, #research-query');
return {
hasInput: !!queryInput,
placeholder: queryInput?.placeholder,
hasPlaceholder: !!queryInput?.placeholder && queryInput.placeholder.length > 0
};
});
return {
passed: result.hasPlaceholder,
message: result.hasPlaceholder
? `Placeholder text: "${result.placeholder?.substring(0, 50)}..."`
: 'No placeholder text set'
};
},
async submitButtonState(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const submitBtn = document.querySelector('button[type="submit"], .start-research, #start-research');
const queryInput = document.querySelector('textarea[name="query"], input[name="query"], #query');
// Check initial state
const initialDisabled = submitBtn?.disabled;
// Type something
if (queryInput) {
queryInput.value = 'Test query';
queryInput.dispatchEvent(new Event('input', { bubbles: true }));
}
// Check after typing
const afterTypingDisabled = submitBtn?.disabled;
return {
hasButton: !!submitBtn,
initialDisabled,
afterTypingDisabled,
buttonText: submitBtn?.textContent?.trim()
};
});
// Button should either always be enabled or enable after typing
const passed = result.hasButton && (result.afterTypingDisabled === false || result.initialDisabled === false);
return {
passed,
message: `Submit button: "${result.buttonText}" (initial disabled=${result.initialDisabled}, after typing=${result.afterTypingDisabled})`
};
}
};
// ============================================================================
// Research Mode Tests
// ============================================================================
const ResearchModeTests = {
async modeSelectionExists(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Look for mode selection (radio buttons, toggle, or dropdown)
const modeRadios = document.querySelectorAll('input[type="radio"][name*="mode"]');
const modeSelect = document.querySelector('select[name*="mode"], #research-mode');
const modeToggle = document.querySelector('.mode-toggle, [class*="mode-selector"], .research-mode');
// Also check for Quick/Detailed buttons/tabs
const modeButtons = document.querySelectorAll('[data-mode], .mode-btn, .mode-option');
return {
hasRadios: modeRadios.length > 0,
radioCount: modeRadios.length,
hasSelect: !!modeSelect,
hasToggle: !!modeToggle,
hasButtons: modeButtons.length > 0,
buttonCount: modeButtons.length
};
});
const hasMode = result.hasRadios || result.hasSelect || result.hasToggle || result.hasButtons;
if (!hasMode) {
return { passed: null, skipped: true, message: 'No mode selection UI found' };
}
return {
passed: true,
message: `Mode selection: radios=${result.radioCount}, select=${result.hasSelect}, toggle=${result.hasToggle}, buttons=${result.buttonCount}`
};
},
async modeToggleFunctionality(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Find mode options
const modeRadios = document.querySelectorAll('input[type="radio"][name*="mode"]');
const modeButtons = document.querySelectorAll('[data-mode], .mode-btn, .mode-option');
if (modeRadios.length >= 2) {
const firstMode = modeRadios[0];
const secondMode = modeRadios[1];
firstMode.click();
const firstChecked = firstMode.checked;
secondMode.click();
const secondChecked = secondMode.checked;
const firstUnchecked = !firstMode.checked;
return {
type: 'radio',
canToggle: firstChecked && secondChecked && firstUnchecked,
modes: Array.from(modeRadios).map(r => r.value)
};
}
if (modeButtons.length >= 2) {
const firstBtn = modeButtons[0];
const secondBtn = modeButtons[1];
firstBtn.click();
const firstActive = firstBtn.classList.contains('active') || firstBtn.getAttribute('aria-selected') === 'true';
secondBtn.click();
const secondActive = secondBtn.classList.contains('active') || secondBtn.getAttribute('aria-selected') === 'true';
return {
type: 'buttons',
canToggle: true,
firstActive,
secondActive
};
}
return { type: 'none', canToggle: false };
});
if (result.type === 'none') {
return { passed: null, skipped: true, message: 'No mode toggle elements found' };
}
return {
passed: result.canToggle,
message: `Mode toggle (${result.type}): works=${result.canToggle}, modes=${result.modes?.join(', ') || 'button-based'}`
};
}
};
// ============================================================================
// Advanced Options Tests
// ============================================================================
const AdvancedOptionsTests = {
async advancedOptionsToggle(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Look for advanced options toggle
const toggleBtn = document.querySelector(
'[data-toggle="advanced"], ' +
'.advanced-toggle, ' +
'#advanced-options-toggle, ' +
'button[aria-controls*="advanced"], ' +
'.collapsible-header, ' +
'[class*="advanced"] button, ' +
'details summary'
);
const advancedSection = document.querySelector(
'.advanced-options, ' +
'#advanced-options, ' +
'[class*="advanced-settings"], ' +
'details'
);
return {
hasToggle: !!toggleBtn,
hasSection: !!advancedSection,
toggleText: toggleBtn?.textContent?.trim()?.substring(0, 50),
sectionVisible: advancedSection ? window.getComputedStyle(advancedSection).display !== 'none' : null
};
});
if (!result.hasToggle && !result.hasSection) {
return { passed: null, skipped: true, message: 'No advanced options section found' };
}
return {
passed: true,
message: `Advanced options: toggle="${result.toggleText}", section visible=${result.sectionVisible}`
};
},
async advancedOptionsExpand(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
// Click advanced options toggle
const clicked = await page.evaluate(() => {
const toggleBtn = document.querySelector(
'[data-toggle="advanced"], ' +
'.advanced-toggle, ' +
'#advanced-options-toggle, ' +
'button[aria-controls*="advanced"], ' +
'.collapsible-header, ' +
'details summary'
);
if (toggleBtn) {
toggleBtn.click();
return true;
}
return false;
});
if (!clicked) {
return { passed: null, skipped: true, message: 'No advanced options toggle to click' };
}
await delay(300);
const result = await page.evaluate(() => {
const advancedSection = document.querySelector(
'.advanced-options, ' +
'#advanced-options, ' +
'[class*="advanced-settings"], ' +
'details[open]'
);
// Look for advanced option inputs
const modelSelect = document.querySelector('select[name*="model"], #model-select');
const iterationsInput = document.querySelector('input[name*="iteration"], #iterations');
const strategySelect = document.querySelector('select[name*="strategy"], #strategy');
return {
sectionExpanded: !!advancedSection && window.getComputedStyle(advancedSection).display !== 'none',
hasModelSelect: !!modelSelect,
hasIterations: !!iterationsInput,
hasStrategy: !!strategySelect
};
});
const hasContent = result.hasModelSelect || result.hasIterations || result.hasStrategy;
return {
passed: result.sectionExpanded || hasContent,
message: `Advanced options expanded: model=${result.hasModelSelect}, iterations=${result.hasIterations}, strategy=${result.hasStrategy}`
};
},
async modelProviderDropdown(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
// Expand advanced options first
await page.evaluate(() => {
const toggle = document.querySelector('[data-toggle="advanced"], .advanced-toggle, details summary');
if (toggle) toggle.click();
});
await delay(300);
const result = await page.evaluate(() => {
const providerSelect = document.querySelector(
'select[name*="provider"], ' +
'#provider, ' +
'#model-provider, ' +
'select[name*="llm"]'
);
if (!providerSelect) return { hasSelect: false };
const options = Array.from(providerSelect.options).map(o => ({
value: o.value,
text: o.text
}));
return {
hasSelect: true,
optionCount: options.length,
options: options.slice(0, 10)
};
});
if (!result.hasSelect) {
return { passed: null, skipped: true, message: 'No model provider dropdown found' };
}
return {
passed: result.optionCount > 0,
message: `Provider dropdown: ${result.optionCount} options (${result.options.map(o => o.text).join(', ')})`
};
},
async searchEngineDropdown(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
// Expand advanced options
await page.evaluate(() => {
const toggle = document.querySelector('[data-toggle="advanced"], .advanced-toggle, details summary');
if (toggle) toggle.click();
});
await delay(300);
const result = await page.evaluate(() => {
// Check for standard <select>
const engineSelect = document.querySelector(
'select[name*="engine"], ' +
'select[name*="search"], ' +
'#search-engine, ' +
'#engine'
);
if (engineSelect && engineSelect.tagName === 'SELECT') {
const options = Array.from(engineSelect.options).map(o => o.text);
return { hasSelect: true, optionCount: options.length, options: options.slice(0, 10) };
}
// Check for custom dropdown component (used by LDR)
const customInput = document.querySelector('#search_engine, input[id="search_engine"]');
if (customInput) {
return { hasSelect: true, isCustom: true, needsClick: true };
}
return { hasSelect: false };
});
if (!result.hasSelect) {
return { passed: null, skipped: true, message: 'No search engine dropdown found' };
}
// Custom dropdown items are only rendered when the dropdown is opened
if (result.isCustom && result.needsClick) {
// Wait for the custom dropdown input to be ready, then click via JS
// (page.click can fail with "not clickable" if the element is covered)
await page.waitForSelector('#search_engine', { timeout: 5000 });
await page.$eval('#search_engine', el => el.click());
// Wait for dropdown items to render after click
await page.waitForSelector('.ldr-custom-dropdown-item, [data-value]', { timeout: 3000 }).catch(() => {});
await delay(500);
const customResult = await page.evaluate(() => {
const customInput = document.querySelector('#search_engine');
const dropdown = customInput.closest('.ldr-custom-dropdown') || customInput.parentElement;
const items = dropdown ? dropdown.querySelectorAll('.ldr-custom-dropdown-item, [data-value]') : [];
const options = Array.from(items).map(i => i.textContent?.trim()).filter(Boolean);
return { optionCount: options.length, options: options.slice(0, 10) };
});
// Close the dropdown by clicking elsewhere
await page.click('body');
await delay(100);
// Custom dropdown may not render items in CI's minimal environment
if (customResult.optionCount === 0) {
return {
passed: null,
skipped: true,
message: 'Search engine custom dropdown rendered 0 items (skipped in CI)'
};
}
return {
passed: customResult.optionCount > 0,
message: `Search engine dropdown (custom): ${customResult.optionCount} options (${customResult.options.join(', ')})`
};
}
return {
passed: result.optionCount > 0,
message: `Search engine dropdown: ${result.optionCount} options (${result.options.join(', ')})`
};
},
async searchStrategyDropdown(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
// Expand advanced options
await page.evaluate(() => {
const toggle = document.querySelector('[data-toggle="advanced"], .advanced-toggle, details summary');
if (toggle) toggle.click();
});
await delay(300);
const result = await page.evaluate(() => {
const strategySelect = document.querySelector(
'select[name*="strategy"], ' +
'#strategy, ' +
'#search-strategy'
);
if (!strategySelect) return { hasSelect: false };
const options = Array.from(strategySelect.options).map(o => o.text);
return {
hasSelect: true,
optionCount: options.length,
options: options.slice(0, 10)
};
});
if (!result.hasSelect) {
return { passed: null, skipped: true, message: 'No search strategy dropdown found' };
}
return {
passed: result.optionCount > 0,
message: `Strategy dropdown: ${result.optionCount} options (${result.options.join(', ')})`
};
},
async iterationsInput(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
// Expand advanced options
await page.evaluate(() => {
const toggle = document.querySelector('[data-toggle="advanced"], .advanced-toggle, details summary');
if (toggle) toggle.click();
});
await delay(300);
const result = await page.evaluate(() => {
const iterInput = document.querySelector(
'input[name*="iteration"], ' +
'#iterations, ' +
'input[name*="search_iterations"]'
);
if (!iterInput) return { hasInput: false };
return {
hasInput: true,
type: iterInput.type,
min: iterInput.min,
max: iterInput.max,
value: iterInput.value
};
});
if (!result.hasInput) {
return { passed: null, skipped: true, message: 'No iterations input found' };
}
return {
passed: true,
message: `Iterations input: type=${result.type}, min=${result.min}, max=${result.max}, default=${result.value}`
};
},
async soundNotificationToggle(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
// Expand advanced options
await page.evaluate(() => {
const toggle = document.querySelector('[data-toggle="advanced"], .advanced-toggle, details summary');
if (toggle) toggle.click();
});
await delay(300);
const result = await page.evaluate(() => {
const soundToggle = document.querySelector(
'input[name*="sound"], ' +
'input[name*="notification"], ' +
'#sound-notifications, ' +
'.sound-toggle'
);
if (!soundToggle) {
// Look for checkbox with sound-related label
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
for (const cb of checkboxes) {
const label = document.querySelector(`label[for="${cb.id}"]`);
const text = (label?.textContent || cb.parentElement?.textContent || '').toLowerCase();
if (text.includes('sound') || text.includes('notification') || text.includes('audio')) {
return {
hasToggle: true,
type: cb.type,
labelText: text.substring(0, 50)
};
}
}
return { hasToggle: false };
}
return {
hasToggle: true,
type: soundToggle.type,
checked: soundToggle.checked
};
});
if (!result.hasToggle) {
return { passed: null, skipped: true, message: 'No sound notification toggle found' };
}
return {
passed: true,
message: `Sound toggle: type=${result.type}, checked=${result.checked}, label="${result.labelText || 'found'}"`
};
}
};
// ============================================================================
// Form Submission Tests
// ============================================================================
const FormSubmissionTests = {
async formValidationEmptyQuery(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
await delay(500);
// Try to submit empty form
const result = await page.evaluate(() => {
const queryInput = document.querySelector('textarea[name="query"], input[name="query"], #query, .ldr-query-input, #research-query');
const submitBtn = document.querySelector('button[type="submit"], .start-research, .ldr-btn-primary, #start-research');
if (!queryInput) {
return { notFound: true };
}
// Clear query
queryInput.value = '';
queryInput.dispatchEvent(new Event('input', { bubbles: true }));
// Check required attribute or minlength
const isRequired = queryInput?.required || queryInput?.hasAttribute('required');
const hasMinLength = queryInput?.minLength > 0;
// Try to submit
if (submitBtn) submitBtn.click();
// Check validation state
const isValid = queryInput?.validity?.valid;
const hasPlaceholder = !!queryInput?.placeholder;
return {
isRequired,
isValid,
hasMinLength,
hasPlaceholder,
validationMessage: queryInput?.validationMessage
};
});
if (result.notFound) {
return { passed: null, skipped: true, message: 'Query input not found for validation test' };
}
// Consider having a placeholder as implicit validation guidance
const hasValidation = result.isRequired || result.isValid === false ||
result.hasMinLength || result.hasPlaceholder;
// Skip instead of fail if validation not detected in CI
if (!hasValidation) {
return { passed: null, skipped: true, message: 'Empty query validation not detected (may use JavaScript validation)' };
}
return {
passed: true,
message: `Empty query validation: required=${result.isRequired}, valid=${result.isValid}, minLength=${result.hasMinLength}`
};
}
};
// ============================================================================
// Keyboard Interaction Tests
// ============================================================================
const KeyboardTests = {
async shiftEnterNewline(page, baseUrl) {
// Shift+Enter newline behavior is unreliable in headless Chrome
if (process.env.CI) {
return { passed: null, skipped: true, message: 'Skipped in CI: Shift+Enter newline unreliable in headless Chrome' };
}
await navigateTo(page, `${baseUrl}/`);
// Wait for the textarea to be visible and interactive before typing
await page.waitForSelector('textarea[name="query"], #query', { visible: true, timeout: 5000 }).catch(() => {});
const queryInput = await page.$('textarea[name="query"], #query');
if (!queryInput) {
return { passed: null, skipped: true, message: 'Textarea query input not found' };
}
// Type first line
await queryInput.type('First line');
// Shift+Enter for new line
await page.keyboard.down('Shift');
await page.keyboard.press('Enter');
await page.keyboard.up('Shift');
// Type second line
await queryInput.type('Second line');
const result = await page.evaluate(() => {
const input = document.querySelector('textarea[name="query"], #query');
const value = input?.value || '';
return {
value,
hasNewline: value.includes('\n'),
lineCount: value.split('\n').length
};
});
return {
passed: result.hasNewline,
message: result.hasNewline
? `Shift+Enter creates newline (${result.lineCount} lines)`
: 'Shift+Enter did not create newline'
};
}
};
// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
log.section('Research Form UI Tests');
const ctx = await setupTest({ authenticate: true });
const results = new TestResults('Research Form 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 {
// Basic Form Tests
log.section('Basic Form Elements');
await run('Form', 'Elements Present', (p, u) => ResearchFormBasicTests.formElementsPresent(p, u));
await run('Form', 'Query Accepts Text', (p, u) => ResearchFormBasicTests.queryInputAcceptsText(p, u));
await run('Form', 'Query Placeholder', (p, u) => ResearchFormBasicTests.queryInputPlaceholder(p, u));
await run('Form', 'Submit Button State', (p, u) => ResearchFormBasicTests.submitButtonState(p, u));
// Mode Tests
log.section('Research Mode');
await run('Mode', 'Selection Exists', (p, u) => ResearchModeTests.modeSelectionExists(p, u));
await run('Mode', 'Toggle Functionality', (p, u) => ResearchModeTests.modeToggleFunctionality(p, u));
// Advanced Options Tests
log.section('Advanced Options');
await run('Advanced', 'Options Toggle', (p, u) => AdvancedOptionsTests.advancedOptionsToggle(p, u));
await run('Advanced', 'Options Expand', (p, u) => AdvancedOptionsTests.advancedOptionsExpand(p, u));
await run('Advanced', 'Model Provider Dropdown', (p, u) => AdvancedOptionsTests.modelProviderDropdown(p, u));
await run('Advanced', 'Search Engine Dropdown', (p, u) => AdvancedOptionsTests.searchEngineDropdown(p, u));
await run('Advanced', 'Strategy Dropdown', (p, u) => AdvancedOptionsTests.searchStrategyDropdown(p, u));
await run('Advanced', 'Iterations Input', (p, u) => AdvancedOptionsTests.iterationsInput(p, u));
await run('Advanced', 'Sound Toggle', (p, u) => AdvancedOptionsTests.soundNotificationToggle(p, u));
// Form Submission Tests
log.section('Form Submission');
await run('Submit', 'Empty Query Validation', (p, u) => FormSubmissionTests.formValidationEmptyQuery(p, u));
// Keyboard Tests
log.section('Keyboard Interactions');
await run('Keyboard', 'Shift+Enter Newline', (p, u) => KeyboardTests.shiftEnterNewline(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 = { ResearchFormBasicTests, ResearchModeTests, AdvancedOptionsTests, FormSubmissionTests, KeyboardTests };