Skip to content

[Bug]: eslint-plugin-storybook rule await-interactions is silently disabled by any import after the Storybook one #35760

Description

@dmitry-dvornitsky

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugeslint-pluginsev:S3Medium priority. Fix within months if possible.

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions