I was working on a Rails app and finished all my tasks. I wanted to be proactive, and a new Ruby minor version was available (3.2.1). It was almost 5 PM, what better time to upgrade my app?
These minor version upgrades are simple, right? I bumped the Ruby version and the CI Docker image, hit bundle install, and the tests passed locally. All good news. I pushed the branch to open a PR, and then… CI broke. What?
The beginning of the pains
I checked the error in CI, and it’s a system spec failing. Weird! Everything passed locally. The error said Capybara couldn’t find a select option. I looked at Capybara’s error screenshot, and the expected text was there!
Okay, desperate moments require desperate actions, so I used an advanced
debugging technique: printing to the CI console.
I printed the text of the option tag, and it… looked exactly what the test
asserted. But strangely, it returned false
when I compared the option.text
to
the expected text.
In disbelief, I copied the option text from the CI logs to my editor when suddenly
I saw a highlighted char. One of the spaces in the text wasn’t a normal blank
space but a U+202f
char.
It’s not a bug. It’s a feature!
Why on Earth did that character end up there? I certainly didn’t add it! I checked
how that select options were being created. Each option content was just a
Time
object converted to a string with Rails translation helpers, nothing
fancy. Except, there was a Stimulus controller attached to them. It was a
controller called local-time
, which allowed users to see the time values in
their local time. This controller looked like this:
import { Controller } from "@hotwired/stimulus"
const locale = document.documentElement.lang
const dateFormatter = new Intl.DateTimeFormat(locale, { timeStyle: "full" })
export default class extends Controller {
connect() {
const datetime = new Date(this.element.value)
const formattedDatetime = dateFormatter.format(datetime)
this.element.textContent = formattedDatetime
}
}
So the error had something to do with JS. After some unlucky Google searches, I tried searching “Google Chrome U+202f” and found this Stack Overflow answer.
It looks like Chromium changed how Intl
date parsing worked. But it
eventually undid that because it was causing too much trouble (Firefox had
done something similar too).
Well, that explained why the test passed on main (using older Chrome) and locally (newer Chrome) but not on that specific branch (a version of Chrome with that behavior).
The fix
This is not super important for this article, but it would feel incomplete without it. The best solution was simply upgrading Chrome on CI, which eliminates that undesired behavior altogether.
If that was not possible, for some reason, we could modify the local-time
Stimulus controller to replace all instances of the U+202f
char with a normal
space:
const formattedDatetime = dateFormatter.format(datetime)
.replaceAll("\u202F", " ")
// ...
The lesson
Although there’s a chance of someone getting to this article and finding a way to fix their bug, I think there’s something more important for us here.
When a bug happens, oftentimes it comes from false assumptions. In this particular case, I had several assumptions while fixing the bug:
- I initially assumed that I was just upgrading Ruby, when in fact, I happened to upgrade CI’s Docker file, which upgraded Chrome as well.
- I assumed that the text in the option tag was equal to the test expectation just because they looked the same.
- I implicitly assumed that the browser had the same behavior in different versions.
- I also assumed my local Chrome instance behaved exactly like the one in CI.
Sometimes, a bug is the culmination of a lengthy chain reaction of falling dominoes. We have to go back and find what caused the initial topple. That is why it’s important to be aware of the assumptions you’re making while debugging. If that topic interests you, we made a whole series about it!