ruact 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +14 -4
- data/RELEASING.md +125 -139
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/version.rb +1 -1
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- metadata +6 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../flight_wire_parser"
|
|
4
|
-
require "ruact/testing/flight_structure_diff"
|
|
5
|
-
|
|
6
|
-
module Ruact
|
|
7
|
-
module Spec
|
|
8
|
-
# Story 15.4 (FR108) — the structural diff engine was PROMOTED onto the
|
|
9
|
-
# gem's `lib/` load path as `Ruact::Testing::FlightStructureDiff` (shared,
|
|
10
|
-
# single implementation — wrap, not fork). This test-only `Ruact::Spec`
|
|
11
|
-
# alias keeps the internal matcher wrappers below and their self-tests
|
|
12
|
-
# working unchanged.
|
|
13
|
-
FlightStructureDiff = Ruact::Testing::FlightStructureDiff
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# `match_flight_fixture(name)` — Phase 1 byte-exact snapshot matcher.
|
|
18
|
-
#
|
|
19
|
-
# Reads `spec/fixtures/flight/<name>.txt` and compares the actual wire bytes
|
|
20
|
-
# to the file contents via `==`. Used when the wire format itself is the
|
|
21
|
-
# contract (escape rules, ordering invariants, payload framing).
|
|
22
|
-
#
|
|
23
|
-
# @example
|
|
24
|
-
# expect(serializer.serialize_value("$danger")).to match_flight_fixture("string_dollar_escape")
|
|
25
|
-
RSpec::Matchers.define :match_flight_fixture do |name|
|
|
26
|
-
match do |actual|
|
|
27
|
-
fixtures_dir = File.expand_path("../../fixtures/flight", __dir__)
|
|
28
|
-
fixture_path = File.join(fixtures_dir, "#{name}.txt")
|
|
29
|
-
@fixture_path = fixture_path
|
|
30
|
-
@expected = File.read(fixture_path)
|
|
31
|
-
actual == @expected
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
failure_message do |actual|
|
|
35
|
-
"Expected output to match fixture at #{@fixture_path}.\n\n" \
|
|
36
|
-
"Expected:\n#{@expected.inspect}\n\n" \
|
|
37
|
-
"Got:\n#{actual.inspect}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
failure_message_when_negated do |_actual|
|
|
41
|
-
"Expected output NOT to match fixture at #{@fixture_path}, but it did."
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
description do
|
|
45
|
-
"match Flight wire fixture '#{name}'"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# `match_flight_structure(expected)` — Phase 2 structural matcher (Story 7.5).
|
|
50
|
-
#
|
|
51
|
-
# Parses the actual Flight wire output via `Ruact::Spec::FlightWireParser`,
|
|
52
|
-
# then compares the resulting array of row records against `expected`. Hash
|
|
53
|
-
# payloads are compared structurally (key insertion order is ignored), so
|
|
54
|
-
# cosmetic JSON re-ordering does not break specs that assert on semantics.
|
|
55
|
-
# Import rows are matched as a multiset; non-import rows are compared
|
|
56
|
-
# positionally because Flight semantics depend on their order.
|
|
57
|
-
#
|
|
58
|
-
# Failure messages name the row index and the differing field — bytes are
|
|
59
|
-
# only printed when no narrower diff is available.
|
|
60
|
-
#
|
|
61
|
-
# @example
|
|
62
|
-
# expect(wire).to match_flight_structure([
|
|
63
|
-
# { id: 1, class: :import, payload: ["/L.jsx", "L", ["/L.jsx"]] },
|
|
64
|
-
# { id: 0, class: :model, payload: ["$", "$L1", nil, {}] }
|
|
65
|
-
# ])
|
|
66
|
-
RSpec::Matchers.define :match_flight_structure do |expected|
|
|
67
|
-
match do |actual|
|
|
68
|
-
@parsed = Ruact::Spec::FlightWireParser.parse(actual)
|
|
69
|
-
@expected_rows = expected
|
|
70
|
-
@diffs = Ruact::Spec::FlightStructureDiff.compute(@parsed, expected)
|
|
71
|
-
@diffs.empty?
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
failure_message do |_actual|
|
|
75
|
-
if @diffs.length == 1
|
|
76
|
-
Ruact::Spec::FlightStructureDiff.format_single(@diffs.first)
|
|
77
|
-
else
|
|
78
|
-
Ruact::Spec::FlightStructureDiff.format_multi(@diffs, @parsed, @expected_rows)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
failure_message_when_negated do |_actual|
|
|
83
|
-
"Expected output NOT to match the given Flight wire structure, but it did."
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
description do
|
|
87
|
-
"match Flight wire structure (#{expected.length} row(s))"
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# `include_flight_row(predicate)` — Phase 2 ordering-independent presence matcher (Story 7.5).
|
|
92
|
-
#
|
|
93
|
-
# Parses the actual Flight wire output, then asserts at least one parsed row
|
|
94
|
-
# satisfies the predicate hash. Subset semantics: only the keys present in
|
|
95
|
-
# `predicate` are compared. Predicate keys must be one of `:id`, `:class`,
|
|
96
|
-
# `:payload`, `:raw` — unknown keys raise upfront so typos don't silently
|
|
97
|
-
# match every row. Values may be plain Ruby values (compared with `==`) or
|
|
98
|
-
# RSpec argument matchers like `hash_including(...)` (compared with `===`).
|
|
99
|
-
# Negation (`not_to`) is supported.
|
|
100
|
-
#
|
|
101
|
-
# @example
|
|
102
|
-
# expect(wire).to include_flight_row(class: :model, payload: hash_including("postId" => 42))
|
|
103
|
-
RSpec::Matchers.define :include_flight_row do |predicate|
|
|
104
|
-
match do |actual|
|
|
105
|
-
Ruact::Spec::FlightStructureDiff.validate_predicate!(predicate)
|
|
106
|
-
@parsed = Ruact::Spec::FlightWireParser.parse(actual)
|
|
107
|
-
@predicate = predicate
|
|
108
|
-
@matched_idx = @parsed.find_index { |row| Ruact::Spec::FlightStructureDiff.row_matches?(row, predicate) }
|
|
109
|
-
!@matched_idx.nil?
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
failure_message do |_actual|
|
|
113
|
-
summary = @parsed.each_with_index.map do |row, i|
|
|
114
|
-
payload_snippet = row[:payload].inspect
|
|
115
|
-
payload_snippet = "#{payload_snippet[0, 80]}…" if payload_snippet.length > 80
|
|
116
|
-
" [#{i}] id=#{row[:id].inspect}, class=#{row[:class]}, payload=#{payload_snippet}"
|
|
117
|
-
end.join("\n")
|
|
118
|
-
summary = " (no rows parsed)" if @parsed.empty?
|
|
119
|
-
|
|
120
|
-
"Expected Flight output to include a row matching: #{predicate.inspect}.\nParsed rows:\n#{summary}"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
failure_message_when_negated do |_actual|
|
|
124
|
-
"Expected Flight output NOT to include a row matching: #{predicate.inspect}, but row #{@matched_idx} matched."
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
description do
|
|
128
|
-
"include a Flight row matching #{predicate.inspect}"
|
|
129
|
-
end
|
|
130
|
-
end
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
|
|
5
|
-
# Specs for the three Flight wire matcher modes added in Story 7.5
|
|
6
|
-
# (`match_flight_fixture`, `match_flight_structure`, `include_flight_row`).
|
|
7
|
-
RSpec.describe "Flight wire matchers" do
|
|
8
|
-
# Captures an `ExpectationNotMetError` raised by the inner block so the
|
|
9
|
-
# spec can make multiple assertions on its message without resorting to a
|
|
10
|
-
# multi-line block chained off `raise_error`.
|
|
11
|
-
def capture_failure
|
|
12
|
-
yield
|
|
13
|
-
nil
|
|
14
|
-
rescue RSpec::Expectations::ExpectationNotMetError => e
|
|
15
|
-
e
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
describe "match_flight_fixture (existing snapshot mode)" do
|
|
19
|
-
let(:nil_wire) { "0:null\n" }
|
|
20
|
-
|
|
21
|
-
it "passes against canonical fixture content (regression check)" do
|
|
22
|
-
expect(nil_wire).to match_flight_fixture("nil")
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe "match_flight_structure" do
|
|
27
|
-
let(:simple_wire) { %(0:{"className":"box"}\n) }
|
|
28
|
-
let(:two_row_wire) { %(1:I["/L.jsx","L",["/L.jsx"]]\n0:["$","$L1",null,{}]\n) }
|
|
29
|
-
|
|
30
|
-
it "passes when the actual wire matches a single-row expected structure" do
|
|
31
|
-
expect(simple_wire).to match_flight_structure([
|
|
32
|
-
{ id: 0, class: :model, payload: { "className" => "box" } }
|
|
33
|
-
])
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
it "passes for a two-row mixed import + model sequence" do
|
|
37
|
-
expect(two_row_wire).to match_flight_structure([
|
|
38
|
-
{ id: 1, class: :import, payload: ["/L.jsx", "L", ["/L.jsx"]] },
|
|
39
|
-
{ id: 0, class: :model, payload: ["$", "$L1", nil, {}] }
|
|
40
|
-
])
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
it "fails with a missing-row message when actual has fewer rows than expected" do
|
|
44
|
-
err = capture_failure do
|
|
45
|
-
expect(simple_wire).to match_flight_structure([
|
|
46
|
-
{ id: 0, class: :model, payload: { "className" => "box" } },
|
|
47
|
-
{ id: 1, class: :import, payload: [] }
|
|
48
|
-
])
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
expect(err.message).to include("Expected row 1 (import) was not produced.")
|
|
52
|
-
expect(err.message).to include("expected: {")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it "produces the AC3 verbatim row-indexed diff for a single-field semantic regression" do
|
|
56
|
-
broken_wire = %(0:["$X","div",null,{"className":"box","children":"hi"}]\n)
|
|
57
|
-
expected_payload = ["$", "div", nil, { "className" => "box", "children" => "hi" }]
|
|
58
|
-
got_payload = ["$X", "div", nil, { "className" => "box", "children" => "hi" }]
|
|
59
|
-
expected_structure = [{ id: 0, class: :model, payload: expected_payload }]
|
|
60
|
-
|
|
61
|
-
# Hash#inspect changed between Ruby 3.3 (`{"a"=>"b"}`) and Ruby 3.4
|
|
62
|
-
# (`{"a" => "b"}`). The AC3 contract is "values shown via .inspect" —
|
|
63
|
-
# so we render the expected message with the same .inspect the matcher
|
|
64
|
-
# uses at runtime, keeping the spec stable across the CI Ruby matrix.
|
|
65
|
-
expected_message = <<~MSG.strip
|
|
66
|
-
Expected Flight output to match structure.
|
|
67
|
-
|
|
68
|
-
Row 0 (model) differs at .payload[0]:
|
|
69
|
-
expected: "$"
|
|
70
|
-
got: "$X"
|
|
71
|
-
|
|
72
|
-
Row 0 (model) full diff:
|
|
73
|
-
expected: #{expected_payload.inspect}
|
|
74
|
-
got: #{got_payload.inspect}
|
|
75
|
-
MSG
|
|
76
|
-
|
|
77
|
-
expect do
|
|
78
|
-
expect(broken_wire).to match_flight_structure(expected_structure)
|
|
79
|
-
end.to raise_error(RSpec::Expectations::ExpectationNotMetError, expected_message)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "fails with an unexpected-row message when actual has extra rows" do
|
|
83
|
-
err = capture_failure do
|
|
84
|
-
expect(two_row_wire).to match_flight_structure([
|
|
85
|
-
{ id: 1, class: :import,
|
|
86
|
-
payload: ["/L.jsx", "L", ["/L.jsx"]] }
|
|
87
|
-
])
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
expect(err.message).to include("Got unexpected row 1 (model)")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
it "tolerates cosmetic JSON key reordering in payload hashes (AC4)" do
|
|
94
|
-
canonical = %(0:{"a":1,"b":2}\n)
|
|
95
|
-
perturbed = %(0:{"b":2,"a":1}\n)
|
|
96
|
-
expected_structure = [{ id: 0, class: :model, payload: { "a" => 1, "b" => 2 } }]
|
|
97
|
-
|
|
98
|
-
expect(canonical).to match_flight_structure(expected_structure)
|
|
99
|
-
expect(perturbed).to match_flight_structure(expected_structure)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
it "passes negation when the structure does not match" do
|
|
103
|
-
expect(simple_wire).not_to match_flight_structure([
|
|
104
|
-
{ id: 0, class: :model, payload: { "className" => "circle" } }
|
|
105
|
-
])
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# AC1: "multiple I rows are an unordered set". The expected list below
|
|
109
|
-
# reverses the import order vs the wire — the structural matcher must
|
|
110
|
-
# still consider this a match because import-row ordering is not
|
|
111
|
-
# protocol-significant within the import class.
|
|
112
|
-
it "treats import rows as an unordered set (AC1)" do
|
|
113
|
-
wire = %(1:I["/A.jsx","A",["/A.jsx"]]\n2:I["/B.jsx","B",["/B.jsx"]]\n0:["$","$L1",null,{}]\n)
|
|
114
|
-
|
|
115
|
-
expect(wire).to match_flight_structure([
|
|
116
|
-
{ id: 2, class: :import, payload: ["/B.jsx", "B", ["/B.jsx"]] },
|
|
117
|
-
{ id: 1, class: :import, payload: ["/A.jsx", "A", ["/A.jsx"]] },
|
|
118
|
-
{ id: 0, class: :model, payload: ["$", "$L1", nil, {}] }
|
|
119
|
-
])
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Defends against incomplete expected rows silently passing when actual
|
|
123
|
-
# payload happens to be nil (e.g. `{ id: 0, class: :model }` without
|
|
124
|
-
# `:payload` would otherwise satisfy any row whose payload is nil).
|
|
125
|
-
it "raises ArgumentError when an expected row is missing :payload" do
|
|
126
|
-
expect do
|
|
127
|
-
expect(simple_wire).to match_flight_structure([{ id: 0, class: :model }])
|
|
128
|
-
end.to raise_error(ArgumentError, /missing required keys.*:payload/)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
it "raises ArgumentError when an expected row is missing :class" do
|
|
132
|
-
expect do
|
|
133
|
-
expect(simple_wire).to match_flight_structure([{ id: 0, payload: {} }])
|
|
134
|
-
end.to raise_error(ArgumentError, /missing required keys.*:class/)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Multi-row failure message: the count, AC3 wording for missing/extra,
|
|
138
|
-
# plus a "✓" line for every matching row so the reader can confirm
|
|
139
|
-
# which rows passed (AC3 — "Other rows that match are summarized as
|
|
140
|
-
# `Row N (<class>): ✓`").
|
|
141
|
-
it "shows matching-row checkmarks alongside multi-row diffs" do
|
|
142
|
-
wire = %(1:I["/A.jsx","A",["/A.jsx"]]\n0:["$X","div",null,{}]\n)
|
|
143
|
-
|
|
144
|
-
err = capture_failure do
|
|
145
|
-
expect(wire).to match_flight_structure([
|
|
146
|
-
{ id: 1, class: :import, payload: ["/A.jsx", "A", ["/A.jsx"]] },
|
|
147
|
-
{ id: 0, class: :model, payload: ["$", "div", nil, {}] },
|
|
148
|
-
{ id: 2, class: :model, payload: ["$", "span", nil, {}] }
|
|
149
|
-
])
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
expect(err.message).to include("Expected Flight output to match structure. 2 rows differ:")
|
|
153
|
-
expect(err.message).to include("Row 0 (import): ✓")
|
|
154
|
-
expect(err.message).to include("Row 1 (model) differs at .payload[0]:")
|
|
155
|
-
expect(err.message).to include("Expected row 2 (model) was not produced.")
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
describe "include_flight_row" do
|
|
160
|
-
let(:wire_with_post_id) do
|
|
161
|
-
%(1:I["/L.jsx","L",["/L.jsx"]]\n0:["$","$L1",null,{"postId":42}]\n)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
it "matches when at least one row satisfies a hash_including payload predicate" do
|
|
165
|
-
expect(wire_with_post_id).to include_flight_row(
|
|
166
|
-
class: :model,
|
|
167
|
-
payload: include("postId" => 42)
|
|
168
|
-
)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
it "fails listing parsed rows when no row matches the predicate" do
|
|
172
|
-
err = capture_failure do
|
|
173
|
-
expect(wire_with_post_id).to include_flight_row(
|
|
174
|
-
class: :model,
|
|
175
|
-
payload: include("postId" => 999)
|
|
176
|
-
)
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
expect(err.message).to include("Expected Flight output to include a row matching")
|
|
180
|
-
expect(err.message).to include("[0] id=1, class=import")
|
|
181
|
-
expect(err.message).to include("[1] id=0, class=model")
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
it "supports negation with not_to" do
|
|
185
|
-
expect(wire_with_post_id).not_to include_flight_row(class: :error)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
it "fails negation when a row matches, naming the offending row index" do
|
|
189
|
-
expect do
|
|
190
|
-
expect(wire_with_post_id).not_to include_flight_row(class: :import)
|
|
191
|
-
end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /but row 0 matched/)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
it "supports array_including for the payload key" do
|
|
195
|
-
expect(wire_with_post_id).to include_flight_row(
|
|
196
|
-
class: :import,
|
|
197
|
-
payload: include("/L.jsx")
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# AC4 fixture-mode failure proof. The structural matcher tolerates the
|
|
202
|
-
# cosmetic perturbation; the fixture matcher fails *positively* with the
|
|
203
|
-
# expected-vs-got diff visible — confirming fixture mode is the wire-
|
|
204
|
-
# format contract guard. This spec verifies the failure message rather
|
|
205
|
-
# than relying on a `not_to` shortcut (which would prove only that the
|
|
206
|
-
# matcher returned false, not that the failure is loud and informative).
|
|
207
|
-
it "demonstrates cosmetic-vs-fixture asymmetry — structure tolerates re-ordering, fixture fails loudly (AC4)" do
|
|
208
|
-
canonical_wire = %(0:{"debug":true,"count":5,"label":"x"}\n)
|
|
209
|
-
perturbed_wire = %(0:{"label":"x","count":5,"debug":true}\n)
|
|
210
|
-
expected_structure = [
|
|
211
|
-
{ id: 0, class: :model, payload: { "debug" => true, "count" => 5, "label" => "x" } }
|
|
212
|
-
]
|
|
213
|
-
|
|
214
|
-
err = capture_failure do
|
|
215
|
-
expect(perturbed_wire).to match_flight_fixture("hash")
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
aggregate_failures do
|
|
219
|
-
# Structural mode: both pass — JSON key reordering is cosmetic.
|
|
220
|
-
expect(canonical_wire).to match_flight_structure(expected_structure)
|
|
221
|
-
expect(perturbed_wire).to match_flight_structure(expected_structure)
|
|
222
|
-
|
|
223
|
-
# Fixture mode: canonical passes — the fixture file is the canonical
|
|
224
|
-
# wire bytes.
|
|
225
|
-
expect(canonical_wire).to match_flight_fixture("hash")
|
|
226
|
-
|
|
227
|
-
# Fixture mode against the perturbed wire fails *loudly* with the
|
|
228
|
-
# bytes-for-bytes diff so a human reviewer can see the cosmetic drift.
|
|
229
|
-
expect(err).to be_a(RSpec::Expectations::ExpectationNotMetError)
|
|
230
|
-
expect(err.message).to include("Expected output to match fixture at", "hash.txt", "Expected:", "Got:")
|
|
231
|
-
expect(err.message).to include(perturbed_wire.inspect)
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Predicate validation: an unknown key (typo) must raise immediately.
|
|
236
|
-
# Otherwise `row[:payloed]` returns nil and `nil == nil` would silently
|
|
237
|
-
# match every row, hiding broken specs.
|
|
238
|
-
it "raises ArgumentError when the predicate has an unknown key" do
|
|
239
|
-
expect do
|
|
240
|
-
expect(wire_with_post_id).to include_flight_row(payloed: { "postId" => 42 })
|
|
241
|
-
end.to raise_error(ArgumentError, /unknown keys.*:payloed/)
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
it "raises ArgumentError when given an empty predicate" do
|
|
245
|
-
expect do
|
|
246
|
-
expect(wire_with_post_id).to include_flight_row({})
|
|
247
|
-
end.to raise_error(ArgumentError, /predicate cannot be empty/)
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
end
|
data/spec/support/rails_stub.rb
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Spec-only Rails bootstrap for tests that need a `Rails` constant. Two modes:
|
|
4
|
-
#
|
|
5
|
-
# 1. **Augment** (default since Story 7.9): when the `rails` gem is in the
|
|
6
|
-
# bundle, load its core (`rails.rb` — just `Rails::VERSION`,
|
|
7
|
-
# `Rails::Railtie`, `ActiveSupport::StringInquirer`, etc., *not*
|
|
8
|
-
# `action_controller` / `action_view`) and add test-only writers
|
|
9
|
-
# (`Rails.root=`, `Rails.env=`, `Rails.logger=`) on top. Specs that need
|
|
10
|
-
# the full Rails request cycle (e.g. controller_request_spec.rb) require
|
|
11
|
-
# `action_controller/railtie` and `action_view/railtie` themselves — the
|
|
12
|
-
# rest of the suite never pays that cost.
|
|
13
|
-
#
|
|
14
|
-
# 2. **Full stub** (fallback): when `rails` is not in the bundle (e.g. a
|
|
15
|
-
# matrix run that pruned it), provide a minimal `Rails` module + a
|
|
16
|
-
# `Rails::Railtie` class with no-op class methods so `gem/lib/ruact/railtie.rb`
|
|
17
|
-
# can `class Railtie < Rails::Railtie` without crashing. `$LOADED_FEATURES`
|
|
18
|
-
# is patched so `require "rails"` inside loaded files no-ops.
|
|
19
|
-
#
|
|
20
|
-
# Loaded automatically by spec_helper.
|
|
21
|
-
#
|
|
22
|
-
# Augmentation is idempotent (each `Rails.define_singleton_method` is guarded
|
|
23
|
-
# by `unless Rails.singleton_class.method_defined?(...)`), so we run through
|
|
24
|
-
# this file every time it loads — even when Rails was already required by
|
|
25
|
-
# another spec file (e.g. controller_request_spec.rb pre-loads
|
|
26
|
-
# `action_controller/railtie`). Skipping with `return if defined?(Rails)`
|
|
27
|
-
# would leave doctor_spec / railtie_spec without the test-only writers when
|
|
28
|
-
# the request spec runs first.
|
|
29
|
-
|
|
30
|
-
unless defined?(Rails)
|
|
31
|
-
begin
|
|
32
|
-
# Loads Rails core only — not the request-cycle subsystem. Cheap.
|
|
33
|
-
require "rails"
|
|
34
|
-
rescue LoadError
|
|
35
|
-
# Rails not available; fall through to the full stub below.
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
if defined?(Rails) && Rails.respond_to?(:application)
|
|
40
|
-
# Augment-mode: real Rails is present. Add test-only writers that the
|
|
41
|
-
# existing suite (doctor_spec, railtie_spec) relies on, without clobbering
|
|
42
|
-
# real Rails's behaviour outside the test override.
|
|
43
|
-
unless Rails.singleton_class.method_defined?(:root=)
|
|
44
|
-
original_root = Rails.method(:root) if Rails.respond_to?(:root)
|
|
45
|
-
|
|
46
|
-
Rails.define_singleton_method(:root=) { |v| @_test_root = v }
|
|
47
|
-
Rails.define_singleton_method(:root) do
|
|
48
|
-
return @_test_root if defined?(@_test_root) && @_test_root
|
|
49
|
-
|
|
50
|
-
original_root&.call
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
unless Rails.singleton_class.method_defined?(:env=)
|
|
55
|
-
Rails.define_singleton_method(:env=) { |v| @_test_env = v }
|
|
56
|
-
original_env = Rails.method(:env) if Rails.respond_to?(:env)
|
|
57
|
-
Rails.define_singleton_method(:env) do
|
|
58
|
-
return @_test_env if defined?(@_test_env) && @_test_env
|
|
59
|
-
|
|
60
|
-
original_env ? original_env.call : ActiveSupport::StringInquirer.new("test")
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
unless Rails.singleton_class.method_defined?(:logger=)
|
|
65
|
-
Rails.define_singleton_method(:logger=) { |v| @_test_logger = v }
|
|
66
|
-
original_logger = Rails.method(:logger) if Rails.respond_to?(:logger)
|
|
67
|
-
Rails.define_singleton_method(:logger) do
|
|
68
|
-
return @_test_logger if defined?(@_test_logger) && @_test_logger
|
|
69
|
-
|
|
70
|
-
original_logger&.call
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Full-stub fallback: rails gem is not in the bundle.
|
|
78
|
-
$LOADED_FEATURES << "rails.rb"
|
|
79
|
-
|
|
80
|
-
module Rails
|
|
81
|
-
class Railtie
|
|
82
|
-
def self.initializer(*, **); end
|
|
83
|
-
def self.rake_tasks(&); end
|
|
84
|
-
|
|
85
|
-
def self.config
|
|
86
|
-
@config ||= Class.new do
|
|
87
|
-
def method_missing(name, *, **, &); end
|
|
88
|
-
|
|
89
|
-
def respond_to_missing?(*, **)
|
|
90
|
-
true
|
|
91
|
-
end
|
|
92
|
-
end.new
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
class << self
|
|
97
|
-
attr_accessor :env, :logger, :root
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
module ActiveSupport # rubocop:disable Style/OneClassPerFile
|
|
102
|
-
class StringInquirer < String
|
|
103
|
-
def method_missing(method_name, *args)
|
|
104
|
-
if method_name.to_s.end_with?("?")
|
|
105
|
-
self == method_name.to_s.chomp("?")
|
|
106
|
-
else
|
|
107
|
-
super
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def respond_to_missing?(*, **)
|
|
112
|
-
true
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|