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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +123 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +16 -6
- 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 -1
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -92
- 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,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
|