From 12066decf77268c485e0e28edea8567f6c055dab Mon Sep 17 00:00:00 2001 From: coliff Date: Fri, 6 Jun 2025 00:02:33 +0900 Subject: [PATCH] Add new rule: `frame-title-require` --- .cspell.json | 1 + dist/core/rules/index.js | 6 +- src/core/rules/frame-title-require.ts | 39 +++++ src/core/rules/index.ts | 1 + test/rules/frame-title-require.spec.js | 139 ++++++++++++++++++ website/src/content/docs/list-rules.md | 1 + .../docs/rules/frame-title-require.mdx | 38 +++++ 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 src/core/rules/frame-title-require.ts create mode 100644 test/rules/frame-title-require.spec.js create mode 100644 website/src/content/docs/rules/frame-title-require.mdx diff --git a/.cspell.json b/.cspell.json index b63732f29..9332da030 100644 --- a/.cspell.json +++ b/.cspell.json @@ -32,6 +32,7 @@ "idclassaddisabled", "Infima", "invision", + "Labelledby", "langtag", "mingo", "msapplication", diff --git a/dist/core/rules/index.js b/dist/core/rules/index.js index 32478ed10..833d2822e 100644 --- a/dist/core/rules/index.js +++ b/dist/core/rules/index.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.titleRequire = exports.tagSelfClose = exports.tagsCheck = exports.tagPair = exports.tagnameSpecialChars = exports.tagnameLowercase = exports.styleDisabled = exports.srcNotEmpty = exports.specCharEscape = exports.spaceTabMixedDisabled = exports.scriptDisabled = exports.metaViewportRequire = exports.metaDescriptionRequire = exports.metaCharsetRequire = exports.mainRequire = exports.inputRequiresLabel = exports.inlineStyleDisabled = exports.inlineScriptDisabled = exports.idUnique = exports.idClassValue = exports.idClassAdDisabled = exports.htmlLangRequire = exports.hrefAbsOrRel = exports.headScriptDisabled = exports.h1Require = exports.emptyTagNotSelfClosed = exports.doctypeHTML5 = exports.doctypeFirst = exports.buttonTypeRequire = exports.attrWhitespace = exports.attrValueSingleQuotes = exports.attrValueNotEmpty = exports.attrValueDoubleQuotes = exports.attrUnsafeChars = exports.attrSort = exports.attrNoUnnecessaryWhitespace = exports.attrNoDuplication = exports.attrLowercase = exports.altRequire = void 0; +exports.titleRequire = exports.tagSelfClose = exports.tagsCheck = exports.tagPair = exports.tagnameSpecialChars = exports.tagnameLowercase = exports.styleDisabled = exports.srcNotEmpty = exports.specCharEscape = exports.spaceTabMixedDisabled = exports.scriptDisabled = exports.metaViewportRequire = exports.metaDescriptionRequire = exports.metaCharsetRequire = exports.mainRequire = exports.inputRequiresLabel = exports.inlineStyleDisabled = exports.inlineScriptDisabled = exports.idUnique = exports.idClassValue = exports.idClassAdDisabled = exports.htmlLangRequire = exports.hrefAbsOrRel = exports.headScriptDisabled = exports.h1Require = exports.frameTitleRequire = exports.emptyTagNotSelfClosed = exports.doctypeHTML5 = exports.doctypeFirst = exports.buttonTypeRequire = exports.attrWhitespace = exports.attrValueSingleQuotes = exports.attrValueNotEmpty = exports.attrValueDoubleQuotes = exports.attrUnsafeChars = exports.attrSort = exports.attrNoUnnecessaryWhitespace = exports.attrNoDuplication = exports.attrLowercase = exports.altRequire = void 0; var alt_require_1 = require("./alt-require"); Object.defineProperty(exports, "altRequire", { enumerable: true, get: function () { return alt_require_1.default; } }); var attr_lowercase_1 = require("./attr-lowercase"); @@ -29,6 +29,8 @@ var doctype_html5_1 = require("./doctype-html5"); Object.defineProperty(exports, "doctypeHTML5", { enumerable: true, get: function () { return doctype_html5_1.default; } }); var empty_tag_not_self_closed_1 = require("./empty-tag-not-self-closed"); Object.defineProperty(exports, "emptyTagNotSelfClosed", { enumerable: true, get: function () { return empty_tag_not_self_closed_1.default; } }); +var frame_title_require_1 = require("./frame-title-require"); +Object.defineProperty(exports, "frameTitleRequire", { enumerable: true, get: function () { return frame_title_require_1.default; } }); var h1_require_1 = require("./h1-require"); Object.defineProperty(exports, "h1Require", { enumerable: true, get: function () { return h1_require_1.default; } }); var head_script_disabled_1 = require("./head-script-disabled"); @@ -79,4 +81,4 @@ var tag_self_close_1 = require("./tag-self-close"); Object.defineProperty(exports, "tagSelfClose", { enumerable: true, get: function () { return tag_self_close_1.default; } }); var title_require_1 = require("./title-require"); Object.defineProperty(exports, "titleRequire", { enumerable: true, get: function () { return title_require_1.default; } }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBcUQ7QUFBNUMseUdBQUEsT0FBTyxPQUFjO0FBQzlCLG1EQUEyRDtBQUFsRCwrR0FBQSxPQUFPLE9BQWlCO0FBQ2pDLDZEQUFvRTtBQUEzRCx3SEFBQSxPQUFPLE9BQXFCO0FBQ3JDLG1GQUF5RjtBQUFoRiw2SUFBQSxPQUFPLE9BQStCO0FBQy9DLDZDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQVk7QUFDNUIseURBQWdFO0FBQXZELG9IQUFBLE9BQU8sT0FBbUI7QUFDbkMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsK0RBQXFFO0FBQTVELHlIQUFBLE9BQU8sT0FBcUI7QUFDckMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMseUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBeUI7QUFDekMsMkNBQW1EO0FBQTFDLHVHQUFBLE9BQU8sT0FBYTtBQUM3QiwrREFBc0U7QUFBN0QsMEhBQUEsT0FBTyxPQUFzQjtBQUN0QyxxREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFnQjtBQUNoQyx5REFBZ0U7QUFBdkQsb0hBQUEsT0FBTyxPQUFtQjtBQUNuQywrREFBcUU7QUFBNUQseUhBQUEsT0FBTyxPQUFxQjtBQUNyQyxtREFBMEQ7QUFBakQsOEdBQUEsT0FBTyxPQUFnQjtBQUNoQyx5Q0FBaUQ7QUFBeEMscUdBQUEsT0FBTyxPQUFZO0FBQzVCLG1FQUEwRTtBQUFqRSw4SEFBQSxPQUFPLE9BQXdCO0FBQ3hDLGlFQUF3RTtBQUEvRCw0SEFBQSxPQUFPLE9BQXVCO0FBQ3ZDLCtEQUFzRTtBQUE3RCwwSEFBQSxPQUFPLE9BQXNCO0FBQ3RDLCtDQUF1RDtBQUE5QywyR0FBQSxPQUFPLE9BQWU7QUFDL0IsK0RBQXNFO0FBQTdELDBIQUFBLE9BQU8sT0FBc0I7QUFDdEMsdUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBMEI7QUFDMUMsaUVBQXdFO0FBQS9ELDRIQUFBLE9BQU8sT0FBdUI7QUFDdkMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsdURBQThEO0FBQXJELGtIQUFBLE9BQU8sT0FBa0I7QUFDbEMsaURBQXdEO0FBQS9DLDRHQUFBLE9BQU8sT0FBZTtBQUMvQixtREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFpQjtBQUNqQyx5REFBaUU7QUFBeEQscUhBQUEsT0FBTyxPQUFvQjtBQUNwQywrREFBdUU7QUFBOUQsMkhBQUEsT0FBTyxPQUF1QjtBQUN2Qyx1Q0FBK0M7QUFBdEMsbUdBQUEsT0FBTyxPQUFXO0FBQzNCLDJDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQWE7QUFDN0IsbURBQTBEO0FBQWpELDhHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0IifQ== \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBcUQ7QUFBNUMseUdBQUEsT0FBTyxPQUFjO0FBQzlCLG1EQUEyRDtBQUFsRCwrR0FBQSxPQUFPLE9BQWlCO0FBQ2pDLDZEQUFvRTtBQUEzRCx3SEFBQSxPQUFPLE9BQXFCO0FBQ3JDLG1GQUF5RjtBQUFoRiw2SUFBQSxPQUFPLE9BQStCO0FBQy9DLDZDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQVk7QUFDNUIseURBQWdFO0FBQXZELG9IQUFBLE9BQU8sT0FBbUI7QUFDbkMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsK0RBQXFFO0FBQTVELHlIQUFBLE9BQU8sT0FBcUI7QUFDckMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMseUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBeUI7QUFDekMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsMkNBQW1EO0FBQTFDLHVHQUFBLE9BQU8sT0FBYTtBQUM3QiwrREFBc0U7QUFBN0QsMEhBQUEsT0FBTyxPQUFzQjtBQUN0QyxxREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFnQjtBQUNoQyx5REFBZ0U7QUFBdkQsb0hBQUEsT0FBTyxPQUFtQjtBQUNuQywrREFBcUU7QUFBNUQseUhBQUEsT0FBTyxPQUFxQjtBQUNyQyxtREFBMEQ7QUFBakQsOEdBQUEsT0FBTyxPQUFnQjtBQUNoQyx5Q0FBaUQ7QUFBeEMscUdBQUEsT0FBTyxPQUFZO0FBQzVCLG1FQUEwRTtBQUFqRSw4SEFBQSxPQUFPLE9BQXdCO0FBQ3hDLGlFQUF3RTtBQUEvRCw0SEFBQSxPQUFPLE9BQXVCO0FBQ3ZDLCtEQUFzRTtBQUE3RCwwSEFBQSxPQUFPLE9BQXNCO0FBQ3RDLCtDQUF1RDtBQUE5QywyR0FBQSxPQUFPLE9BQWU7QUFDL0IsK0RBQXNFO0FBQTdELDBIQUFBLE9BQU8sT0FBc0I7QUFDdEMsdUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBMEI7QUFDMUMsaUVBQXdFO0FBQS9ELDRIQUFBLE9BQU8sT0FBdUI7QUFDdkMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsdURBQThEO0FBQXJELGtIQUFBLE9BQU8sT0FBa0I7QUFDbEMsaURBQXdEO0FBQS9DLDRHQUFBLE9BQU8sT0FBZTtBQUMvQixtREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFpQjtBQUNqQyx5REFBaUU7QUFBeEQscUhBQUEsT0FBTyxPQUFvQjtBQUNwQywrREFBdUU7QUFBOUQsMkhBQUEsT0FBTyxPQUF1QjtBQUN2Qyx1Q0FBK0M7QUFBdEMsbUdBQUEsT0FBTyxPQUFXO0FBQzNCLDJDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQWE7QUFDN0IsbURBQTBEO0FBQWpELDhHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0IifQ== \ No newline at end of file diff --git a/src/core/rules/frame-title-require.ts b/src/core/rules/frame-title-require.ts new file mode 100644 index 000000000..816b1aa28 --- /dev/null +++ b/src/core/rules/frame-title-require.ts @@ -0,0 +1,39 @@ +import { Rule } from '../types' + +export default { + id: 'frame-title-require', + description: 'A or ' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Frame with aria-labelledby should not result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Iframe with aria-labelledby should not result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Frame with title should not result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Iframe with title should not result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Frame with role="presentation" should not result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Iframe with role="none" should not result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Frame without accessible name should result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(7) + expect(messages[0].type).toBe('warning') + expect(messages[0].message).toBe( + 'A element must have an accessible name.' + ) + }) + + it('Iframe without accessible name should result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(8) + expect(messages[0].type).toBe('warning') + expect(messages[0].message).toBe( + 'A ' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(8) + expect(messages[0].type).toBe('warning') + }) + + it('Frame with whitespace-only aria-label should result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(7) + expect(messages[0].type).toBe('warning') + }) + + it('Iframe with whitespace-only aria-labelledby should result in an error', () => { + const code = '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(8) + expect(messages[0].type).toBe('warning') + }) + + it('Multiple iframes - one valid, one invalid should result in one error', () => { + const code = + '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(1) + expect(messages[0].rule.id).toBe(ruleId) + expect(messages[0].line).toBe(1) + expect(messages[0].col).toBe(62) + expect(messages[0].type).toBe('warning') + }) + + it('Other elements should not be affected', () => { + const code = '

Content

' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) +}) diff --git a/website/src/content/docs/list-rules.md b/website/src/content/docs/list-rules.md index a5a571744..e8d97abdb 100644 --- a/website/src/content/docs/list-rules.md +++ b/website/src/content/docs/list-rules.md @@ -31,6 +31,7 @@ draft: true - [`attr-value-single-quotes`](/rules/attr-value-single-quotes): Attribute values must be in single quotes. - [`attr-whitespace`](/rules/attr-whitespace): No leading or trailing spaces in attribute values. - [`button-type-require`](/rules/button-type-require): The type attribute of a button element must be present with a valid value: "button", "submit", or "reset". +- [`frame-title-require`](/rules/frame-title-require): A `` or ` + + + + +``` + +### The following patterns are considered rule violations + +```html + + + + +```