Mock User Agent in View Component Specs
Recently, I implemented a couple of ViewComponents to display links to our app in the app stores. These ViewComponents contained in a single place: the appropriate logo for app store, the link to the app in said app store, descriptions to make the links accessible and click tracking. The links also needed to support a small piece of behaviour. We needed the links to only be displayed on what we deemed an appropriate platform. Explicitly, the link to the Apple App Store shouldn't be rendered on Android devices and the link to the Google Play Store shouldn't be rendered on iOS devices. To keep things very simple and self-contained, we can use the browser gem and access the view helper it provides directly from the view component templates to detect in the component should render anything. The resulting view components look a little something like this. Descriptions and click tracking have been omitted for brevity, we're not looking at those in this post. # app/components/apple_app_store_link_component.rb class AppleAppStoreLinkComponent Testing these components should be simple. We should be able to set the User Agent String to match an android device and an ios device and assert that the component renders (or not) as appropriate. We're using RSpec and we already have :android and :ios tags that are used in system test hooks to set the relevant User Agent String on the Rack driver. config.before(:each, type: :system, android: true) do page.driver.browser.current_session.header("User-Agent", android_user_agent_string) end config.before(:each, type: :system, ios: true) do page.driver.browser.current_session.header("User-Agent", ios_user_agent_string) end Obviously these hooks would not work as-is, we need to also target component tests. config.before(:each, type: :component, android: true) do page.driver.browser.current_session.header("User-Agent", android_user_agent_string) end config.before(:each, type: :component, ios: true) do page.driver.browser.current_session.header("User-Agent", ios_user_agent_string) end Now we can write our simple and descriptive component specs. RSpec.describe AppleAppStoreLinkComponent, type: :component do it "does not render the link on Android devices", :android do render_inline(described_class.new) expect(page).not_to have_css(apple_app_store_selector) end it "renders the link on iOS devices", :ios do render_inline(described_class.new) expect(page).to have_css(apple_app_store_selector) end private def apple_app_store_selector "a[href=''] img[src*='apple_app_store']" end end Unfortunately, this doesn't work. The Capybara driver used in component specs is very simple and doesn't support the full API supported by, say, the Rack driver. Here, we get an error. NoMethodError: undefined method `driver' for an instance of Capybara::Node::Simple We need another way to set the User Agent String for component tests. Looking through the ViewComponent Testing guide didn't help. There were sections on setting the current request parameters, but nothing about how to set arbitrary headers. GPT and Copilot were also of no help, since they just hallucinated non-existent APIs. Before giving up and using a full-on system test for this behaviour, which felt very heavy for a simple case of user agent detection, I looked through the extensive API documentation. I found vc_test_request, a way to access and modify the request object directly in tests - the docs even used the User Agent header as an example. This was perfect! I plugged it into the hook and everything worked as expected. config.before(:each, type: :component, android: true) do vc_test_request.env["HTTP_USER_AGENT"] = android_user_agent_string end config.before(:each, type: :component, ios: true) do vc_test_request.env["HTTP_USER_AGENT"] = ios_user_agent_string end I wrote this post because it wasn't immediately clear to me from the documentation that this testing behaviour was available and I thought maybe others would find it useful. Thanks for reading! Photo by berenice melis on Unsplash
Recently, I implemented a couple of ViewComponents to display links to our app in the app stores. These ViewComponents contained in a single place: the appropriate logo for app store, the link to the app in said app store, descriptions to make the links accessible and click tracking.
The links also needed to support a small piece of behaviour. We needed the links to only be displayed on what we deemed an appropriate platform. Explicitly, the link to the Apple App Store shouldn't be rendered on Android devices and the link to the Google Play Store shouldn't be rendered on iOS devices.
To keep things very simple and self-contained, we can use the browser gem and access the view helper it provides directly from the view component templates to detect in the component should render anything.
The resulting view components look a little something like this. Descriptions and click tracking have been omitted for brevity, we're not looking at those in this post.
# app/components/apple_app_store_link_component.rb
class AppleAppStoreLinkComponent < ViewComponent::Base
end
<% unless helpers.browser.platform.android? %>
<%= link_to "" do %>
<%= image_tag("apple_app_store.svg") %>
<% end %>
<% end %>
Testing these components should be simple. We should be able to set the User Agent String to match an android device and an ios device and assert that the component renders (or not) as appropriate.
We're using RSpec and we already have :android
and :ios
tags that are used in system test hooks to set the relevant User Agent String on the Rack driver.
config.before(:each, type: :system, android: true) do
page.driver.browser.current_session.header("User-Agent", android_user_agent_string)
end
config.before(:each, type: :system, ios: true) do
page.driver.browser.current_session.header("User-Agent", ios_user_agent_string)
end
Obviously these hooks would not work as-is, we need to also target component tests.
config.before(:each, type: :component, android: true) do
page.driver.browser.current_session.header("User-Agent", android_user_agent_string)
end
config.before(:each, type: :component, ios: true) do
page.driver.browser.current_session.header("User-Agent", ios_user_agent_string)
end
Now we can write our simple and descriptive component specs.
RSpec.describe AppleAppStoreLinkComponent, type: :component do
it "does not render the link on Android devices", :android do
render_inline(described_class.new)
expect(page).not_to have_css(apple_app_store_selector)
end
it "renders the link on iOS devices", :ios do
render_inline(described_class.new)
expect(page).to have_css(apple_app_store_selector)
end
private
def apple_app_store_selector
"a[href=''] img[src*='apple_app_store']"
end
end
Unfortunately, this doesn't work. The Capybara driver used in component specs is very simple and doesn't support the full API supported by, say, the Rack driver. Here, we get an error.
NoMethodError:
undefined method `driver' for an instance of Capybara::Node::Simple
We need another way to set the User Agent String for component tests.
Looking through the ViewComponent Testing guide didn't help. There were sections on setting the current request parameters, but nothing about how to set
arbitrary headers. GPT and Copilot were also of no help, since they just hallucinated non-existent APIs.
Before giving up and using a full-on system test for this behaviour, which felt very heavy for a simple case of user agent detection, I looked through the extensive API documentation. I found vc_test_request
, a way to access and modify the request object directly in tests - the docs even used the User Agent header as an example.
This was perfect! I plugged it into the hook and everything worked as expected.
config.before(:each, type: :component, android: true) do
vc_test_request.env["HTTP_USER_AGENT"] = android_user_agent_string
end
config.before(:each, type: :component, ios: true) do
vc_test_request.env["HTTP_USER_AGENT"] = ios_user_agent_string
end
I wrote this post because it wasn't immediately clear to me from the documentation that this testing behaviour was available and I thought maybe others would find it useful.
Thanks for reading!
Photo by berenice melis on Unsplash