'].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'new-line', 'element-open-end': 'new-line' }],
errors: [{ messageId: 'incorrectCloseBrace' }, { messageId: 'incorrectCloseBracket' }],
},
// Element close brace: element-open-end: last-attribute but close on new line
{
code: ['
'].join(
'\n'
),
output: null,
options: [{ 'mustache-open-end': 'last-attribute', 'element-open-end': 'last-attribute' }],
errors: [{ messageId: 'incorrectCloseBrace' }, { messageId: 'incorrectCloseBracket' }],
},
// Incorrect attribute indentation on mustache
{
code: ['{{contact-details', 'firstName=firstName', ' lastName=lastName', '}}'].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'new-line' }],
errors: [{ messageId: 'incorrectParamIndentation' }],
},
// Block form attribute indentation wrong
{
code: ['{{#foo-bar', 'baz=true', '}}', '{{/foo-bar}}'].join('\n'),
output: null,
errors: [{ messageId: 'incorrectParamIndentation' }],
},
// Incorrect attribute indentation on element
{
code: '
',
output: null,
options: [{ 'element-open-end': 'new-line' }],
errors: [{ messageId: 'incorrectParamIndentation' }],
},
// Non-block form more than 30 characters
{
code: '{{contact-details firstName=firstName lastName=lastName}}',
output: null,
options: [{ 'open-invocation-max-len': 30 }],
errors: [
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectCloseBrace' },
],
},
// Block form with wrong attr indent + block params + close brace
{
code: [
'{{#contact-details',
' firstName=firstName lastName=lastName as |contact|}}',
' {{contact.fullName}}',
'{{/contact-details}}',
].join('\n'),
output: null,
errors: [
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectBlockParamIndentation' },
{ messageId: 'incorrectCloseBrace' },
],
},
// Block form with close brace on wrong line
{
code: [
'{{#contact-details',
' firstName=firstName',
' lastName=lastName',
'as |fullName|}}',
' {{fullName}}',
'{{/contact-details}}',
].join('\n'),
output: null,
errors: [{ messageId: 'incorrectCloseBrace' }],
},
// Block form > 80 chars with all on one line
{
code: [
'{{#contact-details firstName=firstName lastName=lastName age=age avatar=avatar as |contact|}}',
' {{fullName}}',
'{{/contact-details}}',
].join('\n'),
output: null,
errors: [
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectBlockParamIndentation' },
{ messageId: 'incorrectCloseBrace' },
],
},
// Block form with empty lines before block params
{
code: [
'{{#contact-details',
'',
'',
'as |contact|}}',
' {{contact.fullName}}',
'{{/contact-details}}',
].join('\n'),
output: null,
errors: [
{ messageId: 'incorrectBlockParamIndentation' },
{ messageId: 'incorrectCloseBrace' },
],
},
// Helper > 80 chars
{
code: '{{if (or logout.isRunning (not session.isAuthenticated)) "Logging Out..." "Log Out"}}',
output: null,
options: [{ 'open-invocation-max-len': 80 }],
errors: [
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectParamIndentation' },
{ messageId: 'incorrectCloseBrace' },
],
},
// Mixed element + mustache: new-line/new-line with wrong close positions
{
code: [
'
',
'{{#contact-details',
' param0',
' param1=abc',
' param2=abc',
'as |ab cd ef cd ef |}}',
' {{contact.fullName}}',
'{{/contact-details}}',
'
',
].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'new-line', 'element-open-end': 'new-line' }],
errors: [{ messageId: 'incorrectCloseBracket' }, { messageId: 'incorrectCloseBrace' }],
},
// Mixed: last-attribute/last-attribute with wrong close positions
{
code: [
'
',
'{{#contact-details',
' param0',
' param1=abc',
' param2=abc',
'as |ab cd ef cd ef |',
'}}',
' {{contact.fullName}}',
'{{/contact-details}}',
'
',
].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'last-attribute', 'element-open-end': 'last-attribute' }],
errors: [{ messageId: 'incorrectCloseBracket' }, { messageId: 'incorrectCloseBrace' }],
},
// Mixed: last-attribute/new-line with wrong close positions
{
code: [
'
',
'{{#contact-details',
' param0',
' param1=abc',
' param2=abc',
'as |ab cd ef cd ef |',
'}}',
' {{contact.fullName}}',
'{{/contact-details}}',
'
',
].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'last-attribute', 'element-open-end': 'new-line' }],
errors: [{ messageId: 'incorrectCloseBracket' }, { messageId: 'incorrectCloseBrace' }],
},
// Mixed: new-line/last-attribute with wrong close positions
{
code: [
'
',
'{{#contact-details',
' param0',
' param1=abc',
' param2=abc',
'as |ab cd ef cd ef |}}',
' {{contact.fullName}}',
'{{/contact-details}}',
'
',
].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'new-line', 'element-open-end': 'last-attribute' }],
errors: [{ messageId: 'incorrectCloseBracket' }, { messageId: 'incorrectCloseBrace' }],
},
// Close brace on wrong line in nested block
{
code: ['{{#foo bar as |foo|}}', ' {{foo.bar', ' baz}}{{/foo}}'].join('\n'),
output: null,
errors: [{ messageId: 'incorrectCloseBrace' }],
},
],
});
// ---- GJS tests ----
gjsRuleTester.run('template-attribute-indentation', rule, {
valid: [
// Short invocation
'
{{foo bar=baz}}',
// Multi-line with proper indentation
{
code: [
'
',
'{{contact-details',
' firstName=firstName',
' lastName=lastName',
'}}',
'',
].join('\n'),
options: [{ 'mustache-open-end': 'new-line' }],
},
// Element with proper indentation
{
code: ['
', '', ''].join('\n'),
options: [{ 'element-open-end': 'new-line' }],
},
],
invalid: [
// Incorrect indentation in GJS
{
code: [
'
',
'{{contact-details',
'firstName=firstName',
' lastName=lastName',
'}}',
'',
].join('\n'),
output: null,
options: [{ 'mustache-open-end': 'new-line' }],
errors: [
{
messageId: 'incorrectParamIndentation',
},
],
},
],
});
// ---- Closing tag tests ----
hbsRuleTester.run('template-attribute-indentation (closing tag)', rule, {
valid: [
// Closing tag correctly aligned with opening tag, after text content
{
code: ['
', ' content', '
'].join('\n'),
options: [{ 'process-elements': true }],
},
// Closing tag correctly aligned, after element child
{
code: ['
', ' text', '
'].join('\n'),
options: [{ 'process-elements': true }],
},
// Short element — canApplyRule returns false (single-line, under maxLength)
{
code: '
content
',
options: [{ 'process-elements': true }],
},
],
invalid: [
// Closing tag indented too far (wrong column)
{
code: ['
', ' content', '
'].join('\n'),
output: null,
options: [{ 'process-elements': true }],
errors: [{ messageId: 'incorrectClosingTag' }],
},
// Closing tag on same line as content (wrong column)
{
code: ['
text'].join('\n'),
output: null,
options: [{ 'process-elements': true }],
errors: [{ messageId: 'incorrectClosingTag' }],
},
// Closing tag indented when it should be at column 0
{
code: ['
', ' text', ' '].join('\n'),
output: null,
options: [{ 'process-elements': true }],
errors: [{ messageId: 'incorrectClosingTag' }],
},
// Closing tag on same line as opening tag attributes (too early)
{
code: ['
content
'].join('\n'),
output: null,
options: [{ 'process-elements': true }],
errors: [{ messageId: 'incorrectClosingTag' }],
},
],
});
================================================
FILE: tests/lib/rules/template-attribute-order.js
================================================
const rule = require('../../../lib/rules/template-attribute-order');
const RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
ruleTester.run('template-attribute-order', rule, {
valid: [
'
',
'
',
'
',
'
',
// Single attribute - no ordering needed
'
',
// All same category
'
',
// Correct full order
'
',
],
invalid: [
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }],
},
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }],
},
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }],
},
// Multiple attributes out of order (3 attrs, fully reversed)
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }, { messageId: 'wrongOrder' }],
},
// Unknown attributes go last
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }],
},
// data-test- prefix before type
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }],
},
// Multiline attributes
{
code: '
',
output: '
',
errors: [{ messageId: 'wrongOrder' }],
},
],
});
================================================
FILE: tests/lib/rules/template-block-indentation.js
================================================
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const editorConfigUtil = require('../../../lib/utils/editorconfig');
const rule = require('../../../lib/rules/template-block-indentation');
const RuleTester = require('eslint').RuleTester;
//------------------------------------------------------------------------------
// Tests
//
// All tests are wrapped in a describe so beforeAll/afterAll can install a spy
// on editorConfigUtil.resolveEditorConfig before any rule invocation. This
// prevents the project's own .editorconfig (indent_size=2) from interfering
// with editorconfig-specific test cases.
//------------------------------------------------------------------------------
describe('template-block-indentation', () => {
beforeAll(() => {
vi.spyOn(editorConfigUtil, 'resolveEditorConfig').mockReturnValue({});
});
afterAll(() => {
vi.restoreAllMocks();
});
const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
const gjsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
// ---- HBS tests ----
hbsRuleTester.run('template-block-indentation', rule, {
valid: [
// Single line - no indentation issues
'{{#if foo}}bar{{/if}}',
'
',
'
foo
',
'
',
'{{#link-to "foo.bar"}}Blah{{/link-to}}',
'{{#if foo}}
Hi!
{{/if}}',
'{{#if foo}}
Hi!
{{else}}
Bye!
{{/if}}',
'{{#if foo}}
Hi!
{{else if bar}}
Hello!
{{else}}
Bye!
{{/if}}',
// Properly indented block
['{{#if foo}}', ' bar', '{{/if}}'].join('\n'),
// Properly indented element
['
', '
{{t "greeting"}}
', '
'].join('\n'),
['
'].join('\n'),
['
'].join('\n'),
// Nested blocks
['{{#if foo}}', ' {{#if bar}}', ' baz', ' {{/if}}', '{{/if}}'].join('\n'),
// Properly indented with else
['{{#if foo}}', ' bar', '{{else}}', ' baz', '{{/if}}'].join('\n'),
// Properly indented with else if
['{{#if foo}}', ' bar', '{{else if baz}}', ' qux', '{{/if}}'].join('\n'),
// Complex if/else if/else
[
'{{#if isMorning}}',
' Good morning',
'{{else foo-bar isAfternoon}}',
' Good afternoon',
'{{else}}',
' Good night',
'{{/if}}',
].join('\n'),
// Nested if/else inside element
[
'
',
' {{#if isMorning}}',
' Good morning',
' {{else if isAfternoon}}',
' Good afternoon',
' {{else}}',
' Good night',
' {{/if}}',
'
',
].join('\n'),
// Void elements (no children to check)
'
',
'
',
'
![]()
',
'
',
// Multi-line input with attributes
['
'].join('\n'),
// Content on same line as open/close
['
', ' foo bar baz', '
'].join('\n'),
// Leading content on the same line
['{{#if foo}}', '
bar baz', '{{/if}}'].join('\n'),
[
'
',
'
Foo{{#some-thing}}
lorum ipsum
{{/some-thing}}',
'
',
].join('\n'),
// Escaped curlies
['
Header
', '
', ' \\{{example}}', '
'].join('\n'),
['
', ' \\{{example}}', '
'].join('\n'),
// Comment with content
['
', ' {{! What a comment}}', ' {{foo-bar}}', '
'].join('\n'),
// Raw blocks
['{{{{if isMorning}}}}', ' Good Morning', '{{{{/if}}}}'].join('\n'),
// Each with newline
'\n{{#each cats as |dog|}}\n{{/each}}',
// Mustache expressions inside blocks
['{{#if foo}}', ' {{foo}}-{{bar}}', '{{/if}}'].join('\n'),
['{{#if foo}}', ' Foo-{{bar}}', '{{/if}}'].join('\n'),
['{{#if foo}}', ' Foo:', ' {{bar}}', '{{/if}}'].join('\n'),
['{{#if foo}}', ' {{foo}}:', ' {{bar}}', '{{/if}}'].join('\n'),
// Tilde (whitespace control) variations
['{{#if foo}}', '
', '{{~/if}}'].join('\n'),
['{{~#if foo}}', '
', '{{/if}}'].join('\n'),
['{{#if foo~}}', '
', '{{/if}}'].join('\n'),
['{{#if foo}}', '
', '{{/if~}}'].join('\n'),
['{{#if foo}}', '
', '{{~else}}', '
', '{{/if}}'].join('\n'),
['{{#if foo}}', '
', '{{else~}}', '
', '{{/if}}'].join('\n'),
['{{#if foo}}', '
', '{{~else~}}', '
', '{{/if}}'].join('\n'),
['{{#if foo}}', '
', '{{else}}', '
', '{{~/if~}}'].join('\n'),
['{{#if foo~}}', ' -', '{{/if}}'].join('\n'),
['{{#if foo}}', '{{else if bar}}', '{{else}}', ' {{#if baz}}', ' {{/if~}}', '{{/if}}'].join(
'\n'
),
[
'{{#if foo}}',
'
do foo
',
'{{else if bar~}}',
'
do bar
',
'{{/if}}',
].join('\n'),
// Multi-line attribute element
['
'].join('\n'),
// HTML entities
['{{#if foo}}', ' Hello', '{{/if}}'].join('\n'),
['{{#if foo}}', '
', '{{/if}}'].join('\n'),
['{{#if foo}}', ' bar', '{{/if}}'].join('\n'),
// 4-space indentation config
{
code: ['
'].join('\n'),
options: [4],
},
{
code: ['
'].join('\n'),
options: [4],
},
// Tab config (1-space indent)
{
code: ['
'].join('\n'),
options: ['tab'],
},
{
code: ['
'].join('\n'),
options: ['tab'],
},
// Object config
{
code: ['
'].join('\n'),
options: [{ indentation: 4 }],
},
// Ignored elements - pre, script, style, textarea
['
', 'no indentation needed', ' or checked', '
'].join('\n'),
'
\nsome text
',
'',
'
',
'',
'
',
'
\n{{#foo}}\n {{#baz}} hi!\n {{derp}}{{/baz}}{{/foo}}\n ',
'',
'',
'
',
// ignoreComments config
{
code: ['
', '', '{{! Comment }}', '
'].join('\n'),
options: [{ ignoreComments: true }],
},
{
code: [
'{{#if foo}}',
'',
' {{foo}}',
'{{else}}',
' {{bar}}',
'{{! Comment }}',
'{{/if}}',
].join('\n'),
options: [{ ignoreComments: true }],
},
{
code: ' {{! Comment }}
foo
',
options: [{ ignoreComments: true }],
},
{
code: '
{{! Comment }}
',
options: [{ ignoreComments: true }],
},
// Note: ignoreComments ignores comments but text after comment is still checked
// '
\n{{! Comment }}foo\n
' is invalid even with ignoreComments: true
{
code: '
foo
{{! Comment }}',
options: [{ ignoreComments: true }],
},
{
code: '
foo
',
options: [{ ignoreComments: true }],
},
{
code: '
',
options: [{ ignoreComments: true }],
},
{
code: '
foo
',
options: [{ ignoreComments: true }],
},
{
code: [
'{{#if foo}}',
'',
' ',
' {{#each bar as |baz|}}',
'{{! Comment }}',
' {{#each baz as |a|}}',
' {{! Comment }}',
' {{a}}',
'',
' {{/each}}',
'{{! Comment }}',
' {{/each}}',
'',
'{{! Comment }}',
'{{else}}',
' {{! Comment }}',
'{{/if}}',
].join('\n'),
options: [{ ignoreComments: true }],
},
{
code: ['
', ' ', ' {{! Comment }}', '
'].join('\n'),
options: [{ ignoreComments: false }],
},
{
code: [
'{{#if foo}}',
' ',
' {{foo}}',
'{{else}}',
' {{bar}}',
' {{! Comment }}',
'{{/if}}',
].join('\n'),
options: [{ ignoreComments: false }],
},
// Inline content with span
'relativeDate
(absoluteDate)',
// Title and path elements
'
\n
',
// Empty block
['
', '
'].join('\n'),
// Component invocation
['
', ' content', ''].join('\n'),
// Nested component invocation with block params
[
'{{#foo-bar as |baz|}}',
' {{#baz.content}}',
' {{#component "foo-bar"}}',
' Content',
' {{/component}}',
' {{/baz.content}}',
'{{/foo-bar}}',
].join('\n'),
// Block with inline else
['{{#each items as |item|}}', ' {{item.name}}', '{{/each}}'].join('\n'),
// Comment with proper indentation
['
', ' {{foo-bar baz="asdf"}}', ' ', '
'].join('\n'),
// String literal
"{{'this works'}}",
],
invalid: [
// Incorrect end indentation
{
code: ['{{#if foo}}', ' bar', ' {{/if}}'].join('\n'),
output: '{{#if foo}}\n bar\n{{/if}}',
errors: [{ messageId: 'incorrectEnd' }],
},
// Incorrect child indentation - missing indent
{
code: ['
'].join('\n'),
output: '
',
errors: [{ messageId: 'incorrectChild' }],
},
// Incorrect child indentation - too much
{
code: ['
', '
{{t "greeting"}}
', '
'].join('\n'),
output: '
',
errors: [{ messageId: 'incorrectChild' }],
},
// Incorrect end indentation for element
{
code: ['
'].join('\n'),
output: '
',
errors: [{ messageId: 'incorrectEnd' }],
},
// Incorrect else indentation
{
code: ['{{#if foo}}', ' bar', ' {{else}}', ' baz', '{{/if}}'].join('\n'),
output: '{{#if foo}}\n bar\n{{else}}\n baz\n{{/if}}',
errors: [{ messageId: 'incorrectElse' }],
},
// Incorrect indentation with 4-space config
{
code: ['
'].join('\n'),
output: '
',
options: [4],
errors: [{ messageId: 'incorrectChild' }],
},
{
code: ['
'].join('\n'),
output: '
',
options: [4],
errors: [{ messageId: 'incorrectChild' }],
},
// Multiple errors - wrong children and end
{
code: ['
', 'foo', '
'].join('\n'),
output: '
\n foo\n
',
errors: [{ messageId: 'incorrectChild' }, { messageId: 'incorrectEnd' }],
},
// Nested indentation error
{
code: ['{{#if foo}}', ' {{#if bar}}', ' baz', ' {{/if}}', '{{/if}}'].join('\n'),
output: '{{#if foo}}\n {{#if bar}}\n baz\n {{/if}}\n{{/if}}',
errors: [{ messageId: 'incorrectChild' }],
},
// Note: '
\n
' is not caught by this rule (empty element)
// Closing tag on same line as content but wrong indent
{
code: '
',
output: null,
errors: [{ messageId: 'incorrectEnd' }],
},
// Child not indented in element
{
code: '
',
output: '
',
errors: [{ messageId: 'incorrectChild' }],
},
// Child not indented in block
{
code: '{{#if}}\n
Stuff goes here
\n{{/if}}',
output: '{{#if}}\n
Stuff goes here
\n{{/if}}',
errors: [{ messageId: 'incorrectChild' }],
},
// Else in nested if with wrong end indent
{
code: [
'{{#if isMorning}}',
'{{else}}',
' {{#if something}}',
' Good night',
' {{/if}}',
'{{/if}}',
].join('\n'),
output:
'{{#if isMorning}}\n{{else}}\n {{#if something}}\n Good night\n {{/if}}\n{{/if}}',
errors: [{ messageId: 'incorrectEnd' }],
},
// Mixed indent - some children correct, some not
{
code: '
\n {{foo}}\n{{bar}}\n
',
output: '
\n {{foo}}\n {{bar}}\n
',
errors: [{ messageId: 'incorrectChild' }],
},
// Text child not indented
{
code: '
\n Foo:\n{{bar}}\n
',
output: '
\n Foo:\n {{bar}}\n
',
errors: [{ messageId: 'incorrectChild' }],
},
// Closing block ends at wrong column when preceded by content on same line
{
code: '
\n Foo{{#some-thing}}\n {{/some-thing}}\n
',
output:
'
\n Foo{{#some-thing}}\n {{/some-thing}}\n
',
errors: [{ messageId: 'incorrectEnd' }],
},
// Element closing tag at wrong indent when preceded by content
{
code: '{{#if foo}}\n {{foo}}
\n Bar\n
\n{{/if}}',
output: '{{#if foo}}\n {{foo}}
\n Bar\n
\n{{/if}}',
errors: [{ messageId: 'incorrectEnd' }],
},
// Else block indentation error
{
code: ['{{#if foo}}', ' {{else}}', '{{/if}}'].join('\n'),
output: '{{#if foo}}\n{{else}}\n{{/if}}',
errors: [{ messageId: 'incorrectElse' }],
},
// Tilde with wrong else/end
{
code: [
'{{#if foo}}',
'{{else if bar}}',
'{{else}}',
' {{#if baz}}',
' {{/if~}}',
' {{/if}}',
].join('\n'),
output: '{{#if foo}}\n{{else if bar}}\n{{else}}\n {{#if baz}}\n {{/if~}}\n{{/if}}',
errors: [{ messageId: 'incorrectEnd' }],
},
// Each with wrong else indent
{
code: ['{{#each foo as |bar|}}', ' {{else}}', '{{/each}}'].join('\n'),
output: '{{#each foo as |bar|}}\n{{else}}\n{{/each}}',
errors: [{ messageId: 'incorrectElse' }],
},
// Note: comment with incorrect indentation is not flagged by this rule
// Tilde with wrong child indent
{
code: [
'{{#if isMorning}}',
' Good morning',
'{{else if isAfternoon~}}',
' Good afternoon',
'{{/if}}',
].join('\n'),
output:
'{{#if isMorning}}\n Good morning\n{{else if isAfternoon~}}\n Good afternoon\n{{/if}}',
errors: [{ messageId: 'incorrectChild' }],
},
// Inline else with wrong position
{
code: ['{{#if foo}}foo{{else}}', ' bar', '{{/if}}'].join('\n'),
output: null,
errors: [{ messageId: 'incorrectChild' }, { messageId: 'incorrectElse' }],
},
// Block params with wrong child indent
{
code: [
'{{#foo bar as |foobar|}}',
' {{#foobar.baz}}{{/foobar.baz}}',
' {{foobar.baz}}',
'{{/foo}}',
].join('\n'),
output:
'{{#foo bar as |foobar|}}\n {{#foobar.baz}}{{/foobar.baz}}\n {{foobar.baz}}\n{{/foo}}',
errors: [{ messageId: 'incorrectChild' }, { messageId: 'incorrectChild' }],
},
// ignoreComments: true still catches non-comment child errors
{
code: ['
', 'test{{! Comment }}', '
'].join('\n'),
output: '
\n test{{! Comment }}\n
',
options: [{ ignoreComments: true }],
errors: [{ messageId: 'incorrectChild' }],
},
],
});
// ---- GJS tests ----
gjsRuleTester.run('template-block-indentation', rule, {
valid: [
// Single line inside template
'
{{#if foo}}bar{{/if}}',
// Properly indented
['
', '{{#if foo}}', ' bar', '{{/if}}', ''].join('\n'),
// Element properly indented
[
'
',
' ',
'',
].join('\n'),
// If/else in GJS template
[
'
',
' {{#if foo}}',
' {{foo}}',
' {{else}}',
' {{bar}}',
' {{/if}}',
'',
].join('\n'),
// If/else if/else in GJS
[
'
',
' {{#if foo}}',
' {{foo}}',
' {{else if bar}}',
' {{bar}}',
' {{else}}',
' {{baz}}',
' {{/if}}',
'',
].join('\n'),
],
invalid: [
// Incorrect child indentation in GJS
{
code: ['
', '', ''].join('\n'),
output: '
\n\n',
errors: [{ messageId: 'incorrectChild' }],
},
// Else block with wrong indent in GJS
{
code: [
'
',
' {{#if foo}}',
' {{foo}}',
' {{else if bar}}',
' {{bar}}',
' {{/if}}',
'',
].join('\n'),
output:
'
\n {{#if foo}}\n {{foo}}\n {{else if bar}}\n {{bar}}\n {{/if}}\n',
errors: [{ messageId: 'incorrectElse' }, { messageId: 'incorrectChild' }],
},
// Nested else block with wrong indent in GJS
{
code: [
'
',
' {{#if a}}',
' {{#if foo}}',
' {{foo}}',
' {{else if bar}}',
' {{bar}}',
' {{/if}}',
' {{/if}}',
'',
].join('\n'),
output:
'
\n {{#if a}}\n {{#if foo}}\n {{foo}}\n {{else if bar}}\n {{bar}}\n {{/if}}\n {{/if}}\n',
errors: [{ messageId: 'incorrectElse' }, { messageId: 'incorrectChild' }],
},
],
});
//----------------------------------------------------------------------------
// EditorConfig integration tests
//----------------------------------------------------------------------------
describe('editorconfig integration', () => {
const hbsRuleTesterEditorConfig = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
afterEach(() => {
editorConfigUtil.resolveEditorConfig.mockReturnValue({});
});
describe('indent_size: 4 from editorconfig used as default', () => {
beforeEach(() => {
editorConfigUtil.resolveEditorConfig.mockReturnValue({ indent_size: 4 });
});
hbsRuleTesterEditorConfig.run('template-block-indentation', rule, {
valid: [
// 4-space indent is correct when editorconfig says indent_size=4
['
'].join('\n'),
],
invalid: [
// 2-space indent is wrong when editorconfig says indent_size=4
{
code: ['
'].join('\n'),
output: '
',
errors: [{ messageId: 'incorrectChild' }],
},
],
});
});
describe('explicit option overrides editorconfig', () => {
beforeEach(() => {
editorConfigUtil.resolveEditorConfig.mockReturnValue({ indent_size: 4 });
});
hbsRuleTesterEditorConfig.run('template-block-indentation', rule, {
valid: [
// explicit option=2 wins over editorconfig indent_size=4
{
code: ['
'].join('\n'),
options: [2],
},
],
invalid: [
// 4-space indent is wrong when explicit option says 2
{
code: ['
'].join('\n'),
output: '
',
options: [2],
errors: [{ messageId: 'incorrectChild' }],
},
],
});
});
});
});
================================================
FILE: tests/lib/rules/template-builtin-component-arguments.js
================================================
const rule = require('../../../lib/rules/template-builtin-component-arguments');
const RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
ruleTester.run('template-builtin-component-arguments', rule, {
valid: [
'
',
'
',
'
',
'
',
// In GJS/GTS: custom Input/Textarea components (not imported from @ember/component) are fine
// https://github.com/ember-template-lint/ember-template-lint/issues/2786
{
filename: 'test.gjs',
code: 'import { Input } from "my-custom-lib";
',
},
{
filename: 'test.gjs',
code: '
',
},
{
filename: 'test.gts',
code: '
',
},
],
invalid: [
// In GJS/GTS: only flag when imported from @ember/component
{
filename: 'test.gjs',
code: 'import { Input } from "@ember/component";
',
output: null,
errors: [
{
message:
'Setting the `type` attribute on the builtin
component is not allowed. Did you mean `@type`?',
},
],
},
{
filename: 'test.gjs',
code: 'import { Input as MyInput } from "@ember/component";
',
output: null,
errors: [
{
message:
'Setting the `type` attribute on the builtin
component is not allowed. Did you mean `@type`?',
},
],
},
{
filename: 'test.gjs',
code: 'import { Textarea } from "@ember/component";
',
output: null,
errors: [
{
message:
'Setting the `value` attribute on the builtin