Skip to content
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
42 changes: 42 additions & 0 deletions tests/lib/rules/template-link-rel-noopener.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ ruleTester.run('template-link-rel-noopener', rule, {
'<template><a href="/" target="_blank" rel="nofollow noreferrer noopener">Link</a></template>',
// no target="_blank" means no rel required
'<template><a href="/">Link</a></template>',
// target="_self" does not require rel
'<template><a href="/some/where" target="_self"></a></template>',
],
invalid: [
// no rel attribute at all
Expand Down Expand Up @@ -43,3 +45,43 @@ ruleTester.run('template-link-rel-noopener', rule, {
},
],
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-link-rel-noopener (hbs)', rule, {
valid: [
'<a href="/some/where"></a>',
'<a href="/some/where" target="_self"></a>',
'<a href="/some/where" target="_blank" rel="noopener noreferrer"></a>',
'<a href="/some/where" target="_blank" rel="noreferrer noopener"></a>',
'<a href="/some/where" target="_blank" rel="nofollow noreferrer noopener"></a>',
],
invalid: [
{
code: '<a href="/some/where" target="_blank"></a>',
output: '<a href="/some/where" target="_blank" rel="noopener noreferrer"></a>',
errors: [{ messageId: 'missingRel' }],
},
{
code: '<a href="/some/where" target="_blank" rel="nofollow"></a>',
output: '<a href="/some/where" target="_blank" rel="nofollow noopener noreferrer"></a>',
errors: [{ messageId: 'missingRel' }],
},
{
code: '<a href="/some/where" target="_blank" rel="noopener"></a>',
output: '<a href="/some/where" target="_blank" rel="noopener noreferrer"></a>',
errors: [{ messageId: 'missingRel' }],
},
{
code: '<a href="/some/where" target="_blank" rel="noreferrer"></a>',
output: '<a href="/some/where" target="_blank" rel="noopener noreferrer"></a>',
errors: [{ messageId: 'missingRel' }],
},
],
});
50 changes: 50 additions & 0 deletions tests/lib/rules/template-no-abstract-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,55 @@ ruleTester.run('template-no-abstract-roles', rule, {
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="composite"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><input role="input" /></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="landmark"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><input role="range" /></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="roletype"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="section"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="sectionhead"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><select role="select"></select></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="structure"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
{
code: '<template><div role="window"></div></template>',
output: null,
errors: [{ messageId: 'abstractRole' }],
},
],
});
18 changes: 18 additions & 0 deletions tests/lib/rules/template-no-accesskey-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,23 @@ ruleTester.run('template-no-accesskey-attribute', rule, {
},
],
},
// Boolean attribute (no value)
{
code: '<template><button accesskey></button></template>',
output: '<template><button></button></template>',
errors: [{ messageId: 'noAccesskey' }],
},
// Dynamic mustache value
{
code: '<template><button accesskey={{someKey}}></button></template>',
output: '<template><button></button></template>',
errors: [{ messageId: 'noAccesskey' }],
},
// Concat string attribute
{
code: '<template><button accesskey="{{someKey}}"></button></template>',
output: '<template><button></button></template>',
errors: [{ messageId: 'noAccesskey' }],
},
],
});
5 changes: 5 additions & 0 deletions tests/lib/rules/template-no-aria-hidden-body.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ ruleTester.run('template-no-aria-hidden-body', rule, {
output: '<template><body></body></template>',
errors: [{ messageId: 'noAriaHiddenBody' }],
},
{
code: '<template><body aria-hidden></body></template>',
output: '<template><body></body></template>',
errors: [{ messageId: 'noAriaHiddenBody' }],
},
],
});
31 changes: 31 additions & 0 deletions tests/lib/rules/template-no-array-prototype-extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ ruleTester.run('template-no-array-prototype-extensions', rule, {
'<template>{{@items}}</template>',
'<template>{{firstObject}}</template>',
'<template>{{length}}</template>',
// get helper with firstObject/lastObject as a direct top-level property (not an extension)
"<template>{{get this 'firstObject'}}</template>",
"<template>{{get this 'lastObject.name'}}</template>",
// Plain text nodes are not flagged
'<template>Just a regular text in the template bar.firstObject bar.lastObject.foo</template>',
// String-literal HTML attributes are not flagged
'<template><Foo foo="bar.firstObject.baz" /></template>',
],

invalid: [
Expand Down Expand Up @@ -84,5 +91,29 @@ ruleTester.run('template-no-array-prototype-extensions', rule, {
output: null,
errors: [{ messageId: 'lastObject' }],
},
// lastObject — in get helper string literal, no fix
{
code: "<template><Foo @bar={{get this 'list.lastObject'}} /></template>",
output: null,
errors: [{ messageId: 'lastObject' }],
},
// firstObject — get helper with `this` as object and string literal path
{
code: "<template><Foo @bar={{get this 'list.firstObject'}} /></template>",
output: "<template><Foo @bar={{get this 'list.0'}} /></template>",
errors: [{ messageId: 'firstObject' }],
},
// firstObject — get helper with @arg as object and firstObject at start of string path
{
code: "<template><Foo @bar={{get @list 'firstObject.name'}} /></template>",
output: "<template><Foo @bar={{get @list '0.name'}} /></template>",
errors: [{ messageId: 'firstObject' }],
},
// lastObject — in named hash argument
{
code: '<template>{{foo bar=@list.lastObject.test}}</template>',
output: null,
errors: [{ messageId: 'lastObject' }],
},
],
});
5 changes: 5 additions & 0 deletions tests/lib/rules/template-no-curly-component-invocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ hbsRuleTester.run('template-no-curly-component-invocation', rule, {
code: '{{#each items as |disallowed|}}{{disallowed}}{{/each}}',
options: [{ disallow: ['disallowed'], noImplicitThis: false }],
},
// requireDash: true — single-word names with named args are not flagged (not obviously a component)
{
code: '{{foo bar=baz}}',
options: [{ requireDash: true }],
},
],
invalid: [
{
Expand Down
2 changes: 2 additions & 0 deletions tests/lib/rules/template-no-duplicate-landmark-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ ruleTester.run('template-no-duplicate-landmark-elements', rule, {
"<template><main><header><h1>Main Page Header</h1></header></main><dialog id='my-dialog'><header><h1>Dialog Header</h1></header></dialog></template>",
// Landmarks inside dialog are in a separate scope
'<template><nav></nav><dialog><nav></nav></dialog></template>',
// Landmarks inside popover element are in a separate scope
'<template><nav></nav><div popover><nav></nav></div></template>',
// Dynamic role values — can't determine role statically
'<template><div role={{this.role}}></div><div role={{this.role}}></div></template>',
// Dynamic aria-label on one landmark — can't infer whether it duplicates a sibling
Expand Down
78 changes: 78 additions & 0 deletions tests/lib/rules/template-no-element-event-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ ruleTester.run('template-no-element-event-actions', rule, {
`,
output: null,
},
// requireActionHelper: true — non-action mustache is not flagged
{
filename: 'my-component.gjs',
code: '<template><button type="button" onclick={{this.myAction}}></button></template>',
output: null,
options: [{ requireActionHelper: true }],
},
// requireActionHelper: false — string event handler is not flagged
{
filename: 'my-component.gjs',
code: '<template><button type="button" onclick="myFunction()"></button></template>',
output: null,
options: [{ requireActionHelper: false }],
},
],

invalid: [
Expand Down Expand Up @@ -69,5 +83,69 @@ ruleTester.run('template-no-element-event-actions', rule, {
},
],
},
// requireActionHelper: false — any mustache on event attribute is flagged
{
filename: 'my-component.gjs',
code: '<template><button type="button" onclick={{this.myAction}}></button></template>',
output: null,
options: [{ requireActionHelper: false }],
errors: [{ messageId: 'noElementEventActions' }],
},
// requireActionHelper: true — only {{action ...}} mustaches are flagged
{
filename: 'my-component.gjs',
code: '<template><button onclick={{action "myAction"}}></button></template>',
output: null,
options: [{ requireActionHelper: true }],
errors: [{ messageId: 'noElementEventActions' }],
},
],
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-no-element-event-actions (hbs)', rule, {
valid: [
'<button></button>',
'<button type="button" onclick="myFunction()"></button>',
'<button type="button" {{on "click" this.handleClick}}></button>',
{
code: '<button type="button" onclick={{this.myAction}}></button>',
options: [{ requireActionHelper: true }],
},
{
code: '<button type="button" onclick="myFunction()"></button>',
options: [{ requireActionHelper: false }],
},
],
invalid: [
{
code: '<button onclick={{action "myAction"}}></button>',
output: null,
errors: [{ messageId: 'noElementEventActions' }],
},
{
code: '<button type="button" onclick={{this.myAction}}></button>',
output: null,
errors: [{ messageId: 'noElementEventActions' }],
},
{
code: '<button type="button" onclick={{this.myAction}}></button>',
output: null,
options: [{ requireActionHelper: false }],
errors: [{ messageId: 'noElementEventActions' }],
},
{
code: '<button onclick={{action "myAction"}}></button>',
output: null,
options: [{ requireActionHelper: true }],
errors: [{ messageId: 'noElementEventActions' }],
},
],
});
24 changes: 24 additions & 0 deletions tests/lib/rules/template-no-invalid-interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,30 @@ ruleTester.run('template-no-invalid-interactive', rule, {
'<template><@someComponent onclick={{this.click}} /></template>',
'<template><this.myComponent onclick={{this.click}} /></template>',
'<template><ns.SomeWidget onclick={{this.click}} /></template>',

// additionalInteractiveTags: tags listed are treated as interactive
{
code: '<template><div {{on "click" this.onClick}}></div></template>',
options: [{ additionalInteractiveTags: ['div'] }],
},
{
code: '<template><div {{action "foo"}}></div></template>',
options: [{ additionalInteractiveTags: ['div'] }],
},
{
code: '<template><div onclick={{action "foo"}}></div></template>',
options: [{ additionalInteractiveTags: ['div'] }],
},

// ignoredTags: tags listed are skipped entirely
{
code: '<template><div {{on "click" this.actionName}}>...</div></template>',
options: [{ ignoredTags: ['div'] }],
},
{
code: '<template><div onclick={{action "foo"}}></div></template>',
options: [{ ignoredTags: ['div'] }],
},
],

invalid: [
Expand Down
11 changes: 11 additions & 0 deletions tests/lib/rules/template-no-invalid-role.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ ruleTester.run('template-no-invalid-role', rule, {
'<template><div role="Button">Click</div></template>',
'<template><div role="NAVIGATION">Nav</div></template>',
'<template><div role="ALERT">Alert</div></template>',
// catchNonexistentRoles: false — non-existent roles are not flagged
{
code: '<template><div role="command interface"></div></template>',
options: [{ catchNonexistentRoles: false }],
},
],

invalid: [
Expand Down Expand Up @@ -166,6 +171,12 @@ ruleTester.run('template-no-invalid-role', rule, {
output: null,
errors: [{ message: "Invalid ARIA role 'COMMAND INTERFACE'. Must be a valid ARIA role." }],
},
{
code: '<template><div role="command interface"></div></template>',
output: null,
options: [{ catchNonexistentRoles: true }],
errors: [{ message: "Invalid ARIA role 'command interface'. Must be a valid ARIA role." }],
},

// Newly added SEMANTIC_ELEMENTS: presentation/none on iframe, video, audio
{
Expand Down
10 changes: 10 additions & 0 deletions tests/lib/rules/template-no-nested-interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ ruleTester.run('template-no-nested-interactive', rule, {
'<template><div tabindex=-1><button>Click me!</button></div></template>',
'<template><div tabindex="1"><button></button></div></template>',
'<template><label><input></label></template>',
// Config: ignoreUsemapAttribute (alias for ignoreUsemap)
{
code: '<template><button><img usemap=""></button></template>',
options: [{ ignoreUsemapAttribute: true }],
},
'<template><details><summary>Details</summary>Something small enough to escape casual notice.</details></template>',
'<template><details> <summary>Details</summary>Something small enough to escape casual notice.</details></template>',
`<template>
Expand Down Expand Up @@ -293,6 +298,11 @@ hbsRuleTester.run('template-no-nested-interactive', rule, {
code: '<button><img usemap=""></button>',
options: [{ ignoreUsemap: true }],
},
// Config: ignoreUsemapAttribute (alias for ignoreUsemap)
{
code: '<button><img usemap=""></button>',
options: [{ ignoreUsemapAttribute: true }],
},
],
invalid: [
{
Expand Down
Loading
Loading