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

809 lines
30 KiB
JavaScript

#!/usr/bin/env node
/**
* Loading & Feedback UI Tests
*
* Tests for loading states, spinners, toasts, success/error messages,
* and visual feedback mechanisms.
*
* Run: node test_loading_feedback_ci.js
*/
const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib');
// ============================================================================
// Loading States Tests
// ============================================================================
const LoadingStatesTests = {
async loadingSpinnersExist(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Check CSS for spinner definitions
const styleSheets = Array.from(document.styleSheets);
let hasSpinnerCSS = false;
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if (cssText.includes('spinner') || cssText.includes('loading') ||
cssText.includes('@keyframes') && (cssText.includes('spin') || cssText.includes('rotate'))) {
hasSpinnerCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
// Check for spinner elements in DOM
const spinnerElements = document.querySelectorAll(
'.spinner, ' +
'.loader, ' +
'.loading, ' +
'[class*="spinner"], ' +
'[class*="loader"], ' +
'.spinner-border, ' +
'.spinner-grow'
);
return {
hasSpinnerCSS,
spinnerCount: spinnerElements.length
};
});
const hasSpinners = result.hasSpinnerCSS || result.spinnerCount > 0;
return {
passed: hasSpinners,
message: `Spinners: CSS=${result.hasSpinnerCSS}, elements=${result.spinnerCount}`
};
},
async skeletonLoadersExist(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const skeletons = document.querySelectorAll(
'.skeleton, ' +
'.placeholder, ' +
'[class*="skeleton"], ' +
'[class*="placeholder"], ' +
'.loading-placeholder'
);
// Check CSS for skeleton animations
const styleSheets = Array.from(document.styleSheets);
let hasSkeletonCSS = false;
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if (cssText.includes('skeleton') || cssText.includes('placeholder') ||
cssText.includes('shimmer') || cssText.includes('pulse')) {
hasSkeletonCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
hasSkeletonCSS,
skeletonCount: skeletons.length
};
});
if (!result.hasSkeletonCSS && result.skeletonCount === 0) {
return { passed: null, skipped: true, message: 'No skeleton loaders found' };
}
return {
passed: true,
message: `Skeleton loaders: CSS=${result.hasSkeletonCSS}, elements=${result.skeletonCount}`
};
},
async buttonLoadingState(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const buttons = document.querySelectorAll('button[type="submit"], .submit-btn, .btn-primary');
// Check for loading state classes/attributes in button
let hasLoadingState = false;
let loadingClass = '';
for (const btn of buttons) {
const classes = btn.className;
const dataset = btn.dataset;
if (classes.includes('loading') || dataset.loading || btn.hasAttribute('disabled')) {
hasLoadingState = true;
loadingClass = classes;
break;
}
}
// Check CSS for button loading styles
const styleSheets = Array.from(document.styleSheets);
let hasButtonLoadingCSS = false;
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if ((cssText.includes('button') || cssText.includes('.btn')) &&
(cssText.includes('loading') || cssText.includes('disabled'))) {
hasButtonLoadingCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
buttonCount: buttons.length,
hasLoadingState,
hasButtonLoadingCSS
};
});
return {
passed: result.hasLoadingState || result.hasButtonLoadingCSS,
message: `Button loading: ${result.buttonCount} buttons, state=${result.hasLoadingState}, CSS=${result.hasButtonLoadingCSS}`
};
}
};
// ============================================================================
// Toast Notifications Tests
// ============================================================================
const ToastNotificationsTests = {
async toastContainerExists(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const toastContainer = document.querySelector(
'.toast-container, ' +
'.toasts, ' +
'#toast-container, ' +
'[class*="toast-wrapper"], ' +
'.notification-container'
);
// Check for toast CSS
const styleSheets = Array.from(document.styleSheets);
let hasToastCSS = false;
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if (cssText.includes('.toast') || cssText.includes('notification')) {
hasToastCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
hasContainer: !!toastContainer,
hasToastCSS
};
});
if (!result.hasContainer && !result.hasToastCSS) {
return { passed: null, skipped: true, message: 'No toast notification system found' };
}
return {
passed: true,
message: `Toast system: container=${result.hasContainer}, CSS=${result.hasToastCSS}`
};
},
async flashMessagesDisplayed(page, baseUrl) {
// The app shows flash/alert messages by routing showAlert(...) through
// window.showSafeAlert, which renders into the #research-alert live region.
// The old test queried alert selectors on a fresh / and returned passed:true
// unconditionally — it never verified a message is displayed. Drive the real
// display function with a marker and assert it renders a visible alert.
//
// showSafeAlert clears the container and appends the alert SYNCHRONOUSLY, so we
// trigger and read in a single page.evaluate: that both isolates the marker from
// the on-load /api/warnings check (which also renders into this same container)
// and removes any async race. We drive showSafeAlert directly rather than a real
// user trigger, so this is a rendering-mechanism check.
await navigateTo(page, `${baseUrl}/`);
await page.waitForSelector('#research-alert', { timeout: 15000 });
const MARKER = 'LDR_UI_TEST_FLASH_MARKER';
const result = await page.evaluate((marker) => {
if (typeof window.showSafeAlert !== 'function') return { hasMechanism: false };
const a = document.querySelector('#research-alert');
if (!a) return { hasMechanism: true, hasContainer: false };
a.innerHTML = '';
a.style.display = 'none';
window.showSafeAlert('research-alert', marker, 'error');
// Read in the same synchronous tick — no async warning re-render can race it.
const child = a.firstElementChild;
return {
hasMechanism: true,
hasContainer: true,
visible: window.getComputedStyle(a).display !== 'none',
childCount: a.children.length,
rendered: a.textContent.includes(marker),
// role of the APPENDED alert element (createSafeAlertElement sets
// role="alert"), not the container's static template role — so this
// conjunct actually verifies the rendered alert, not a constant.
childRole: child ? child.getAttribute('role') : null,
};
}, MARKER);
// Restore #research-alert to its hidden/empty default so the marker alert
// doesn't leak into sibling tests that share this page.
await page.evaluate(() => {
const a = document.querySelector('#research-alert');
if (a) { a.innerHTML = ''; a.style.display = 'none'; }
});
if (!result.hasMechanism) {
return { passed: false, message: 'Alert mechanism (window.showSafeAlert) not loaded on /' };
}
if (!result.hasContainer) {
return { passed: false, message: '#research-alert container not found on /' };
}
const passed = result.rendered && result.visible && result.childCount > 0
&& result.childRole === 'alert';
return {
passed,
message: passed
? `Flash message rendered into #research-alert (alert role="alert", visible, ${result.childCount} child)`
: `Flash message not displayed (rendered=${result.rendered}, visible=${result.visible}, children=${result.childCount}, childRole=${result.childRole})`
};
}
};
// ============================================================================
// Success/Error Feedback Tests
// ============================================================================
const FeedbackTests = {
async successFeedbackStyles(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Check for success-related CSS classes
const successElements = document.querySelectorAll(
'.success, ' +
'.alert-success, ' +
'.text-success, ' +
'.bg-success, ' +
'[class*="success"]'
);
// Check CSS for success colors
let hasSuccessCSS = false;
const styleSheets = Array.from(document.styleSheets);
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if (cssText.includes('success') && (cssText.includes('green') || cssText.includes('#28a745') || cssText.includes('#198754'))) {
hasSuccessCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
elementCount: successElements.length,
hasSuccessCSS
};
});
return {
passed: result.hasSuccessCSS || result.elementCount > 0,
message: `Success styles: elements=${result.elementCount}, CSS=${result.hasSuccessCSS}`
};
},
async errorFeedbackStyles(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
// Check for error-related CSS classes
const errorElements = document.querySelectorAll(
'.error, ' +
'.alert-danger, ' +
'.text-danger, ' +
'.bg-danger, ' +
'[class*="error"], ' +
'.invalid-feedback'
);
// Check CSS for error colors
let hasErrorCSS = false;
const styleSheets = Array.from(document.styleSheets);
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if ((cssText.includes('error') || cssText.includes('danger') || cssText.includes('invalid')) &&
(cssText.includes('red') || cssText.includes('#dc3545') || cssText.includes('#f44336'))) {
hasErrorCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
elementCount: errorElements.length,
hasErrorCSS
};
});
return {
passed: result.hasErrorCSS || result.elementCount > 0,
message: `Error styles: elements=${result.elementCount}, CSS=${result.hasErrorCSS}`
};
},
async warningFeedbackStyles(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const warningElements = document.querySelectorAll(
'.warning, ' +
'.alert-warning, ' +
'.text-warning, ' +
'[class*="warning"]'
);
let hasWarningCSS = false;
const styleSheets = Array.from(document.styleSheets);
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if (cssText.includes('warning') && (cssText.includes('yellow') || cssText.includes('orange') || cssText.includes('#ffc107'))) {
hasWarningCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
elementCount: warningElements.length,
hasWarningCSS
};
});
return {
passed: result.hasWarningCSS || result.elementCount > 0,
message: `Warning styles: elements=${result.elementCount}, CSS=${result.hasWarningCSS}`
};
}
};
// ============================================================================
// Progress Indicators Tests
// ============================================================================
const ProgressIndicatorsTests = {
async progressBarExists(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
await delay(500);
const result = await page.evaluate(() => {
// Broader search for progress indicators
const progressBars = document.querySelectorAll(
'.progress, ' +
'.progress-bar, ' +
'[role="progressbar"], ' +
'progress, ' +
'[class*="progress"], ' +
'.spinner, ' +
'[class*="spinner"], ' +
'[class*="loading"], ' +
'.loader'
);
// Check CSS for progress styles
let hasProgressCSS = false;
const styleSheets = Array.from(document.styleSheets);
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
const cssText = rule.cssText?.toLowerCase() || '';
if (cssText.includes('.progress') || cssText.includes('progressbar') ||
cssText.includes('spinner') || cssText.includes('@keyframes')) {
hasProgressCSS = true;
break;
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
count: progressBars.length,
hasProgressCSS
};
});
// Skip instead of fail if no progress bars found - they may appear dynamically
if (result.count === 0 && !result.hasProgressCSS) {
return {
passed: null,
skipped: true,
message: 'No progress indicators visible (may appear during active research)'
};
}
return {
passed: true,
message: `Progress bars: ${result.count} found, CSS=${result.hasProgressCSS}`
};
},
async progressPageHasIndicators(page, baseUrl) {
// Navigate to a progress page if available
await navigateTo(page, `${baseUrl}/history`);
// Look for any in-progress research
const researchId = await page.evaluate(() => {
const inProgress = document.querySelector('[data-status="in_progress"], .in-progress, [class*="running"]');
if (inProgress) {
const link = inProgress.querySelector('a[href*="progress"]');
const match = link?.href?.match(/\/progress\/([a-zA-Z0-9-]+)/);
return match ? match[1] : null;
}
return null;
});
if (!researchId) {
// Try to access any progress page
await navigateTo(page, `${baseUrl}/progress/test-id`).catch(() => {});
} else {
await navigateTo(page, `${baseUrl}/progress/${researchId}`);
}
const result = await page.evaluate(() => {
const progressBar = document.querySelector('.progress, .progress-bar, [role="progressbar"]');
const statusText = document.querySelector('.status, .status-text, [class*="status"]');
const spinner = document.querySelector('.spinner, .loader, [class*="loading"]');
const percentText = document.body.textContent.match(/\d+%/);
return {
hasProgressBar: !!progressBar,
hasStatusText: !!statusText,
hasSpinner: !!spinner,
hasPercent: !!percentText,
statusContent: statusText?.textContent?.trim()?.substring(0, 50)
};
});
const hasIndicators = result.hasProgressBar || result.hasStatusText || result.hasSpinner || result.hasPercent;
return {
passed: hasIndicators,
message: `Progress page: bar=${result.hasProgressBar}, status="${result.statusContent}", spinner=${result.hasSpinner}`
};
}
};
// ============================================================================
// Hover & Active States Tests
// ============================================================================
const HoverActiveStatesTests = {
async buttonHoverStates(page, baseUrl) {
// :hover pseudo-class is unreliable in headless Chrome — visual changes
// may not apply even when hover() is called
if (process.env.CI) {
return { passed: null, skipped: true, message: 'Hover styles unreliable in headless Chrome' };
}
await navigateTo(page, `${baseUrl}/`);
const button = await page.$('button, .btn');
if (!button) {
return { passed: null, skipped: true, message: 'No button to test hover state' };
}
const beforeHover = await page.evaluate(() => {
const btn = document.querySelector('button, .btn');
const style = window.getComputedStyle(btn);
return {
backgroundColor: style.backgroundColor,
boxShadow: style.boxShadow,
transform: style.transform
};
});
await button.hover();
await delay(200);
const afterHover = await page.evaluate(() => {
const btn = document.querySelector('button, .btn');
const style = window.getComputedStyle(btn);
return {
backgroundColor: style.backgroundColor,
boxShadow: style.boxShadow,
transform: style.transform
};
});
const hasHoverEffect = beforeHover.backgroundColor !== afterHover.backgroundColor ||
beforeHover.boxShadow !== afterHover.boxShadow ||
beforeHover.transform !== afterHover.transform;
return {
passed: hasHoverEffect,
message: hasHoverEffect
? 'Buttons have hover state changes'
: 'No visible hover effect on buttons'
};
},
async linkHoverStates(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
await delay(300);
const link = await page.$('a[href]');
if (!link) {
return { passed: null, skipped: true, message: 'No link to test hover state' };
}
try {
const beforeHover = await page.evaluate(() => {
const a = document.querySelector('a[href]');
if (!a) return null;
const style = window.getComputedStyle(a);
return {
textDecoration: style.textDecoration,
color: style.color,
opacity: style.opacity,
transform: style.transform
};
});
if (!beforeHover) {
return { passed: null, skipped: true, message: 'Link not accessible for hover test' };
}
await link.hover();
await delay(300);
const afterHover = await page.evaluate(() => {
const a = document.querySelector('a[href]');
if (!a) return null;
const style = window.getComputedStyle(a);
return {
textDecoration: style.textDecoration,
color: style.color,
opacity: style.opacity,
transform: style.transform
};
});
if (!afterHover) {
return { passed: null, skipped: true, message: 'Link state changed during hover test' };
}
const hasHoverEffect = beforeHover.textDecoration !== afterHover.textDecoration ||
beforeHover.color !== afterHover.color ||
beforeHover.opacity !== afterHover.opacity ||
beforeHover.transform !== afterHover.transform;
// Skip instead of fail - hover effects may be subtle or not implemented
if (!hasHoverEffect) {
return {
passed: null,
skipped: true,
message: 'No visible hover effect detected on links (may use subtle or CSS-in-JS styles)'
};
}
return {
passed: true,
message: `Links have hover effect (decoration: ${beforeHover.textDecoration} -> ${afterHover.textDecoration})`
};
} catch (err) {
return { passed: null, skipped: true, message: `Hover test failed: ${err.message}` };
}
}
};
// ============================================================================
// Animation Tests
// ============================================================================
const AnimationTests = {
async cssAnimationsDefined(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const styleSheets = Array.from(document.styleSheets);
const animations = [];
try {
for (const sheet of styleSheets) {
if (sheet.cssRules) {
for (const rule of sheet.cssRules) {
if (rule.type === CSSRule.KEYFRAMES_RULE) {
animations.push(rule.name);
}
}
}
}
} catch {
// Cross-origin stylesheets will throw
}
return {
count: animations.length,
names: animations.slice(0, 10)
};
});
return {
passed: result.count > 0,
message: `CSS animations: ${result.count} defined (${result.names.join(', ')})`
};
},
async transitionsUsed(page, baseUrl) {
await navigateTo(page, `${baseUrl}/`);
const result = await page.evaluate(() => {
const elementsWithTransitions = [];
const allElements = document.querySelectorAll('*');
for (const el of allElements) {
const style = window.getComputedStyle(el);
if (style.transition && style.transition !== 'none' && style.transition !== 'all 0s ease 0s') {
elementsWithTransitions.push(el.tagName);
if (elementsWithTransitions.length >= 10) break;
}
}
return {
count: elementsWithTransitions.length,
elements: elementsWithTransitions.slice(0, 5)
};
});
return {
passed: result.count > 0,
message: `CSS transitions: ${result.count}+ elements (${result.elements.join(', ')})`
};
}
};
// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
log.section('Loading & Feedback UI Tests');
const ctx = await setupTest({ authenticate: true });
const results = new TestResults('Loading Feedback 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 {
// Loading States
log.section('Loading States');
await run('Loading', 'Spinners', (p, u) => LoadingStatesTests.loadingSpinnersExist(p, u));
await run('Loading', 'Skeletons', (p, u) => LoadingStatesTests.skeletonLoadersExist(p, u));
await run('Loading', 'Button States', (p, u) => LoadingStatesTests.buttonLoadingState(p, u));
// Toast Notifications
log.section('Toast Notifications');
await run('Toast', 'Container', (p, u) => ToastNotificationsTests.toastContainerExists(p, u));
await run('Toast', 'Flash Messages', (p, u) => ToastNotificationsTests.flashMessagesDisplayed(p, u));
// Success/Error Feedback
log.section('Feedback Styles');
await run('Feedback', 'Success Styles', (p, u) => FeedbackTests.successFeedbackStyles(p, u));
await run('Feedback', 'Error Styles', (p, u) => FeedbackTests.errorFeedbackStyles(p, u));
await run('Feedback', 'Warning Styles', (p, u) => FeedbackTests.warningFeedbackStyles(p, u));
// Progress Indicators
log.section('Progress Indicators');
await run('Progress', 'Bar Exists', (p, u) => ProgressIndicatorsTests.progressBarExists(p, u));
await run('Progress', 'Page Indicators', (p, u) => ProgressIndicatorsTests.progressPageHasIndicators(p, u));
// Hover & Active States
log.section('Hover States');
await run('Hover', 'Button States', (p, u) => HoverActiveStatesTests.buttonHoverStates(p, u));
await run('Hover', 'Link States', (p, u) => HoverActiveStatesTests.linkHoverStates(p, u));
// Animations
log.section('Animations');
await run('Animation', 'CSS Animations', (p, u) => AnimationTests.cssAnimationsDefined(p, u));
await run('Animation', 'Transitions', (p, u) => AnimationTests.transitionsUsed(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 = { LoadingStatesTests, ToastNotificationsTests, FeedbackTests, ProgressIndicatorsTests, HoverActiveStatesTests, AnimationTests };