eslint-plugin-smarthr 2.0.0 → 2.1.0
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/CHANGELOG.md +8 -0
- package/libs/format_styled_components.js +1 -1
- package/package.json +4 -4
- package/rules/a11y-image-has-alt-attribute/index.js +0 -1
- package/rules/a11y-prohibit-useless-sectioning-fragment/index.js +9 -44
- package/rules/a11y-trigger-has-button/index.js +18 -36
- package/rules/best-practice-for-async-current-target/index.js +14 -21
- package/rules/best-practice-for-button-element/index.js +10 -23
- package/rules/best-practice-for-data-test-attribute/index.js +9 -12
- package/rules/best-practice-for-date/index.js +16 -29
- package/rules/best-practice-for-nested-attributes-array-index/index.js +7 -15
- package/rules/best-practice-for-remote-trigger-dialog/index.js +10 -23
- package/rules/best-practice-for-tailwind-prohibit-root-margin/index.js +5 -93
- package/rules/design-system-guideline-prohibit-double-icons/index.js +6 -31
- package/rules/prohibit-export-array-type/index.js +6 -9
- package/rules/require-i18n-text/README.md +123 -0
- package/rules/require-i18n-text/index.js +94 -0
- package/test/a11y-prohibit-useless-sectioning-fragment.js +7 -7
- package/test/a11y-trigger-has-button.js +8 -7
- package/test/best-practice-for-button-element.js +0 -3
- package/test/best-practice-for-data-test-attribute.js +9 -8
- package/test/best-practice-for-remote-trigger-dialog.js +3 -9
- package/test/best-practice-for-tailwind-prohibit-root-margin.js +16 -7
- package/test/design-system-guideline-prohibit-double-icons.js +1 -3
- package/test/require-i18n-text.js +170 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.1.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.0.0...eslint-plugin-smarthr-v2.1.0) (2025-10-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* prohibit-export-array-typeの型チェックをArray<any>の場合も対象にする ([#869](https://github.com/kufu/tamatebako/issues/869)) ([ee4a647](https://github.com/kufu/tamatebako/commit/ee4a64755c36880d8f43c31ebd47244d71ea2fe9))
|
|
11
|
+
* コンポーネントの子要素やプロパティの文字列リテラルを多言語化の観点で検査するルールを追加 ([#815](https://github.com/kufu/tamatebako/issues/815)) ([9dc8bda](https://github.com/kufu/tamatebako/commit/9dc8bda98d1a978dd0e4d61b91c858ef1c117d9c))
|
|
12
|
+
|
|
5
13
|
## [2.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.11.0...eslint-plugin-smarthr-v2.0.0) (2025-10-16)
|
|
6
14
|
|
|
7
15
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=22.
|
|
9
|
+
"node": ">=22.21.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "jest"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"json5": "^2.2.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"typescript-eslint": "^8.46.
|
|
29
|
+
"typescript-eslint": "^8.46.2"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"eslint": "^9"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "41a58b02c8a6116db8f65bfed94283fa0187d53f"
|
|
41
41
|
}
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
const BARE_SECTIONING_TAG_REGEX = /^(article|aside|nav|section)$/
|
|
2
|
-
const SECTIONING_REGEX = /((A(rticle|side))|Nav|Section)$/
|
|
3
|
-
const SECTIONING_FRAGMENT = 'SectioningFragment'
|
|
4
|
-
const LAYOUT_REGEX = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/
|
|
5
|
-
const AS_REGEX = /^(as|forwardedAs)$/
|
|
6
|
-
|
|
7
|
-
const includeSectioningAsAttr = (a) => AS_REGEX.test(a.name?.name) && BARE_SECTIONING_TAG_REGEX.test(a.value.value)
|
|
8
|
-
|
|
9
|
-
const searchSectioningFragment = (node) => {
|
|
10
|
-
switch (node.type) {
|
|
11
|
-
case 'JSXElement':
|
|
12
|
-
return SECTIONING_FRAGMENT === node.openingElement.name?.name ? node.openingElement : null
|
|
13
|
-
case 'Program':
|
|
14
|
-
return null
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return searchSectioningFragment(node.parent)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
1
|
const SCHEMA = []
|
|
21
2
|
|
|
3
|
+
const SECTIONING_FRAGMENT_ELEMENT = 'JSXElement[openingElement.name.name="SectioningFragment"]'
|
|
4
|
+
const SECTIONING_CONTENT_ELEMENT = 'JSXOpeningElement[name.name=/((A(rticle|side))|Nav|Section)$/]'
|
|
5
|
+
const SECTIONING_LAYOUT_ELEMENT = 'JSXOpeningElement[name.name=/((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/]:has(JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^(article|aside|nav|section)$/])'
|
|
6
|
+
|
|
22
7
|
/**
|
|
23
8
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
24
9
|
*/
|
|
@@ -29,31 +14,11 @@ module.exports = {
|
|
|
29
14
|
},
|
|
30
15
|
create(context) {
|
|
31
16
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (SECTIONING_REGEX.test(name)) {
|
|
38
|
-
hit = true
|
|
39
|
-
} else {
|
|
40
|
-
asAttr = LAYOUT_REGEX.test(name) && node.attributes.find(includeSectioningAsAttr)
|
|
41
|
-
|
|
42
|
-
if (asAttr) {
|
|
43
|
-
hit = true
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (hit) {
|
|
48
|
-
result = searchSectioningFragment(node.parent.parent)
|
|
49
|
-
|
|
50
|
-
if (result) {
|
|
51
|
-
context.report({
|
|
52
|
-
node: result,
|
|
53
|
-
message: `無意味なSectioningFragmentが記述されています。子要素である<${name}${asAttr ? ` ${asAttr.name.name}="${asAttr.value.value}"` : ''}>で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
}
|
|
17
|
+
[`${SECTIONING_FRAGMENT_ELEMENT}:has(:matches(${SECTIONING_CONTENT_ELEMENT}, ${SECTIONING_LAYOUT_ELEMENT}))`]: (node) => {
|
|
18
|
+
context.report({
|
|
19
|
+
node,
|
|
20
|
+
message: `無意味なSectioningFragmentが記述されています。子要素で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
|
|
21
|
+
})
|
|
57
22
|
},
|
|
58
23
|
}
|
|
59
24
|
},
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const TRIGGER_REGEX = /(Dropdown|Dialog)Trigger$/
|
|
2
|
-
const HELP_DIALOG_TRIGGER_REGEX = /HelpDialogTrigger$/
|
|
3
2
|
const BUTTON_REGEX = /(B|^b)utton$/
|
|
4
3
|
const ANCHOR_BUTTON_REGEX = /AnchorButton$/
|
|
5
4
|
const FALSY_TEXT_REGEX = /^\s*\n+\s*$/
|
|
@@ -18,54 +17,37 @@ module.exports = {
|
|
|
18
17
|
},
|
|
19
18
|
create(context) {
|
|
20
19
|
return {
|
|
21
|
-
JSXElement: (parentNode) => {
|
|
22
|
-
// HINT: 閉じタグが存在しない === 子が存在しない
|
|
23
|
-
// 子を持っていない場合はおそらく固定の要素を吐き出すコンポーネントと考えられるため
|
|
24
|
-
// その中身をチェックすることで担保できるのでskipする
|
|
25
|
-
if (!parentNode.closingElement) {
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const node = parentNode.openingElement
|
|
30
|
-
|
|
31
|
-
if (!node.name.name) {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const match = node.name.name.match(TRIGGER_REGEX)
|
|
36
|
-
|
|
37
|
-
if (!match || HELP_DIALOG_TRIGGER_REGEX.test(node.name.name)) {
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
|
|
20
|
+
[`JSXElement[openingElement.name.name=${TRIGGER_REGEX}]:not([openingElement.name.name=/HelpDialogTrigger$/])`]: (parentNode) => {
|
|
41
21
|
const children = filterFalsyJSXText(parentNode.children)
|
|
42
22
|
|
|
43
23
|
if (children.length > 1) {
|
|
24
|
+
const node = parentNode.openingElement
|
|
25
|
+
const match = node.name.name.match(TRIGGER_REGEX)
|
|
26
|
+
|
|
44
27
|
context.report({
|
|
45
28
|
node,
|
|
46
|
-
message: `${match[1]}Trigger の直下には複数のコンポーネントを設置することは出来ません。button
|
|
29
|
+
message: `${match[1]}Trigger の直下には複数のコンポーネントを設置することは出来ません。button要素が一つだけ設置されている状態にしてください`,
|
|
47
30
|
})
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
children.forEach((c) => {
|
|
53
|
-
// `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
|
|
54
|
-
if (c.type === 'JSXExpressionContainer') {
|
|
55
|
-
return false
|
|
56
|
-
}
|
|
31
|
+
} else {
|
|
32
|
+
const c = children[0]
|
|
57
33
|
|
|
58
34
|
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
35
|
+
// `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
|
|
36
|
+
c &&
|
|
37
|
+
c.type !== 'JSXExpressionContainer' && (
|
|
38
|
+
c.type !== 'JSXElement' ||
|
|
39
|
+
!BUTTON_REGEX.test(c.openingElement.name.name) ||
|
|
40
|
+
ANCHOR_BUTTON_REGEX.test(c.openingElement.name.name)
|
|
41
|
+
)
|
|
62
42
|
) {
|
|
43
|
+
const match = parentNode.openingElement.name.name.match(TRIGGER_REGEX)
|
|
44
|
+
|
|
63
45
|
context.report({
|
|
64
46
|
node: c,
|
|
65
|
-
message: `${match[1]}Trigger の直下にはbutton
|
|
47
|
+
message: `${match[1]}Trigger の直下にはbutton要素のみ設置してください(AnchorButtonはa要素のため設置できません)`,
|
|
66
48
|
})
|
|
67
49
|
}
|
|
68
|
-
}
|
|
50
|
+
}
|
|
69
51
|
},
|
|
70
52
|
}
|
|
71
53
|
},
|
|
@@ -33,16 +33,12 @@ module.exports = {
|
|
|
33
33
|
const getSourceCodeText = (node) => context.sourceCode.getText(node)
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
|
-
MemberExpression: (node) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
case 1:
|
|
43
|
-
context.report({
|
|
44
|
-
node,
|
|
45
|
-
message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。awaitの宣言より前にcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
36
|
+
[`:matches(FunctionExpression, ArrowFunctionExpression) MemberExpression[property.name="currentTarget"][object.name]`]: (node) => {
|
|
37
|
+
switch (checkFunctionTopVariable(node.parent, node.object.name, getSourceCodeText)) {
|
|
38
|
+
case 1:
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。awaitの宣言より前にcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
46
42
|
- 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
47
43
|
- NG例:
|
|
48
44
|
const onChange = async (e) => {
|
|
@@ -55,13 +51,13 @@ module.exports = {
|
|
|
55
51
|
await hoge()
|
|
56
52
|
fuga(value)
|
|
57
53
|
}`,
|
|
58
|
-
|
|
54
|
+
})
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
break
|
|
57
|
+
case 2:
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。イベントハンドラ用関数のスコープ直下でcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
|
|
65
61
|
- 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
|
|
66
62
|
- React/useStateのsetterは第一引数に関数を渡すと非同期処理になるためこの問題が起きる可能性があります
|
|
67
63
|
- イベントハンドラ内で関数を定義すると参照タイミングがずれる可能性があるため、イベントハンドラ直下のスコープ内にcurrentTarget関連の参照を変数に残すことをオススメします
|
|
@@ -74,12 +70,9 @@ module.exports = {
|
|
|
74
70
|
const value = e.currentTarget.value
|
|
75
71
|
setItem((current) => ({ ...current, value }))
|
|
76
72
|
}`,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
}
|
|
73
|
+
})
|
|
82
74
|
|
|
75
|
+
break
|
|
83
76
|
}
|
|
84
77
|
},
|
|
85
78
|
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
const { checkImportStyledComponents, getStyledComponentBaseName } = require('../../libs/format_styled_components')
|
|
2
|
-
|
|
3
1
|
const ERRORMESSAGE_SUFFIX = `
|
|
4
2
|
- button要素のtype属性のデフォルトは "submit" のため、button要素がformでラップされていると意図しないsubmitを引き起こす可能性があります
|
|
5
3
|
- smarthr-ui/Button, smarthr-ui/UnstyledButtonのtype属性のデフォルトは "button" になっているため、buttonから置き換えることをおすすめします`
|
|
6
|
-
const ERRORMESSAGE_REQUIRED_TYPE_ATTR = `button要素を利用する場合、type属性に "button" もしくは "submit" を指定してください${ERRORMESSAGE_SUFFIX}`
|
|
7
|
-
const ERRORMESSAGE_PROHIBIT_STYLED = `"styled.button" の直接利用をやめ、smarthr-ui/Button、もしくはsmarthr-ui/UnstyledButtonを利用してください${ERRORMESSAGE_SUFFIX}`
|
|
8
|
-
|
|
9
|
-
const findTypeAttr = (a) => a.type === 'JSXAttribute' && a.name.name === 'type'
|
|
10
4
|
|
|
11
5
|
const SCHEMA = []
|
|
12
6
|
|
|
@@ -21,24 +15,17 @@ module.exports = {
|
|
|
21
15
|
},
|
|
22
16
|
create(context) {
|
|
23
17
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
context.report({
|
|
30
|
-
node,
|
|
31
|
-
message: ERRORMESSAGE_REQUIRED_TYPE_ATTR,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
18
|
+
[`JSXOpeningElement[name.name="button"]:not(:has(JSXAttribute[name.name="type"]))`]: (node) => {
|
|
19
|
+
context.report({
|
|
20
|
+
node,
|
|
21
|
+
message: `button要素を利用する場合、type属性に "button" もしくは "submit" を指定してください${ERRORMESSAGE_SUFFIX}`,
|
|
22
|
+
});
|
|
34
23
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
}
|
|
24
|
+
[`MemberExpression[object.name="styled"][property.name="button"]`]: (node) => {
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
message: `"styled.button" の直接利用をやめ、smarthr-ui/Button、もしくはsmarthr-ui/UnstyledButtonを利用してください${ERRORMESSAGE_SUFFIX}`,
|
|
28
|
+
});
|
|
42
29
|
},
|
|
43
30
|
}
|
|
44
31
|
},
|
|
@@ -12,20 +12,17 @@ module.exports = {
|
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
14
14
|
return {
|
|
15
|
-
JSXAttribute: (node) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
node,
|
|
21
|
-
message: `テストのために要素を指定するために、${hit[1]} 属性を利用するのではなく、他の方法で要素を指定することを検討してください。
|
|
22
|
-
- 方法1: click_link, click_button等を利用したりすることで、利用しているテスト環境に準じた方法で要素を指定することを検討してください。
|
|
15
|
+
'JSXAttribute[name.name=/^(data-(spec|testid))$/]': (node) => {
|
|
16
|
+
context.report({
|
|
17
|
+
node,
|
|
18
|
+
message: `テストしたい要素を指定するためにテスト用の属性は利用せず、他の方法を検討してください
|
|
19
|
+
- 方法1: click_link, click_button等を利用することで、テスト環境に準じた方法で要素を指定することを検討してください
|
|
23
20
|
- 参考(Testing Library): https://testing-library.com/docs/queries/about
|
|
24
21
|
- 参考(Capybara): https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Finders
|
|
25
|
-
- 方法2: テスト環境のメソッド等で要素が指定できない場合はrole
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
22
|
+
- 方法2: テスト環境のメソッド等で要素が指定できない場合はrole、name、aria系などユーザーが認識できる属性を利用した方法で要素を指定することを検討してください
|
|
23
|
+
- 画像の場合、alt属性が利用できます
|
|
24
|
+
- id, class属性は基本的にユーザーが認識出来ないため利用しないでください`,
|
|
25
|
+
});
|
|
29
26
|
},
|
|
30
27
|
}
|
|
31
28
|
},
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
const MESSAGE_NEW_DATE = `'new Date(arg)' のように引数を一つだけ指定したDate instanceの生成は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
|
|
2
|
-
- 'new Date(2022, 12 - 1, 31)' のように数値を個別に指定する
|
|
3
|
-
- dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).toDate()')`
|
|
4
|
-
const MESSAGE_PARSE = `Date.parse は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
|
|
5
|
-
- 'new Date(2022, 12 - 1, 31).getTime()' のように数値を個別に指定する
|
|
6
|
-
- dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).valueOf()')`
|
|
7
|
-
|
|
8
1
|
const SEPARATOR = '(\/|-)'
|
|
9
2
|
const DATE_REGEX = new RegExp(`^([0-9]{4})${SEPARATOR}([0-9]{1,2})${SEPARATOR}([0-9]{1,2})`)
|
|
10
3
|
|
|
@@ -36,29 +29,23 @@ module.exports = {
|
|
|
36
29
|
},
|
|
37
30
|
create(context) {
|
|
38
31
|
return {
|
|
39
|
-
NewExpression: (node) => {
|
|
40
|
-
|
|
41
|
-
node
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fix: (fixer) => fixAction(fixer, node),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
32
|
+
'NewExpression[callee.name="Date"][arguments.length=1]': (node) => {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
message: `'new Date(arg)' のように引数を一つだけ指定したDate instanceの生成は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
|
|
36
|
+
- 'new Date(2022, 12 - 1, 31)' のように数値を個別に指定する
|
|
37
|
+
- dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).toDate()')`,
|
|
38
|
+
fix: (fixer) => fixAction(fixer, node),
|
|
39
|
+
});
|
|
50
40
|
},
|
|
51
|
-
CallExpression: (node) => {
|
|
52
|
-
|
|
53
|
-
node
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
fix: (fixer) => fixAction(fixer, node, '.getTime()'),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
41
|
+
'CallExpression[callee.object.name="Date"][callee.property.name="parse"]': (node) => {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
message: `Date.parse は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
|
|
45
|
+
- 'new Date(2022, 12 - 1, 31).getTime()' のように数値を個別に指定する
|
|
46
|
+
- dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).valueOf()')`,
|
|
47
|
+
fix: (fixer) => fixAction(fixer, node, '.getTime()'),
|
|
48
|
+
});
|
|
62
49
|
},
|
|
63
50
|
}
|
|
64
51
|
},
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const SCHEMA = []
|
|
2
2
|
|
|
3
|
-
const NOINDEX_ARRAY_REGEX = /\[\]\[/
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
7
5
|
*/
|
|
@@ -11,24 +9,18 @@ module.exports = {
|
|
|
11
9
|
schema: SCHEMA,
|
|
12
10
|
},
|
|
13
11
|
create(context) {
|
|
14
|
-
const checker = (node
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
message: `入力要素のname属性に対して、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy] )、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してください。
|
|
12
|
+
const checker = (node) => {
|
|
13
|
+
context.report({
|
|
14
|
+
node,
|
|
15
|
+
message: `入力要素のname属性に対して、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy] )、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してください。
|
|
19
16
|
- 例のyyyに当たる値が配列内の別アイテムに紐づいてしまう場合があります。
|
|
20
17
|
- 詳しくは https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-nested-attributes-array-index を参照してください`,
|
|
21
|
-
|
|
22
|
-
}
|
|
18
|
+
})
|
|
23
19
|
}
|
|
24
20
|
|
|
25
21
|
return {
|
|
26
|
-
Literal:
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
TemplateElement: (node) => {
|
|
30
|
-
checker(node, node.value.cooked)
|
|
31
|
-
},
|
|
22
|
+
'Literal[value=/\\[\\]\\[/]': checker,
|
|
23
|
+
'TemplateElement[value.cooked=/\\[\\]\\[/]': checker,
|
|
32
24
|
}
|
|
33
25
|
},
|
|
34
26
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
const REGEX_REMOTE_TRIGGER_DIALOG = /RemoteTrigger(Action|Form|Message|Modeless)Dialog$/
|
|
2
|
-
const REGEX_REMOTE_DIALOG_TRIGGER = /RemoteDialogTrigger$/
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
6
3
|
*/
|
|
@@ -10,27 +7,17 @@ module.exports = {
|
|
|
10
7
|
schema: [],
|
|
11
8
|
},
|
|
12
9
|
create(context) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const attrName = regexRemoteTriggerDialog ? 'id' : 'targetId'
|
|
21
|
-
const id = node.attributes.find((a) => a.name?.name === attrName)
|
|
10
|
+
const checker = (node) => {
|
|
11
|
+
context.report({
|
|
12
|
+
node,
|
|
13
|
+
message: `${node.parent.name.name}の${node.name.name}属性には直接文字列を指定してください。
|
|
14
|
+
- 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)`,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
message: `${nodeName}の${attrName}属性には直接文字列を指定してください。
|
|
27
|
-
- 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)
|
|
28
|
-
- RemoteTriggerActionDialogはループやDropdown内にTriggerが存在する場合に利用してください
|
|
29
|
-
- ループやDropdown以外にTriggerが設定されている場合、TriggerAndActionDialogを利用してください`,
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
18
|
+
return {
|
|
19
|
+
'JSXOpeningElement[name.name=/RemoteTrigger(Action|Form|Message|Modeless)Dialog$/] JSXAttribute[name.name="id"]:not([value.type="Literal"])': checker,
|
|
20
|
+
'JSXOpeningElement[name.name=/RemoteDialogTrigger$/] JSXAttribute[name.name="targetId"]:not([value.type="Literal"])': checker,
|
|
34
21
|
}
|
|
35
22
|
},
|
|
36
23
|
}
|
|
@@ -1,46 +1,4 @@
|
|
|
1
|
-
const { AST_NODE_TYPES } = require('@typescript-eslint/utils')
|
|
2
|
-
|
|
3
1
|
const SCHEMA = []
|
|
4
|
-
const MARGIN_CLASS_PATTERNS = /shr-m[trbl]?-/ // mt-, mr-, mb-, ml-, m-
|
|
5
|
-
|
|
6
|
-
const findClassNameAttr = (attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'className'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* コンポーネントのルート要素を渡し、該当の余白クラスが存在すればそれを、なければNULLを返す
|
|
10
|
-
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
|
|
11
|
-
* @returns {import('@typescript-eslint/utils').TSESTree.Literal | null}
|
|
12
|
-
*/
|
|
13
|
-
const findSpacingClassInRootElement = (node) => {
|
|
14
|
-
// JSX でなければ対象外
|
|
15
|
-
if (node.type !== AST_NODE_TYPES.JSXElement) return null
|
|
16
|
-
|
|
17
|
-
const classNameAttr = node.openingElement.attributes.find(findClassNameAttr)
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
classNameAttr &&
|
|
21
|
-
// className属性の値がリテラル、かつ余白クラスの場合
|
|
22
|
-
classNameAttr.value?.type === AST_NODE_TYPES.Literal && typeof classNameAttr.value.value === 'string' &&
|
|
23
|
-
MARGIN_CLASS_PATTERNS.test(classNameAttr.value.value)
|
|
24
|
-
) {
|
|
25
|
-
return classNameAttr.value
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* ブロックステートメント内から、JSX要素を返す ReturnStatement を返す
|
|
33
|
-
* @param {import('@typescript-eslint/utils').TSESTree.BlockStatement} block
|
|
34
|
-
* @returns {import('@typescript-eslint/utils').TSESTree.ReturnStatement | null}
|
|
35
|
-
*/
|
|
36
|
-
const findJSXReturnStatement = (block) => {
|
|
37
|
-
for (const statement of block.body) {
|
|
38
|
-
if (statement.type === AST_NODE_TYPES.ReturnStatement && statement.argument?.type === AST_NODE_TYPES.JSXElement) {
|
|
39
|
-
return statement
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return null
|
|
43
|
-
}
|
|
44
2
|
|
|
45
3
|
/**
|
|
46
4
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule}
|
|
@@ -49,61 +7,15 @@ module.exports = {
|
|
|
49
7
|
meta: {
|
|
50
8
|
type: 'problem',
|
|
51
9
|
schema: SCHEMA,
|
|
52
|
-
messages: {
|
|
53
|
-
noRootSpacing:
|
|
54
|
-
'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。',
|
|
55
|
-
},
|
|
56
10
|
},
|
|
57
11
|
create(context) {
|
|
58
|
-
/**
|
|
59
|
-
* 関数本体をチェックし、ルート要素で余白クラスが設定されたJSXを返している場合、エラーを報告する
|
|
60
|
-
* @param {import('@typescript-eslint/utils').TSESTree.Node} body
|
|
61
|
-
*/
|
|
62
|
-
const checkFunctionBody = (n) => {
|
|
63
|
-
const body = n.body
|
|
64
|
-
|
|
65
|
-
switch (body.type) {
|
|
66
|
-
// 関数がブロックを持たずに直接JSXを返すパターン
|
|
67
|
-
case AST_NODE_TYPES.JSXElement: {
|
|
68
|
-
const spacingClass = findSpacingClassInRootElement(body)
|
|
69
|
-
|
|
70
|
-
if (spacingClass) {
|
|
71
|
-
context.report({
|
|
72
|
-
node: spacingClass,
|
|
73
|
-
messageId: 'noRootSpacing',
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
break
|
|
78
|
-
}
|
|
79
|
-
// 関数がブロック内で JSX を return するパターン
|
|
80
|
-
case AST_NODE_TYPES.BlockStatement: {
|
|
81
|
-
const returnStatement = findJSXReturnStatement(body)
|
|
82
|
-
if (returnStatement?.argument) {
|
|
83
|
-
const spacingClass = findSpacingClassInRootElement(returnStatement.argument)
|
|
84
|
-
if (spacingClass) {
|
|
85
|
-
context.report({
|
|
86
|
-
node: spacingClass,
|
|
87
|
-
messageId: 'noRootSpacing',
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
12
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
13
|
+
':matches(ArrowFunctionExpression,FunctionDeclaration,ReturnStatement)>JSXElement>JSXOpeningElement JSXAttribute[name.name="className"][value.value=/( |^)shr-m[trbl]?-/]': (node) => {
|
|
14
|
+
context.report({
|
|
15
|
+
node,
|
|
16
|
+
message: 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。',
|
|
17
|
+
})
|
|
103
18
|
},
|
|
104
|
-
|
|
105
|
-
// function宣言のコンポーネントをチェック
|
|
106
|
-
FunctionDeclaration: checkFunctionBody,
|
|
107
19
|
}
|
|
108
20
|
},
|
|
109
21
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const SCHEMA = []
|
|
2
2
|
|
|
3
|
-
const REGEX_PATTERN = /(Button|Link)$/
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
7
5
|
*/
|
|
@@ -12,35 +10,12 @@ module.exports = {
|
|
|
12
10
|
},
|
|
13
11
|
create(context) {
|
|
14
12
|
return {
|
|
15
|
-
JSXOpeningElement: (node) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
for (const attr of node.attributes) {
|
|
23
|
-
switch (attr.name.name) {
|
|
24
|
-
case 'prefix':
|
|
25
|
-
prefix = attr
|
|
26
|
-
break
|
|
27
|
-
case 'suffix':
|
|
28
|
-
suffix = attr
|
|
29
|
-
break
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if(prefix && suffix) {
|
|
33
|
-
context.report({
|
|
34
|
-
node,
|
|
35
|
-
message: `${nodeName} には prefix と suffix は同時に設定できません。
|
|
36
|
-
- prefix または suffix のみを設定してください。
|
|
37
|
-
- どちらにもアイコンをつけられそうな場合は、アイコン付き(右)(サフィックス)を優先し、アイコン付き(左)(プレフィックス)には指定しないでください。
|
|
38
|
-
- 両方設定したい場合は、'eslint-disable-next-line' 等を利用して、このルールを無効化してください。`,
|
|
39
|
-
})
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
13
|
+
'JSXOpeningElement[name.name=/(Button|Link)$/]:has(JSXAttribute[name.name="prefix"]):has(JSXAttribute[name.name="suffix"])': (node) => {
|
|
14
|
+
context.report({
|
|
15
|
+
node,
|
|
16
|
+
message: `${node.name.name} には prefix と suffix は同時に設定できません。
|
|
17
|
+
- どちらにもアイコンをつけられそうな場合は、prefixを優先してください。`,
|
|
18
|
+
})
|
|
44
19
|
}
|
|
45
20
|
}
|
|
46
21
|
},
|
|
@@ -7,18 +7,15 @@ module.exports = {
|
|
|
7
7
|
schema: [],
|
|
8
8
|
},
|
|
9
9
|
create(context) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
return {
|
|
11
|
+
':matches(TSTypeReference,ExportNamedDeclaration):matches([declaration.typeAnnotation.type="TSArrayType"],[declaration.typeAnnotation.typeName.name="Array"])': (node) => {
|
|
12
12
|
context.report({
|
|
13
13
|
node,
|
|
14
|
-
message:
|
|
14
|
+
message: `型をexportする際、配列ではなくアイテムの型をexportしてください。
|
|
15
|
+
- 型を配列でexportすると、その型が配列かどうかを判定するための情報は名称のみになります
|
|
16
|
+
- 名称から配列かどうかを判定しにくい場合があるため、利用するファイル内で配列として型を設定してください`,
|
|
15
17
|
})
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
ExportDefaultDeclaration: checker,
|
|
21
|
-
ExportNamedDeclaration: checker,
|
|
18
|
+
},
|
|
22
19
|
}
|
|
23
20
|
},
|
|
24
21
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# smarthr/require-i18n-text
|
|
2
|
+
|
|
3
|
+
- JSX/TSXファイル内で文字列リテラルが直接指定されていないかをチェックするルールです
|
|
4
|
+
- 多言語化対応での翻訳対応漏れを防ぐために使用します
|
|
5
|
+
- HTML要素の属性、カスタムコンポーネントの属性、子要素の文字列リテラルを検査対象とします
|
|
6
|
+
- 数値リテラル、真偽値、空文字列は検査対象外です
|
|
7
|
+
|
|
8
|
+
## rules
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
{
|
|
12
|
+
rules: {
|
|
13
|
+
'smarthr/require-i18n-text': [
|
|
14
|
+
'error', // 'warn', 'off'
|
|
15
|
+
{
|
|
16
|
+
elements: {
|
|
17
|
+
// HTML要素
|
|
18
|
+
'img': ['alt', 'title'],
|
|
19
|
+
'input': ['placeholder', 'title'],
|
|
20
|
+
'button': ['title', 'aria-label'],
|
|
21
|
+
|
|
22
|
+
// カスタムコンポーネント
|
|
23
|
+
'Button': ['label', 'errorMessage'],
|
|
24
|
+
'Input': ['placeholder', 'helperText'],
|
|
25
|
+
'Dialog': ['title', 'description'],
|
|
26
|
+
|
|
27
|
+
// すべての要素に適用
|
|
28
|
+
'*': ['data-tooltip']
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## options
|
|
37
|
+
|
|
38
|
+
### elements
|
|
39
|
+
|
|
40
|
+
HTML要素とカスタムコンポーネントごとに検査対象とする属性名を指定します。
|
|
41
|
+
オブジェクトのキーに要素名/コンポーネント名、値に属性名の配列を指定します。
|
|
42
|
+
|
|
43
|
+
デフォルト: `{ '*': ['alt', 'aria-label', 'term', 'title'] }`
|
|
44
|
+
|
|
45
|
+
オプションを指定しない場合、デフォルトでi18n候補属性がすべての要素で検査されます。
|
|
46
|
+
|
|
47
|
+
#### ワイルドカード `'*'` の使用
|
|
48
|
+
|
|
49
|
+
`'*'` をキーとして使用すると、すべての要素(HTML要素とカスタムコンポーネント両方)に対して属性をチェックできます。
|
|
50
|
+
個別の要素設定がある場合は、そちらが優先されます。
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
{
|
|
54
|
+
elements: {
|
|
55
|
+
// すべての要素で title をチェック
|
|
56
|
+
'*': ['title'],
|
|
57
|
+
// img要素は alt も追加でチェック
|
|
58
|
+
'img': ['alt', 'title'],
|
|
59
|
+
// Icon は除外(空配列で上書き)
|
|
60
|
+
'Icon': []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### デフォルト設定の上書き
|
|
66
|
+
|
|
67
|
+
ワイルドカード `'*'` を明示的に設定すると、デフォルト設定を上書きできます。
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
{
|
|
71
|
+
elements: {
|
|
72
|
+
// デフォルトを上書き
|
|
73
|
+
'*': ['data-tooltip'],
|
|
74
|
+
// 個別設定も可能
|
|
75
|
+
'Button': ['label']
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## ❌ Incorrect
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
// HTML要素の属性に文字列リテラルを直接指定
|
|
84
|
+
<img alt="Profile picture" />
|
|
85
|
+
<input placeholder="Enter your name" />
|
|
86
|
+
<button title="Close dialog" />
|
|
87
|
+
|
|
88
|
+
// カスタムコンポーネントの属性に文字列リテラルを直接指定
|
|
89
|
+
<Button label="Submit" />
|
|
90
|
+
<Input helperText="Required field" />
|
|
91
|
+
|
|
92
|
+
// 子要素に文字列リテラルを直接指定
|
|
93
|
+
<div>Hello World</div>
|
|
94
|
+
<Button>Submit</Button>
|
|
95
|
+
<p>Welcome to our application</p>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## ✅ Correct
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
// 翻訳関数を使用
|
|
102
|
+
<img alt={t('profile_picture')} />
|
|
103
|
+
<input placeholder={t('enter_your_name')} />
|
|
104
|
+
<button title={t('close_dialog')} />
|
|
105
|
+
|
|
106
|
+
// カスタムコンポーネントでも翻訳関数を使用
|
|
107
|
+
<Button label={t('submit')} />
|
|
108
|
+
<Input helperText={t('required_field')} />
|
|
109
|
+
|
|
110
|
+
// 子要素でも翻訳関数を使用
|
|
111
|
+
<div>{t('hello_world')}</div>
|
|
112
|
+
<Button>{t('submit')}</Button>
|
|
113
|
+
<p>{t('welcome_message')}</p>
|
|
114
|
+
|
|
115
|
+
// 数値リテラル、真偽値、空文字列は検査対象外
|
|
116
|
+
<div>{123}</div>
|
|
117
|
+
<Button disabled={true} />
|
|
118
|
+
<input value="" />
|
|
119
|
+
|
|
120
|
+
// 検査対象外の属性(設定していない属性)
|
|
121
|
+
<input name="username" />
|
|
122
|
+
<div id="main-content" />
|
|
123
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// デフォルトのワイルドカード設定
|
|
2
|
+
const DEFAULT_WILDCARD_ATTRIBUTES = [
|
|
3
|
+
'alt',
|
|
4
|
+
'aria-label',
|
|
5
|
+
// smarthr-ui DefinitionListItem
|
|
6
|
+
'term',
|
|
7
|
+
'title',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
const SCHEMA = [
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
elements: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
patternProperties: {
|
|
17
|
+
'.+': {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
default: {},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
// 文字列リテラルを持つ属性を選択するセレクタの条件部分
|
|
32
|
+
const STRING_LITERAL_CONDITION =
|
|
33
|
+
':matches([value.type="Literal"][value.value=/\\S/], [value.type="JSXExpressionContainer"][value.expression.type="Literal"][value.expression.value=/\\S/])'
|
|
34
|
+
|
|
35
|
+
const generateAttributeSelector = (attributes) =>
|
|
36
|
+
`JSXAttribute[name.name=/^(${attributes.join('|')})$/]${STRING_LITERAL_CONDITION}`
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
40
|
+
*/
|
|
41
|
+
module.exports = {
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'suggestion',
|
|
44
|
+
schema: SCHEMA,
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
const options = context.options[0] || {}
|
|
48
|
+
const elementsObj = options.elements || {}
|
|
49
|
+
|
|
50
|
+
// ユーザーが'*'を設定していない場合のみデフォルトを適用
|
|
51
|
+
const wildcardAttributes = elementsObj['*'] || DEFAULT_WILDCARD_ATTRIBUTES
|
|
52
|
+
const specificElements = Object.keys(elementsObj).filter((k) => k !== '*')
|
|
53
|
+
const handlers = {}
|
|
54
|
+
|
|
55
|
+
const reportAttributeError = (node) => {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
message: `${node.parent.name.name}の${node.name.name}属性に文字列リテラルが指定されています。多言語化対応のため、翻訳関数を使用してください`,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 個別要素の設定
|
|
63
|
+
for (const elementName of specificElements) {
|
|
64
|
+
const attributes = elementsObj[elementName]
|
|
65
|
+
if (attributes.length === 0) continue
|
|
66
|
+
|
|
67
|
+
handlers[`JSXOpeningElement[name.name="${elementName}"] > ${generateAttributeSelector(attributes)}`] = reportAttributeError
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ワイルドカード設定
|
|
71
|
+
if (wildcardAttributes && wildcardAttributes.length > 0) {
|
|
72
|
+
const attributeSelector = generateAttributeSelector(wildcardAttributes)
|
|
73
|
+
if (specificElements.length > 0) {
|
|
74
|
+
// 個別設定要素を除外
|
|
75
|
+
handlers[`JSXOpeningElement:not([name.name=/^(${specificElements.join('|')})$/]) > ${attributeSelector}`] =
|
|
76
|
+
reportAttributeError
|
|
77
|
+
} else {
|
|
78
|
+
handlers[attributeSelector] = reportAttributeError
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 子要素の文字列リテラルチェック(空白のみのテキストは除外)
|
|
83
|
+
handlers['JSXText[value=/\\S/]'] = (node) => {
|
|
84
|
+
context.report({
|
|
85
|
+
node,
|
|
86
|
+
message: '子要素に文字列リテラルが指定されています。多言語化対応のため、翻訳関数を使用してください',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return handlers
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports.schema = SCHEMA
|
|
@@ -10,7 +10,7 @@ const ruleTester = new RuleTester({
|
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
12
|
})
|
|
13
|
-
const
|
|
13
|
+
const ERROR = `無意味なSectioningFragmentが記述されています。子要素で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
|
|
14
14
|
|
|
15
15
|
ruleTester.run('a11y-prohibit-useless-sectioning-fragment', rule, {
|
|
16
16
|
valid: [
|
|
@@ -21,11 +21,11 @@ ruleTester.run('a11y-prohibit-useless-sectioning-fragment', rule, {
|
|
|
21
21
|
{ code: `<AnyArticle>hoge</AnyArticle>` },
|
|
22
22
|
],
|
|
23
23
|
invalid: [
|
|
24
|
-
{ code: `<SectioningFragment><AnySection /></SectioningFragment>`, errors: [ { message:
|
|
25
|
-
{ code: `<SectioningFragment><AnyAside>hoge</AnyAside></SectioningFragment>`, errors: [ { message:
|
|
26
|
-
{ code: `<SectioningFragment><HogeStack as="aside">hoge</HogeStack></SectioningFragment>`, errors: [ { message:
|
|
27
|
-
{ code: `<SectioningFragment><HogeReel forwardedAs="nav">hoge</HogeReel></SectioningFragment>`, errors: [ { message:
|
|
28
|
-
{ code: `<SectioningFragment><FugaBase as="article">hoge</FugaBase></SectioningFragment>`, errors: [ { message:
|
|
29
|
-
{ code: `<SectioningFragment><FugaBaseColumn as="article">hoge</FugaBaseColumn></SectioningFragment>`, errors: [ { message:
|
|
24
|
+
{ code: `<SectioningFragment><AnySection /></SectioningFragment>`, errors: [ { message: ERROR } ] },
|
|
25
|
+
{ code: `<SectioningFragment><AnyAside>hoge</AnyAside></SectioningFragment>`, errors: [ { message: ERROR } ] },
|
|
26
|
+
{ code: `<SectioningFragment><HogeStack as="aside">hoge</HogeStack></SectioningFragment>`, errors: [ { message: ERROR } ] },
|
|
27
|
+
{ code: `<SectioningFragment><HogeReel forwardedAs="nav">hoge</HogeReel></SectioningFragment>`, errors: [ { message: ERROR } ] },
|
|
28
|
+
{ code: `<SectioningFragment><FugaBase as="article">hoge</FugaBase></SectioningFragment>`, errors: [ { message: ERROR } ] },
|
|
29
|
+
{ code: `<SectioningFragment><FugaBaseColumn as="article">hoge</FugaBaseColumn></SectioningFragment>`, errors: [ { message: ERROR } ] },
|
|
30
30
|
]
|
|
31
31
|
})
|
|
@@ -13,15 +13,16 @@ const ruleTester = new RuleTester({
|
|
|
13
13
|
ruleTester.run('a11y-trigger-has-button', rule, {
|
|
14
14
|
valid: [
|
|
15
15
|
{ code: '<DropdownTrigger><button>hoge</button></DropdownTrigger>' },
|
|
16
|
-
{ code: '<DialogTrigger><
|
|
16
|
+
{ code: '<DialogTrigger><AnyButton>{hoge}</AnyButton></DialogTrigger>' },
|
|
17
17
|
{ code: '<DropdownTrigger>{hoge}</DropdownTrigger>' },
|
|
18
|
+
{ code: '<AnyDropdownTrigger/>' },
|
|
18
19
|
],
|
|
19
20
|
invalid: [
|
|
20
|
-
{ code: '<DropdownTrigger>ほげ</DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbutton
|
|
21
|
-
{ code: '<DialogTrigger><span><Button>ほげ</Button></span></DialogTrigger>', errors: [ { message: 'DialogTrigger の直下にはbutton
|
|
22
|
-
{ code: '<DropdownTrigger><AnchorButton>ほげ</AnchorButton></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbutton
|
|
23
|
-
{ code: '<DropdownTrigger><ButtonAnchor>ほげ</ButtonAnchor></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbutton
|
|
24
|
-
{ code: '<DialogTrigger><button>{hoge}</button>{hoge}</DialogTrigger>', errors: [ { message: 'DialogTrigger の直下には複数のコンポーネントを設置することは出来ません。button
|
|
25
|
-
{ code: '<DropdownTrigger>{hoge}<span>text</span></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下には複数のコンポーネントを設置することは出来ません。button
|
|
21
|
+
{ code: '<DropdownTrigger>ほげ</DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbutton要素のみ設置してください(AnchorButtonはa要素のため設置できません)' } ] },
|
|
22
|
+
{ code: '<DialogTrigger><span><Button>ほげ</Button></span></DialogTrigger>', errors: [ { message: 'DialogTrigger の直下にはbutton要素のみ設置してください(AnchorButtonはa要素のため設置できません)' } ] },
|
|
23
|
+
{ code: '<DropdownTrigger><AnchorButton>ほげ</AnchorButton></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbutton要素のみ設置してください(AnchorButtonはa要素のため設置できません)' } ] },
|
|
24
|
+
{ code: '<DropdownTrigger><ButtonAnchor>ほげ</ButtonAnchor></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbutton要素のみ設置してください(AnchorButtonはa要素のため設置できません)' } ] },
|
|
25
|
+
{ code: '<DialogTrigger><button>{hoge}</button>{hoge}</DialogTrigger>', errors: [ { message: 'DialogTrigger の直下には複数のコンポーネントを設置することは出来ません。button要素が一つだけ設置されている状態にしてください' } ] },
|
|
26
|
+
{ code: '<DropdownTrigger>{hoge}<span>text</span></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下には複数のコンポーネントを設置することは出来ません。button要素が一つだけ設置されている状態にしてください' } ] },
|
|
26
27
|
]
|
|
27
28
|
})
|
|
@@ -19,8 +19,6 @@ const ERRORMESSAGE_PROHIBIT_STYLED = `"styled.button" の直接利用をやめ
|
|
|
19
19
|
|
|
20
20
|
ruleTester.run('best-practice-for-button-element', rule, {
|
|
21
21
|
valid: [
|
|
22
|
-
{ code: `import styled from 'styled-components'` },
|
|
23
|
-
{ code: `import styled, { css } from 'styled-components'` },
|
|
24
22
|
{ code: `<Button />` },
|
|
25
23
|
{ code: `<Button>ほげ</Button>` },
|
|
26
24
|
{ code: `<AnyButton>ほげ</AnyButton>` },
|
|
@@ -29,7 +27,6 @@ ruleTester.run('best-practice-for-button-element', rule, {
|
|
|
29
27
|
{ code: 'const HogeButton = styled(HogeButton)``' },
|
|
30
28
|
],
|
|
31
29
|
invalid: [
|
|
32
|
-
{ code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
|
|
33
30
|
{ code: `<button>ほげ</button>`, errors: [ { message: ERRORMESSAGE_REQUIRED_TYPE_ATTR } ] },
|
|
34
31
|
{ code: 'const HogeButton = styled.button``', errors: [ { message: ERRORMESSAGE_PROHIBIT_STYLED } ] },
|
|
35
32
|
]
|
|
@@ -10,12 +10,13 @@ const ruleTester = new RuleTester({
|
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
12
|
})
|
|
13
|
-
const
|
|
14
|
-
- 方法1: click_link, click_button
|
|
13
|
+
const ERROR_MESSAGE = `テストしたい要素を指定するためにテスト用の属性は利用せず、他の方法を検討してください
|
|
14
|
+
- 方法1: click_link, click_button等を利用することで、テスト環境に準じた方法で要素を指定することを検討してください
|
|
15
15
|
- 参考(Testing Library): https://testing-library.com/docs/queries/about
|
|
16
16
|
- 参考(Capybara): https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Finders
|
|
17
|
-
- 方法2: テスト環境のメソッド等で要素が指定できない場合はrole
|
|
18
|
-
|
|
17
|
+
- 方法2: テスト環境のメソッド等で要素が指定できない場合はrole、name、aria系などユーザーが認識できる属性を利用した方法で要素を指定することを検討してください
|
|
18
|
+
- 画像の場合、alt属性が利用できます
|
|
19
|
+
- id, class属性は基本的にユーザーが認識出来ないため利用しないでください`
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
ruleTester.run('best-practice-for-data-test-attribute', rule, {
|
|
@@ -25,9 +26,9 @@ ruleTester.run('best-practice-for-data-test-attribute', rule, {
|
|
|
25
26
|
{ code: '<Any data-any="fuga">ほげ</Any>'},
|
|
26
27
|
],
|
|
27
28
|
invalid: [
|
|
28
|
-
{ code: '<Any data-spec="hijklmn">ほげ</Any>', errors: [{message:
|
|
29
|
-
{ code: '<Any data-spec>ほげ</Any>', errors: [{message:
|
|
30
|
-
{ code: '<Any data-testid="abcdefg">ほげ</Any>', errors: [{message:
|
|
31
|
-
{ code: '<Any data-testid>ほげ</Any>', errors: [{message:
|
|
29
|
+
{ code: '<Any data-spec="hijklmn">ほげ</Any>', errors: [{message: ERROR_MESSAGE}] },
|
|
30
|
+
{ code: '<Any data-spec>ほげ</Any>', errors: [{message: ERROR_MESSAGE}] },
|
|
31
|
+
{ code: '<Any data-testid="abcdefg">ほげ</Any>', errors: [{message: ERROR_MESSAGE}] },
|
|
32
|
+
{ code: '<Any data-testid>ほげ</Any>', errors: [{message: ERROR_MESSAGE}] },
|
|
32
33
|
]
|
|
33
34
|
})
|
|
@@ -19,16 +19,10 @@ ruleTester.run('best-practice-for-remote-trigger-dialog', rule, {
|
|
|
19
19
|
],
|
|
20
20
|
invalid: [
|
|
21
21
|
{ code: '<RemoteDialogTrigger targetId={hoge}>open.</RemoteDialogTrigger>', errors: [ { message: `RemoteDialogTriggerのtargetId属性には直接文字列を指定してください。
|
|
22
|
-
- 変数などは利用できません(これは関連するTriggerとDialog
|
|
23
|
-
- RemoteTriggerActionDialogはループやDropdown内にTriggerが存在する場合に利用してください
|
|
24
|
-
- ループやDropdown以外にTriggerが設定されている場合、TriggerAndActionDialogを利用してください` } ] },
|
|
22
|
+
- 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)` } ] },
|
|
25
23
|
{ code: '<StyledRemoteDialogTrigger targetId={"fuga"}>open.</StyledRemoteDialogTrigger>', errors: [ { message: `StyledRemoteDialogTriggerのtargetId属性には直接文字列を指定してください。
|
|
26
|
-
- 変数などは利用できません(これは関連するTriggerとDialog
|
|
27
|
-
- RemoteTriggerActionDialogはループやDropdown内にTriggerが存在する場合に利用してください
|
|
28
|
-
- ループやDropdown以外にTriggerが設定されている場合、TriggerAndActionDialogを利用してください` } ] },
|
|
24
|
+
- 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)` } ] },
|
|
29
25
|
{ code: '<StyldRemoteTriggerActionDialog {...args} id={"fuga"}>content.</StyldRemoteTriggerActionDialog>', errors: [ { message: `StyldRemoteTriggerActionDialogのid属性には直接文字列を指定してください。
|
|
30
|
-
- 変数などは利用できません(これは関連するTriggerとDialog
|
|
31
|
-
- RemoteTriggerActionDialogはループやDropdown内にTriggerが存在する場合に利用してください
|
|
32
|
-
- ループやDropdown以外にTriggerが設定されている場合、TriggerAndActionDialogを利用してください` } ] },
|
|
26
|
+
- 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)` } ] },
|
|
33
27
|
]
|
|
34
28
|
})
|
|
@@ -25,15 +25,24 @@ ruleTester.run('best-practice-for-tailwind-prohibit-root-margin', rule, {
|
|
|
25
25
|
`,
|
|
26
26
|
},
|
|
27
27
|
// コンポーネントのルート以外の要素での余白使用
|
|
28
|
+
{
|
|
29
|
+
code: `
|
|
30
|
+
const Card = () => (
|
|
31
|
+
<div className="shr-bg-white">
|
|
32
|
+
<h2 className="shr-mt-4">Title</h2>
|
|
33
|
+
<p className="shr-mb-2 shr-pt-2">Content</p>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
`,
|
|
37
|
+
},
|
|
38
|
+
// returnしない場合
|
|
28
39
|
{
|
|
29
40
|
code: `
|
|
30
41
|
const Card = () => {
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
</div>
|
|
36
|
-
)
|
|
42
|
+
<div className="shr-bg-white">
|
|
43
|
+
<h2 className="shr-mt-4">Title</h2>
|
|
44
|
+
<p className="shr-mb-2 shr-pt-2">Content</p>
|
|
45
|
+
</div>
|
|
37
46
|
}
|
|
38
47
|
`,
|
|
39
48
|
},
|
|
@@ -91,7 +100,7 @@ ruleTester.run('best-practice-for-tailwind-prohibit-root-margin', rule, {
|
|
|
91
100
|
{
|
|
92
101
|
code: `
|
|
93
102
|
const Box = () => (
|
|
94
|
-
<div className="shr-bg-gray-100 shr-ml-2">
|
|
103
|
+
<div className="shr-bg-gray-100 shr-ml-2 hoge">
|
|
95
104
|
<p>Content</p>
|
|
96
105
|
</div>
|
|
97
106
|
)
|
|
@@ -11,9 +11,7 @@ const ruleTester = new RuleTester({
|
|
|
11
11
|
},
|
|
12
12
|
})
|
|
13
13
|
const generateErrorText = (name) => `${name} には prefix と suffix は同時に設定できません。
|
|
14
|
-
- prefix
|
|
15
|
-
- どちらにもアイコンをつけられそうな場合は、アイコン付き(右)(サフィックス)を優先し、アイコン付き(左)(プレフィックス)には指定しないでください。
|
|
16
|
-
- 両方設定したい場合は、'eslint-disable-next-line' 等を利用して、このルールを無効化してください。`
|
|
14
|
+
- どちらにもアイコンをつけられそうな場合は、prefixを優先してください。`
|
|
17
15
|
|
|
18
16
|
ruleTester.run('design-system-guideline-prohibit-double-icons', rule, {
|
|
19
17
|
valid: [
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const rule = require('../rules/require-i18n-text')
|
|
2
|
+
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const attributeError = (element, attr) =>
|
|
15
|
+
`${element}の${attr}属性に文字列リテラルが指定されています。多言語化対応のため、翻訳関数を使用してください`
|
|
16
|
+
const childTextError = '子要素に文字列リテラルが指定されています。多言語化対応のため、翻訳関数を使用してください'
|
|
17
|
+
|
|
18
|
+
const options = [
|
|
19
|
+
{
|
|
20
|
+
elements: {
|
|
21
|
+
img: ['alt', 'title'],
|
|
22
|
+
div: ['title'],
|
|
23
|
+
Button: ['label'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
ruleTester.run('require-i18n-text', rule, {
|
|
29
|
+
valid: [
|
|
30
|
+
// 翻訳関数を使用している場合
|
|
31
|
+
{ code: `<img alt={t('profile_picture')} />`, options },
|
|
32
|
+
{ code: `<div>{t('hello')}</div>`, options },
|
|
33
|
+
|
|
34
|
+
// 検査対象外の属性
|
|
35
|
+
{ code: `<img src="test.png" />`, options },
|
|
36
|
+
|
|
37
|
+
// 検査対象外の要素
|
|
38
|
+
{ code: `<Input label="test" />`, options },
|
|
39
|
+
|
|
40
|
+
// 数値リテラル(検査対象外)
|
|
41
|
+
{ code: `<div>{123}</div>`, options },
|
|
42
|
+
|
|
43
|
+
// 真偽値(検査対象外)
|
|
44
|
+
{ code: `<Button disabled={true} />`, options },
|
|
45
|
+
|
|
46
|
+
// 空文字列(検査対象外)
|
|
47
|
+
{ code: `<img alt="" />`, options },
|
|
48
|
+
|
|
49
|
+
// 空白のみのテキスト(検査対象外)
|
|
50
|
+
{ code: `<div> </div>`, options },
|
|
51
|
+
|
|
52
|
+
// デフォルト設定対象外の属性
|
|
53
|
+
{ code: `<img src="image.png" />` },
|
|
54
|
+
{ code: `<div data-testid="test" />` },
|
|
55
|
+
|
|
56
|
+
// ワイルドカード - 空配列で除外
|
|
57
|
+
{
|
|
58
|
+
code: `<Icon label="Icon text" />`,
|
|
59
|
+
options: [
|
|
60
|
+
{
|
|
61
|
+
elements: {
|
|
62
|
+
'*': ['label'],
|
|
63
|
+
Icon: [],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// デフォルト設定の上書き
|
|
70
|
+
{
|
|
71
|
+
code: `<img alt="text" />`,
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
elements: {
|
|
75
|
+
'*': ['data-tooltip'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
invalid: [
|
|
82
|
+
// 属性エラー: デフォルト設定
|
|
83
|
+
{
|
|
84
|
+
code: `<img alt="Profile picture" />`,
|
|
85
|
+
errors: [{ message: attributeError('img', 'alt') }],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: `<CustomComponent aria-label="Label" />`,
|
|
89
|
+
errors: [{ message: attributeError('CustomComponent', 'aria-label') }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
code: `<DefinitionListItem term="Label" />`,
|
|
93
|
+
errors: [{ message: attributeError('DefinitionListItem', 'term') }],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
code: `<button title="Click me" />`,
|
|
97
|
+
errors: [{ message: attributeError('button', 'title') }],
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// 属性エラー: カスタムオプション
|
|
101
|
+
{
|
|
102
|
+
code: `<img alt="Profile picture" />`,
|
|
103
|
+
options,
|
|
104
|
+
errors: [{ message: attributeError('img', 'alt') }],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
code: `<img alt={'Profile'} />`,
|
|
108
|
+
options,
|
|
109
|
+
errors: [{ message: attributeError('img', 'alt') }],
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// 属性エラー: 同一要素の複数属性
|
|
113
|
+
{
|
|
114
|
+
code: `<img alt="Profile" title="User profile" />`,
|
|
115
|
+
options,
|
|
116
|
+
errors: [{ message: attributeError('img', 'alt') }, { message: attributeError('img', 'title') }],
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// 属性エラー: ワイルドカード
|
|
120
|
+
{
|
|
121
|
+
code: `<CustomComponent label="Text" />`,
|
|
122
|
+
options: [
|
|
123
|
+
{
|
|
124
|
+
elements: {
|
|
125
|
+
'*': ['label'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
errors: [{ message: attributeError('CustomComponent', 'label') }],
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// 属性エラー: 個別設定がワイルドカードより優先
|
|
133
|
+
{
|
|
134
|
+
code: `<Button label="Submit" helperText="Help" />`,
|
|
135
|
+
options: [
|
|
136
|
+
{
|
|
137
|
+
elements: {
|
|
138
|
+
'*': ['label'],
|
|
139
|
+
Button: ['label', 'helperText'],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
errors: [{ message: attributeError('Button', 'label') }, { message: attributeError('Button', 'helperText') }],
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// 子要素エラー(オプション未設定時でもチェックされる)
|
|
147
|
+
{
|
|
148
|
+
code: `<div>Hello World</div>`,
|
|
149
|
+
errors: [{ message: childTextError }],
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// 複合エラー: 属性と子要素
|
|
153
|
+
{
|
|
154
|
+
code: `<Button label="Submit">Click here</Button>`,
|
|
155
|
+
options,
|
|
156
|
+
errors: [{ message: attributeError('Button', 'label') }, { message: childTextError }],
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// 複合エラー: 入れ子構造
|
|
160
|
+
{
|
|
161
|
+
code: `<div title="Parent"><Button label="Child">Grandchild text</Button></div>`,
|
|
162
|
+
options,
|
|
163
|
+
errors: [
|
|
164
|
+
{ message: attributeError('div', 'title') },
|
|
165
|
+
{ message: attributeError('Button', 'label') },
|
|
166
|
+
{ message: childTextError },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
})
|