harnery 0.17.0 → 0.19.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 +1 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +187 -4
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +76 -4
- package/dist/core/supervisor/index.d.ts +3 -3
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +2 -2
- package/dist/core/supervisor/plan-read.d.ts +2 -1
- package/dist/core/supervisor/plan-read.d.ts.map +1 -1
- package/dist/core/supervisor/plan-read.js +222 -3
- package/dist/core/supervisor/plan-types.d.ts +60 -3
- package/dist/core/supervisor/plan-types.d.ts.map +1 -1
- package/dist/core/supervisor/plan-types.js +2 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -1
- package/dist/core/supervisor/planning.js +804 -56
- package/dist/core/supervisor/read.d.ts +3 -3
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/read.js +2 -2
- package/dist/core/supervisor/runner.d.ts.map +1 -1
- package/dist/core/supervisor/runner.js +7 -2
- package/dist/core/supervisor/service.js +3 -0
- package/dist/core/supervisor/state.d.ts +17 -2
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +211 -33
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +24 -6
- package/dist/core/workflow/types.d.ts +6 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +3 -2
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +31 -3
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +39 -0
- package/dist/lib/browser/geometry.d.ts +146 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -0
- package/dist/lib/browser/geometry.js +525 -0
- package/dist/lib/browser/index.d.ts +2 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/target-size.d.ts +65 -0
- package/dist/lib/browser/target-size.d.ts.map +1 -0
- package/dist/lib/browser/target-size.js +108 -0
- package/package.json +7 -1
- package/src/commands/browse.ts +319 -0
- package/src/commands/supervisor.ts +92 -5
- package/src/core/supervisor/index.ts +17 -1
- package/src/core/supervisor/plan-read.ts +292 -2
- package/src/core/supervisor/plan-types.ts +69 -1
- package/src/core/supervisor/planning.ts +1012 -51
- package/src/core/supervisor/read.ts +16 -1
- package/src/core/supervisor/runner.ts +9 -2
- package/src/core/supervisor/service.ts +3 -0
- package/src/core/supervisor/state.ts +272 -31
- package/src/core/workflow/engine.ts +29 -6
- package/src/core/workflow/types.ts +6 -0
- package/src/core/workflow/validate.ts +34 -3
- package/src/lib/browser/client.ts +62 -0
- package/src/lib/browser/geometry.ts +695 -0
- package/src/lib/browser/index.ts +25 -0
- package/src/lib/browser/target-size.ts +180 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
export function median(values) {
|
|
2
|
+
if (values.length === 0)
|
|
3
|
+
return 0;
|
|
4
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5
|
+
const middle = Math.floor(sorted.length / 2);
|
|
6
|
+
if (sorted.length % 2 === 1)
|
|
7
|
+
return sorted[middle] ?? 0;
|
|
8
|
+
return ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
9
|
+
}
|
|
10
|
+
export function intersectRects(a, b) {
|
|
11
|
+
const left = Math.max(a.left, b.left);
|
|
12
|
+
const top = Math.max(a.top, b.top);
|
|
13
|
+
const right = Math.min(a.right, b.right);
|
|
14
|
+
const bottom = Math.min(a.bottom, b.bottom);
|
|
15
|
+
if (right <= left || bottom <= top)
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
x: left,
|
|
19
|
+
y: top,
|
|
20
|
+
width: right - left,
|
|
21
|
+
height: bottom - top,
|
|
22
|
+
top,
|
|
23
|
+
right,
|
|
24
|
+
bottom,
|
|
25
|
+
left,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One self-contained page evaluator for the rendered-geometry rule family.
|
|
30
|
+
* Helpers are nested because Playwright serializes the returned function and
|
|
31
|
+
* cannot follow module-level closures into the browser context.
|
|
32
|
+
*/
|
|
33
|
+
export function buildLayoutLintCheck() {
|
|
34
|
+
return (request) => {
|
|
35
|
+
const CHILD_LIMIT = 250;
|
|
36
|
+
const ISSUE_LIMIT = 100;
|
|
37
|
+
const rectOf = (rect) => ({
|
|
38
|
+
x: rect.x,
|
|
39
|
+
y: rect.y,
|
|
40
|
+
width: rect.width,
|
|
41
|
+
height: rect.height,
|
|
42
|
+
top: rect.top,
|
|
43
|
+
right: rect.right,
|
|
44
|
+
bottom: rect.bottom,
|
|
45
|
+
left: rect.left,
|
|
46
|
+
});
|
|
47
|
+
const medianOf = (values) => {
|
|
48
|
+
if (values.length === 0)
|
|
49
|
+
return 0;
|
|
50
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
51
|
+
const middle = Math.floor(sorted.length / 2);
|
|
52
|
+
if (sorted.length % 2 === 1)
|
|
53
|
+
return sorted[middle] ?? 0;
|
|
54
|
+
return ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
55
|
+
};
|
|
56
|
+
const labelOf = (element) => {
|
|
57
|
+
const tag = element.tagName.toLowerCase();
|
|
58
|
+
if (element.id)
|
|
59
|
+
return `${tag}#${element.id}`;
|
|
60
|
+
const cls = typeof element.className === "string"
|
|
61
|
+
? element.className.trim()
|
|
62
|
+
: (element.getAttribute("class") ?? "").trim();
|
|
63
|
+
return cls ? `${tag}.${cls.split(/\s+/).slice(0, 2).join(".")}` : tag;
|
|
64
|
+
};
|
|
65
|
+
const isHidden = (element) => {
|
|
66
|
+
let current = element;
|
|
67
|
+
while (current) {
|
|
68
|
+
const style = getComputedStyle(current);
|
|
69
|
+
const contentVisibility = style
|
|
70
|
+
.contentVisibility;
|
|
71
|
+
if (style.display === "none" ||
|
|
72
|
+
style.visibility === "hidden" ||
|
|
73
|
+
style.visibility === "collapse" ||
|
|
74
|
+
Number.parseFloat(style.opacity || "1") === 0 ||
|
|
75
|
+
contentVisibility === "hidden") {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
current = current.parentElement;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
};
|
|
82
|
+
const firstTextRect = (element) => {
|
|
83
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
84
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
85
|
+
const value = node.nodeValue ?? "";
|
|
86
|
+
const match = /\S+/.exec(value);
|
|
87
|
+
if (!match || match.index === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
const parent = node.parentElement;
|
|
90
|
+
if (parent && ["SCRIPT", "STYLE", "NOSCRIPT"].includes(parent.tagName))
|
|
91
|
+
continue;
|
|
92
|
+
const range = document.createRange();
|
|
93
|
+
range.setStart(node, match.index);
|
|
94
|
+
range.setEnd(node, match.index + match[0].length);
|
|
95
|
+
const rect = range.getBoundingClientRect();
|
|
96
|
+
if (rect.width > 0 && rect.height > 0)
|
|
97
|
+
return rect;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
const measure = (element, index, visual) => {
|
|
102
|
+
const box = element.getBoundingClientRect();
|
|
103
|
+
let source = "box";
|
|
104
|
+
let chosen = box;
|
|
105
|
+
if (visual) {
|
|
106
|
+
const textRect = firstTextRect(element);
|
|
107
|
+
const svg = element instanceof SVGElement ? element : element.querySelector("svg");
|
|
108
|
+
const svgRect = svg?.getBoundingClientRect();
|
|
109
|
+
if (textRect) {
|
|
110
|
+
chosen = textRect;
|
|
111
|
+
source = "text";
|
|
112
|
+
}
|
|
113
|
+
else if (svgRect && svgRect.width > 0 && svgRect.height > 0) {
|
|
114
|
+
chosen = svgRect;
|
|
115
|
+
source = "svg";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
index,
|
|
120
|
+
tag: element.tagName.toLowerCase(),
|
|
121
|
+
label: labelOf(element),
|
|
122
|
+
snippet: element.outerHTML.replace(/\s+/g, " ").slice(0, 160),
|
|
123
|
+
source,
|
|
124
|
+
rect: rectOf(chosen),
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const collectChildren = (container, visual) => {
|
|
128
|
+
const measured = [];
|
|
129
|
+
const excluded = [];
|
|
130
|
+
const children = [...container.children];
|
|
131
|
+
for (let index = 0; index < children.length; index++) {
|
|
132
|
+
const element = children[index];
|
|
133
|
+
if (!element)
|
|
134
|
+
continue;
|
|
135
|
+
if (index >= CHILD_LIMIT) {
|
|
136
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "limit" });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (isHidden(element)) {
|
|
140
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "hidden" });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const style = getComputedStyle(element);
|
|
144
|
+
if (style.position === "absolute" || style.position === "fixed") {
|
|
145
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "out-of-flow" });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const rect = element.getBoundingClientRect();
|
|
149
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
150
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "zero-area" });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
measured.push(measure(element, index, visual));
|
|
154
|
+
}
|
|
155
|
+
return { measured, excluded };
|
|
156
|
+
};
|
|
157
|
+
const resolveAxis = (container, requested) => {
|
|
158
|
+
if (requested !== "auto")
|
|
159
|
+
return requested;
|
|
160
|
+
const style = getComputedStyle(container);
|
|
161
|
+
if (style.display.includes("flex") && style.flexDirection.startsWith("column"))
|
|
162
|
+
return "x";
|
|
163
|
+
return "y";
|
|
164
|
+
};
|
|
165
|
+
const clustersOf = (children, axis) => {
|
|
166
|
+
const crossStart = (item) => axis === "y" ? item.rect.top : item.rect.left;
|
|
167
|
+
const crossEnd = (item) => axis === "y" ? item.rect.bottom : item.rect.right;
|
|
168
|
+
const ordered = [...children].sort((a, b) => crossStart(a) - crossStart(b));
|
|
169
|
+
const clusters = [];
|
|
170
|
+
for (const child of ordered) {
|
|
171
|
+
const start = crossStart(child);
|
|
172
|
+
const end = crossEnd(child);
|
|
173
|
+
let cluster = clusters.find((candidate) => start < candidate.end && end > candidate.start);
|
|
174
|
+
if (!cluster) {
|
|
175
|
+
cluster = { start, end, children: [] };
|
|
176
|
+
clusters.push(cluster);
|
|
177
|
+
}
|
|
178
|
+
cluster.start = Math.min(cluster.start, start);
|
|
179
|
+
cluster.end = Math.max(cluster.end, end);
|
|
180
|
+
cluster.children.push(child);
|
|
181
|
+
}
|
|
182
|
+
const mainStart = (item) => axis === "y" ? item.rect.left : item.rect.top;
|
|
183
|
+
return clusters.map((cluster) => cluster.children.sort((a, b) => mainStart(a) - mainStart(b)));
|
|
184
|
+
};
|
|
185
|
+
const emptyAlign = (selector, axis, tolerancePx) => ({
|
|
186
|
+
rule: "align",
|
|
187
|
+
selector,
|
|
188
|
+
found: false,
|
|
189
|
+
outcome: "fail",
|
|
190
|
+
axis,
|
|
191
|
+
tolerancePx,
|
|
192
|
+
clusters: [],
|
|
193
|
+
excluded: [],
|
|
194
|
+
});
|
|
195
|
+
const align = request.align.map(({ selector, axis: requested, tolerancePx }) => {
|
|
196
|
+
const container = document.querySelector(selector);
|
|
197
|
+
const fallbackAxis = requested === "x" ? "x" : "y";
|
|
198
|
+
if (!(container instanceof Element))
|
|
199
|
+
return emptyAlign(selector, fallbackAxis, tolerancePx);
|
|
200
|
+
const axis = resolveAxis(container, requested);
|
|
201
|
+
const { measured, excluded } = collectChildren(container, true);
|
|
202
|
+
const clusters = clustersOf(measured, axis).map((children) => {
|
|
203
|
+
const text = children.filter((child) => child.source === "text");
|
|
204
|
+
const population = text.length > 0 ? text : children;
|
|
205
|
+
const centerOf = (child) => axis === "y"
|
|
206
|
+
? child.rect.top + child.rect.height / 2
|
|
207
|
+
: child.rect.left + child.rect.width / 2;
|
|
208
|
+
const referenceMid = medianOf(population.map(centerOf));
|
|
209
|
+
return {
|
|
210
|
+
referenceMid,
|
|
211
|
+
referenceSource: text.length > 0 ? "text-median" : "all-median",
|
|
212
|
+
children: children.map((child) => {
|
|
213
|
+
const mid = centerOf(child);
|
|
214
|
+
const deltaPx = mid - referenceMid;
|
|
215
|
+
return { ...child, mid, deltaPx, fail: Math.abs(deltaPx) > tolerancePx };
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
const failed = clusters.some((cluster) => cluster.children.some((child) => child.fail));
|
|
220
|
+
return {
|
|
221
|
+
rule: "align",
|
|
222
|
+
selector,
|
|
223
|
+
found: true,
|
|
224
|
+
outcome: failed ? "fail" : "pass",
|
|
225
|
+
axis,
|
|
226
|
+
tolerancePx,
|
|
227
|
+
clusters,
|
|
228
|
+
excluded,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
const gap = request.gap.map(({ selector, axis: requested, tolerancePx, expectedGapPx }) => {
|
|
232
|
+
const container = document.querySelector(selector);
|
|
233
|
+
const fallbackAxis = requested === "x" ? "x" : "y";
|
|
234
|
+
if (!(container instanceof Element)) {
|
|
235
|
+
return {
|
|
236
|
+
rule: "gap",
|
|
237
|
+
selector,
|
|
238
|
+
found: false,
|
|
239
|
+
outcome: "fail",
|
|
240
|
+
axis: fallbackAxis,
|
|
241
|
+
tolerancePx,
|
|
242
|
+
expectedGapPx,
|
|
243
|
+
clusters: [],
|
|
244
|
+
excluded: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const axis = resolveAxis(container, requested);
|
|
248
|
+
const { measured, excluded } = collectChildren(container, false);
|
|
249
|
+
const clusters = clustersOf(measured, axis).map((children) => {
|
|
250
|
+
const observed = children.slice(1).map((child, index) => {
|
|
251
|
+
const previous = children[index];
|
|
252
|
+
if (!previous)
|
|
253
|
+
return 0;
|
|
254
|
+
return axis === "y"
|
|
255
|
+
? child.rect.left - previous.rect.right
|
|
256
|
+
: child.rect.top - previous.rect.bottom;
|
|
257
|
+
});
|
|
258
|
+
const canInfer = children.length >= 3;
|
|
259
|
+
const reference = expectedGapPx ?? (canInfer ? medianOf(observed) : null);
|
|
260
|
+
const pairs = observed.map((observedGapPx, index) => {
|
|
261
|
+
const before = children[index];
|
|
262
|
+
const after = children[index + 1];
|
|
263
|
+
const deltaPx = reference === null ? null : observedGapPx - reference;
|
|
264
|
+
return {
|
|
265
|
+
before,
|
|
266
|
+
after,
|
|
267
|
+
observedGapPx,
|
|
268
|
+
referenceGapPx: reference,
|
|
269
|
+
deltaPx,
|
|
270
|
+
fail: deltaPx !== null && Math.abs(deltaPx) > tolerancePx,
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
const outcome = reference === null ? "unknown" : pairs.some((pair) => pair.fail) ? "fail" : "pass";
|
|
274
|
+
return {
|
|
275
|
+
referenceGapPx: reference,
|
|
276
|
+
referenceSource: expectedGapPx !== null ? "explicit" : canInfer ? "median" : "unavailable",
|
|
277
|
+
outcome,
|
|
278
|
+
pairs,
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
const outcome = clusters.some((cluster) => cluster.outcome === "fail")
|
|
282
|
+
? "fail"
|
|
283
|
+
: clusters.some((cluster) => cluster.outcome === "unknown")
|
|
284
|
+
? "unknown"
|
|
285
|
+
: "pass";
|
|
286
|
+
return {
|
|
287
|
+
rule: "gap",
|
|
288
|
+
selector,
|
|
289
|
+
found: true,
|
|
290
|
+
outcome,
|
|
291
|
+
axis,
|
|
292
|
+
tolerancePx,
|
|
293
|
+
expectedGapPx,
|
|
294
|
+
clusters,
|
|
295
|
+
excluded,
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
const clippingStyle = (element) => {
|
|
299
|
+
const style = getComputedStyle(element);
|
|
300
|
+
const clips = new Set(["hidden", "clip", "auto", "scroll"]);
|
|
301
|
+
return { x: clips.has(style.overflowX), y: clips.has(style.overflowY) };
|
|
302
|
+
};
|
|
303
|
+
const paddingRect = (element) => {
|
|
304
|
+
const rect = element.getBoundingClientRect();
|
|
305
|
+
const html = element;
|
|
306
|
+
const left = rect.left + html.clientLeft;
|
|
307
|
+
const top = rect.top + html.clientTop;
|
|
308
|
+
const width = html.clientWidth;
|
|
309
|
+
const height = html.clientHeight;
|
|
310
|
+
return {
|
|
311
|
+
x: left,
|
|
312
|
+
y: top,
|
|
313
|
+
width,
|
|
314
|
+
height,
|
|
315
|
+
top,
|
|
316
|
+
right: left + width,
|
|
317
|
+
bottom: top + height,
|
|
318
|
+
left,
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
const clip = request.clip.map(({ selector, tolerancePx }) => {
|
|
322
|
+
const container = document.querySelector(selector);
|
|
323
|
+
if (!(container instanceof Element)) {
|
|
324
|
+
return {
|
|
325
|
+
rule: "clip",
|
|
326
|
+
selector,
|
|
327
|
+
found: false,
|
|
328
|
+
outcome: "fail",
|
|
329
|
+
tolerancePx,
|
|
330
|
+
issues: [],
|
|
331
|
+
unsupported: [],
|
|
332
|
+
excluded: [],
|
|
333
|
+
truncated: false,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const unsupported = new Set();
|
|
337
|
+
const excluded = [];
|
|
338
|
+
const issues = [];
|
|
339
|
+
const descendants = [...container.querySelectorAll("*")];
|
|
340
|
+
let truncated = descendants.length > CHILD_LIMIT;
|
|
341
|
+
descendants.slice(0, CHILD_LIMIT).forEach((element, index) => {
|
|
342
|
+
if (isHidden(element)) {
|
|
343
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "hidden" });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const rect = element.getBoundingClientRect();
|
|
347
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
348
|
+
excluded.push({ index, tag: element.tagName.toLowerCase(), reason: "zero-area" });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
let allowed = paddingRect(container);
|
|
352
|
+
let clippedBy = labelOf(container);
|
|
353
|
+
let current = element.parentElement;
|
|
354
|
+
while (current && container.contains(current)) {
|
|
355
|
+
const style = getComputedStyle(current);
|
|
356
|
+
if (style.clipPath && style.clipPath !== "none")
|
|
357
|
+
unsupported.add(`${labelOf(current)}:clip-path`);
|
|
358
|
+
if (style.borderRadius && !/^0(?:px)?(?: 0(?:px)?){0,3}$/.test(style.borderRadius)) {
|
|
359
|
+
unsupported.add(`${labelOf(current)}:border-radius`);
|
|
360
|
+
}
|
|
361
|
+
if (style.transform && style.transform !== "none")
|
|
362
|
+
unsupported.add(`${labelOf(current)}:transform`);
|
|
363
|
+
const clips = current === container ? { x: true, y: true } : clippingStyle(current);
|
|
364
|
+
if (clips.x || clips.y) {
|
|
365
|
+
const candidate = paddingRect(current);
|
|
366
|
+
allowed = {
|
|
367
|
+
x: clips.x ? Math.max(allowed.left, candidate.left) : allowed.left,
|
|
368
|
+
y: clips.y ? Math.max(allowed.top, candidate.top) : allowed.top,
|
|
369
|
+
left: clips.x ? Math.max(allowed.left, candidate.left) : allowed.left,
|
|
370
|
+
top: clips.y ? Math.max(allowed.top, candidate.top) : allowed.top,
|
|
371
|
+
right: clips.x ? Math.min(allowed.right, candidate.right) : allowed.right,
|
|
372
|
+
bottom: clips.y ? Math.min(allowed.bottom, candidate.bottom) : allowed.bottom,
|
|
373
|
+
width: 0,
|
|
374
|
+
height: 0,
|
|
375
|
+
};
|
|
376
|
+
allowed.width = Math.max(0, allowed.right - allowed.left);
|
|
377
|
+
allowed.height = Math.max(0, allowed.bottom - allowed.top);
|
|
378
|
+
clippedBy = labelOf(current);
|
|
379
|
+
}
|
|
380
|
+
if (current === container)
|
|
381
|
+
break;
|
|
382
|
+
current = current.parentElement;
|
|
383
|
+
}
|
|
384
|
+
const overrun = {
|
|
385
|
+
top: Math.max(0, allowed.top - rect.top),
|
|
386
|
+
right: Math.max(0, rect.right - allowed.right),
|
|
387
|
+
bottom: Math.max(0, rect.bottom - allowed.bottom),
|
|
388
|
+
left: Math.max(0, allowed.left - rect.left),
|
|
389
|
+
};
|
|
390
|
+
const maxOverrunPx = Math.max(overrun.top, overrun.right, overrun.bottom, overrun.left);
|
|
391
|
+
if (maxOverrunPx > tolerancePx && issues.length < ISSUE_LIMIT) {
|
|
392
|
+
issues.push({
|
|
393
|
+
element: measure(element, index, false),
|
|
394
|
+
clippedBy,
|
|
395
|
+
overrun,
|
|
396
|
+
maxOverrunPx,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
if (issues.length >= ISSUE_LIMIT)
|
|
401
|
+
truncated = true;
|
|
402
|
+
const outcome = issues.length > 0 ? "fail" : unsupported.size > 0 ? "unknown" : "pass";
|
|
403
|
+
return {
|
|
404
|
+
rule: "clip",
|
|
405
|
+
selector,
|
|
406
|
+
found: true,
|
|
407
|
+
outcome,
|
|
408
|
+
tolerancePx,
|
|
409
|
+
issues,
|
|
410
|
+
unsupported: [...unsupported].slice(0, 50),
|
|
411
|
+
excluded,
|
|
412
|
+
truncated,
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
const overlap = request.overlap.map(({ selector, tolerancePx }) => {
|
|
416
|
+
const container = document.querySelector(selector);
|
|
417
|
+
if (!(container instanceof Element)) {
|
|
418
|
+
return {
|
|
419
|
+
rule: "overlap",
|
|
420
|
+
selector,
|
|
421
|
+
found: false,
|
|
422
|
+
outcome: "fail",
|
|
423
|
+
tolerancePx,
|
|
424
|
+
issues: [],
|
|
425
|
+
excluded: [],
|
|
426
|
+
truncated: false,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const { measured, excluded } = collectChildren(container, false);
|
|
430
|
+
const issues = [];
|
|
431
|
+
let truncated = false;
|
|
432
|
+
for (let firstIndex = 0; firstIndex < measured.length; firstIndex++) {
|
|
433
|
+
for (let secondIndex = firstIndex + 1; secondIndex < measured.length; secondIndex++) {
|
|
434
|
+
const first = measured[firstIndex];
|
|
435
|
+
const second = measured[secondIndex];
|
|
436
|
+
if (!first || !second)
|
|
437
|
+
continue;
|
|
438
|
+
const left = Math.max(first.rect.left, second.rect.left);
|
|
439
|
+
const top = Math.max(first.rect.top, second.rect.top);
|
|
440
|
+
const right = Math.min(first.rect.right, second.rect.right);
|
|
441
|
+
const bottom = Math.min(first.rect.bottom, second.rect.bottom);
|
|
442
|
+
const width = right - left;
|
|
443
|
+
const height = bottom - top;
|
|
444
|
+
if (width <= tolerancePx || height <= tolerancePx)
|
|
445
|
+
continue;
|
|
446
|
+
issues.push({
|
|
447
|
+
first,
|
|
448
|
+
second,
|
|
449
|
+
intersection: { x: left, y: top, width, height, top, right, bottom, left },
|
|
450
|
+
areaPx: width * height,
|
|
451
|
+
});
|
|
452
|
+
if (issues.length >= ISSUE_LIMIT) {
|
|
453
|
+
truncated = true;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (truncated)
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
rule: "overlap",
|
|
462
|
+
selector,
|
|
463
|
+
found: true,
|
|
464
|
+
outcome: issues.length > 0 ? "fail" : "pass",
|
|
465
|
+
tolerancePx,
|
|
466
|
+
issues,
|
|
467
|
+
excluded,
|
|
468
|
+
truncated,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
return { align, gap, clip, overlap };
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
export function buildLayoutLintAnnotateScript() {
|
|
475
|
+
return (result) => {
|
|
476
|
+
const ROOT_ID = "__harnery-layout-lint-annotations__";
|
|
477
|
+
document.getElementById(ROOT_ID)?.remove();
|
|
478
|
+
const root = document.createElement("div");
|
|
479
|
+
root.id = ROOT_ID;
|
|
480
|
+
root.style.cssText =
|
|
481
|
+
"position:absolute;top:0;left:0;width:100%;height:0;overflow:visible;pointer-events:none;z-index:2147483646";
|
|
482
|
+
document.body.appendChild(root);
|
|
483
|
+
const scrollX = window.scrollX;
|
|
484
|
+
const scrollY = window.scrollY;
|
|
485
|
+
const box = (rect, color, label) => {
|
|
486
|
+
const node = document.createElement("div");
|
|
487
|
+
node.style.cssText = `position:absolute;left:${rect.x + scrollX}px;top:${rect.y + scrollY}px;width:${rect.width}px;height:${rect.height}px;border:2px solid ${color};box-sizing:border-box;background:${color}1a;pointer-events:none`;
|
|
488
|
+
const tag = document.createElement("div");
|
|
489
|
+
tag.textContent = label.slice(0, 120);
|
|
490
|
+
tag.style.cssText = `position:absolute;left:0;top:-18px;background:${color};color:white;font:12px/1.4 system-ui,sans-serif;padding:1px 5px;border-radius:3px;white-space:nowrap`;
|
|
491
|
+
node.appendChild(tag);
|
|
492
|
+
root.appendChild(node);
|
|
493
|
+
};
|
|
494
|
+
for (const check of result.align) {
|
|
495
|
+
for (const cluster of check.clusters) {
|
|
496
|
+
for (const child of cluster.children) {
|
|
497
|
+
box(child.rect, child.fail ? "#ef4444" : "#10b981", `align ${child.deltaPx.toFixed(1)}px`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (const check of result.gap) {
|
|
502
|
+
for (const cluster of check.clusters) {
|
|
503
|
+
for (const pair of cluster.pairs) {
|
|
504
|
+
if (pair.fail)
|
|
505
|
+
box(pair.after.rect, "#f97316", `gap ${pair.observedGapPx.toFixed(1)}px`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const check of result.clip) {
|
|
510
|
+
for (const issue of check.issues) {
|
|
511
|
+
box(issue.element.rect, "#d946ef", `clip ${issue.maxOverrunPx.toFixed(1)}px`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const check of result.overlap) {
|
|
515
|
+
for (const issue of check.issues) {
|
|
516
|
+
box(issue.intersection, "#e11d48", `overlap ${issue.areaPx.toFixed(0)}px²`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
export function buildClearLayoutLintAnnotationsScript() {
|
|
522
|
+
return () => {
|
|
523
|
+
document.getElementById("__harnery-layout-lint-annotations__")?.remove();
|
|
524
|
+
};
|
|
525
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { Browser, type BrowserOptions, type ConsoleEvent, type Diagnostics, type FailedRequest, type NavigateResult, type PageErrorEvent, } from "./client.js";
|
|
2
2
|
export { captureDevOverlay, type DevOverlayError, type DevOverlayResult, } from "./dev-overlay.js";
|
|
3
|
+
export type { AlignChild, AlignCluster, AlignResult, ClipIssue, ClipResult, GapCluster, GapPair, GapResult, LayoutAxis, LayoutElementMeasurement, LayoutExclusion, LayoutLintRequest, LayoutLintResult, LayoutOutcome, LayoutRect, OverlapIssue, OverlapResult, } from "./geometry.js";
|
|
3
4
|
export { isWSL, wslHeadedLaunchArgs } from "./launch-args.js";
|
|
4
5
|
export type { OverflowElement, OverflowResult, WidthResult, } from "./layout.js";
|
|
5
6
|
export type { RuntHit, RuntsResult, } from "./runts.js";
|
|
7
|
+
export type { TargetSizeNode, TargetSizeOutcome, TargetSizeProfile, TargetSizeResult, } from "./target-size.js";
|
|
6
8
|
export type { CheckVisibilityOptions, VisibilityResult, VisibilitySample, } from "./visibility.js";
|
|
7
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,SAAS,EACT,UAAU,EACV,UAAU,EACV,OAAO,EACP,SAAS,EACT,UAAU,EACV,wBAAwB,EACxB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type TargetSizeOutcome = "pass" | "fail" | "unknown";
|
|
2
|
+
export type TargetSizeProfile = "wcag-aa" | "comfortable";
|
|
3
|
+
export interface TargetSizeNode {
|
|
4
|
+
outcome: TargetSizeOutcome;
|
|
5
|
+
target: string[];
|
|
6
|
+
html: string;
|
|
7
|
+
message: string;
|
|
8
|
+
failureSummary: string | null;
|
|
9
|
+
relatedTargets: string[][];
|
|
10
|
+
data: Record<string, unknown> | null;
|
|
11
|
+
rect: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
export interface TargetSizeResult {
|
|
19
|
+
rule: "hit";
|
|
20
|
+
selector: string | null;
|
|
21
|
+
found: boolean;
|
|
22
|
+
outcome: TargetSizeOutcome;
|
|
23
|
+
profile: TargetSizeProfile;
|
|
24
|
+
minSizePx: number;
|
|
25
|
+
nodes: TargetSizeNode[];
|
|
26
|
+
}
|
|
27
|
+
interface AxeCheckResult {
|
|
28
|
+
message: string;
|
|
29
|
+
data: Record<string, unknown> | null;
|
|
30
|
+
relatedNodes?: Array<{
|
|
31
|
+
target?: unknown;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
interface AxeNodeResult {
|
|
35
|
+
target?: unknown;
|
|
36
|
+
html?: string;
|
|
37
|
+
failureSummary?: string;
|
|
38
|
+
any?: AxeCheckResult[];
|
|
39
|
+
all?: AxeCheckResult[];
|
|
40
|
+
none?: AxeCheckResult[];
|
|
41
|
+
}
|
|
42
|
+
interface AxeRuleResult {
|
|
43
|
+
nodes?: AxeNodeResult[];
|
|
44
|
+
}
|
|
45
|
+
interface AxeResults {
|
|
46
|
+
violations?: AxeRuleResult[];
|
|
47
|
+
passes?: AxeRuleResult[];
|
|
48
|
+
incomplete?: AxeRuleResult[];
|
|
49
|
+
}
|
|
50
|
+
declare global {
|
|
51
|
+
interface Window {
|
|
52
|
+
axe?: {
|
|
53
|
+
configure: (config: unknown) => void;
|
|
54
|
+
run: (context: unknown, options: unknown) => Promise<AxeResults>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export declare function buildTargetSizeCheck(): (args: {
|
|
59
|
+
selector: string | null;
|
|
60
|
+
profile: TargetSizeProfile;
|
|
61
|
+
}) => Promise<TargetSizeResult>;
|
|
62
|
+
export declare function buildTargetSizeAnnotateScript(): (results: TargetSizeResult[]) => void;
|
|
63
|
+
export declare function buildClearTargetSizeAnnotationsScript(): () => void;
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=target-size.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-size.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/target-size.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,aAAa,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,EAAE,EAAE,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,KAAK,CAAC;IACZ,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,iBAAiB,CAAC;IAC3B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC5C;AAED,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,cAAc,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,cAAc,EAAE,CAAC;IACvB,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,UAAU,aAAa;IACrB,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,UAAU,UAAU;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;CAC9B;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,GAAG,CAAC,EAAE;YACJ,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;YACrC,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;SAClE,CAAC;KACH;CACF;AAED,wBAAgB,oBAAoB,IAAI,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,iBAAiB,CAAC;CAC5B,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAqF9B;AAED,wBAAgB,6BAA6B,IAAI,CAAC,OAAO,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAyBrF;AAED,wBAAgB,qCAAqC,IAAI,MAAM,IAAI,CAIlE"}
|