Skip to content

Instantly share code, notes, and snippets.

@adrienpoly
Last active March 22, 2024 08:14
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adrienpoly/862846f5882796fdeb4fc85b260b3c5a to your computer and use it in GitHub Desktop.
Save adrienpoly/862846f5882796fdeb4fc85b260b3c5a to your computer and use it in GitHub Desktop.
Capybara / Stimulus test helper to ensure JS is ready when test starts
<!DOCTYPE html>
<html>
<head>
...
</head>
<body data-controller="js">
<%= yield %>
</body>
</html>
import { Controller } from 'stimulus'
export default class extends Controller {
static values = { loaded: Boolean }
connect() {
this.loadedValue = true
}
}
# ...
scenario 'test some js behavior', js: true do
visit some_path
ensure_js_is_ready
# JS is fully loaded you can start interacting with the page
end
# ...
def ensure_js_is_ready
expect(page).to have_css('[data-js-loaded-value="true"]')
end
@jordanell
Copy link

Works perfectly.

@minimul
Copy link

minimul commented Oct 31, 2022

Thanks!

FWIW my implementation was:

app/views/layouts/application.html.erb

<body class='h-full overflow-hidden' <%== %{data-controller='js'} if Rails.env.test? %>>

app/javascript/controllers/js_controller.js

import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static values = { loaded: { type: Boolean, default: false } }

  connect () {
    this.loadedValue = true
  }
}

@danielricecodes
Copy link

danielricecodes commented Mar 10, 2023

But this doesn't actually tell Capybara to wait until something has loaded. I think its fast enough that the expectation just never fails. What if the stimulus controller is more complex or has async loading, such as in a more complex Stripe Elements form?

@rbclark
Copy link

rbclark commented Aug 29, 2023

This works but it doesn't really scale up well when your page starts having more and more stimulus controllers attaching to it. It starts to get really messy when all controllers need this added to them. I really wish there was a way to handle this for all controllers even if it's only in test mode.

@adrienpoly
Copy link
Author

This works but it doesn't really scale up well when your page starts having more and more stimulus controllers attaching to it. It starts to get really messy when all controllers need this added to them. I really wish there was a way to handle this for all controllers even if it's only in test mode.

There is only one controller needed in my example. Once a controller is connected it means all controllers on your page are connected.

@rbclark
Copy link

rbclark commented Aug 29, 2023

There is only one controller needed in my example. Once a controller is connected it means all controllers on your page are connected.

Part of the issue I have is I’m using turbo streams to dynamically load additional parts of the page which have stimulus controllers making it all more complicated. In my case one controller being connected doesn’t necessarily mean all of them are.

@adrienpoly
Copy link
Author

Lazy loaded frames have a complete attribute when. they are loaded
Turbo Stream channel have a connected attribute once they are ready to receive messages

given that the Stimulus application is already started on the page when new content is added it gets connected almost immediately. It never created a flaky on my side.

Can you give a more detailed example that provides unreliable test?

@rbclark
Copy link

rbclark commented Aug 30, 2023

Sure, I have a flaky test at the moment that uses a toggle controller to show or hide a dropdown element. The controller is extremely basic:

toggle_controller.js

import {Controller} from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["content"]
  static classes = ["visibility"]

  toggle(event) {
    event.preventDefault();
    this.contentTargets.forEach(t => t.classList.toggle(this.visibilityClass));
  }
}

On the page I have the following:

div data-controller="toggle" data-toggle-visibility-class="hidden"
	button#dropdown-menu data-action="toggle#toggle"
	  .w-6 = "Dropdown"
	.relative
	  .hidden data-toggle-target="content"
        = link_to some_path, "Some Action"
	  ...

I then have a test the does the following:

	visit "/"
    ensure_js_is_ready
    click_on "dropdown-menu"
    click on "Some Action"

I get the following failure sometimes:

     Failure/Error: click_on "Some Action"
     
     Capybara::ElementNotFound:
       Unable to find visible link or button "Some Action"

This test intermittently fails for me since even though the JS controller is loaded, the toggle controller has not attached to the button yet. I can see from the test screenshot that the click has fired since the button is selected but the dropdown is missing. In order to fix this I have been having to add this.loadedValue = true individually to all of my stimulus controllers, which isn't ideal but it is the only way I can get somewhat reliable tests.

@adrienpoly
Copy link
Author

Interesting I never had any issue with similar test

here is what you could try

connect() {
  requestAnimationFrame(() => {
      this.loadedValue = true
   })
}

loaded value will be set in the next tick after connect that could help hopefully

@rbclark
Copy link

rbclark commented Aug 30, 2023

@adrienpoly Thanks for the suggestion! I'll have to try that out and report back. For now I've just been adding this.loadedValue = true on connect for all my controllers where this problem presents itself. It works but it really litters up all my stimulus controllers. It would really be nice if stimulus were just able to handle this instead of requiring workarounds like this.

@andrewyoo
Copy link

I've been playing around with this as well. I noticed when i load all my controllers using import "controllers", they appear to load alphabetically when i put console.log in the various connect() function. So i renamed the js_controller to zz_js_controller and i'm going to try that out.

Given it's an intermittent issue only in my system tests, i can't confirm if it will fix anything. Thanks for this post though!

Copy link

ghost commented Feb 7, 2024

@andrewyoo Did it work?

@andrewyoo
Copy link

@alan-pie Yes, it works very well. I have no intermittent issues after implementing it in my CI tests and identifying the problem areas (right after a visit).

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