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

707 lines
29 KiB
JavaScript

#!/usr/bin/env node
/**
* UI Functionality Tests for CI
*
* Comprehensive automated tests for UI functionality including:
* - Form validation and submission
* - Dropdown interactions
* - Modal/dialog behavior
* - Navigation functionality
* - Scroll behavior
* - Button states
*
* Run: node test_ui_functionality_ci.js
*
* Environment variables:
* - BASE_URL: Server URL (default: http://127.0.0.1:5000)
* - CI: Set to 'true' for CI mode
* - HEADLESS: 'true' or 'false' (default: true)
*/
const { setupTest, teardownTest, TestResults, log, delay, waitForVisible, config, navigateTo, withTimeout } = require('../test_lib');
// ============================================================================
// Form Tests
// ============================================================================
const FormTests = {
async researchFormStructure(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const form = document.querySelector('form');
const queryInput = document.querySelector('#query, textarea[name="query"], input[name="query"]');
const submitBtn = document.querySelector('button[type="submit"], input[type="submit"]');
return {
formExists: !!form,
queryInputExists: !!queryInput,
submitBtnExists: !!submitBtn,
queryInputType: queryInput?.tagName?.toLowerCase(),
submitBtnText: submitBtn?.textContent?.trim()
};
});
const passed = result.formExists && result.queryInputExists && result.submitBtnExists;
return {
passed,
message: passed
? `Research form complete (query: ${result.queryInputType}, submit: "${result.submitBtnText}")`
: `Missing elements: form=${result.formExists}, query=${result.queryInputExists}, submit=${result.submitBtnExists}`
};
},
async inputFocusStates(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const input = document.querySelector('#query, textarea[name="query"], input[name="query"]');
if (!input) return { exists: false };
const initialOutline = window.getComputedStyle(input).outline;
const initialBoxShadow = window.getComputedStyle(input).boxShadow;
const initialBorderColor = window.getComputedStyle(input).borderColor;
const initialBackgroundColor = window.getComputedStyle(input).backgroundColor;
input.focus();
// Give browser time to apply focus styles
const focusedOutline = window.getComputedStyle(input).outline;
const focusedBoxShadow = window.getComputedStyle(input).boxShadow;
const focusedBorderColor = window.getComputedStyle(input).borderColor;
const focusedBackgroundColor = window.getComputedStyle(input).backgroundColor;
const hasVisualChange =
initialOutline !== focusedOutline ||
initialBoxShadow !== focusedBoxShadow ||
initialBorderColor !== focusedBorderColor ||
initialBackgroundColor !== focusedBackgroundColor;
// Also check if input is focusable (a valid accessibility check)
const isFocusable = document.activeElement === input;
return { exists: true, hasVisualChange, isFocusable };
});
if (!result.exists) {
return { passed: false, message: 'Query input not found' };
}
// Pass if input is focusable, even if no visual change is detected
// (visual changes may depend on CSS frameworks/themes)
const passed = result.isFocusable || result.hasVisualChange;
return {
passed,
message: passed
? result.hasVisualChange
? 'Input shows visible focus state'
: 'Input is focusable (visual focus style may vary by theme)'
: 'Input not focusable (accessibility issue)'
};
},
async settingsFormButtons(page, baseUrl) {
await navigateTo(page, `${baseUrl}/settings/`);
const result = await page.evaluate(() => {
const saveBtns = Array.from(document.querySelectorAll('button, input[type="submit"]')).filter(btn =>
btn.textContent?.toLowerCase().includes('save') ||
btn.type === 'submit' ||
btn.value?.toLowerCase().includes('save')
);
const resetBtns = Array.from(document.querySelectorAll('button, input[type="reset"]')).filter(btn =>
btn.textContent?.toLowerCase().includes('reset') ||
btn.type === 'reset' ||
btn.value?.toLowerCase().includes('reset')
);
return {
saveBtnExists: saveBtns.length > 0,
resetBtnExists: resetBtns.length > 0,
saveBtnText: saveBtns[0]?.textContent?.trim() || saveBtns[0]?.value,
resetBtnText: resetBtns[0]?.textContent?.trim() || resetBtns[0]?.value
};
});
return {
passed: result.saveBtnExists,
message: result.saveBtnExists
? `Settings form has save button ("${result.saveBtnText}")${result.resetBtnExists ? ` and reset ("${result.resetBtnText}")` : ''}`
: 'Settings form missing save button'
};
}
};
// ============================================================================
// Dropdown Tests
// ============================================================================
const DropdownTests = {
async dropdownStructure(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const dropdown = document.querySelector('.ldr-custom-dropdown');
if (!dropdown) return { exists: false };
const input = dropdown.querySelector('input');
const list = dropdown.querySelector('.ldr-custom-dropdown-list, [class*="dropdown-list"]');
const hiddenInput = dropdown.querySelector('input[type="hidden"]');
return {
exists: true,
hasInput: !!input,
hasList: !!list,
hasHiddenInput: !!hiddenInput,
listItemCount: list?.querySelectorAll('li, .dropdown-item').length || 0
};
});
// If dropdown doesn't exist, this is a real skip - not a pass
if (!result.exists) {
return { passed: null, skipped: true, message: 'No custom dropdown on this page' };
}
const hasRequiredParts = result.hasInput && result.hasList;
return {
passed: hasRequiredParts,
message: hasRequiredParts
? `Dropdown structure valid (${result.listItemCount} items, hidden input: ${result.hasHiddenInput})`
: `Dropdown missing parts: input=${result.hasInput}, list=${result.hasList}`
};
},
async dropdownOpensOnClick(page, baseUrl) {
// Clicking a custom-dropdown input runs custom_dropdown.js::showDropdown,
// which sets the list to display:block, adds .ldr-dropdown-active, AND
// reparents the list to document.body. We target the search-engine
// dropdown specifically (#search_engine): its options come from
// /settings/api/available-search-engines (no LLM), unlike the model
// dropdown which is empty without a configured provider. Because the list
// is moved to <body>, read it by id (#search-engine-dropdown-list), not as
// a descendant of .ldr-custom-dropdown. The old test had OR/skip fallbacks
// that passed even when the dropdown never opened.
await navigateTo(page, `${baseUrl}/`);
await page.waitForSelector('#search_engine', { timeout: 15000 });
// research.js sets up #search_engine TWICE: synchronously on load and again
// inside Promise.all([...loadSearchEngineOptions]).then() once the fetch
// resolves (research.js:329-344). Every setupCustomDropdown() ends with
// hideDropdown() (custom_dropdown.js:629), so a click before that async
// re-setup gets force-closed. navigateTo only waits for domcontentloaded,
// so wait for the network to settle (the home page has no SSE/polling) to
// guarantee the final setup has run before we open the dropdown.
await page.waitForNetworkIdle({ idleTime: 500, timeout: 15000 }).catch(() => {});
await page.click('#search_engine');
await page.waitForFunction(() => {
const list = document.querySelector('#search-engine-dropdown-list');
return list && window.getComputedStyle(list).display === 'block'
&& list.classList.contains('ldr-dropdown-active');
}, { timeout: 5000 }).catch(() => {});
const result = await page.evaluate(() => {
const list = document.querySelector('#search-engine-dropdown-list');
if (!list) return { hasList: false };
return {
hasList: true,
display: window.getComputedStyle(list).display,
active: list.classList.contains('ldr-dropdown-active'),
itemCount: list.querySelectorAll('.ldr-custom-dropdown-item, [role="option"], li').length,
};
});
if (!result.hasList) {
return { passed: false, message: '#search-engine-dropdown-list not found on /' };
}
const passed = result.display === 'block' && result.active;
return {
passed,
message: passed
? `Search-engine dropdown opens on click (display=${result.display}, active=${result.active}, items=${result.itemCount})`
: `Dropdown did not open on click (display=${result.display}, active=${result.active})`
};
},
async dropdownSelection(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const dropdown = document.querySelector('.ldr-custom-dropdown');
if (!dropdown) return { exists: false };
const hiddenInput = dropdown.querySelector('input[type="hidden"]');
const items = dropdown.querySelectorAll('.ldr-custom-dropdown-list li, [class*="dropdown-list"] li, .dropdown-item');
return {
exists: true,
hasHiddenInput: !!hiddenInput,
hiddenInputName: hiddenInput?.name,
hiddenInputValue: hiddenInput?.value,
itemCount: items.length
};
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No dropdown on this page' };
}
return {
passed: result.hasHiddenInput,
message: result.hasHiddenInput
? `Dropdown has hidden input (name: ${result.hiddenInputName}, value: ${result.hiddenInputValue})`
: 'Dropdown missing hidden input for form submission'
};
}
};
// ============================================================================
// Modal Tests
// ============================================================================
const ModalTests = {
async deleteModalStructure(page, baseUrl) {
// #deleteConfirmModal is a static include in library.html (line 1140 →
// components/delete_confirmation_modal.html), present-but-hidden. The old
// test navigated to /history/ where it doesn't exist (→ permanent skip).
// Assert the real modal structure on /library/.
await navigateTo(page, `${baseUrl}/library/`);
await page.waitForSelector('#deleteConfirmModal', { timeout: 15000 });
const result = await page.evaluate(() => {
const modal = document.querySelector('#deleteConfirmModal');
if (!modal) return { exists: false };
return {
exists: true,
hasTitle: !!modal.querySelector('#deleteConfirmModalLabel'),
hasConfirm: !!modal.querySelector('#deleteConfirmBtn'),
hasCancel: !!modal.querySelector('.btn-secondary[data-bs-dismiss="modal"]'),
};
});
if (!result.exists) {
return { passed: false, message: '#deleteConfirmModal not found on /library/' };
}
const passed = result.hasTitle && result.hasConfirm && result.hasCancel;
return {
passed,
message: passed
? 'Delete modal has title + confirm (#deleteConfirmBtn) + cancel (data-bs-dismiss) buttons'
: `Delete modal incomplete (title=${result.hasTitle}, confirm=${result.hasConfirm}, cancel=${result.hasCancel})`
};
},
async modalZIndex(page, baseUrl) {
// #deleteConfirmModal carries an inline z-index: 10000
// (components/delete_confirmation_modal.html), included in library.html.
// The old test navigated to /history/ (modal absent → skip). Assert the
// z-index on /library/ so the modal sits above the app chrome. We check
// BOTH an absolute floor and a relative comparison: the mobile bottom nav
// is z-index 1500 (mobile-responsive.css), already above a bare 1000 floor,
// so the relative check (modal > sidebar AND modal > nav) is what actually
// guarantees the modal overlays the chrome. getComputedStyle is only called
// on elements that exist (getComputedStyle(null) throws); absent chrome → 0.
await navigateTo(page, `${baseUrl}/library/`);
await page.waitForSelector('#deleteConfirmModal', { timeout: 15000 });
const result = await page.evaluate(() => {
const modal = document.querySelector('#deleteConfirmModal');
if (!modal) return { exists: false };
const zOf = (sel) => {
const el = document.querySelector(sel);
if (!el) return 0;
const z = parseInt(window.getComputedStyle(el).zIndex, 10);
return Number.isFinite(z) ? z : 0;
};
const z = parseInt(window.getComputedStyle(modal).zIndex, 10);
return {
exists: true,
zIndex: Number.isFinite(z) ? z : 0,
sidebarZ: zOf('.ldr-sidebar'),
navZ: zOf('.ldr-mobile-bottom-nav'),
};
});
if (!result.exists) {
return { passed: false, message: '#deleteConfirmModal not found on /library/' };
}
const aboveChrome = result.zIndex > result.sidebarZ && result.zIndex > result.navZ;
const passed = result.zIndex >= 1000 && aboveChrome;
return {
passed,
message: passed
? `Delete modal z-index ${result.zIndex} >= 1000 and above chrome (sidebar=${result.sidebarZ}, nav=${result.navZ})`
: `Delete modal z-index ${result.zIndex} fails (>=1000 && >sidebar(${result.sidebarZ}) && >nav(${result.navZ}))`
};
}
};
// ============================================================================
// Navigation Tests
// ============================================================================
const NavigationTests = {
async sidebarLinksClickable(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const sidebar = document.querySelector('.ldr-sidebar');
if (!sidebar) return { exists: false };
const links = Array.from(sidebar.querySelectorAll('a[href]'));
const clickableLinks = links.filter(link => {
const rect = link.getBoundingClientRect();
const style = window.getComputedStyle(link);
return rect.width > 0 &&
rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.pointerEvents !== 'none';
});
return {
exists: true,
totalLinks: links.length,
clickableLinks: clickableLinks.length
};
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No sidebar on this page' };
}
return {
passed: result.clickableLinks > 0,
message: result.clickableLinks > 0
? `${result.clickableLinks}/${result.totalLinks} sidebar links are clickable`
: 'No clickable sidebar links found'
};
},
async activePageHighlighted(page, baseUrl) {
const pages = [
{ url: '/', name: 'Research' },
{ url: '/history/', name: 'History' },
{ url: '/settings/', name: 'Settings' }
];
let passCount = 0;
const details = [];
for (const pageInfo of pages) {
await navigateTo(page, `${baseUrl}${pageInfo.url}`);
const hasActive = await page.evaluate(() => {
const sidebar = document.querySelector('.ldr-sidebar');
if (!sidebar) return { hasSidebar: false };
const activeLinks = sidebar.querySelectorAll('.active, [class*="active"], [aria-current="page"]');
return {
hasSidebar: true,
hasActiveClass: activeLinks.length > 0
};
}, pageInfo.url);
if (hasActive.hasSidebar && hasActive.hasActiveClass) {
passCount++;
details.push(`${pageInfo.name}: OK`);
} else {
details.push(`${pageInfo.name}: MISSING`);
}
}
// All pages should have active highlighting
const allPassed = passCount === pages.length;
return {
passed: allPassed,
message: `Active state highlighting: ${details.join(', ')}`
};
},
async mobileNavTabs(page, baseUrl) {
await page.setViewport({ width: 375, height: 667, isMobile: true });
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const mobileNav = document.querySelector('.ldr-mobile-bottom-nav');
if (!mobileNav) return { exists: false };
const tabs = mobileNav.querySelectorAll('.ldr-mobile-nav-tab, [class*="nav-tab"], button, a');
const clickableTabs = Array.from(tabs).filter(tab => {
const rect = tab.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
return {
exists: true,
tabCount: clickableTabs.length,
tabLabels: clickableTabs.slice(0, 5).map(t => t.textContent?.trim() || t.getAttribute('aria-label'))
};
});
// Reset viewport
await page.setViewport({ width: 1280, height: 800 });
if (!result.exists) {
return { passed: null, skipped: true, message: 'No mobile nav on desktop viewport' };
}
return {
passed: result.tabCount >= 2,
message: result.tabCount >= 2
? `Mobile nav has ${result.tabCount} tabs: ${result.tabLabels.join(', ')}`
: `Mobile nav has insufficient tabs (${result.tabCount})`
};
}
};
// ============================================================================
// Scroll Tests
// ============================================================================
const ScrollTests = {
async sidebarScrollable(page, baseUrl) {
await page.setViewport({ width: 1280, height: 500 }); // Short viewport
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const sidebar = document.querySelector('.ldr-sidebar');
if (!sidebar) return { exists: false };
const nav = sidebar.querySelector('.ldr-sidebar-nav, nav, ul');
if (!nav) return { exists: true, hasNav: false };
const style = window.getComputedStyle(nav);
const parentStyle = window.getComputedStyle(sidebar);
return {
exists: true,
hasNav: true,
navScrollHeight: nav.scrollHeight,
navClientHeight: nav.clientHeight,
isOverflowing: nav.scrollHeight > nav.clientHeight,
overflowY: style.overflowY,
parentOverflowY: parentStyle.overflowY
};
});
// Reset viewport
await page.setViewport({ width: 1280, height: 800 });
if (!result.exists || !result.hasNav) {
return { passed: null, skipped: true, message: 'No sidebar nav to test' };
}
const isScrollable = result.overflowY === 'auto' ||
result.overflowY === 'scroll' ||
result.parentOverflowY === 'auto' ||
result.parentOverflowY === 'scroll';
return {
passed: !result.isOverflowing || isScrollable,
message: result.isOverflowing
? (isScrollable ? 'Sidebar nav is scrollable when content overflows' : 'Sidebar nav overflows but is not scrollable')
: 'Sidebar nav fits without scrolling'
};
},
async contentNotCutOff(page, baseUrl) {
await navigateTo(page, `${baseUrl}/settings/`);
const result = await page.evaluate(() => {
const mainEl = document.querySelector('main, .main-content, .container');
if (!mainEl) return { exists: false };
const allElements = mainEl.querySelectorAll('*');
let lastBottom = 0;
allElements.forEach(el => {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
if (rect.height > 0 && style.display !== 'none' && rect.bottom > lastBottom) {
lastBottom = rect.bottom;
}
});
const viewportHeight = window.innerHeight;
const bodyHeight = document.body.scrollHeight;
const isScrollable = bodyHeight > viewportHeight;
// Content is cut off if last element bottom exceeds viewport AND page is not scrollable
const isCutOff = lastBottom > viewportHeight && !isScrollable;
return {
exists: true,
lastBottom,
viewportHeight,
bodyHeight,
isScrollable,
isCutOff
};
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No main content found' };
}
return {
passed: !result.isCutOff,
message: result.isCutOff
? `Content is cut off at ${result.lastBottom}px (viewport: ${result.viewportHeight}px, not scrollable)`
: result.isScrollable
? `Page is scrollable (body: ${result.bodyHeight}px, viewport: ${result.viewportHeight}px)`
: `Page fits in viewport (last element at ${Math.round(result.lastBottom)}px)`
};
}
};
// ============================================================================
// Button State Tests
// ============================================================================
const ButtonTests = {
async buttonStates(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const buttons = document.querySelectorAll('button, .btn, input[type="submit"]');
if (buttons.length === 0) return { exists: false };
const btn = buttons[0];
const initialCursor = window.getComputedStyle(btn).cursor;
return {
exists: true,
buttonCount: buttons.length,
hasCursorPointer: initialCursor === 'pointer'
};
});
if (!result.exists) {
return { passed: false, message: 'No buttons found on page' };
}
return {
passed: result.hasCursorPointer,
message: result.hasCursorPointer
? `Buttons have pointer cursor (${result.buttonCount} buttons found)`
: 'Buttons may lack proper cursor style'
};
},
async disabledButtonStyle(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const disabledBtns = document.querySelectorAll('button[disabled], .btn[disabled], button.disabled, .btn.disabled');
if (disabledBtns.length === 0) return { hasDisabled: false };
const btn = disabledBtns[0];
const style = window.getComputedStyle(btn);
return {
hasDisabled: true,
opacity: style.opacity,
cursor: style.cursor,
pointerEvents: style.pointerEvents
};
});
if (!result.hasDisabled) {
return { passed: null, skipped: true, message: 'No disabled buttons to test' };
}
const isProperlyStyled = parseFloat(result.opacity) < 1 ||
result.cursor === 'not-allowed' ||
result.pointerEvents === 'none';
return {
passed: isProperlyStyled,
message: isProperlyStyled
? `Disabled buttons styled correctly (opacity: ${result.opacity}, cursor: ${result.cursor})`
: 'Disabled buttons may not be visually distinct'
};
}
};
// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
log.section('UI Functionality Tests');
const ctx = await setupTest({
authenticate: true,
viewport: { width: 1280, height: 800 }
});
const results = new TestResults('UI Functionality Tests');
const { page } = ctx;
const { baseUrl } = ctx.config;
async function run(category, name, testFn) {
try {
const result = await withTimeout(
testFn(page, baseUrl),
30000,
`${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 {
// Form Tests
log.section('Form Tests');
await run('Forms', 'Research Form Structure', (p, u) => FormTests.researchFormStructure(p, u));
await run('Forms', 'Input Focus States', (p, u) => FormTests.inputFocusStates(p, u));
await run('Forms', 'Settings Form Buttons', (p, u) => FormTests.settingsFormButtons(p, u));
// Dropdown Tests
log.section('Dropdown Tests');
await run('Dropdowns', 'Dropdown Structure', (p, u) => DropdownTests.dropdownStructure(p, u));
await run('Dropdowns', 'Dropdown Opens On Click', (p, u) => DropdownTests.dropdownOpensOnClick(p, u));
await run('Dropdowns', 'Dropdown Selection', (p, u) => DropdownTests.dropdownSelection(p, u));
// Modal Tests
log.section('Modal Tests');
await run('Modals', 'Delete Modal Structure', (p, u) => ModalTests.deleteModalStructure(p, u));
await run('Modals', 'Modal Z-Index', (p, u) => ModalTests.modalZIndex(p, u));
// Navigation Tests
log.section('Navigation Tests');
await run('Navigation', 'Sidebar Links Clickable', (p, u) => NavigationTests.sidebarLinksClickable(p, u));
await run('Navigation', 'Active Page Highlighted', (p, u) => NavigationTests.activePageHighlighted(p, u));
await run('Navigation', 'Mobile Nav Tabs', (p, u) => NavigationTests.mobileNavTabs(p, u));
// Scroll Tests
log.section('Scroll Tests');
await run('Scroll', 'Sidebar Scrollable', (p, u) => ScrollTests.sidebarScrollable(p, u));
await run('Scroll', 'Content Not Cut Off', (p, u) => ScrollTests.contentNotCutOff(p, u));
// Button Tests
log.section('Button Tests');
await run('Buttons', 'Button States', (p, u) => ButtonTests.buttonStates(p, u));
await run('Buttons', 'Disabled Button Style', (p, u) => ButtonTests.disabledButtonStyle(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 = { FormTests, DropdownTests, ModalTests, NavigationTests, ScrollTests, ButtonTests };