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,671 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Story 7.9 — Bug 7.8-B regression spec.
|
|
4
|
-
#
|
|
5
|
-
# Exercises Ruact::Controller end-to-end through Rails 8's full controller
|
|
6
|
-
# lifecycle (ActionController::Base#dispatch → ActionView::Base allocation →
|
|
7
|
-
# template render → __ruact_component__ helper). The bug closed by this spec
|
|
8
|
-
# was invisible to unit-only controller_spec.rb because that file stubs
|
|
9
|
-
# render_to_string and never instantiates ActionView::Base.
|
|
10
|
-
#
|
|
11
|
-
# Rails request-cycle subsystems are loaded HERE (not in spec_helper.rb) so
|
|
12
|
-
# the rest of the suite continues to use spec/support/rails_stub.rb without
|
|
13
|
-
# paying the action_controller / action_view boot cost.
|
|
14
|
-
require "action_controller/railtie"
|
|
15
|
-
require "action_view/railtie"
|
|
16
|
-
|
|
17
|
-
require "spec_helper"
|
|
18
|
-
|
|
19
|
-
require "rack/test"
|
|
20
|
-
require "tmpdir"
|
|
21
|
-
require "fileutils"
|
|
22
|
-
require "pathname"
|
|
23
|
-
require "active_model"
|
|
24
|
-
|
|
25
|
-
# Ruact::Controller is normally loaded by the Railtie's `ruact.load_controller`
|
|
26
|
-
# initializer at app boot. This spec instantiates a Rails::Application but does
|
|
27
|
-
# not depend on initializer ordering, so we load the concern explicitly.
|
|
28
|
-
require "ruact/controller"
|
|
29
|
-
|
|
30
|
-
# Test-support helpers and demo controller — top-level so the Rails routes
|
|
31
|
-
# resolver finds them. Defined before the describe block so the controller
|
|
32
|
-
# class exists when routes are appended.
|
|
33
|
-
module ControllerRequestSpecSupport
|
|
34
|
-
class << self
|
|
35
|
-
attr_reader :manifest_path
|
|
36
|
-
|
|
37
|
-
def app_class
|
|
38
|
-
@app_class ||= build_app_class
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Story 10.0 — a writable on-disk app root so an implicit-`default_render`
|
|
42
|
-
# controller's conventional template (`Rails.root/app/views/<ctrl>/<action>`)
|
|
43
|
-
# exists. Unlike the `append_view_path` demo controllers (which call
|
|
44
|
-
# `ruact_render` directly), the implicit path is what `ruact_template_exists?`
|
|
45
|
-
# probes — the bug under test lives in `default_render`, never reached when a
|
|
46
|
-
# template is only on an appended path. Memoized so `config.root` and every
|
|
47
|
-
# `write_view` call share one directory.
|
|
48
|
-
def app_root
|
|
49
|
-
@app_root ||= Pathname.new(Dir.mktmpdir("ruact-story-10-0-root"))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Story 10.0 — write a conventional template under the app root so Rails'
|
|
53
|
-
# default view path picks it up and `ruact_template_exists?` returns true.
|
|
54
|
-
# Called at file-load time (before `initialize!`) so the `app/views` dir
|
|
55
|
-
# exists when Rails computes `paths["app/views"].existent`.
|
|
56
|
-
def write_view(controller_path, action, erb)
|
|
57
|
-
view_dir = File.join(app_root, "app", "views", controller_path)
|
|
58
|
-
FileUtils.mkdir_p(view_dir)
|
|
59
|
-
File.write(File.join(view_dir, "#{action}.html.erb"), erb)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def boot!
|
|
63
|
-
return if @booted
|
|
64
|
-
|
|
65
|
-
@tmpdir = Dir.mktmpdir("ruact-story-7-9")
|
|
66
|
-
@manifest_path = File.join(@tmpdir, "react-client-manifest.json")
|
|
67
|
-
File.write(@manifest_path, JSON.dump(
|
|
68
|
-
"DemoButton" => {
|
|
69
|
-
"id" => "/DemoButton.jsx",
|
|
70
|
-
"name" => "DemoButton",
|
|
71
|
-
"chunks" => ["/DemoButton.jsx"]
|
|
72
|
-
}
|
|
73
|
-
))
|
|
74
|
-
|
|
75
|
-
# Story 7.3: Ruact.config is frozen after the first configure block. The
|
|
76
|
-
# spec_helper before hook resets it between examples; the describe's
|
|
77
|
-
# per-example `before` re-primes the manifest_path. Here we just put the
|
|
78
|
-
# initial state in place and cache the manifest at module level so it
|
|
79
|
-
# survives subsequent config resets.
|
|
80
|
-
Ruact.instance_variable_set(:@config, nil)
|
|
81
|
-
Ruact.instance_variable_set(:@configured_at_least_once, false)
|
|
82
|
-
Ruact.configure do |c|
|
|
83
|
-
c.manifest_path = @manifest_path
|
|
84
|
-
end
|
|
85
|
-
Ruact.manifest # prime the cache
|
|
86
|
-
|
|
87
|
-
app_class.instance.initialize!
|
|
88
|
-
@booted = true
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
def build_app_class
|
|
94
|
-
app_root_path = app_root
|
|
95
|
-
Class.new(Rails::Application) do
|
|
96
|
-
# Story 10.0 — a real on-disk root so the implicit-render controller's
|
|
97
|
-
# conventional `app/views` template is discoverable by `default_render`.
|
|
98
|
-
config.root = app_root_path
|
|
99
|
-
config.eager_load = false
|
|
100
|
-
config.consider_all_requests_local = true
|
|
101
|
-
config.action_controller.perform_caching = false
|
|
102
|
-
config.action_dispatch.show_exceptions = :none
|
|
103
|
-
config.logger = Logger.new(IO::NULL)
|
|
104
|
-
config.active_support.deprecation = :silence
|
|
105
|
-
config.secret_key_base = "x" * 64
|
|
106
|
-
config.hosts.clear if config.respond_to?(:hosts)
|
|
107
|
-
|
|
108
|
-
routes.append do
|
|
109
|
-
get "/demo/show", to: "controller_request_spec_support/demo#show"
|
|
110
|
-
# Story 10.0 — implicit-`default_render` page action (empty body), backed
|
|
111
|
-
# by a conventional `Rails.root/app/views` template, for the non-HTML
|
|
112
|
-
# Accept graceful-degradation matrix.
|
|
113
|
-
get "/implicit-demo/show", to: "controller_request_spec_support/implicit_demo#show"
|
|
114
|
-
# Story 13.3 (FR98) — Bucket-1 redirect-back round-trip routes.
|
|
115
|
-
get "/errors-demo/new", to: "controller_request_spec_support/errors_demo#new"
|
|
116
|
-
post "/errors-demo/create", to: "controller_request_spec_support/errors_demo#create"
|
|
117
|
-
post "/errors-demo/create_valid", to: "controller_request_spec_support/errors_demo#create_valid"
|
|
118
|
-
# The layout owns the document — a page rendered through a migrated
|
|
119
|
-
# host layout, and one through a layout that never calls
|
|
120
|
-
# `ruact_js_assets`.
|
|
121
|
-
get "/layout-demo/show", to: "controller_request_spec_support/layout_demo#show"
|
|
122
|
-
get "/unwired-layout-demo/show", to: "controller_request_spec_support/unwired_layout_demo#show"
|
|
123
|
-
get "/exploding-layout-demo/show", to: "controller_request_spec_support/exploding_layout_demo#show"
|
|
124
|
-
get "/rootless-layout-demo/show", to: "controller_request_spec_support/rootless_layout_demo#show"
|
|
125
|
-
get "/ghost-layout-demo/show", to: "controller_request_spec_support/ghost_layout_demo#show"
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Story 13.3 (FR98) — an ActiveModel record with a presence validation for the
|
|
132
|
-
# redirect-back round-trip demo controller below.
|
|
133
|
-
class ErrorsDemoPost
|
|
134
|
-
include ActiveModel::Model
|
|
135
|
-
include ActiveModel::Attributes
|
|
136
|
-
|
|
137
|
-
attribute :title, :string
|
|
138
|
-
|
|
139
|
-
validates :title, presence: true
|
|
140
|
-
|
|
141
|
-
def self.name = "ErrorsDemoPost"
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Story 13.3 (FR98) — the Bucket-1 (native form / navigation) Inertia-style
|
|
145
|
-
# redirect-back demo: `create` registers errors via `ruact_errors` then
|
|
146
|
-
# `redirect_to`s the form; the errors survive the Flight redirect in `flash`
|
|
147
|
-
# and arrive as an `errors` prop on the re-rendered `new` page.
|
|
148
|
-
class ErrorsDemoController < ActionController::Base
|
|
149
|
-
include Ruact::Controller
|
|
150
|
-
|
|
151
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
152
|
-
|
|
153
|
-
def new
|
|
154
|
-
ruact_render
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def create
|
|
158
|
-
post = ErrorsDemoPost.new(title: nil)
|
|
159
|
-
post.valid? # false → populates errors
|
|
160
|
-
ruact_errors(post)
|
|
161
|
-
redirect_to "/errors-demo/new"
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def create_valid
|
|
165
|
-
post = ErrorsDemoPost.new(title: "Hi")
|
|
166
|
-
post.valid? # true → no errors
|
|
167
|
-
ruact_errors(post)
|
|
168
|
-
redirect_to "/errors-demo/new"
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Demo controller for the spec. Uses an inline `append_view_path` instead of
|
|
173
|
-
# relying on Rails.root/app/views, so Controller#ruact_render is invoked
|
|
174
|
-
# directly from #show rather than via default_render (which short-circuits
|
|
175
|
-
# when the conventional template path does not exist on disk).
|
|
176
|
-
class DemoController < ActionController::Base
|
|
177
|
-
include Ruact::Controller
|
|
178
|
-
|
|
179
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
180
|
-
|
|
181
|
-
def show
|
|
182
|
-
ruact_render
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Story 10.0 — an implicit-`default_render` page controller: an EMPTY GET
|
|
187
|
-
# action (no `render`/`respond_to`), backed by a CONVENTIONAL template at
|
|
188
|
-
# `Rails.root/app/views/.../show.html.erb` (written via `write_view` below).
|
|
189
|
-
# This is the exact scaffold shape (ivar-only GET) whose `*/*` request 500'd
|
|
190
|
-
# pre-10.0. No `append_view_path` — the template must live on the default path
|
|
191
|
-
# that `ruact_template_exists?` probes for the bug to be reachable.
|
|
192
|
-
class ImplicitDemoController < ActionController::Base
|
|
193
|
-
include Ruact::Controller
|
|
194
|
-
|
|
195
|
-
def show; end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# The layout owns the document. Both controllers declare a NAMED layout
|
|
199
|
-
# instead of relying on `layouts/application`, so adding them cannot change
|
|
200
|
-
# what every other controller in this file renders (the rest have no
|
|
201
|
-
# resolvable layout and must keep getting ruact's built-in shell).
|
|
202
|
-
class LayoutDemoController < ActionController::Base
|
|
203
|
-
include Ruact::Controller
|
|
204
|
-
|
|
205
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
206
|
-
layout "ruact_host"
|
|
207
|
-
|
|
208
|
-
def show
|
|
209
|
-
ruact_render
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# An unmigrated layout that RAISES if rendered (it reads an ivar a ruact
|
|
214
|
-
# action never sets). With the default config ruact must never execute it.
|
|
215
|
-
class ExplodingLayoutDemoController < ActionController::Base
|
|
216
|
-
include Ruact::Controller
|
|
217
|
-
|
|
218
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
219
|
-
layout "exploding_host"
|
|
220
|
-
|
|
221
|
-
def show
|
|
222
|
-
ruact_render
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Declares a layout that does not exist. `_default_layout` hands back the
|
|
227
|
-
# String path WITHOUT checking, so "not nil" read as resolvable and the render
|
|
228
|
-
# raised MissingTemplate instead of degrading.
|
|
229
|
-
class GhostLayoutDemoController < ActionController::Base
|
|
230
|
-
include Ruact::Controller
|
|
231
|
-
|
|
232
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
233
|
-
layout "no_such_host"
|
|
234
|
-
|
|
235
|
-
def show
|
|
236
|
-
ruact_render
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# A layout that calls `ruact_js_assets` but has no root div to mount into.
|
|
241
|
-
class RootlessLayoutDemoController < ActionController::Base
|
|
242
|
-
include Ruact::Controller
|
|
243
|
-
|
|
244
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
245
|
-
layout "rootless_host"
|
|
246
|
-
|
|
247
|
-
def show
|
|
248
|
-
ruact_render
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Same page, but through a layout that never calls `ruact_js_assets` — the
|
|
253
|
-
# state every app installed before the layout owned the document is in.
|
|
254
|
-
class UnwiredLayoutDemoController < ActionController::Base
|
|
255
|
-
include Ruact::Controller
|
|
256
|
-
|
|
257
|
-
append_view_path File.expand_path("../fixtures/story_7_9_views", __dir__)
|
|
258
|
-
layout "bare_host"
|
|
259
|
-
|
|
260
|
-
def show
|
|
261
|
-
ruact_render
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Story 10.0 — write the implicit-render template at file-load time (before the
|
|
267
|
-
# app's `initialize!` in `boot!`) so the `app/views` dir exists when Rails
|
|
268
|
-
# computes its view paths.
|
|
269
|
-
ControllerRequestSpecSupport.write_view(
|
|
270
|
-
"controller_request_spec_support/implicit_demo", "show", <<~ERB
|
|
271
|
-
<div>
|
|
272
|
-
<DemoButton label={"hello"} />
|
|
273
|
-
</div>
|
|
274
|
-
ERB
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
# Reset Rails.application so this spec can boot its own minimal app even if a
|
|
278
|
-
# prior spec ran a different Rails::Application subclass (the constant is
|
|
279
|
-
# memoized in Rails::Application; we own the reset here).
|
|
280
|
-
Rails.application = nil if Rails.respond_to?(:application) && !Rails.application.is_a?(Rails::Application)
|
|
281
|
-
|
|
282
|
-
module Ruact # rubocop:disable Style/OneClassPerFile
|
|
283
|
-
RSpec.describe "Story 7.9: Bug 7.8-B — render_to_string view-context ivar handoff", :story_7_9 do
|
|
284
|
-
include Rack::Test::Methods
|
|
285
|
-
|
|
286
|
-
let(:app_class) { ControllerRequestSpecSupport.app_class }
|
|
287
|
-
let(:app) { app_class.instance }
|
|
288
|
-
|
|
289
|
-
# Boot lives inside a per-example `before` hook (not `before(:all)`) so
|
|
290
|
-
# RSpec's per-test lifecycle for rspec-mocks is active when Rails
|
|
291
|
-
# initializers fire. Boot is memoized so the cost is paid once across all
|
|
292
|
-
# examples.
|
|
293
|
-
before do
|
|
294
|
-
# railtie_spec.rb assigns Rails.logger to an instance_double via the
|
|
295
|
-
# writer (not via rspec-mocks); the assignment persists across examples
|
|
296
|
-
# and the request-cycle code path calls Rails.logger.* — replace with a
|
|
297
|
-
# real logger that survives the example lifecycle.
|
|
298
|
-
Rails.logger = Logger.new(IO::NULL)
|
|
299
|
-
ControllerRequestSpecSupport.boot!
|
|
300
|
-
# Story 10.0 — pin Rails.root to the booted app root. The Rails.root stub
|
|
301
|
-
# (spec/support/rails_stub.rb) is a writable singleton ivar; doctor_spec
|
|
302
|
-
# sets `Rails.root = <its tmpdir>` and never clears it, so in full-suite
|
|
303
|
-
# ordering a leftover root would make `ruact_template_exists?` (which
|
|
304
|
-
# probes `Rails.root/app/views`) miss the implicit-render template → super
|
|
305
|
-
# → the very 500 this story closes. Re-pinning per example is order-proof.
|
|
306
|
-
Rails.root = ControllerRequestSpecSupport.app_root
|
|
307
|
-
# Re-prime Ruact.config after spec_helper's per-example reset wiped it.
|
|
308
|
-
Ruact.configure do |c|
|
|
309
|
-
c.manifest_path = ControllerRequestSpecSupport.manifest_path
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
describe "happy path: PascalCase tag renders successfully" do
|
|
314
|
-
it "GET /demo/show returns 200 with the Flight payload (was 500 pre-7.9)" do
|
|
315
|
-
get "/demo/show"
|
|
316
|
-
expect(last_response.status).to(eq(200),
|
|
317
|
-
"expected 200, got #{last_response.status} body=#{last_response.body[0, 400]}")
|
|
318
|
-
expect(last_response.body).to include("DemoButton")
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
it "RSC request returns text/x-component with the registered component" do
|
|
322
|
-
get "/demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
323
|
-
expect(last_response.status).to eq(200)
|
|
324
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
325
|
-
expect(last_response.body).to include("DemoButton")
|
|
326
|
-
end
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# The bug this closes: a ruact page could not carry ANY of the host app's
|
|
330
|
-
# CSS. `ruact_render` rendered the view with `layout: false` and then wrapped
|
|
331
|
-
# the payload in a hardcoded shell whose `<head>` has no stylesheet slot, so
|
|
332
|
-
# `stylesheet_link_tag` in the app's layout never reached the browser. The
|
|
333
|
-
# generated `--shadcn` scaffold was therefore unstyled by construction, and
|
|
334
|
-
# Epic 12 (`ruact_meta` → tags in `<head>`) had no surface to write into.
|
|
335
|
-
#
|
|
336
|
-
# The opt-in is EXPLICIT (`Ruact.config.layout`). ruact used to infer
|
|
337
|
-
# readiness from the layout itself; three review rounds each found another
|
|
338
|
-
# template shape that fooled the inference, so the question is no longer
|
|
339
|
-
# asked. These examples pin both halves: nothing happens without the config,
|
|
340
|
-
# and everything happens with it.
|
|
341
|
-
describe "the layout owns the document" do
|
|
342
|
-
def configure_layout(value)
|
|
343
|
-
Ruact.configure do |c|
|
|
344
|
-
c.manifest_path = ControllerRequestSpecSupport.manifest_path
|
|
345
|
-
c.layout = value
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
# The whole backward-compatibility story, and it is now structural rather
|
|
350
|
-
# than argued: with the default config ruact never looks at, resolves, or
|
|
351
|
-
# renders a layout at all.
|
|
352
|
-
context "with the default configuration (layout = false)" do
|
|
353
|
-
it "renders the built-in shell, exactly as before the layout path existed" do
|
|
354
|
-
get "/layout-demo/show"
|
|
355
|
-
|
|
356
|
-
expect(last_response.status).to eq(200)
|
|
357
|
-
expect(last_response.body).to include("Rails RSC")
|
|
358
|
-
expect(last_response.body).not_to include("host-app.css")
|
|
359
|
-
expect(last_response.body).to include("DemoButton")
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# This layout raises if it is rendered (it reads an ivar a ruact action
|
|
363
|
-
# never sets) — the shape of a real pre-migration app. Nothing may
|
|
364
|
-
# execute it.
|
|
365
|
-
it "never executes an unmigrated layout, so a working page cannot become a 500" do
|
|
366
|
-
get "/exploding-layout-demo/show"
|
|
367
|
-
|
|
368
|
-
expect(last_response.status).to(eq(200),
|
|
369
|
-
"expected the layout NOT to be rendered; got " \
|
|
370
|
-
"#{last_response.status} body=#{last_response.body[0, 300]}")
|
|
371
|
-
expect(last_response.body).to include("Rails RSC")
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
context "when the app opts in (layout = true)" do
|
|
376
|
-
before { configure_layout(true) }
|
|
377
|
-
|
|
378
|
-
it "puts the host app's stylesheet on a ruact page (the CSS could not arrive before)" do
|
|
379
|
-
get "/layout-demo/show"
|
|
380
|
-
|
|
381
|
-
expect(last_response.status).to(eq(200),
|
|
382
|
-
"expected 200, got #{last_response.status} " \
|
|
383
|
-
"body=#{last_response.body[0, 400]}")
|
|
384
|
-
expect(last_response.body).to include('<link rel="stylesheet" href="/host-app.css" />')
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
it "keeps the host layout's own <title> instead of ruact's placeholder" do
|
|
388
|
-
get "/layout-demo/show"
|
|
389
|
-
|
|
390
|
-
expect(last_response.body).to include("<title>Host App Title</title>")
|
|
391
|
-
expect(last_response.body).not_to include("Rails RSC")
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
it "still ships the Flight payload and the component, through the layout's ruact_js_assets" do
|
|
395
|
-
get "/layout-demo/show"
|
|
396
|
-
|
|
397
|
-
expect(last_response.body).to include("__FLIGHT_DATA")
|
|
398
|
-
expect(last_response.body).to include("DemoButton")
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
it "does not apply the layout to a Flight request (the wire shape is unchanged)" do
|
|
402
|
-
get "/layout-demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
403
|
-
|
|
404
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
405
|
-
expect(last_response.body).not_to include("host-app.css")
|
|
406
|
-
expect(last_response.body).to include("DemoButton")
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
# Opting in and pointing ruact at a layout that cannot mount the app is
|
|
410
|
-
# a configuration error, and silence would cost a blank page.
|
|
411
|
-
it "fails loudly when the opted-in layout never calls ruact_js_assets" do
|
|
412
|
-
expect { get "/unwired-layout-demo/show" }
|
|
413
|
-
.to raise_error(Ruact::Error, /ruact_js_assets/)
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
it "fails loudly when the opted-in layout has no root div to mount into" do
|
|
417
|
-
expect { get "/rootless-layout-demo/show" }
|
|
418
|
-
.to raise_error(Ruact::Error, /root/)
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
# A controller that declares a layout which does not exist: Rails'
|
|
422
|
-
# `_default_layout` returns the String path without checking it, so
|
|
423
|
-
# "resolvable" has to mean "the template is actually there".
|
|
424
|
-
it "degrades to the shell when a declared layout does not exist" do
|
|
425
|
-
get "/ghost-layout-demo/show"
|
|
426
|
-
|
|
427
|
-
expect(last_response.status).to(eq(200),
|
|
428
|
-
"expected a degrade, got #{last_response.status} " \
|
|
429
|
-
"body=#{last_response.body[0, 300]}")
|
|
430
|
-
expect(last_response.body).to include("Rails RSC")
|
|
431
|
-
expect(last_response.body).to include("DemoButton")
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
# `layout false` on one controller inside an app that opted in globally
|
|
435
|
-
# is a normal Rails pattern, not a mistake — it must degrade, not raise.
|
|
436
|
-
it "degrades to the shell for a controller with no resolvable layout" do
|
|
437
|
-
get "/demo/show"
|
|
438
|
-
|
|
439
|
-
expect(last_response.status).to eq(200)
|
|
440
|
-
expect(last_response.body).to include("Rails RSC")
|
|
441
|
-
expect(last_response.body).to include("DemoButton")
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
context "when a named layout is configured" do
|
|
446
|
-
before { configure_layout("ruact_host") }
|
|
447
|
-
|
|
448
|
-
it "renders every ruact page through that layout" do
|
|
449
|
-
get "/demo/show"
|
|
450
|
-
|
|
451
|
-
expect(last_response.body).to include("host-app.css")
|
|
452
|
-
expect(last_response.body).to include("DemoButton")
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
describe "regression guard: render context reaches ViewHelper" do
|
|
458
|
-
it "ViewHelper#__ruact_component__ does NOT raise the outside-flow error" do
|
|
459
|
-
# If the handoff regresses, the request returns 500 with this exact
|
|
460
|
-
# message in the body or raises the exception in the request chain.
|
|
461
|
-
get "/demo/show"
|
|
462
|
-
expect(last_response.body).not_to include("__ruact_component__ called outside a ruact_render flow")
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
it "Flight payload contains the registered component import row" do
|
|
466
|
-
# End-to-end probe: a successful render emits a Flight I-row (import)
|
|
467
|
-
# for every registered client component. The presence of the
|
|
468
|
-
# DemoButton's module path proves the handoff ran end-to-end rather
|
|
469
|
-
# than the request happening to 200 for some unrelated reason.
|
|
470
|
-
get "/demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
471
|
-
expect(last_response.status).to eq(200)
|
|
472
|
-
expect(last_response.body).to match(%r{\h+:I\[.*/DemoButton\.jsx}),
|
|
473
|
-
"expected a Flight I-row referencing /DemoButton.jsx; " \
|
|
474
|
-
"got: #{last_response.body[0, 400]}"
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
it "render context is cleared after the render returns" do
|
|
478
|
-
# AC-adjacent regression: the controller's ensure block must restore
|
|
479
|
-
# @ruact_render_context so a subsequent error/rescue render in the
|
|
480
|
-
# same request cannot see stale context. Re-issuing the same request
|
|
481
|
-
# on the same Rack stack exercises the per-request lifecycle.
|
|
482
|
-
get "/demo/show"
|
|
483
|
-
get "/demo/show"
|
|
484
|
-
expect(last_response.status).to eq(200)
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
it "ensure block removes the ivar entirely when it was not defined before" do
|
|
488
|
-
# Direct unit-level guard: when ruact_render starts on a controller
|
|
489
|
-
# instance with no prior @ruact_render_context, the ensure block
|
|
490
|
-
# must put the controller back into the instance_variable_defined?
|
|
491
|
-
# = false state — not into the "ivar is defined as nil" state,
|
|
492
|
-
# which would leak a phantom `{"ruact_render_context" => nil}`
|
|
493
|
-
# entry into view_assigns on any subsequent error render.
|
|
494
|
-
controller = ControllerRequestSpecSupport::DemoController.new
|
|
495
|
-
controller.instance_variable_set(:@_request, nil)
|
|
496
|
-
# Invoke just the ivar lifecycle by stubbing the heavy parts.
|
|
497
|
-
allow(controller).to receive_messages(
|
|
498
|
-
render_to_string: "<div></div>",
|
|
499
|
-
ruact_request?: false,
|
|
500
|
-
ruact_manifest: Ruact.manifest,
|
|
501
|
-
controller_path: "controller_request_spec_support/demo",
|
|
502
|
-
logger: Logger.new(IO::NULL),
|
|
503
|
-
action_name: "show"
|
|
504
|
-
)
|
|
505
|
-
allow(controller).to receive(:render)
|
|
506
|
-
|
|
507
|
-
expect(controller.instance_variable_defined?(:@ruact_render_context)).to be(false)
|
|
508
|
-
controller.send(:ruact_render)
|
|
509
|
-
expect(controller.instance_variable_defined?(:@ruact_render_context)).to be(false)
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
# Story 13.3 (FR98) — the Bucket-1 (native form / navigation) half of the
|
|
514
|
-
# Inertia-style validation `errors` round-trip (AC4): errors survive a
|
|
515
|
-
# redirect-back in flash and arrive as an `errors` prop on the re-render.
|
|
516
|
-
describe "Story 13.3: redirect-back errors prop (FR98)", :story_13_3 do
|
|
517
|
-
let(:flight_headers) { { "HTTP_ACCEPT" => "text/x-component" } }
|
|
518
|
-
|
|
519
|
-
it "exposes errors={} on a plain page render with no prior redirect (always present)" do
|
|
520
|
-
get "/errors-demo/new", {}, flight_headers
|
|
521
|
-
expect(last_response.status).to eq(200)
|
|
522
|
-
expect(last_response.body).to include("DemoButton")
|
|
523
|
-
# The component receives a present-but-empty errors prop, never a message.
|
|
524
|
-
expect(last_response.body).not_to include("can't be blank")
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
it "survives a redirect-back and re-renders with the errors prop populated" do
|
|
528
|
-
# Writing request (Bucket 1, Flight): registers errors, then redirects.
|
|
529
|
-
post "/errors-demo/create", {}, flight_headers
|
|
530
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
531
|
-
expect(last_response.body).to include("redirectUrl")
|
|
532
|
-
|
|
533
|
-
# The router follows the redirect with a fresh Flight GET; rack-test
|
|
534
|
-
# carries the session cookie, so flash[:ruact_errors] is read back.
|
|
535
|
-
get "/errors-demo/new", {}, flight_headers
|
|
536
|
-
expect(last_response.status).to eq(200)
|
|
537
|
-
expect(last_response.body).to include("Title can't be blank")
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
it "registers errors={} for a successful save and does NOT leak a message across the redirect" do
|
|
541
|
-
post "/errors-demo/create_valid", {}, flight_headers
|
|
542
|
-
get "/errors-demo/new", {}, flight_headers
|
|
543
|
-
expect(last_response.status).to eq(200)
|
|
544
|
-
expect(last_response.body).not_to include("can't be blank")
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
it "is single-use: a second re-render after the flash is swept shows no errors" do
|
|
548
|
-
post "/errors-demo/create", {}, flight_headers
|
|
549
|
-
get "/errors-demo/new", {}, flight_headers
|
|
550
|
-
expect(last_response.body).to include("Title can't be blank")
|
|
551
|
-
|
|
552
|
-
# flash is swept after the first read; the next render is clean.
|
|
553
|
-
get "/errors-demo/new", {}, flight_headers
|
|
554
|
-
expect(last_response.body).not_to include("Title can't be blank")
|
|
555
|
-
end
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
# Story 10.0 — implicit `default_render` must degrade gracefully on non-HTML
|
|
559
|
-
# Accept. Exercised against an ivar-only GET page action (empty body) whose
|
|
560
|
-
# conventional template exists, so the request traverses `default_render`'s
|
|
561
|
-
# activation predicate (NOT a hand-written `ruact_render` like demo#show).
|
|
562
|
-
describe "Story 10.0: default_render graceful degradation on non-HTML Accept", :story_10_0 do
|
|
563
|
-
it "GET with Accept: */* renders the HTML shell (was a 500 pre-10.0) (AC1)" do
|
|
564
|
-
# RED pre-fix: `*/*` → format.html? false, ruact_request? false → super →
|
|
565
|
-
# Rails renders the .html.erb outside a ruact_render flow → raises
|
|
566
|
-
# "__ruact_component__ called outside a ruact_render flow".
|
|
567
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "*/*" }
|
|
568
|
-
expect(last_response.status).to(eq(200),
|
|
569
|
-
"expected 200, got #{last_response.status} body=#{last_response.body[0, 400]}")
|
|
570
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
571
|
-
expect(last_response.body).to include("DemoButton")
|
|
572
|
-
expect(last_response.body).not_to include("__ruact_component__ called outside a ruact_render flow")
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
it "GET with an empty Accept header (defaults to HTML) renders the HTML shell (AC1)" do
|
|
576
|
-
# An empty Accept token parses to `[nil]` (not blank, not a concrete
|
|
577
|
-
# type) — it must degrade to the HTML shell, never raise NoMethodError.
|
|
578
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "" }
|
|
579
|
-
expect(last_response.status).to eq(200)
|
|
580
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
581
|
-
expect(last_response.body).to include("DemoButton")
|
|
582
|
-
end
|
|
583
|
-
|
|
584
|
-
it "GET with Accept: text/html renders the same HTML shell as */* (AC2)" do
|
|
585
|
-
# AC2: the explicit text/html path is unchanged and the */* path now
|
|
586
|
-
# yields the SAME shell. Normalize the per-request CSRF token (the only
|
|
587
|
-
# request-varying bytes) before comparing.
|
|
588
|
-
strip_csrf = ->(body) { body.gsub(/content="[^"]+"/, 'content="CSRF"') }
|
|
589
|
-
|
|
590
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "text/html" }
|
|
591
|
-
html_response = last_response.body
|
|
592
|
-
expect(last_response.status).to eq(200)
|
|
593
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
594
|
-
expect(html_response).to include("DemoButton")
|
|
595
|
-
|
|
596
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "*/*" }
|
|
597
|
-
expect(strip_csrf.call(last_response.body)).to eq(strip_csrf.call(html_response))
|
|
598
|
-
end
|
|
599
|
-
|
|
600
|
-
it "GET with Accept: text/x-component returns a raw Flight payload (AC3)" do
|
|
601
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "text/x-component" }
|
|
602
|
-
expect(last_response.status).to eq(200)
|
|
603
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
604
|
-
expect(last_response.body).to match(%r{\h+:I\[.*/DemoButton\.jsx})
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
it "GET with the Ruact-Request: 1 header returns a Flight payload (AC3)" do
|
|
608
|
-
get "/implicit-demo/show", {}, { "HTTP_RUACT_REQUEST" => "1" }
|
|
609
|
-
expect(last_response.status).to eq(200)
|
|
610
|
-
expect(last_response.headers["Content-Type"]).to include("text/x-component")
|
|
611
|
-
end
|
|
612
|
-
|
|
613
|
-
it "GET with concrete Accept: application/json bypasses RSC to super (AC4)" do
|
|
614
|
-
# The concrete non-HTML format carries no html/wildcard accept, so
|
|
615
|
-
# `default_render` must fall through to `super` (Rails default rendering).
|
|
616
|
-
# With no JSON template and no respond_to, Rails raises an UnknownFormat /
|
|
617
|
-
# MissingTemplate — NOT the ruact outside-flow 500. We only assert it is
|
|
618
|
-
# NOT the ruact bug; Rails' exact choice of error is its own concern.
|
|
619
|
-
error = nil
|
|
620
|
-
begin
|
|
621
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "application/json" }
|
|
622
|
-
rescue StandardError => e
|
|
623
|
-
error = e
|
|
624
|
-
end
|
|
625
|
-
expect(error).not_to be_nil
|
|
626
|
-
expect(error.message).not_to include("__ruact_component__ called outside a ruact_render flow")
|
|
627
|
-
expect(error).to be_a(ActionController::ActionControllerError).or(be_a(ActionView::MissingTemplate))
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
it "GET with two concrete non-HTML types (application/json, application/xml) bypasses to super (AC4)" do
|
|
631
|
-
# AC4's literal example: NEITHER member is html nor the wildcard, so the
|
|
632
|
-
# predicate is false and the request must fall through to `super`.
|
|
633
|
-
error = nil
|
|
634
|
-
begin
|
|
635
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "application/json, application/xml" }
|
|
636
|
-
rescue StandardError => e
|
|
637
|
-
error = e
|
|
638
|
-
end
|
|
639
|
-
expect(error).not_to be_nil
|
|
640
|
-
expect(error.message).not_to include("__ruact_component__ called outside a ruact_render flow")
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
it "GET with a mixed Accept that includes text/html (application/json, text/html) renders the shell" do
|
|
644
|
-
# HTML is acceptable (listed explicitly) → membership predicate activates
|
|
645
|
-
# the shell. default_render is only reached on an implicitly-rendered
|
|
646
|
-
# action (no JSON representation to prefer), so serving the accepted HTML
|
|
647
|
-
# is the sensible non-error outcome — and a 200, not the pre-10.0 500.
|
|
648
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "application/json, text/html" }
|
|
649
|
-
expect(last_response.status).to eq(200)
|
|
650
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
651
|
-
expect(last_response.body).to include("DemoButton")
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
it "GET with a concrete-then-wildcard Accept (application/json, */*) renders the shell" do
|
|
655
|
-
get "/implicit-demo/show", {}, { "HTTP_ACCEPT" => "application/json, */*" }
|
|
656
|
-
expect(last_response.status).to eq(200)
|
|
657
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
658
|
-
expect(last_response.body).to include("DemoButton")
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
it "GET with a real browser Accept (text/html,...,*/*;q=0.8) renders the shell (AC2-adjacent)" do
|
|
662
|
-
get "/implicit-demo/show", {}, {
|
|
663
|
-
"HTTP_ACCEPT" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
664
|
-
}
|
|
665
|
-
expect(last_response.status).to eq(200)
|
|
666
|
-
expect(last_response.headers["Content-Type"]).to include("text/html")
|
|
667
|
-
expect(last_response.body).to include("DemoButton")
|
|
668
|
-
end
|
|
669
|
-
end
|
|
670
|
-
end
|
|
671
|
-
end
|