Describe the bug
await-interactions tracks "did this file import from Storybook" in a single flag that every
ImportDeclaration overwrites rather than latches:
|
}>; |
|
|
|
return { |
|
ImportDeclaration(node: TSESTree.ImportDeclaration) { |
|
isImportedFromStorybook = |
|
isUserEventFromStorybookImported(node) || isExpectFromStorybookImported(node); |
|
}, |
ImportDeclaration(node: TSESTree.ImportDeclaration) {
isImportedFromStorybook =
isUserEventFromStorybookImported(node) || isExpectFromStorybookImported(node);
},
So whatever the last ImportDeclaration in the file happens to be decides the flag, and
Program:exit reports nothing when it is false:
|
if (isImportedFromStorybook && invocationsThatShouldBeAwaited.length) { |
A single non-Storybook import placed after the Storybook one disables the rule for the whole
file. That is the common case, not a corner: a story file imports the component it documents,
and most import-ordering conventions (and autofixers) will not put storybook/test last.
This is very likely the root cause of the long-standing storybookjs/eslint-plugin-storybook#153,
which reported the rule as simply "not working" and observed that removing the
if (isImportedFromStorybook) guard in Program:exit makes it work. That repository is now
archived, so I am reporting it here against the live source.
Note the VariableDeclarator handler on the next lines clears the same flag for any declarator
whose id is not a plain identifier, which looks like a second way for it to be lost — I have not
reduced that one to a repro, so treat it as an observation rather than a claim.
Not a duplicate of #35456: that one is about which module paths are recognised, this one is
about the flag being lost regardless of path. Both have to be fixed for the rule to be reliable —
with #35456 fixed and this open, a story using storybook/test would still go silent whenever a
component import follows it.
Reproduction steps
Two files, identical bodies, differing only in import order. @storybook/test is used in
both so that #35456 is out of the picture.
eslint.config.mjs:
import storybook from 'eslint-plugin-storybook'
export default [
{
files: ['**/*.stories.js'],
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: { storybook },
rules: { 'storybook/await-interactions': 'error' },
},
]
a.stories.js — component import first, Storybook import last:
import { Button } from './button'
import { expect, userEvent, within } from '@storybook/test'
export default { component: Button }
export const Probe = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
userEvent.click(canvas.getByRole('button'))
expect(canvas.getByRole('button')).toBeInTheDocument()
},
}
b.stories.js — the same file with the two imports swapped.
Run npx eslint --no-config-lookup --config eslint.config.mjs "*.stories.js":
a.stories.js
9:5 error Interaction should be awaited: userEvent storybook/await-interactions
10:5 error Interaction should be awaited: toBeInTheDocument storybook/await-interactions
✖ 2 problems (2 errors, 0 warnings)
b.stories.js reports nothing.
Expected behavior
The flag latches — once any import in the file brings userEvent or expect in from a Storybook
test module, the rule stays armed for that file no matter what is imported afterwards. b should
report the same two errors as a.
System
eslint-plugin-storybook: 10.5.5
eslint: 10.8.0
node: 22
Also reproduced through oxlint's ESLint-plugin bridge with the same result, so it is not
ESLint-runner specific.
Describe the bug
await-interactionstracks "did this file import from Storybook" in a single flag that everyImportDeclarationoverwrites rather than latches:storybook/code/lib/eslint-plugin/src/rules/await-interactions.ts
Lines 168 to 174 in c6b8914
So whatever the last
ImportDeclarationin the file happens to be decides the flag, andProgram:exitreports nothing when it is false:storybook/code/lib/eslint-plugin/src/rules/await-interactions.ts
Line 186 in c6b8914
A single non-Storybook import placed after the Storybook one disables the rule for the whole
file. That is the common case, not a corner: a story file imports the component it documents,
and most import-ordering conventions (and autofixers) will not put
storybook/testlast.This is very likely the root cause of the long-standing storybookjs/eslint-plugin-storybook#153,
which reported the rule as simply "not working" and observed that removing the
if (isImportedFromStorybook)guard inProgram:exitmakes it work. That repository is nowarchived, so I am reporting it here against the live source.
Note the
VariableDeclaratorhandler on the next lines clears the same flag for any declaratorwhose id is not a plain identifier, which looks like a second way for it to be lost — I have not
reduced that one to a repro, so treat it as an observation rather than a claim.
Not a duplicate of #35456: that one is about which module paths are recognised, this one is
about the flag being lost regardless of path. Both have to be fixed for the rule to be reliable —
with #35456 fixed and this open, a story using
storybook/testwould still go silent whenever acomponent import follows it.
Reproduction steps
Two files, identical bodies, differing only in import order.
@storybook/testis used inboth so that #35456 is out of the picture.
eslint.config.mjs:a.stories.js— component import first, Storybook import last:b.stories.js— the same file with the two imports swapped.Run
npx eslint --no-config-lookup --config eslint.config.mjs "*.stories.js":b.stories.jsreports nothing.Expected behavior
The flag latches — once any import in the file brings
userEventorexpectin from a Storybooktest module, the rule stays armed for that file no matter what is imported afterwards.
bshouldreport the same two errors as
a.System
Also reproduced through oxlint's ESLint-plugin bridge with the same result, so it is not
ESLint-runner specific.