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
537 lines
22 KiB
JavaScript
537 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Settings Pages UI Tests
|
|
*
|
|
* Tests for the settings dashboard, tabs, and configuration options.
|
|
*
|
|
* Run: node test_settings_pages_ci.js
|
|
*/
|
|
const { setupTest, teardownTest, TestResults, log, delay, waitForVisible, navigateTo, withTimeout } = require('./test_lib');
|
|
|
|
// Open /settings/ on a clean "All Settings" tab. navigateTo() no-ops when the
|
|
// page is already on /settings/, so a prior test's active tab leaks in (e.g.
|
|
// the 'llm' tab, which hides search settings). Force the 'all' tab so every
|
|
// setting renders before the Input assertions read the DOM.
|
|
async function openSettingsAllTab(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
// Require the 'all' tab to exist so a missing tab fails here with context,
|
|
// rather than silently proceeding to an opaque downstream timeout.
|
|
await page.waitForSelector('.ldr-settings-tab[data-tab="all"]', { timeout: 5000 });
|
|
const isActive = await page.$eval('.ldr-settings-tab[data-tab="all"]', el => el.classList.contains('active'));
|
|
if (!isActive) {
|
|
await page.click('.ldr-settings-tab[data-tab="all"]');
|
|
await page.waitForFunction(
|
|
() => document.querySelector('.ldr-settings-tab[data-tab="all"]')?.classList.contains('active') === true,
|
|
{ timeout: 5000 }
|
|
).catch(() => {});
|
|
}
|
|
}
|
|
|
|
// llm.provider and search.tool both render as custom dropdowns (custom_dropdown.html):
|
|
// a hidden input[name=<key>] carries the form value inside a .ldr-custom-dropdown,
|
|
// and the dropdown JS resolves the selection asynchronously after load. Assert a
|
|
// real value ends up selected. The .catch on the value-wait lets the assertion
|
|
// below report a specific `value=""` message rather than a raw timeout.
|
|
async function assertCustomDropdownSelected(page, name, label) {
|
|
await page.waitForSelector(`#settings-content [name="${name}"]`, { timeout: 15000 });
|
|
await page.waitForFunction(
|
|
(n) => (document.querySelector(`[name="${n}"]`)?.value || '').length > 0,
|
|
{ timeout: 10000 }, name
|
|
).catch(() => {});
|
|
const result = await page.evaluate((n) => {
|
|
const hidden = document.querySelector(`[name="${n}"]`);
|
|
return { value: hidden?.value || '', hasDropdown: !!hidden?.closest('.ldr-custom-dropdown') };
|
|
}, name);
|
|
const passed = result.value.length > 0 && result.hasDropdown;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `${label} renders a real selection ("${result.value}") in a custom dropdown`
|
|
: `${label} incomplete (value="${result.value}", customDropdown=${result.hasDropdown})`
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Settings Page Structure Tests
|
|
// ============================================================================
|
|
const SettingsPageTests = {
|
|
async settingsPageLoads(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
return {
|
|
hasSettingsContent: !!document.querySelector('.settings-container, .ldr-settings, #settings, .settings-dashboard'),
|
|
hasForm: !!document.querySelector('form'),
|
|
hasTabs: !!document.querySelector('.nav-tabs, .tab-list, [role="tablist"], .settings-tabs'),
|
|
pageTitle: document.title,
|
|
hasAnyInputs: document.querySelectorAll('input, select, textarea').length > 0
|
|
};
|
|
});
|
|
|
|
const passed = result.hasSettingsContent || result.hasAnyInputs;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Settings page loaded (content=${result.hasSettingsContent}, form=${result.hasForm}, tabs=${result.hasTabs})`
|
|
: 'Settings page failed to load expected content'
|
|
};
|
|
},
|
|
|
|
async settingsTabsExist(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const tabs = document.querySelectorAll('.nav-tabs .nav-link, .tab-list button, [role="tab"], .settings-tab');
|
|
const tabTexts = Array.from(tabs).map(t => t.textContent?.trim());
|
|
|
|
// Also check for category sections if tabs aren't used
|
|
const categories = document.querySelectorAll('.settings-category, .setting-section, h2, h3');
|
|
const categoryTexts = Array.from(categories).map(c => c.textContent?.trim()).filter(t => t && t.length < 50);
|
|
|
|
return {
|
|
tabCount: tabs.length,
|
|
tabNames: tabTexts.slice(0, 10),
|
|
categoryCount: categories.length,
|
|
categoryNames: categoryTexts.slice(0, 10)
|
|
};
|
|
});
|
|
|
|
const hasTabs = result.tabCount > 0;
|
|
const hasCategories = result.categoryCount > 0;
|
|
|
|
return {
|
|
passed: hasTabs || hasCategories,
|
|
message: hasTabs
|
|
? `Found ${result.tabCount} tabs: ${result.tabNames.join(', ')}`
|
|
: hasCategories
|
|
? `Found ${result.categoryCount} setting categories`
|
|
: 'No tabs or categories found'
|
|
};
|
|
},
|
|
|
|
async settingsTabNavigation(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const tabs = await page.$$('.nav-tabs .nav-link, [role="tab"]');
|
|
if (tabs.length < 2) {
|
|
return { passed: null, skipped: true, message: 'Not enough tabs to test navigation' };
|
|
}
|
|
|
|
try {
|
|
// Click second tab using evaluate to avoid stale element issues
|
|
await page.evaluate(() => {
|
|
const tabElements = document.querySelectorAll('.nav-tabs .nav-link, [role="tab"]');
|
|
if (tabElements[1]) {
|
|
tabElements[1].click();
|
|
}
|
|
});
|
|
|
|
// Wait for page to stabilize
|
|
await delay(1000);
|
|
|
|
// Wait for network to settle in case of navigation
|
|
await page.waitForFunction(() => document.readyState === 'complete', { timeout: 5000 }).catch(() => {});
|
|
|
|
const result = await page.evaluate(() => {
|
|
const activeTab = document.querySelector('.nav-link.active, [role="tab"][aria-selected="true"]');
|
|
const visiblePanel = document.querySelector('.tab-pane.active, .tab-pane.show, [role="tabpanel"]:not([hidden])');
|
|
|
|
return {
|
|
hasActiveTab: !!activeTab,
|
|
activeTabText: activeTab?.textContent?.trim(),
|
|
hasVisiblePanel: !!visiblePanel
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.hasActiveTab,
|
|
message: result.hasActiveTab
|
|
? `Tab navigation works (active: "${result.activeTabText}")`
|
|
: 'Tab navigation not working properly'
|
|
};
|
|
} catch (err) {
|
|
// Handle navigation or context destruction gracefully
|
|
if (err.message && (err.message.includes('context') || err.message.includes('navigation') || err.message.includes('Target closed'))) {
|
|
return { passed: null, skipped: true, message: 'Tab click caused page navigation - skipping' };
|
|
}
|
|
return { passed: null, skipped: true, message: `Tab click failed: ${err.message}` };
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Input Tests
|
|
// ============================================================================
|
|
const SettingsInputTests = {
|
|
async modelProviderSetting(page, baseUrl) {
|
|
// llm.provider renders as a custom dropdown; assert a *real* provider
|
|
// ends up selected (the old test returned passed:true for any input).
|
|
await openSettingsAllTab(page, baseUrl);
|
|
return assertCustomDropdownSelected(page, 'llm.provider', 'Provider setting');
|
|
},
|
|
|
|
async searchEngineSetting(page, baseUrl) {
|
|
// search.tool also renders as a custom dropdown. The old test's
|
|
// select[name*=search] matched an unrelated country <select> (e.g.
|
|
// SearXNG region); target the real search.tool field instead.
|
|
await openSettingsAllTab(page, baseUrl);
|
|
return assertCustomDropdownSelected(page, 'search.tool', 'Search engine setting');
|
|
},
|
|
|
|
async temperatureSetting(page, baseUrl) {
|
|
// llm.temperature renders as a range slider (settings_form.html). The
|
|
// old test passed as long as any temperature input existed; assert the
|
|
// real control contract: type=range with a numeric value within [min,max].
|
|
await openSettingsAllTab(page, baseUrl);
|
|
await page.waitForSelector('#settings-content [name="llm.temperature"]', { timeout: 15000 });
|
|
|
|
const result = await page.evaluate(() => {
|
|
const el = document.querySelector('[name="llm.temperature"]');
|
|
if (!el) return { exists: false };
|
|
return { exists: true, type: el.type, value: el.value, min: el.min, max: el.max, step: el.step };
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: false, message: 'llm.temperature input not rendered' };
|
|
}
|
|
const val = parseFloat(result.value), min = parseFloat(result.min),
|
|
max = parseFloat(result.max), step = parseFloat(result.step);
|
|
const passed = result.type === 'range'
|
|
&& Number.isFinite(val) && Number.isFinite(min) && Number.isFinite(max)
|
|
&& val >= min && val <= max
|
|
&& Number.isFinite(step) && step > 0; // a real slider has a positive step
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Temperature is a range slider (value=${result.value} in [${result.min}, ${result.max}], step=${result.step})`
|
|
: `Temperature contract failed (type=${result.type}, value=${result.value}, min=${result.min}, max=${result.max}, step=${result.step})`
|
|
};
|
|
},
|
|
|
|
async apiKeyFieldMasked(page, baseUrl) {
|
|
// API-key settings render as password inputs (settings_form.html,
|
|
// ui_element == "password"). Assert *every* rendered api_key field is
|
|
// masked, not just that one exists (the old test checked only the first).
|
|
await openSettingsAllTab(page, baseUrl);
|
|
await page.waitForSelector('#settings-content input[name$=".api_key"]', { timeout: 15000 });
|
|
|
|
const result = await page.evaluate(() => {
|
|
const fields = Array.from(document.querySelectorAll('#settings-content input[name$=".api_key"]'));
|
|
const unmasked = fields.filter(f => f.type !== 'password').map(f => f.name);
|
|
return { count: fields.length, unmasked };
|
|
});
|
|
|
|
const passed = result.count > 0 && result.unmasked.length === 0;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `All ${result.count} api_key fields are masked (type=password)`
|
|
: `${result.unmasked.length}/${result.count} api_key fields NOT masked: ${result.unmasked.slice(0, 3).join(', ')}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Action Tests
|
|
// ============================================================================
|
|
const SettingsActionTests = {
|
|
async saveButtonExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
// Note: :contains() is not valid CSS, removed from selector
|
|
const saveButtonsByCss = document.querySelectorAll(
|
|
'button[type="submit"], ' +
|
|
'.btn-save, ' +
|
|
'[onclick*="save"], ' +
|
|
'button.btn-primary'
|
|
);
|
|
|
|
// More thorough search - find buttons by text content
|
|
const allButtons = Array.from(document.querySelectorAll('button, input[type="submit"]'));
|
|
const saveBtn = allButtons.find(b =>
|
|
b.textContent?.toLowerCase().includes('save') ||
|
|
b.value?.toLowerCase().includes('save')
|
|
);
|
|
|
|
return {
|
|
hasSaveButton: !!saveBtn || saveButtonsByCss.length > 0,
|
|
buttonText: saveBtn?.textContent?.trim() || saveBtn?.value,
|
|
buttonCount: allButtons.filter(b =>
|
|
b.textContent?.toLowerCase().includes('save') ||
|
|
b.value?.toLowerCase().includes('save')
|
|
).length
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.hasSaveButton,
|
|
message: result.hasSaveButton
|
|
? `Save button found ("${result.buttonText}")`
|
|
: 'No save button found'
|
|
};
|
|
},
|
|
|
|
async resetButtonExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const allButtons = Array.from(document.querySelectorAll('button, input[type="button"], input[type="reset"]'));
|
|
const resetBtn = allButtons.find(b =>
|
|
b.textContent?.toLowerCase().includes('reset') ||
|
|
b.textContent?.toLowerCase().includes('default') ||
|
|
b.value?.toLowerCase().includes('reset')
|
|
);
|
|
|
|
return {
|
|
hasResetButton: !!resetBtn,
|
|
buttonText: resetBtn?.textContent?.trim() || resetBtn?.value
|
|
};
|
|
});
|
|
|
|
if (!result.hasResetButton) {
|
|
return { passed: null, skipped: true, message: 'No reset/defaults button found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Reset button found ("${result.buttonText}")`
|
|
};
|
|
},
|
|
|
|
async searchFilterExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const searchInput = document.querySelector(
|
|
'input[type="search"], ' +
|
|
'input[placeholder*="search"], ' +
|
|
'input[placeholder*="filter"], ' +
|
|
'#settings-search, ' +
|
|
'.settings-filter'
|
|
);
|
|
|
|
return {
|
|
hasSearch: !!searchInput,
|
|
placeholder: searchInput?.placeholder
|
|
};
|
|
});
|
|
|
|
if (!result.hasSearch) {
|
|
return { passed: null, skipped: true, message: 'No search/filter input on settings page' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Settings search/filter found (placeholder: "${result.placeholder}")`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Status Tests
|
|
// ============================================================================
|
|
const SettingsStatusTests = {
|
|
async warningsDisplay(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const warnings = document.querySelectorAll(
|
|
'.alert-warning, ' +
|
|
'.warning, ' +
|
|
'.config-warning, ' +
|
|
'[class*="warning"]'
|
|
);
|
|
|
|
const warningTexts = Array.from(warnings)
|
|
.map(w => w.textContent?.trim())
|
|
.filter(t => t && t.length < 200);
|
|
|
|
return {
|
|
warningCount: warnings.length,
|
|
warningTexts: warningTexts.slice(0, 3)
|
|
};
|
|
});
|
|
|
|
// This is informational - either warnings or no warnings is OK
|
|
return {
|
|
passed: true,
|
|
message: result.warningCount > 0
|
|
? `${result.warningCount} configuration warning(s) displayed`
|
|
: 'No configuration warnings (good configuration)'
|
|
};
|
|
},
|
|
|
|
async ollamaStatusIndicator(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
// Wait for settings form to render before checking for ollama elements
|
|
try {
|
|
await page.waitForSelector('[name="llm.provider"], input[name*="api_key"]', { timeout: 15000 });
|
|
} catch {
|
|
// Form didn't render — skip
|
|
}
|
|
|
|
const result = await page.evaluate(() => {
|
|
const ollamaStatus = document.querySelector(
|
|
'.ollama-status, ' +
|
|
'[data-ollama-status], ' +
|
|
'#ollama-status, ' +
|
|
'.status-indicator[class*="ollama"]'
|
|
);
|
|
|
|
const ollamaSection = document.querySelector('[class*="ollama"], [id*="ollama"]');
|
|
|
|
return {
|
|
hasStatusIndicator: !!ollamaStatus,
|
|
hasOllamaSection: !!ollamaSection,
|
|
statusText: ollamaStatus?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
if (!result.hasStatusIndicator && !result.hasOllamaSection) {
|
|
return { passed: null, skipped: true, message: 'No Ollama status indicator found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: result.hasStatusIndicator
|
|
? `Ollama status: "${result.statusText}"`
|
|
: 'Ollama section present on page'
|
|
};
|
|
},
|
|
|
|
async availableModelsApiWorks(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/settings/api/available-models`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
return {
|
|
ok: true,
|
|
hasProviders: Object.keys(data).length > 0,
|
|
providers: Object.keys(data).slice(0, 5)
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Available models API works (providers: ${result.providers?.join(', ') || 'none'})`
|
|
: `Available models API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
},
|
|
|
|
async availableSearchEnginesApiWorks(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings/`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/settings/api/available-search-engines`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
const engines = Array.isArray(data) ? data : Object.keys(data);
|
|
return {
|
|
ok: true,
|
|
engineCount: engines.length,
|
|
engines: engines.slice(0, 5)
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Search engines API works (${result.engineCount} engines: ${result.engines?.join(', ')})`
|
|
: `Search engines API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
async function main() {
|
|
log.section('Settings Pages Tests');
|
|
|
|
const ctx = await setupTest({ authenticate: true });
|
|
const results = new TestResults('Settings Pages 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 {
|
|
// Page Structure Tests
|
|
log.section('Page Structure');
|
|
await run('Structure', 'Settings Page Loads', (p, u) => SettingsPageTests.settingsPageLoads(p, u));
|
|
await run('Structure', 'Settings Tabs Exist', (p, u) => SettingsPageTests.settingsTabsExist(p, u));
|
|
await run('Structure', 'Tab Navigation', (p, u) => SettingsPageTests.settingsTabNavigation(p, u));
|
|
|
|
// Input Tests
|
|
log.section('Settings Inputs');
|
|
await run('Inputs', 'Model Provider Setting', (p, u) => SettingsInputTests.modelProviderSetting(p, u));
|
|
await run('Inputs', 'Search Engine Setting', (p, u) => SettingsInputTests.searchEngineSetting(p, u));
|
|
await run('Inputs', 'Temperature Setting', (p, u) => SettingsInputTests.temperatureSetting(p, u));
|
|
await run('Inputs', 'API Key Field Masked', (p, u) => SettingsInputTests.apiKeyFieldMasked(p, u));
|
|
|
|
// Action Tests
|
|
log.section('Settings Actions');
|
|
await run('Actions', 'Save Button Exists', (p, u) => SettingsActionTests.saveButtonExists(p, u));
|
|
await run('Actions', 'Reset Button Exists', (p, u) => SettingsActionTests.resetButtonExists(p, u));
|
|
await run('Actions', 'Search Filter Exists', (p, u) => SettingsActionTests.searchFilterExists(p, u));
|
|
|
|
// Status Tests
|
|
log.section('Settings Status & APIs');
|
|
await run('Status', 'Warnings Display', (p, u) => SettingsStatusTests.warningsDisplay(p, u));
|
|
await run('Status', 'Ollama Status Indicator', (p, u) => SettingsStatusTests.ollamaStatusIndicator(p, u));
|
|
await run('API', 'Available Models API', (p, u) => SettingsStatusTests.availableModelsApiWorks(p, u));
|
|
await run('API', 'Available Search Engines API', (p, u) => SettingsStatusTests.availableSearchEnginesApiWorks(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 = { SettingsPageTests, SettingsInputTests, SettingsActionTests, SettingsStatusTests };
|