bee-ladybug 0.5.3 → 0.6.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/README.MD +18 -1
- package/package.json +1 -1
- package/src/adapters/AntAdapter.js +52 -1
- package/src/adapters/CSSAdapter.js +257 -14
- package/src/adapters/CanvasAdapter.js +87 -1
- package/src/adapters/SpiderAdapter.js +53 -2
- package/src/adapters/WebDOMAdapter.js +209 -1
- package/src/adapters/python/PythonBridge.js +74 -2
- package/src/core/BeeLadybugCore.js +48 -4
- package/src/core/ReportExporter.js +615 -0
- package/src/core/UIOverlay.js +4 -1
- package/src/index.d.ts +89 -0
- package/src/index.js +1 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Reads generic AABB entities, measures host FPS, computes collisions,
|
|
5
5
|
* and pushes packets into the core. Hitboxes are drawn on a stacked
|
|
6
6
|
* overlay canvas so the game pixels stay untouched.
|
|
7
|
+
* `snapshot()` hands the report hitboxes, collisions, and FPS min/avg/max.
|
|
7
8
|
*
|
|
8
9
|
* The core must never import this file.
|
|
9
10
|
*
|
|
@@ -59,6 +60,7 @@ export class CanvasAdapter {
|
|
|
59
60
|
this.timeScale = 1;
|
|
60
61
|
|
|
61
62
|
this.#attached = false;
|
|
63
|
+
this.#reporting = true;
|
|
62
64
|
this.#overlay = null;
|
|
63
65
|
this.#unsubscribe = null;
|
|
64
66
|
this.#rafId = 0;
|
|
@@ -66,6 +68,7 @@ export class CanvasAdapter {
|
|
|
66
68
|
this.#manualPumpWarned = false;
|
|
67
69
|
this.#lastPump = 0;
|
|
68
70
|
this.#fps = 0;
|
|
71
|
+
this.#fpsSamples = [];
|
|
69
72
|
this.#wasColliding = new Set();
|
|
70
73
|
this.#onResize = () => this.#syncOverlay();
|
|
71
74
|
this.#onTick = () => {
|
|
@@ -80,6 +83,7 @@ export class CanvasAdapter {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
#attached;
|
|
86
|
+
#reporting;
|
|
83
87
|
#overlay;
|
|
84
88
|
#unsubscribe;
|
|
85
89
|
#rafId;
|
|
@@ -87,6 +91,7 @@ export class CanvasAdapter {
|
|
|
87
91
|
#manualPumpWarned;
|
|
88
92
|
#lastPump;
|
|
89
93
|
#fps;
|
|
94
|
+
#fpsSamples;
|
|
90
95
|
#wasColliding;
|
|
91
96
|
#onResize;
|
|
92
97
|
#onTick;
|
|
@@ -108,6 +113,15 @@ export class CanvasAdapter {
|
|
|
108
113
|
}
|
|
109
114
|
if (this.autoPump) this.startAutoPump();
|
|
110
115
|
|
|
116
|
+
this.#reporting = true;
|
|
117
|
+
this.core?.registerAdapter('canvas', {
|
|
118
|
+
label: 'CANVAS',
|
|
119
|
+
enabled: true,
|
|
120
|
+
enable: () => this.#enable(),
|
|
121
|
+
disable: () => this.#disable(),
|
|
122
|
+
snapshot: () => this.snapshot()
|
|
123
|
+
});
|
|
124
|
+
|
|
111
125
|
this.core?.sendData('log', {
|
|
112
126
|
source: this.source,
|
|
113
127
|
message: `CanvasAdapter attached (${this.entities.length} entities).`
|
|
@@ -126,6 +140,7 @@ export class CanvasAdapter {
|
|
|
126
140
|
this.#stopAutoPumpLoop();
|
|
127
141
|
if (this.#unsubscribe) this.#unsubscribe();
|
|
128
142
|
this.#unsubscribe = null;
|
|
143
|
+
this.core?.unregisterAdapter('canvas');
|
|
129
144
|
this.#unmountOverlay();
|
|
130
145
|
if (typeof window !== 'undefined') {
|
|
131
146
|
window.removeEventListener('resize', this.#onResize);
|
|
@@ -161,6 +176,28 @@ export class CanvasAdapter {
|
|
|
161
176
|
return this;
|
|
162
177
|
}
|
|
163
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Hitboxes currently on the entity list, plus FPS min/avg/max.
|
|
181
|
+
* Returns null when the adapter is not attached. The core stores this
|
|
182
|
+
* function and does not read the rows.
|
|
183
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
184
|
+
*/
|
|
185
|
+
snapshot() {
|
|
186
|
+
if (!this.#attached) return null;
|
|
187
|
+
const boxes = this.#collectBoxes();
|
|
188
|
+
this.#markCollisions(boxes);
|
|
189
|
+
const items = boxes.map((box) => ({
|
|
190
|
+
severity: box.hit ? 'warn' : 'ok',
|
|
191
|
+
selector: box.name,
|
|
192
|
+
rule: 'hitbox',
|
|
193
|
+
value: `${box.x},${box.y} ${box.w}x${box.h}${box.hit ? ' colliding' : ''}`,
|
|
194
|
+
source: this.source
|
|
195
|
+
}));
|
|
196
|
+
const fps = this.#fpsItem();
|
|
197
|
+
if (fps) items.push(fps);
|
|
198
|
+
return { name: 'canvas', items };
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
/**
|
|
165
202
|
* Call once per host frame, after entity positions are updated.
|
|
166
203
|
* Pass a 2D context only if overlay is disabled and you want hitboxes
|
|
@@ -174,6 +211,7 @@ export class CanvasAdapter {
|
|
|
174
211
|
this.#warnManualPump();
|
|
175
212
|
return this;
|
|
176
213
|
}
|
|
214
|
+
if (!this.#reporting) return this;
|
|
177
215
|
|
|
178
216
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
179
217
|
if (this.#lastPump) {
|
|
@@ -257,6 +295,8 @@ export class CanvasAdapter {
|
|
|
257
295
|
if (!tracked && boxes[0]) tracked = boxes[0];
|
|
258
296
|
|
|
259
297
|
if (this.#fps > 0) {
|
|
298
|
+
this.#fpsSamples.push(this.#fps);
|
|
299
|
+
if (this.#fpsSamples.length > 120) this.#fpsSamples.shift();
|
|
260
300
|
this.core.sendData('fps', { value: this.#fps, source: this.source });
|
|
261
301
|
}
|
|
262
302
|
this.core.sendData('metric', {
|
|
@@ -374,7 +414,49 @@ export class CanvasAdapter {
|
|
|
374
414
|
overlay.style.top = `${rect.top}px`;
|
|
375
415
|
overlay.style.width = `${rect.width}px`;
|
|
376
416
|
overlay.style.height = `${rect.height}px`;
|
|
377
|
-
|
|
417
|
+
const hidden = !this.#reporting || (this.core && this.core.visible === false);
|
|
418
|
+
overlay.style.display = hidden ? 'none' : 'block';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#enable() {
|
|
422
|
+
this.#reporting = true;
|
|
423
|
+
this.#syncOverlay();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#disable() {
|
|
427
|
+
this.#reporting = false;
|
|
428
|
+
this.#syncOverlay();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#fpsItem() {
|
|
432
|
+
const stats = this.#fpsStats();
|
|
433
|
+
if (!stats) return null;
|
|
434
|
+
return {
|
|
435
|
+
severity: 'ok',
|
|
436
|
+
selector: 'host',
|
|
437
|
+
rule: 'fps',
|
|
438
|
+
value: `min ${round1(stats.min)} avg ${round1(stats.avg)} max ${round1(stats.max)}`,
|
|
439
|
+
source: this.source
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#fpsStats() {
|
|
444
|
+
const history = typeof this.core?.getHistory === 'function' ? this.core.getHistory('fps') : null;
|
|
445
|
+
if (history && Array.isArray(history.values) && history.values.length) {
|
|
446
|
+
return { min: history.min, avg: history.avg, max: history.max };
|
|
447
|
+
}
|
|
448
|
+
const samples = this.#fpsSamples;
|
|
449
|
+
if (!samples.length) return null;
|
|
450
|
+
let min = samples[0];
|
|
451
|
+
let max = samples[0];
|
|
452
|
+
let sum = 0;
|
|
453
|
+
for (let i = 0; i < samples.length; i++) {
|
|
454
|
+
const value = samples[i];
|
|
455
|
+
if (value < min) min = value;
|
|
456
|
+
if (value > max) max = value;
|
|
457
|
+
sum += value;
|
|
458
|
+
}
|
|
459
|
+
return { min, avg: sum / samples.length, max };
|
|
378
460
|
}
|
|
379
461
|
}
|
|
380
462
|
|
|
@@ -409,3 +491,7 @@ function idFromKey(id) {
|
|
|
409
491
|
const parts = String(id).split(':');
|
|
410
492
|
return parts[parts.length - 1] || id;
|
|
411
493
|
}
|
|
494
|
+
|
|
495
|
+
function round1(value) {
|
|
496
|
+
return Math.round(Number(value) * 10) / 10;
|
|
497
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SpiderAdapter — long-task and slow-resource tracker.
|
|
3
3
|
*
|
|
4
|
-
* Native PerformanceObserver only.
|
|
4
|
+
* Native PerformanceObserver only. `snapshot()` keeps the long tasks and
|
|
5
|
+
* slow resources already seen. The core must never import this file.
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
8
|
* const spider = new SpiderAdapter(core, { resourceThreshold: 500 });
|
|
8
9
|
* spider.attach();
|
|
9
10
|
*/
|
|
10
11
|
|
|
12
|
+
const MAX_SPIDER = 40;
|
|
13
|
+
|
|
11
14
|
export const SPIDER_ADAPTER_DEFAULTS = Object.freeze({
|
|
12
15
|
longTaskThreshold: 50,
|
|
13
16
|
resourceThreshold: 500,
|
|
@@ -30,6 +33,8 @@ export class SpiderAdapter {
|
|
|
30
33
|
this.#longObserver = null;
|
|
31
34
|
this.#resourceObserver = null;
|
|
32
35
|
this.#longTaskMissingLogged = false;
|
|
36
|
+
this.#longTasks = [];
|
|
37
|
+
this.#resources = [];
|
|
33
38
|
this.#onLongTask = (list) => this.#ingestLongTasks(list);
|
|
34
39
|
this.#onResource = (list) => this.#ingestResources(list);
|
|
35
40
|
}
|
|
@@ -38,6 +43,8 @@ export class SpiderAdapter {
|
|
|
38
43
|
#longObserver;
|
|
39
44
|
#resourceObserver;
|
|
40
45
|
#longTaskMissingLogged;
|
|
46
|
+
#longTasks;
|
|
47
|
+
#resources;
|
|
41
48
|
#onLongTask;
|
|
42
49
|
#onResource;
|
|
43
50
|
|
|
@@ -48,7 +55,8 @@ export class SpiderAdapter {
|
|
|
48
55
|
this.core?.registerAdapter('spider', {
|
|
49
56
|
enable: () => this.#enable(),
|
|
50
57
|
disable: () => this.#disable(),
|
|
51
|
-
label: 'SPIDER'
|
|
58
|
+
label: 'SPIDER',
|
|
59
|
+
snapshot: () => this.snapshot()
|
|
52
60
|
});
|
|
53
61
|
this.core?.sendData('log', {
|
|
54
62
|
source: this.source,
|
|
@@ -65,6 +73,36 @@ export class SpiderAdapter {
|
|
|
65
73
|
return this;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Long tasks and slow resources kept since attach. Returns null when detached.
|
|
78
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
79
|
+
*/
|
|
80
|
+
snapshot() {
|
|
81
|
+
if (!this.#attached) return null;
|
|
82
|
+
const items = [];
|
|
83
|
+
for (let i = 0; i < this.#longTasks.length; i++) {
|
|
84
|
+
const entry = this.#longTasks[i];
|
|
85
|
+
items.push({
|
|
86
|
+
severity: 'warn',
|
|
87
|
+
selector: entry.attribution,
|
|
88
|
+
rule: 'longtask',
|
|
89
|
+
value: `${entry.duration}ms`,
|
|
90
|
+
source: this.source
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < this.#resources.length; i++) {
|
|
94
|
+
const entry = this.#resources[i];
|
|
95
|
+
items.push({
|
|
96
|
+
severity: 'warn',
|
|
97
|
+
selector: entry.name,
|
|
98
|
+
rule: 'resource',
|
|
99
|
+
value: `${entry.duration}ms`,
|
|
100
|
+
source: this.source
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { name: 'spider', items };
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
#enable() {
|
|
69
107
|
if (!this.#attached) return;
|
|
70
108
|
if (typeof PerformanceObserver !== 'function') {
|
|
@@ -111,6 +149,10 @@ export class SpiderAdapter {
|
|
|
111
149
|
for (let i = 0; i < entries.length; i++) {
|
|
112
150
|
const entry = entries[i];
|
|
113
151
|
if (!entry || entry.duration <= this.longTaskThreshold) continue;
|
|
152
|
+
remember(this.#longTasks, {
|
|
153
|
+
attribution: entry.attribution?.[0]?.containerType ?? 'unknown',
|
|
154
|
+
duration: Math.round(entry.duration)
|
|
155
|
+
});
|
|
114
156
|
this.core.sendData('telemetry', {
|
|
115
157
|
source: this.source,
|
|
116
158
|
kind: 'longtask',
|
|
@@ -126,6 +168,10 @@ export class SpiderAdapter {
|
|
|
126
168
|
for (let i = 0; i < entries.length; i++) {
|
|
127
169
|
const entry = entries[i];
|
|
128
170
|
if (!entry || entry.duration <= this.resourceThreshold) continue;
|
|
171
|
+
remember(this.#resources, {
|
|
172
|
+
name: String(entry.name || 'resource'),
|
|
173
|
+
duration: Math.round(entry.duration)
|
|
174
|
+
});
|
|
129
175
|
this.core.sendData('warn', {
|
|
130
176
|
source: this.source,
|
|
131
177
|
message: `Risorsa lenta: ${entry.name} (${Math.round(entry.duration)}ms)`
|
|
@@ -143,6 +189,11 @@ export class SpiderAdapter {
|
|
|
143
189
|
}
|
|
144
190
|
}
|
|
145
191
|
|
|
192
|
+
function remember(list, entry) {
|
|
193
|
+
list.unshift(entry);
|
|
194
|
+
if (list.length > MAX_SPIDER) list.pop();
|
|
195
|
+
}
|
|
196
|
+
|
|
146
197
|
function positive(value, fallback) {
|
|
147
198
|
const n = Number(value);
|
|
148
199
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Reads box models, the node under the pointer, and mutation noise,
|
|
5
5
|
* then pushes packets into the core. Highlights are drawn on a stacked
|
|
6
6
|
* overlay canvas so the page stays untouched.
|
|
7
|
+
* `snapshot()` hands the report the box model, problem nodes, and
|
|
8
|
+
* the latest mutation counts.
|
|
7
9
|
*
|
|
8
10
|
* The core must never import this file.
|
|
9
11
|
*
|
|
@@ -12,6 +14,10 @@
|
|
|
12
14
|
* adapter.attach();
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
import { isReportNoise } from '../core/ReportExporter.js';
|
|
18
|
+
|
|
19
|
+
const MAX_PROBLEM_ROWS = 40;
|
|
20
|
+
|
|
15
21
|
export const WEB_DOM_ADAPTER_DEFAULTS = Object.freeze({
|
|
16
22
|
overlay: true,
|
|
17
23
|
autoTick: true,
|
|
@@ -52,6 +58,7 @@ export class WebDOMAdapter {
|
|
|
52
58
|
this.hover = null;
|
|
53
59
|
|
|
54
60
|
this.#attached = false;
|
|
61
|
+
this.#reporting = true;
|
|
55
62
|
this.#overlay = null;
|
|
56
63
|
this.#unsubscribe = null;
|
|
57
64
|
this.#observer = null;
|
|
@@ -62,6 +69,9 @@ export class WebDOMAdapter {
|
|
|
62
69
|
this.#mutAdded = 0;
|
|
63
70
|
this.#mutRemoved = 0;
|
|
64
71
|
this.#mutAttr = 0;
|
|
72
|
+
this.#seenAdded = 0;
|
|
73
|
+
this.#seenRemoved = 0;
|
|
74
|
+
this.#seenAttr = 0;
|
|
65
75
|
this.#mutTimer = 0;
|
|
66
76
|
this.#lastHoverKey = '';
|
|
67
77
|
this.#onMove = (event) => {
|
|
@@ -78,6 +88,7 @@ export class WebDOMAdapter {
|
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
#attached;
|
|
91
|
+
#reporting;
|
|
81
92
|
#overlay;
|
|
82
93
|
#unsubscribe;
|
|
83
94
|
#observer;
|
|
@@ -88,6 +99,9 @@ export class WebDOMAdapter {
|
|
|
88
99
|
#mutAdded;
|
|
89
100
|
#mutRemoved;
|
|
90
101
|
#mutAttr;
|
|
102
|
+
#seenAdded;
|
|
103
|
+
#seenRemoved;
|
|
104
|
+
#seenAttr;
|
|
91
105
|
#mutTimer;
|
|
92
106
|
#lastHoverKey;
|
|
93
107
|
#onMove;
|
|
@@ -123,6 +137,15 @@ export class WebDOMAdapter {
|
|
|
123
137
|
this.#rafId = requestAnimationFrame(this.#onTick);
|
|
124
138
|
}
|
|
125
139
|
|
|
140
|
+
this.#reporting = true;
|
|
141
|
+
this.core?.registerAdapter('dom', {
|
|
142
|
+
label: 'DOM',
|
|
143
|
+
enabled: true,
|
|
144
|
+
enable: () => this.#enable(),
|
|
145
|
+
disable: () => this.#disable(),
|
|
146
|
+
snapshot: () => this.snapshot()
|
|
147
|
+
});
|
|
148
|
+
|
|
126
149
|
this.core?.sendData('log', {
|
|
127
150
|
source: this.source,
|
|
128
151
|
message: 'WebDOMAdapter attached. Hover a node to inspect the box model.'
|
|
@@ -148,6 +171,7 @@ export class WebDOMAdapter {
|
|
|
148
171
|
this.#rafId = 0;
|
|
149
172
|
if (this.#mutTimer && typeof clearTimeout === 'function') clearTimeout(this.#mutTimer);
|
|
150
173
|
this.#mutTimer = 0;
|
|
174
|
+
this.core?.unregisterAdapter('dom');
|
|
151
175
|
if (typeof window !== 'undefined') {
|
|
152
176
|
window.removeEventListener('mousemove', this.#onMove, true);
|
|
153
177
|
window.removeEventListener('resize', this.#onResize);
|
|
@@ -172,8 +196,36 @@ export class WebDOMAdapter {
|
|
|
172
196
|
return this;
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Box model of the hover and watched nodes, layout problems under root,
|
|
201
|
+
* and the latest mutation counts. Returns null when detached.
|
|
202
|
+
* Hidden boxes, the WordPress admin bar, and BeeLadybug chrome are skipped.
|
|
203
|
+
* The core stores this function and does not read the rows.
|
|
204
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
205
|
+
*/
|
|
206
|
+
snapshot() {
|
|
207
|
+
if (!this.#attached) return null;
|
|
208
|
+
const items = [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const targets = [];
|
|
211
|
+
if (this.hover) targets.push(this.hover);
|
|
212
|
+
const watched = this.#watchedNodes();
|
|
213
|
+
for (let i = 0; i < watched.length; i++) targets.push(watched[i]);
|
|
214
|
+
for (let i = 0; i < targets.length; i++) {
|
|
215
|
+
const node = targets[i];
|
|
216
|
+
if (!node || seen.has(node) || !reportable(node)) continue;
|
|
217
|
+
seen.add(node);
|
|
218
|
+
items.push(boxItem(node, this.source));
|
|
219
|
+
}
|
|
220
|
+
const problems = problemItems(this.root, this.source);
|
|
221
|
+
for (let i = 0; i < problems.length; i++) items.push(problems[i]);
|
|
222
|
+
items.push(mutationItem(this.#mutationCounts(), this.root, this.source));
|
|
223
|
+
return { name: 'dom', items };
|
|
224
|
+
}
|
|
225
|
+
|
|
175
226
|
pump() {
|
|
176
227
|
if (!this.#attached || !this.core) return this;
|
|
228
|
+
if (!this.#reporting) return this;
|
|
177
229
|
|
|
178
230
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
179
231
|
if (this.#lastPump) {
|
|
@@ -311,6 +363,11 @@ export class WebDOMAdapter {
|
|
|
311
363
|
const added = this.#mutAdded;
|
|
312
364
|
const removed = this.#mutRemoved;
|
|
313
365
|
const attr = this.#mutAttr;
|
|
366
|
+
if (added || removed || attr) {
|
|
367
|
+
this.#seenAdded = added;
|
|
368
|
+
this.#seenRemoved = removed;
|
|
369
|
+
this.#seenAttr = attr;
|
|
370
|
+
}
|
|
314
371
|
this.#mutAdded = 0;
|
|
315
372
|
this.#mutRemoved = 0;
|
|
316
373
|
this.#mutAttr = 0;
|
|
@@ -394,7 +451,26 @@ export class WebDOMAdapter {
|
|
|
394
451
|
canvas.style.height = `${h}px`;
|
|
395
452
|
canvas.width = Math.round(w * dpr);
|
|
396
453
|
canvas.height = Math.round(h * dpr);
|
|
397
|
-
|
|
454
|
+
const hidden = !this.#reporting || (this.core && this.core.visible === false);
|
|
455
|
+
canvas.style.display = hidden ? 'none' : 'block';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#enable() {
|
|
459
|
+
this.#reporting = true;
|
|
460
|
+
this.#syncOverlay();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#disable() {
|
|
464
|
+
this.#reporting = false;
|
|
465
|
+
this.#syncOverlay();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#mutationCounts() {
|
|
469
|
+
const pending = this.#mutAdded + this.#mutRemoved + this.#mutAttr;
|
|
470
|
+
if (pending) {
|
|
471
|
+
return { added: this.#mutAdded, removed: this.#mutRemoved, attr: this.#mutAttr };
|
|
472
|
+
}
|
|
473
|
+
return { added: this.#seenAdded, removed: this.#seenRemoved, attr: this.#seenAttr };
|
|
398
474
|
}
|
|
399
475
|
}
|
|
400
476
|
|
|
@@ -504,6 +580,138 @@ function drawLabel(ctx, el, viewW, viewH, color) {
|
|
|
504
580
|
ctx.fillText(text, x + 6, y + height / 2);
|
|
505
581
|
}
|
|
506
582
|
|
|
583
|
+
function reportable(node) {
|
|
584
|
+
if (!node || node.nodeType !== 1) return false;
|
|
585
|
+
if (typeof node.getBoundingClientRect !== 'function') return false;
|
|
586
|
+
if (isBeeChrome(node) || isReportNoise(node)) return false;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function boxItem(node, source) {
|
|
591
|
+
const model = boxModel(node);
|
|
592
|
+
return {
|
|
593
|
+
severity: 'ok',
|
|
594
|
+
selector: cssPath(node),
|
|
595
|
+
rule: 'box',
|
|
596
|
+
value: boxValue(model, styleOf(node)),
|
|
597
|
+
source
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function boxValue(model, style) {
|
|
602
|
+
const border = model.border;
|
|
603
|
+
const content = model.content;
|
|
604
|
+
return `border ${Math.round(border.w)}x${Math.round(border.h)} margin ${sideText(style, 'margin')} padding ${sideText(style, 'padding')} content ${Math.round(content.w)}x${Math.round(content.h)} ${model.display}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function sideText(style, kind) {
|
|
608
|
+
const prefix = kind === 'margin' ? 'margin' : 'padding';
|
|
609
|
+
const top = style ? parsePx(style[`${prefix}Top`]) : 0;
|
|
610
|
+
const right = style ? parsePx(style[`${prefix}Right`]) : 0;
|
|
611
|
+
const bottom = style ? parsePx(style[`${prefix}Bottom`]) : 0;
|
|
612
|
+
const left = style ? parsePx(style[`${prefix}Left`]) : 0;
|
|
613
|
+
return `${top},${right},${bottom},${left}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function problemItems(root, source) {
|
|
617
|
+
const nodes = collectElements(root);
|
|
618
|
+
const items = [];
|
|
619
|
+
const seen = new Set();
|
|
620
|
+
for (let i = 0; i < nodes.length && items.length < MAX_PROBLEM_ROWS; i++) {
|
|
621
|
+
const node = nodes[i];
|
|
622
|
+
if (!node || seen.has(node) || !reportable(node)) continue;
|
|
623
|
+
seen.add(node);
|
|
624
|
+
const style = styleOf(node);
|
|
625
|
+
const margin = sideText(style, 'margin');
|
|
626
|
+
if (hasNegative(margin)) {
|
|
627
|
+
items.push(problemItem(node, 'negative margin', margin, source));
|
|
628
|
+
if (items.length >= MAX_PROBLEM_ROWS) break;
|
|
629
|
+
}
|
|
630
|
+
const clip = clipValue(node);
|
|
631
|
+
if (clip) {
|
|
632
|
+
items.push(problemItem(node, 'clipped', clip, source));
|
|
633
|
+
if (items.length >= MAX_PROBLEM_ROWS) break;
|
|
634
|
+
}
|
|
635
|
+
const model = boxModel(node);
|
|
636
|
+
if (model.border.w > 0 && model.border.h > 0 && (model.content.w === 0 || model.content.h === 0)) {
|
|
637
|
+
items.push(problemItem(
|
|
638
|
+
node,
|
|
639
|
+
'collapsed content',
|
|
640
|
+
`content ${Math.round(model.content.w)}x${Math.round(model.content.h)}`,
|
|
641
|
+
source
|
|
642
|
+
));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return items;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function problemItem(node, rule, value, source) {
|
|
649
|
+
return {
|
|
650
|
+
severity: 'warn',
|
|
651
|
+
selector: cssPath(node),
|
|
652
|
+
rule,
|
|
653
|
+
value,
|
|
654
|
+
source
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function hasNegative(sides) {
|
|
659
|
+
return sides.split(',').some((part) => Number(part) < 0);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function clipValue(node) {
|
|
663
|
+
const sw = finite(node.scrollWidth);
|
|
664
|
+
const cw = finite(node.clientWidth);
|
|
665
|
+
const sh = finite(node.scrollHeight);
|
|
666
|
+
const ch = finite(node.clientHeight);
|
|
667
|
+
const wide = sw != null && cw != null && sw > cw + 2;
|
|
668
|
+
const tall = sh != null && ch != null && sh > ch + 2;
|
|
669
|
+
if (!wide && !tall) return '';
|
|
670
|
+
return `scroll ${sw ?? 0}x${sh ?? 0} client ${cw ?? 0}x${ch ?? 0}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function mutationItem(counts, root, source) {
|
|
674
|
+
const total = counts.added + counts.removed + counts.attr;
|
|
675
|
+
return {
|
|
676
|
+
severity: total > 0 ? 'warn' : 'ok',
|
|
677
|
+
selector: root && root.nodeType === 1 ? cssPath(root) : 'root',
|
|
678
|
+
rule: 'mutations',
|
|
679
|
+
value: `+${counts.added} -${counts.removed} attr ${counts.attr}`,
|
|
680
|
+
source
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function styleOf(node) {
|
|
685
|
+
if (typeof getComputedStyle !== 'function') return null;
|
|
686
|
+
try {
|
|
687
|
+
return getComputedStyle(node);
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function collectElements(root) {
|
|
694
|
+
if (!root) return [];
|
|
695
|
+
const out = [];
|
|
696
|
+
if (root.nodeType === 1) out.push(root);
|
|
697
|
+
let list = null;
|
|
698
|
+
if (typeof root.querySelectorAll === 'function') {
|
|
699
|
+
try {
|
|
700
|
+
list = root.querySelectorAll('*');
|
|
701
|
+
} catch {
|
|
702
|
+
list = null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (list) {
|
|
706
|
+
for (let i = 0; i < list.length; i++) out.push(list[i]);
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function finite(value) {
|
|
712
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
713
|
+
}
|
|
714
|
+
|
|
507
715
|
function countElements(root) {
|
|
508
716
|
if (!root) return 0;
|
|
509
717
|
if (root.nodeType === 1) return 1 + (root.getElementsByTagName?.('*').length ?? 0);
|