@navinjoseph/web-perf-core 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audits/accessibility.d.ts +10 -0
- package/dist/audits/accessibility.d.ts.map +1 -0
- package/dist/audits/accessibility.js +258 -0
- package/dist/audits/accessibility.js.map +1 -0
- package/dist/audits/best-practices.d.ts +10 -0
- package/dist/audits/best-practices.d.ts.map +1 -0
- package/dist/audits/best-practices.js +696 -0
- package/dist/audits/best-practices.js.map +1 -0
- package/dist/audits/constants.d.ts +8 -0
- package/dist/audits/constants.d.ts.map +1 -0
- package/dist/audits/constants.js +278 -0
- package/dist/audits/constants.js.map +1 -0
- package/dist/audits/performance.d.ts +11 -0
- package/dist/audits/performance.d.ts.map +1 -0
- package/dist/audits/performance.js +497 -0
- package/dist/audits/performance.js.map +1 -0
- package/dist/audits/pwa.d.ts +10 -0
- package/dist/audits/pwa.d.ts.map +1 -0
- package/dist/audits/pwa.js +396 -0
- package/dist/audits/pwa.js.map +1 -0
- package/dist/audits/security.d.ts +10 -0
- package/dist/audits/security.d.ts.map +1 -0
- package/dist/audits/security.js +249 -0
- package/dist/audits/security.js.map +1 -0
- package/dist/audits/seo.d.ts +10 -0
- package/dist/audits/seo.d.ts.map +1 -0
- package/dist/audits/seo.js +471 -0
- package/dist/audits/seo.js.map +1 -0
- package/dist/browser.d.ts +21 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +178 -0
- package/dist/browser.js.map +1 -0
- package/dist/dom-collector.d.ts +45 -0
- package/dist/dom-collector.d.ts.map +1 -0
- package/dist/dom-collector.js +173 -0
- package/dist/dom-collector.js.map +1 -0
- package/dist/formatter.d.ts +60 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +164 -0
- package/dist/formatter.js.map +1 -0
- package/dist/id-generator.d.ts +22 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +29 -0
- package/dist/id-generator.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator.d.ts +41 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +194 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/reporters/html.d.ts +6 -0
- package/dist/reporters/html.d.ts.map +1 -0
- package/dist/reporters/html.js +688 -0
- package/dist/reporters/html.js.map +1 -0
- package/dist/reporters/index.d.ts +4 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +17 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/json.d.ts +6 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +7 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/reporters/terminal.d.ts +6 -0
- package/dist/reporters/terminal.d.ts.map +1 -0
- package/dist/reporters/terminal.js +180 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/reporters/types.d.ts +2 -0
- package/dist/reporters/types.d.ts.map +1 -0
- package/dist/reporters/types.js +2 -0
- package/dist/reporters/types.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +36 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { buildSelector } from "../browser.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run best practices audit
|
|
4
|
+
*/
|
|
5
|
+
export async function auditBestPractices(page) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const passed = [];
|
|
8
|
+
// DOCTYPE check
|
|
9
|
+
const doctypeIssue = checkDoctype(page);
|
|
10
|
+
if (doctypeIssue) {
|
|
11
|
+
issues.push(doctypeIssue);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
passed.push({
|
|
15
|
+
id: "bp-doctype",
|
|
16
|
+
name: "DOCTYPE Declaration",
|
|
17
|
+
category: "document",
|
|
18
|
+
description: "Page has a valid HTML5 DOCTYPE",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
// Character encoding
|
|
22
|
+
const charsetIssue = checkCharset(page);
|
|
23
|
+
if (charsetIssue) {
|
|
24
|
+
issues.push(charsetIssue);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
passed.push({
|
|
28
|
+
id: "bp-charset",
|
|
29
|
+
name: "Character Encoding",
|
|
30
|
+
category: "document",
|
|
31
|
+
description: "Page specifies UTF-8 character encoding",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// HTML lang attribute
|
|
35
|
+
const langIssue = checkHtmlLang(page);
|
|
36
|
+
if (langIssue) {
|
|
37
|
+
issues.push(langIssue);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
passed.push({
|
|
41
|
+
id: "bp-html-lang",
|
|
42
|
+
name: "HTML Language",
|
|
43
|
+
category: "document",
|
|
44
|
+
description: "HTML element has valid lang attribute",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Deprecated elements
|
|
48
|
+
const deprecatedIssues = checkDeprecatedElements(page);
|
|
49
|
+
deprecatedIssues.forEach((issue) => issues.push(issue));
|
|
50
|
+
if (deprecatedIssues.length === 0) {
|
|
51
|
+
passed.push({
|
|
52
|
+
id: "bp-deprecated",
|
|
53
|
+
name: "No Deprecated Elements",
|
|
54
|
+
category: "structure",
|
|
55
|
+
description: "Page does not use deprecated HTML elements",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Duplicate IDs
|
|
59
|
+
const duplicateIdIssues = checkDuplicateIds(page);
|
|
60
|
+
duplicateIdIssues.forEach((issue) => issues.push(issue));
|
|
61
|
+
if (duplicateIdIssues.length === 0) {
|
|
62
|
+
passed.push({
|
|
63
|
+
id: "bp-duplicate-ids",
|
|
64
|
+
name: "Unique IDs",
|
|
65
|
+
category: "technical",
|
|
66
|
+
description: "All element IDs are unique",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Broken images
|
|
70
|
+
const brokenImageIssues = checkBrokenImages(page);
|
|
71
|
+
brokenImageIssues.forEach((issue) => issues.push(issue));
|
|
72
|
+
if (brokenImageIssues.length === 0) {
|
|
73
|
+
passed.push({
|
|
74
|
+
id: "bp-images",
|
|
75
|
+
name: "Image Sources",
|
|
76
|
+
category: "images",
|
|
77
|
+
description: "All images have valid src attributes",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Invalid links
|
|
81
|
+
const linkIssues = checkLinks(page);
|
|
82
|
+
linkIssues.forEach((issue) => issues.push(issue));
|
|
83
|
+
if (linkIssues.length === 0) {
|
|
84
|
+
passed.push({
|
|
85
|
+
id: "bp-links",
|
|
86
|
+
name: "Link Validity",
|
|
87
|
+
category: "interactive",
|
|
88
|
+
description: "All links have valid href attributes",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Meta refresh
|
|
92
|
+
const metaRefreshIssue = checkMetaRefresh(page);
|
|
93
|
+
if (metaRefreshIssue) {
|
|
94
|
+
issues.push(metaRefreshIssue);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
passed.push({
|
|
98
|
+
id: "bp-meta-refresh",
|
|
99
|
+
name: "No Meta Refresh",
|
|
100
|
+
category: "document",
|
|
101
|
+
description: "Page does not use meta refresh redirects",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Vulnerable libraries
|
|
105
|
+
const vulnLibIssues = checkVulnerableLibraries(page);
|
|
106
|
+
vulnLibIssues.forEach((issue) => issues.push(issue));
|
|
107
|
+
if (vulnLibIssues.length === 0) {
|
|
108
|
+
passed.push({
|
|
109
|
+
id: "bp-vulnerable-libs",
|
|
110
|
+
name: "Library Security",
|
|
111
|
+
category: "technical",
|
|
112
|
+
description: "No known vulnerable libraries detected",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Password paste prevention
|
|
116
|
+
const passwordPasteIssues = checkPasswordPaste(page);
|
|
117
|
+
passwordPasteIssues.forEach((issue) => issues.push(issue));
|
|
118
|
+
if (passwordPasteIssues.length === 0) {
|
|
119
|
+
passed.push({
|
|
120
|
+
id: "bp-password-paste",
|
|
121
|
+
name: "Password Paste Allowed",
|
|
122
|
+
category: "forms",
|
|
123
|
+
description: "Password fields allow pasting",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Intrusive permissions
|
|
127
|
+
const permissionIssues = checkIntrusivePermissions(page);
|
|
128
|
+
permissionIssues.forEach((issue) => issues.push(issue));
|
|
129
|
+
if (permissionIssues.length === 0) {
|
|
130
|
+
passed.push({
|
|
131
|
+
id: "bp-permissions",
|
|
132
|
+
name: "Non-Intrusive Permissions",
|
|
133
|
+
category: "technical",
|
|
134
|
+
description: "No intrusive permission requests detected",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return { issues, passed };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Check for valid DOCTYPE
|
|
141
|
+
*/
|
|
142
|
+
function checkDoctype(page) {
|
|
143
|
+
const document = page.document;
|
|
144
|
+
const doctype = document.doctype;
|
|
145
|
+
if (!doctype || doctype.name !== "html") {
|
|
146
|
+
return {
|
|
147
|
+
id: `bp-no-doctype-${Date.now()}`,
|
|
148
|
+
ruleId: "doctype-missing",
|
|
149
|
+
severity: "serious",
|
|
150
|
+
category: "document",
|
|
151
|
+
message: "Page missing HTML5 DOCTYPE declaration",
|
|
152
|
+
description: "A valid DOCTYPE is required to ensure browsers render the page in standards mode. Without it, browsers may use quirks mode, leading to inconsistent rendering.",
|
|
153
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Glossary/Doctype",
|
|
154
|
+
wcag: {
|
|
155
|
+
id: "N/A",
|
|
156
|
+
level: "A",
|
|
157
|
+
name: "HTML Best Practice",
|
|
158
|
+
description: "Pages should have a valid DOCTYPE",
|
|
159
|
+
},
|
|
160
|
+
element: {
|
|
161
|
+
selector: "html",
|
|
162
|
+
html: "<!DOCTYPE html>",
|
|
163
|
+
failureSummary: "Missing or invalid DOCTYPE declaration",
|
|
164
|
+
},
|
|
165
|
+
fix: {
|
|
166
|
+
description: "Add HTML5 DOCTYPE at the beginning of the document",
|
|
167
|
+
code: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n ...",
|
|
168
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Glossary/Doctype",
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check character encoding
|
|
176
|
+
*/
|
|
177
|
+
function checkCharset(page) {
|
|
178
|
+
const document = page.document;
|
|
179
|
+
const metaCharset = document.querySelector('meta[charset]');
|
|
180
|
+
const metaHttpEquiv = document.querySelector('meta[http-equiv="Content-Type"]');
|
|
181
|
+
if (!metaCharset && !metaHttpEquiv) {
|
|
182
|
+
return {
|
|
183
|
+
id: `bp-no-charset-${Date.now()}`,
|
|
184
|
+
ruleId: "charset-missing",
|
|
185
|
+
severity: "serious",
|
|
186
|
+
category: "document",
|
|
187
|
+
message: "Page missing character encoding declaration",
|
|
188
|
+
description: "Specifying the character encoding prevents encoding-related rendering issues and potential security vulnerabilities.",
|
|
189
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset",
|
|
190
|
+
wcag: {
|
|
191
|
+
id: "N/A",
|
|
192
|
+
level: "A",
|
|
193
|
+
name: "HTML Best Practice",
|
|
194
|
+
description: "Declare character encoding",
|
|
195
|
+
},
|
|
196
|
+
element: {
|
|
197
|
+
selector: "head",
|
|
198
|
+
html: '<meta charset="UTF-8">',
|
|
199
|
+
failureSummary: "No character encoding specified",
|
|
200
|
+
},
|
|
201
|
+
fix: {
|
|
202
|
+
description: "Add charset meta tag in the head section",
|
|
203
|
+
code: '<head>\n <meta charset="UTF-8">\n ...',
|
|
204
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset",
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const charset = metaCharset?.getAttribute("charset") || "";
|
|
209
|
+
if (charset && charset.toLowerCase() !== "utf-8") {
|
|
210
|
+
return {
|
|
211
|
+
id: `bp-non-utf8-charset-${Date.now()}`,
|
|
212
|
+
ruleId: "charset-not-utf8",
|
|
213
|
+
severity: "moderate",
|
|
214
|
+
category: "document",
|
|
215
|
+
message: `Character encoding is ${charset}, not UTF-8`,
|
|
216
|
+
description: "UTF-8 is the recommended character encoding for HTML documents as it supports all characters and languages.",
|
|
217
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset",
|
|
218
|
+
wcag: {
|
|
219
|
+
id: "N/A",
|
|
220
|
+
level: "AA",
|
|
221
|
+
name: "HTML Best Practice",
|
|
222
|
+
description: "Use UTF-8 encoding",
|
|
223
|
+
},
|
|
224
|
+
element: {
|
|
225
|
+
selector: "meta[charset]",
|
|
226
|
+
html: metaCharset?.outerHTML || "",
|
|
227
|
+
failureSummary: `Charset is ${charset} instead of UTF-8`,
|
|
228
|
+
},
|
|
229
|
+
fix: {
|
|
230
|
+
description: "Change charset to UTF-8",
|
|
231
|
+
code: '<meta charset="UTF-8">',
|
|
232
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset",
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check HTML lang attribute
|
|
240
|
+
*/
|
|
241
|
+
function checkHtmlLang(page) {
|
|
242
|
+
const document = page.document;
|
|
243
|
+
const html = document.documentElement;
|
|
244
|
+
const lang = html.getAttribute("lang");
|
|
245
|
+
if (!lang) {
|
|
246
|
+
return {
|
|
247
|
+
id: `bp-no-html-lang-${Date.now()}`,
|
|
248
|
+
ruleId: "html-lang-missing",
|
|
249
|
+
severity: "serious",
|
|
250
|
+
category: "document",
|
|
251
|
+
message: "HTML element missing lang attribute",
|
|
252
|
+
description: "The lang attribute helps screen readers use the correct pronunciation and helps search engines serve language-specific results.",
|
|
253
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang",
|
|
254
|
+
wcag: {
|
|
255
|
+
id: "WCAG 3.1.1",
|
|
256
|
+
level: "A",
|
|
257
|
+
name: "Language of Page",
|
|
258
|
+
description: "The default language can be programmatically determined",
|
|
259
|
+
},
|
|
260
|
+
element: {
|
|
261
|
+
selector: "html",
|
|
262
|
+
html: '<html lang="en">',
|
|
263
|
+
failureSummary: "HTML element does not have a lang attribute",
|
|
264
|
+
},
|
|
265
|
+
fix: {
|
|
266
|
+
description: "Add lang attribute to the HTML element",
|
|
267
|
+
code: '<html lang="en">',
|
|
268
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang",
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Check for deprecated HTML elements
|
|
276
|
+
*/
|
|
277
|
+
function checkDeprecatedElements(page) {
|
|
278
|
+
const issues = [];
|
|
279
|
+
const document = page.document;
|
|
280
|
+
const deprecatedElements = [
|
|
281
|
+
{ tag: "marquee", name: "Marquee" },
|
|
282
|
+
{ tag: "blink", name: "Blink" },
|
|
283
|
+
{ tag: "font", name: "Font" },
|
|
284
|
+
{ tag: "center", name: "Center" },
|
|
285
|
+
];
|
|
286
|
+
deprecatedElements.forEach(({ tag, name }) => {
|
|
287
|
+
const elements = document.querySelectorAll(tag);
|
|
288
|
+
elements.forEach((element, index) => {
|
|
289
|
+
issues.push({
|
|
290
|
+
id: `bp-deprecated-${tag}-${index}`,
|
|
291
|
+
ruleId: `deprecated-${tag}`,
|
|
292
|
+
severity: "moderate",
|
|
293
|
+
category: "structure",
|
|
294
|
+
message: `Deprecated <${tag}> element used`,
|
|
295
|
+
description: `The <${tag}> element is deprecated and should not be used. Use CSS for styling and animations instead.`,
|
|
296
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element#deprecated_and_obsolete_elements",
|
|
297
|
+
wcag: {
|
|
298
|
+
id: "N/A",
|
|
299
|
+
level: "A",
|
|
300
|
+
name: "HTML Best Practice",
|
|
301
|
+
description: "Avoid deprecated elements",
|
|
302
|
+
},
|
|
303
|
+
element: {
|
|
304
|
+
selector: buildSelector(element),
|
|
305
|
+
html: element.outerHTML,
|
|
306
|
+
failureSummary: `${name} element is deprecated`,
|
|
307
|
+
},
|
|
308
|
+
fix: {
|
|
309
|
+
description: `Replace <${tag}> with modern CSS`,
|
|
310
|
+
code: tag === "marquee"
|
|
311
|
+
? "/* Use CSS animation */\n@keyframes scroll {\n from { transform: translateX(100%); }\n to { transform: translateX(-100%); }\n}"
|
|
312
|
+
: tag === "center"
|
|
313
|
+
? "/* Use CSS */\n.centered {\n text-align: center;\n}"
|
|
314
|
+
: "/* Use CSS for styling */",
|
|
315
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element#deprecated_and_obsolete_elements",
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
return issues;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check for duplicate IDs
|
|
324
|
+
*/
|
|
325
|
+
function checkDuplicateIds(page) {
|
|
326
|
+
const issues = [];
|
|
327
|
+
const document = page.document;
|
|
328
|
+
const allElements = document.querySelectorAll("[id]");
|
|
329
|
+
const idCount = new Map();
|
|
330
|
+
allElements.forEach((element) => {
|
|
331
|
+
const id = element.getAttribute("id");
|
|
332
|
+
if (id) {
|
|
333
|
+
if (!idCount.has(id)) {
|
|
334
|
+
idCount.set(id, []);
|
|
335
|
+
}
|
|
336
|
+
idCount.get(id).push(element);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
idCount.forEach((elements, id) => {
|
|
340
|
+
if (elements.length > 1) {
|
|
341
|
+
issues.push({
|
|
342
|
+
id: `bp-duplicate-id-${id}`,
|
|
343
|
+
ruleId: "duplicate-id",
|
|
344
|
+
severity: "serious",
|
|
345
|
+
category: "technical",
|
|
346
|
+
message: `Duplicate ID "${id}" found ${elements.length} times`,
|
|
347
|
+
description: "Element IDs must be unique. Duplicate IDs can cause accessibility issues, JavaScript errors, and unexpected CSS styling.",
|
|
348
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id",
|
|
349
|
+
wcag: {
|
|
350
|
+
id: "WCAG 4.1.1",
|
|
351
|
+
level: "A",
|
|
352
|
+
name: "Parsing",
|
|
353
|
+
description: "Elements have unique IDs",
|
|
354
|
+
},
|
|
355
|
+
element: {
|
|
356
|
+
selector: `#${id}`,
|
|
357
|
+
html: elements[0].outerHTML,
|
|
358
|
+
failureSummary: `ID "${id}" is used ${elements.length} times`,
|
|
359
|
+
},
|
|
360
|
+
fix: {
|
|
361
|
+
description: "Make each ID unique or use classes instead",
|
|
362
|
+
code: `<!-- Use unique IDs -->\n<div id="${id}-1">...</div>\n<div id="${id}-2">...</div>\n\n<!-- Or use classes -->\n<div class="${id}">...</div>\n<div class="${id}">...</div>`,
|
|
363
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id",
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return issues;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Check for broken images
|
|
372
|
+
*/
|
|
373
|
+
function checkBrokenImages(page) {
|
|
374
|
+
const issues = [];
|
|
375
|
+
const document = page.document;
|
|
376
|
+
const images = document.querySelectorAll("img");
|
|
377
|
+
images.forEach((img, index) => {
|
|
378
|
+
const src = img.getAttribute("src");
|
|
379
|
+
if (!src || src.trim() === "") {
|
|
380
|
+
issues.push({
|
|
381
|
+
id: `bp-img-no-src-${index}`,
|
|
382
|
+
ruleId: "image-no-src",
|
|
383
|
+
severity: "serious",
|
|
384
|
+
category: "images",
|
|
385
|
+
message: "Image has empty or missing src attribute",
|
|
386
|
+
description: "Images must have a valid src attribute. Empty src attributes can cause unnecessary HTTP requests and accessibility issues.",
|
|
387
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-src",
|
|
388
|
+
wcag: {
|
|
389
|
+
id: "N/A",
|
|
390
|
+
level: "A",
|
|
391
|
+
name: "HTML Best Practice",
|
|
392
|
+
description: "Images must have valid src",
|
|
393
|
+
},
|
|
394
|
+
element: {
|
|
395
|
+
selector: buildSelector(img),
|
|
396
|
+
html: img.outerHTML,
|
|
397
|
+
failureSummary: "Image src attribute is empty or missing",
|
|
398
|
+
},
|
|
399
|
+
fix: {
|
|
400
|
+
description: "Add a valid src attribute to the image",
|
|
401
|
+
code: '<img src="/images/photo.jpg" alt="Description">',
|
|
402
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-src",
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
return issues;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Check for invalid links
|
|
411
|
+
*/
|
|
412
|
+
function checkLinks(page) {
|
|
413
|
+
const issues = [];
|
|
414
|
+
const document = page.document;
|
|
415
|
+
const links = document.querySelectorAll("a");
|
|
416
|
+
links.forEach((link, index) => {
|
|
417
|
+
const href = link.getAttribute("href");
|
|
418
|
+
// Empty href
|
|
419
|
+
if (!href || href.trim() === "") {
|
|
420
|
+
issues.push({
|
|
421
|
+
id: `bp-link-no-href-${index}`,
|
|
422
|
+
ruleId: "link-no-href",
|
|
423
|
+
severity: "moderate",
|
|
424
|
+
category: "interactive",
|
|
425
|
+
message: "Link has empty or missing href attribute",
|
|
426
|
+
description: "Links should have a valid href attribute. Empty hrefs may confuse users and accessibility tools.",
|
|
427
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href",
|
|
428
|
+
wcag: {
|
|
429
|
+
id: "N/A",
|
|
430
|
+
level: "A",
|
|
431
|
+
name: "HTML Best Practice",
|
|
432
|
+
description: "Links must have valid href",
|
|
433
|
+
},
|
|
434
|
+
element: {
|
|
435
|
+
selector: buildSelector(link),
|
|
436
|
+
html: link.outerHTML,
|
|
437
|
+
failureSummary: "Link href attribute is empty or missing",
|
|
438
|
+
},
|
|
439
|
+
fix: {
|
|
440
|
+
description: "Add a valid href or use a button instead",
|
|
441
|
+
code: '<!-- Use a link -->\n<a href="/page">Link text</a>\n\n<!-- Or use a button for actions -->\n<button type="button">Click me</button>',
|
|
442
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href",
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// Empty link text
|
|
447
|
+
const text = link.textContent?.trim() || "";
|
|
448
|
+
const ariaLabel = link.getAttribute("aria-label");
|
|
449
|
+
const title = link.getAttribute("title");
|
|
450
|
+
const hasImgChild = link.querySelector("img[alt]");
|
|
451
|
+
if (!text && !ariaLabel && !title && !hasImgChild) {
|
|
452
|
+
issues.push({
|
|
453
|
+
id: `bp-link-no-text-${index}`,
|
|
454
|
+
ruleId: "link-no-text",
|
|
455
|
+
severity: "serious",
|
|
456
|
+
category: "interactive",
|
|
457
|
+
message: "Link has no accessible name",
|
|
458
|
+
description: "Links must have accessible text for screen readers. Add text content, aria-label, or ensure child images have alt text.",
|
|
459
|
+
helpUrl: "https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context",
|
|
460
|
+
wcag: {
|
|
461
|
+
id: "WCAG 2.4.4",
|
|
462
|
+
level: "A",
|
|
463
|
+
name: "Link Purpose",
|
|
464
|
+
description: "Links have accessible names",
|
|
465
|
+
},
|
|
466
|
+
element: {
|
|
467
|
+
selector: buildSelector(link),
|
|
468
|
+
html: link.outerHTML,
|
|
469
|
+
failureSummary: "Link has no accessible name",
|
|
470
|
+
},
|
|
471
|
+
fix: {
|
|
472
|
+
description: "Add text content or aria-label",
|
|
473
|
+
code: '<!-- Add text -->\n<a href="/page">Go to page</a>\n\n<!-- Or aria-label -->\n<a href="/page" aria-label="Go to page">\n <svg>...</svg>\n</a>',
|
|
474
|
+
learnMoreUrl: "https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context",
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
return issues;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Check for meta refresh
|
|
483
|
+
*/
|
|
484
|
+
function checkMetaRefresh(page) {
|
|
485
|
+
const document = page.document;
|
|
486
|
+
const metaRefresh = document.querySelector('meta[http-equiv="refresh"]');
|
|
487
|
+
if (metaRefresh) {
|
|
488
|
+
return {
|
|
489
|
+
id: `bp-meta-refresh-${Date.now()}`,
|
|
490
|
+
ruleId: "meta-refresh",
|
|
491
|
+
severity: "serious",
|
|
492
|
+
category: "document",
|
|
493
|
+
message: "Page uses meta refresh for redirection",
|
|
494
|
+
description: "Meta refresh redirects are problematic for accessibility, SEO, and user experience. Use server-side redirects (301/302) or JavaScript instead.",
|
|
495
|
+
helpUrl: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv",
|
|
496
|
+
wcag: {
|
|
497
|
+
id: "WCAG 2.2.1",
|
|
498
|
+
level: "A",
|
|
499
|
+
name: "Timing Adjustable",
|
|
500
|
+
description: "Avoid automatic redirects",
|
|
501
|
+
},
|
|
502
|
+
element: {
|
|
503
|
+
selector: 'meta[http-equiv="refresh"]',
|
|
504
|
+
html: metaRefresh.outerHTML,
|
|
505
|
+
failureSummary: "Meta refresh redirect detected",
|
|
506
|
+
},
|
|
507
|
+
fix: {
|
|
508
|
+
description: "Use server-side redirect or JavaScript",
|
|
509
|
+
code: "// Server-side (e.g., Apache .htaccess)\nRedirect 301 /old-page /new-page\n\n// Or JavaScript\nwindow.location.href = '/new-page';",
|
|
510
|
+
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections",
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Check for vulnerable JavaScript libraries
|
|
518
|
+
*/
|
|
519
|
+
function checkVulnerableLibraries(page) {
|
|
520
|
+
const issues = [];
|
|
521
|
+
const document = page.document;
|
|
522
|
+
const vulnerableLibs = [
|
|
523
|
+
{ pattern: /jquery[-.@]([0-9.]+)/i, name: "jQuery", maxSafeVersion: "3.5.0" },
|
|
524
|
+
{ pattern: /lodash[-.@]([0-9.]+)/i, name: "Lodash", maxSafeVersion: "4.17.21" },
|
|
525
|
+
{ pattern: /angular[-.@]1\.([0-9.]+)/i, name: "Angular 1.x", maxSafeVersion: null },
|
|
526
|
+
{ pattern: /bootstrap[-.@]([0-9.]+)/i, name: "Bootstrap", maxSafeVersion: "3.4.0" },
|
|
527
|
+
];
|
|
528
|
+
const scripts = document.querySelectorAll("script[src]");
|
|
529
|
+
scripts.forEach((script) => {
|
|
530
|
+
const src = script.getAttribute("src") || "";
|
|
531
|
+
vulnerableLibs.forEach(({ pattern, name, maxSafeVersion }) => {
|
|
532
|
+
const match = src.match(pattern);
|
|
533
|
+
if (match) {
|
|
534
|
+
const version = match[1];
|
|
535
|
+
let isVulnerable = false;
|
|
536
|
+
if (maxSafeVersion === null) {
|
|
537
|
+
isVulnerable = true; // e.g., Angular 1.x is always flagged
|
|
538
|
+
}
|
|
539
|
+
else if (version) {
|
|
540
|
+
isVulnerable = compareVersions(version, maxSafeVersion) < 0;
|
|
541
|
+
}
|
|
542
|
+
if (isVulnerable) {
|
|
543
|
+
issues.push({
|
|
544
|
+
id: `bp-vulnerable-lib-${name}-${Date.now()}`,
|
|
545
|
+
ruleId: "vulnerable-library",
|
|
546
|
+
severity: "critical",
|
|
547
|
+
category: "technical",
|
|
548
|
+
message: `Vulnerable ${name} version detected`,
|
|
549
|
+
description: `The page uses ${name} ${version || 'unknown version'}, which has known security vulnerabilities. Update to the latest version.`,
|
|
550
|
+
helpUrl: "https://snyk.io/vuln/",
|
|
551
|
+
wcag: {
|
|
552
|
+
id: "N/A",
|
|
553
|
+
level: "AAA",
|
|
554
|
+
name: "Security Best Practice",
|
|
555
|
+
description: "Avoid vulnerable libraries",
|
|
556
|
+
},
|
|
557
|
+
element: {
|
|
558
|
+
selector: buildSelector(script),
|
|
559
|
+
html: script.outerHTML,
|
|
560
|
+
failureSummary: `${name} ${version} has known vulnerabilities`,
|
|
561
|
+
},
|
|
562
|
+
fix: {
|
|
563
|
+
description: `Update ${name} to the latest secure version`,
|
|
564
|
+
code: `// Update to latest version\n// Check https://snyk.io/vuln/ for details\n// npm update ${name.toLowerCase()}`,
|
|
565
|
+
learnMoreUrl: "https://snyk.io/vuln/",
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
return issues;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Check for password paste prevention
|
|
576
|
+
*/
|
|
577
|
+
function checkPasswordPaste(page) {
|
|
578
|
+
const issues = [];
|
|
579
|
+
const document = page.document;
|
|
580
|
+
const passwordFields = document.querySelectorAll('input[type="password"]');
|
|
581
|
+
passwordFields.forEach((field, index) => {
|
|
582
|
+
const onpaste = field.getAttribute("onpaste");
|
|
583
|
+
if (onpaste && onpaste.includes("return false")) {
|
|
584
|
+
issues.push({
|
|
585
|
+
id: `bp-password-no-paste-${index}`,
|
|
586
|
+
ruleId: "password-paste-blocked",
|
|
587
|
+
severity: "serious",
|
|
588
|
+
category: "forms",
|
|
589
|
+
message: "Password field blocks pasting",
|
|
590
|
+
description: "Preventing password pasting discourages the use of password managers, weakening security. Users should be allowed to paste passwords.",
|
|
591
|
+
helpUrl: "https://www.ncsc.gov.uk/blog-post/let-them-paste-passwords",
|
|
592
|
+
wcag: {
|
|
593
|
+
id: "N/A",
|
|
594
|
+
level: "AAA",
|
|
595
|
+
name: "Security Best Practice",
|
|
596
|
+
description: "Allow password pasting",
|
|
597
|
+
},
|
|
598
|
+
element: {
|
|
599
|
+
selector: buildSelector(field),
|
|
600
|
+
html: field.outerHTML,
|
|
601
|
+
failureSummary: "Password field prevents pasting",
|
|
602
|
+
},
|
|
603
|
+
fix: {
|
|
604
|
+
description: "Remove onpaste restriction",
|
|
605
|
+
code: '<!-- Allow pasting -->\n<input type="password" name="password">',
|
|
606
|
+
learnMoreUrl: "https://www.ncsc.gov.uk/blog-post/let-them-paste-passwords",
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
return issues;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Check for intrusive permission requests
|
|
615
|
+
*/
|
|
616
|
+
function checkIntrusivePermissions(page) {
|
|
617
|
+
const issues = [];
|
|
618
|
+
const document = page.document;
|
|
619
|
+
// Check for geolocation API calls in inline scripts
|
|
620
|
+
const inlineScripts = document.querySelectorAll("script:not([src])");
|
|
621
|
+
inlineScripts.forEach((script, index) => {
|
|
622
|
+
const content = script.textContent || "";
|
|
623
|
+
if (content.includes("navigator.geolocation")) {
|
|
624
|
+
issues.push({
|
|
625
|
+
id: `bp-geolocation-${index}`,
|
|
626
|
+
ruleId: "intrusive-geolocation",
|
|
627
|
+
severity: "moderate",
|
|
628
|
+
category: "technical",
|
|
629
|
+
message: "Page may request geolocation on load",
|
|
630
|
+
description: "Requesting geolocation permission immediately on page load is intrusive. Request permissions only when needed and in response to user actions.",
|
|
631
|
+
helpUrl: "https://web.dev/permission-ux/",
|
|
632
|
+
wcag: {
|
|
633
|
+
id: "N/A",
|
|
634
|
+
level: "AAA",
|
|
635
|
+
name: "UX Best Practice",
|
|
636
|
+
description: "Request permissions contextually",
|
|
637
|
+
},
|
|
638
|
+
element: {
|
|
639
|
+
selector: buildSelector(script),
|
|
640
|
+
html: script.outerHTML.substring(0, 200),
|
|
641
|
+
failureSummary: "Geolocation API called in script",
|
|
642
|
+
},
|
|
643
|
+
fix: {
|
|
644
|
+
description: "Request geolocation only when user initiates action",
|
|
645
|
+
code: "// Request on user action\nbutton.addEventListener('click', () => {\n navigator.geolocation.getCurrentPosition(...);\n});",
|
|
646
|
+
learnMoreUrl: "https://web.dev/permission-ux/",
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (content.includes("Notification.requestPermission")) {
|
|
651
|
+
issues.push({
|
|
652
|
+
id: `bp-notification-${index}`,
|
|
653
|
+
ruleId: "intrusive-notification",
|
|
654
|
+
severity: "moderate",
|
|
655
|
+
category: "technical",
|
|
656
|
+
message: "Page may request notification permission on load",
|
|
657
|
+
description: "Requesting notification permission immediately is intrusive and often denied. Request permissions contextually after user engagement.",
|
|
658
|
+
helpUrl: "https://web.dev/permission-ux/",
|
|
659
|
+
wcag: {
|
|
660
|
+
id: "N/A",
|
|
661
|
+
level: "AAA",
|
|
662
|
+
name: "UX Best Practice",
|
|
663
|
+
description: "Request permissions contextually",
|
|
664
|
+
},
|
|
665
|
+
element: {
|
|
666
|
+
selector: buildSelector(script),
|
|
667
|
+
html: script.outerHTML.substring(0, 200),
|
|
668
|
+
failureSummary: "Notification permission requested in script",
|
|
669
|
+
},
|
|
670
|
+
fix: {
|
|
671
|
+
description: "Request notification permission in response to user action",
|
|
672
|
+
code: "// Request on user action\nbutton.addEventListener('click', async () => {\n const permission = await Notification.requestPermission();\n // Handle permission\n});",
|
|
673
|
+
learnMoreUrl: "https://web.dev/permission-ux/",
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
return issues;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Compare semantic versions
|
|
682
|
+
*/
|
|
683
|
+
function compareVersions(v1, v2) {
|
|
684
|
+
const parts1 = v1.split(".").map(Number);
|
|
685
|
+
const parts2 = v2.split(".").map(Number);
|
|
686
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
687
|
+
const part1 = parts1[i] || 0;
|
|
688
|
+
const part2 = parts2[i] || 0;
|
|
689
|
+
if (part1 < part2)
|
|
690
|
+
return -1;
|
|
691
|
+
if (part1 > part2)
|
|
692
|
+
return 1;
|
|
693
|
+
}
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=best-practices.js.map
|