Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(label-has-associated-control): add option for enforcing label's htmlFor matches control's id #1042

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions __mocks__/JSXFragmentMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @flow
*/

export type JSXFragmentMockType = {
type: 'JSXFragment',
children: Array<Node>,
};

export default function JSXFragmentMock(
children?: Array<Node> = [],
): JSXFragmentMockType {
return {
type: 'JSXFragment',
children,
};
}
33 changes: 33 additions & 0 deletions __tests__/src/rules/label-has-associated-control-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const errorMessages = {
nesting: 'A form label must have an associated control as a descendant.',
either: 'A form label must either have a valid htmlFor attribute or a control as a descendant.',
both: 'A form label must have a valid htmlFor attribute and a control as a descendant.',
htmlForShouldMatchId: 'A form label must have a htmlFor attribute that matches the id of the associated control.',
};
const expectedErrors = {};
Object.keys(errorMessages).forEach((key) => {
Expand Down Expand Up @@ -58,6 +59,7 @@ const htmlForValid = [
{ code: '<label htmlFor="js_id" aria-label="A label" />' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
{ code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>' },
{ code: '<div><label htmlFor={inputId}>A label</label><input id={inputId} /></div>' },
{ code: '<label for="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], settings: attributesSettings },
{ code: '<label for="js_id" aria-label="A label" />', settings: attributesSettings },
{ code: '<label for="js_id" aria-labelledby="A label" />', settings: attributesSettings },
Expand Down Expand Up @@ -128,6 +130,12 @@ const alwaysValid = [
{ code: '<input type="hidden" />' },
];

const shouldHtmlForMatchIdValid = [
{ code: '<label htmlFor="js_id" aria-label="A label"><span><span><input id="js_id" /></span></span></label>', options: [{ depth: 4, shouldHtmlForMatchId: true }] },
{ code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>', options: [{ shouldHtmlForMatchId: true }] },
{ code: '<div><label htmlFor={inputId} aria-label="A label" /><input id={inputId} /></div>', options: [{ shouldHtmlForMatchId: true }] },
];

const htmlForInvalid = (assertType) => {
const expectedError = expectedErrors[assertType];
return [
Expand Down Expand Up @@ -164,6 +172,13 @@ const nestingInvalid = (assertType) => {
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedError] },
];
};
const shouldHtmlForMatchIdInvalid = [
{ code: '<label htmlFor="js_id" aria-label="A label"><span><span><input /></span></span></label>', options: [{ depth: 4, shouldHtmlForMatchId: true }], errors: [expectedErrors.htmlForShouldMatchId] },
{ code: '<label htmlFor="js_id" aria-label="A label"><span><span><input name="js_id" /></span></span></label>', options: [{ depth: 4, shouldHtmlForMatchId: true }], errors: [expectedErrors.htmlForShouldMatchId] },
{ code: '<div><label htmlFor="js_id">A label</label><input /></div>', options: [{ shouldHtmlForMatchId: true }], errors: [expectedErrors.htmlForShouldMatchId] },
{ code: '<div><label htmlFor="js_id">A label</label><input name="js_id" /></div>', options: [{ shouldHtmlForMatchId: true }], errors: [expectedErrors.htmlForShouldMatchId] },
{ code: '<div><label htmlFor={inputId} aria-label="A label" /><input name={inputId} /></div>', options: [{ shouldHtmlForMatchId: true }], errors: [expectedErrors.htmlForShouldMatchId] },
];

const neverValid = (assertType) => {
const expectedError = expectedErrors[assertType];
Expand Down Expand Up @@ -266,3 +281,21 @@ ruleTester.run(ruleName, rule, {
assert: 'both',
})).map(parserOptionsMapper),
});

// shouldHtmlForMatchId
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...shouldHtmlForMatchIdValid,
))
.map(ruleOptionsMapperFactory({
assert: 'htmlFor',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...shouldHtmlForMatchIdInvalid,
))
.map(ruleOptionsMapperFactory({
assert: 'htmlFor',
}))
.map(parserOptionsMapper),
});
224 changes: 224 additions & 0 deletions __tests__/src/util/getChildComponent-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import test from 'tape';

import getChildComponent from '../../../src/util/getChildComponent';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';

test('mayContainChildComponent', (t) => {
t.equal(
getChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
'FancyComponent',
5,
),
undefined,
'no FancyComponent returns undefined',
);

t.test('contains an indicated component', (st) => {
const inputMock = JSXElementMock('input');
st.equal(
getChildComponent(
JSXElementMock('div', [], [
inputMock,
]),
'input',
),
inputMock,
'returns input',
);

const FancyComponentMock = JSXElementMock('FancyComponent');
st.equal(
getChildComponent(
JSXElementMock('div', [], [
FancyComponentMock,
]),
'FancyComponent',
),
FancyComponentMock,
'returns FancyComponent',
);

st.equal(
getChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
]),
'FancyComponent',
),
undefined,
'FancyComponent is outside of default depth, should return undefined',
);

st.equal(
getChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
FancyComponentMock,
]),
]),
'FancyComponent',
2,
),
FancyComponentMock,
'FancyComponent is inside of custom depth, should return FancyComponent',
);

st.equal(
getChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('span', [], [
FancyComponentMock,
]),
]),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
'FancyComponent',
6,
),
FancyComponentMock,
'deep nesting, returns FancyComponent',
);

st.end();
});

const MysteryBox = JSXExpressionContainerMock('mysteryBox');
t.equal(
getChildComponent(
JSXElementMock('div', [], [
MysteryBox,
]),
'FancyComponent',
),
MysteryBox,
'Indeterminate situations + expression container children - returns component',
);

t.test('Glob name matching - component name contains question mark ? - match any single character', (st) => {
const FancyComponentMock = JSXElementMock('FancyComponent');
st.equal(
getChildComponent(
JSXElementMock('div', [], [
FancyComponentMock,
]),
'Fanc?Co??onent',
),
FancyComponentMock,
'returns component',
);

st.equal(
getChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'FancyComponent?',
),
undefined,
'returns undefined',
);

st.test('component name contains asterisk * - match zero or more characters', (s2t) => {
s2t.equal(
getChildComponent(
JSXElementMock('div', [], [
FancyComponentMock,
]),
'Fancy*',
),
FancyComponentMock,
'returns component',
);

s2t.equal(
getChildComponent(
JSXElementMock('div', [], [
FancyComponentMock,
]),
'*Component',
),
FancyComponentMock,
'returns component',
);

s2t.equal(
getChildComponent(
JSXElementMock('div', [], [
FancyComponentMock,
]),
'Fancy*C*t',
),
FancyComponentMock,
'returns component',
);

s2t.end();
});

st.end();
});

t.test('using a custom elementType function', (st) => {
const CustomInputMock = JSXElementMock('CustomInput');
st.equal(
getChildComponent(
JSXElementMock('div', [], [
CustomInputMock,
]),
'input',
2,
() => 'input',
),
CustomInputMock,
'returns the component when the custom elementType returns the proper name',
);

st.equal(
getChildComponent(
JSXElementMock('div', [], [
JSXElementMock('CustomInput'),
]),
'input',
2,
() => 'button',
),
undefined,
'returns undefined when the custom elementType returns a wrong name',
);

st.end();
});

t.end();
});
Loading
Loading