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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -48
  3. data/CONTRIBUTING.md +233 -0
  4. data/README.md +14 -4
  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 -12
  24. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
  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,170 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "ruact/testing"
5
-
6
- # Story 15.4 (FR108) — end-to-end specs for the PUBLIC render-assertion matcher
7
- # `have_ruact_component`. Every example asserts against a REAL Flight wire body
8
- # produced by `Ruact::RenderPipeline` (the same serializer the controller runs),
9
- # not a hand-authored fixture — so import rows, `$L<id>` references, and the
10
- # serialized props hash are exactly what a host would receive.
11
- module Ruact
12
- RSpec.describe "have_ruact_component matcher", :story_15_4 do
13
- let(:manifest) do
14
- ClientManifest.from_hash({
15
- "LikeButton" => {
16
- "id" => "/assets/LikeButton-abc.js",
17
- "name" => "LikeButton",
18
- "chunks" => ["/assets/LikeButton-abc.js"]
19
- },
20
- "PostCard" => {
21
- "id" => "/components/PostCard.jsx",
22
- "name" => "PostCard",
23
- "chunks" => ["/components/PostCard.jsx"]
24
- }
25
- })
26
- end
27
-
28
- let(:pipeline) { RenderPipeline.new(manifest) }
29
-
30
- # Renders an ERB snippet to a REAL Flight wire string.
31
- def render_wire(erb_source, **ivars)
32
- ctx = Object.new
33
- ivars.each { |k, v| ctx.instance_variable_set("@#{k}", v) }
34
- pipeline.render({ erb: erb_source, binding: ctx.instance_eval { binding } }, mode: :string)
35
- end
36
-
37
- # Wraps a wire in the REAL HTML shell the gem emits (`__FLIGHT_DATA` push),
38
- # via the shipped `Ruact::ViewHelper#ruact_flight_data_script`.
39
- def html_shell(wire)
40
- emitter = Object.new.extend(Ruact::ViewHelper)
41
- script = emitter.send(:ruact_flight_data_script, wire)
42
- "<!DOCTYPE html><html><body><div id=\"root\"></div>#{script}</body></html>"
43
- end
44
-
45
- # A minimal duck-typed response object exposing `.body` (+ optional
46
- # `.content_type`), like an ActionDispatch/Rack response.
47
- def response_double(body, content_type: nil)
48
- Struct.new(:body, :content_type).new(body, content_type)
49
- end
50
-
51
- describe "presence (raw text/x-component body)" do
52
- it "passes when the named component was rendered" do
53
- wire = render_wire("<LikeButton postId={42} />")
54
- expect(wire).to have_ruact_component("LikeButton")
55
- end
56
-
57
- it "resolves the name by module basename too (D3)" do
58
- wire = render_wire("<PostCard title={\"Hi\"} />")
59
- # Manifest module path is /components/PostCard.jsx → basename PostCard.
60
- expect(wire).to have_ruact_component("PostCard")
61
- end
62
-
63
- it "fails with a legible message naming what was found when absent" do
64
- wire = render_wire("<LikeButton />")
65
- expect do
66
- expect(wire).to have_ruact_component("Missing")
67
- end.to raise_error(RSpec::Expectations::ExpectationNotMetError,
68
- /component named "Missing".*found.*LikeButton/m)
69
- end
70
-
71
- it "supports negation for an absent component" do
72
- wire = render_wire("<LikeButton />")
73
- expect(wire).not_to have_ruact_component("Missing")
74
- end
75
-
76
- it "negation fails legibly when the component IS present" do
77
- wire = render_wire("<LikeButton />")
78
- expect do
79
- expect(wire).not_to have_ruact_component("LikeButton")
80
- end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /NOT to have rendered.*"LikeButton"/m)
81
- end
82
- end
83
-
84
- describe ".with_props (serialized wire form, D4)" do
85
- it "passes when props satisfy a hash_including matcher" do
86
- wire = render_wire("<LikeButton postId={42} />")
87
- expect(wire).to have_ruact_component("LikeButton").with_props(a_hash_including("postId" => 42))
88
- end
89
-
90
- it "passes with a bare key-presence include matcher" do
91
- wire = render_wire("<LikeButton postId={42} />")
92
- expect(wire).to have_ruact_component("LikeButton").with_props(including("postId"))
93
- end
94
-
95
- it "fails with a legible message when props mismatch" do
96
- wire = render_wire("<LikeButton postId={42} />")
97
- expect do
98
- expect(wire).to have_ruact_component("LikeButton").with_props(a_hash_including("postId" => 99))
99
- end.to raise_error(RSpec::Expectations::ExpectationNotMetError,
100
- /props matching.*serialized wire form.*"postId".*42/m)
101
- end
102
-
103
- it "supports negation with props" do
104
- wire = render_wire("<LikeButton postId={42} />")
105
- expect(wire).not_to have_ruact_component("LikeButton").with_props(a_hash_including("postId" => 99))
106
- end
107
- end
108
-
109
- describe "input shapes (D2)" do
110
- it "accepts a response object exposing #body (raw wire)" do
111
- wire = render_wire("<LikeButton postId={7} />")
112
- response = response_double(wire, content_type: "text/x-component")
113
- expect(response).to have_ruact_component("LikeButton").with_props(a_hash_including("postId" => 7))
114
- end
115
-
116
- it "extracts Flight from an HTML shell embedding __FLIGHT_DATA" do
117
- wire = render_wire("<LikeButton postId={5} />")
118
- html = html_shell(wire)
119
- expect(html).to have_ruact_component("LikeButton").with_props(a_hash_including("postId" => 5))
120
- end
121
-
122
- it "ignores an unrelated earlier .push and anchors to __FLIGHT_DATA" do
123
- wire = render_wire("<LikeButton postId={5} />")
124
- shell = html_shell(wire)
125
- # A realistic layout has other scripts BEFORE the ruact bootstrap.
126
- noise = %(<script>window.dataLayer=window.dataLayer||[];dataLayer.push("not flight");</script>)
127
- html = shell.sub("<div id=\"root\"></div>", "<div id=\"root\"></div>#{noise}")
128
- expect(html).to have_ruact_component("LikeButton").with_props(a_hash_including("postId" => 5))
129
- end
130
-
131
- it "extracts Flight from an HTML-response object (content_type text/html)" do
132
- wire = render_wire("<LikeButton postId={5} />")
133
- response = response_double(html_shell(wire), content_type: "text/html; charset=utf-8")
134
- expect(response).to have_ruact_component("LikeButton")
135
- end
136
-
137
- it "raises a clear error on a plain-JSON function-call body" do
138
- json = response_double(%({"id":1,"title":"Hi"}), content_type: "application/json")
139
- expect do
140
- expect(json).to have_ruact_component("PostCard")
141
- end.to raise_error(Ruact::Testing::NotAFlightResponseError, /plain-JSON response.*JSON\.parse/m)
142
- end
143
-
144
- it "raises a clear error on an empty (204) body" do
145
- empty = response_double("", content_type: nil)
146
- expect do
147
- expect(empty).to have_ruact_component("PostCard")
148
- end.to raise_error(Ruact::Testing::NotAFlightResponseError, /empty response body/m)
149
- end
150
-
151
- it "raises a clear error when handed an HTML doc with no Flight payload" do
152
- html = "<!DOCTYPE html><html><body><h1>Nothing here</h1></body></html>"
153
- expect do
154
- expect(html).to have_ruact_component("PostCard")
155
- end.to raise_error(Ruact::Testing::NotAFlightResponseError, /no `__FLIGHT_DATA` payload/m)
156
- end
157
- end
158
-
159
- describe "escape round-trip through the HTML shell" do
160
- it "recovers a payload containing quotes and interpolation-like sequences" do
161
- # Props whose serialized form contains characters Ruby's #inspect
162
- # escapes (`"`, `#{`, newlines) must survive extraction from the shell.
163
- wire = render_wire("<LikeButton label={@label} />", label: %(a "quoted" \#{literal}\nvalue))
164
- html = html_shell(wire)
165
- expect(html).to have_ruact_component("LikeButton")
166
- .with_props(a_hash_including("label" => a_string_matching(/quoted/)))
167
- end
168
- end
169
- end
170
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "English"
5
- require "shellwords"
6
-
7
- # Story 15.4 (FR108), AC4c + AC2 — the public test surface must not load in a
8
- # production boot, and the promoted parser/diff must be the SINGLE shipped
9
- # implementation the internal `Ruact::Spec` matchers share (wrap, not fork).
10
- module Ruact
11
- RSpec.describe "Ruact::Testing load boundary", :story_15_4 do
12
- it "a bare `require \"ruact\"` does NOT define the public test surface" do
13
- # Shell out to a pristine Ruby process so nothing this suite already
14
- # loaded (spec/support requires `ruact/testing/*`) pollutes the check.
15
- lib = File.expand_path("../../../lib", __dir__)
16
- script = <<~RUBY
17
- $LOAD_PATH.unshift(#{lib.inspect})
18
- require "ruact"
19
- defined = Ruact.const_defined?(:Testing, false)
20
- # `have_ruact_component` must not be registered on the RSpec matcher surface either.
21
- rspec_loaded = $LOADED_FEATURES.any? { |f| f.include?("rspec/expectations") }
22
- print [defined, rspec_loaded].inspect
23
- RUBY
24
-
25
- output = `#{RbConfig.ruby} -e #{Shellwords.escape(script)} 2>&1`
26
- expect($CHILD_STATUS).to be_success, "subprocess failed: #{output}"
27
- expect(output).to eq("[false, false]"),
28
- "expected `require \"ruact\"` to leave Ruact::Testing undefined and " \
29
- "RSpec unloaded, got: #{output}"
30
- end
31
-
32
- it "shares ONE implementation with the internal matchers (no fork)" do
33
- require "ruact/testing"
34
- require_relative "../../support/flight_wire_parser"
35
- require_relative "../../support/matchers/flight_fixture_matcher"
36
-
37
- expect(Ruact::Spec::FlightWireParser).to equal(Ruact::Testing::FlightWireParser)
38
- expect(Ruact::Spec::FlightStructureDiff).to equal(Ruact::Testing::FlightStructureDiff)
39
- end
40
- end
41
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Story 13.3 (FR98) — unit specs for the pure validation-error normalizer
4
- # Ruact::ServerFunctions::ValidationErrors.normalize. The normalizer turns an
5
- # ActiveModel-ish record, a raw ActiveModel::Errors, or a pre-shaped Hash into
6
- # the canonical wire shape `{ [attribute: String] => Array<String> }` (full
7
- # messages, `base` key, `{}` on a valid/empty/nil source, idempotent on
8
- # already-canonical input). Pure — no Rails/request/Ruact.config reads.
9
-
10
- require "spec_helper"
11
- require "active_model"
12
-
13
- require "ruact/server_functions/validation_errors"
14
-
15
- module Ruact
16
- module ServerFunctions
17
- RSpec.describe ValidationErrors, :story_13_3 do
18
- # A minimal ActiveModel record so the normalizer can exercise the real
19
- # `errors.group_by_attribute` / `ActiveModel::Error#full_message` API
20
- # (present since Rails 6.1; gem pins `rails ~> 8.0`).
21
- let(:record_class) do
22
- Class.new do
23
- include ActiveModel::Model
24
- include ActiveModel::Attributes
25
-
26
- attribute :title, :string
27
- attribute :body, :string
28
-
29
- def self.name = "ValidationErrorsSpecPost"
30
- end
31
- end
32
-
33
- describe ".normalize" do
34
- context "with a record responding to #errors" do
35
- it "returns the canonical { attr => [full_message] } shape" do
36
- record = record_class.new
37
- record.errors.add(:title, :blank) # "Title can't be blank"
38
-
39
- expect(described_class.normalize(record)).to eq("title" => ["Title can't be blank"])
40
- end
41
-
42
- it "keys base-level errors under the string 'base' with the bare message" do
43
- record = record_class.new
44
- record.errors.add(:base, "is invalid overall")
45
-
46
- expect(described_class.normalize(record)).to eq("base" => ["is invalid overall"])
47
- end
48
-
49
- it "groups multiple attributes and multiple messages per attribute" do
50
- record = record_class.new
51
- record.errors.add(:title, :blank) # "Title can't be blank"
52
- record.errors.add(:title, "is too short") # "Title is too short"
53
- record.errors.add(:body, "is invalid") # "Body is invalid"
54
-
55
- expect(described_class.normalize(record)).to eq(
56
- "title" => ["Title can't be blank", "Title is too short"],
57
- "body" => ["Body is invalid"]
58
- )
59
- end
60
-
61
- it "returns {} for a record with no errors (success symmetry)" do
62
- expect(described_class.normalize(record_class.new)).to eq({})
63
- end
64
- end
65
-
66
- context "with a raw ActiveModel::Errors" do
67
- it "normalizes the Errors object directly" do
68
- record = record_class.new
69
- record.errors.add(:title, :blank)
70
-
71
- expect(described_class.normalize(record.errors)).to eq("title" => ["Title can't be blank"])
72
- end
73
- end
74
-
75
- context "with a pre-shaped Hash" do
76
- it "stringifies keys and leaves canonical array values untouched (idempotent)" do
77
- canonical = { "title" => ["Title can't be blank"] }
78
-
79
- expect(described_class.normalize(canonical)).to eq(canonical)
80
- end
81
-
82
- it "stringifies symbol keys" do
83
- expect(described_class.normalize(title: ["x"])).to eq("title" => ["x"])
84
- end
85
-
86
- it "coerces a scalar value into a one-element array" do
87
- expect(described_class.normalize("title" => "can't be blank")).to eq("title" => ["can't be blank"])
88
- end
89
-
90
- it "coerces non-string message values to strings" do
91
- expect(described_class.normalize("count" => [1, 2])).to eq("count" => %w[1 2])
92
- end
93
-
94
- it "returns {} for an empty Hash" do
95
- expect(described_class.normalize({})).to eq({})
96
- end
97
- end
98
-
99
- context "with nil / empty sources" do
100
- it "returns {} for nil" do
101
- expect(described_class.normalize(nil)).to eq({})
102
- end
103
- end
104
-
105
- it "is idempotent — normalizing its own output yields the same Hash" do
106
- record = record_class.new
107
- record.errors.add(:title, :blank)
108
- record.errors.add(:base, "is invalid overall")
109
-
110
- once = described_class.normalize(record)
111
- expect(described_class.normalize(once)).to eq(once)
112
- end
113
- end
114
- end
115
- end
116
- end
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require "active_support/core_ext/string/output_safety"
5
- require "active_support/string_inquirer"
6
-
7
- module Ruact
8
- RSpec.describe ViewHelper do
9
- let(:render_context) { RenderContext.new }
10
- let(:helper_obj) do
11
- obj = Object.new
12
- obj.extend(described_class)
13
- obj.instance_variable_set(:@ruact_render_context, render_context)
14
- obj
15
- end
16
-
17
- describe "#__ruact_component__" do
18
- it "registers the component in the render context and returns an HTML comment" do
19
- result = helper_obj.__ruact_component__("NavBar", { "currentUser" => 1 })
20
- expect(result).to match(/<!-- __RUACT_\d+__ -->/)
21
- expect(render_context.components.length).to eq(1)
22
- expect(render_context.components.first[:name]).to eq("NavBar")
23
- expect(render_context.components.first[:props]).to eq({ "currentUser" => 1 })
24
- end
25
-
26
- it "returns an html_safe string so ActionView does not escape the comment" do
27
- result = helper_obj.__ruact_component__("Button", {})
28
- expect(result).to be_html_safe
29
- end
30
-
31
- it "uses incrementing token numbers for successive registrations" do
32
- token0 = helper_obj.__ruact_component__("Foo", {})
33
- token1 = helper_obj.__ruact_component__("Bar", {})
34
- expect(token0).to include("__RUACT_0__")
35
- expect(token1).to include("__RUACT_1__")
36
- end
37
-
38
- it "passes props through to the registry entry" do
39
- helper_obj.__ruact_component__("LikeButton", { "postId" => 42, "label" => "Like" })
40
- entry = render_context.components.first
41
- expect(entry[:props]["postId"]).to eq(42)
42
- expect(entry[:props]["label"]).to eq("Like")
43
- end
44
-
45
- it "raises a clear error when called outside a ruact_render flow" do
46
- bare = Object.new
47
- bare.extend(described_class)
48
- expect { bare.__ruact_component__("NavBar", {}) }
49
- .to raise_error(Ruact::Error, /__ruact_component__ called outside a ruact_render flow/)
50
- end
51
- end
52
-
53
- # Story 14.2 (FR104) — the public JS-asset helper. Emits the dev/prod
54
- # bootstrap entry `<script>` tags (re-targeting `virtual:ruact/bootstrap`)
55
- # plus the `__FLIGHT_DATA` inline script. The controller delegates to this
56
- # one implementation (parity asserted in controller_spec).
57
- describe "#ruact_js_assets", :story_14_2 do
58
- let(:asset_helper) do
59
- obj = Object.new
60
- obj.extend(described_class)
61
- obj
62
- end
63
- let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
64
-
65
- context "when in dev with the Vite dev server running" do
66
- before do
67
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
68
- allow(asset_helper).to receive(:vite_dev_running?).and_return(true)
69
- end
70
-
71
- it "emits the react-refresh preamble, @vite/client, and the bootstrap module", :aggregate_failures do
72
- html = asset_helper.ruact_js_assets(payload)
73
- expect(html).to include("__vite_plugin_react_preamble_installed__")
74
- expect(html).to include("http://localhost:5173/@vite/client")
75
- # Targets the virtual entry at the dev server's /@id/__x00__ URL — NOT
76
- # a stale application.jsx path.
77
- expect(html).to include("/@id/__x00__#{Ruact.bootstrap_virtual_id}")
78
- expect(html).not_to include("application.jsx")
79
- end
80
-
81
- it "includes the __FLIGHT_DATA inline bootstrap script", :aggregate_failures do
82
- html = asset_helper.ruact_js_assets(payload)
83
- expect(html).to include("__FLIGHT_DATA")
84
- expect(html).to include("d.push(")
85
- end
86
-
87
- it "returns an html_safe buffer" do
88
- expect(asset_helper.ruact_js_assets(payload)).to be_html_safe
89
- end
90
- end
91
-
92
- context "when in prod (Vite manifest lookup)" do
93
- before do
94
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
95
- end
96
-
97
- it "looks up the manifest with the SAME id the generated vite.config input uses (AC3 — no drift)" do
98
- allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
99
- asset_helper.ruact_js_assets(payload)
100
- expect(asset_helper).to have_received(:vite_manifest_entry).with(Ruact.bootstrap_virtual_id)
101
- end
102
-
103
- it "emits the hashed bootstrap URL from the manifest entry", :aggregate_failures do
104
- allow(asset_helper).to receive(:vite_manifest_entry)
105
- .with(Ruact.bootstrap_virtual_id).and_return({ "file" => "bootstrap-abc123.js" })
106
- html = asset_helper.ruact_js_assets(payload)
107
- expect(html).to include(%(src="/assets/bootstrap-abc123.js"))
108
- expect(html).to include("__FLIGHT_DATA")
109
- end
110
-
111
- it "falls back to /assets/application.js when the manifest entry is missing" do
112
- allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
113
- expect(asset_helper.ruact_js_assets(payload)).to include("/assets/application.js")
114
- end
115
- end
116
-
117
- context "without a Flight payload (entry tags only)" do
118
- before do
119
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
120
- allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
121
- end
122
-
123
- it "emits only the entry tag (no __FLIGHT_DATA)", :aggregate_failures do
124
- html = asset_helper.ruact_js_assets
125
- expect(html).not_to include("__FLIGHT_DATA")
126
- expect(html).to include("<script type=\"module\"")
127
- end
128
- end
129
- end
130
- end
131
- end
data/spec/spec_helper.rb DELETED
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "simplecov"
4
- require "simplecov-lcov"
5
-
6
- SimpleCov::Formatter::LcovFormatter.config do |c|
7
- c.report_with_single_file = true
8
- c.single_report_path = "coverage/lcov.info"
9
- end
10
-
11
- SimpleCov.start do
12
- enable_coverage :branch
13
- primary_coverage :line
14
- formatter SimpleCov::Formatter::MultiFormatter.new(
15
- [
16
- SimpleCov::Formatter::HTMLFormatter,
17
- SimpleCov::Formatter::LcovFormatter
18
- ]
19
- )
20
- add_filter %r{^/spec/}
21
- add_filter %r{^/bin/}
22
- add_filter %r{lib/generators/.+/templates/}
23
- add_filter "lib/ruact/version.rb"
24
- end
25
-
26
- require "logger"
27
- require "ruact"
28
-
29
- Dir[File.join(__dir__, "support", "**", "*.rb")].each do |f|
30
- next if f.end_with?("_spec.rb") # spec files are loaded by the RSpec runner; avoid double registration
31
-
32
- require f
33
- end
34
-
35
- RSpec.configure do |config|
36
- config.order = :random
37
- config.expect_with :rspec do |expectations|
38
- expectations.max_formatted_output_length = 2000
39
- end
40
- config.mock_with :rspec do |mocks|
41
- mocks.verify_partial_doubles = true
42
- end
43
-
44
- # Story 7.3: Ruact.config is a frozen singleton with a boot-state flag for the
45
- # re-configuration warning. Reset both before every example so the boot flag
46
- # cannot leak between specs — otherwise the AC3 warning may fire into a
47
- # Rails.logger that has become a per-example RSpec double from an earlier
48
- # spec, producing "originally created in one example but has leaked" failures
49
- # under random order.
50
- config.before do
51
- Ruact.instance_variable_set(:@config, nil)
52
- Ruact.instance_variable_set(:@configured_at_least_once, false)
53
- # Story 9.3: specs that set `Rails.logger = instance_double(Logger)` (e.g.
54
- # railtie_spec, configuration_spec) leave a per-example double on the global
55
- # after they finish; rspec then refuses to call it from the NEXT example.
56
- # That was harmless until the codegen/rake paths began reading `Rails.logger`
57
- # (the `[ruact] codegen: exposing …` line). Reset it to a real, silent logger
58
- # before every example so no leaked double survives — examples that need a
59
- # specific logger set their own in their own `before` (which runs after this).
60
- Rails.logger = Logger.new(IO::NULL) if defined?(Rails) && Rails.respond_to?(:logger=)
61
-
62
- # Manifest-over-HTTP fix: the gem suite runs with `Rails.env == development`
63
- # by default, where `Ruact::ManifestResolver` would otherwise fetch the live
64
- # manifest from the Vite dev server (localhost:5173) on every ActionView
65
- # render — making the suite hit the network and go flaky on a dev machine
66
- # that happens to run a foreign Vite there. Pin the pre-fix behaviour (return
67
- # the boot-loaded `Ruact.manifest`) so the whole suite is hermetic. The
68
- # resolver's own spec re-stubs these `.and_call_original` to exercise the real
69
- # HTTP/fallback paths with `Net::HTTP` mocked. (The programmatic
70
- # `RenderPipeline#render({erb:})` path passes `registry: nil` and never
71
- # touches the resolver, so the benchmark measures the real render cost.)
72
- if defined?(Ruact::ManifestResolver)
73
- allow(Ruact::ManifestResolver).to receive(:resolve) { Ruact.manifest }
74
- allow(Ruact::ManifestResolver).to receive(:resolve_soft) { Ruact.manifest }
75
- end
76
- end
77
- end
Binary file
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Story 15.4 (FR108) — the pure Flight wire parser was PROMOTED onto the gem's
4
- # `lib/` load path as `Ruact::Testing::FlightWireParser` so the new public
5
- # `have_ruact_component` matcher and the gem's own internal matchers share ONE
6
- # implementation (wrap, not fork). This file is now a thin shim: it loads the
7
- # promoted implementation and re-exports it under the historical test-only
8
- # `Ruact::Spec` namespace so the existing internal matcher wrappers and their
9
- # self-tests keep working unchanged.
10
- require "ruact/testing/flight_wire_parser"
11
-
12
- module Ruact
13
- # Test-support utilities. Code under `Ruact::Spec` is consumed only by the
14
- # gem's own RSpec suite; it is not part of the public API and may change
15
- # shape across stories without a deprecation cycle. The parser/diff it names
16
- # are aliases for the shipped `Ruact::Testing::*` implementation.
17
- module Spec
18
- FlightWireParser = Ruact::Testing::FlightWireParser
19
- FlightWireParseError = Ruact::Testing::FlightWireParseError
20
- end
21
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
- require_relative "flight_wire_parser"
5
-
6
- module Ruact
7
- module Spec
8
- RSpec.describe FlightWireParser do
9
- describe ".parse" do
10
- it "parses a single model row" do
11
- rows = described_class.parse(%(0:{"className":"box"}\n))
12
-
13
- expect(rows).to eq([
14
- {
15
- id: 0,
16
- class: :model,
17
- payload: { "className" => "box" },
18
- raw: %(0:{"className":"box"}\n)
19
- }
20
- ])
21
- end
22
-
23
- it "parses a single import row and decodes the hex id" do
24
- rows = described_class.parse(%(a:I["/L.jsx","L",["/L.jsx"]]\n))
25
-
26
- expect(rows.length).to eq(1)
27
- expect(rows.first).to include(
28
- id: 10,
29
- class: :import,
30
- payload: ["/L.jsx", "L", ["/L.jsx"]]
31
- )
32
- end
33
-
34
- it "parses a single error row" do
35
- rows = described_class.parse(%(2:E{"message":"boom"}\n))
36
-
37
- expect(rows.first).to include(
38
- id: 2,
39
- class: :error,
40
- payload: { "message" => "boom" }
41
- )
42
- end
43
-
44
- it "parses a hint row with no id" do
45
- rows = described_class.parse(%(:HL"/preload.js"\n))
46
-
47
- expect(rows.first).to include(
48
- id: nil,
49
- class: :hint,
50
- payload: ["L", "/preload.js"]
51
- )
52
- end
53
-
54
- it "parses a T row of exactly LARGE_TEXT_THRESHOLD bytes followed by a model row that references it" do
55
- large_text = "a" * 1024
56
- wire = "1:T#{1024.to_s(16)},#{large_text}0:\"$T1\"\n"
57
-
58
- rows = described_class.parse(wire)
59
-
60
- expect(rows.length).to eq(2)
61
- expect(rows[0]).to include(id: 1, class: :text, payload: large_text)
62
- expect(rows[1]).to include(id: 0, class: :model, payload: "$T1")
63
- end
64
-
65
- it "parses a mixed-row sequence preserving wire order" do
66
- wire = %(1:I["/L.jsx","L",["/L.jsx"]]\n0:["$","$L1",null,{}]\n)
67
-
68
- rows = described_class.parse(wire)
69
-
70
- expect(rows.map { |r| [r[:id], r[:class]] }).to eq([[1, :import], [0, :model]])
71
- end
72
-
73
- it "parses empty input as an empty array" do
74
- expect(described_class.parse("")).to eq([])
75
- end
76
-
77
- it "raises FlightWireParseError naming the byte offset on malformed JSON" do
78
- wire = %(0:{not-json}\n)
79
-
80
- expect { described_class.parse(wire) }
81
- .to raise_error(FlightWireParseError, /cannot parse row at offset \d+/)
82
- end
83
-
84
- it "raises FlightWireParseError when a T row is truncated" do
85
- wire = "0:T#{10.to_s(16)},abc" # claims 16 bytes but provides 3
86
-
87
- expect { described_class.parse(wire) }
88
- .to raise_error(FlightWireParseError, /cannot parse row at offset 0: T row truncated/)
89
- end
90
- end
91
- end
92
- end
93
- end