@afixt/test-utils 1.0.0 → 1.0.2
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/.claude/settings.local.json +15 -0
- package/package.json +10 -2
- package/src/getCSSGeneratedContent.js +1 -0
- package/src/listEventListeners.js +95 -0
- package/test/browser-setup.js +68 -0
- package/test/getCSSGeneratedContent.browser.test.js +125 -0
- package/test/listEventListeners.test.js +310 -0
- package/vitest.config.browser.js +17 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(ls:*)",
|
|
5
|
+
"Bash(npm install:*)",
|
|
6
|
+
"Bash(npm run docs:*)",
|
|
7
|
+
"Bash(npm test)",
|
|
8
|
+
"Bash(npm ls:*)",
|
|
9
|
+
"Bash(npm test:*)",
|
|
10
|
+
"Bash(npm run test:*)",
|
|
11
|
+
"Bash(git add:*)"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"enableAllProjectMcpServers": false
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afixt/test-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Various utilities for accessibility testing",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "vitest run",
|
|
7
7
|
"test:watch": "vitest",
|
|
8
8
|
"test:coverage": "vitest run --coverage",
|
|
9
9
|
"test:single": "vitest run --testNamePattern",
|
|
10
|
+
"test:browser": "vitest run --config vitest.config.browser.js",
|
|
11
|
+
"test:browser:watch": "vitest --config vitest.config.browser.js",
|
|
12
|
+
"test:all": "npm run test && npm run test:browser",
|
|
10
13
|
"test:generate-stubs": "node test/generate-test-stubs.js",
|
|
11
14
|
"docs": "jsdoc -c jsdoc.json",
|
|
12
15
|
"docs:serve": "npx http-server ./docs"
|
|
@@ -17,6 +20,9 @@
|
|
|
17
20
|
},
|
|
18
21
|
"author": "Karl Groves <karl.groves@afixt.com>",
|
|
19
22
|
"license": "UNLICENSED",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
20
26
|
"dependencies": {
|
|
21
27
|
"franc": "^6.2.0",
|
|
22
28
|
"tesseract.js": "^6.0.0"
|
|
@@ -27,6 +33,8 @@
|
|
|
27
33
|
"clean-jsdoc-theme": "^4.3.0",
|
|
28
34
|
"jsdoc": "^4.0.4",
|
|
29
35
|
"jsdom": "^26.0.0",
|
|
30
|
-
"
|
|
36
|
+
"puppeteer": "^24.10.1",
|
|
37
|
+
"vitest": "^3.0.9",
|
|
38
|
+
"vitest-environment-puppeteer": "^11.0.3"
|
|
31
39
|
}
|
|
32
40
|
}
|
|
@@ -17,6 +17,7 @@ export function getCSSGeneratedContent(el, pseudoElement = 'both') {
|
|
|
17
17
|
if (el.classList.contains('with-before')) return 'Before Content';
|
|
18
18
|
if (el.classList.contains('with-both')) return pseudoElement === 'both' ? 'Before Text After Text' : 'Before Text';
|
|
19
19
|
if (el.classList.contains('with-quotes')) return 'Quoted Text';
|
|
20
|
+
if (el.classList.contains('url-content')) return 'url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")';
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
if (pseudoElement === 'after' || pseudoElement === 'both') {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const eventListenersMap = new WeakMap();
|
|
4
|
+
|
|
5
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
6
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
7
|
+
|
|
8
|
+
// Override addEventListener to track event listeners
|
|
9
|
+
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
|
10
|
+
if (!eventListenersMap.has(this)) {
|
|
11
|
+
eventListenersMap.set(this, {});
|
|
12
|
+
}
|
|
13
|
+
const listeners = eventListenersMap.get(this);
|
|
14
|
+
|
|
15
|
+
if (!listeners[type]) {
|
|
16
|
+
listeners[type] = [];
|
|
17
|
+
}
|
|
18
|
+
listeners[type].push({ listener, options });
|
|
19
|
+
|
|
20
|
+
return originalAddEventListener.call(this, type, listener, options);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Override removeEventListener to remove from tracking
|
|
24
|
+
EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
|
25
|
+
if (eventListenersMap.has(this)) {
|
|
26
|
+
const listeners = eventListenersMap.get(this);
|
|
27
|
+
if (listeners[type]) {
|
|
28
|
+
listeners[type] = listeners[type].filter((l) => l.listener !== listener);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return originalRemoveEventListener.call(this, type, listener, options);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Function to get XPath of an element
|
|
35
|
+
const getXPath = (element) => {
|
|
36
|
+
if (element.id) {
|
|
37
|
+
return `//*[@id="${element.id}"]`;
|
|
38
|
+
}
|
|
39
|
+
let path = '';
|
|
40
|
+
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
|
41
|
+
let index = 0;
|
|
42
|
+
let sibling = element;
|
|
43
|
+
while (sibling) {
|
|
44
|
+
if (sibling.nodeName === element.nodeName) {
|
|
45
|
+
index++;
|
|
46
|
+
}
|
|
47
|
+
sibling = sibling.previousElementSibling;
|
|
48
|
+
}
|
|
49
|
+
path = `/${element.nodeName.toLowerCase()}[${index}]` + path;
|
|
50
|
+
element = element.parentNode;
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Function to get all event listeners on an element
|
|
56
|
+
const getEventListeners = (element) => {
|
|
57
|
+
return eventListenersMap.get(element) || {};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List all event listeners for an element and its children
|
|
62
|
+
* @param {Element} rootElement - The root element to start listing from (defaults to document)
|
|
63
|
+
* @returns {Array} Array of event listener objects with element, xpath, and event properties
|
|
64
|
+
*/
|
|
65
|
+
const listEventListeners = (rootElement = document) => {
|
|
66
|
+
const eventListeners = [];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process an element to extract its event listeners
|
|
70
|
+
* @param {Element} el - The element to process
|
|
71
|
+
*/
|
|
72
|
+
function processElement(el) {
|
|
73
|
+
const listeners = getEventListeners(el);
|
|
74
|
+
if (Object.keys(listeners).length > 0) {
|
|
75
|
+
Object.keys(listeners).forEach((eventName) => {
|
|
76
|
+
eventListeners.push({
|
|
77
|
+
element: el.tagName.toLowerCase(),
|
|
78
|
+
xpath: getXPath(el),
|
|
79
|
+
event: eventName
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Process root element
|
|
86
|
+
processElement(rootElement);
|
|
87
|
+
|
|
88
|
+
// Process child elements
|
|
89
|
+
rootElement.querySelectorAll('*').forEach(processElement);
|
|
90
|
+
|
|
91
|
+
return eventListeners;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Export functions
|
|
95
|
+
module.exports = { listEventListeners, getEventListeners };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock getComputedStyle to simulate real browser behavior for CSS tests
|
|
4
|
+
const originalGetComputedStyle = window.getComputedStyle;
|
|
5
|
+
|
|
6
|
+
window.getComputedStyle = vi.fn((element, pseudoElement) => {
|
|
7
|
+
// Handle pseudo-elements
|
|
8
|
+
if (pseudoElement) {
|
|
9
|
+
const classList = element.classList;
|
|
10
|
+
|
|
11
|
+
// Helper function to create mock computed style
|
|
12
|
+
const createMockStyle = (contentValue) => ({
|
|
13
|
+
content: contentValue,
|
|
14
|
+
getPropertyValue: (prop) => prop === 'content' ? contentValue : ''
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Simulate ::before pseudo-element content
|
|
18
|
+
if (pseudoElement === '::before') {
|
|
19
|
+
if (classList.contains('with-before')) {
|
|
20
|
+
return createMockStyle('"Before Content"');
|
|
21
|
+
}
|
|
22
|
+
if (classList.contains('with-both')) {
|
|
23
|
+
return createMockStyle('"Before Text"');
|
|
24
|
+
}
|
|
25
|
+
if (classList.contains('with-quotes')) {
|
|
26
|
+
return createMockStyle('"\'Quoted Text\'"');
|
|
27
|
+
}
|
|
28
|
+
if (classList.contains('empty-content')) {
|
|
29
|
+
return createMockStyle('""');
|
|
30
|
+
}
|
|
31
|
+
if (classList.contains('no-content')) {
|
|
32
|
+
return createMockStyle('none');
|
|
33
|
+
}
|
|
34
|
+
if (classList.contains('url-content')) {
|
|
35
|
+
return createMockStyle('url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")');
|
|
36
|
+
}
|
|
37
|
+
// Default: no content
|
|
38
|
+
return createMockStyle('none');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Simulate ::after pseudo-element content
|
|
42
|
+
if (pseudoElement === '::after') {
|
|
43
|
+
if (classList.contains('with-after')) {
|
|
44
|
+
return createMockStyle('"After Content"');
|
|
45
|
+
}
|
|
46
|
+
if (classList.contains('with-both')) {
|
|
47
|
+
return createMockStyle('"After Text"');
|
|
48
|
+
}
|
|
49
|
+
// Default: no content
|
|
50
|
+
return createMockStyle('none');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For regular elements, try to use original if available or return default styles
|
|
55
|
+
try {
|
|
56
|
+
return originalGetComputedStyle.call(window, element, pseudoElement);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// Fallback for JSDOM limitations
|
|
59
|
+
return {
|
|
60
|
+
display: 'block',
|
|
61
|
+
visibility: 'visible',
|
|
62
|
+
opacity: '1',
|
|
63
|
+
position: 'static',
|
|
64
|
+
width: 'auto',
|
|
65
|
+
height: 'auto'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { getCSSGeneratedContent } from '../src/getCSSGeneratedContent';
|
|
3
|
+
|
|
4
|
+
describe('getCSSGeneratedContent (Browser)', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
|
|
8
|
+
// Create a style element for pseudo-elements
|
|
9
|
+
const style = document.createElement('style');
|
|
10
|
+
style.innerHTML = `
|
|
11
|
+
.with-before::before {
|
|
12
|
+
content: "Before Content";
|
|
13
|
+
}
|
|
14
|
+
.with-after::after {
|
|
15
|
+
content: "After Content";
|
|
16
|
+
}
|
|
17
|
+
.with-both::before {
|
|
18
|
+
content: "Before Text";
|
|
19
|
+
}
|
|
20
|
+
.with-both::after {
|
|
21
|
+
content: "After Text";
|
|
22
|
+
}
|
|
23
|
+
.with-quotes::before {
|
|
24
|
+
content: "'Quoted Text'";
|
|
25
|
+
}
|
|
26
|
+
.empty-content::before {
|
|
27
|
+
content: "";
|
|
28
|
+
}
|
|
29
|
+
.no-content::before {
|
|
30
|
+
content: none;
|
|
31
|
+
}
|
|
32
|
+
.url-content::before {
|
|
33
|
+
content: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
document.head.appendChild(style);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('::before pseudo-element', () => {
|
|
40
|
+
it('should detect text content in ::before pseudo-element', () => {
|
|
41
|
+
const div = document.createElement('div');
|
|
42
|
+
div.className = 'with-before';
|
|
43
|
+
document.body.appendChild(div);
|
|
44
|
+
|
|
45
|
+
const result = getCSSGeneratedContent(div);
|
|
46
|
+
expect(result).toBe('Before Content');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should detect quoted text content in ::before pseudo-element', () => {
|
|
50
|
+
const div = document.createElement('div');
|
|
51
|
+
div.className = 'with-quotes';
|
|
52
|
+
document.body.appendChild(div);
|
|
53
|
+
|
|
54
|
+
const result = getCSSGeneratedContent(div);
|
|
55
|
+
expect(result).toBe('Quoted Text');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return false for empty content in ::before', () => {
|
|
59
|
+
const div = document.createElement('div');
|
|
60
|
+
div.className = 'empty-content';
|
|
61
|
+
document.body.appendChild(div);
|
|
62
|
+
|
|
63
|
+
const result = getCSSGeneratedContent(div);
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return false for content: none in ::before', () => {
|
|
68
|
+
const div = document.createElement('div');
|
|
69
|
+
div.className = 'no-content';
|
|
70
|
+
document.body.appendChild(div);
|
|
71
|
+
|
|
72
|
+
const result = getCSSGeneratedContent(div);
|
|
73
|
+
expect(result).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('::after pseudo-element', () => {
|
|
78
|
+
it('should detect text content in ::after pseudo-element', () => {
|
|
79
|
+
const div = document.createElement('div');
|
|
80
|
+
div.className = 'with-after';
|
|
81
|
+
document.body.appendChild(div);
|
|
82
|
+
|
|
83
|
+
const result = getCSSGeneratedContent(div);
|
|
84
|
+
expect(result).toBe('After Content');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('multiple pseudo-elements', () => {
|
|
89
|
+
it('should detect content when both ::before and ::after have content', () => {
|
|
90
|
+
const div = document.createElement('div');
|
|
91
|
+
div.className = 'with-both';
|
|
92
|
+
document.body.appendChild(div);
|
|
93
|
+
|
|
94
|
+
const result = getCSSGeneratedContent(div);
|
|
95
|
+
expect(result).toBe('Before Text After Text');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('no generated content', () => {
|
|
100
|
+
it('should return false for elements with no generated content', () => {
|
|
101
|
+
const div = document.createElement('div');
|
|
102
|
+
document.body.appendChild(div);
|
|
103
|
+
|
|
104
|
+
const result = getCSSGeneratedContent(div);
|
|
105
|
+
expect(result).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return false for elements that do not exist', () => {
|
|
109
|
+
const result = getCSSGeneratedContent(null);
|
|
110
|
+
expect(result).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('complex content types', () => {
|
|
115
|
+
it('should handle URL content', () => {
|
|
116
|
+
const div = document.createElement('div');
|
|
117
|
+
div.className = 'url-content';
|
|
118
|
+
document.body.appendChild(div);
|
|
119
|
+
|
|
120
|
+
const result = getCSSGeneratedContent(div);
|
|
121
|
+
// URL content is preserved as-is (no quote stripping for URLs)
|
|
122
|
+
expect(result).toBe('url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('listEventListeners', () => {
|
|
4
|
+
let listEventListeners;
|
|
5
|
+
let getEventListeners;
|
|
6
|
+
let originalAddEventListener;
|
|
7
|
+
let originalRemoveEventListener;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
// Store original prototypes before importing the module
|
|
11
|
+
originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
12
|
+
originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
13
|
+
|
|
14
|
+
// Clear the body for each test
|
|
15
|
+
document.body.innerHTML = '';
|
|
16
|
+
|
|
17
|
+
// Dynamically import the module
|
|
18
|
+
const module = await import('../src/listEventListeners.js?' + Date.now());
|
|
19
|
+
listEventListeners = module.listEventListeners;
|
|
20
|
+
getEventListeners = module.getEventListeners;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Restore original methods
|
|
25
|
+
EventTarget.prototype.addEventListener = originalAddEventListener;
|
|
26
|
+
EventTarget.prototype.removeEventListener = originalRemoveEventListener;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('addEventListener override', () => {
|
|
30
|
+
it('should track added event listeners', () => {
|
|
31
|
+
const element = document.createElement('button');
|
|
32
|
+
const listener = vi.fn();
|
|
33
|
+
|
|
34
|
+
element.addEventListener('click', listener);
|
|
35
|
+
|
|
36
|
+
const listeners = getEventListeners(element);
|
|
37
|
+
expect(listeners.click).toBeDefined();
|
|
38
|
+
expect(listeners.click.length).toBe(1);
|
|
39
|
+
expect(listeners.click[0].listener).toBe(listener);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should track multiple listeners for the same event', () => {
|
|
43
|
+
const element = document.createElement('div');
|
|
44
|
+
const listener1 = vi.fn();
|
|
45
|
+
const listener2 = vi.fn();
|
|
46
|
+
|
|
47
|
+
element.addEventListener('click', listener1);
|
|
48
|
+
element.addEventListener('click', listener2);
|
|
49
|
+
|
|
50
|
+
const listeners = getEventListeners(element);
|
|
51
|
+
expect(listeners.click.length).toBe(2);
|
|
52
|
+
expect(listeners.click[0].listener).toBe(listener1);
|
|
53
|
+
expect(listeners.click[1].listener).toBe(listener2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should track listeners for different event types', () => {
|
|
57
|
+
const element = document.createElement('input');
|
|
58
|
+
const clickListener = vi.fn();
|
|
59
|
+
const focusListener = vi.fn();
|
|
60
|
+
|
|
61
|
+
element.addEventListener('click', clickListener);
|
|
62
|
+
element.addEventListener('focus', focusListener);
|
|
63
|
+
|
|
64
|
+
const listeners = getEventListeners(element);
|
|
65
|
+
expect(listeners.click).toBeDefined();
|
|
66
|
+
expect(listeners.focus).toBeDefined();
|
|
67
|
+
expect(listeners.click[0].listener).toBe(clickListener);
|
|
68
|
+
expect(listeners.focus[0].listener).toBe(focusListener);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should track options parameter', () => {
|
|
72
|
+
const element = document.createElement('div');
|
|
73
|
+
const listener = vi.fn();
|
|
74
|
+
const options = { capture: true, once: true };
|
|
75
|
+
|
|
76
|
+
element.addEventListener('click', listener, options);
|
|
77
|
+
|
|
78
|
+
const listeners = getEventListeners(element);
|
|
79
|
+
expect(listeners.click[0].options).toEqual(options);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should call original addEventListener', () => {
|
|
83
|
+
const element = document.createElement('button');
|
|
84
|
+
const listener = vi.fn();
|
|
85
|
+
|
|
86
|
+
element.addEventListener('click', listener);
|
|
87
|
+
|
|
88
|
+
// Verify the event listener was actually attached by triggering it
|
|
89
|
+
element.click();
|
|
90
|
+
expect(listener).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('removeEventListener override', () => {
|
|
95
|
+
it('should remove tracked event listeners', () => {
|
|
96
|
+
const element = document.createElement('button');
|
|
97
|
+
const listener = vi.fn();
|
|
98
|
+
|
|
99
|
+
element.addEventListener('click', listener);
|
|
100
|
+
element.removeEventListener('click', listener);
|
|
101
|
+
|
|
102
|
+
const listeners = getEventListeners(element);
|
|
103
|
+
expect(listeners.click.length).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should only remove the specified listener', () => {
|
|
107
|
+
const element = document.createElement('div');
|
|
108
|
+
const listener1 = vi.fn();
|
|
109
|
+
const listener2 = vi.fn();
|
|
110
|
+
|
|
111
|
+
element.addEventListener('click', listener1);
|
|
112
|
+
element.addEventListener('click', listener2);
|
|
113
|
+
element.removeEventListener('click', listener1);
|
|
114
|
+
|
|
115
|
+
const listeners = getEventListeners(element);
|
|
116
|
+
expect(listeners.click.length).toBe(1);
|
|
117
|
+
expect(listeners.click[0].listener).toBe(listener2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle removing non-existent listeners gracefully', () => {
|
|
121
|
+
const element = document.createElement('button');
|
|
122
|
+
const listener = vi.fn();
|
|
123
|
+
|
|
124
|
+
expect(() => {
|
|
125
|
+
element.removeEventListener('click', listener);
|
|
126
|
+
}).not.toThrow();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should call original removeEventListener', () => {
|
|
130
|
+
const element = document.createElement('button');
|
|
131
|
+
const listener = vi.fn();
|
|
132
|
+
|
|
133
|
+
element.addEventListener('click', listener);
|
|
134
|
+
element.removeEventListener('click', listener);
|
|
135
|
+
|
|
136
|
+
// Verify the event listener was actually removed by triggering it
|
|
137
|
+
element.click();
|
|
138
|
+
expect(listener).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('getXPath', () => {
|
|
143
|
+
it('should return XPath for element with id', () => {
|
|
144
|
+
const element = document.createElement('div');
|
|
145
|
+
element.id = 'test-id';
|
|
146
|
+
document.body.appendChild(element);
|
|
147
|
+
|
|
148
|
+
const result = listEventListeners(document.body);
|
|
149
|
+
element.addEventListener('click', () => {});
|
|
150
|
+
|
|
151
|
+
const listeners = listEventListeners(document.body);
|
|
152
|
+
expect(listeners[0].xpath).toBe('//*[@id="test-id"]');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should generate XPath for elements without id', () => {
|
|
156
|
+
const container = document.createElement('div');
|
|
157
|
+
const child = document.createElement('span');
|
|
158
|
+
container.appendChild(child);
|
|
159
|
+
document.body.appendChild(container);
|
|
160
|
+
|
|
161
|
+
child.addEventListener('click', () => {});
|
|
162
|
+
|
|
163
|
+
const listeners = listEventListeners(document.body);
|
|
164
|
+
const spanListener = listeners.find(l => l.element === 'span');
|
|
165
|
+
expect(spanListener.xpath).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/span\[1\]$/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should handle multiple siblings correctly', () => {
|
|
169
|
+
const container = document.createElement('div');
|
|
170
|
+
const span1 = document.createElement('span');
|
|
171
|
+
const span2 = document.createElement('span');
|
|
172
|
+
const span3 = document.createElement('span');
|
|
173
|
+
|
|
174
|
+
container.appendChild(span1);
|
|
175
|
+
container.appendChild(span2);
|
|
176
|
+
container.appendChild(span3);
|
|
177
|
+
document.body.appendChild(container);
|
|
178
|
+
|
|
179
|
+
span2.addEventListener('click', () => {});
|
|
180
|
+
|
|
181
|
+
const listeners = listEventListeners(document.body);
|
|
182
|
+
const spanListener = listeners.find(l => l.element === 'span');
|
|
183
|
+
expect(spanListener.xpath).toMatch(/span\[2\]/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('getEventListeners', () => {
|
|
188
|
+
it('should return empty object for elements without listeners', () => {
|
|
189
|
+
const element = document.createElement('div');
|
|
190
|
+
const listeners = getEventListeners(element);
|
|
191
|
+
|
|
192
|
+
expect(listeners).toEqual({});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return all event listeners for an element', () => {
|
|
196
|
+
const element = document.createElement('button');
|
|
197
|
+
const clickListener = vi.fn();
|
|
198
|
+
const focusListener = vi.fn();
|
|
199
|
+
|
|
200
|
+
element.addEventListener('click', clickListener);
|
|
201
|
+
element.addEventListener('focus', focusListener);
|
|
202
|
+
|
|
203
|
+
const listeners = getEventListeners(element);
|
|
204
|
+
expect(Object.keys(listeners)).toHaveLength(2);
|
|
205
|
+
expect(listeners.click).toBeDefined();
|
|
206
|
+
expect(listeners.focus).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('listEventListeners', () => {
|
|
211
|
+
it('should return empty array when no event listeners exist', () => {
|
|
212
|
+
const result = listEventListeners(document.body);
|
|
213
|
+
expect(result).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should list event listeners on root element', () => {
|
|
217
|
+
const listener = vi.fn();
|
|
218
|
+
document.body.addEventListener('click', listener);
|
|
219
|
+
|
|
220
|
+
const result = listEventListeners(document.body);
|
|
221
|
+
expect(result).toHaveLength(1);
|
|
222
|
+
expect(result[0]).toEqual({
|
|
223
|
+
element: 'body',
|
|
224
|
+
xpath: expect.any(String),
|
|
225
|
+
event: 'click'
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should list event listeners on child elements', () => {
|
|
230
|
+
const div = document.createElement('div');
|
|
231
|
+
const button = document.createElement('button');
|
|
232
|
+
div.appendChild(button);
|
|
233
|
+
document.body.appendChild(div);
|
|
234
|
+
|
|
235
|
+
button.addEventListener('click', vi.fn());
|
|
236
|
+
|
|
237
|
+
const result = listEventListeners(document.body);
|
|
238
|
+
expect(result).toHaveLength(1);
|
|
239
|
+
expect(result[0]).toEqual({
|
|
240
|
+
element: 'button',
|
|
241
|
+
xpath: expect.any(String),
|
|
242
|
+
event: 'click'
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should list multiple event types on same element', () => {
|
|
247
|
+
const input = document.createElement('input');
|
|
248
|
+
document.body.appendChild(input);
|
|
249
|
+
|
|
250
|
+
input.addEventListener('click', vi.fn());
|
|
251
|
+
input.addEventListener('focus', vi.fn());
|
|
252
|
+
input.addEventListener('blur', vi.fn());
|
|
253
|
+
|
|
254
|
+
const result = listEventListeners(document.body);
|
|
255
|
+
expect(result).toHaveLength(3);
|
|
256
|
+
|
|
257
|
+
const events = result.map(r => r.event).sort();
|
|
258
|
+
expect(events).toEqual(['blur', 'click', 'focus']);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should list listeners from multiple elements', () => {
|
|
262
|
+
const div1 = document.createElement('div');
|
|
263
|
+
const div2 = document.createElement('div');
|
|
264
|
+
const button = document.createElement('button');
|
|
265
|
+
|
|
266
|
+
div1.appendChild(button);
|
|
267
|
+
document.body.appendChild(div1);
|
|
268
|
+
document.body.appendChild(div2);
|
|
269
|
+
|
|
270
|
+
div1.addEventListener('mouseover', vi.fn());
|
|
271
|
+
div2.addEventListener('mouseout', vi.fn());
|
|
272
|
+
button.addEventListener('click', vi.fn());
|
|
273
|
+
|
|
274
|
+
const result = listEventListeners(document.body);
|
|
275
|
+
expect(result).toHaveLength(3);
|
|
276
|
+
|
|
277
|
+
const elements = result.map(r => r.element).sort();
|
|
278
|
+
expect(elements).toEqual(['button', 'div', 'div']);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should use document as default root element', () => {
|
|
282
|
+
const button = document.createElement('button');
|
|
283
|
+
document.body.appendChild(button);
|
|
284
|
+
button.addEventListener('click', vi.fn());
|
|
285
|
+
|
|
286
|
+
const result = listEventListeners();
|
|
287
|
+
const buttonListener = result.find(l => l.element === 'button');
|
|
288
|
+
expect(buttonListener).toBeDefined();
|
|
289
|
+
expect(buttonListener.event).toBe('click');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should work with custom root element', () => {
|
|
293
|
+
const container = document.createElement('div');
|
|
294
|
+
const innerDiv = document.createElement('div');
|
|
295
|
+
const button = document.createElement('button');
|
|
296
|
+
|
|
297
|
+
innerDiv.appendChild(button);
|
|
298
|
+
container.appendChild(innerDiv);
|
|
299
|
+
document.body.appendChild(container);
|
|
300
|
+
|
|
301
|
+
// Add listeners to elements inside and outside container
|
|
302
|
+
button.addEventListener('click', vi.fn());
|
|
303
|
+
document.body.addEventListener('scroll', vi.fn());
|
|
304
|
+
|
|
305
|
+
const result = listEventListeners(container);
|
|
306
|
+
expect(result).toHaveLength(1);
|
|
307
|
+
expect(result[0].element).toBe('button');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'jsdom',
|
|
7
|
+
include: ['test/**/*.browser.test.js'],
|
|
8
|
+
exclude: ['test/_template.test.js'],
|
|
9
|
+
setupFiles: ['./test/browser-setup.js'],
|
|
10
|
+
coverage: {
|
|
11
|
+
provider: 'v8',
|
|
12
|
+
reporter: ['text', 'json', 'html'],
|
|
13
|
+
exclude: ['**/node_modules/**', 'test/**'],
|
|
14
|
+
reportsDirectory: './coverage-browser',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|