rlint 0.4.0 → 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 +235 -32
- package/dist/checks/clickability.d.ts.map +1 -1
- package/dist/checks/clickability.js +65 -30
- package/dist/checks/clickability.js.map +1 -1
- package/dist/checks/index.d.ts +1 -0
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +5 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/touch-targets.d.ts.map +1 -1
- package/dist/checks/touch-targets.js +134 -47
- package/dist/checks/touch-targets.js.map +1 -1
- package/dist/checks/viewport-meta.d.ts +3 -0
- package/dist/checks/viewport-meta.d.ts.map +1 -0
- package/dist/checks/viewport-meta.js +113 -0
- package/dist/checks/viewport-meta.js.map +1 -0
- package/dist/cli.js +179 -49
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.js +158 -31
- package/dist/mcp-server.js.map +1 -1
- package/dist/proxy/client-script.d.ts +9 -0
- package/dist/proxy/client-script.d.ts.map +1 -0
- package/dist/proxy/client-script.js +449 -0
- package/dist/proxy/client-script.js.map +1 -0
- package/dist/proxy/index.d.ts +4 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +4 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/result-cache.d.ts +46 -0
- package/dist/proxy/result-cache.d.ts.map +1 -0
- package/dist/proxy/result-cache.js +27 -0
- package/dist/proxy/result-cache.js.map +1 -0
- package/dist/proxy/server.d.ts +17 -0
- package/dist/proxy/server.d.ts.map +1 -0
- package/dist/proxy/server.js +190 -0
- package/dist/proxy/server.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const touchTargetsCheck = {
|
|
2
2
|
name: 'touch-targets',
|
|
3
|
-
description: 'Ensures interactive elements meet minimum touch target size (44x44px)',
|
|
3
|
+
description: 'Ensures interactive elements meet minimum touch target size and spacing (44x44px, 8px gap)',
|
|
4
4
|
async run(ctx) {
|
|
5
5
|
const { page, config } = ctx;
|
|
6
6
|
const checkConfig = typeof config.checks?.touchTargets === 'object'
|
|
@@ -8,6 +8,7 @@ export const touchTargetsCheck = {
|
|
|
8
8
|
: {};
|
|
9
9
|
const minWidth = checkConfig.minWidth ?? 44;
|
|
10
10
|
const minHeight = checkConfig.minHeight ?? 44;
|
|
11
|
+
const MIN_GAP = 8;
|
|
11
12
|
const selectors = checkConfig.selectors || [
|
|
12
13
|
'button',
|
|
13
14
|
'a',
|
|
@@ -25,12 +26,32 @@ export const touchTargetsCheck = {
|
|
|
25
26
|
...(checkConfig.ignore || []),
|
|
26
27
|
'[data-rlint-ignore]',
|
|
27
28
|
];
|
|
28
|
-
const smallElements = await page.evaluate(({ selectors, ignoreSelectors, minWidth, minHeight }) => {
|
|
29
|
-
const
|
|
29
|
+
const { smallElements, tooClosePairs } = await page.evaluate(({ selectors, ignoreSelectors, minWidth, minHeight, minGap }) => {
|
|
30
|
+
const smalls = [];
|
|
30
31
|
const selectorString = selectors.join(', ');
|
|
31
32
|
const elements = document.querySelectorAll(selectorString);
|
|
33
|
+
function makeSelector(el) {
|
|
34
|
+
let s = el.tagName.toLowerCase();
|
|
35
|
+
if (el.id)
|
|
36
|
+
s += `#${el.id}`;
|
|
37
|
+
if (el.className && typeof el.className === 'string') {
|
|
38
|
+
s += '.' + el.className.trim().split(/\s+/).join('.');
|
|
39
|
+
}
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
function makeRect(rect) {
|
|
43
|
+
return {
|
|
44
|
+
width: rect.width,
|
|
45
|
+
height: rect.height,
|
|
46
|
+
left: rect.left,
|
|
47
|
+
top: rect.top,
|
|
48
|
+
right: rect.right,
|
|
49
|
+
bottom: rect.bottom,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Collect all visible interactive elements and their rects
|
|
53
|
+
const visibleElements = [];
|
|
32
54
|
for (const el of elements) {
|
|
33
|
-
// Skip ignored elements
|
|
34
55
|
const shouldIgnore = ignoreSelectors.some(sel => {
|
|
35
56
|
try {
|
|
36
57
|
return el.matches(sel);
|
|
@@ -41,7 +62,6 @@ export const touchTargetsCheck = {
|
|
|
41
62
|
});
|
|
42
63
|
if (shouldIgnore)
|
|
43
64
|
continue;
|
|
44
|
-
// Skip hidden elements
|
|
45
65
|
const style = getComputedStyle(el);
|
|
46
66
|
if (style.display === 'none' ||
|
|
47
67
|
style.visibility === 'hidden' ||
|
|
@@ -49,68 +69,135 @@ export const touchTargetsCheck = {
|
|
|
49
69
|
continue;
|
|
50
70
|
}
|
|
51
71
|
const rect = el.getBoundingClientRect();
|
|
52
|
-
// Skip zero-size elements (likely hidden)
|
|
53
72
|
if (rect.width === 0 && rect.height === 0)
|
|
54
73
|
continue;
|
|
55
|
-
// Skip off-screen elements
|
|
56
74
|
if (rect.right < 0 ||
|
|
57
75
|
rect.bottom < 0 ||
|
|
58
76
|
rect.left > window.innerWidth ||
|
|
59
77
|
rect.top > window.innerHeight) {
|
|
60
78
|
continue;
|
|
61
79
|
}
|
|
62
|
-
|
|
80
|
+
visibleElements.push({ el, rect });
|
|
81
|
+
// Size check
|
|
63
82
|
if (rect.width < minWidth || rect.height < minHeight) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
selector += `#${el.id}`;
|
|
67
|
-
if (el.className && typeof el.className === 'string') {
|
|
68
|
-
selector += '.' + el.className.trim().split(/\s+/).join('.');
|
|
69
|
-
}
|
|
70
|
-
results.push({
|
|
71
|
-
selector,
|
|
83
|
+
smalls.push({
|
|
84
|
+
selector: makeSelector(el),
|
|
72
85
|
tagName: el.tagName,
|
|
73
86
|
className: typeof el.className === 'string' ? el.className : '',
|
|
74
87
|
id: el.id || null,
|
|
75
88
|
textContent: el.textContent?.slice(0, 30)?.trim() || null,
|
|
76
|
-
rect:
|
|
77
|
-
width: rect.width,
|
|
78
|
-
height: rect.height,
|
|
79
|
-
left: rect.left,
|
|
80
|
-
top: rect.top,
|
|
81
|
-
right: rect.right,
|
|
82
|
-
bottom: rect.bottom,
|
|
83
|
-
},
|
|
89
|
+
rect: makeRect(rect),
|
|
84
90
|
actualWidth: rect.width,
|
|
85
91
|
actualHeight: rect.height,
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
// Spacing check: find pairs of interactive elements that are too close
|
|
96
|
+
const closePairs = [];
|
|
97
|
+
const MAX_PAIRS = 20;
|
|
98
|
+
for (let i = 0; i < visibleElements.length && closePairs.length < MAX_PAIRS; i++) {
|
|
99
|
+
const a = visibleElements[i];
|
|
100
|
+
for (let j = i + 1; j < visibleElements.length && closePairs.length < MAX_PAIRS; j++) {
|
|
101
|
+
const b = visibleElements[j];
|
|
102
|
+
// Skip if one contains the other (parent-child)
|
|
103
|
+
if (a.el.contains(b.el) || b.el.contains(a.el))
|
|
104
|
+
continue;
|
|
105
|
+
// Calculate gap between bounding boxes
|
|
106
|
+
const horizGap = Math.max(0, Math.max(b.rect.left - a.rect.right, a.rect.left - b.rect.right));
|
|
107
|
+
const vertGap = Math.max(0, Math.max(b.rect.top - a.rect.bottom, a.rect.top - b.rect.bottom));
|
|
108
|
+
// If they don't overlap on one axis, the gap is the distance on that axis
|
|
109
|
+
// If they overlap on both axes, gap is 0 (overlapping)
|
|
110
|
+
const overlapHoriz = a.rect.right > b.rect.left && b.rect.right > a.rect.left;
|
|
111
|
+
const overlapVert = a.rect.bottom > b.rect.top && b.rect.bottom > a.rect.top;
|
|
112
|
+
let gap;
|
|
113
|
+
if (overlapHoriz && overlapVert) {
|
|
114
|
+
gap = 0; // overlapping
|
|
115
|
+
}
|
|
116
|
+
else if (overlapHoriz) {
|
|
117
|
+
gap = vertGap; // horizontally aligned, measure vertical gap
|
|
118
|
+
}
|
|
119
|
+
else if (overlapVert) {
|
|
120
|
+
gap = horizGap; // vertically aligned, measure horizontal gap
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Diagonal — use the Euclidean distance between nearest corners
|
|
124
|
+
gap = Math.sqrt(horizGap * horizGap + vertGap * vertGap);
|
|
125
|
+
}
|
|
126
|
+
if (gap < minGap) {
|
|
127
|
+
closePairs.push({
|
|
128
|
+
elementA: {
|
|
129
|
+
selector: makeSelector(a.el),
|
|
130
|
+
tagName: a.el.tagName,
|
|
131
|
+
className: typeof a.el.className === 'string' ? a.el.className : '',
|
|
132
|
+
id: a.el.id || null,
|
|
133
|
+
textContent: a.el.textContent?.slice(0, 30)?.trim() || null,
|
|
134
|
+
rect: makeRect(a.rect),
|
|
135
|
+
},
|
|
136
|
+
elementB: {
|
|
137
|
+
selector: makeSelector(b.el),
|
|
138
|
+
tagName: b.el.tagName,
|
|
139
|
+
className: typeof b.el.className === 'string' ? b.el.className : '',
|
|
140
|
+
id: b.el.id || null,
|
|
141
|
+
textContent: b.el.textContent?.slice(0, 30)?.trim() || null,
|
|
142
|
+
rect: makeRect(b.rect),
|
|
143
|
+
},
|
|
144
|
+
gap: Math.round(gap),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { smallElements: smalls, tooClosePairs: closePairs };
|
|
150
|
+
}, { selectors, ignoreSelectors, minWidth, minHeight, minGap: MIN_GAP });
|
|
151
|
+
const issues = [];
|
|
152
|
+
// Size issues
|
|
153
|
+
for (const el of smallElements) {
|
|
154
|
+
issues.push({
|
|
155
|
+
check: 'touch-targets',
|
|
156
|
+
severity: 'warning',
|
|
157
|
+
message: `Touch target too small: ${Math.round(el.actualWidth)}x${Math.round(el.actualHeight)}px (min: ${minWidth}x${minHeight}px)`,
|
|
158
|
+
element: {
|
|
159
|
+
selector: el.selector,
|
|
160
|
+
tagName: el.tagName,
|
|
161
|
+
className: el.className,
|
|
162
|
+
id: el.id,
|
|
163
|
+
textContent: el.textContent,
|
|
164
|
+
rect: el.rect,
|
|
165
|
+
},
|
|
166
|
+
details: {
|
|
167
|
+
actualWidth: el.actualWidth,
|
|
168
|
+
actualHeight: el.actualHeight,
|
|
169
|
+
minWidth,
|
|
170
|
+
minHeight,
|
|
171
|
+
},
|
|
172
|
+
fixHint: `Add min-width: ${minWidth}px and min-height: ${minHeight}px, or increase padding`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Spacing issues
|
|
176
|
+
for (const pair of tooClosePairs) {
|
|
177
|
+
issues.push({
|
|
178
|
+
check: 'touch-targets',
|
|
179
|
+
severity: 'warning',
|
|
180
|
+
message: `Touch targets too close: ${pair.gap}px gap between ${pair.elementA.selector} and ${pair.elementB.selector} (min: ${MIN_GAP}px)`,
|
|
181
|
+
element: {
|
|
182
|
+
selector: pair.elementA.selector,
|
|
183
|
+
tagName: pair.elementA.tagName,
|
|
184
|
+
className: pair.elementA.className,
|
|
185
|
+
id: pair.elementA.id,
|
|
186
|
+
textContent: pair.elementA.textContent,
|
|
187
|
+
rect: pair.elementA.rect,
|
|
188
|
+
},
|
|
189
|
+
details: {
|
|
190
|
+
gap: pair.gap,
|
|
191
|
+
minGap: MIN_GAP,
|
|
192
|
+
nearElement: pair.elementB.selector,
|
|
193
|
+
},
|
|
194
|
+
fixHint: `Add at least ${MIN_GAP}px margin between ${pair.elementA.selector} and ${pair.elementB.selector}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const totalElements = (await page.$$(selectors.join(', '))).length;
|
|
111
198
|
return {
|
|
112
199
|
check: 'touch-targets',
|
|
113
|
-
passed: Math.max(0,
|
|
200
|
+
passed: Math.max(0, totalElements - issues.length),
|
|
114
201
|
issues,
|
|
115
202
|
};
|
|
116
203
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"touch-targets.js","sourceRoot":"","sources":["../../src/checks/touch-targets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"touch-targets.js","sourceRoot":"","sources":["../../src/checks/touch-targets.ts"],"names":[],"mappings":"AAwCA,MAAM,CAAC,MAAM,iBAAiB,GAAU;IACtC,IAAI,EAAE,eAAe;IACrB,WAAW,EAAE,4FAA4F;IAEzG,KAAK,CAAC,GAAG,CAAC,GAAiB;QACzB,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;QAC7B,MAAM,WAAW,GAAG,OAAO,MAAM,CAAC,MAAM,EAAE,YAAY,KAAK,QAAQ;YACjE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY;YAC5B,CAAC,CAAC,EAAE,CAAC;QAEP,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,IAAI,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,IAAI,EAAE,CAAC;QAC9C,MAAM,OAAO,GAAG,CAAC,CAAC;QAElB,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,IAAI;YACzC,QAAQ;YACR,GAAG;YACH,4BAA4B;YAC5B,QAAQ;YACR,UAAU;YACV,iBAAiB;YACjB,eAAe;YACf,mBAAmB;YACnB,gBAAgB;YAChB,gBAAgB;SACjB,CAAC;QAEF,MAAM,eAAe,GAAG;YACtB,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;YACxB,GAAG,CAAC,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC;YAC7B,qBAAqB;SACtB,CAAC;QAEF,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC1D,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE;YAC9D,MAAM,MAAM,GAAmB,EAAE,CAAC;YAClC,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5C,MAAM,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;YAE3D,SAAS,YAAY,CAAC,EAAW;gBAC/B,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;gBACjC,IAAI,EAAE,CAAC,EAAE;oBAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC5B,IAAI,EAAE,CAAC,SAAS,IAAI,OAAO,EAAE,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;oBACrD,CAAC,IAAI,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACxD,CAAC;gBACD,OAAO,CAAC,CAAC;YACX,CAAC;YAED,SAAS,QAAQ,CAAC,IAAa;gBAC7B,OAAO;oBACL,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,MAAM,EAAE,IAAI,CAAC,MAAM;iBACpB,CAAC;YACJ,CAAC;YAED,2DAA2D;YAC3D,MAAM,eAAe,GAAqC,EAAE,CAAC;YAE7D,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC1B,MAAM,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;oBAC9C,IAAI,CAAC;wBACH,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;oBACzB,CAAC;oBAAC,MAAM,CAAC;wBACP,OAAO,KAAK,CAAC;oBACf,CAAC;gBACH,CAAC,CAAC,CAAC;gBACH,IAAI,YAAY;oBAAE,SAAS;gBAE3B,MAAM,KAAK,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAC;gBACnC,IACE,KAAK,CAAC,OAAO,KAAK,MAAM;oBACxB,KAAK,CAAC,UAAU,KAAK,QAAQ;oBAC7B,KAAK,CAAC,OAAO,KAAK,GAAG,EACrB,CAAC;oBACD,SAAS;gBACX,CAAC;gBAED,MAAM,IAAI,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC;gBACxC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;oBAAE,SAAS;gBAEpD,IACE,IAAI,CAAC,KAAK,GAAG,CAAC;oBACd,IAAI,CAAC,MAAM,GAAG,CAAC;oBACf,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,UAAU;oBAC7B,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,WAAW,EAC7B,CAAC;oBACD,SAAS;gBACX,CAAC;gBAED,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;gBAEnC,aAAa;gBACb,IAAI,IAAI,CAAC,KAAK,GAAG,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;oBACrD,MAAM,CAAC,IAAI,CAAC;wBACV,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC;wBAC1B,OAAO,EAAE,EAAE,CAAC,OAAO;wBACnB,SAAS,EAAE,OAAO,EAAE,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;wBAC/D,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,IAAI;wBACjB,WAAW,EAAE,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI;wBACzD,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;wBACpB,WAAW,EAAE,IAAI,CAAC,KAAK;wBACvB,YAAY,EAAE,IAAI,CAAC,MAAM;qBAC1B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAED,uEAAuE;YACvE,MAAM,UAAU,GAAmB,EAAE,CAAC;YACtC,MAAM,SAAS,GAAG,EAAE,CAAC;YAErB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,MAAM,IAAI,UAAU,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;gBACjF,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;gBAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,MAAM,IAAI,UAAU,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;oBACrF,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;oBAE7B,gDAAgD;oBAChD,IAAI,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;wBAAE,SAAS;oBAEzD,uCAAuC;oBACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;oBAC/F,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;oBAE9F,0EAA0E;oBAC1E,uDAAuD;oBACvD,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;oBAC9E,MAAM,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;oBAE7E,IAAI,GAAW,CAAC;oBAChB,IAAI,YAAY,IAAI,WAAW,EAAE,CAAC;wBAChC,GAAG,GAAG,CAAC,CAAC,CAAC,cAAc;oBACzB,CAAC;yBAAM,IAAI,YAAY,EAAE,CAAC;wBACxB,GAAG,GAAG,OAAO,CAAC,CAAC,6CAA6C;oBAC9D,CAAC;yBAAM,IAAI,WAAW,EAAE,CAAC;wBACvB,GAAG,GAAG,QAAQ,CAAC,CAAC,6CAA6C;oBAC/D,CAAC;yBAAM,CAAC;wBACN,gEAAgE;wBAChE,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC,CAAC;oBAC3D,CAAC;oBAED,IAAI,GAAG,GAAG,MAAM,EAAE,CAAC;wBACjB,UAAU,CAAC,IAAI,CAAC;4BACd,QAAQ,EAAE;gCACR,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;gCAC5B,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,OAAO;gCACrB,SAAS,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;gCACnE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,IAAI;gCACnB,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI;gCAC3D,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;6BACvB;4BACD,QAAQ,EAAE;gCACR,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;gCAC5B,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,OAAO;gCACrB,SAAS,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;gCACnE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,IAAI;gCACnB,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI;gCAC3D,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;6BACvB;4BACD,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;yBACrB,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC;QAC9D,CAAC,EACD,EAAE,SAAS,EAAE,eAAe,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CACrE,CAAC;QAEF,MAAM,MAAM,GAAY,EAAE,CAAC;QAE3B,cAAc;QACd,KAAK,MAAM,EAAE,IAAI,aAAa,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,eAAe;gBACtB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,2BAA2B,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,QAAQ,IAAI,SAAS,KAAK;gBACnI,OAAO,EAAE;oBACP,QAAQ,EAAE,EAAE,CAAC,QAAQ;oBACrB,OAAO,EAAE,EAAE,CAAC,OAAO;oBACnB,SAAS,EAAE,EAAE,CAAC,SAAS;oBACvB,EAAE,EAAE,EAAE,CAAC,EAAE;oBACT,WAAW,EAAE,EAAE,CAAC,WAAW;oBAC3B,IAAI,EAAE,EAAE,CAAC,IAAI;iBACd;gBACD,OAAO,EAAE;oBACP,WAAW,EAAE,EAAE,CAAC,WAAW;oBAC3B,YAAY,EAAE,EAAE,CAAC,YAAY;oBAC7B,QAAQ;oBACR,SAAS;iBACV;gBACD,OAAO,EAAE,kBAAkB,QAAQ,sBAAsB,SAAS,yBAAyB;aAC5F,CAAC,CAAC;QACL,CAAC;QAED,iBAAiB;QACjB,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,eAAe;gBACtB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,4BAA4B,IAAI,CAAC,GAAG,kBAAkB,IAAI,CAAC,QAAQ,CAAC,QAAQ,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,UAAU,OAAO,KAAK;gBACzI,OAAO,EAAE;oBACP,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ;oBAChC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO;oBAC9B,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS;oBAClC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;oBACpB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,WAAW;oBACtC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;iBACzB;gBACD,OAAO,EAAE;oBACP,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,MAAM,EAAE,OAAO;oBACf,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ;iBACpC;gBACD,OAAO,EAAE,gBAAgB,OAAO,qBAAqB,IAAI,CAAC,QAAQ,CAAC,QAAQ,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE;aAC5G,CAAC,CAAC;QACL,CAAC;QAED,MAAM,aAAa,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACnE,OAAO;YACL,KAAK,EAAE,eAAe;YACtB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC;YAClD,MAAM;SACP,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport-meta.d.ts","sourceRoot":"","sources":["../../src/checks/viewport-meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAuB,MAAM,aAAa,CAAC;AAU9D,eAAO,MAAM,iBAAiB,EAAE,KA+H/B,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const viewportMetaCheck = {
|
|
2
|
+
name: 'viewport-meta',
|
|
3
|
+
description: 'Checks for a proper viewport meta tag for mobile rendering',
|
|
4
|
+
async run(ctx) {
|
|
5
|
+
const { page, config } = ctx;
|
|
6
|
+
const checkConfig = typeof config.checks?.viewportMeta === 'object'
|
|
7
|
+
? config.checks.viewportMeta
|
|
8
|
+
: {};
|
|
9
|
+
const ignoreSelectors = [
|
|
10
|
+
...(config.ignore || []),
|
|
11
|
+
...(checkConfig.ignore || []),
|
|
12
|
+
];
|
|
13
|
+
const result = await page.evaluate(() => {
|
|
14
|
+
const meta = document.querySelector('meta[name="viewport"]');
|
|
15
|
+
if (!meta) {
|
|
16
|
+
return {
|
|
17
|
+
exists: false,
|
|
18
|
+
content: null,
|
|
19
|
+
hasWidthDeviceWidth: false,
|
|
20
|
+
hasUserScalableNo: false,
|
|
21
|
+
hasMaximumScale1: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const content = meta.getAttribute('content') || '';
|
|
25
|
+
const directives = content.split(',').map(d => d.trim().toLowerCase());
|
|
26
|
+
const hasWidthDeviceWidth = directives.some(d => d.replace(/\s+/g, '').startsWith('width=device-width'));
|
|
27
|
+
const hasUserScalableNo = directives.some(d => {
|
|
28
|
+
const cleaned = d.replace(/\s+/g, '');
|
|
29
|
+
return cleaned === 'user-scalable=no' || cleaned === 'user-scalable=0';
|
|
30
|
+
});
|
|
31
|
+
const hasMaximumScale1 = directives.some(d => {
|
|
32
|
+
const cleaned = d.replace(/\s+/g, '');
|
|
33
|
+
return cleaned.startsWith('maximum-scale=') && parseFloat(cleaned.split('=')[1]) <= 1;
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
exists: true,
|
|
37
|
+
content,
|
|
38
|
+
hasWidthDeviceWidth,
|
|
39
|
+
hasUserScalableNo,
|
|
40
|
+
hasMaximumScale1,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const issues = [];
|
|
44
|
+
const dummyElement = {
|
|
45
|
+
selector: 'head',
|
|
46
|
+
tagName: 'HEAD',
|
|
47
|
+
className: '',
|
|
48
|
+
id: null,
|
|
49
|
+
textContent: null,
|
|
50
|
+
rect: { width: 0, height: 0, left: 0, top: 0, right: 0, bottom: 0 },
|
|
51
|
+
};
|
|
52
|
+
if (!result.exists) {
|
|
53
|
+
issues.push({
|
|
54
|
+
check: 'viewport-meta',
|
|
55
|
+
severity: 'warning',
|
|
56
|
+
message: 'Missing viewport meta tag — page will not render correctly on mobile devices',
|
|
57
|
+
element: dummyElement,
|
|
58
|
+
details: {
|
|
59
|
+
expected: '<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
60
|
+
},
|
|
61
|
+
fixHint: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> to <head>',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (!result.hasWidthDeviceWidth) {
|
|
66
|
+
issues.push({
|
|
67
|
+
check: 'viewport-meta',
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
message: 'Viewport meta tag is missing width=device-width — mobile browsers will use default 980px width',
|
|
70
|
+
element: dummyElement,
|
|
71
|
+
details: {
|
|
72
|
+
current: result.content,
|
|
73
|
+
expected: 'width=device-width',
|
|
74
|
+
},
|
|
75
|
+
fixHint: 'Add width=device-width to your viewport meta tag',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (result.hasUserScalableNo) {
|
|
79
|
+
issues.push({
|
|
80
|
+
check: 'viewport-meta',
|
|
81
|
+
severity: 'warning',
|
|
82
|
+
message: 'Viewport meta tag disables user scaling — this is an accessibility problem (WCAG 1.4.4)',
|
|
83
|
+
element: dummyElement,
|
|
84
|
+
details: {
|
|
85
|
+
current: result.content,
|
|
86
|
+
problem: 'user-scalable=no',
|
|
87
|
+
},
|
|
88
|
+
fixHint: 'Remove user-scalable=no from your viewport meta tag to allow pinch-to-zoom',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (result.hasMaximumScale1) {
|
|
92
|
+
issues.push({
|
|
93
|
+
check: 'viewport-meta',
|
|
94
|
+
severity: 'warning',
|
|
95
|
+
message: 'Viewport meta tag restricts maximum scale to 1 — this prevents pinch-to-zoom (WCAG 1.4.4)',
|
|
96
|
+
element: dummyElement,
|
|
97
|
+
details: {
|
|
98
|
+
current: result.content,
|
|
99
|
+
problem: 'maximum-scale=1',
|
|
100
|
+
},
|
|
101
|
+
fixHint: 'Remove maximum-scale=1 or set it to at least 5 to allow zooming',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const passed = issues.length === 0 ? 1 : 0;
|
|
106
|
+
return {
|
|
107
|
+
check: 'viewport-meta',
|
|
108
|
+
passed,
|
|
109
|
+
issues,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=viewport-meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport-meta.js","sourceRoot":"","sources":["../../src/checks/viewport-meta.ts"],"names":[],"mappings":"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"}
|