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
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,24 +560,25 @@ To disable a rule for an entire `.gjs`/`.gts` file, use a regular ESLint file-le

### Testing

| Name                                       | Description | 💼 | 🔧 | 💡 |
| :----------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :- | :- | :- |
| [no-current-route-name](docs/rules/no-current-route-name.md) | disallow usage of the `currentRouteName()` test helper | | | |
| [no-ember-testing-in-module-scope](docs/rules/no-ember-testing-in-module-scope.md) | disallow use of `Ember.testing` in module scope | ✅ | | |
| [no-invalid-test-waiters](docs/rules/no-invalid-test-waiters.md) | disallow incorrect usage of test waiter APIs | ✅ | | |
| [no-legacy-test-waiters](docs/rules/no-legacy-test-waiters.md) | disallow the use of the legacy test waiter APIs | ✅ | | |
| [no-noop-setup-on-error-in-before](docs/rules/no-noop-setup-on-error-in-before.md) | disallows using no-op setupOnerror in `before` or `beforeEach` | ✅ | 🔧 | |
| [no-pause-test](docs/rules/no-pause-test.md) | disallow usage of the `pauseTest` helper in tests | ✅ | | |
| [no-replace-test-comments](docs/rules/no-replace-test-comments.md) | disallow 'Replace this with your real tests' comments in test files | | | |
| [no-restricted-resolver-tests](docs/rules/no-restricted-resolver-tests.md) | disallow the use of patterns that use the restricted resolver in tests | ✅ | | |
| [no-settled-after-test-helper](docs/rules/no-settled-after-test-helper.md) | disallow usage of `await settled()` right after test helper that calls it internally | ✅ | 🔧 | |
| [no-test-and-then](docs/rules/no-test-and-then.md) | disallow usage of the `andThen` test wait helper | ✅ | | |
| [no-test-import-export](docs/rules/no-test-import-export.md) | disallow importing of "-test.js" in a test file and exporting from a test file | ✅ | | |
| [no-test-module-for](docs/rules/no-test-module-for.md) | disallow usage of `moduleFor`, `moduleForComponent`, etc | ✅ | | |
| [no-test-support-import](docs/rules/no-test-support-import.md) | disallow importing of "test-support" files in production code. | ✅ | | |
| [no-test-this-render](docs/rules/no-test-this-render.md) | disallow usage of the `this.render` in tests, recommending to use @ember/test-helpers' `render` instead. | ✅ | | |
| [prefer-ember-test-helpers](docs/rules/prefer-ember-test-helpers.md) | enforce usage of `@ember/test-helpers` methods over native window methods | ✅ | | |
| [require-valid-css-selector-in-test-helpers](docs/rules/require-valid-css-selector-in-test-helpers.md) | disallow using invalid CSS selectors in test helpers | ✅ | 🔧 | |
| Name                                       | Description | 💼 | 🔧 | 💡 |
| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
| [no-current-route-name](docs/rules/no-current-route-name.md) | disallow usage of the `currentRouteName()` test helper | | | |
| [no-ember-testing-in-module-scope](docs/rules/no-ember-testing-in-module-scope.md) | disallow use of `Ember.testing` in module scope | ✅ | | |
| [no-invalid-test-waiters](docs/rules/no-invalid-test-waiters.md) | disallow incorrect usage of test waiter APIs | ✅ | | |
| [no-legacy-test-waiters](docs/rules/no-legacy-test-waiters.md) | disallow the use of the legacy test waiter APIs | ✅ | | |
| [no-noop-setup-on-error-in-before](docs/rules/no-noop-setup-on-error-in-before.md) | disallows using no-op setupOnerror in `before` or `beforeEach` | ✅ | 🔧 | |
| [no-pause-test](docs/rules/no-pause-test.md) | disallow usage of the `pauseTest` helper in tests | ✅ | | |
| [no-replace-test-comments](docs/rules/no-replace-test-comments.md) | disallow 'Replace this with your real tests' comments in test files | | | |
| [no-restricted-resolver-tests](docs/rules/no-restricted-resolver-tests.md) | disallow the use of patterns that use the restricted resolver in tests | ✅ | | |
| [no-settled-after-test-helper](docs/rules/no-settled-after-test-helper.md) | disallow usage of `await settled()` right after test helper that calls it internally | ✅ | 🔧 | |
| [no-test-and-then](docs/rules/no-test-and-then.md) | disallow usage of the `andThen` test wait helper | ✅ | | |
| [no-test-import-export](docs/rules/no-test-import-export.md) | disallow importing of "-test.js" in a test file and exporting from a test file | ✅ | | |
| [no-test-module-for](docs/rules/no-test-module-for.md) | disallow usage of `moduleFor`, `moduleForComponent`, etc | ✅ | | |
| [no-test-support-import](docs/rules/no-test-support-import.md) | disallow importing of "test-support" files in production code. | ✅ | | |
| [no-test-this-render](docs/rules/no-test-this-render.md) | disallow usage of the `this.render` in tests, recommending to use @ember/test-helpers' `render` instead. | ✅ | | |
| [no-test-this-set-get](docs/rules/no-test-this-set-get.md) | disallow usage of `this.set`, `this.get`, `this.setProperties`, and `this.getProperties` in `.gjs`/`.gts` tests | | | |
| [prefer-ember-test-helpers](docs/rules/prefer-ember-test-helpers.md) | enforce usage of `@ember/test-helpers` methods over native window methods | ✅ | | |
| [require-valid-css-selector-in-test-helpers](docs/rules/require-valid-css-selector-in-test-helpers.md) | disallow using invalid CSS selectors in test helpers | ✅ | 🔧 | |

<!-- end auto-generated rules list -->

Expand Down
79 changes: 79 additions & 0 deletions docs/rules/no-test-this-set-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ember/no-test-this-set-get

<!-- end auto-generated rule header -->

In `.gjs`/`.gts` tests, the `<template>` tag has access to the surrounding scope, so any value used by a test's template can be a local variable (optionally tracked) instead of being assigned to the test context (`this`). As a result, the legacy `this.set`, `this.get`, `this.setProperties`, and `this.getProperties` helpers are no longer recommended in template-tag tests.

## Rule Details

This rule disallows the following in `.gjs` and `.gts` test files:

- `this.set(...)`, `this.get(...)`, `this.setProperties(...)`, `this.getProperties(...)` (including the computed `this["set"](...)` form).
- Calls to `set`, `get`, `setProperties`, and `getProperties` when imported from `@ember/object` (including aliased imports).

It does not flag these calls in `.js`/`.ts` tests, or in non-test files.

## Examples

Examples of **incorrect** code for this rule (in a `*-test.gjs` or `*-test.gts` file):

```gjs
test('it renders', async function (assert) {
this.set('name', 'Zoey');
await render(<template>{{this.name}}</template>);
});
```

```gjs
test('it reads', function (assert) {
const value = this.get('name');
});
```

```gjs
test('it sets many', function (assert) {
this.setProperties({ name: 'Zoey', age: 4 });
});
```

```gjs
test('it reads many', function (assert) {
const { name, age } = this.getProperties('name', 'age');
});
```

```gjs
import { set } from '@ember/object';

test('it sets', function (assert) {
set(obj, 'name', 'Zoey');
});
```

Examples of **correct** code for this rule (in a `*-test.gjs` or `*-test.gts` file):

```gjs
test('it renders', async function (assert) {
const name = 'Zoey';
await render(<template>{{name}}</template>);
});
```

```gjs
import { tracked } from '@glimmer/tracking';

class State {
@tracked name = 'Zoey';
}

test('it renders', async function (assert) {
const state = new State();
await render(<template>{{state.name}}</template>);
state.name = 'Tomster';
});
```

## References

- [RFC 779: First-Class Component Templates](https://github.com/emberjs/rfcs/pull/779)
- The legacy [`set`](https://api.emberjs.com/ember/release/functions/@ember%2Fobject/set), [`get`](https://api.emberjs.com/ember/release/functions/@ember%2Fobject/get), `setProperties`, and `getProperties` helpers from `@ember/object`.
102 changes: 102 additions & 0 deletions lib/rules/no-test-this-set-get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const emberUtils = require('../utils/ember');
const types = require('../utils/types');
const { getImportIdentifier } = require('../utils/import');

const DISALLOWED_METHODS = ['set', 'get', 'setProperties', 'getProperties'];
const DISALLOWED_METHOD_SET = new Set(DISALLOWED_METHODS);

function makeThisErrorMessage(methodName) {
return `Do not use \`this.${methodName}()\` in gjs/gts tests. Use tracked state or local variables in the test \`<template>\` instead.`;
}

function makeImportedErrorMessage(methodName) {
return `Do not use \`${methodName}()\` from \`@ember/object\` in gjs/gts tests. Use tracked state or local variables in the test \`<template>\` instead.`;
}

function isGjsOrGtsTestFile(fileName) {
return (
emberUtils.isTestFile(fileName) && (fileName.endsWith('.gjs') || fileName.endsWith('.gts'))
);
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'disallow usage of `this.set`, `this.get`, `this.setProperties`, and `this.getProperties` in `.gjs`/`.gts` tests',
category: 'Testing',
recommended: false,
recommendedGjs: false,
recommendedGts: false,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-test-this-set-get.md',
},
fixable: null,
schema: [],
},

makeErrorMessage: makeThisErrorMessage,
makeThisErrorMessage,
makeImportedErrorMessage,

create(context) {
if (!isGjsOrGtsTestFile(context.filename)) {
return {};
}

// Map from local identifier name -> the original `@ember/object` export name.
const importedNames = new Map();

return {
ImportDeclaration(node) {
if (node.source.value !== '@ember/object') {
return;
}
for (const original of DISALLOWED_METHODS) {
const localName = getImportIdentifier(node, '@ember/object', original);
if (localName) {
importedNames.set(localName, original);
}
}
},

CallExpression(node) {
const { callee } = node;

// Flag `this.set(...)`, `this.get(...)`, etc., including the computed
// form `this["set"](...)`.
if (callee.type === 'MemberExpression' && types.isThisExpression(callee.object)) {
let propertyName;
if (!callee.computed && types.isIdentifier(callee.property)) {
propertyName = callee.property.name;
} else if (callee.computed && types.isStringLiteral(callee.property)) {
propertyName = callee.property.value;
}
if (propertyName && DISALLOWED_METHOD_SET.has(propertyName)) {
context.report({
node,
message: makeThisErrorMessage(propertyName),
});
return;
}
}

// Flag calls to `set`/`get`/`setProperties`/`getProperties` when
// imported from `@ember/object`.
if (types.isIdentifier(callee) && importedNames.has(callee.name)) {
context.report({
node,
message: makeImportedErrorMessage(importedNames.get(callee.name)),
});
}
},
};
},
};
Loading
Loading