Skip to content

Instantly share code, notes, and snippets.

@bholtbholt
Last active February 4, 2025 20:42
Show Gist options
  • Save bholtbholt/c8351665a861aee62e915d8b32e2c759 to your computer and use it in GitHub Desktop.
Save bholtbholt/c8351665a861aee62e915d8b32e2c759 to your computer and use it in GitHub Desktop.
Testing Stimulus with Jest in a Rails App. Stimulus isn't mounted before the test runs, so these helpers wrap the calls in async functions to fix race conditions.
// This helper file provides a consistent API for testing Stimulus Controllers
//
// Use:
// import { getHTML, setHTML, startStimulus } from './_stimulus_helper';
// import MyController from '@javascripts/controllers/my_controller';
//
// beforeEach(() => startStimulus('my', MyController));
// test('should do something', async () => {
// await setHTML(`<button data-controller="my" data-action="my#action">click</button>`);
//
// const button = screen.getByText('click');
// await button.click();
//
// expect(getHTML()).toEqual('something');
// });
//
import { Application } from '@hotwired/stimulus';
// Initializes and registers the controller for the test file
// Use it in a before block:
// beforeEach(() => startStimulus('dom', DomController));
//
// @name = string of the controller
// @controller = controller class
//
// https://stimulus.hotwired.dev/handbook/installing#using-other-build-systems
export function startStimulus(name, controller) {
const application = Application.start();
application.register(name, controller);
}
// Helper function for setting HTML
// - It trims content to prevent false negatives
// - It's async so there's time for the Stimulus controller to load
//
// Use within tests:
// await setHTML(`<p>My HTML Content</p>`);
export async function setHTML(content = '') {
document.body.innerHTML = content.trim();
return document.body.innerHTML;
}
// Helper function for getting HTML content
// - Trims content to prevent false negatives
// - Is consistent with setHTML
//
// Use within tests:
// expect(getHTML()).toEqual('something');
export function getHTML() {
return document.body.innerHTML.trim();
}
import { screen } from '@testing-library/dom';
import { getHTML, setHTML, startStimulus } from './_stimulus_helper';
import DomController from '@javascripts/controllers/dom_controller';
beforeEach(() => startStimulus('dom', DomController));
test('should remove itself', async () => {
await setHTML(`<button data-controller="dom" data-action="dom#remove">remove</button>`);
const button = screen.getByText('remove');
await button.click();
expect(getHTML()).toEqual('');
});
test('should remove the parent element', async () => {
await setHTML(`
<div data-controller="dom">
<a data-action="dom#remove:once">remove</a>
</div>
`);
const button = screen.getByText('remove');
await button.click();
expect(getHTML()).toEqual('');
});
module.exports = {
setupFilesAfterEnv: ['./jest.setup.js'],
testMatch: ['**/test/**/*.test.js'],
cacheDirectory: './tmp/cache/jest',
moduleNameMapper: {
'@javascripts(.*)$': '<rootDir>/app/assets/javascripts$1',
},
testEnvironment: 'jsdom',
transformIgnorePatterns: ['node_modules']
};
import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom';
@bholtbholt
Copy link
Author

IIRC we used js-bundling with web pack. I wrote the original docs for the setup here, but it's been a couple years so I'm not sure it's still the best way.

Personally, I'd use Vite-Ruby today.

@t3k4y
Copy link

t3k4y commented Feb 4, 2025

IIRC we used js-bundling with web pack. I wrote the original docs for the setup here, but it's been a couple years so I'm not sure it's still the best way.

Personally, I'd use Vite-Ruby today.

Thank you for your response + links!
I'm actually in progress migrating an webpacker project towards vite-ruby. Beeing a little unsure if this is a good decision (not leading to next migration in a year or two) your comment supporting it 😅

@bholtbholt
Copy link
Author

@t3k4y I've been using Vite in my JS projects since ~2021 or maybe even 2020. I'm familiar and comfortable with Webpack, but it's a lot of configuration. I think Webpacker did a good job abstracting Webpack for Rails, but it felt very complicated. In Rails projects, I often went the more explicit route and tried to get as close to raw web pack as possible. In the project I was on when I wrote this gist, we were pretty happy to move away from Webpacker and were actively moving as close to vanilla Rails 7 as possible, but still relied on Node for richer JS libraries. I can't recall if vite-ruby was around yet or maybe it was untested or the team was skeptical, but for whatever reason it didn't get much consideration.

I contracted on a Rails project recently where we used vite-ruby, TypeScript, Svelte, and InertiaJS and it was a really nice dev experience. The setup was pretty straightforward and any complexity was between the JS libraries themselves, not Rails or the bundler. (For example, Storybook didn't support Svelte very well.)

At this point in time, I think many JS projects are moving off of webpack when they can and adding deeper Vite support. Vite powers VueJS and Svelte by default, as well as several React Frameworks (Remix for example). I'd say it's a pretty safe bet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment