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
621 lines
25 KiB
JavaScript
621 lines
25 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Settings Interactions UI Tests
|
|
*
|
|
* Tests for settings page interactions including tabs, search,
|
|
* toggles, inputs, and save functionality.
|
|
*
|
|
* Run: node test_settings_interactions_ci.js
|
|
*/
|
|
|
|
const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout } = require('./test_lib');
|
|
|
|
// ============================================================================
|
|
// Settings Page Structure Tests
|
|
// ============================================================================
|
|
const SettingsPageTests = {
|
|
async settingsPageLoads(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const hasContent = document.body.textContent.length > 100;
|
|
const title = document.title.toLowerCase();
|
|
const hasSettingsContent = title.includes('settings') || title.includes('configuration') ||
|
|
!!document.querySelector('.settings, #settings, [class*="settings"]');
|
|
|
|
return {
|
|
hasContent,
|
|
hasSettingsContent,
|
|
title,
|
|
url: window.location.href
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.hasContent,
|
|
message: `Settings page loads (title: "${result.title}")`
|
|
};
|
|
},
|
|
|
|
async settingsSearchFilter(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 settings search found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Settings search: placeholder="${result.placeholder}"`
|
|
};
|
|
},
|
|
|
|
async settingsSearchFiltering(page, baseUrl) {
|
|
// #settings-search re-renders #settings-content with a filtered subset
|
|
// (settings.js::handleSearchInput filters allSettings by key/name/
|
|
// description/category) and restores the full active tab when cleared.
|
|
// The old test typed 'model', never asserted the count actually
|
|
// changed, and always returned passed:true.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
|
|
const count = () => page.$$eval('#settings-content .ldr-settings-item', els => els.length);
|
|
const initial = await count();
|
|
if (initial === 0) {
|
|
return { passed: false, message: 'No settings rendered to filter' };
|
|
}
|
|
|
|
// A term matching nothing collapses the rendered list. The .catch
|
|
// tolerates the wait timing out — the count assertion below reports the
|
|
// actual diff, a clearer failure than a bare waitForFunction timeout.
|
|
await page.type('#settings-search', 'zzznomatchzzz');
|
|
await page.waitForFunction(
|
|
() => document.querySelectorAll('#settings-content .ldr-settings-item').length === 0,
|
|
{ timeout: 5000 }
|
|
).catch(() => {});
|
|
const filtered = await count();
|
|
|
|
// ...and clearing it restores the list. Set value + dispatch a real
|
|
// 'input' event (what handleSearchInput listens for) — more portable
|
|
// than triple-click + Backspace across headless targets.
|
|
await page.$eval('#settings-search', el => {
|
|
el.value = '';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await page.waitForFunction(
|
|
(n) => document.querySelectorAll('#settings-content .ldr-settings-item').length === n,
|
|
{ timeout: 5000 }, initial
|
|
).catch(() => {});
|
|
const restored = await count();
|
|
|
|
const passed = filtered < initial && restored === initial;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Search filters list (${initial} → ${filtered} on no-match → ${restored} restored)`
|
|
: `Filter contract failed (initial=${initial}, filtered=${filtered}, restored=${restored})`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Tabs Tests
|
|
// ============================================================================
|
|
const SettingsTabsTests = {
|
|
async settingsTabsExist(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const tabs = document.querySelectorAll(
|
|
'.tab, ' +
|
|
'.nav-tab, ' +
|
|
'[role="tab"], ' +
|
|
'.settings-tab, ' +
|
|
'.nav-link'
|
|
);
|
|
|
|
const tabTexts = Array.from(tabs).slice(0, 10).map(t => t.textContent?.trim());
|
|
|
|
return {
|
|
tabCount: tabs.length,
|
|
tabs: tabTexts
|
|
};
|
|
});
|
|
|
|
if (result.tabCount === 0) {
|
|
return { passed: null, skipped: true, message: 'No settings tabs found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `${result.tabCount} tabs: ${result.tabs.join(', ')}`
|
|
};
|
|
},
|
|
|
|
async tabNavigationWorks(page, baseUrl) {
|
|
// Tabs are .ldr-settings-tab[data-tab]; clicking one adds .active and
|
|
// re-renders #settings-content in place (settings.js:3930-3976) — there
|
|
// is no page navigation and no .tab-content/.tab-pane element, so the
|
|
// old probe matched nothing and the catch hardcoded passed:true.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
await page.waitForSelector('.ldr-settings-tab[data-tab="llm"]', { timeout: 5000 });
|
|
|
|
// Precondition: the dashboard opens on the 'all' tab. Make it explicit
|
|
// so a changed default fails here with a clear message.
|
|
const start = await page.evaluate(() => ({
|
|
allActive: document.querySelector('.ldr-settings-tab[data-tab="all"]')?.classList.contains('active'),
|
|
itemCount: document.querySelectorAll('#settings-content .ldr-settings-item').length,
|
|
}));
|
|
if (start.allActive !== true) {
|
|
return { passed: false, message: `Expected 'all' tab active on load (allActive=${start.allActive})` };
|
|
}
|
|
|
|
// Switch to 'llm' — a strict subset of 'all', so the rendered item set
|
|
// must shrink (proves the content actually re-rendered, not just the
|
|
// tab class flipping).
|
|
await page.click('.ldr-settings-tab[data-tab="llm"]');
|
|
await page.waitForFunction(
|
|
() => document.querySelector('.ldr-settings-tab[data-tab="llm"]')?.classList.contains('active') === true,
|
|
{ timeout: 5000 }
|
|
);
|
|
|
|
const state = await page.evaluate(() => ({
|
|
llmActive: document.querySelector('.ldr-settings-tab[data-tab="llm"]')?.classList.contains('active'),
|
|
allActive: document.querySelector('.ldr-settings-tab[data-tab="all"]')?.classList.contains('active'),
|
|
itemCount: document.querySelectorAll('#settings-content .ldr-settings-item').length,
|
|
}));
|
|
|
|
const reRendered = state.itemCount < start.itemCount;
|
|
const passed = state.llmActive === true && state.allActive === false && reRendered;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Tab switch works (llm active, all deactivated, items ${start.itemCount}→${state.itemCount})`
|
|
: `Tab switch failed (llmActive=${state.llmActive}, allActive=${state.allActive}, items ${start.itemCount}→${state.itemCount})`
|
|
};
|
|
},
|
|
|
|
async specificTabsPresent(page, baseUrl) {
|
|
// The dashboard renders a fixed set of category tabs as
|
|
// .ldr-settings-tab[data-tab] (settings_dashboard.html). Assert the
|
|
// real data-tab set rather than fuzzy-matching body text.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('.ldr-settings-tab[data-tab]', { timeout: 10000 });
|
|
|
|
const tabs = await page.$$eval('.ldr-settings-tab[data-tab]', els => els.map(e => e.getAttribute('data-tab')));
|
|
const expected = ['llm', 'search', 'report', 'app', 'notifications'];
|
|
const missing = expected.filter(t => !tabs.includes(t));
|
|
|
|
return {
|
|
passed: missing.length === 0,
|
|
message: missing.length === 0
|
|
? `All expected category tabs present: ${tabs.join(', ')}`
|
|
: `Missing category tabs: ${missing.join(', ')} (present: ${tabs.join(', ')})`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Controls Tests
|
|
// ============================================================================
|
|
const SettingsControlsTests = {
|
|
async textInputSettings(page, baseUrl) {
|
|
// Real rendered text settings are input.ldr-settings-input[type=text]
|
|
// inside #settings-content (settings_form.html). The old selector list
|
|
// (.settings input etc.) matched nothing → permanent skip.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
|
|
const result = await page.evaluate(() => {
|
|
const inputs = document.querySelectorAll('#settings-content input.ldr-settings-input[type="text"][name]');
|
|
const first = inputs[0];
|
|
return { count: inputs.length, firstName: first?.name };
|
|
});
|
|
|
|
return {
|
|
passed: result.count > 0,
|
|
message: result.count > 0
|
|
? `Text settings inputs: ${result.count} (first: "${result.firstName}")`
|
|
: 'No .ldr-settings-input[type=text] rendered in #settings-content'
|
|
};
|
|
},
|
|
|
|
async numberInputSettings(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
const result = await page.evaluate(() => {
|
|
const inputs = document.querySelectorAll('#settings-content input[type="number"][name]');
|
|
const first = inputs[0];
|
|
return {
|
|
count: inputs.length,
|
|
firstName: first?.name,
|
|
min: first?.min,
|
|
max: first?.max,
|
|
step: first?.step
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.count > 0,
|
|
message: result.count > 0
|
|
? `Number settings inputs: ${result.count} (first: "${result.firstName}", min=${result.min}, max=${result.max}, step=${result.step})`
|
|
: 'No number inputs rendered in #settings-content'
|
|
};
|
|
},
|
|
|
|
async toggleSwitchSettings(page, baseUrl) {
|
|
// Real setting checkboxes are input.ldr-settings-checkbox
|
|
// (settings_form.html), each paired with a .ldr-checkbox-hidden-fallback.
|
|
// The old broad selectors matched nothing → permanent skip.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
|
|
const result = await page.evaluate(() => {
|
|
const boxes = document.querySelectorAll('#settings-content input.ldr-settings-checkbox[name]');
|
|
const first = boxes[0];
|
|
return { count: boxes.length, firstName: first?.name, checked: first?.checked };
|
|
});
|
|
|
|
return {
|
|
passed: result.count > 0,
|
|
message: result.count > 0
|
|
? `Setting checkboxes: ${result.count} (first: "${result.firstName}", checked=${result.checked})`
|
|
: 'No .ldr-settings-checkbox rendered in #settings-content'
|
|
};
|
|
},
|
|
|
|
async dropdownSelectSettings(page, baseUrl) {
|
|
// Dropdown settings render either as a plain <select.ldr-settings-select>
|
|
// or — for llm.provider / llm.model / search.tool — as a custom
|
|
// .ldr-custom-dropdown widget (components/custom_dropdown.html). The
|
|
// default config only has the custom kind (no plain <select>), so the
|
|
// contract is "at least one dropdown control exists".
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
|
|
const result = await page.evaluate(() => {
|
|
const selects = document.querySelectorAll('#settings-content select.ldr-settings-select[name]');
|
|
const customs = document.querySelectorAll('#settings-content .ldr-custom-dropdown');
|
|
const first = selects[0];
|
|
return {
|
|
selectCount: selects.length,
|
|
customCount: customs.length,
|
|
firstName: first?.name,
|
|
optionCount: first ? first.options.length : 0,
|
|
};
|
|
});
|
|
|
|
const total = result.selectCount + result.customCount;
|
|
// A plain <select>, if present, must have options; custom dropdowns
|
|
// populate their list lazily, so only their presence is asserted.
|
|
const selectsOk = result.selectCount === 0 || result.optionCount > 0;
|
|
return {
|
|
passed: total > 0 && selectsOk,
|
|
message: total > 0
|
|
? `Dropdown settings: ${result.selectCount} <select>${result.firstName ? ` ("${result.firstName}", ${result.optionCount} opts)` : ''} + ${result.customCount} custom-dropdown`
|
|
: 'No dropdown controls (.ldr-settings-select / .ldr-custom-dropdown) rendered in #settings-content'
|
|
};
|
|
},
|
|
|
|
async toggleSwitchToggleable(page, baseUrl) {
|
|
// Click a real, enabled setting checkbox and assert its checked state
|
|
// flips. Uses page.click (a real user gesture) on the confirmed
|
|
// .ldr-settings-checkbox selector.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
const sel = '#settings-content input.ldr-settings-checkbox[name]:not([disabled])';
|
|
await page.waitForSelector(sel, { timeout: 15000 });
|
|
|
|
const before = await page.$eval(sel, el => el.checked);
|
|
await page.click(sel);
|
|
const after = await page.$eval(sel, el => el.checked);
|
|
|
|
const toggled = before !== after;
|
|
return {
|
|
passed: toggled,
|
|
message: toggled
|
|
? `Checkbox toggles (before=${before}, after=${after})`
|
|
: `Checkbox did not flip (before=${before}, after=${after})`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Save Tests
|
|
// ============================================================================
|
|
const SettingsSaveTests = {
|
|
async saveButtonExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, input[type="submit"], .btn'));
|
|
const saveBtn = buttons.find(btn => {
|
|
const text = btn.textContent?.toLowerCase() || btn.value?.toLowerCase() || '';
|
|
return text.includes('save') || text.includes('apply') || text.includes('update');
|
|
});
|
|
|
|
return {
|
|
hasSaveBtn: !!saveBtn,
|
|
buttonText: saveBtn?.textContent?.trim() || saveBtn?.value
|
|
};
|
|
});
|
|
|
|
if (!result.hasSaveBtn) {
|
|
// Check for auto-save indicator
|
|
const autoSave = await page.evaluate(() => {
|
|
const text = document.body.textContent.toLowerCase();
|
|
return text.includes('auto-save') || text.includes('automatically saved');
|
|
});
|
|
|
|
if (autoSave) {
|
|
return { passed: true, message: 'Settings use auto-save (no manual save button needed)' };
|
|
}
|
|
|
|
return { passed: null, skipped: true, message: 'No save button found (may use auto-save)' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Save button: "${result.buttonText}"`
|
|
};
|
|
},
|
|
|
|
async resetToDefaultsButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, .btn'));
|
|
const resetBtn = buttons.find(btn => {
|
|
const text = btn.textContent?.toLowerCase() || '';
|
|
return text.includes('reset') || text.includes('default') || text.includes('restore');
|
|
});
|
|
|
|
return {
|
|
hasResetBtn: !!resetBtn,
|
|
buttonText: resetBtn?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
if (!result.hasResetBtn) {
|
|
return { passed: null, skipped: true, message: 'No reset to defaults button found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Reset button: "${result.buttonText}"`
|
|
};
|
|
},
|
|
|
|
async autoSaveFunctionality(page, baseUrl) {
|
|
// Settings auto-save on change: handleInputChange schedules a save and,
|
|
// on success, showSaveSuccess() adds .ldr-save-success to the changed
|
|
// input (settings.js:2651). The old selector '.settings input' matched
|
|
// nothing (permanent skip) and it looked for a .toast that isn't used.
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
const sel = '#settings-content input.ldr-settings-checkbox[name]:not([disabled])';
|
|
await page.waitForSelector(sel, { timeout: 15000 });
|
|
|
|
await page.click(sel);
|
|
|
|
// Wait for the save round-trip to flag success on the input.
|
|
let sawSuccess = false;
|
|
try {
|
|
await page.waitForFunction(
|
|
() => !!document.querySelector('.ldr-save-success'),
|
|
{ timeout: 8000 }
|
|
);
|
|
sawSuccess = true;
|
|
} catch { /* timeout — no save feedback appeared */ }
|
|
|
|
return {
|
|
passed: sawSuccess,
|
|
message: sawSuccess
|
|
? 'Auto-save fired (.ldr-save-success applied after changing a setting)'
|
|
: 'No .ldr-save-success feedback within 8s of changing a setting'
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Settings Help Tests
|
|
// ============================================================================
|
|
const SettingsHelpTests = {
|
|
async settingDescriptions(page, baseUrl) {
|
|
// Real per-setting help text is .ldr-input-help (settings_form.html).
|
|
// The old probe [class*=description] only matched static page-header
|
|
// divs (tautological).
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
await page.waitForSelector('#settings-content .ldr-settings-item', { timeout: 15000 });
|
|
|
|
const result = await page.evaluate(() => {
|
|
const helps = Array.from(document.querySelectorAll('#settings-content .ldr-input-help'))
|
|
.filter(el => el.textContent.trim().length > 0);
|
|
return { count: helps.length, firstText: helps[0]?.textContent.trim().slice(0, 80) };
|
|
});
|
|
|
|
return {
|
|
passed: result.count > 0,
|
|
message: result.count > 0
|
|
? `Setting help text: ${result.count} non-empty .ldr-input-help (first: "${result.firstText}")`
|
|
: 'No non-empty .ldr-input-help rendered in #settings-content'
|
|
};
|
|
},
|
|
|
|
async tooltipsPresent(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const tooltipTriggers = document.querySelectorAll(
|
|
'[data-tooltip], ' +
|
|
'[title], ' +
|
|
'.tooltip-trigger, ' +
|
|
'[data-toggle="tooltip"], ' +
|
|
'.info-icon, ' +
|
|
'.help-icon'
|
|
);
|
|
|
|
return {
|
|
count: tooltipTriggers.length,
|
|
hasTooltips: tooltipTriggers.length > 0
|
|
};
|
|
});
|
|
|
|
if (!result.hasTooltips) {
|
|
return { passed: null, skipped: true, message: 'No tooltips found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Tooltips: ${result.count} found`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Raw Config Editor Tests
|
|
// ============================================================================
|
|
const RawConfigTests = {
|
|
async rawConfigEditorExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/settings`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const jsonEditor = document.querySelector(
|
|
'.json-editor, ' +
|
|
'#raw-config, ' +
|
|
'textarea.config-editor, ' +
|
|
'.code-editor, ' +
|
|
'[class*="raw-config"]'
|
|
);
|
|
|
|
const toggleBtn = document.querySelector(
|
|
'[data-toggle="raw-config"], ' +
|
|
'.show-raw-config, ' +
|
|
'#toggle-raw-config'
|
|
);
|
|
|
|
return {
|
|
hasEditor: !!jsonEditor,
|
|
hasToggle: !!toggleBtn,
|
|
toggleText: toggleBtn?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
if (!result.hasEditor && !result.hasToggle) {
|
|
return { passed: null, skipped: true, message: 'No raw config editor found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Raw config: editor=${result.hasEditor}, toggle="${result.toggleText}"`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
async function main() {
|
|
log.section('Settings Interactions Tests');
|
|
|
|
const ctx = await setupTest({ authenticate: true });
|
|
const results = new TestResults('Settings Interactions 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 {
|
|
// Settings Page Structure
|
|
log.section('Settings Page Structure');
|
|
|
|
await run('Page', 'Loads', (p, u) => SettingsPageTests.settingsPageLoads(p, u));
|
|
await run('Page', 'Search Filter', (p, u) => SettingsPageTests.settingsSearchFilter(p, u));
|
|
await run('Page', 'Search Filtering', (p, u) => SettingsPageTests.settingsSearchFiltering(p, u));
|
|
|
|
// Settings Tabs
|
|
log.section('Settings Tabs');
|
|
|
|
await run('Tabs', 'Exist', (p, u) => SettingsTabsTests.settingsTabsExist(p, u));
|
|
await run('Tabs', 'Navigation', (p, u) => SettingsTabsTests.tabNavigationWorks(p, u));
|
|
await run('Tabs', 'Categories', (p, u) => SettingsTabsTests.specificTabsPresent(p, u));
|
|
|
|
// Settings Controls
|
|
log.section('Settings Controls');
|
|
|
|
await run('Controls', 'Text Inputs', (p, u) => SettingsControlsTests.textInputSettings(p, u));
|
|
await run('Controls', 'Number Inputs', (p, u) => SettingsControlsTests.numberInputSettings(p, u));
|
|
await run('Controls', 'Toggles', (p, u) => SettingsControlsTests.toggleSwitchSettings(p, u));
|
|
await run('Controls', 'Dropdowns', (p, u) => SettingsControlsTests.dropdownSelectSettings(p, u));
|
|
await run('Controls', 'Toggle Works', (p, u) => SettingsControlsTests.toggleSwitchToggleable(p, u));
|
|
|
|
// Settings Save
|
|
log.section('Settings Save');
|
|
|
|
await run('Save', 'Button Exists', (p, u) => SettingsSaveTests.saveButtonExists(p, u));
|
|
await run('Save', 'Reset Button', (p, u) => SettingsSaveTests.resetToDefaultsButton(p, u));
|
|
await run('Save', 'Auto-Save', (p, u) => SettingsSaveTests.autoSaveFunctionality(p, u));
|
|
|
|
// Settings Help
|
|
log.section('Settings Help');
|
|
|
|
await run('Help', 'Descriptions', (p, u) => SettingsHelpTests.settingDescriptions(p, u));
|
|
await run('Help', 'Tooltips', (p, u) => SettingsHelpTests.tooltipsPresent(p, u));
|
|
|
|
// Raw Config
|
|
log.section('Raw Config');
|
|
|
|
await run('RawConfig', 'Editor Exists', (p, u) => RawConfigTests.rawConfigEditorExists(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, SettingsTabsTests, SettingsControlsTests, SettingsSaveTests, SettingsHelpTests, RawConfigTests };
|