testaro 74.2.1 → 74.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/UPGRADES.md +348 -0
- package/package.json +1 -1
- package/tests/axe.js +45 -2
package/UPGRADES.md
CHANGED
|
@@ -8360,3 +8360,351 @@ pool@jpdev testaro % claude
|
|
|
8360
8360
|
|
|
8361
8361
|
claude --resume bb937c34-19d9-4fd5-b04b-1356fb39b355
|
|
8362
8362
|
```
|
|
8363
|
+
|
|
8364
|
+
## Initial modal dialogs
|
|
8365
|
+
|
|
8366
|
+
|
|
8367
|
+
|
|
8368
|
+
|
|
8369
|
+
|
|
8370
|
+
|
|
8371
|
+
|
|
8372
|
+
|
|
8373
|
+
Playwright provides several methods to detect modal dialogs, ranging from native browser dialogs to custom modal components.
|
|
8374
|
+
|
|
8375
|
+
## Modal Detection Methods
|
|
8376
|
+
|
|
8377
|
+
### 1. Native Browser Dialogs (alert, confirm, prompt)
|
|
8378
|
+
```javascript
|
|
8379
|
+
// Handle native dialogs that block execution
|
|
8380
|
+
page.on('dialog', async dialog => {
|
|
8381
|
+
console.log('Dialog type:', dialog.type());
|
|
8382
|
+
console.log('Dialog message:', dialog.message());
|
|
8383
|
+
await dialog.accept(); // or dialog.dismiss()
|
|
8384
|
+
});
|
|
8385
|
+
|
|
8386
|
+
// Check if dialog handler was triggered
|
|
8387
|
+
let dialogAppeared = false;
|
|
8388
|
+
page.on('dialog', () => { dialogAppeared = true; });
|
|
8389
|
+
await someAction(); // Action that might trigger dialog
|
|
8390
|
+
if (dialogAppeared) {
|
|
8391
|
+
// Handle modal
|
|
8392
|
+
}
|
|
8393
|
+
```
|
|
8394
|
+
|
|
8395
|
+
### 2. ARIA-Based Modal Detection
|
|
8396
|
+
```javascript
|
|
8397
|
+
// Detect modals by ARIA role and attributes
|
|
8398
|
+
async function detectAriaModal(page) {
|
|
8399
|
+
const modalSelectors = [
|
|
8400
|
+
'[role="dialog"]',
|
|
8401
|
+
'[role="alertdialog"]',
|
|
8402
|
+
'[aria-modal="true"]'
|
|
8403
|
+
];
|
|
8404
|
+
|
|
8405
|
+
for (const selector of modalSelectors) {
|
|
8406
|
+
const modal = page.locator(selector);
|
|
8407
|
+
if (await modal.isVisible()) {
|
|
8408
|
+
return modal;
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
return null;
|
|
8412
|
+
}
|
|
8413
|
+
|
|
8414
|
+
// Usage
|
|
8415
|
+
const modal = await detectAriaModal(page);
|
|
8416
|
+
if (modal) {
|
|
8417
|
+
console.log('ARIA modal detected');
|
|
8418
|
+
// Test modal accessibility
|
|
8419
|
+
}
|
|
8420
|
+
```
|
|
8421
|
+
|
|
8422
|
+
### 3. CSS-Based Modal Detection
|
|
8423
|
+
```javascript
|
|
8424
|
+
// Detect modals by common CSS patterns
|
|
8425
|
+
async function detectCssModal(page) {
|
|
8426
|
+
const modalSelectors = [
|
|
8427
|
+
'.modal:visible',
|
|
8428
|
+
'.modal-dialog:visible',
|
|
8429
|
+
'.modal-overlay:visible',
|
|
8430
|
+
'.popup:visible',
|
|
8431
|
+
'.dialog:visible',
|
|
8432
|
+
'[style*="display: block"][style*="position: fixed"]'
|
|
8433
|
+
];
|
|
8434
|
+
|
|
8435
|
+
for (const selector of modalSelectors) {
|
|
8436
|
+
try {
|
|
8437
|
+
const modal = page.locator(selector);
|
|
8438
|
+
if (await modal.isVisible({ timeout: 1000 })) {
|
|
8439
|
+
return modal;
|
|
8440
|
+
}
|
|
8441
|
+
} catch (e) {
|
|
8442
|
+
// Element not found, continue
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
return null;
|
|
8446
|
+
}
|
|
8447
|
+
```
|
|
8448
|
+
|
|
8449
|
+
### 4. Comprehensive Modal Detection
|
|
8450
|
+
```javascript
|
|
8451
|
+
async function detectAnyModal(page) {
|
|
8452
|
+
// Check native dialogs first
|
|
8453
|
+
let dialogAppeared = false;
|
|
8454
|
+
const dialogHandler = () => { dialogAppeared = true; };
|
|
8455
|
+
page.on('dialog', dialogHandler);
|
|
8456
|
+
|
|
8457
|
+
// Check ARIA modals
|
|
8458
|
+
const ariaModal = await detectAriaModal(page);
|
|
8459
|
+
if (ariaModal) return { type: 'aria', element: ariaModal };
|
|
8460
|
+
|
|
8461
|
+
// Check CSS modals
|
|
8462
|
+
const cssModal = await detectCssModal(page);
|
|
8463
|
+
if (cssModal) return { type: 'css', element: cssModal };
|
|
8464
|
+
|
|
8465
|
+
// Check native dialog
|
|
8466
|
+
if (dialogAppeared) return { type: 'native', element: null };
|
|
8467
|
+
|
|
8468
|
+
return null;
|
|
8469
|
+
}
|
|
8470
|
+
```
|
|
8471
|
+
|
|
8472
|
+
### 5. Accessibility-Focused Modal Testing
|
|
8473
|
+
```javascript
|
|
8474
|
+
async function testModalAccessibility(page) {
|
|
8475
|
+
const modal = await detectAnyModal(page);
|
|
8476
|
+
if (!modal) return null;
|
|
8477
|
+
|
|
8478
|
+
const element = modal.element;
|
|
8479
|
+
const accessibilityTests = {
|
|
8480
|
+
hasAccessibleName: await element.getAttribute('aria-label') ||
|
|
8481
|
+
await element.getAttribute('aria-labelledby'),
|
|
8482
|
+
hasModalRole: await element.getAttribute('role') === 'dialog' ||
|
|
8483
|
+
await element.getAttribute('aria-modal') === 'true',
|
|
8484
|
+
isFocusTrapped: await testFocusTrap(page, element),
|
|
8485
|
+
hasCloseMethod: await page.locator('[aria-label="Close"], .close, button[title="Close"]').isVisible()
|
|
8486
|
+
};
|
|
8487
|
+
|
|
8488
|
+
return {
|
|
8489
|
+
detected: modal,
|
|
8490
|
+
accessibility: accessibilityTests
|
|
8491
|
+
};
|
|
8492
|
+
}
|
|
8493
|
+
|
|
8494
|
+
async function testFocusTrap(page, modalElement) {
|
|
8495
|
+
// Test Tab key stays within modal
|
|
8496
|
+
await page.keyboard.press('Tab');
|
|
8497
|
+
const focusedElement = await page.locator(':focus');
|
|
8498
|
+
const modalContainsFocus = await modalElement.contains(focusedElement);
|
|
8499
|
+
return modalContainsFocus;
|
|
8500
|
+
}
|
|
8501
|
+
```
|
|
8502
|
+
|
|
8503
|
+
## Implementation for Testaro
|
|
8504
|
+
|
|
8505
|
+
```javascript
|
|
8506
|
+
// Modal detection workflow for accessibility testing
|
|
8507
|
+
async function checkPageModals(page) {
|
|
8508
|
+
const modalInfo = await testModalAccessibility(page);
|
|
8509
|
+
|
|
8510
|
+
if (modalInfo) {
|
|
8511
|
+
console.log(`Modal detected: ${modalInfo.detected.type}`);
|
|
8512
|
+
|
|
8513
|
+
// Test focus management
|
|
8514
|
+
await testFocusManagement(page, modalInfo.detected.element);
|
|
8515
|
+
|
|
8516
|
+
// Test keyboard dismissability
|
|
8517
|
+
await testKeyboardDismiss(page, modalInfo.detected.element);
|
|
8518
|
+
|
|
8519
|
+
return modalInfo;
|
|
8520
|
+
}
|
|
8521
|
+
|
|
8522
|
+
return null;
|
|
8523
|
+
}
|
|
8524
|
+
|
|
8525
|
+
async function testFocusManagement(page, modal) {
|
|
8526
|
+
// Check if focus is properly managed
|
|
8527
|
+
const initialFocus = await page.locator(':focus');
|
|
8528
|
+
await page.keyboard.press('Tab');
|
|
8529
|
+
const afterTabFocus = await page.locator(':focus');
|
|
8530
|
+
|
|
8531
|
+
return {
|
|
8532
|
+
focusInitiallyInModal: await modal.contains(initialFocus),
|
|
8533
|
+
focusRemainsInModal: await modal.contains(afterTabFocus)
|
|
8534
|
+
};
|
|
8535
|
+
}
|
|
8536
|
+
```
|
|
8537
|
+
|
|
8538
|
+
This comprehensive approach allows Testaro to detect and test accessibility of various modal types across different implementations.
|
|
8539
|
+
|
|
8540
|
+
## State cleaning before each page load
|
|
8541
|
+
|
|
8542
|
+
|
|
8543
|
+
|
|
8544
|
+
|
|
8545
|
+
|
|
8546
|
+
|
|
8547
|
+
|
|
8548
|
+
|
|
8549
|
+
|
|
8550
|
+
|
|
8551
|
+
Yes, Playwright provides comprehensive state reset capabilities that are consistent across Chromium, WebKit, and Firefox.
|
|
8552
|
+
|
|
8553
|
+
## Complete State Reset Capabilities
|
|
8554
|
+
|
|
8555
|
+
### Available Methods
|
|
8556
|
+
Playwright's `BrowserContext` class provides these state management methods:
|
|
8557
|
+
|
|
8558
|
+
**Cookies:**
|
|
8559
|
+
```javascript
|
|
8560
|
+
await context.clearCookies(); // Clear all cookies
|
|
8561
|
+
await context.clearCookies({ domain: 'example.com' }); // Clear specific domain
|
|
8562
|
+
```
|
|
8563
|
+
|
|
8564
|
+
**Storage State:**
|
|
8565
|
+
```javascript
|
|
8566
|
+
// Clear and reset all storage
|
|
8567
|
+
await context.setStorageState({ cookies: [], origins: [] });
|
|
8568
|
+
|
|
8569
|
+
// Save current state
|
|
8570
|
+
await context.storageState({ path: 'state.json' });
|
|
8571
|
+
|
|
8572
|
+
// Load clean state (effectively a reset)
|
|
8573
|
+
await context.setStorageState('clean-state.json');
|
|
8574
|
+
```
|
|
8575
|
+
|
|
8576
|
+
### Complete Reset Implementation
|
|
8577
|
+
For the "Complete State Reset" I listed earlier:
|
|
8578
|
+
|
|
8579
|
+
```javascript
|
|
8580
|
+
// Universal state reset across all browsers
|
|
8581
|
+
async function resetBrowserState(context) {
|
|
8582
|
+
await context.clearCookies(); // Clear all cookies
|
|
8583
|
+
await context.clearPermissions(); // Clear permission overrides
|
|
8584
|
+
await context.setStorageState({ // Clear localStorage, sessionStorage, IndexedDB
|
|
8585
|
+
cookies: [],
|
|
8586
|
+
origins: []
|
|
8587
|
+
});
|
|
8588
|
+
|
|
8589
|
+
// Alternative: Create fresh context
|
|
8590
|
+
const freshContext = await browser.newContext();
|
|
8591
|
+
return freshContext;
|
|
8592
|
+
}
|
|
8593
|
+
```
|
|
8594
|
+
|
|
8595
|
+
## Cross-Browser Compatibility
|
|
8596
|
+
|
|
8597
|
+
**Yes, the methods are identical across all three browsers:**
|
|
8598
|
+
|
|
8599
|
+
- **Chromium**: Full support for all state management APIs
|
|
8600
|
+
- **WebKit**: Full support for all state management APIs
|
|
8601
|
+
- **Firefox**: Full support for all state management APIs
|
|
8602
|
+
|
|
8603
|
+
### Key Points:
|
|
8604
|
+
|
|
8605
|
+
1. **API Consistency**: The same `BrowserContext` methods work identically across all browsers
|
|
8606
|
+
2. **Storage Coverage**: Methods handle cookies, localStorage, sessionStorage, and IndexedDB uniformly
|
|
8607
|
+
3. **No Browser-Specific Code**: No conditional logic needed for different browsers
|
|
8608
|
+
|
|
8609
|
+
### Implementation for Testaro
|
|
8610
|
+
|
|
8611
|
+
```javascript
|
|
8612
|
+
// Browser-agnostic state reset
|
|
8613
|
+
async function ensureCleanState(browser, testUrl) {
|
|
8614
|
+
const context = await browser.newContext();
|
|
8615
|
+
await context.clearCookies();
|
|
8616
|
+
await context.setStorageState({ cookies: [], origins: [] });
|
|
8617
|
+
|
|
8618
|
+
const page = await context.newPage();
|
|
8619
|
+
await page.goto(testUrl);
|
|
8620
|
+
|
|
8621
|
+
return { context, page };
|
|
8622
|
+
}
|
|
8623
|
+
```
|
|
8624
|
+
|
|
8625
|
+
## Multi-State Testing Strategy
|
|
8626
|
+
|
|
8627
|
+
With Playwright's consistent APIs, Testaro can implement:
|
|
8628
|
+
|
|
8629
|
+
1. **First Load Test**: Fresh context → no cookies → test initial page
|
|
8630
|
+
2. **Dialog State Test**: Interact with dialogs → retest
|
|
8631
|
+
3. **Post-Interaction Test**: Dismiss dialogs → retest clean state
|
|
8632
|
+
|
|
8633
|
+
All using the same codebase across Chromium, WebKit, and Firefox.
|
|
8634
|
+
|
|
8635
|
+
## Code-hygiene-based testing
|
|
8636
|
+
|
|
8637
|
+
|
|
8638
|
+
Your investigation reveals critical flaws in current accessibility testing methodology that go far beyond the initial SVG issue.
|
|
8639
|
+
|
|
8640
|
+
## (1) Test Isolation and State Management Problems
|
|
8641
|
+
|
|
8642
|
+
### The Cookie State Issue
|
|
8643
|
+
Your discovery demonstrates that **page state fundamentally affects accessibility testing**:
|
|
8644
|
+
|
|
8645
|
+
- **First load (no cookies)**: Cookie banner appears, SVG becomes visible without accessible name
|
|
8646
|
+
- **Subsequent loads (with cookies)**: Cookie banner suppressed, SVG remains hidden
|
|
8647
|
+
- **Current testing**: May capture only one state, missing violations in the other
|
|
8648
|
+
|
|
8649
|
+
### Required Testaro Improvements
|
|
8650
|
+
|
|
8651
|
+
**Complete State Reset**:
|
|
8652
|
+
- Clear all site cookies before each page load
|
|
8653
|
+
- Clear localStorage, sessionStorage, IndexedDB
|
|
8654
|
+
- Reset browser state to truly "first visit" conditions
|
|
8655
|
+
|
|
8656
|
+
**Multi-State Testing**:
|
|
8657
|
+
1. **First Load Test**: Test page with no stored state
|
|
8658
|
+
2. **Dialog Dismissal Test**: Dismiss initial dialogs, then retest
|
|
8659
|
+
3. **State Change Test**: Test after common user interactions
|
|
8660
|
+
|
|
8661
|
+
**Modal Dialog Testing**:
|
|
8662
|
+
- Identify modal dialogs on first load
|
|
8663
|
+
- Test focus trapping behavior
|
|
8664
|
+
- Verify keyboard dismissability
|
|
8665
|
+
- Check ARIA attributes in visible state
|
|
8666
|
+
|
|
8667
|
+
## (2) Rule Definition Philosophy: Momentary vs. Hygiene
|
|
8668
|
+
|
|
8669
|
+
### Current Problem: Momentary Correctness
|
|
8670
|
+
Rules that test "is this accessible right now?" fail because:
|
|
8671
|
+
- They miss conditional violations
|
|
8672
|
+
- They don't account for state changes
|
|
8673
|
+
- They create false negatives/positives
|
|
8674
|
+
|
|
8675
|
+
### Better Approach: Coding Hygiene Rules
|
|
8676
|
+
|
|
8677
|
+
Your SVG example illustrates this perfectly:
|
|
8678
|
+
**Poor hygiene**: SVG lacks accessible name, relies on ancestor being hidden
|
|
8679
|
+
**Good hygiene**: SVG has accessible name regardless of visibility state
|
|
8680
|
+
|
|
8681
|
+
### Hygiene-Based Rule Examples
|
|
8682
|
+
|
|
8683
|
+
1. **SVG Naming Rule**: All SVG elements must have accessible names, regardless of ancestor visibility
|
|
8684
|
+
2. **State Independence Rule**: Element accessibility must not depend on CSS display properties
|
|
8685
|
+
3. **Consistency Rule**: If an element can become visible, it must be accessible in all states
|
|
8686
|
+
|
|
8687
|
+
## Benefits of Hygiene-Based Rules
|
|
8688
|
+
|
|
8689
|
+
1. **Prevention-focused**: Catch issues before they cause accessibility problems
|
|
8690
|
+
2. **State-agnostic**: Don't depend on specific page states during testing
|
|
8691
|
+
3. **Developer-friendly**: Clear coding standards rather than conditional requirements
|
|
8692
|
+
4. **Reduced false positives**: Rules apply consistently regardless of testing conditions
|
|
8693
|
+
|
|
8694
|
+
## Implementation Strategy
|
|
8695
|
+
|
|
8696
|
+
**Testaro Enhancement**:
|
|
8697
|
+
- Add state isolation procedures
|
|
8698
|
+
- Implement multi-state testing workflows
|
|
8699
|
+
- Develop hygiene-focused rule sets
|
|
8700
|
+
- Create modal dialog detection and testing
|
|
8701
|
+
|
|
8702
|
+
**AI Agent Evolution**:
|
|
8703
|
+
- Recognize state-dependent patterns
|
|
8704
|
+
- Test elements across multiple states
|
|
8705
|
+
- Apply hygiene-based rule evaluation
|
|
8706
|
+
- Predict potential accessibility failures
|
|
8707
|
+
|
|
8708
|
+
## Conclusion
|
|
8709
|
+
|
|
8710
|
+
Your investigation reveals that **current accessibility testing is fundamentally incomplete**. The solution requires both technical improvements (state management) and philosophical shifts (hygiene-based rules). This significantly raises the complexity bar but also the potential effectiveness of automated accessibility testing.
|
package/package.json
CHANGED
package/tests/axe.js
CHANGED
|
@@ -134,6 +134,45 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
134
134
|
});
|
|
135
135
|
// If standard results are to be reported and there are any suspicions:
|
|
136
136
|
if (standard && (totals.rulesViolated || totals.rulesWarned)) {
|
|
137
|
+
// Resolve each suspected element's FULL data-xpath from the live DOM,
|
|
138
|
+
// keyed by its axe target. axe-core truncates node.html (~300 chars,
|
|
139
|
+
// appends '...'), so parsing data-xpath out of node.html yields a
|
|
140
|
+
// truncated XPath for elements with long attribute lists. node.target
|
|
141
|
+
// is a reliable CSS selector; read the injected data-xpath directly off
|
|
142
|
+
// the element instead. Falls back to the node.html parse on any failure.
|
|
143
|
+
const fullXPathByTargetKey = {};
|
|
144
|
+
try {
|
|
145
|
+
const suspectNodes = ['incomplete', 'violations']
|
|
146
|
+
.filter(certainty => nativeResult?.details?.[certainty])
|
|
147
|
+
.flatMap(certainty => nativeResult.details[certainty].flatMap(rule => rule.nodes));
|
|
148
|
+
const targets = suspectNodes.map(node => node.target);
|
|
149
|
+
const resolvedXPaths = await page.evaluate(targetList => targetList.map(target => {
|
|
150
|
+
try {
|
|
151
|
+
// axe target is an array of selectors (nested arrays for frames/
|
|
152
|
+
// shadow roots). Use the deepest plain-string selector for the
|
|
153
|
+
// common, non-framed case; skip otherwise.
|
|
154
|
+
const selector = Array.isArray(target)
|
|
155
|
+
? (typeof target[target.length - 1] === 'string' ? target[target.length - 1] : null)
|
|
156
|
+
: (typeof target === 'string' ? target : null);
|
|
157
|
+
if (! selector) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const element = document.querySelector(selector);
|
|
161
|
+
return element ? element.getAttribute('data-xpath') : null;
|
|
162
|
+
}
|
|
163
|
+
catch(error) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}), targets);
|
|
167
|
+
suspectNodes.forEach((node, index) => {
|
|
168
|
+
if (resolvedXPaths[index]) {
|
|
169
|
+
fullXPathByTargetKey[JSON.stringify(node.target)] = resolvedXPaths[index];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch(error) {
|
|
174
|
+
// Leave the map empty; every instance falls back to node.html below.
|
|
175
|
+
}
|
|
137
176
|
// For each certainty type:
|
|
138
177
|
['incomplete', 'violations'].forEach(certainty => {
|
|
139
178
|
// If there are any suspicions of this type:
|
|
@@ -153,8 +192,12 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
153
192
|
+ (certainty === 'violations' ? 2 : 0);
|
|
154
193
|
// Increment the standard total.
|
|
155
194
|
standardResult.totals[ordinalSeverity]++;
|
|
156
|
-
// Get the XPath of the suspected element from its data-xpath
|
|
157
|
-
|
|
195
|
+
// Get the XPath of the suspected element from its data-xpath
|
|
196
|
+
// attribute. Prefer the full value resolved from the live DOM
|
|
197
|
+
// (above); fall back to parsing it out of axe's node.html,
|
|
198
|
+
// which axe truncates and can corrupt the XPath.
|
|
199
|
+
const xPath = fullXPathByTargetKey[JSON.stringify(node.target)]
|
|
200
|
+
|| getAttributeXPath(node.html);
|
|
158
201
|
const instance = {
|
|
159
202
|
ruleID: rule.id,
|
|
160
203
|
what: Array.from(whatSet.values()).join('; '),
|