framewatch-mcp-server 0.1.1 → 0.2.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 +797 -8
- package/dist/constants.d.ts +264 -0
- package/dist/constants.js +268 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.js +4 -0
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/clicks.d.ts +221 -0
- package/dist/engine/clicks.js +801 -0
- package/dist/engine/clicks.js.map +1 -0
- package/dist/engine/forms.d.ts +137 -0
- package/dist/engine/forms.js +474 -0
- package/dist/engine/forms.js.map +1 -0
- package/dist/engine/hmr.d.ts +41 -0
- package/dist/engine/hmr.js +91 -0
- package/dist/engine/hmr.js.map +1 -0
- package/dist/engine/inspect.d.ts +31 -0
- package/dist/engine/inspect.js +383 -0
- package/dist/engine/inspect.js.map +1 -0
- package/dist/engine/interaction.d.ts +6 -1
- package/dist/engine/interaction.js +21 -6
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/links.d.ts +134 -0
- package/dist/engine/links.js +384 -0
- package/dist/engine/links.js.map +1 -0
- package/dist/engine/mocks.d.ts +53 -0
- package/dist/engine/mocks.js +148 -0
- package/dist/engine/mocks.js.map +1 -0
- package/dist/engine/rtl.d.ts +129 -0
- package/dist/engine/rtl.js +540 -0
- package/dist/engine/rtl.js.map +1 -0
- package/dist/engine/seo.d.ts +189 -0
- package/dist/engine/seo.js +398 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/snapshot.d.ts +29 -0
- package/dist/engine/snapshot.js +10 -0
- package/dist/engine/snapshot.js.map +1 -0
- package/dist/engine/vue.d.ts +54 -0
- package/dist/engine/vue.js +419 -0
- package/dist/engine/vue.js.map +1 -0
- package/dist/index.js +42 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.js +4 -3
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/api-mock.d.ts +405 -0
- package/dist/tools/api-mock.js +186 -0
- package/dist/tools/api-mock.js.map +1 -0
- package/dist/tools/capture.d.ts +83 -23
- package/dist/tools/capture.js +105 -60
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.js +4 -3
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/dead-clicks.d.ts +128 -0
- package/dist/tools/dead-clicks.js +570 -0
- package/dist/tools/dead-clicks.js.map +1 -0
- package/dist/tools/form-test.d.ts +112 -0
- package/dist/tools/form-test.js +477 -0
- package/dist/tools/form-test.js.map +1 -0
- package/dist/tools/index.d.ts +16 -1
- package/dist/tools/index.js +42 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspect.d.ts +78 -0
- package/dist/tools/inspect.js +136 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/interact.d.ts +29 -16
- package/dist/tools/interact.js +99 -13
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/links.d.ts +129 -0
- package/dist/tools/links.js +640 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/responsive.d.ts +6 -6
- package/dist/tools/responsive.js +9 -9
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/rtl.d.ts +241 -0
- package/dist/tools/rtl.js +410 -0
- package/dist/tools/rtl.js.map +1 -0
- package/dist/tools/save-auth.d.ts +22 -22
- package/dist/tools/screenshot.js +13 -5
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seo.d.ts +113 -0
- package/dist/tools/seo.js +281 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/snapshot.d.ts +122 -0
- package/dist/tools/snapshot.js +183 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/wait-for.d.ts +107 -0
- package/dist/tools/wait-for.js +167 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/utils/arabic-text.d.ts +14 -0
- package/dist/utils/arabic-text.js +193 -0
- package/dist/utils/arabic-text.js.map +1 -0
- package/dist/utils/budget.d.ts +41 -0
- package/dist/utils/budget.js +182 -0
- package/dist/utils/budget.js.map +1 -0
- package/dist/utils/format.d.ts +11 -1
- package/dist/utils/format.js +27 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/highlight.d.ts +69 -0
- package/dist/utils/highlight.js +181 -0
- package/dist/utils/highlight.js.map +1 -0
- package/dist/utils/link-rules.d.ts +100 -0
- package/dist/utils/link-rules.js +284 -0
- package/dist/utils/link-rules.js.map +1 -0
- package/dist/utils/mock-rules.d.ts +144 -0
- package/dist/utils/mock-rules.js +224 -0
- package/dist/utils/mock-rules.js.map +1 -0
- package/dist/utils/rtl-rules.d.ts +142 -0
- package/dist/utils/rtl-rules.js +296 -0
- package/dist/utils/rtl-rules.js.map +1 -0
- package/dist/utils/seo-rules.d.ts +129 -0
- package/dist/utils/seo-rules.js +726 -0
- package/dist/utils/seo-rules.js.map +1 -0
- package/dist/utils/snapshot-rules.d.ts +33 -0
- package/dist/utils/snapshot-rules.js +111 -0
- package/dist/utils/snapshot-rules.js.map +1 -0
- package/dist/utils/storage-state.d.ts +42 -1
- package/dist/utils/storage-state.js +115 -4
- package/dist/utils/storage-state.js.map +1 -1
- package/dist/utils/style-rules.d.ts +107 -0
- package/dist/utils/style-rules.js +223 -0
- package/dist/utils/style-rules.js.map +1 -0
- package/dist/utils/test-data.d.ts +75 -0
- package/dist/utils/test-data.js +294 -0
- package/dist/utils/test-data.js.map +1 -0
- package/dist/utils/vue-rules.d.ts +72 -0
- package/dist/utils/vue-rules.js +108 -0
- package/dist/utils/vue-rules.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BoundingBox } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* What `framewatch_inspect` says about a measurement.
|
|
4
|
+
*
|
|
5
|
+
* One element's computed style and geometry in, lines of wording out. Nothing
|
|
6
|
+
* here touches a browser: the colour maths (WCAG contrast), the alignment
|
|
7
|
+
* descriptions and the formatting are all decided on plain numbers, so every
|
|
8
|
+
* verdict can be tested in milliseconds and the browser tests only have to
|
|
9
|
+
* prove the numbers are real.
|
|
10
|
+
*/
|
|
11
|
+
export interface Rgba {
|
|
12
|
+
r: number;
|
|
13
|
+
g: number;
|
|
14
|
+
b: number;
|
|
15
|
+
/** 0..1 */
|
|
16
|
+
a: number;
|
|
17
|
+
}
|
|
18
|
+
/** A colour as `getComputedStyle` prints it (`rgb()` / `rgba()`), or as a hex literal, or `transparent`. */
|
|
19
|
+
export declare function parseColour(css: string): Rgba | null;
|
|
20
|
+
/** `top` painted over `bottom`, the way the browser composites a translucent background. */
|
|
21
|
+
export declare function compositeOver(top: Rgba, bottom: Rgba): Rgba;
|
|
22
|
+
export declare function toHex(colour: Rgba): string;
|
|
23
|
+
/** WCAG 2 relative luminance of an opaque colour. */
|
|
24
|
+
export declare function relativeLuminance(colour: Rgba): number;
|
|
25
|
+
/** WCAG 2 contrast ratio, 1..21, whichever way round the colours are given. */
|
|
26
|
+
export declare function contrastRatio(a: Rgba, b: Rgba): number;
|
|
27
|
+
/** WCAG's "large text": 24px, or 18.66px (14pt) when bold. */
|
|
28
|
+
export declare function isLargeText(fontSizePx: number, fontWeight: number): boolean;
|
|
29
|
+
export type ContrastVerdict = "AAA" | "AA" | "fail";
|
|
30
|
+
export declare function contrastVerdict(ratio: number, large: boolean): ContrastVerdict;
|
|
31
|
+
export declare function describeContrast(ratio: number, large: boolean): string;
|
|
32
|
+
/**
|
|
33
|
+
* Where a box sits relative to its parent, and to the sibling before it.
|
|
34
|
+
* Horizontal first (flush, centred, or the inset from each side), then the
|
|
35
|
+
* vertical relationship to the previous sibling.
|
|
36
|
+
*/
|
|
37
|
+
export declare function describeAlignment(box: BoundingBox, parent: BoundingBox, previous?: BoundingBox): string;
|
|
38
|
+
/** One element, as measured inside the page. Colours are as `getComputedStyle` printed them. */
|
|
39
|
+
export interface ElementMeasurement {
|
|
40
|
+
/** What the caller asked for: a ref (`e8`) or a selector. */
|
|
41
|
+
target: string;
|
|
42
|
+
tag: string;
|
|
43
|
+
id?: string;
|
|
44
|
+
classes: string[];
|
|
45
|
+
role?: string;
|
|
46
|
+
/** Accessible name, when the element has one. */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** Own visible text, trimmed and elided. */
|
|
49
|
+
text?: string;
|
|
50
|
+
/** Viewport pixels. */
|
|
51
|
+
box: BoundingBox;
|
|
52
|
+
visible: boolean;
|
|
53
|
+
in_viewport: "fully" | "partially" | "none";
|
|
54
|
+
/** An ancestor with `overflow` other than visible cuts part of it off. */
|
|
55
|
+
clipped: boolean;
|
|
56
|
+
/** Its own content is wider or taller than the box it was given. */
|
|
57
|
+
text_overflows: boolean;
|
|
58
|
+
display: string;
|
|
59
|
+
position: string;
|
|
60
|
+
opacity: number;
|
|
61
|
+
font: {
|
|
62
|
+
family: string;
|
|
63
|
+
size: number;
|
|
64
|
+
weight: number;
|
|
65
|
+
line_height: number | null;
|
|
66
|
+
style: string;
|
|
67
|
+
};
|
|
68
|
+
color: string;
|
|
69
|
+
/** Effective background: its own, composited over its ancestors' until opaque. */
|
|
70
|
+
background: string;
|
|
71
|
+
/** top, right, bottom, left — pixels. */
|
|
72
|
+
padding: [number, number, number, number];
|
|
73
|
+
margin: [number, number, number, number];
|
|
74
|
+
gap: number | null;
|
|
75
|
+
/** Shorthand as computed, e.g. `1px solid rgb(0, 0, 0)` or `0px none rgb(0, 0, 0)`. */
|
|
76
|
+
border: string;
|
|
77
|
+
radius: string;
|
|
78
|
+
parent?: {
|
|
79
|
+
tag: string;
|
|
80
|
+
box: BoundingBox;
|
|
81
|
+
};
|
|
82
|
+
previous?: {
|
|
83
|
+
tag: string;
|
|
84
|
+
box: BoundingBox;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** The lines `framewatch_inspect` prints for one element. */
|
|
88
|
+
export declare function formatInspection(m: ElementMeasurement, index: number): string[];
|
|
89
|
+
export interface Tally {
|
|
90
|
+
value: string;
|
|
91
|
+
count: number;
|
|
92
|
+
}
|
|
93
|
+
/** What the page is built from — every value in use, with how often. Most-used first. */
|
|
94
|
+
export interface PageInventory {
|
|
95
|
+
elements: number;
|
|
96
|
+
/** Elements with visible text of their own. */
|
|
97
|
+
text_elements: number;
|
|
98
|
+
fonts: Tally[];
|
|
99
|
+
font_sizes: Tally[];
|
|
100
|
+
font_weights: Tally[];
|
|
101
|
+
text_colours: Tally[];
|
|
102
|
+
backgrounds: Tally[];
|
|
103
|
+
/** Non-zero padding, margin and gap values. */
|
|
104
|
+
spacing: Tally[];
|
|
105
|
+
radii: Tally[];
|
|
106
|
+
}
|
|
107
|
+
export declare function formatInventory(inventory: PageInventory): string[];
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/** A colour as `getComputedStyle` prints it (`rgb()` / `rgba()`), or as a hex literal, or `transparent`. */
|
|
2
|
+
export function parseColour(css) {
|
|
3
|
+
const value = css.trim().toLowerCase();
|
|
4
|
+
if (value === "transparent")
|
|
5
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
6
|
+
const fn = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(value);
|
|
7
|
+
if (fn) {
|
|
8
|
+
return {
|
|
9
|
+
r: Math.round(Number(fn[1])),
|
|
10
|
+
g: Math.round(Number(fn[2])),
|
|
11
|
+
b: Math.round(Number(fn[3])),
|
|
12
|
+
a: fn[4] === undefined ? 1 : Number(fn[4]),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// The space-separated modern syntax, which Chromium uses for colours outside sRGB.
|
|
16
|
+
const modern = /^rgba?\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(value);
|
|
17
|
+
if (modern) {
|
|
18
|
+
const alpha = modern[4] === undefined ? 1 : modern[4].endsWith("%") ? Number(modern[4].slice(0, -1)) / 100 : Number(modern[4]);
|
|
19
|
+
return { r: Math.round(Number(modern[1])), g: Math.round(Number(modern[2])), b: Math.round(Number(modern[3])), a: alpha };
|
|
20
|
+
}
|
|
21
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(value);
|
|
22
|
+
if (hex) {
|
|
23
|
+
const digits = hex[1];
|
|
24
|
+
if (digits.length === 3 || digits.length === 4) {
|
|
25
|
+
const [r, g, b, a] = digits.split("").map((d) => parseInt(d + d, 16));
|
|
26
|
+
return { r, g, b, a: digits.length === 4 ? a / 255 : 1 };
|
|
27
|
+
}
|
|
28
|
+
if (digits.length === 6 || digits.length === 8) {
|
|
29
|
+
const r = parseInt(digits.slice(0, 2), 16);
|
|
30
|
+
const g = parseInt(digits.slice(2, 4), 16);
|
|
31
|
+
const b = parseInt(digits.slice(4, 6), 16);
|
|
32
|
+
const a = digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;
|
|
33
|
+
return { r, g, b, a };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** `top` painted over `bottom`, the way the browser composites a translucent background. */
|
|
39
|
+
export function compositeOver(top, bottom) {
|
|
40
|
+
if (top.a >= 1)
|
|
41
|
+
return { ...top };
|
|
42
|
+
if (top.a <= 0)
|
|
43
|
+
return { ...bottom };
|
|
44
|
+
const a = top.a + bottom.a * (1 - top.a);
|
|
45
|
+
const mix = (t, b) => Math.round((t * top.a + b * bottom.a * (1 - top.a)) / a);
|
|
46
|
+
return { r: mix(top.r, bottom.r), g: mix(top.g, bottom.g), b: mix(top.b, bottom.b), a };
|
|
47
|
+
}
|
|
48
|
+
export function toHex(colour) {
|
|
49
|
+
const part = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
50
|
+
const rgb = `#${part(colour.r)}${part(colour.g)}${part(colour.b)}`;
|
|
51
|
+
return colour.a >= 1 ? rgb : `${rgb}${part(colour.a * 255)}`;
|
|
52
|
+
}
|
|
53
|
+
/** WCAG 2 relative luminance of an opaque colour. */
|
|
54
|
+
export function relativeLuminance(colour) {
|
|
55
|
+
const channel = (n) => {
|
|
56
|
+
const s = n / 255;
|
|
57
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
58
|
+
};
|
|
59
|
+
return 0.2126 * channel(colour.r) + 0.7152 * channel(colour.g) + 0.0722 * channel(colour.b);
|
|
60
|
+
}
|
|
61
|
+
/** WCAG 2 contrast ratio, 1..21, whichever way round the colours are given. */
|
|
62
|
+
export function contrastRatio(a, b) {
|
|
63
|
+
const la = relativeLuminance(a);
|
|
64
|
+
const lb = relativeLuminance(b);
|
|
65
|
+
const [light, dark] = la >= lb ? [la, lb] : [lb, la];
|
|
66
|
+
return (light + 0.05) / (dark + 0.05);
|
|
67
|
+
}
|
|
68
|
+
/** WCAG's "large text": 24px, or 18.66px (14pt) when bold. */
|
|
69
|
+
export function isLargeText(fontSizePx, fontWeight) {
|
|
70
|
+
if (fontSizePx >= 24)
|
|
71
|
+
return true;
|
|
72
|
+
return fontWeight >= 700 && fontSizePx >= 18.66;
|
|
73
|
+
}
|
|
74
|
+
export function contrastVerdict(ratio, large) {
|
|
75
|
+
const aaa = large ? 4.5 : 7;
|
|
76
|
+
const aa = large ? 3 : 4.5;
|
|
77
|
+
if (ratio >= aaa)
|
|
78
|
+
return "AAA";
|
|
79
|
+
if (ratio >= aa)
|
|
80
|
+
return "AA";
|
|
81
|
+
return "fail";
|
|
82
|
+
}
|
|
83
|
+
export function describeContrast(ratio, large) {
|
|
84
|
+
const verdict = contrastVerdict(ratio, large);
|
|
85
|
+
const size = large ? "large text" : "normal text";
|
|
86
|
+
const shown = `contrast ${ratio.toFixed(2)}:1`;
|
|
87
|
+
if (verdict === "fail")
|
|
88
|
+
return `${shown} — fails AA for ${size} (needs ${large ? "3" : "4.5"}:1)`;
|
|
89
|
+
return `${shown} — passes ${verdict} for ${size}`;
|
|
90
|
+
}
|
|
91
|
+
/** Positions within a pixel of each other count as the same: layout rounds, and so does the reader. */
|
|
92
|
+
const ALIGN_TOLERANCE_PX = 1;
|
|
93
|
+
/**
|
|
94
|
+
* Where a box sits relative to its parent, and to the sibling before it.
|
|
95
|
+
* Horizontal first (flush, centred, or the inset from each side), then the
|
|
96
|
+
* vertical relationship to the previous sibling.
|
|
97
|
+
*/
|
|
98
|
+
export function describeAlignment(box, parent, previous) {
|
|
99
|
+
const parts = [];
|
|
100
|
+
const left = box.x - parent.x;
|
|
101
|
+
const right = parent.x + parent.width - (box.x + box.width);
|
|
102
|
+
if (Math.abs(left) <= ALIGN_TOLERANCE_PX && Math.abs(right) <= ALIGN_TOLERANCE_PX) {
|
|
103
|
+
parts.push("fills parent's width");
|
|
104
|
+
}
|
|
105
|
+
else if (Math.abs(left - right) <= ALIGN_TOLERANCE_PX) {
|
|
106
|
+
parts.push("horizontally centred in parent");
|
|
107
|
+
}
|
|
108
|
+
else if (Math.abs(left) <= ALIGN_TOLERANCE_PX) {
|
|
109
|
+
parts.push("left edge flush with parent");
|
|
110
|
+
}
|
|
111
|
+
else if (Math.abs(right) <= ALIGN_TOLERANCE_PX) {
|
|
112
|
+
parts.push("right edge flush with parent");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
parts.push(`${Math.round(left)}px from parent's left, ${Math.round(right)}px from parent's right`);
|
|
116
|
+
}
|
|
117
|
+
if (previous) {
|
|
118
|
+
const gap = box.y - (previous.y + previous.height);
|
|
119
|
+
const vertical = gap >= 0 ? `${Math.round(gap)}px below previous sibling` : `overlaps previous sibling by ${Math.round(-gap)}px`;
|
|
120
|
+
const shift = box.x - previous.x;
|
|
121
|
+
const horizontal = Math.abs(shift) <= ALIGN_TOLERANCE_PX
|
|
122
|
+
? "left-aligned with it"
|
|
123
|
+
: `${Math.round(Math.abs(shift))}px ${shift > 0 ? "right" : "left"} of its left edge`;
|
|
124
|
+
parts.push(`${vertical}, ${horizontal}`);
|
|
125
|
+
}
|
|
126
|
+
return parts.join("; ");
|
|
127
|
+
}
|
|
128
|
+
/** The lines `framewatch_inspect` prints for one element. */
|
|
129
|
+
export function formatInspection(m, index) {
|
|
130
|
+
const lines = [];
|
|
131
|
+
const identity = [m.target, m.role ?? m.tag, m.name !== undefined ? `"${m.name}"` : null].filter(Boolean).join(" ");
|
|
132
|
+
lines.push(`${index}. ${identity} — <${describeElement(m)}>`);
|
|
133
|
+
const { box } = m;
|
|
134
|
+
const centre = `${Math.round(box.x + box.width / 2)},${Math.round(box.y + box.height / 2)}`;
|
|
135
|
+
const state = [];
|
|
136
|
+
state.push(m.visible ? "visible" : `not visible${m.opacity === 0 ? " (opacity 0)" : ""}`);
|
|
137
|
+
state.push(m.in_viewport === "fully"
|
|
138
|
+
? "fully in viewport"
|
|
139
|
+
: m.in_viewport === "partially"
|
|
140
|
+
? "partly outside the viewport"
|
|
141
|
+
: "outside the viewport");
|
|
142
|
+
if (m.clipped)
|
|
143
|
+
state.push("clipped by an ancestor");
|
|
144
|
+
if (m.text_overflows)
|
|
145
|
+
state.push("text overflows its box");
|
|
146
|
+
lines.push(` box: ${box.x},${box.y} ${box.width}x${box.height} (viewport px; centre ${centre}) — ${state.join(", ")}`);
|
|
147
|
+
lines.push(` text: ${describeText(m)}`);
|
|
148
|
+
const spacing = [`padding ${sides(m.padding)}`, `margin ${sides(m.margin)}`];
|
|
149
|
+
if (m.gap !== null)
|
|
150
|
+
spacing.push(`gap ${m.gap}`);
|
|
151
|
+
lines.push(` spacing: ${spacing.join("; ")}`);
|
|
152
|
+
lines.push(` border: ${describeBorder(m.border)}; radius ${m.radius}`);
|
|
153
|
+
const layout = [m.display];
|
|
154
|
+
if (m.position !== "static")
|
|
155
|
+
layout.push(`position ${m.position}`);
|
|
156
|
+
if (m.parent)
|
|
157
|
+
layout.push(describeAlignment(m.box, m.parent.box, m.previous?.box));
|
|
158
|
+
lines.push(` layout: ${layout.join("; ")}`);
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
161
|
+
function describeElement(m) {
|
|
162
|
+
let out = m.tag;
|
|
163
|
+
if (m.id)
|
|
164
|
+
out += `#${m.id}`;
|
|
165
|
+
else if (m.classes.length > 0)
|
|
166
|
+
out += `.${m.classes[0]}`;
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
function describeText(m) {
|
|
170
|
+
const font = m.font;
|
|
171
|
+
const size = font.line_height !== null ? `${font.size}px/${font.line_height}px` : `${font.size}px`;
|
|
172
|
+
const parts = [`${size} ${font.family} ${font.weight}${font.style !== "normal" ? ` ${font.style}` : ""}`];
|
|
173
|
+
const fg = parseColour(m.color);
|
|
174
|
+
const bg = parseColour(m.background);
|
|
175
|
+
// A translucent text colour renders as a blend with what is behind it; that
|
|
176
|
+
// blend is what the eye sees and what the contrast is rated on, so it is the
|
|
177
|
+
// one printed, with the declared value beside it.
|
|
178
|
+
const rendered = fg && bg && fg.a < 1 && bg.a >= 1 ? compositeOver(fg, bg) : fg;
|
|
179
|
+
const shownFg = rendered ? (fg && fg.a < 1 && rendered !== fg ? `${toHex(rendered)} (${toHex(fg)} as declared)` : toHex(rendered)) : m.color;
|
|
180
|
+
parts.push(`${shownFg} on ${bg ? toHex(bg) : m.background}`);
|
|
181
|
+
const hasText = (m.text ?? "").length > 0 || (m.name ?? "").length > 0;
|
|
182
|
+
if (hasText && rendered && bg && bg.a >= 1) {
|
|
183
|
+
parts.push(describeContrast(contrastRatio(rendered, bg), isLargeText(font.size, font.weight)));
|
|
184
|
+
}
|
|
185
|
+
return parts.join(" — ");
|
|
186
|
+
}
|
|
187
|
+
/** `8 16 8 16` → `8 16`, `8 8 8 8` → `8`, the way a stylesheet would write it. */
|
|
188
|
+
function sides([top, right, bottom, left]) {
|
|
189
|
+
if (top === right && right === bottom && bottom === left)
|
|
190
|
+
return String(top);
|
|
191
|
+
if (top === bottom && right === left)
|
|
192
|
+
return `${top} ${right}`;
|
|
193
|
+
return `${top} ${right} ${bottom} ${left}`;
|
|
194
|
+
}
|
|
195
|
+
function describeBorder(border) {
|
|
196
|
+
const match = /^([\d.]+)px\s+(\w+)\s+(.+)$/.exec(border.trim());
|
|
197
|
+
if (!match)
|
|
198
|
+
return border || "none";
|
|
199
|
+
const width = Number(match[1]);
|
|
200
|
+
if (width === 0 || match[2] === "none")
|
|
201
|
+
return "none";
|
|
202
|
+
const colour = parseColour(match[3]);
|
|
203
|
+
return `${match[1]}px ${match[2]} ${colour ? toHex(colour) : match[3]}`;
|
|
204
|
+
}
|
|
205
|
+
export function formatInventory(inventory) {
|
|
206
|
+
const lines = [`Design inventory — ${inventory.elements} elements, ${inventory.text_elements} with text`];
|
|
207
|
+
const row = (label, tallies) => {
|
|
208
|
+
if (tallies.length === 0) {
|
|
209
|
+
lines.push(` ${label}: none`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
lines.push(` ${label} (${tallies.length}): ${tallies.map((t) => `${t.value} ×${t.count}`).join(", ")}`);
|
|
213
|
+
};
|
|
214
|
+
row("fonts", inventory.fonts);
|
|
215
|
+
row("font sizes", inventory.font_sizes);
|
|
216
|
+
row("font weights", inventory.font_weights);
|
|
217
|
+
row("text colours", inventory.text_colours);
|
|
218
|
+
row("backgrounds", inventory.backgrounds);
|
|
219
|
+
row("spacing", inventory.spacing);
|
|
220
|
+
row("radii", inventory.radii);
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=style-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style-rules.js","sourceRoot":"","sources":["../../src/utils/style-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BoundingBox } from \"../types.js\";\n\n/**\n * What `framewatch_inspect` says about a measurement.\n *\n * One element's computed style and geometry in, lines of wording out. Nothing\n * here touches a browser: the colour maths (WCAG contrast), the alignment\n * descriptions and the formatting are all decided on plain numbers, so every\n * verdict can be tested in milliseconds and the browser tests only have to\n * prove the numbers are real.\n */\n\nexport interface Rgba {\n r: number;\n g: number;\n b: number;\n /** 0..1 */\n a: number;\n}\n\n/** A colour as `getComputedStyle` prints it (`rgb()` / `rgba()`), or as a hex literal, or `transparent`. */\nexport function parseColour(css: string): Rgba | null {\n const value = css.trim().toLowerCase();\n if (value === \"transparent\") return { r: 0, g: 0, b: 0, a: 0 };\n\n const fn = /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*(?:,\\s*([\\d.]+)\\s*)?\\)$/.exec(value);\n if (fn) {\n return {\n r: Math.round(Number(fn[1])),\n g: Math.round(Number(fn[2])),\n b: Math.round(Number(fn[3])),\n a: fn[4] === undefined ? 1 : Number(fn[4]),\n };\n }\n // The space-separated modern syntax, which Chromium uses for colours outside sRGB.\n const modern = /^rgba?\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+%?)\\s*)?\\)$/.exec(value);\n if (modern) {\n const alpha = modern[4] === undefined ? 1 : modern[4].endsWith(\"%\") ? Number(modern[4].slice(0, -1)) / 100 : Number(modern[4]);\n return { r: Math.round(Number(modern[1])), g: Math.round(Number(modern[2])), b: Math.round(Number(modern[3])), a: alpha };\n }\n\n const hex = /^#([0-9a-f]{3,8})$/.exec(value);\n if (hex) {\n const digits = hex[1];\n if (digits.length === 3 || digits.length === 4) {\n const [r, g, b, a] = digits.split(\"\").map((d) => parseInt(d + d, 16));\n return { r, g, b, a: digits.length === 4 ? a / 255 : 1 };\n }\n if (digits.length === 6 || digits.length === 8) {\n const r = parseInt(digits.slice(0, 2), 16);\n const g = parseInt(digits.slice(2, 4), 16);\n const b = parseInt(digits.slice(4, 6), 16);\n const a = digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n }\n }\n return null;\n}\n\n/** `top` painted over `bottom`, the way the browser composites a translucent background. */\nexport function compositeOver(top: Rgba, bottom: Rgba): Rgba {\n if (top.a >= 1) return { ...top };\n if (top.a <= 0) return { ...bottom };\n const a = top.a + bottom.a * (1 - top.a);\n const mix = (t: number, b: number): number => Math.round((t * top.a + b * bottom.a * (1 - top.a)) / a);\n return { r: mix(top.r, bottom.r), g: mix(top.g, bottom.g), b: mix(top.b, bottom.b), a };\n}\n\nexport function toHex(colour: Rgba): string {\n const part = (n: number): string => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n const rgb = `#${part(colour.r)}${part(colour.g)}${part(colour.b)}`;\n return colour.a >= 1 ? rgb : `${rgb}${part(colour.a * 255)}`;\n}\n\n/** WCAG 2 relative luminance of an opaque colour. */\nexport function relativeLuminance(colour: Rgba): number {\n const channel = (n: number): number => {\n const s = n / 255;\n return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n };\n return 0.2126 * channel(colour.r) + 0.7152 * channel(colour.g) + 0.0722 * channel(colour.b);\n}\n\n/** WCAG 2 contrast ratio, 1..21, whichever way round the colours are given. */\nexport function contrastRatio(a: Rgba, b: Rgba): number {\n const la = relativeLuminance(a);\n const lb = relativeLuminance(b);\n const [light, dark] = la >= lb ? [la, lb] : [lb, la];\n return (light + 0.05) / (dark + 0.05);\n}\n\n/** WCAG's \"large text\": 24px, or 18.66px (14pt) when bold. */\nexport function isLargeText(fontSizePx: number, fontWeight: number): boolean {\n if (fontSizePx >= 24) return true;\n return fontWeight >= 700 && fontSizePx >= 18.66;\n}\n\nexport type ContrastVerdict = \"AAA\" | \"AA\" | \"fail\";\n\nexport function contrastVerdict(ratio: number, large: boolean): ContrastVerdict {\n const aaa = large ? 4.5 : 7;\n const aa = large ? 3 : 4.5;\n if (ratio >= aaa) return \"AAA\";\n if (ratio >= aa) return \"AA\";\n return \"fail\";\n}\n\nexport function describeContrast(ratio: number, large: boolean): string {\n const verdict = contrastVerdict(ratio, large);\n const size = large ? \"large text\" : \"normal text\";\n const shown = `contrast ${ratio.toFixed(2)}:1`;\n if (verdict === \"fail\") return `${shown} — fails AA for ${size} (needs ${large ? \"3\" : \"4.5\"}:1)`;\n return `${shown} — passes ${verdict} for ${size}`;\n}\n\n/** Positions within a pixel of each other count as the same: layout rounds, and so does the reader. */\nconst ALIGN_TOLERANCE_PX = 1;\n\n/**\n * Where a box sits relative to its parent, and to the sibling before it.\n * Horizontal first (flush, centred, or the inset from each side), then the\n * vertical relationship to the previous sibling.\n */\nexport function describeAlignment(box: BoundingBox, parent: BoundingBox, previous?: BoundingBox): string {\n const parts: string[] = [];\n\n const left = box.x - parent.x;\n const right = parent.x + parent.width - (box.x + box.width);\n if (Math.abs(left) <= ALIGN_TOLERANCE_PX && Math.abs(right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"fills parent's width\");\n } else if (Math.abs(left - right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"horizontally centred in parent\");\n } else if (Math.abs(left) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"left edge flush with parent\");\n } else if (Math.abs(right) <= ALIGN_TOLERANCE_PX) {\n parts.push(\"right edge flush with parent\");\n } else {\n parts.push(`${Math.round(left)}px from parent's left, ${Math.round(right)}px from parent's right`);\n }\n\n if (previous) {\n const gap = box.y - (previous.y + previous.height);\n const vertical =\n gap >= 0 ? `${Math.round(gap)}px below previous sibling` : `overlaps previous sibling by ${Math.round(-gap)}px`;\n const shift = box.x - previous.x;\n const horizontal =\n Math.abs(shift) <= ALIGN_TOLERANCE_PX\n ? \"left-aligned with it\"\n : `${Math.round(Math.abs(shift))}px ${shift > 0 ? \"right\" : \"left\"} of its left edge`;\n parts.push(`${vertical}, ${horizontal}`);\n }\n\n return parts.join(\"; \");\n}\n\n/** One element, as measured inside the page. Colours are as `getComputedStyle` printed them. */\nexport interface ElementMeasurement {\n /** What the caller asked for: a ref (`e8`) or a selector. */\n target: string;\n tag: string;\n id?: string;\n classes: string[];\n role?: string;\n /** Accessible name, when the element has one. */\n name?: string;\n /** Own visible text, trimmed and elided. */\n text?: string;\n /** Viewport pixels. */\n box: BoundingBox;\n visible: boolean;\n in_viewport: \"fully\" | \"partially\" | \"none\";\n /** An ancestor with `overflow` other than visible cuts part of it off. */\n clipped: boolean;\n /** Its own content is wider or taller than the box it was given. */\n text_overflows: boolean;\n display: string;\n position: string;\n opacity: number;\n font: { family: string; size: number; weight: number; line_height: number | null; style: string };\n color: string;\n /** Effective background: its own, composited over its ancestors' until opaque. */\n background: string;\n /** top, right, bottom, left — pixels. */\n padding: [number, number, number, number];\n margin: [number, number, number, number];\n gap: number | null;\n /** Shorthand as computed, e.g. `1px solid rgb(0, 0, 0)` or `0px none rgb(0, 0, 0)`. */\n border: string;\n radius: string;\n parent?: { tag: string; box: BoundingBox };\n previous?: { tag: string; box: BoundingBox };\n}\n\n/** The lines `framewatch_inspect` prints for one element. */\nexport function formatInspection(m: ElementMeasurement, index: number): string[] {\n const lines: string[] = [];\n\n const identity = [m.target, m.role ?? m.tag, m.name !== undefined ? `\"${m.name}\"` : null].filter(Boolean).join(\" \");\n lines.push(`${index}. ${identity} — <${describeElement(m)}>`);\n\n const { box } = m;\n const centre = `${Math.round(box.x + box.width / 2)},${Math.round(box.y + box.height / 2)}`;\n const state: string[] = [];\n state.push(m.visible ? \"visible\" : `not visible${m.opacity === 0 ? \" (opacity 0)\" : \"\"}`);\n state.push(\n m.in_viewport === \"fully\"\n ? \"fully in viewport\"\n : m.in_viewport === \"partially\"\n ? \"partly outside the viewport\"\n : \"outside the viewport\",\n );\n if (m.clipped) state.push(\"clipped by an ancestor\");\n if (m.text_overflows) state.push(\"text overflows its box\");\n lines.push(` box: ${box.x},${box.y} ${box.width}x${box.height} (viewport px; centre ${centre}) — ${state.join(\", \")}`);\n\n lines.push(` text: ${describeText(m)}`);\n\n const spacing = [`padding ${sides(m.padding)}`, `margin ${sides(m.margin)}`];\n if (m.gap !== null) spacing.push(`gap ${m.gap}`);\n lines.push(` spacing: ${spacing.join(\"; \")}`);\n\n lines.push(` border: ${describeBorder(m.border)}; radius ${m.radius}`);\n\n const layout = [m.display];\n if (m.position !== \"static\") layout.push(`position ${m.position}`);\n if (m.parent) layout.push(describeAlignment(m.box, m.parent.box, m.previous?.box));\n lines.push(` layout: ${layout.join(\"; \")}`);\n\n return lines;\n}\n\nfunction describeElement(m: ElementMeasurement): string {\n let out = m.tag;\n if (m.id) out += `#${m.id}`;\n else if (m.classes.length > 0) out += `.${m.classes[0]}`;\n return out;\n}\n\nfunction describeText(m: ElementMeasurement): string {\n const font = m.font;\n const size = font.line_height !== null ? `${font.size}px/${font.line_height}px` : `${font.size}px`;\n const parts = [`${size} ${font.family} ${font.weight}${font.style !== \"normal\" ? ` ${font.style}` : \"\"}`];\n\n const fg = parseColour(m.color);\n const bg = parseColour(m.background);\n // A translucent text colour renders as a blend with what is behind it; that\n // blend is what the eye sees and what the contrast is rated on, so it is the\n // one printed, with the declared value beside it.\n const rendered = fg && bg && fg.a < 1 && bg.a >= 1 ? compositeOver(fg, bg) : fg;\n const shownFg = rendered ? (fg && fg.a < 1 && rendered !== fg ? `${toHex(rendered)} (${toHex(fg)} as declared)` : toHex(rendered)) : m.color;\n parts.push(`${shownFg} on ${bg ? toHex(bg) : m.background}`);\n\n const hasText = (m.text ?? \"\").length > 0 || (m.name ?? \"\").length > 0;\n if (hasText && rendered && bg && bg.a >= 1) {\n parts.push(describeContrast(contrastRatio(rendered, bg), isLargeText(font.size, font.weight)));\n }\n return parts.join(\" — \");\n}\n\n/** `8 16 8 16` → `8 16`, `8 8 8 8` → `8`, the way a stylesheet would write it. */\nfunction sides([top, right, bottom, left]: [number, number, number, number]): string {\n if (top === right && right === bottom && bottom === left) return String(top);\n if (top === bottom && right === left) return `${top} ${right}`;\n return `${top} ${right} ${bottom} ${left}`;\n}\n\nfunction describeBorder(border: string): string {\n const match = /^([\\d.]+)px\\s+(\\w+)\\s+(.+)$/.exec(border.trim());\n if (!match) return border || \"none\";\n const width = Number(match[1]);\n if (width === 0 || match[2] === \"none\") return \"none\";\n const colour = parseColour(match[3]);\n return `${match[1]}px ${match[2]} ${colour ? toHex(colour) : match[3]}`;\n}\n\n/* ── Page inventory ───────────────────────────────────────────────────── */\n\nexport interface Tally {\n value: string;\n count: number;\n}\n\n/** What the page is built from — every value in use, with how often. Most-used first. */\nexport interface PageInventory {\n elements: number;\n /** Elements with visible text of their own. */\n text_elements: number;\n fonts: Tally[];\n font_sizes: Tally[];\n font_weights: Tally[];\n text_colours: Tally[];\n backgrounds: Tally[];\n /** Non-zero padding, margin and gap values. */\n spacing: Tally[];\n radii: Tally[];\n}\n\nexport function formatInventory(inventory: PageInventory): string[] {\n const lines = [`Design inventory — ${inventory.elements} elements, ${inventory.text_elements} with text`];\n const row = (label: string, tallies: Tally[]): void => {\n if (tallies.length === 0) {\n lines.push(` ${label}: none`);\n return;\n }\n lines.push(` ${label} (${tallies.length}): ${tallies.map((t) => `${t.value} ×${t.count}`).join(\", \")}`);\n };\n row(\"fonts\", inventory.fonts);\n row(\"font sizes\", inventory.font_sizes);\n row(\"font weights\", inventory.font_weights);\n row(\"text colours\", inventory.text_colours);\n row(\"backgrounds\", inventory.backgrounds);\n row(\"spacing\", inventory.spacing);\n row(\"radii\", inventory.radii);\n return lines;\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test data generators.
|
|
3
|
+
*
|
|
4
|
+
* One job: given a field and a strategy, say what should go in it. Nothing
|
|
5
|
+
* here touches a browser, so the interesting decisions — what "valid" means
|
|
6
|
+
* for a field called `user_email`, what happens when Arabic text meets
|
|
7
|
+
* `<input type="date">` — are testable on their own.
|
|
8
|
+
*
|
|
9
|
+
* Two rules run through all of it:
|
|
10
|
+
*
|
|
11
|
+
* - A value must be one the field can actually hold. Chromium refuses a
|
|
12
|
+
* malformed value outright for the constrained input types (number, date,
|
|
13
|
+
* color and friends), so a strategy that cannot be expressed in one of
|
|
14
|
+
* them falls back to a valid value rather than producing a fill error that
|
|
15
|
+
* says nothing about the app.
|
|
16
|
+
* - A value never exceeds the field's own `maxlength`. Otherwise every
|
|
17
|
+
* strategy would report the same truncation on the same short field, and
|
|
18
|
+
* the one report that matters — an app truncating input in JavaScript,
|
|
19
|
+
* with no maxlength anywhere — would be lost in it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FORM_STRATEGIES: readonly ["valid", "empty", "maxlength", "special_chars", "rtl_arabic", "numbers_only", "spaces_only", "boundary", "xss"];
|
|
22
|
+
export type FormStrategy = (typeof FORM_STRATEGIES)[number];
|
|
23
|
+
/**
|
|
24
|
+
* What `framewatch_form_test` runs when the caller names no strategies: the
|
|
25
|
+
* happy path, the empty submit, the characters that break naive escaping, and
|
|
26
|
+
* RTL text. Between them they catch most of what a form gets wrong.
|
|
27
|
+
*/
|
|
28
|
+
export declare const DEFAULT_FORM_STRATEGIES: FormStrategy[];
|
|
29
|
+
/**
|
|
30
|
+
* The global an XSS payload sets when it executes.
|
|
31
|
+
*
|
|
32
|
+
* Payloads are written to assign this rather than to do anything: whether the
|
|
33
|
+
* marker is set after a submit is a yes/no answer to "did my input run as
|
|
34
|
+
* code", where looking for the payload in the DOM afterwards is guesswork —
|
|
35
|
+
* an input's own `value` serialises with its angle brackets intact and would
|
|
36
|
+
* read as an injection on a page that escaped everything correctly.
|
|
37
|
+
*/
|
|
38
|
+
export declare const XSS_MARKER = "__framewatch_xss";
|
|
39
|
+
export interface OptionInfo {
|
|
40
|
+
value: string;
|
|
41
|
+
label: string;
|
|
42
|
+
disabled: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** What the generators need to know about a field. A subset of `FieldInfo` in engine/forms.ts. */
|
|
45
|
+
export interface FieldShape {
|
|
46
|
+
/** Input type lowercased ("text", "email", …), "textarea", or "select-one" / "select-multiple". */
|
|
47
|
+
type: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
placeholder?: string;
|
|
51
|
+
maxlength?: number;
|
|
52
|
+
min?: string;
|
|
53
|
+
max?: string;
|
|
54
|
+
step?: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function describeStrategy(strategy: FormStrategy): string;
|
|
57
|
+
/**
|
|
58
|
+
* The value to put in a text-like field.
|
|
59
|
+
*
|
|
60
|
+
* `variant` is the field's position in the form. It is used to walk through
|
|
61
|
+
* the XSS payload list, so a form with five fields tries five different
|
|
62
|
+
* injections in one pass instead of the same one five times.
|
|
63
|
+
*/
|
|
64
|
+
export declare function valueFor(field: FieldShape, strategy: FormStrategy, variant?: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* Which option a `<select>` should end up on, or null to leave it alone.
|
|
67
|
+
*
|
|
68
|
+
* Disabled options are never chosen — picking one fails, and a form that
|
|
69
|
+
* offers nothing else has a bug worth reporting on its own.
|
|
70
|
+
*/
|
|
71
|
+
export declare function optionFor(field: FieldShape & {
|
|
72
|
+
options: OptionInfo[];
|
|
73
|
+
}, strategy: FormStrategy): string | null;
|
|
74
|
+
/** Whether a checkbox or radio should end up ticked. */
|
|
75
|
+
export declare function checkedFor(strategy: FormStrategy): boolean;
|