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
543 lines
21 KiB
JavaScript
543 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Form Validation ARIA Support Tests
|
|
*
|
|
* Tests the FormValidator utility on the research form:
|
|
* - Inline error display with ldr-field-invalid class
|
|
* - aria-invalid attribute toggling
|
|
* - aria-describedby linking to error element
|
|
* - aria-required on query textarea
|
|
* - Error clears after typing valid input
|
|
* - Validation fires on blur
|
|
*
|
|
* Run: node test_form_validation_aria_ci.js
|
|
*/
|
|
|
|
const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout } = require('./test_lib');
|
|
|
|
// Helpers used by the validation tests below. We wait on real DOM state instead
|
|
// of fixed delays so the tests stay correct on slow CI runners without padding
|
|
// the wall-clock time on fast ones.
|
|
async function waitForQueryReady(page, timeout = 5000) {
|
|
await page.waitForSelector('#query', { visible: true, timeout });
|
|
// The FormValidator hook is what actually gates "loading UI shown / not shown"
|
|
// on submit. addValidation() inserts a .ldr-field-error sibling next to #query,
|
|
// so we wait for that DOM marker — it proves the validator is registered, not
|
|
// just that the class is on window.
|
|
await page.waitForFunction(
|
|
() => {
|
|
const q = document.querySelector('#query');
|
|
if (!q || typeof window.FormValidator !== 'function') return false;
|
|
const sibling = q.nextElementSibling;
|
|
return !!sibling && sibling.classList.contains('ldr-field-error');
|
|
},
|
|
{ timeout }
|
|
).catch(() => {});
|
|
}
|
|
async function waitForValidationState(page, expectInvalid, timeout = 2000) {
|
|
await page.waitForFunction(
|
|
(wantInvalid) => {
|
|
const q = document.querySelector('#query');
|
|
if (!q) return false;
|
|
const isInvalid = q.classList.contains('ldr-field-invalid');
|
|
return wantInvalid ? isInvalid : !isInvalid;
|
|
},
|
|
{ timeout },
|
|
expectInvalid
|
|
).catch(() => {});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ARIA Attribute Tests
|
|
// ============================================================================
|
|
const AriaAttributeTests = {
|
|
async queryHasAriaRequired(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
return {
|
|
exists: !!query,
|
|
ariaRequired: query?.getAttribute('aria-required'),
|
|
hasHtmlRequired: query?.hasAttribute('required')
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'Query textarea not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.ariaRequired === 'true' && !result.hasHtmlRequired,
|
|
message: `aria-required="${result.ariaRequired}", HTML required=${result.hasHtmlRequired}`
|
|
};
|
|
},
|
|
|
|
async formValidatorLoaded(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
// Wait for the FormValidator module to actually attach instead of a fixed delay.
|
|
await page.waitForFunction(
|
|
() => typeof window.FormValidator === 'function' &&
|
|
typeof window.formValidators === 'object' &&
|
|
window.formValidators !== null,
|
|
{ timeout: 5000 }
|
|
).catch(() => {});
|
|
|
|
const result = await page.evaluate(() => {
|
|
return {
|
|
hasFormValidator: typeof window.FormValidator === 'function',
|
|
hasFormValidators: typeof window.formValidators === 'object' && window.formValidators !== null
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.hasFormValidator && result.hasFormValidators,
|
|
message: `FormValidator=${result.hasFormValidator}, formValidators=${result.hasFormValidators}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Inline Validation Tests
|
|
// ============================================================================
|
|
const InlineValidationTests = {
|
|
async emptySubmitShowsInlineError(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
await waitForQueryReady(page);
|
|
|
|
// Clear query and submit
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) {
|
|
query.value = '';
|
|
query.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
// Click submit
|
|
await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
|
|
await waitForValidationState(page, true);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (!query) return { exists: false };
|
|
|
|
const hasInvalidClass = query.classList.contains('ldr-field-invalid');
|
|
const ariaInvalid = query.getAttribute('aria-invalid');
|
|
|
|
// Find the error element
|
|
const errorId = query.getAttribute('aria-describedby');
|
|
let errorElement = null;
|
|
let errorText = '';
|
|
let errorVisible = false;
|
|
|
|
if (errorId) {
|
|
// aria-describedby may contain multiple IDs
|
|
const ids = errorId.split(' ');
|
|
for (const id of ids) {
|
|
const el = document.getElementById(id);
|
|
if (el && el.classList.contains('ldr-field-error')) {
|
|
errorElement = el;
|
|
errorText = el.textContent;
|
|
errorVisible = window.getComputedStyle(el).display !== 'none';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
exists: true,
|
|
hasInvalidClass,
|
|
ariaInvalid,
|
|
hasErrorElement: !!errorElement,
|
|
errorText,
|
|
errorVisible
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'Query textarea not found' };
|
|
}
|
|
|
|
const passed = result.hasInvalidClass &&
|
|
result.ariaInvalid === 'true' &&
|
|
result.hasErrorElement &&
|
|
result.errorVisible &&
|
|
result.errorText.length > 0;
|
|
|
|
return {
|
|
passed,
|
|
message: `invalid-class=${result.hasInvalidClass}, aria-invalid="${result.ariaInvalid}", ` +
|
|
`error-visible=${result.errorVisible}, error="${result.errorText}"`
|
|
};
|
|
},
|
|
|
|
async blurOnEmptyShowsError(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
await waitForQueryReady(page);
|
|
|
|
// Clear query field
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) {
|
|
query.value = '';
|
|
query.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
// Focus then blur the query field
|
|
await page.focus('#query');
|
|
await page.waitForFunction(() => document.activeElement?.id === 'query', { timeout: 1000 }).catch(() => {});
|
|
// Click somewhere else to trigger blur
|
|
await page.$eval('#query', el => el.blur());
|
|
await waitForValidationState(page, true);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (!query) return { exists: false };
|
|
|
|
return {
|
|
exists: true,
|
|
hasInvalidClass: query.classList.contains('ldr-field-invalid'),
|
|
ariaInvalid: query.getAttribute('aria-invalid')
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'Query textarea not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.hasInvalidClass && result.ariaInvalid === 'true',
|
|
message: `After blur on empty: invalid-class=${result.hasInvalidClass}, aria-invalid="${result.ariaInvalid}"`
|
|
};
|
|
},
|
|
|
|
async errorClearsOnValidSubmit(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
await waitForQueryReady(page);
|
|
|
|
// First trigger the error by submitting empty
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) {
|
|
query.value = '';
|
|
query.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
});
|
|
await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
|
|
await waitForValidationState(page, true);
|
|
|
|
// Verify error is shown
|
|
const errorShown = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
return query?.classList.contains('ldr-field-invalid') === true;
|
|
});
|
|
|
|
if (!errorShown) {
|
|
return { passed: null, skipped: true, message: 'Could not trigger validation error first' };
|
|
}
|
|
|
|
// Now type a valid query - this should clear the error on next submit attempt
|
|
await page.focus('#query');
|
|
await page.keyboard.type('Test research query');
|
|
// Wait for the typed value to land in the field instead of a fixed sleep.
|
|
await page.waitForFunction(
|
|
() => (document.querySelector('#query')?.value || '').includes('Test research'),
|
|
{ timeout: 2000 }
|
|
).catch(() => {});
|
|
|
|
// Verify the text was actually entered
|
|
const queryValue = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
return query?.value || '';
|
|
});
|
|
|
|
if (!queryValue.includes('Test research')) {
|
|
return { passed: null, skipped: true, message: `Text entry failed, value="${queryValue}"` };
|
|
}
|
|
|
|
// Intercept form submission to prevent navigation that would destroy page state
|
|
await page.evaluate(() => {
|
|
const form = document.querySelector('#query')?.closest('form');
|
|
if (form) {
|
|
form.addEventListener('submit', (e) => { e.preventDefault(); }, { once: true });
|
|
}
|
|
});
|
|
|
|
// Click submit - the validation should pass and clear errors
|
|
await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
|
|
// Wait for the ldr-field-invalid class to clear instead of a fixed sleep.
|
|
await waitForValidationState(page, false);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (!query) return { exists: false };
|
|
|
|
return {
|
|
exists: true,
|
|
hasInvalidClass: query.classList.contains('ldr-field-invalid'),
|
|
ariaInvalid: query.getAttribute('aria-invalid')
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'Query textarea not found after submit' };
|
|
}
|
|
|
|
return {
|
|
passed: !result.hasInvalidClass && result.ariaInvalid !== 'true',
|
|
message: `After valid submit: invalid-class=${result.hasInvalidClass}, aria-invalid="${result.ariaInvalid}"`
|
|
};
|
|
},
|
|
|
|
async noLoadingUiOnEmptySubmit(page, baseUrl) {
|
|
// This test asserts no loading UI appears on an empty submit. A previous test
|
|
// leaves the page on `/` (navigateTo skips when paths match) with a stale
|
|
// .ldr-loading-overlay from a half-finished real submission. We need a
|
|
// genuinely fresh page here, so force a reload via page.goto instead of
|
|
// relying on the same-path optimization.
|
|
await page.goto(`${baseUrl}/`, { waitUntil: 'domcontentloaded' });
|
|
await waitForQueryReady(page);
|
|
|
|
// Clear query
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) {
|
|
query.value = '';
|
|
query.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
// Submit and immediately check for loading overlay
|
|
await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
|
|
|
|
// Check immediately - there should be no loading overlay or spinner
|
|
const result = await page.evaluate(() => {
|
|
const overlay = document.querySelector('.ldr-loading-overlay');
|
|
const btn = document.querySelector('button[type="submit"], .start-research, #start-research');
|
|
const btnDisabled = btn?.disabled;
|
|
const btnHasSpinner = btn?.innerHTML?.includes('fa-spinner');
|
|
|
|
return {
|
|
hasOverlay: !!overlay,
|
|
btnDisabled: !!btnDisabled,
|
|
btnHasSpinner: !!btnHasSpinner
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: !result.hasOverlay && !result.btnDisabled && !result.btnHasSpinner,
|
|
message: `On empty submit: overlay=${result.hasOverlay}, btn-disabled=${result.btnDisabled}, spinner=${result.btnHasSpinner}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Error Element Structure Tests
|
|
// ============================================================================
|
|
const ErrorElementTests = {
|
|
async errorElementHasAriaLive(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
await waitForQueryReady(page);
|
|
|
|
// Trigger validation to create error element
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) {
|
|
query.value = '';
|
|
query.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
});
|
|
await page.focus('#query');
|
|
await page.waitForFunction(() => document.activeElement?.id === 'query', { timeout: 1000 }).catch(() => {});
|
|
await page.$eval('#query', el => el.blur());
|
|
await waitForValidationState(page, true);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (!query) return { exists: false };
|
|
|
|
const describedBy = query.getAttribute('aria-describedby');
|
|
if (!describedBy) return { exists: true, hasDescribedBy: false };
|
|
|
|
const ids = describedBy.split(' ');
|
|
for (const id of ids) {
|
|
const el = document.getElementById(id);
|
|
if (el && el.classList.contains('ldr-field-error')) {
|
|
return {
|
|
exists: true,
|
|
hasDescribedBy: true,
|
|
errorId: id,
|
|
ariaLive: el.getAttribute('aria-live'),
|
|
className: el.className
|
|
};
|
|
}
|
|
}
|
|
|
|
return { exists: true, hasDescribedBy: true, errorElementFound: false };
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'Query textarea not found' };
|
|
}
|
|
|
|
if (!result.hasDescribedBy) {
|
|
return { passed: false, message: 'No aria-describedby on query field' };
|
|
}
|
|
|
|
return {
|
|
passed: result.ariaLive === 'polite',
|
|
message: `Error element: id="${result.errorId}", aria-live="${result.ariaLive}", class="${result.className}"`
|
|
};
|
|
},
|
|
|
|
async errorPositionedAfterTextarea(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
await waitForQueryReady(page);
|
|
|
|
// Trigger validation
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) query.value = '';
|
|
});
|
|
await page.focus('#query');
|
|
await page.waitForFunction(() => document.activeElement?.id === 'query', { timeout: 1000 }).catch(() => {});
|
|
await page.$eval('#query', el => el.blur());
|
|
await waitForValidationState(page, true);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (!query) return { exists: false };
|
|
|
|
const nextSibling = query.nextElementSibling;
|
|
const isErrorElement = nextSibling?.classList.contains('ldr-field-error');
|
|
const searchHints = query.parentNode?.querySelector('.ldr-search-hints');
|
|
|
|
return {
|
|
exists: true,
|
|
nextSiblingIsError: isErrorElement,
|
|
nextSiblingClass: nextSibling?.className,
|
|
hintsExist: !!searchHints
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'Query textarea not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.nextSiblingIsError,
|
|
message: `Next sibling after textarea: class="${result.nextSiblingClass}", hints exist=${result.hintsExist}`
|
|
};
|
|
},
|
|
|
|
async focusMovesToFieldOnError(page, baseUrl) {
|
|
// Headless Chrome focus management after form submission is unreliable
|
|
if (process.env.CI) {
|
|
return { passed: null, skipped: true, message: 'CI: headless Chrome focus management after form submit is unreliable' };
|
|
}
|
|
|
|
await navigateTo(page, `${baseUrl}/`);
|
|
await waitForQueryReady(page);
|
|
|
|
// Clear and submit
|
|
await page.evaluate(() => {
|
|
const query = document.querySelector('#query');
|
|
if (query) {
|
|
query.value = '';
|
|
query.dispatchEvent(new Event('input', { bubbles: true }));
|
|
// Focus something else first
|
|
document.body.focus();
|
|
}
|
|
});
|
|
|
|
await page.click('button[type="submit"], .start-research, #start-research').catch(() => {});
|
|
await waitForValidationState(page, true);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const activeElement = document.activeElement;
|
|
return {
|
|
focusedId: activeElement?.id,
|
|
focusedTag: activeElement?.tagName?.toLowerCase()
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.focusedId === 'query',
|
|
message: `Focus after empty submit: id="${result.focusedId}", tag="${result.focusedTag}"`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
async function main() {
|
|
log.section('Form Validation ARIA Support Tests');
|
|
|
|
const ctx = await setupTest({ authenticate: true });
|
|
const results = new TestResults('Form Validation ARIA 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 {
|
|
// ARIA Attribute Tests
|
|
log.section('ARIA Attributes');
|
|
|
|
await run('ARIA', 'Query has aria-required', (p, u) => AriaAttributeTests.queryHasAriaRequired(p, u));
|
|
await run('ARIA', 'FormValidator loaded', (p, u) => AriaAttributeTests.formValidatorLoaded(p, u));
|
|
|
|
// Inline Validation Tests
|
|
log.section('Inline Validation');
|
|
|
|
await run('Validation', 'Empty submit shows inline error', (p, u) => InlineValidationTests.emptySubmitShowsInlineError(p, u));
|
|
await run('Validation', 'Blur on empty shows error', (p, u) => InlineValidationTests.blurOnEmptyShowsError(p, u));
|
|
await run('Validation', 'Error clears on valid submit', (p, u) => InlineValidationTests.errorClearsOnValidSubmit(p, u));
|
|
await run('Validation', 'No loading UI on empty submit', (p, u) => InlineValidationTests.noLoadingUiOnEmptySubmit(p, u));
|
|
|
|
// Error Element Structure Tests
|
|
log.section('Error Element Structure');
|
|
|
|
await run('Structure', 'Error element has aria-live=polite', (p, u) => ErrorElementTests.errorElementHasAriaLive(p, u));
|
|
await run('Structure', 'Error positioned after textarea', (p, u) => ErrorElementTests.errorPositionedAfterTextarea(p, u));
|
|
await run('Structure', 'Focus moves to field on error', (p, u) => ErrorElementTests.focusMovesToFieldOnError(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());
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch(error => {
|
|
console.error('Test runner failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { AriaAttributeTests, InlineValidationTests, ErrorElementTests };
|