Skip to content

Conversation

@23tux
Copy link

@23tux 23tux commented Oct 28, 2025

What are you trying to accomplish?

  1. Sometimes we call t(".some_key") inside a #render? method. Think of def render? = items.any? and items is a hash of translations
  2. To test a components method, I'd like to be able to just do the setup of a component's render lifecycle inside my test. This shortens our test run time, as we don't need to actually render the HTML. And due to the change when the @virtual_path is actually set, I can now test t(..) calls in my helper method, because the teardown hasn't happened yet.

What approach did you choose and why?

  • I extracted the setup, actual render, and teardown of the #render_in method into methods.
  • I moved the line @view_context.instance_variable_set(:@virtual_path, virtual_path) outside of the @output_buffer.with_buffer do block. I just hope this doesn't break anything else.

Anything you want to highlight for special attention from reviewers?

  • I couldn't get the system tests with a real browser to run, but all the other tests are green
  • I'm not sure if I can just change the values in assert_allocations

@23tux
Copy link
Author

23tux commented Oct 28, 2025

Maybe someone can help with the failing checks, I'm not sure what to do

@boardfish
Copy link
Collaborator

@23tux This looks good to me. Those checks are failing on main – I don't think they're blockers to merging this, but because this affects the number of allocations it'd probably be good to make sure those tests pass locally if it's convenient to do so.

23tux and others added 3 commits October 28, 2025 20:52
@23tux
Copy link
Author

23tux commented Oct 28, 2025

@boardfish I added a test helper as well, could you have another look at it? Apart from the integration/system specs that require a browser (as I said, I had some problems with the setup and can't run them), all the tests pass on my machine. I added one commit to make the allocation spec more robust.

And the Lint check also fails, but it doesn't seem to involve my changes.

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>
@henrikbjorn
Copy link
Contributor

This sounds a bit like the issue I have been having with ViewComponent 4. Where we have some deeply nested component slots and it fails to resolve the t('.key') correctly.

Claude made a single file test case.

# frozen_string_literal: true

# Run with: ruby virtual_path_issue_test.rb
#
# This test demonstrates an issue with ViewComponent where t('.key') in blocks
# passed from views to deeply nested component slots resolves to the wrong i18n scope.
#
# Issue: When a view passes a block containing t('.key') to a component slot,
# and that slot renders through multiple nested components, the virtual_path
# is set to the innermost component's path instead of the original view's path.
#
# Expected: t('.edit') should resolve to "test_views.index.edit" -> "Edit"
# Actual: t('.edit') resolves to "cell_component.edit" -> "Cell edit" (or missing translation)

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  gem "rails", "~> 8.0"
  gem "view_component", "~> 4.1"
end

require "action_controller/railtie"
require "action_view/railtie"
require "view_component"
require "view_component/test_helpers"
require "minitest/autorun"

class TestApp < Rails::Application
  config.eager_load = false
  config.logger = Logger.new($stdout)
  config.logger.level = Logger::WARN
  config.secret_key_base = "test"
  routes.draw do
    root to: "test#index"
  end
end

Rails.application.initialize!

class CellComponent < ViewComponent::Base
  erb_template <<~ERB
    <td><%= content %></td>
  ERB
end

class RowComponent < ViewComponent::Base
  renders_many :cells, CellComponent

  erb_template <<~ERB
    <tr><% cells.each do |cell| %><%= cell %><% end %></tr>
  ERB
end

class TableComponent < ViewComponent::Base
  renders_many :rows, RowComponent

  erb_template <<~ERB
    <table><tbody><% rows.each do |row| %><%= row %><% end %></tbody></table>
  ERB
end

class TestController < ActionController::Base
  def index
    render inline: <<~ERB, layout: false
      <%= render(TableComponent.new) do |table| %>
        <% table.with_row do |row| %>
          <% row.with_cell do %>
            <%= t('.edit') %>
          <% end %>
        <% end %>
      <% end %>
    ERB
  end
end

class VirtualPathIssueTest < ActionDispatch::IntegrationTest
  include ViewComponent::TestHelpers

  setup do
    I18n.backend.store_translations(:en, {
      test: {
        index: {
          edit: "Edit from view"
        }
      },
      cell_component: {
        edit: "Edit from cell"
      }
    })
  end

  test "t('.key') in deeply nested slot block should use the view's virtual_path" do
    get "/"

    # The t('.edit') call is in the view (test/index), so it should resolve
    # to "test.index.edit" -> "Edit from view"
    #
    # But due to the bug, it resolves using the cell component's virtual_path
    # to "cell_component.edit" -> "Edit from cell"
    assert_includes response.body, "Edit from view",
      "Expected t('.edit') to resolve using view's scope (test.index.edit), " \
      "but it used the component's scope instead. Response: #{response.body}"
  end
end

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants