ruact 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +147 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +18 -8
  5. data/RELEASING.md +125 -139
  6. data/lib/generators/ruact/install/install_generator.rb +305 -126
  7. data/lib/generators/ruact/install/templates/AGENTS.md.tt +14 -13
  8. data/lib/generators/ruact/install/templates/initializer.rb.tt +29 -7
  9. data/lib/generators/ruact/install/templates/tsconfig.json.tt +3 -0
  10. data/lib/generators/ruact/layout/layout_generator.rb +52 -0
  11. data/lib/generators/ruact/scaffold/templates/controller.rb.tt +5 -3
  12. data/lib/ruact/configuration.rb +65 -13
  13. data/lib/ruact/controller/document_rendering.rb +72 -16
  14. data/lib/ruact/controller/page_rendering.rb +134 -0
  15. data/lib/ruact/controller/pages.rb +116 -0
  16. data/lib/ruact/controller.rb +78 -11
  17. data/lib/ruact/doctor.rb +233 -25
  18. data/lib/ruact/layout_source.rb +29 -7
  19. data/lib/ruact/navigation_boundary.rb +240 -0
  20. data/lib/ruact/packaging.rb +68 -0
  21. data/lib/ruact/railtie.rb +30 -0
  22. data/lib/ruact/server.rb +10 -1
  23. data/lib/ruact/version.rb +1 -1
  24. data/lib/ruact/view_helper.rb +158 -1
  25. data/lib/ruact/views/layouts/ruact.html.erb +32 -0
  26. data/lib/ruact.rb +29 -0
  27. data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
  28. data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
  29. data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
  30. data/vendor/javascript/vite-plugin-ruact/ruact-router.test.mjs +433 -0
  31. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
  32. data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +170 -7
  33. metadata +12 -107
  34. data/.codecov.yml +0 -31
  35. data/.github/workflows/ci.yml +0 -284
  36. data/.github/workflows/server-functions-bench.yml +0 -54
  37. data/.rubocop.yml +0 -107
  38. data/.rubocop_todo.yml +0 -63
  39. data/Rakefile +0 -10
  40. data/bench/server_functions_dispatch_bench.rb +0 -276
  41. data/bench/server_functions_dispatch_bench.results.md +0 -150
  42. data/docs/internal/README.md +0 -9
  43. data/docs/internal/decisions/server-functions-api.md +0 -2236
  44. data/spec/benchmarks/baseline.json +0 -12
  45. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
  46. data/spec/fixtures/flight/README.md +0 -136
  47. data/spec/fixtures/flight/array.txt +0 -1
  48. data/spec/fixtures/flight/as_json_object.txt +0 -2
  49. data/spec/fixtures/flight/bigint.txt +0 -1
  50. data/spec/fixtures/flight/boolean_false.txt +0 -1
  51. data/spec/fixtures/flight/boolean_true.txt +0 -1
  52. data/spec/fixtures/flight/client_component_with_props.txt +0 -2
  53. data/spec/fixtures/flight/client_reference.txt +0 -2
  54. data/spec/fixtures/flight/hash.txt +0 -1
  55. data/spec/fixtures/flight/infinity.txt +0 -1
  56. data/spec/fixtures/flight/nan.txt +0 -1
  57. data/spec/fixtures/flight/negative_infinity.txt +0 -1
  58. data/spec/fixtures/flight/nil.txt +0 -1
  59. data/spec/fixtures/flight/number_float.txt +0 -1
  60. data/spec/fixtures/flight/number_integer.txt +0 -1
  61. data/spec/fixtures/flight/react_element_no_props.txt +0 -1
  62. data/spec/fixtures/flight/redirect_row.txt +0 -1
  63. data/spec/fixtures/flight/serializable_object.txt +0 -2
  64. data/spec/fixtures/flight/string_basic.txt +0 -1
  65. data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
  66. data/spec/fixtures/flight/undefined.txt +0 -1
  67. data/spec/fixtures/readme/children-error.html.erb +0 -3
  68. data/spec/fixtures/readme/children-error.txt +0 -1
  69. data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
  70. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
  71. data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
  72. data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
  73. data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
  74. data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
  75. data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
  76. data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
  77. data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
  78. data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
  79. data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
  80. data/spec/readme_demo_message_spec.rb +0 -67
  81. data/spec/readme_spec.rb +0 -282
  82. data/spec/ruact/client_manifest_spec.rb +0 -270
  83. data/spec/ruact/component_contract_spec.rb +0 -119
  84. data/spec/ruact/configuration_spec.rb +0 -518
  85. data/spec/ruact/controller_request_spec.rb +0 -671
  86. data/spec/ruact/controller_spec.rb +0 -343
  87. data/spec/ruact/doctor_spec.rb +0 -769
  88. data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
  89. data/spec/ruact/erb_preprocessor_spec.rb +0 -361
  90. data/spec/ruact/errors_spec.rb +0 -93
  91. data/spec/ruact/flight/renderer_spec.rb +0 -133
  92. data/spec/ruact/flight/serializer_spec.rb +0 -494
  93. data/spec/ruact/html_converter_spec.rb +0 -375
  94. data/spec/ruact/install_generator_spec.rb +0 -1549
  95. data/spec/ruact/layout_source_spec.rb +0 -108
  96. data/spec/ruact/manifest_resolver_spec.rb +0 -174
  97. data/spec/ruact/query_request_spec.rb +0 -706
  98. data/spec/ruact/query_spec.rb +0 -105
  99. data/spec/ruact/railtie_spec.rb +0 -155
  100. data/spec/ruact/render_context_spec.rb +0 -58
  101. data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
  102. data/spec/ruact/render_pipeline_spec.rb +0 -928
  103. data/spec/ruact/scaffold_generator_spec.rb +0 -1849
  104. data/spec/ruact/serializable_spec.rb +0 -179
  105. data/spec/ruact/server_bucket_request_spec.rb +0 -785
  106. data/spec/ruact/server_function_name_spec.rb +0 -53
  107. data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
  108. data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
  109. data/spec/ruact/server_functions/codegen_spec.rb +0 -397
  110. data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
  111. data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
  112. data/spec/ruact/server_functions/introspection_spec.rb +0 -135
  113. data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
  114. data/spec/ruact/server_functions/query_context_spec.rb +0 -72
  115. data/spec/ruact/server_functions/query_source_spec.rb +0 -193
  116. data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
  117. data/spec/ruact/server_functions/rake_spec.rb +0 -86
  118. data/spec/ruact/server_functions/route_source_spec.rb +0 -202
  119. data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
  120. data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
  121. data/spec/ruact/server_rescue_request_spec.rb +0 -416
  122. data/spec/ruact/server_spec.rb +0 -179
  123. data/spec/ruact/server_upload_request_spec.rb +0 -311
  124. data/spec/ruact/signed_references_spec.rb +0 -164
  125. data/spec/ruact/string_distance_spec.rb +0 -38
  126. data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
  127. data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
  128. data/spec/ruact/testing/no_production_load_spec.rb +0 -41
  129. data/spec/ruact/validation_errors_spec.rb +0 -116
  130. data/spec/ruact/view_helper_spec.rb +0 -131
  131. data/spec/spec_helper.rb +0 -77
  132. data/spec/support/fixtures/pixel.png +0 -0
  133. data/spec/support/flight_wire_parser.rb +0 -21
  134. data/spec/support/flight_wire_parser_spec.rb +0 -93
  135. data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
  136. data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
  137. data/spec/support/rails_stub.rb +0 -115
@@ -1,375 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe HtmlConverter do
7
- subject(:convert) { ->(html, registry = []) { described_class.convert(html, registry) } }
8
-
9
- describe "text nodes" do
10
- it "returns a plain string for plain text" do
11
- expect(convert.call("hello")).to eq("hello")
12
- end
13
- end
14
-
15
- # Bugfix (Sprint Change Proposal 2026-06-16 §4.5): the ERB `delay` attribute
16
- # is carried as `data-ruact-delay` and must reach SuspenseElement#delay; an
17
- # absent, blank, or unparseable value falls back to the element's default.
18
- describe "Suspense element conversion (delay)" do
19
- let(:default_delay) do
20
- Ruact::Flight::SuspenseElement.new(fallback: nil, children: "x").delay
21
- end
22
-
23
- it "parses data-ruact-delay into SuspenseElement#delay as a Float" do
24
- html = %(<ruact-suspense data-ruact-fallback="loading" data-ruact-delay="2.5"><p>content</p></ruact-suspense>)
25
- result = convert.call(html)
26
- expect(result).to be_a(Ruact::Flight::SuspenseElement)
27
- expect(result.delay).to eq(2.5)
28
- end
29
-
30
- it "falls back to the default delay when the attribute is absent" do
31
- html = %(<ruact-suspense data-ruact-fallback="loading"><p>content</p></ruact-suspense>)
32
- result = convert.call(html)
33
- expect(result).to be_a(Ruact::Flight::SuspenseElement)
34
- expect(result.delay).to eq(default_delay)
35
- end
36
-
37
- it "falls back to the default delay when the attribute is unparseable" do
38
- html = %(<ruact-suspense data-ruact-fallback="loading" data-ruact-delay="soon"><p>content</p></ruact-suspense>)
39
- expect(convert.call(html).delay).to eq(default_delay)
40
- end
41
-
42
- it "falls back to the default delay when the attribute is blank" do
43
- html = %(<ruact-suspense data-ruact-fallback="loading" data-ruact-delay=""><p>content</p></ruact-suspense>)
44
- expect(convert.call(html).delay).to eq(default_delay)
45
- end
46
-
47
- it "treats delay=\"0\" as an explicit zero delay, not absent" do
48
- html = %(<ruact-suspense data-ruact-fallback="loading" data-ruact-delay="0"><p>content</p></ruact-suspense>)
49
- expect(convert.call(html).delay).to eq(0.0)
50
- end
51
-
52
- it "parses a delay value with surrounding whitespace" do
53
- html = %(<ruact-suspense data-ruact-fallback="loading" data-ruact-delay=" 2.5 "><p>content</p></ruact-suspense>)
54
- expect(convert.call(html).delay).to eq(2.5)
55
- end
56
-
57
- it "falls back to the default delay when the value overflows to a non-finite Float" do
58
- # Float("1e309") => Infinity (Float() does not raise on overflow); a
59
- # non-finite delay must not reach the renderer's sleep (RangeError).
60
- html = %(<ruact-suspense data-ruact-fallback="loading" data-ruact-delay="1e309"><p>content</p></ruact-suspense>)
61
- expect(convert.call(html).delay).to eq(default_delay)
62
- end
63
- end
64
-
65
- describe "single DOM element" do
66
- it "converts a div with class and text child" do
67
- result = convert.call('<div class="box">hi</div>')
68
- expect(result).to be_a(Flight::ReactElement)
69
- expect(result.type).to eq("div")
70
- expect(result.props["className"]).to eq("box")
71
- expect(result.props["children"]).to eq("hi")
72
- end
73
-
74
- it "converts `for` attribute to `htmlFor`" do
75
- result = convert.call('<label for="email">Email</label>')
76
- expect(result.props.keys).to include("htmlFor")
77
- expect(result.props["htmlFor"]).to eq("email")
78
- end
79
-
80
- it "extracts `data-react-key` into the element key and removes it from props" do
81
- result = convert.call('<article data-react-key="post-1">body</article>')
82
- expect(result.key).to eq("post-1")
83
- expect(result.props).not_to have_key("data-react-key")
84
- end
85
- end
86
-
87
- describe "nested elements" do
88
- it "produces a child ReactElement for a nested element" do
89
- result = convert.call("<div><span>hello</span></div>")
90
- expect(result.type).to eq("div")
91
- child = result.props["children"]
92
- expect(child).to be_a(Flight::ReactElement)
93
- expect(child.type).to eq("span")
94
- end
95
-
96
- it "produces an array of children for multiple child elements" do
97
- result = convert.call("<ul><li>a</li><li>b</li></ul>")
98
- children = result.props["children"]
99
- expect(children).to be_an(Array)
100
- expect(children.length).to eq(2)
101
- expect(children[0].type).to eq("li")
102
- expect(children[1].type).to eq("li")
103
- end
104
- end
105
-
106
- describe "fragment (multiple root elements)" do
107
- it "returns an array for multiple sibling root elements" do
108
- result = convert.call("<p>one</p><p>two</p>")
109
- expect(result).to be_an(Array)
110
- expect(result.length).to eq(2)
111
- end
112
- end
113
-
114
- describe "client component via registry" do
115
- it "replaces Ruact comment placeholder with the registered ReactElement" do
116
- ref = Flight::ClientReference.new(module_id: "./LikeButton", export_name: "LikeButton")
117
- registry = [{ token: "__RUACT_0__", name: "LikeButton", ref: ref, props: { "postId" => 1 } }]
118
- result = convert.call("<!-- __RUACT_0__ -->", registry)
119
-
120
- expect(result).to be_a(Flight::ReactElement)
121
- expect(result.type).to eq(ref)
122
- expect(result.props).to eq({ "postId" => 1 })
123
- end
124
-
125
- it "wraps a client component inside a parent DOM element" do
126
- ref = Flight::ClientReference.new(module_id: "./Button", export_name: "Button")
127
- registry = [{ token: "__RUACT_0__", name: "Button", ref: ref, props: {} }]
128
- result = convert.call('<div class="wrapper"><!-- __RUACT_0__ --></div>', registry)
129
-
130
- expect(result.type).to eq("div")
131
- child = result.props["children"]
132
- expect(child).to be_a(Flight::ReactElement)
133
- expect(child.type).to eq(ref)
134
- end
135
- end
136
-
137
- describe "form element value handling" do
138
- it "converts text input value to defaultValue (uncontrolled)" do
139
- result = convert.call('<input type="text" value="hello" />')
140
- expect(result.props).to have_key("defaultValue")
141
- expect(result.props["defaultValue"]).to eq("hello")
142
- expect(result.props).not_to have_key("value")
143
- end
144
-
145
- it "preserves value on submit inputs (controlled is correct for buttons)" do
146
- result = convert.call('<input type="submit" value="Save" />')
147
- expect(result.props).to have_key("value")
148
- expect(result.props["value"]).to eq("Save")
149
- expect(result.props).not_to have_key("defaultValue")
150
- end
151
-
152
- it "preserves value on reset inputs" do
153
- result = convert.call('<input type="reset" value="Clear" />')
154
- expect(result.props["value"]).to eq("Clear")
155
- expect(result.props).not_to have_key("defaultValue")
156
- end
157
-
158
- it "converts textarea value to defaultValue" do
159
- result = convert.call('<textarea value="content">content</textarea>')
160
- expect(result.props["defaultValue"]).to eq("content")
161
- expect(result.props).not_to have_key("value")
162
- end
163
-
164
- it "converts select value to defaultValue" do
165
- result = convert.call('<select value="b"><option value="a">A</option><option value="b">B</option></select>')
166
- expect(result.props["defaultValue"]).to eq("b")
167
- expect(result.props).not_to have_key("value")
168
- end
169
-
170
- it "converts checked attribute to defaultChecked" do
171
- result = convert.call('<input type="checkbox" checked="checked" />')
172
- expect(result.props).to have_key("defaultChecked")
173
- expect(result.props["defaultChecked"]).to eq("checked")
174
- expect(result.props).not_to have_key("checked")
175
- end
176
- end
177
-
178
- describe "style attribute" do
179
- it "converts a CSS string to a camelCase hash" do
180
- result = convert.call('<h1 style="font-size:28px;font-weight:700;margin-bottom:24px">Title</h1>')
181
- expect(result.props["style"]).to eq({
182
- "fontSize" => "28px",
183
- "fontWeight" => "700",
184
- "marginBottom" => "24px"
185
- })
186
- end
187
-
188
- it "handles multi-word property values without splitting" do
189
- result = convert.call('<div style="max-width:640px;margin:40px auto">x</div>')
190
- expect(result.props["style"]).to eq({
191
- "maxWidth" => "640px",
192
- "margin" => "40px auto"
193
- })
194
- end
195
- end
196
-
197
- describe "input validation (Story 7.4)" do
198
- describe "nil input" do
199
- it "raises Ruact::HtmlConverterError" do
200
- expect { described_class.convert(nil) }.to raise_error(Ruact::HtmlConverterError)
201
- end
202
-
203
- it "message contains 'received nil; expected a String of HTML'" do
204
- expect { described_class.convert(nil) }
205
- .to raise_error(Ruact::HtmlConverterError,
206
- /HtmlConverter\.convert received nil; expected a String of HTML/)
207
- end
208
-
209
- it "message contains the 'Most likely cause' hint" do
210
- expect { described_class.convert(nil) }
211
- .to raise_error(Ruact::HtmlConverterError, /Most likely cause:/)
212
- expect { described_class.convert(nil) }
213
- .to raise_error(Ruact::HtmlConverterError, /missing yield, an empty respond_to branch/)
214
- end
215
-
216
- it "message contains the documentation pointer" do
217
- expect { described_class.convert(nil) }
218
- .to raise_error(Ruact::HtmlConverterError,
219
- /See HtmlConverter\.convert documentation for the canonical contract\./)
220
- end
221
-
222
- it "message points at the spec's file:line, not html_converter.rb (Called from)" do
223
- expected_line = __LINE__ + 1
224
- expect { described_class.convert(nil) }
225
- .to raise_error(Ruact::HtmlConverterError,
226
- /Called from: #{Regexp.escape(__FILE__)}:#{expected_line}/)
227
- end
228
-
229
- it "does not invoke Nokogiri::HTML::DocumentFragment.parse" do
230
- allow(Nokogiri::HTML::DocumentFragment).to receive(:parse).and_call_original
231
- expect { described_class.convert(nil) }.to raise_error(Ruact::HtmlConverterError)
232
- expect(Nokogiri::HTML::DocumentFragment).not_to have_received(:parse)
233
- end
234
- end
235
-
236
- describe "non-String inputs" do
237
- it "raises with 'got Integer' and 'Received: 42' for an Integer" do
238
- expect { described_class.convert(42) }
239
- .to raise_error(Ruact::HtmlConverterError,
240
- /HtmlConverter\.convert expected a String of HTML; got Integer/)
241
- expect { described_class.convert(42) }
242
- .to raise_error(Ruact::HtmlConverterError, /Received: 42/)
243
- end
244
-
245
- it "raises with 'got Symbol' for a Symbol" do
246
- expect { described_class.convert(:symbol) }
247
- .to raise_error(Ruact::HtmlConverterError, /got Symbol/)
248
- expect { described_class.convert(:symbol) }
249
- .to raise_error(Ruact::HtmlConverterError, /Received: :symbol/)
250
- end
251
-
252
- it "raises with 'got Array' for an Array" do
253
- expect { described_class.convert(["array"]) }
254
- .to raise_error(Ruact::HtmlConverterError, /got Array/)
255
- expect { described_class.convert(["array"]) }
256
- .to raise_error(Ruact::HtmlConverterError, /Received: \["array"\]/)
257
- end
258
-
259
- it "raises with 'got Hash' for a Hash" do
260
- expect { described_class.convert({ hash: 1 }) }
261
- .to raise_error(Ruact::HtmlConverterError, /got Hash/)
262
- expect { described_class.convert({ hash: 1 }) }
263
- .to raise_error(Ruact::HtmlConverterError, /Received: \{.*hash.*1.*\}/)
264
- end
265
-
266
- it "raises with 'got Object' for a generic Object" do
267
- expect { described_class.convert(Object.new) }
268
- .to raise_error(Ruact::HtmlConverterError, /got Object/)
269
- end
270
-
271
- it "message points at the spec's file:line for non-nil non-String input" do
272
- expected_line = __LINE__ + 1
273
- expect { described_class.convert(42) }
274
- .to raise_error(Ruact::HtmlConverterError,
275
- /Called from: #{Regexp.escape(__FILE__)}:#{expected_line}/)
276
- end
277
-
278
- it "truncates large input previews to ≤ 84 chars (80 + '...')" do
279
- large_value = (1..10_000).to_a
280
- message = nil
281
- begin
282
- described_class.convert(large_value)
283
- rescue Ruact::HtmlConverterError => e
284
- message = e.message
285
- end
286
-
287
- received_line = message.lines.find { |l| l.include?("Received:") }
288
- expect(received_line).not_to be_nil
289
- preview = received_line.sub(/^\s*Received:\s/, "").strip
290
- expect(preview.length).to be <= 84
291
- expect(preview).to end_with("...")
292
- end
293
- end
294
-
295
- describe "subclasses and edge cases" do
296
- it "accepts String subclasses (e.g. ActionView SafeBuffer-like)" do
297
- safe_buffer_class = Class.new(String)
298
- input = safe_buffer_class.new("<div>hi</div>")
299
- expect { described_class.convert(input) }.not_to raise_error
300
-
301
- result = described_class.convert(input)
302
- expect(result).to be_a(Ruact::Flight::ReactElement)
303
- expect(result.type).to eq("div")
304
- end
305
-
306
- it "accepts an empty string without raising" do
307
- expect { described_class.convert("") }.not_to raise_error
308
- end
309
-
310
- it "raises Ruact::HtmlConverterError (not NoMethodError) for BasicObject inputs" do
311
- basic = BasicObject.new
312
- expect { described_class.convert(basic) }
313
- .to raise_error(Ruact::HtmlConverterError, /HtmlConverter\.convert expected a String of HTML/)
314
- end
315
-
316
- it "produces a readable message even when value#inspect raises" do
317
- hostile = Class.new do
318
- def inspect
319
- raise "I refuse to introspect"
320
- end
321
- end.new
322
-
323
- expect { described_class.convert(hostile) }
324
- .to raise_error(Ruact::HtmlConverterError, /Received: <inspect raised>/)
325
- end
326
-
327
- it "produces a readable message even when value#class raises (BasicObject)" do
328
- basic = BasicObject.new
329
- expect { described_class.convert(basic) }
330
- .to raise_error(Ruact::HtmlConverterError) do |error|
331
- expect(error.message).to include("got ")
332
- expect(error.message).not_to match(/got\s*$/)
333
- end
334
- end
335
- end
336
-
337
- describe "backtrace shape" do
338
- it "frame 0 of the backtrace points outside the gem" do
339
- described_class.convert(nil)
340
- rescue Ruact::HtmlConverterError => e
341
- expect(e.backtrace.first).not_to include("/lib/ruact/html_converter.rb")
342
- expect(e.backtrace.first).to include(__FILE__)
343
- end
344
-
345
- it "frame 0 references the test file when called from a spec" do
346
- described_class.convert(42)
347
- rescue Ruact::HtmlConverterError => e
348
- expect(e.backtrace.first).to start_with(__FILE__)
349
- end
350
- end
351
- end
352
-
353
- # Detailed coverage of HtmlConverter input validation (nil + non-String inputs,
354
- # backtrace shape, BasicObject handling) lives in the "input validation (Story 7.4)"
355
- # block above (lines 147-300). This regression suite is intentionally minimal — two
356
- # smoke specs that name the suite for greppability via `:story_7_7` and the
357
- # "Story 7.7" describe substring; do NOT duplicate the 7.4 detailed coverage here.
358
- describe "edge cases (Story 7.7) — code-review regression suite", :story_7_7 do
359
- it "raises Ruact::HtmlConverterError for nil input (smoke; full coverage at :147-184)" do
360
- expect { described_class.convert(nil) }.to raise_error(Ruact::HtmlConverterError) do |error|
361
- expect(error.message).to include("received nil")
362
- expect(error.message).to include("expected a String of HTML")
363
- end
364
- end
365
-
366
- it "raises Ruact::HtmlConverterError for a non-String input (smoke; full coverage at :186-243)" do
367
- expect { described_class.convert(42) }.to raise_error(Ruact::HtmlConverterError) do |error|
368
- expect(error.message).to include("expected a String of HTML")
369
- expect(error.message).to include("got Integer")
370
- expect(error.message).to include("Received: 42")
371
- end
372
- end
373
- end
374
- end
375
- end