Skip to content

Commit

Permalink
chore(htmlcs): fix input missing label detection
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mendez committed Aug 9, 2023
1 parent 7968210 commit 31edb80
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 49 deletions.
3 changes: 2 additions & 1 deletion fast_htmlcs/HTMLCS.Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,8 @@ _global.HTMLCS.util = {
red: parseInt(colour.substring(0, 2), 16) / 255,
green: parseInt(colour.substring(2, 4), 16) / 255,
blue: parseInt(colour.substring(4, 6), 16) / 255,
alpha: colour.length === 8 ? parseInt(colour.substring(6, 8), 16) / 255 : 1,
alpha:
colour.length === 8 ? parseInt(colour.substring(6, 8), 16) / 255 : 1,
};
}

Expand Down
11 changes: 5 additions & 6 deletions fast_htmlcs/HTMLCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ _global.HTMLCS = new (function () {
_standards.size && _standards.clear();
_tags.size && _tags.clear();
_duplicates.size && _duplicates.clear();

this.messages = [];

if (typeof _global.translation[language] !== "undefined") {
Expand Down Expand Up @@ -98,7 +98,7 @@ _global.HTMLCS = new (function () {
this.run = function (callback, content) {
let element = null;
let loadingFrame = false;

// todo: remove iframe handling
if (typeof content === "string") {
loadingFrame = true;
Expand Down Expand Up @@ -162,7 +162,7 @@ _global.HTMLCS = new (function () {
// Get all the elements in the parent element.
// Add the parent element too, which will trigger "_top" element codes.
const elements = HTMLCS.util.getAllElements(element);

elements.unshift(element);
_run(elements, element, callback || function () {});
}
Expand Down Expand Up @@ -233,7 +233,6 @@ _global.HTMLCS = new (function () {
});
} else {
const pos = _duplicates.get(textId);
// increment the recurrence counter.
this.messages[pos].recurrence = this.messages[pos].recurrence + 1;
}
};
Expand Down Expand Up @@ -302,7 +301,7 @@ _global.HTMLCS = new (function () {

_currentSniff = sniff;

if (sniff.useCallback === true) {
if (sniff.useCallback) {
// If the useCallback property is set:
// - Process the sniff.
// - Recurse into ourselves with remaining sniffs, with no callback.
Expand All @@ -318,7 +317,7 @@ _global.HTMLCS = new (function () {
}
}

if (callback instanceof Function === true) {
if (callback instanceof Function) {
callback.call(this);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,20 +221,19 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle1_Guideline1_3_1_3_1 = {
* @param {DOMNode} top The top element of the tested code.
*/
testLabelsOnInputs: function (element, _, muteErrors) {
let inputType = element.nodeName;
let inputType = element.nodeName.toLowerCase();

if (inputType === "INPUT") {
if (inputType === "input") {
if (element.hasAttribute("type")) {
// can return `hidden`
inputType = element.getAttribute("type");
inputType = element.getAttribute("type").toLowerCase();
} else {
inputType = "TEXT";
inputType = "text";
}
}

let hasLabel = undefined;
let hasLabel: boolean | Record<string, any> = false;

const addToLabelList = function (found) {
let addToLabelList = function (found) {
if (!hasLabel) {
hasLabel = {};
}
Expand All @@ -244,9 +243,11 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle1_Guideline1_3_1_3_1 = {
// Firstly, work out whether it needs a label.
let needsLabel = false;

if (inputType === "SELECT" || inputType === "TEXTAREA") {
if (inputType === "select" || inputType === "textarea") {
needsLabel = true;
} else if (/^(RADIO|CHECKBOX|TEXT|FILE|PASSWORD)$/.test(inputType)) {
} else if (
/^(radio|checkbox|text|file|password)$/.test(inputType) === true
) {
needsLabel = true;
}

Expand All @@ -255,34 +256,33 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle1_Guideline1_3_1_3_1 = {
}

// Find an explicit label.
const explicitLabel = element.ownerDocument.querySelector(
let explicitLabel = element.ownerDocument.querySelector(
'label[for="' + element.id + '"]'
);

if (explicitLabel) {
addToLabelList("explicit");
}

// Find an implicit label.
let foundImplicit = false as boolean;

let foundImplicit = false;
if (element.parentNode) {
HTMLCS.util.eachParentNode(element, function (parent) {
if (parent.nodeName === "LABEL") {
if (parent.nodeName.toLowerCase() === "label") {
foundImplicit = true;
}
});
}

if (foundImplicit) {
// @ts-ignore
if (foundImplicit === true) {
addToLabelList("implicit");
}

// Find a title attribute.
const title = element.getAttribute("title");
let title = element.getAttribute("title");

if (title !== null) {
if (/^\s*$/.test(title) && needsLabel) {
if (/^\s*$/.test(title) === true && needsLabel === true) {
HTMLCS.addMessage(
HTMLCS.WARNING,
element,
Expand All @@ -295,7 +295,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle1_Guideline1_3_1_3_1 = {
}

// Find an aria-label attribute.
if (element.hasAttribute("aria-label")) {
if (element.hasAttribute("aria-label") === true) {
if (HTMLCS.util.hasValidAriaLabel(element) === false) {
HTMLCS.addMessage(
HTMLCS.WARNING,
Expand All @@ -309,8 +309,8 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle1_Guideline1_3_1_3_1 = {
}

// Find an aria-labelledby attribute.
if (element.hasAttribute("aria-labelledby")) {
if (!HTMLCS.util.hasValidAriaLabel(element)) {
if (element.hasAttribute("aria-labelledby") === true) {
if (HTMLCS.util.hasValidAriaLabel(element) === false) {
HTMLCS.addMessage(
HTMLCS.WARNING,
element,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,37 +62,37 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
);
}

for (var i = 0; i < errors.emptyNoId.length; i++) {
for (const emptyNoId of errors.emptyNoId) {
HTMLCS.addMessage(
HTMLCS.ERROR,
errors.emptyNoId[i],
emptyNoId,
_global.HTMLCS.getTranslation("4_1_2_H91.A.EmptyNoId"),
"H91.A.EmptyNoId"
);
}

for (var i = 0; i < errors.noHref.length; i++) {
for (const noHref of errors.noHref) {
HTMLCS.addMessage(
HTMLCS.WARNING,
errors.noHref[i],
noHref,
_global.HTMLCS.getTranslation("4_1_2_H91.A.NoHref"),
"H91.A.NoHref"
);
}

for (var i = 0; i < errors.placeholder.length; i++) {
for (const placeholder of errors.placeholder) {
HTMLCS.addMessage(
HTMLCS.WARNING,
errors.placeholder[i],
placeholder,
_global.HTMLCS.getTranslation("4_1_2_H91.A.Placeholder"),
"H91.A.Placeholder"
);
}

for (var i = 0; i < errors.noContent.length; i++) {
for (const noContent of errors.noContent) {
HTMLCS.addMessage(
HTMLCS.ERROR,
errors.noContent[i],
noContent,
_global.HTMLCS.getTranslation("4_1_2_H91.A.NoContent"),
"H91.A.NoContent"
);
Expand Down Expand Up @@ -133,7 +133,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
hrefFound = true;
}

if (hrefFound === false) {
if (!hrefFound) {
// No href. We don't want these because, although they are commonly used
// to create targets, they can be picked up by screen readers and
// displayed to the user as empty links. A elements are defined by H91 as
Expand All @@ -152,10 +152,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
// Giving a benefit of the doubt here - if a link has text and also
// an ID, but no href, it might be because it is being manipulated by
// a script.
if (
element.hasAttribute("id") === true ||
element.hasAttribute("name") === true
) {
if (element.hasAttribute("id") || element.hasAttribute("name")) {
errors.noHref.push(element);
} else {
// HTML5 allows A elements with text but no href, "for where a
Expand All @@ -171,8 +168,8 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
// A link around an image with no alt text is already covered in SC
// 1.1.1 (test H30).
if (
element.querySelectorAll("img").length === 0 &&
HTMLCS.util.hasValidAriaLabel(element) === false
!element.querySelectorAll("img").length &&
!HTMLCS.util.hasValidAriaLabel(element)
) {
errors.noContent.push(element);
}
Expand Down Expand Up @@ -233,12 +230,13 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
top,
'button, fieldset, input, select, textarea, [role="button"]'
)) {
var nodeName = element.nodeName.toLowerCase();
let nodeName = element.nodeName.toLowerCase();
let msgSubCode =
element.nodeName.substring(0, 1).toUpperCase() +
element.nodeName.substring(1).toLowerCase();

if (nodeName === "input") {
if (element.hasAttribute("type") === false) {
if (!element.hasAttribute("type")) {
// If no type attribute, default to text.
nodeName += "_text";
} else {
Expand Down Expand Up @@ -267,8 +265,11 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {

// Check all possible combinations of names to ensure that one exists.
if (matchingRequiredNames) {
for (var i = 0; i < matchingRequiredNames.length; i++) {
let i = 0;

for (; i < matchingRequiredNames.length; i++) {
let requiredName = matchingRequiredNames[i];

if (requiredName === "_content") {
// Work with content.
if (
Expand Down Expand Up @@ -318,6 +319,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
}
}

// all items processed
if (i === matchingRequiredNames.length) {
let msgNodeType =
nodeName + " " + _global.HTMLCS.getTranslation("4_1_2_element");
Expand All @@ -331,6 +333,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
0,
matchingRequiredNames.length
);

for (let a = 0; a < builtAttrs.length; a++) {
if (builtAttrs[a] === "_content") {
builtAttrs[a] = _global.HTMLCS.getTranslation(
Expand All @@ -352,6 +355,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
let msg = _global.HTMLCS.getTranslation("4_1_2_msg_pattern")
.replace(/\{\{msgNodeType\}\}/g, msgNodeType)
.replace(/\{\{builtAttrs\}\}/g, builtAttrs.join(", "));

if (
element.hasAttribute("role") &&
element.getAttribute("role") === "button"
Expand All @@ -360,6 +364,7 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
"4_1_2_msg_pattern_role_of_button"
).replace(/\{\{builtAttrs\}\}/g, builtAttrs.join(", "));
}

errors.push({
element: element,
msg: msg,
Expand All @@ -381,8 +386,9 @@ _global.HTMLCS_WCAG2AAA_Sniffs_Principle4_Guideline4_1_4_1_2 = {
}
} else if (requiredValue === "option_selected") {
// Select lists are recommended to have a selected Option element.
if (element.hasAttribute("multiple") === false) {
var selected = element.querySelector("option[selected]");
if (!element.hasAttribute("multiple")) {
const selected = element.querySelector("option[selected]");

if (selected !== null) {
valueFound = true;
}
Expand Down
2 changes: 1 addition & 1 deletion fast_htmlcs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fast_htmlcs",
"version": "0.0.63",
"version": "0.0.65",
"description": "A high performance fork of HTML_CodeSniffer.",
"license": "BSD-3-Clause",
"main": "index.js",
Expand Down
3 changes: 2 additions & 1 deletion kayle/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kayle",
"version": "0.5.28",
"version": "0.5.30",
"description": "Extremely fast and accurate accessibility testing using CDP",
"main": "./build/index.js",
"keywords": [
Expand Down Expand Up @@ -35,6 +35,7 @@
"test:puppeteer": "npm run compile:test && node _tests/tests/basic.js",
"test:puppeteer:axe": "npm run compile:test && node _tests/tests/basic-axe.js",
"test:puppeteer:htmlcs": "npm run compile:test && node _tests/tests/basic-htmlcs.js",
"test:missing": "npm run compile:test && node _tests/tests/missing.js",
"test:playwright": "npm run compile:test && npx playwright test ./tests/basic-playwright.spec.ts",
"test:playwright:axe": "npm run compile:test && npx playwright test ./tests/basic-axe-playwright.spec.ts",
"test:playwright:htmlcs": "npm run compile:test && npx playwright test ./tests/basic-htmlcs-playwright.spec",
Expand Down
48 changes: 48 additions & 0 deletions kayle/tests/missing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// test critical missing alt, input label, and etc.

import assert from "assert";
import puppeteer from "puppeteer";
import { kayle } from "kayle";
import { performance } from "perf_hooks";

const defaultHTML = `<!DOCTYPE html>
<html>
<body>
<h1>Testing Accessibility</h1>
<p>Is this inclusive?</p>
<input type="text" value="something"></input>
<img src="/something" type="text" />
</body>
</html>`;

(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
if (process.env.LOG_ENABLED) {
page.on("console", (msg) => console.log("PAGE LOG:", msg.text()));
}
const startTime = performance.now();
const { issues, pageUrl, documentTitle, meta, automateable } = await kayle({
page,
browser,
runners: ["htmlcs"],
includeWarnings: true,
html: defaultHTML,
origin: "http://www.myspace.com", // origin is the fake url in place of the raw content
});
const nextTime = performance.now() - startTime;

console.log(meta);
console.log(automateable);
console.log("time took", nextTime);

assert(Array.isArray(issues));
assert(meta.errorCount === 5);
assert(meta.warningCount === 0);
assert(meta.accessScore === 100); // TODO: add alt missing to access scoring

assert(typeof pageUrl === "string");
assert(typeof documentTitle === "string");

await browser.close();
})();

0 comments on commit 31edb80

Please sign in to comment.