eslint-plugin-task-needs-wait-for
Version:
Helps to prevent writing ember-concurrency tasks that would create flaky tests.
183 lines (127 loc) • 4.8 kB
Markdown
# eslint plugin task-needs-wait-for
## Why
As per [ember-test-waiters documentation](https://github.com/emberjs/ember-test-waiters/tree/master):
> ... the waitFor function can be use to wait for async behavior. It can be used with async functions, and in decorator form to wrap an async function so calls to it are registered with the test waiter system. It can also be used with generator functions such as those used in ember-concurrency.
This hints that it's _possible_ to write an ember-concurrency task in a way so that ember testing won't be aware of it and it will create a race condition.
## Examples
Consider following component:
```js
import Component from "@glimmer/component";
import { task, timeout } from "ember-concurrency";
import { tracked } from "@glimmer/tracking";
export default class FooComponent extends Component {
@tracked output = "nothing";
@task
*goodButton() {
this.output = "bad";
yield timeout(1000);
this.output = "good";
}
}
```
Thanks to using `timeout` from `ember-concurrency` we have the guarantee that tests will wait for the task to finish and the `this.output` will have value `good`.
Compare it to following:
```js
import Component from "@glimmer/component";
import { task, timeout } from "ember-concurrency";
import { tracked } from "@glimmer/tracking";
export default class FooComponent extends Component {
@tracked output = "nothing";
@task
*badButton() {
this.output = "bad";
yield new Promise((resolve) => {
setTimeout(resolve, 1000);
});
this.output = "good";
}
}
```
This task will _also_ wait _1000ms_ till it proceeds, but ember testing _will not_ wait for it to complete, because yielding a simple `Promise` won't register it with the ember test waiter system.
- This type of error is _very_ easy to make.
- It leads to [flaky tests](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html). Imagine that the `setTimeout` (or whatever async behaviour) is _very_ fast to resolve. Will the value of `output` be `good` or `bad`? Sometimes this, sometimes that.
- It is an issue that is _extremely_ hard to fix, because:
- It flakes only sometimes.
- It will fail on CI, but work perfectly fine on developer's machine.
- Can point at completely unrelated pieces of code.
This issue can be fixed simply by adding `@waitFor`:
```js
import Component from "@glimmer/component";
import { task, timeout } from "ember-concurrency";
import { tracked } from "@glimmer/tracking";
import { waitFor } from "@ember/test-waiters";
export default class FooComponent extends Component {
@tracked output = "nothing";
@task
@waitFor
*badButtonWithWaitFor() {
this.output = "bad";
yield new Promise((resolve) => {
setTimeout(resolve, 1000);
});
this.output = "good";
}
}
```
## Conclusion
And since preventing people from [stepping on a rake](<https://english.stackexchange.com/questions/605066/does-the-idiom-step-on-a-rake-mean-making-the-same-mistake-twice#:~:text=In%20(American)%20English%20%22stepping,being%20so%20sloppy%20and%20careless.>) is a good idea, this plugin is trying to make sure that folks will have a good time.
## Functionality
### decorator-presence
Linter with auto-fix that makes sure that every `@task` has `@waitFor` after it:
```js
// bad
class Foo {
@task *example() {}
}
// good
import { waitFor } from "@ember/test-waiters";
class Foo {
@task @waitFor *example() {}
}
```
### decorator-order
Linter with auto-fix that makes sure as per [ember-test-waiters documentation](https://github.com/emberjs/ember-test-waiters/tree/master) that we have certain order of the decorators:
> waitFor acts as a wrapper for the generator function, producing another generator function that is registered with the test waiter system, suitable for wrapping in an ember-concurrency task. So @waitFor/waitFor() needs to be applied directly to the generator function, and @task/task() applied to the result.
```js
//bad
class Foo {
@waitFor @task *example() {}
}
// good
class Foo {
@task @waitFor *example() {}
}
```
## Contributing
### Prerequisities
1. [pnpm](https://pnpm.io/installation)
### Setup
```sh
pnpm install
```
### Testing
#### Automated tests
```sh
pnpm test
```
#### Running locally in a project
1. Clone this repo next to your project
2. Add following line to your project `package.json`:
```json
"eslint-plugin-task-needs-wait-for": "file:../eslint-plugin-task-needs-wait-for",
```
3. Add following line to your project `.eslintrc.js`:
```js
plugins: ['task-needs-wait-for'],
extends: {
'plugin:task-needs-wait-for/recommended',
}
```
4. Run in a console:
```sh
pnpm build --watch
```
5. Then inside your project you can run following to test:
```sh
npx eslint .
```