ruact 0.0.10 → 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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +123 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +16 -6
  5. data/RELEASING.md +125 -139
  6. data/lib/ruact/packaging.rb +68 -0
  7. data/lib/ruact/version.rb +1 -1
  8. data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
  9. data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
  10. data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
  11. data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
  12. metadata +6 -107
  13. data/.codecov.yml +0 -31
  14. data/.github/workflows/ci.yml +0 -284
  15. data/.github/workflows/server-functions-bench.yml +0 -54
  16. data/.rubocop.yml +0 -107
  17. data/.rubocop_todo.yml +0 -63
  18. data/Rakefile +0 -10
  19. data/bench/server_functions_dispatch_bench.rb +0 -276
  20. data/bench/server_functions_dispatch_bench.results.md +0 -150
  21. data/docs/internal/README.md +0 -9
  22. data/docs/internal/decisions/server-functions-api.md +0 -2236
  23. data/spec/benchmarks/baseline.json +0 -1
  24. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
  25. data/spec/fixtures/flight/README.md +0 -136
  26. data/spec/fixtures/flight/array.txt +0 -1
  27. data/spec/fixtures/flight/as_json_object.txt +0 -2
  28. data/spec/fixtures/flight/bigint.txt +0 -1
  29. data/spec/fixtures/flight/boolean_false.txt +0 -1
  30. data/spec/fixtures/flight/boolean_true.txt +0 -1
  31. data/spec/fixtures/flight/client_component_with_props.txt +0 -2
  32. data/spec/fixtures/flight/client_reference.txt +0 -2
  33. data/spec/fixtures/flight/hash.txt +0 -1
  34. data/spec/fixtures/flight/infinity.txt +0 -1
  35. data/spec/fixtures/flight/nan.txt +0 -1
  36. data/spec/fixtures/flight/negative_infinity.txt +0 -1
  37. data/spec/fixtures/flight/nil.txt +0 -1
  38. data/spec/fixtures/flight/number_float.txt +0 -1
  39. data/spec/fixtures/flight/number_integer.txt +0 -1
  40. data/spec/fixtures/flight/react_element_no_props.txt +0 -1
  41. data/spec/fixtures/flight/redirect_row.txt +0 -1
  42. data/spec/fixtures/flight/serializable_object.txt +0 -2
  43. data/spec/fixtures/flight/string_basic.txt +0 -1
  44. data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
  45. data/spec/fixtures/flight/undefined.txt +0 -1
  46. data/spec/fixtures/readme/children-error.html.erb +0 -3
  47. data/spec/fixtures/readme/children-error.txt +0 -1
  48. data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
  49. data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
  50. data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
  51. data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
  52. data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
  53. data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
  54. data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
  55. data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
  56. data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
  57. data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
  58. data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
  59. data/spec/readme_demo_message_spec.rb +0 -67
  60. data/spec/readme_spec.rb +0 -282
  61. data/spec/ruact/client_manifest_spec.rb +0 -270
  62. data/spec/ruact/component_contract_spec.rb +0 -119
  63. data/spec/ruact/configuration_spec.rb +0 -518
  64. data/spec/ruact/controller_request_spec.rb +0 -671
  65. data/spec/ruact/controller_spec.rb +0 -343
  66. data/spec/ruact/doctor_spec.rb +0 -769
  67. data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
  68. data/spec/ruact/erb_preprocessor_spec.rb +0 -361
  69. data/spec/ruact/errors_spec.rb +0 -93
  70. data/spec/ruact/flight/renderer_spec.rb +0 -133
  71. data/spec/ruact/flight/serializer_spec.rb +0 -494
  72. data/spec/ruact/html_converter_spec.rb +0 -375
  73. data/spec/ruact/install_generator_spec.rb +0 -1549
  74. data/spec/ruact/layout_source_spec.rb +0 -108
  75. data/spec/ruact/manifest_resolver_spec.rb +0 -174
  76. data/spec/ruact/query_request_spec.rb +0 -706
  77. data/spec/ruact/query_spec.rb +0 -105
  78. data/spec/ruact/railtie_spec.rb +0 -155
  79. data/spec/ruact/render_context_spec.rb +0 -58
  80. data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
  81. data/spec/ruact/render_pipeline_spec.rb +0 -928
  82. data/spec/ruact/scaffold_generator_spec.rb +0 -1849
  83. data/spec/ruact/serializable_spec.rb +0 -179
  84. data/spec/ruact/server_bucket_request_spec.rb +0 -785
  85. data/spec/ruact/server_function_name_spec.rb +0 -53
  86. data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
  87. data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
  88. data/spec/ruact/server_functions/codegen_spec.rb +0 -397
  89. data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
  90. data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
  91. data/spec/ruact/server_functions/introspection_spec.rb +0 -135
  92. data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
  93. data/spec/ruact/server_functions/query_context_spec.rb +0 -72
  94. data/spec/ruact/server_functions/query_source_spec.rb +0 -193
  95. data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
  96. data/spec/ruact/server_functions/rake_spec.rb +0 -86
  97. data/spec/ruact/server_functions/route_source_spec.rb +0 -202
  98. data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
  99. data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
  100. data/spec/ruact/server_rescue_request_spec.rb +0 -416
  101. data/spec/ruact/server_spec.rb +0 -179
  102. data/spec/ruact/server_upload_request_spec.rb +0 -311
  103. data/spec/ruact/signed_references_spec.rb +0 -164
  104. data/spec/ruact/string_distance_spec.rb +0 -38
  105. data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
  106. data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
  107. data/spec/ruact/testing/no_production_load_spec.rb +0 -41
  108. data/spec/ruact/validation_errors_spec.rb +0 -116
  109. data/spec/ruact/view_helper_spec.rb +0 -131
  110. data/spec/spec_helper.rb +0 -77
  111. data/spec/support/fixtures/pixel.png +0 -0
  112. data/spec/support/flight_wire_parser.rb +0 -21
  113. data/spec/support/flight_wire_parser_spec.rb +0 -93
  114. data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
  115. data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
  116. data/spec/support/rails_stub.rb +0 -115
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe ErbPreprocessorHook do
7
- # Minimal stand-in for ActionView::Template::Handlers::ERB:
8
- # base implementation just returns source unchanged so we can inspect the
9
- # transformed version that the hook passes to super.
10
- let(:handler_class) do
11
- klass = Class.new do
12
- def call(_template, source)
13
- source
14
- end
15
- end
16
- klass.prepend(described_class)
17
- klass
18
- end
19
-
20
- let(:handler) { handler_class.new }
21
- # Story 13.5 — the hook now forwards +template.identifier+ into the
22
- # preprocessor (for contract-violation file:line), so the stand-in template
23
- # must answer +identifier+.
24
- let(:fake_template) { Struct.new(:identifier).new("app/views/posts/show.html.erb") }
25
-
26
- describe "#call" do
27
- it "applies ErbPreprocessor.transform to source before calling super" do
28
- source = "<LikeButton postId={1} />"
29
- result = handler.call(fake_template, source)
30
- expect(result).to include("__ruact_component__")
31
- expect(result).to include('"LikeButton"')
32
- expect(result).not_to include("<LikeButton")
33
- end
34
-
35
- it "passes source unchanged when no PascalCase tags present (fast-path)" do
36
- source = "<div class=\"hello\"><p>No ruact here</p></div>"
37
- result = handler.call(fake_template, source)
38
- expect(result).to eq(source)
39
- end
40
-
41
- it "processes multiple components in a single template" do
42
- source = "<NavBar /><LikeButton postId={1} />"
43
- result = handler.call(fake_template, source)
44
- expect(result).to include('"NavBar"')
45
- expect(result).to include('"LikeButton"')
46
- end
47
-
48
- it "transforms Suspense tags correctly" do
49
- source = "<Suspense fallback=\"Loading\"><PostCard /></Suspense>"
50
- result = handler.call(fake_template, source)
51
- expect(result).to include("__ruact_component__")
52
- end
53
- end
54
- end
55
- end
@@ -1,361 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe ErbPreprocessor do
7
- subject(:transform) { ->(source) { described_class.transform(source) } }
8
-
9
- describe "self-closing tags" do
10
- it "transforms a self-closing tag with no props" do
11
- expect(transform.call("<Button />")).to eq(%(<%= __ruact_component__("Button", {}) %>))
12
- end
13
-
14
- it "transforms a self-closing tag with props" do
15
- result = transform.call("<LikeButton postId={@post.id} initialCount={5} />")
16
- expect(result).to eq(%(<%= __ruact_component__("LikeButton", { "postId" => @post.id, "initialCount" => 5 }) %>))
17
- end
18
- end
19
-
20
- describe "opening tags" do
21
- it "transforms an opening tag with props" do
22
- result = transform.call("<Dialog open={true}>")
23
- expect(result).to eq(%(<%= __ruact_component__("Dialog", { "open" => true }) %>))
24
- end
25
- end
26
-
27
- describe "passthrough (no transformation)" do
28
- it "does not touch lowercase HTML tags" do
29
- source = '<div class="foo"><span>hello</span></div>'
30
- expect(transform.call(source)).to eq(source)
31
- end
32
-
33
- it "does not touch ERB tags" do
34
- source = "<%= @post.title %>"
35
- expect(transform.call(source)).to eq(source)
36
- end
37
- end
38
-
39
- describe "complex prop expressions" do
40
- it "handles nested braces in a prop value" do
41
- result = transform.call("<Select options={Category.all.map { |c| c.id }} />")
42
- expect(result).to eq(%(<%= __ruact_component__("Select", { "options" => Category.all.map { |c| c.id } }) %>))
43
- end
44
- end
45
-
46
- describe "error handling" do
47
- it "raises PreprocessorError with line number and snippet for unclosed brace (AC#3)" do
48
- source = "<LikeButton postId={@post.id />"
49
- expect { transform.call(source) }
50
- .to raise_error(PreprocessorError, /unclosed brace/)
51
- expect { transform.call(source) }
52
- .to raise_error(PreprocessorError, /line 1/)
53
- expect { transform.call(source) }
54
- .to raise_error(PreprocessorError, /LikeButton/)
55
- end
56
-
57
- it "includes the correct line number for an error on line 3" do
58
- source = "line1\nline2\n<Bad prop={unclosed />"
59
- expect { transform.call(source) }
60
- .to raise_error(PreprocessorError, /line 3/)
61
- end
62
- end
63
-
64
- describe "multiple components" do
65
- it "transforms multiple components in the same string" do
66
- source = '<Button /> and <Badge label={"hello"} />'
67
- result = transform.call(source)
68
- expect(result).to match(/__ruact_component__\("Button"/)
69
- expect(result).to match(/__ruact_component__\("Badge"/)
70
- expect(result).to match(/"hello"/)
71
- end
72
- end
73
-
74
- describe "mixed content" do
75
- it "preserves surrounding HTML while transforming components" do
76
- source = <<~ERB
77
- <div class="container">
78
- <h1>Hello</h1>
79
- <LikeButton postId={1} />
80
- </div>
81
- ERB
82
- result = transform.call(source)
83
- expect(result).to match(/<div class="container">/)
84
- expect(result).to match(%r{<h1>Hello</h1>})
85
- expect(result).to match(/__ruact_component__\("LikeButton"/)
86
- end
87
- end
88
-
89
- # Bugfix (Sprint Change Proposal 2026-06-16 §4.5): `<Suspense delay="2.5">`
90
- # used to be ignored — the preprocessor never carried the attribute, so the
91
- # SuspenseElement always took its default delay. It now forwards an optional
92
- # `delay` as `data-ruact-delay` on the emitted <ruact-suspense> element.
93
- describe "Suspense delay attribute" do
94
- it "forwards delay to the ruact-suspense element as data-ruact-delay" do
95
- result = transform.call(%(<Suspense fallback="loading" delay="2.5"><Spinner /></Suspense>))
96
- expect(result).to include(%(data-ruact-delay="2.5"))
97
- expect(result).to include(%(data-ruact-fallback="loading"))
98
- expect(result).to include("</ruact-suspense>")
99
- end
100
-
101
- it "omits data-ruact-delay when no delay attribute is present" do
102
- result = transform.call(%(<Suspense fallback="loading"><Spinner /></Suspense>))
103
- expect(result).to include(%(data-ruact-fallback="loading"))
104
- expect(result).not_to include("data-ruact-delay")
105
- end
106
-
107
- it "extracts delay regardless of attribute order" do
108
- result = transform.call(%(<Suspense delay="0.75" fallback="wait"><X /></Suspense>))
109
- expect(result).to include(%(data-ruact-delay="0.75"))
110
- expect(result).to include(%(data-ruact-fallback="wait"))
111
- end
112
-
113
- it "accepts a single-quoted delay attribute" do
114
- result = transform.call(%(<Suspense fallback='loading' delay='1.5'><X /></Suspense>))
115
- expect(result).to include(%(data-ruact-delay="1.5"))
116
- end
117
- end
118
-
119
- # Story 15.2 (FR106) — children inside a PascalCase component tag (a matching
120
- # closing tag) fail LOUDLY at preprocess time instead of degrading silently.
121
- describe "loud children error (FR106)", :story_15_2 do
122
- def run(source, identifier: nil)
123
- described_class.transform(source, identifier: identifier)
124
- end
125
-
126
- it "raises on `<Card>Hello</Card>` naming component + fix (AC#1)" do
127
- expect { run("<Card>Hello</Card>") }
128
- .to raise_error(ChildrenNotSupportedError) do |e|
129
- expect(e.message).to include("Card")
130
- expect(e.message).to include("children are not supported")
131
- expect(e.message).to include("pass content as a prop")
132
- expect(e.message).to include("<Card content={...} />")
133
- end
134
- end
135
-
136
- it "is a Ruact::PreprocessorError (AC#1 — subclass IS-A base)" do
137
- expect { run("<Card>Hello</Card>") }.to raise_error(Ruact::PreprocessorError)
138
- end
139
-
140
- it "raises on an empty pair `<Card></Card>` (AC#1)" do
141
- expect { run("<Card></Card>") }.to raise_error(ChildrenNotSupportedError, /Card/)
142
- end
143
-
144
- it "raises on multi-line children (AC#1)" do
145
- source = "<Card>\n <p>hi</p>\n</Card>"
146
- expect { run(source) }.to raise_error(ChildrenNotSupportedError, /children are not supported/)
147
- end
148
-
149
- it "raises on a paired tag that also holds a nested component" do
150
- expect { run("<Card><Button /></Card>") }
151
- .to raise_error(ChildrenNotSupportedError, /<Card>/)
152
- end
153
-
154
- it "names the supplied identifier and the correct line for a non-first-line pair (AC#4)" do
155
- source = "line1\nline2\n<Card>Hello</Card>"
156
- expect { run(source, identifier: "app/views/posts/show.html.erb") }
157
- .to raise_error(ChildrenNotSupportedError) do |e|
158
- expect(e.message).to include("app/views/posts/show.html.erb:3")
159
- end
160
- end
161
-
162
- it "reports the opening tag's line, honoring attributes on the opening tag" do
163
- expect { run("<Card variant={:wide}>x</Card>", identifier: "t.erb") }
164
- .to raise_error(ChildrenNotSupportedError, /t\.erb:1/)
165
- end
166
-
167
- # Regression (Codex Round 1, Patch 1): a MULTI-LINE Suspense opening tag
168
- # must not shift the reported line — the `<Card>` below is physically on
169
- # line 5 and must report :5 (Suspense is masked newline-for-newline).
170
- it "reports the exact source line even after a multi-line Suspense opening" do
171
- source = <<~ERB
172
- <Suspense
173
- fallback="loading"
174
- delay="2.5">
175
- </Suspense>
176
- <Card>Hello</Card>
177
- ERB
178
- expect { run(source, identifier: "t.erb") }
179
- .to raise_error(ChildrenNotSupportedError, /t\.erb:5/)
180
- end
181
-
182
- # Regression (Codex Round 2, Patch 1): a `</Dialog>` living inside an ERB
183
- # island (Ruby string/comment) must NOT be mistaken for a real component
184
- # closing tag — a valid bare `<Dialog open={true}>` stays valid.
185
- it "does not false-pair a bare opening with a `</Tag>` inside an ERB island" do
186
- source = %(<Dialog open={true}>\n<% x = "</Dialog>" %>)
187
- expect { run(source) }.not_to raise_error
188
- end
189
-
190
- it "still fires when the closing tag is real ERB body, not inside `<% %>`" do
191
- expect { run("<Card><%= @body %></Card>") }
192
- .to raise_error(ChildrenNotSupportedError, /Card/)
193
- end
194
-
195
- # Regression (Codex Round 2, Patch 2): many bare non-self-closing openings
196
- # with no close must stay linear (single-pass stack scan) and silent (D3).
197
- it "stays silent and does not blow up on many bare unclosed openings" do
198
- source = "<Dialog open={true}>\n" * 5000
199
- expect { run(source) }.not_to raise_error
200
- end
201
-
202
- # Regression (Codex Round 3, Patch 1): stray/unmatched PascalCase closing
203
- # tags (no preceding matching open) are literal text — never an error — and
204
- # must stay linear (per-name O(1) lookup, no `rindex`). Correctness pin;
205
- # perf verified live, not timed here (avoids a flaky benchmark spec).
206
- it "does not raise on stray closing tags with no matching opening" do
207
- source = "</Card>\n" * 5000
208
- expect { run(source) }.not_to raise_error
209
- end
210
-
211
- it "still raises when a real opening precedes the stray closes" do
212
- source = "<Card>x</Card>\n#{'</Card>' * 100}"
213
- expect { run(source) }.to raise_error(ChildrenNotSupportedError, /Card/)
214
- end
215
-
216
- # AC#2 regression — the loud error must NOT fire on any valid pattern.
217
- describe "does NOT fire on valid patterns (AC#2 byte-identical)" do
218
- it "self-closing tag, no props" do
219
- expect { run("<Button />") }.not_to raise_error
220
- end
221
-
222
- it "self-closing tag with props" do
223
- expect { run("<LikeButton postId={@post.id} />") }.not_to raise_error
224
- end
225
-
226
- it "bare non-self-closing opening tag with NO closing tag (`<Dialog open={true}>`)" do
227
- expect { run("<Dialog open={true}>") }.not_to raise_error
228
- end
229
-
230
- it "nested-brace prop value" do
231
- expect { run("<Select options={Category.all.map { |c| c.id }} />") }.not_to raise_error
232
- end
233
-
234
- it "multiple self-closing components" do
235
- expect { run('<Button /> and <Badge label={"hello"} />') }.not_to raise_error
236
- end
237
-
238
- it "mixed HTML around a self-closing component" do
239
- source = %(<div class="container">\n <h1>Hi</h1>\n <LikeButton postId={1} />\n</div>)
240
- expect { run(source) }.not_to raise_error
241
- end
242
-
243
- it "emits byte-identical output for `<Dialog open={true}>`" do
244
- expect(run("<Dialog open={true}>"))
245
- .to eq(%(<%= __ruact_component__("Dialog", { "open" => true }) %>))
246
- end
247
- end
248
-
249
- # AC#3 — Suspense children are the ONE legitimate paired PascalCase tag and
250
- # must never trip the error (normalized to <ruact-suspense> in Step 1).
251
- describe "Suspense children never trip the error (AC#3)" do
252
- it "does not raise for `<Suspense><Spinner /></Suspense>`" do
253
- expect { run(%(<Suspense fallback="loading"><Spinner /></Suspense>)) }.not_to raise_error
254
- end
255
-
256
- it "normalizes Suspense to <ruact-suspense> exactly as today" do
257
- result = run(%(<Suspense fallback="loading"><Spinner /></Suspense>))
258
- expect(result).to include(%(data-ruact-fallback="loading"))
259
- expect(result).to include("</ruact-suspense>")
260
- end
261
- end
262
- end
263
-
264
- # Story 13.5 (FR100) — preprocess-time component-contract validation, wired
265
- # through an injectable registry seam (a stub responding to +contract_for+).
266
- describe "component contract validation", :story_13_5 do
267
- # A minimal stub registry: maps component name → contract Hash (or nil).
268
- def registry_for(contracts)
269
- Class.new do
270
- def initialize(contracts) = (@contracts = contracts)
271
- def contract_for(name, **) = @contracts[name]
272
- end.new(contracts)
273
- end
274
-
275
- let(:contract) do
276
- { "props" => { "postId" => "required", "initialCount" => "optional" } }
277
- end
278
- let(:registry) { registry_for("LikeButton" => contract) }
279
-
280
- def run(source, identifier: "app/views/posts/show.html.erb")
281
- described_class.transform(source, identifier: identifier, registry: registry)
282
- end
283
-
284
- it "raises on a missing required prop, naming component + file:line + fix (AC#1, AC#3)" do
285
- expect { run("<LikeButton initialCount={5} />") }
286
- .to raise_error(ComponentContractError) do |e|
287
- expect(e.message).to include("LikeButton")
288
- expect(e.message).to include("app/views/posts/show.html.erb:1")
289
- expect(e.message).to include("postId")
290
- expect(e.message).to include("add the required prop")
291
- end
292
- end
293
-
294
- it "raises on an unknown prop with a did-you-mean suggestion (AC#3)" do
295
- expect { run("<LikeButton postId={1} postID={2} />") }
296
- .to raise_error(ComponentContractError, /did you mean "postId"\?/)
297
- end
298
-
299
- it "computes the correct line for a call site lower in the template" do
300
- source = "line1\nline2\n<LikeButton initialCount={5} />"
301
- expect { run(source) }.to raise_error(ComponentContractError, /show\.html\.erb:3/)
302
- end
303
-
304
- it "passes a valid call and emits the normal placeholder (AC#1)" do
305
- result = run("<LikeButton postId={@post.id} initialCount={5} />")
306
- expect(result)
307
- .to eq(%(<%= __ruact_component__("LikeButton", { "postId" => @post.id, "initialCount" => 5 }) %>))
308
- end
309
-
310
- it "does NOT re-wrap the contract error with the generic line/snippet tail" do
311
- expect { run("<LikeButton initialCount={5} />") }
312
- .to raise_error(ComponentContractError) { |e| expect(e.message).not_to match(/at line \d+:/) }
313
- end
314
-
315
- # AC#2 — opt-in / byte-identity: a component with no contract entry is
316
- # validated NOT AT ALL and emits the exact same placeholder as pre-13.5.
317
- describe "opt-in fail-open (AC#2)" do
318
- it "emits byte-identical output for a contract-less component" do
319
- source = "<NavBar foo={1} bar={2} />"
320
- with_contract = run(source) # registry has NO "NavBar" entry → fail open
321
- without_registry = described_class.transform(source, registry: nil)
322
- expected = %(<%= __ruact_component__("NavBar", { "foo" => 1, "bar" => 2 }) %>)
323
- expect(with_contract).to eq(expected)
324
- expect(without_registry).to eq(expected)
325
- end
326
-
327
- it "never consults the registry for a no-tag source (fast path)" do
328
- spy_registry = registry_for({})
329
- allow(spy_registry).to receive(:contract_for).and_call_original
330
- result = described_class.transform("<div><p>plain</p></div>", registry: spy_registry)
331
- expect(result).to eq("<div><p>plain</p></div>")
332
- expect(spy_registry).not_to have_received(:contract_for)
333
- end
334
-
335
- # Codex review (Patch 2) — the DEFAULT registry (`Ruact.manifest`) must
336
- # not even be read when the source has no component tags.
337
- it "never reads Ruact.manifest for a no-tag source (default registry)" do
338
- allow(Ruact).to receive(:manifest)
339
- described_class.transform("<div><p>plain</p></div>")
340
- expect(Ruact).not_to have_received(:manifest)
341
- end
342
- end
343
-
344
- describe "slots (AC#5)" do
345
- let(:contract) do
346
- { "props" => { "title" => "required" }, "slots" => { "header" => "required" } }
347
- end
348
- let(:registry) { registry_for("Card" => contract) }
349
-
350
- it "raises when a required slot attribute is omitted at the call site" do
351
- expect { run("<Card title={@t} />") }
352
- .to raise_error(ComponentContractError, /missing required slot.*header/m)
353
- end
354
-
355
- it "passes when the declared slot is supplied as an attribute" do
356
- expect { run("<Card title={@t} header={@h} />") }.not_to raise_error
357
- end
358
- end
359
- end
360
- end
361
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- RSpec.describe "Error classes" do
7
- describe "Ruact::Error" do
8
- it "is a subclass of StandardError" do
9
- expect(Error.ancestors).to include(StandardError)
10
- end
11
- end
12
-
13
- describe "Ruact::ManifestError" do
14
- it "is a subclass of Ruact::Error" do
15
- expect(ManifestError.ancestors).to include(Error)
16
- end
17
-
18
- it "can be raised and rescued as Ruact::Error" do
19
- expect { raise ManifestError, "test" }.to raise_error(Error)
20
- end
21
- end
22
-
23
- describe "Ruact::SerializationError" do
24
- it "is a subclass of Ruact::Error" do
25
- expect(SerializationError.ancestors).to include(Error)
26
- end
27
-
28
- it "can be raised and rescued as Ruact::Error" do
29
- expect { raise SerializationError, "test" }.to raise_error(Error)
30
- end
31
- end
32
-
33
- describe "Ruact::PreprocessorError" do
34
- it "is a subclass of Ruact::Error" do
35
- expect(PreprocessorError.ancestors).to include(Error)
36
- end
37
-
38
- it "can be raised and rescued as Ruact::Error" do
39
- expect { raise PreprocessorError, "test" }.to raise_error(Error)
40
- end
41
- end
42
-
43
- describe "Ruact::ConfigurationError" do
44
- it "is a subclass of Ruact::Error" do
45
- expect(ConfigurationError.ancestors).to include(Error)
46
- end
47
-
48
- it "can be raised and rescued as Ruact::Error" do
49
- expect { raise ConfigurationError, "test" }.to raise_error(Error)
50
- end
51
- end
52
-
53
- describe "Ruact::HtmlConverterError" do
54
- it "is a subclass of Ruact::Error" do
55
- expect(HtmlConverterError.ancestors).to include(Error)
56
- end
57
-
58
- it "can be raised and rescued as Ruact::Error" do
59
- expect { raise HtmlConverterError, "test" }.to raise_error(Error)
60
- end
61
- end
62
-
63
- describe "Ruact::UploadTooLargeError", :story_8_5 do
64
- it "is a subclass of Ruact::Error (so Story 8.4 rescue_from StandardError catches it)" do
65
- expect(UploadTooLargeError.ancestors).to include(Error)
66
- end
67
-
68
- it "carries received_bytes and limit_bytes attr_readers" do
69
- error = UploadTooLargeError.new(received_bytes: 11_534_336, limit_bytes: 10_485_760)
70
- expect(error.received_bytes).to eq(11_534_336)
71
- expect(error.limit_bytes).to eq(10_485_760)
72
- end
73
-
74
- it "synthesises a default message that includes both numbers" do
75
- error = UploadTooLargeError.new(received_bytes: 11_534_336, limit_bytes: 10_485_760)
76
- expect(error.message).to include("received_bytes=11534336")
77
- expect(error.message).to include("limit_bytes=10485760")
78
- end
79
-
80
- it "honours an explicit custom message" do
81
- error = UploadTooLargeError.new(received_bytes: 1, limit_bytes: 2, message: "custom")
82
- expect(error.message).to eq("custom")
83
- end
84
-
85
- it "can be rescued as Ruact::Error and StandardError" do
86
- expect { raise UploadTooLargeError.new(received_bytes: 1, limit_bytes: 0) }
87
- .to raise_error(Error)
88
- expect { raise UploadTooLargeError.new(received_bytes: 1, limit_bytes: 0) }
89
- .to raise_error(StandardError)
90
- end
91
- end
92
- end
93
- end
@@ -1,133 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- module Ruact
6
- module Flight
7
- RSpec.describe Renderer do
8
- let(:empty_manifest) { ClientManifest.from_hash({}) }
9
- let(:manifest_with_button) do
10
- ClientManifest.from_hash({
11
- "Button" => {
12
- "id" => "/assets/Button-abc.js",
13
- "name" => "Button",
14
- "chunks" => ["/assets/Button-abc.js"]
15
- }
16
- })
17
- end
18
-
19
- describe ".render" do
20
- it "renders ReactElement with no props to fixture" do
21
- element = ReactElement.new(type: "div")
22
- output = described_class.render(element, empty_manifest)
23
- expect(output).to match_flight_fixture("react_element_no_props")
24
- end
25
-
26
- it "returns a String" do
27
- element = ReactElement.new(type: "span")
28
- expect(described_class.render(element, empty_manifest)).to be_a(String)
29
- end
30
-
31
- it "root row always has id 0" do
32
- element = ReactElement.new(type: "p")
33
- output = described_class.render(element, empty_manifest)
34
- expect(Ruact::Spec::FlightWireParser.parse(output).last).to include(id: 0, class: :model)
35
- end
36
- end
37
-
38
- describe ".each" do
39
- it "yields rows when called with a block" do
40
- element = ReactElement.new(type: "div")
41
- rows = []
42
- described_class.each(element, empty_manifest) { |row| rows << row }
43
- expect(rows).not_to be_empty
44
- end
45
-
46
- it "produces identical output to .render" do
47
- element = ReactElement.new(type: "article")
48
- via_each = described_class.each(element, empty_manifest, streaming: false).to_a.join
49
- via_render = described_class.render(element, empty_manifest)
50
- expect(via_each).to eq(via_render)
51
- end
52
-
53
- it "returns Enumerator when called without block" do
54
- element = ReactElement.new(type: "div")
55
- result = described_class.each(element, empty_manifest)
56
- expect(result).to be_an(Enumerator)
57
- end
58
- end
59
-
60
- describe "suspense timeout (Story 3.5 AC #4)" do
61
- let(:fallback) { ReactElement.new(type: "span") }
62
- let(:inner) { ReactElement.new(type: "div") }
63
-
64
- # Configuration is frozen post-Story 7.3, so RSpec mocks cannot proxy it.
65
- # Configure via Ruact.configure and reset around each example.
66
- around do |example|
67
- Ruact.instance_variable_set(:@config, nil)
68
- Ruact.instance_variable_set(:@configured_at_least_once, false)
69
- example.run
70
- Ruact.instance_variable_set(:@config, nil)
71
- Ruact.instance_variable_set(:@configured_at_least_once, false)
72
- end
73
-
74
- context "when deferred delay exceeds suspense_timeout (streaming: true)" do
75
- before do
76
- Ruact.configure { |c| c.suspense_timeout = 1.0 }
77
- end
78
-
79
- it "emits an E-type error row instead of the model row" do
80
- suspense = SuspenseElement.new(fallback: fallback, children: inner, delay: 5.0)
81
- rows = described_class.each(suspense, empty_manifest, streaming: true).to_a
82
- expect(rows).to include(a_string_matching(/:E/))
83
- end
84
-
85
- it "error row contains 'Suspense timeout exceeded'" do
86
- suspense = SuspenseElement.new(fallback: fallback, children: inner, delay: 5.0)
87
- rows = described_class.each(suspense, empty_manifest, streaming: true).to_a
88
- error_row = rows.find { |r| r.include?(":E") }
89
- # Error message string is the contract — see Story 7.6 Decision E.
90
- expect(error_row).to include("Suspense timeout exceeded")
91
- end
92
-
93
- it "does NOT emit a plain model row for the timed-out chunk" do
94
- suspense = SuspenseElement.new(fallback: fallback, children: inner, delay: 5.0)
95
- rows = described_class.each(suspense, empty_manifest, streaming: true).to_a
96
- # Only root (0:) and the error row (1:E...) should exist — no bare model for deferred id
97
- unexpected = rows.reject { |r| r.start_with?("0:") || r.include?(":E") }
98
- expect(unexpected).to be_empty
99
- end
100
- end
101
-
102
- context "when deferred delay is within suspense_timeout (streaming: true)" do
103
- before do
104
- Ruact.configure { |c| c.suspense_timeout = 10.0 }
105
- end
106
-
107
- it "emits a model row (not an error row) and no E row" do
108
- suspense = SuspenseElement.new(fallback: fallback, children: inner, delay: 0.0)
109
- rows = described_class.each(suspense, empty_manifest, streaming: true).to_a
110
- expect(rows).not_to include(a_string_matching(/:E/))
111
- end
112
- end
113
- end
114
-
115
- describe "import row ordering" do
116
- it "emits import rows before the root model row" do
117
- element = ReactElement.new(
118
- type: ClientReference.new(module_id: "/assets/Button-abc.js", export_name: "Button"),
119
- props: {}
120
- )
121
- output = described_class.render(element, manifest_with_button)
122
- lines = output.lines.map(&:strip).reject(&:empty?)
123
-
124
- import_idx = lines.index { |l| l.include?(":I[") }
125
- root_idx = lines.index { |l| l.start_with?("0:") }
126
-
127
- expect(import_idx).not_to be_nil
128
- expect(import_idx).to be < root_idx
129
- end
130
- end
131
- end
132
- end
133
- end