Troubleshooting
Concrete symptom → likely cause → fix. If your issue isn't here, open a GitHub issue with a minimal reproduction.
"Controller is undefined or targets aren't wired"
Symptom
TypeError: Cannot read properties of undefined (reading 'textContent')…right after await render(…).
Causes & fixes
- You forgot
awaitonrender(). It is async — alwaysawaitit. - Your test runner uses
jsdomand your Vitest project hasn't opted in. Settest.environment: 'happy-dom'(or'jsdom') invitest.config.ts. - The controller class name does not end in
Controllerand the identifier could not be inferred. Passoptions.identifierexplicitly. - Your build tool mangles class names. Pass
options.identifieror preserve names via your bundler config.
"data-hello-greeting-value is ignored / value is undefined"
Symptom
controller.greetingValue is undefined even though the fixture contains the attribute.
Causes & fixes
- The
static valuesdeclaration in the controller does not include the key. Values must be declared on the controller class for Stimulus to pick them up. - The attribute name has a typo. Use
attr.controller('hello', { greeting: 'Hi' })to generate the correct name. - The identifier in the attribute does not match the controller's identifier. A fixture with
data-controller="hello"needsdata-hello-…, notdata-Hello-…— Stimulus is case-sensitive.
"connect() never fires" / "Stimulus doesn't see the element"
Causes & fixes
document.body.innerHTML = …afterrender()wipes the fixture. Don't touchdocument.bodymanually — usererender()instead.- You passed a detached element in
options.htmland a customcontainerthat is also detached. Append the container todocumentfirst. - You started your own
Applicationand forgot toregisterthe controller. Letrender()do it, or register before callingrender().
"Two tests in a row, the second one sees the first test's DOM"
Cause — No cleanup is registered.
Fix — Add setupFiles: ['@tito10047/stimulus-test-utils/register'] to your Vitest config, or wire afterEach(cleanup) manually. See Cleanup & isolation.
"getByRole(...) throws Unable to find an accessible element with the role "…""
Causes & fixes
- The markup has no element with that role.
getByRole('button', …)requires an actual<button>orrole="button"— a<div>withdata-actionis not enough. - The element is hidden (
hidden,display: none,aria-hidden="true").getByRoleexcludes hidden elements by default. Pass{ hidden: true }if you really want it. - Your accessible name is off. A button labelled by a nested
<img alt="">with an emptyalthas no accessible name. Fix the label in the markup or usegetByTestId.
"findBy* times out"
Causes & fixes
- The controller performs an async task that never completes (unmocked
fetch, aPromisethat never resolves). Mock it. vi.useFakeTimers()is active and a timer in your controller never gets advanced. Advance it withvi.advanceTimersByTime(n), or switch to real timers for this test.- The element does eventually appear, just after the 1 s default. Increase the timeout via
waitFor(…, { timeout: 2000 }).
"user.click(...) throws pointer-events: none" or "element is not visible"
user.* refuses to act on elements a real user couldn't. Make the element visible (remove hidden, adjust CSS) or assert on the underlying state rather than the click.
"Test passes locally, fails in CI"
Causes & fixes
- Timing. Increase
waitFortimeouts; preferfindBy*oversetTimeoutfor element appearance. - Non-seeded randomness. If your controller uses
Math.random/crypto.getRandomValuesfor ids, stub them. - Locale-sensitive assertions.
toLocaleString()produces different strings in different environments. Pin the locale or assert on raw numbers.
"TypeScript complains Property 'xxxTarget' does not exist on type 'Controller'"
Your controller doesn't declare the target. Add:
declare readonly xxxTarget: HTMLElementSee TypeScript for the full pattern.
"Error: duplicate Stimulus controller identifier "hello" inside attr.combine()"
You passed the same identifier to two attr.controller() calls. Collapse them into one call and merge the values / classes / outlets objects:
// Wrong
attr.combine(
attr.controller('hello', { a: 1 }),
attr.controller('hello', { b: 2 }),
)
// Right
attr.controller('hello', { a: 1, b: 2 })"happy-dom throws Not implemented: HTMLCanvasElement.getContext" (or similar)
happy-dom doesn't implement every browser API. Options:
- Mock the API:
vi.stubGlobal('HTMLCanvasElement.prototype.getContext', vi.fn()). - Switch that test to
jsdom(per-file// @vitest-environment jsdom). - Run the test in a real browser via
vitest --browseror Playwright.
Still stuck?
Open an issue at github.com/tito10047/stimulus-test-utils with:
@tito10047/stimulus-test-utilsversion (npm ls @tito10047/stimulus-test-utils).@hotwired/stimulusversion.- Vitest version and a snippet of
vitest.config.ts. - A minimal failing test (controller + fixture + assertion).
Minimal reproductions get fixed fastest.