diff --git a/lib/rules/template-require-iframe-title.js b/lib/rules/template-require-iframe-title.js index d4f4b2f636..2ee9ebceee 100644 --- a/lib/rules/template-require-iframe-title.js +++ b/lib/rules/template-require-iframe-title.js @@ -1,3 +1,22 @@ +// Mustache path nodes that produce no accessible name. Booleans, null, undefined +// all coerce to empty-ish strings; numeric literals ("42") are accepted by HTML +// but provide no useful title for assistive tech. +function isInvalidTitleLiteral(path) { + if (!path) { + return false; + } + if (path.type === 'GlimmerBooleanLiteral') { + return true; + } + if (path.type === 'GlimmerNullLiteral' || path.type === 'GlimmerUndefinedLiteral') { + return true; + } + if (path.type === 'GlimmerNumberLiteral') { + return true; + } + return false; +} + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -93,19 +112,21 @@ module.exports = { break; } case 'GlimmerMustacheStatement': { - // title={{false}} → BooleanLiteral false is invalid - if (titleAttr.value.path?.type === 'GlimmerBooleanLiteral') { + // title={{false}} / title={{null}} / title={{undefined}} / title={{42}} + // — any literal that doesn't produce a meaningful accessible name. + if (isInvalidTitleLiteral(titleAttr.value.path)) { context.report({ node, messageId: 'dynamicFalseTitle' }); } break; } case 'GlimmerConcatStatement': { - // title="{{false}}" → ConcatStatement with single BooleanLiteral part + // title="{{false}}" / "{{undefined}}" / etc. — ConcatStatement + // with a single literal part that doesn't produce a name. const parts = titleAttr.value.parts || []; if ( parts.length === 1 && parts[0].type === 'GlimmerMustacheStatement' && - parts[0].path?.type === 'GlimmerBooleanLiteral' + isInvalidTitleLiteral(parts[0].path) ) { context.report({ node, messageId: 'dynamicFalseTitle' }); } diff --git a/tests/audit/iframe-title/peer-parity.js b/tests/audit/iframe-title/peer-parity.js new file mode 100644 index 0000000000..de95a37bd0 --- /dev/null +++ b/tests/audit/iframe-title/peer-parity.js @@ -0,0 +1,209 @@ +// Audit fixture — peer-plugin parity for `ember/template-require-iframe-title`. +// These tests are NOT part of the main suite and do not run in CI. They encode +// the CURRENT behavior of our rule so that running this file reports pass. +// Each divergence from an upstream plugin is annotated as "DIVERGENCE —". +// +// Source files (context/ checkouts): +// - eslint-plugin-jsx-a11y-main/__tests__/src/rules/iframe-has-title-test.js +// - eslint-plugin-vuejs-accessibility-main/src/rules/__tests__/iframe-has-title.test.ts +// - eslint-plugin-lit-a11y/tests/lib/rules/iframe-title.js + +'use strict'; + +const rule = require('../../../lib/rules/template-require-iframe-title'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('audit:iframe-title (gts)', rule, { + valid: [ + // === Upstream parity — basic cases === + // jsx-a11y / vue-a11y / lit-a11y: valid (no iframe, or titled iframe). + '', + '', + output: null, + errors: [{ messageId: 'missingTitle' }], + }, + + // === Upstream parity — empty title string === + // jsx-a11y, vue-a11y, lit-a11y all flag `title=""`. + { + code: '