@eyeglass/inspector 0.1.0
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/dist/fiber-walker.d.ts +15 -0
- package/dist/fiber-walker.js +223 -0
- package/dist/fiber-walker.test.d.ts +1 -0
- package/dist/fiber-walker.test.js +252 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +33 -0
- package/dist/inspector.d.ts +83 -0
- package/dist/inspector.js +1932 -0
- package/dist/snapshot.d.ts +8 -0
- package/dist/snapshot.js +192 -0
- package/dist/snapshot.test.d.ts +1 -0
- package/dist/snapshot.test.js +283 -0
- package/package.json +38 -0
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captures a semantic snapshot of a DOM element
|
|
3
|
+
*/
|
|
4
|
+
import { extractFrameworkInfo } from './fiber-walker.js';
|
|
5
|
+
/**
|
|
6
|
+
* Get computed accessibility properties
|
|
7
|
+
*/
|
|
8
|
+
function getA11yInfo(element) {
|
|
9
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
10
|
+
const ariaDescribedBy = element.getAttribute('aria-describedby');
|
|
11
|
+
const ariaDisabled = element.getAttribute('aria-disabled');
|
|
12
|
+
const ariaExpanded = element.getAttribute('aria-expanded');
|
|
13
|
+
const ariaChecked = element.getAttribute('aria-checked');
|
|
14
|
+
const ariaHidden = element.getAttribute('aria-hidden');
|
|
15
|
+
// Get description from aria-describedby if it points to an element
|
|
16
|
+
let description = null;
|
|
17
|
+
if (ariaDescribedBy) {
|
|
18
|
+
const descElement = document.getElementById(ariaDescribedBy);
|
|
19
|
+
description = descElement?.textContent?.trim() || null;
|
|
20
|
+
}
|
|
21
|
+
// Check if element is disabled
|
|
22
|
+
const disabled = ariaDisabled === 'true' ||
|
|
23
|
+
element.disabled ||
|
|
24
|
+
element.hasAttribute('disabled');
|
|
25
|
+
return {
|
|
26
|
+
label: ariaLabel || element.getAttribute('title') || null,
|
|
27
|
+
description,
|
|
28
|
+
disabled,
|
|
29
|
+
expanded: ariaExpanded ? ariaExpanded === 'true' : undefined,
|
|
30
|
+
checked: ariaChecked === 'true'
|
|
31
|
+
? true
|
|
32
|
+
: ariaChecked === 'false'
|
|
33
|
+
? false
|
|
34
|
+
: ariaChecked === 'mixed'
|
|
35
|
+
? 'mixed'
|
|
36
|
+
: undefined,
|
|
37
|
+
hidden: ariaHidden === 'true' || element.hidden || false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get geometry information
|
|
42
|
+
*/
|
|
43
|
+
function getGeometry(element) {
|
|
44
|
+
const rect = element.getBoundingClientRect();
|
|
45
|
+
return {
|
|
46
|
+
x: Math.round(rect.x),
|
|
47
|
+
y: Math.round(rect.y),
|
|
48
|
+
width: Math.round(rect.width),
|
|
49
|
+
height: Math.round(rect.height),
|
|
50
|
+
visible: rect.width > 0 && rect.height > 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get targeted computed styles
|
|
55
|
+
*/
|
|
56
|
+
function getStyles(element) {
|
|
57
|
+
const computed = getComputedStyle(element);
|
|
58
|
+
return {
|
|
59
|
+
display: computed.display,
|
|
60
|
+
position: computed.position,
|
|
61
|
+
flexDirection: computed.flexDirection !== 'row' ? computed.flexDirection : undefined,
|
|
62
|
+
gridTemplate: computed.display === 'grid'
|
|
63
|
+
? `${computed.gridTemplateColumns} / ${computed.gridTemplateRows}`
|
|
64
|
+
: undefined,
|
|
65
|
+
padding: computed.padding,
|
|
66
|
+
margin: computed.margin,
|
|
67
|
+
color: computed.color,
|
|
68
|
+
backgroundColor: computed.backgroundColor,
|
|
69
|
+
fontFamily: computed.fontFamily,
|
|
70
|
+
zIndex: computed.zIndex,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the accessible role of an element
|
|
75
|
+
*/
|
|
76
|
+
function getRole(element) {
|
|
77
|
+
// Explicit role takes precedence
|
|
78
|
+
const explicitRole = element.getAttribute('role');
|
|
79
|
+
if (explicitRole)
|
|
80
|
+
return explicitRole;
|
|
81
|
+
// Implicit roles based on tag
|
|
82
|
+
const tag = element.tagName.toLowerCase();
|
|
83
|
+
const roleMap = {
|
|
84
|
+
a: 'link',
|
|
85
|
+
button: 'button',
|
|
86
|
+
input: element.type || 'textbox',
|
|
87
|
+
select: 'combobox',
|
|
88
|
+
textarea: 'textbox',
|
|
89
|
+
img: 'img',
|
|
90
|
+
nav: 'navigation',
|
|
91
|
+
main: 'main',
|
|
92
|
+
header: 'banner',
|
|
93
|
+
footer: 'contentinfo',
|
|
94
|
+
aside: 'complementary',
|
|
95
|
+
article: 'article',
|
|
96
|
+
section: 'region',
|
|
97
|
+
form: 'form',
|
|
98
|
+
ul: 'list',
|
|
99
|
+
ol: 'list',
|
|
100
|
+
li: 'listitem',
|
|
101
|
+
table: 'table',
|
|
102
|
+
tr: 'row',
|
|
103
|
+
td: 'cell',
|
|
104
|
+
th: 'columnheader',
|
|
105
|
+
dialog: 'dialog',
|
|
106
|
+
h1: 'heading',
|
|
107
|
+
h2: 'heading',
|
|
108
|
+
h3: 'heading',
|
|
109
|
+
h4: 'heading',
|
|
110
|
+
h5: 'heading',
|
|
111
|
+
h6: 'heading',
|
|
112
|
+
};
|
|
113
|
+
return roleMap[tag] || 'generic';
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get the accessible name of an element
|
|
117
|
+
*/
|
|
118
|
+
function getAccessibleName(element) {
|
|
119
|
+
// aria-label
|
|
120
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
121
|
+
if (ariaLabel)
|
|
122
|
+
return ariaLabel;
|
|
123
|
+
// aria-labelledby
|
|
124
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
125
|
+
if (labelledBy) {
|
|
126
|
+
const labelElement = document.getElementById(labelledBy);
|
|
127
|
+
if (labelElement)
|
|
128
|
+
return labelElement.textContent?.trim() || '';
|
|
129
|
+
}
|
|
130
|
+
// For inputs, check associated label
|
|
131
|
+
if (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') {
|
|
132
|
+
const id = element.getAttribute('id');
|
|
133
|
+
if (id) {
|
|
134
|
+
const label = document.querySelector(`label[for="${id}"]`);
|
|
135
|
+
if (label)
|
|
136
|
+
return label.textContent?.trim() || '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// For images, use alt
|
|
140
|
+
if (element.tagName === 'IMG') {
|
|
141
|
+
return element.alt || '';
|
|
142
|
+
}
|
|
143
|
+
// Text content (truncated)
|
|
144
|
+
const text = element.textContent?.trim() || '';
|
|
145
|
+
return text.length > 50 ? text.slice(0, 50) + '...' : text;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get element identifiers (id, className, data-* attributes)
|
|
149
|
+
*/
|
|
150
|
+
function getElementIdentifiers(element) {
|
|
151
|
+
const result = {};
|
|
152
|
+
// Get id
|
|
153
|
+
const id = element.getAttribute('id');
|
|
154
|
+
if (id) {
|
|
155
|
+
result.id = id;
|
|
156
|
+
}
|
|
157
|
+
// Get class names
|
|
158
|
+
const className = element.getAttribute('class');
|
|
159
|
+
if (className?.trim()) {
|
|
160
|
+
result.className = className.trim();
|
|
161
|
+
}
|
|
162
|
+
// Get data-* attributes
|
|
163
|
+
const dataAttrs = {};
|
|
164
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
165
|
+
const attr = element.attributes[i];
|
|
166
|
+
if (attr.name.startsWith('data-')) {
|
|
167
|
+
dataAttrs[attr.name] = attr.value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (Object.keys(dataAttrs).length > 0) {
|
|
171
|
+
result.dataAttributes = dataAttrs;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Capture a complete semantic snapshot of an element
|
|
177
|
+
*/
|
|
178
|
+
export function captureSnapshot(element) {
|
|
179
|
+
const identifiers = getElementIdentifiers(element);
|
|
180
|
+
return {
|
|
181
|
+
role: getRole(element),
|
|
182
|
+
name: getAccessibleName(element),
|
|
183
|
+
tagName: element.tagName.toLowerCase(),
|
|
184
|
+
...identifiers,
|
|
185
|
+
framework: extractFrameworkInfo(element),
|
|
186
|
+
a11y: getA11yInfo(element),
|
|
187
|
+
geometry: getGeometry(element),
|
|
188
|
+
styles: getStyles(element),
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
url: window.location.href,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { captureSnapshot } from './snapshot.js';
|
|
3
|
+
describe('captureSnapshot', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
document.body.innerHTML = '';
|
|
6
|
+
});
|
|
7
|
+
describe('basic element info', () => {
|
|
8
|
+
it('should capture tag name', () => {
|
|
9
|
+
const button = document.createElement('button');
|
|
10
|
+
document.body.appendChild(button);
|
|
11
|
+
const snapshot = captureSnapshot(button);
|
|
12
|
+
expect(snapshot.tagName).toBe('button');
|
|
13
|
+
});
|
|
14
|
+
it('should capture text content as name', () => {
|
|
15
|
+
const button = document.createElement('button');
|
|
16
|
+
button.textContent = 'Click me';
|
|
17
|
+
document.body.appendChild(button);
|
|
18
|
+
const snapshot = captureSnapshot(button);
|
|
19
|
+
expect(snapshot.name).toBe('Click me');
|
|
20
|
+
});
|
|
21
|
+
it('should truncate long text content', () => {
|
|
22
|
+
const div = document.createElement('div');
|
|
23
|
+
div.textContent = 'A'.repeat(100);
|
|
24
|
+
document.body.appendChild(div);
|
|
25
|
+
const snapshot = captureSnapshot(div);
|
|
26
|
+
expect(snapshot.name).toHaveLength(53); // 50 chars + '...'
|
|
27
|
+
expect(snapshot.name.endsWith('...')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('element identifiers', () => {
|
|
31
|
+
it('should capture id', () => {
|
|
32
|
+
const div = document.createElement('div');
|
|
33
|
+
div.id = 'my-element';
|
|
34
|
+
document.body.appendChild(div);
|
|
35
|
+
const snapshot = captureSnapshot(div);
|
|
36
|
+
expect(snapshot.id).toBe('my-element');
|
|
37
|
+
});
|
|
38
|
+
it('should capture className', () => {
|
|
39
|
+
const div = document.createElement('div');
|
|
40
|
+
div.className = 'foo bar baz';
|
|
41
|
+
document.body.appendChild(div);
|
|
42
|
+
const snapshot = captureSnapshot(div);
|
|
43
|
+
expect(snapshot.className).toBe('foo bar baz');
|
|
44
|
+
});
|
|
45
|
+
it('should capture data attributes', () => {
|
|
46
|
+
const div = document.createElement('div');
|
|
47
|
+
div.setAttribute('data-testid', 'my-test');
|
|
48
|
+
div.setAttribute('data-value', '42');
|
|
49
|
+
document.body.appendChild(div);
|
|
50
|
+
const snapshot = captureSnapshot(div);
|
|
51
|
+
expect(snapshot.dataAttributes).toEqual({
|
|
52
|
+
'data-testid': 'my-test',
|
|
53
|
+
'data-value': '42',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it('should not include empty identifiers', () => {
|
|
57
|
+
const div = document.createElement('div');
|
|
58
|
+
document.body.appendChild(div);
|
|
59
|
+
const snapshot = captureSnapshot(div);
|
|
60
|
+
expect(snapshot.id).toBeUndefined();
|
|
61
|
+
expect(snapshot.className).toBeUndefined();
|
|
62
|
+
expect(snapshot.dataAttributes).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('role detection', () => {
|
|
66
|
+
it('should use explicit role attribute', () => {
|
|
67
|
+
const div = document.createElement('div');
|
|
68
|
+
div.setAttribute('role', 'navigation');
|
|
69
|
+
document.body.appendChild(div);
|
|
70
|
+
const snapshot = captureSnapshot(div);
|
|
71
|
+
expect(snapshot.role).toBe('navigation');
|
|
72
|
+
});
|
|
73
|
+
it('should infer role from button tag', () => {
|
|
74
|
+
const button = document.createElement('button');
|
|
75
|
+
document.body.appendChild(button);
|
|
76
|
+
const snapshot = captureSnapshot(button);
|
|
77
|
+
expect(snapshot.role).toBe('button');
|
|
78
|
+
});
|
|
79
|
+
it('should infer role from link tag', () => {
|
|
80
|
+
const link = document.createElement('a');
|
|
81
|
+
document.body.appendChild(link);
|
|
82
|
+
const snapshot = captureSnapshot(link);
|
|
83
|
+
expect(snapshot.role).toBe('link');
|
|
84
|
+
});
|
|
85
|
+
it('should infer role from semantic tags', () => {
|
|
86
|
+
const nav = document.createElement('nav');
|
|
87
|
+
document.body.appendChild(nav);
|
|
88
|
+
const snapshot = captureSnapshot(nav);
|
|
89
|
+
expect(snapshot.role).toBe('navigation');
|
|
90
|
+
});
|
|
91
|
+
it('should return generic for unknown tags', () => {
|
|
92
|
+
const div = document.createElement('div');
|
|
93
|
+
document.body.appendChild(div);
|
|
94
|
+
const snapshot = captureSnapshot(div);
|
|
95
|
+
expect(snapshot.role).toBe('generic');
|
|
96
|
+
});
|
|
97
|
+
it('should detect input type as role', () => {
|
|
98
|
+
const input = document.createElement('input');
|
|
99
|
+
input.type = 'checkbox';
|
|
100
|
+
document.body.appendChild(input);
|
|
101
|
+
const snapshot = captureSnapshot(input);
|
|
102
|
+
expect(snapshot.role).toBe('checkbox');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('accessibility info', () => {
|
|
106
|
+
it('should capture aria-label', () => {
|
|
107
|
+
const button = document.createElement('button');
|
|
108
|
+
button.setAttribute('aria-label', 'Close dialog');
|
|
109
|
+
document.body.appendChild(button);
|
|
110
|
+
const snapshot = captureSnapshot(button);
|
|
111
|
+
expect(snapshot.a11y.label).toBe('Close dialog');
|
|
112
|
+
expect(snapshot.name).toBe('Close dialog');
|
|
113
|
+
});
|
|
114
|
+
it('should capture title as label fallback', () => {
|
|
115
|
+
const button = document.createElement('button');
|
|
116
|
+
button.setAttribute('title', 'Helpful tooltip');
|
|
117
|
+
document.body.appendChild(button);
|
|
118
|
+
const snapshot = captureSnapshot(button);
|
|
119
|
+
expect(snapshot.a11y.label).toBe('Helpful tooltip');
|
|
120
|
+
});
|
|
121
|
+
it('should capture aria-describedby', () => {
|
|
122
|
+
const desc = document.createElement('span');
|
|
123
|
+
desc.id = 'desc-text';
|
|
124
|
+
desc.textContent = 'This is the description';
|
|
125
|
+
document.body.appendChild(desc);
|
|
126
|
+
const button = document.createElement('button');
|
|
127
|
+
button.setAttribute('aria-describedby', 'desc-text');
|
|
128
|
+
document.body.appendChild(button);
|
|
129
|
+
const snapshot = captureSnapshot(button);
|
|
130
|
+
expect(snapshot.a11y.description).toBe('This is the description');
|
|
131
|
+
});
|
|
132
|
+
it('should capture disabled state from aria-disabled', () => {
|
|
133
|
+
const button = document.createElement('button');
|
|
134
|
+
button.setAttribute('aria-disabled', 'true');
|
|
135
|
+
document.body.appendChild(button);
|
|
136
|
+
const snapshot = captureSnapshot(button);
|
|
137
|
+
expect(snapshot.a11y.disabled).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
it('should capture disabled state from disabled attribute', () => {
|
|
140
|
+
const button = document.createElement('button');
|
|
141
|
+
button.disabled = true;
|
|
142
|
+
document.body.appendChild(button);
|
|
143
|
+
const snapshot = captureSnapshot(button);
|
|
144
|
+
expect(snapshot.a11y.disabled).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
it('should capture aria-expanded', () => {
|
|
147
|
+
const button = document.createElement('button');
|
|
148
|
+
button.setAttribute('aria-expanded', 'true');
|
|
149
|
+
document.body.appendChild(button);
|
|
150
|
+
const snapshot = captureSnapshot(button);
|
|
151
|
+
expect(snapshot.a11y.expanded).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
it('should capture aria-checked states', () => {
|
|
154
|
+
const checkbox1 = document.createElement('input');
|
|
155
|
+
checkbox1.type = 'checkbox';
|
|
156
|
+
checkbox1.setAttribute('aria-checked', 'true');
|
|
157
|
+
document.body.appendChild(checkbox1);
|
|
158
|
+
const checkbox2 = document.createElement('input');
|
|
159
|
+
checkbox2.type = 'checkbox';
|
|
160
|
+
checkbox2.setAttribute('aria-checked', 'mixed');
|
|
161
|
+
document.body.appendChild(checkbox2);
|
|
162
|
+
expect(captureSnapshot(checkbox1).a11y.checked).toBe(true);
|
|
163
|
+
expect(captureSnapshot(checkbox2).a11y.checked).toBe('mixed');
|
|
164
|
+
});
|
|
165
|
+
it('should capture hidden state', () => {
|
|
166
|
+
const div = document.createElement('div');
|
|
167
|
+
div.setAttribute('aria-hidden', 'true');
|
|
168
|
+
document.body.appendChild(div);
|
|
169
|
+
const snapshot = captureSnapshot(div);
|
|
170
|
+
expect(snapshot.a11y.hidden).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('accessible name computation', () => {
|
|
174
|
+
it('should use aria-labelledby', () => {
|
|
175
|
+
const label = document.createElement('span');
|
|
176
|
+
label.id = 'my-label';
|
|
177
|
+
label.textContent = 'Field Label';
|
|
178
|
+
document.body.appendChild(label);
|
|
179
|
+
const input = document.createElement('input');
|
|
180
|
+
input.setAttribute('aria-labelledby', 'my-label');
|
|
181
|
+
document.body.appendChild(input);
|
|
182
|
+
const snapshot = captureSnapshot(input);
|
|
183
|
+
expect(snapshot.name).toBe('Field Label');
|
|
184
|
+
});
|
|
185
|
+
it('should use associated label for inputs', () => {
|
|
186
|
+
const label = document.createElement('label');
|
|
187
|
+
label.setAttribute('for', 'my-input');
|
|
188
|
+
label.textContent = 'Email Address';
|
|
189
|
+
document.body.appendChild(label);
|
|
190
|
+
const input = document.createElement('input');
|
|
191
|
+
input.id = 'my-input';
|
|
192
|
+
document.body.appendChild(input);
|
|
193
|
+
const snapshot = captureSnapshot(input);
|
|
194
|
+
expect(snapshot.name).toBe('Email Address');
|
|
195
|
+
});
|
|
196
|
+
it('should use alt text for images', () => {
|
|
197
|
+
const img = document.createElement('img');
|
|
198
|
+
img.alt = 'Company logo';
|
|
199
|
+
document.body.appendChild(img);
|
|
200
|
+
const snapshot = captureSnapshot(img);
|
|
201
|
+
expect(snapshot.name).toBe('Company logo');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('geometry', () => {
|
|
205
|
+
it('should capture bounding rect', () => {
|
|
206
|
+
const div = document.createElement('div');
|
|
207
|
+
div.style.width = '200px';
|
|
208
|
+
div.style.height = '100px';
|
|
209
|
+
document.body.appendChild(div);
|
|
210
|
+
const snapshot = captureSnapshot(div);
|
|
211
|
+
expect(snapshot.geometry.width).toBeGreaterThanOrEqual(0);
|
|
212
|
+
expect(snapshot.geometry.height).toBeGreaterThanOrEqual(0);
|
|
213
|
+
expect(typeof snapshot.geometry.x).toBe('number');
|
|
214
|
+
expect(typeof snapshot.geometry.y).toBe('number');
|
|
215
|
+
});
|
|
216
|
+
it('should mark zero-size elements as not visible', () => {
|
|
217
|
+
const div = document.createElement('div');
|
|
218
|
+
div.style.width = '0px';
|
|
219
|
+
div.style.height = '0px';
|
|
220
|
+
document.body.appendChild(div);
|
|
221
|
+
const snapshot = captureSnapshot(div);
|
|
222
|
+
expect(snapshot.geometry.visible).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('styles', () => {
|
|
226
|
+
it('should capture display', () => {
|
|
227
|
+
const div = document.createElement('div');
|
|
228
|
+
div.style.display = 'flex';
|
|
229
|
+
document.body.appendChild(div);
|
|
230
|
+
const snapshot = captureSnapshot(div);
|
|
231
|
+
expect(snapshot.styles.display).toBe('flex');
|
|
232
|
+
});
|
|
233
|
+
it('should capture position', () => {
|
|
234
|
+
const div = document.createElement('div');
|
|
235
|
+
div.style.position = 'absolute';
|
|
236
|
+
document.body.appendChild(div);
|
|
237
|
+
const snapshot = captureSnapshot(div);
|
|
238
|
+
expect(snapshot.styles.position).toBe('absolute');
|
|
239
|
+
});
|
|
240
|
+
it('should capture flex direction when not default', () => {
|
|
241
|
+
const div = document.createElement('div');
|
|
242
|
+
div.style.display = 'flex';
|
|
243
|
+
div.style.flexDirection = 'column';
|
|
244
|
+
document.body.appendChild(div);
|
|
245
|
+
const snapshot = captureSnapshot(div);
|
|
246
|
+
expect(snapshot.styles.flexDirection).toBe('column');
|
|
247
|
+
});
|
|
248
|
+
it('should capture colors', () => {
|
|
249
|
+
const div = document.createElement('div');
|
|
250
|
+
div.style.color = 'red';
|
|
251
|
+
div.style.backgroundColor = 'blue';
|
|
252
|
+
document.body.appendChild(div);
|
|
253
|
+
const snapshot = captureSnapshot(div);
|
|
254
|
+
expect(snapshot.styles.color).toBeDefined();
|
|
255
|
+
expect(snapshot.styles.backgroundColor).toBeDefined();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('framework detection', () => {
|
|
259
|
+
it('should default to vanilla when no framework detected', () => {
|
|
260
|
+
const div = document.createElement('div');
|
|
261
|
+
document.body.appendChild(div);
|
|
262
|
+
const snapshot = captureSnapshot(div);
|
|
263
|
+
expect(snapshot.framework.name).toBe('vanilla');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe('metadata', () => {
|
|
267
|
+
it('should include timestamp', () => {
|
|
268
|
+
const div = document.createElement('div');
|
|
269
|
+
document.body.appendChild(div);
|
|
270
|
+
const before = Date.now();
|
|
271
|
+
const snapshot = captureSnapshot(div);
|
|
272
|
+
const after = Date.now();
|
|
273
|
+
expect(snapshot.timestamp).toBeGreaterThanOrEqual(before);
|
|
274
|
+
expect(snapshot.timestamp).toBeLessThanOrEqual(after);
|
|
275
|
+
});
|
|
276
|
+
it('should include URL', () => {
|
|
277
|
+
const div = document.createElement('div');
|
|
278
|
+
document.body.appendChild(div);
|
|
279
|
+
const snapshot = captureSnapshot(div);
|
|
280
|
+
expect(snapshot.url).toBe(window.location.href);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eyeglass/inspector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser inspector component for Eyeglass",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test": "vitest run --project browser",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"eyeglass",
|
|
19
|
+
"inspector",
|
|
20
|
+
"web-component",
|
|
21
|
+
"react",
|
|
22
|
+
"vue",
|
|
23
|
+
"svelte"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/donutboyband/eyeglass.git",
|
|
29
|
+
"directory": "packages/inspector"
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": true,
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@eyeglass/types": "^0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.3.0"
|
|
37
|
+
}
|
|
38
|
+
}
|