ruact 0.0.11 → 0.0.12
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- 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
|