Skip to content

Commit 2527525

Browse files
hasumikinclaude
andcommitted
Address review: validate the component constant, test the props path
render_component now rejects constants that are not Funicular::Component subclasses with a clear ArgumentError instead of failing later with a confusing NoMethodError. The props test misnomer is fixed with a ProbeComponent fixture that reads both props and state, so each channel is asserted through the public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f8e697d commit 2527525

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

lib/funicular/ssr.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def self.render_component(component_name, props: {}, state: {}, source_dir: nil)
5252
raise "Funicular router is not configured; check app/funicular/initializer.rb" unless router
5353

5454
component_class = Object.const_get(component_name.to_s)
55+
unless component_class.is_a?(Class) && component_class < Funicular::Component
56+
raise ArgumentError, "#{component_name} is not a Funicular::Component subclass"
57+
end
58+
5559
instance = component_class.new(symbolize_keys(props))
5660
instance.runtime = Funicular::Runtime.new(router)
5761
instance.seed_state(state)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Reads both props and state so SSR.render_component tests can assert
2+
# each channel is wired through. Not routed on purpose: rendering it
3+
# is only possible through the single-component entry point.
4+
class ProbeComponent < Funicular::Component
5+
def initialize_state
6+
{ note: "default note" }
7+
end
8+
9+
def render
10+
div(class: "probe") do
11+
h2 { props[:label].to_s }
12+
p { state[:note] }
13+
end
14+
end
15+
end

minitest/ssr_test.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def test_route_params_become_props
124124

125125
# --- Single-component render (no route lookup) ------------------------
126126

127-
def test_render_component_renders_by_name_with_props_and_state
127+
def test_render_component_renders_by_name_without_a_route
128128
html = Funicular::SSR.render_component(
129129
"GreetingComponent",
130130
state: { title: "Embedded" },
@@ -134,6 +134,17 @@ def test_render_component_renders_by_name_with_props_and_state
134134
assert_includes html, 'class="greeting"'
135135
end
136136

137+
def test_render_component_passes_props_and_state
138+
html = Funicular::SSR.render_component(
139+
"ProbeComponent",
140+
props: { "label" => "From Props" },
141+
state: { note: "injected note" },
142+
source_dir: APP_DIR
143+
)
144+
assert_includes html, "<h2>From Props</h2>"
145+
assert_includes html, "<p>injected note</p>"
146+
end
147+
137148
def test_render_component_uses_initialize_state_without_injection
138149
html = Funicular::SSR.render_component("GreetingComponent", source_dir: APP_DIR)
139150
assert_includes html, "<h1>Default Title</h1>"
@@ -145,6 +156,13 @@ def test_render_component_raises_on_unknown_component
145156
end
146157
end
147158

159+
def test_render_component_rejects_a_non_component_constant
160+
error = assert_raises(ArgumentError) do
161+
Funicular::SSR.render_component("String", source_dir: APP_DIR)
162+
end
163+
assert_match(/not a Funicular::Component/, error.message)
164+
end
165+
148166
def test_symbolize_keys_handles_nil_and_string_keys
149167
assert_equal({}, Funicular::SSR.symbolize_keys(nil))
150168
assert_equal({ a: 1, b: 2 }, Funicular::SSR.symbolize_keys("a" => 1, "b" => 2))

0 commit comments

Comments
 (0)