admin_suite 0.3.1 → 0.5.0
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 +210 -1
- data/app/assets/vendor/chart.umd.min.js +14 -0
- data/app/assets/vendor/easymde.min.css +7 -0
- data/app/assets/vendor/easymde.min.js +7 -0
- data/app/controllers/admin_suite/application_controller.rb +26 -47
- data/app/controllers/admin_suite/portals_controller.rb +2 -2
- data/app/controllers/admin_suite/resources_controller.rb +138 -6
- data/app/helpers/admin_suite/base_helper.rb +232 -391
- data/app/javascript/admin_suite_application.js +3 -0
- data/app/javascript/controllers/admin_suite/chart_controller.js +173 -0
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
- data/app/views/admin_suite/panels/_chart.html.erb +158 -18
- data/app/views/admin_suite/panels/_stat.html.erb +10 -0
- data/app/views/admin_suite/resources/index.html.erb +31 -82
- data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +15 -9
- data/app/views/layouts/admin_suite/application.html.erb +11 -3
- data/config/routes.rb +3 -0
- data/lib/admin/base/action_executor.rb +19 -51
- data/lib/admin/base/filter_builder.rb +48 -5
- data/lib/admin/base/resource.rb +92 -17
- data/lib/admin_suite/configuration.rb +32 -3
- data/lib/admin_suite/definition_loader.rb +194 -0
- data/lib/admin_suite/deprecation.rb +48 -0
- data/lib/admin_suite/engine.rb +55 -76
- data/lib/admin_suite/host_autoload_policy.rb +132 -0
- data/lib/admin_suite/legacy_custom_renderer_procs.rb +29 -0
- data/lib/admin_suite/portal_definition.rb +11 -0
- data/lib/admin_suite/renderer.rb +133 -0
- data/lib/admin_suite/renderer_registry.rb +81 -0
- data/lib/admin_suite/renderers/code_renderer.rb +15 -0
- data/lib/admin_suite/renderers/json_renderer.rb +15 -0
- data/lib/admin_suite/renderers/key_value_renderer.rb +39 -0
- data/lib/admin_suite/renderers/legacy_gleania.rb +232 -0
- data/lib/admin_suite/renderers/table_from_renderer.rb +22 -0
- data/lib/admin_suite/section_definition.rb +42 -0
- data/lib/admin_suite/ui/dashboard_definition.rb +6 -0
- data/lib/admin_suite/ui/field_renderer_registry.rb +31 -4
- data/lib/admin_suite/ui/form_field_renderer.rb +1 -7
- data/lib/admin_suite/ui/show_formatter_registry.rb +9 -0
- data/lib/admin_suite/ui/show_value_formatter.rb +7 -3
- data/lib/admin_suite/version.rb +1 -1
- data/lib/admin_suite.rb +48 -0
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +0 -4
- data/test/controllers/resources_controller_test.rb +76 -1
- data/test/integration/association_linking_test.rb +292 -0
- data/test/integration/chart_panel_test.rb +491 -0
- data/test/integration/dashboard_test.rb +9 -2
- data/test/integration/index_table_test.rb +289 -0
- data/test/integration/layout_assets_test.rb +112 -0
- data/test/integration/navigation_sections_test.rb +62 -0
- data/test/integration/pagination_and_stats_test.rb +152 -0
- data/test/integration/searchable_select_search_test.rb +368 -0
- data/test/integration/show_hide_blank_test.rb +195 -0
- data/test/integration/toggle_test.rb +106 -0
- data/test/lib/action_executor_redirect_test.rb +41 -0
- data/test/lib/builtin_renderers_test.rb +202 -0
- data/test/lib/definition_loader_test.rb +276 -0
- data/test/lib/engine_defaults_test.rb +39 -0
- data/test/lib/form_field_renderer_test.rb +136 -0
- data/test/lib/format_table_cell_test.rb +49 -0
- data/test/lib/index_includes_test.rb +223 -0
- data/test/lib/legacy_renderer_deprecation_test.rb +42 -0
- data/test/lib/renderer_test.rb +237 -0
- data/test/lib/resource_exportable_deprecation_test.rb +47 -0
- data/test/lib/show_value_formatter_test.rb +88 -0
- data/test/lib/zeitwerk_integration_test.rb +28 -64
- data/test/test_helper.rb +121 -5
- metadata +62 -5
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class ActionExecutorRedirectTest < ActiveSupport::TestCase
|
|
7
|
+
Action = Struct.new(:name, :label, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
test "AASM is not referenced as a bare constant" do
|
|
10
|
+
source = File.read(File.expand_path("../../lib/admin/base/action_executor.rb", __dir__))
|
|
11
|
+
refute_match(/rescue AASM::InvalidTransition/, source)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test "redirect target is derived for any action returning a persisted record" do
|
|
15
|
+
executor = Admin::Base::ActionExecutor.new(RedirectFixtures::WidgetResource, :clone_widget, nil)
|
|
16
|
+
url = executor.send(:redirect_url_for_action, Action.new(name: :clone_widget, label: "Clone"), RedirectFixtures::Widget.new)
|
|
17
|
+
assert_equal "/internal/admin_suite/ops/redirect_widgets/42", url
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
test "no redirect for results that are not persisted records" do
|
|
21
|
+
executor = Admin::Base::ActionExecutor.new(RedirectFixtures::WidgetResource, :ping, nil)
|
|
22
|
+
assert_nil executor.send(:redirect_url_for_action, Action.new(name: :ping, label: "Ping"), true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
test "a plain RuntimeError from a model action surfaces its real message, not a NameError" do
|
|
26
|
+
executor = Admin::Base::ActionExecutor.new(Admin::Resources::ExceptionHandlingBoomerResource, :kaboom, nil)
|
|
27
|
+
result = executor.execute_member(ExceptionHandlingFixtures::Boomer.new)
|
|
28
|
+
|
|
29
|
+
assert result.failure?
|
|
30
|
+
assert_equal "Error: actual failure message", result.message
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test "an exception whose class name contains InvalidTransition is reported as a friendly failure" do
|
|
34
|
+
executor = Admin::Base::ActionExecutor.new(Admin::Resources::ExceptionHandlingStateMachineResource, :transition, nil)
|
|
35
|
+
result = executor.execute_member(ExceptionHandlingFixtures::StateMachineWidget.new)
|
|
36
|
+
|
|
37
|
+
assert result.failure?
|
|
38
|
+
assert_equal "Invalid state transition: cannot transition from draft to published", result.message
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class BuiltinRenderersTest < ActionView::TestCase
|
|
7
|
+
include ::ERB::Util
|
|
8
|
+
include AdminSuite::BaseHelper
|
|
9
|
+
|
|
10
|
+
Record = Struct.new(:id, :config, :rows, :snippet) do
|
|
11
|
+
def attributes = { "id" => id }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def record
|
|
15
|
+
Record.new(
|
|
16
|
+
3,
|
|
17
|
+
{ "mode" => "fast" },
|
|
18
|
+
[ { name: "alpha", count: 2 }, { name: "beta", count: 5 } ],
|
|
19
|
+
"puts :hi"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build(key, **options)
|
|
24
|
+
# The four built-ins (and their aliases) are gem defaults now (Finding
|
|
25
|
+
# 2 of the whole-branch review: `register_default`, not `register`,
|
|
26
|
+
# so a host's own explicit registration or renderer class can beat
|
|
27
|
+
# them) -- fall back to the default store the same way
|
|
28
|
+
# `render_custom_section` does.
|
|
29
|
+
klass = AdminSuite::RendererRegistry.lookup(key) || AdminSuite::RendererRegistry.lookup_default(key)
|
|
30
|
+
klass.new(record, self, options).render
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A minimal double for the shape TableFromRenderer/KeyValueRenderer must
|
|
34
|
+
# keep supporting: something relation-like — not an Array, not a Hash,
|
|
35
|
+
# but Enumerable/#to_a-able, the way a real ActiveRecord::Relation or
|
|
36
|
+
# CollectionProxy is. Deliberately not ActiveRecord itself: the dummy
|
|
37
|
+
# app is database-free (see test/test_helper.rb).
|
|
38
|
+
class FakeRelation
|
|
39
|
+
include Enumerable
|
|
40
|
+
|
|
41
|
+
def initialize(records) = @records = records
|
|
42
|
+
def each(&block) = @records.each(&block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
test "json renderer prints the source hash" do
|
|
46
|
+
assert_includes build(:json, source: ->(r) { r.config }), "mode"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
test "json renderer defaults to record attributes" do
|
|
50
|
+
# json_block HTML-escapes the pretty-printed JSON (see
|
|
51
|
+
# RendererTest#"json_block renders pretty-printed JSON..." for the
|
|
52
|
+
# same " pattern), so a literal `"id"` never appears verbatim.
|
|
53
|
+
assert_includes build(:json), ""id""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
test "key_value renderer renders each pair" do
|
|
57
|
+
# KeyValueRenderer humanizes keys (`k.to_s.humanize`), so "mode"
|
|
58
|
+
# renders as "Mode".
|
|
59
|
+
html = build(:key_value, source: ->(r) { r.config })
|
|
60
|
+
assert_includes html, "Mode"
|
|
61
|
+
assert_includes html, "fast"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test "table_from renderer renders rows and infers columns" do
|
|
65
|
+
html = build(:table_from, source: ->(r) { r.rows })
|
|
66
|
+
assert_includes html, "alpha"
|
|
67
|
+
assert_includes html, "beta"
|
|
68
|
+
assert_includes html, "Count"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
test "table_from renderer honours explicit columns and empty message" do
|
|
72
|
+
html = build(:table_from, source: ->(_r) { [] }, columns: %i[name], empty: "no data")
|
|
73
|
+
assert_includes html, "no data"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Regression: Rails deserializes JSONB columns to String-keyed Hashes —
|
|
77
|
+
# exactly what trust_growth's organic_results/metadata/SERP arrays are.
|
|
78
|
+
# TableFromRenderer infers Symbol columns from `rows.first.keys.to_sym`,
|
|
79
|
+
# but `data_table`'s cell lookup (`row[c]`) previously looked those
|
|
80
|
+
# Symbols up against the original String-keyed rows and silently found
|
|
81
|
+
# nothing — headers rendered, every <td> came back blank, and
|
|
82
|
+
# `rows.blank?` never caught it because the rows themselves were present.
|
|
83
|
+
test "table_from renderer renders cell values (not just headers) for String-keyed rows" do
|
|
84
|
+
html = build(:table_from, source: ->(_r) { [ { "name" => "alpha", "count" => 2 } ] })
|
|
85
|
+
|
|
86
|
+
assert_includes html, "Name"
|
|
87
|
+
assert_includes html, "Count"
|
|
88
|
+
# The values, not just the headers, must survive the String/Symbol
|
|
89
|
+
# key mismatch — this is the assertion whose absence let the bug
|
|
90
|
+
# through originally.
|
|
91
|
+
assert_includes html, "alpha"
|
|
92
|
+
assert_includes html, "2"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
test "table_from renderer raises a clear error for a non-Array source" do
|
|
96
|
+
error = assert_raises(ArgumentError) { build(:table_from, source: ->(_r) { { name: "alpha" } }) }
|
|
97
|
+
assert_match(/table_from expects an Array of Hashes, got Hash/, error.message)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Regression: the Array-only guard above must not narrow what worked
|
|
101
|
+
# before it existed. `Array(source_value([]))` used to coerce any
|
|
102
|
+
# #to_a-able Enumerable — most importantly an ActiveRecord relation,
|
|
103
|
+
# the single most likely `source:` shape in the real migration — into a
|
|
104
|
+
# real Array. Only a bare Hash is the actual contract violation.
|
|
105
|
+
test "table_from renderer accepts a non-Array Enumerable source (e.g. an AR relation)" do
|
|
106
|
+
relation = FakeRelation.new([ { name: "alpha", count: 2 } ])
|
|
107
|
+
html = build(:table_from, source: ->(_r) { relation })
|
|
108
|
+
|
|
109
|
+
assert_includes html, "alpha"
|
|
110
|
+
assert_includes html, "2"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
test "code renderer renders the snippet" do
|
|
114
|
+
assert_includes build(:code, source: ->(r) { r.snippet }, language: :ruby), "puts"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
test "a symbol source resolves a method on the record" do
|
|
118
|
+
assert_includes build(:json, source: :config), "mode"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test "json_preview and code_preview are registered aliases to the same classes" do
|
|
122
|
+
assert_equal AdminSuite::Renderers::JsonRenderer, AdminSuite::RendererRegistry.lookup_default(:json_preview)
|
|
123
|
+
assert_equal AdminSuite::Renderers::CodeRenderer, AdminSuite::RendererRegistry.lookup_default(:code_preview)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
test "key_value renderer shows an empty state for a blank source" do
|
|
127
|
+
html = build(:key_value, source: ->(_r) { {} })
|
|
128
|
+
assert_includes html, "Nothing to display."
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test "key_value renderer raises a clear error for a flat non-pair Array" do
|
|
132
|
+
error = assert_raises(ArgumentError) { build(:key_value, source: ->(_r) { [ 1, 2, 3 ] }) }
|
|
133
|
+
assert_match(/key_value expects a Hash or an Array of \[key, value\] pairs/, error.message)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Same narrowing risk as table_from's Enumerable regression test above:
|
|
137
|
+
# a non-Array Enumerable of pairs must still coerce, not raise.
|
|
138
|
+
test "key_value renderer accepts a non-Array Enumerable of pairs" do
|
|
139
|
+
relation = FakeRelation.new([ [ "mode", "fast" ] ])
|
|
140
|
+
html = build(:key_value, source: ->(_r) { relation })
|
|
141
|
+
|
|
142
|
+
assert_includes html, "Mode"
|
|
143
|
+
assert_includes html, "fast"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# data_table is a public primitive hosts may call directly from their own
|
|
147
|
+
# Renderer subclasses (not just through TableFromRenderer), so the
|
|
148
|
+
# String-keyed-rows fix belongs on the primitive itself.
|
|
149
|
+
test "data_table renders cell values for String-keyed rows" do
|
|
150
|
+
html = AdminSuite::Renderer.new(record, self).send(
|
|
151
|
+
:data_table, [ { "name" => "alpha", "count" => 2 } ], columns: %i[name count]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert_includes html, "alpha"
|
|
155
|
+
assert_includes html, "2"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
test "table_from renderer's :columns option reaches the renderer through render_custom_section" do
|
|
159
|
+
section = Admin::Base::Resource::ShowSectionDefinition.new(
|
|
160
|
+
name: :costs,
|
|
161
|
+
render: :table_from,
|
|
162
|
+
options: { source: ->(r) { r.rows }, columns: %i[name] }
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
html = render_custom_section(record, section.render, section.options)
|
|
166
|
+
|
|
167
|
+
assert_includes html, "alpha"
|
|
168
|
+
refute_includes html, "Count"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Full DSL path: a host resource's `show { panel ... }` block, through
|
|
172
|
+
# ShowConfig#build_section, to render_show_section, to render_custom_section,
|
|
173
|
+
# to the registered renderer. No prior test in this suite exercised
|
|
174
|
+
# build_section at all, so this pins the plumbing this task added:
|
|
175
|
+
# ShowSectionDefinition#options is populated from the panel's leftover
|
|
176
|
+
# DSL keys (source:, columns:, empty:), including `columns:` a second
|
|
177
|
+
# time even though it also has its own dedicated `section.columns` slot.
|
|
178
|
+
class DslProbeResource < Admin::Base::Resource
|
|
179
|
+
show do
|
|
180
|
+
panel :costs, title: "Provider Costs",
|
|
181
|
+
render: :table_from,
|
|
182
|
+
source: ->(r) { r.rows },
|
|
183
|
+
columns: %i[name],
|
|
184
|
+
empty: "No spend"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
test "panel DSL options reach the renderer end-to-end" do
|
|
189
|
+
section = DslProbeResource.show_config.sections_list.find { |s| s.name == :costs }
|
|
190
|
+
|
|
191
|
+
assert_equal :table_from, section.render
|
|
192
|
+
assert_equal %i[name], section.columns
|
|
193
|
+
assert_equal({ source: section.options[:source], columns: %i[name], empty: "No spend" }, section.options)
|
|
194
|
+
|
|
195
|
+
html = render_show_section(record, section, :main)
|
|
196
|
+
|
|
197
|
+
assert_includes html, "alpha"
|
|
198
|
+
assert_includes html, "beta"
|
|
199
|
+
refute_includes html, "Count"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class DefinitionLoaderTest < ActiveSupport::TestCase
|
|
7
|
+
def with_globs(kind, paths)
|
|
8
|
+
key = { resources: :resource_globs, portals: :portal_globs,
|
|
9
|
+
dashboards: :dashboard_globs, actions: :action_globs }.fetch(kind)
|
|
10
|
+
saved = AdminSuite.config.public_send(key)
|
|
11
|
+
AdminSuite.config.public_send("#{key}=", paths)
|
|
12
|
+
restore_registry = snapshot_registry(kind)
|
|
13
|
+
AdminSuite::DefinitionLoader.reset!(kind)
|
|
14
|
+
yield
|
|
15
|
+
ensure
|
|
16
|
+
AdminSuite.config.public_send("#{key}=", saved)
|
|
17
|
+
restore_registry.call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# `DefinitionLoader.reset!` clears shared global state (the resource
|
|
21
|
+
# registry, the portal registry) that accumulates real fixtures
|
|
22
|
+
# registered by *other* tests at file-load time -- e.g.
|
|
23
|
+
# `test/integration/read_only_resource_test.rb`'s `ReadOnlyWidgetResource`
|
|
24
|
+
# and `test_helper.rb`'s fixture resources are registered once, forever,
|
|
25
|
+
# before any test runs. An earlier version of this helper called
|
|
26
|
+
# `reset!` bare and broke `navigation_sections_test.rb` whenever it ran
|
|
27
|
+
# afterward. Snapshot what's there before resetting and restore exactly
|
|
28
|
+
# that afterward, rather than trusting it to come back on its own.
|
|
29
|
+
def snapshot_registry(kind)
|
|
30
|
+
case kind
|
|
31
|
+
when :resources
|
|
32
|
+
before = Admin::Base::Resource.registered_resources.dup
|
|
33
|
+
lambda do
|
|
34
|
+
Admin::Base::Resource.reset_registry!
|
|
35
|
+
before.each { |r| Admin::Base::Resource.registered_resources << r }
|
|
36
|
+
end
|
|
37
|
+
when :portals
|
|
38
|
+
before = AdminSuite::PortalRegistry.all.dup
|
|
39
|
+
lambda do
|
|
40
|
+
AdminSuite::PortalRegistry.reset!
|
|
41
|
+
before.each_value { |definition| AdminSuite::PortalRegistry.register(definition) }
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
# :dashboards/:actions have no accumulating registry to snapshot --
|
|
45
|
+
# unlike :resources/:portals (which pick up real fixtures registered
|
|
46
|
+
# by *other* test files at load time), reset! for these two kinds
|
|
47
|
+
# only clears a single flag/definition object
|
|
48
|
+
# (`root_dashboard_loaded`/`root_dashboard_definition`,
|
|
49
|
+
# `handlers_loaded`) that is entirely local to whichever test set it,
|
|
50
|
+
# via its own `with_globs`/`with_dashboard`-style ensure block. So
|
|
51
|
+
# there is nothing cross-test left to preserve, and "restore" here
|
|
52
|
+
# deliberately degrades to a second `reset!` call rather than a real
|
|
53
|
+
# snapshot+restore -- do not read this branch as returning a
|
|
54
|
+
# value-preserving restore the way the :resources/:portals branches
|
|
55
|
+
# above do.
|
|
56
|
+
-> { AdminSuite::DefinitionLoader.reset!(kind) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Stubs Rails.env for the duration of the block, restoring it afterward.
|
|
61
|
+
# Used to exercise DefinitionLoader's `load` (vs `require`) file-loading
|
|
62
|
+
# mode, which never runs under the dummy app's normal (test-env) CI
|
|
63
|
+
# suite.
|
|
64
|
+
def with_rails_env(name)
|
|
65
|
+
original = Rails.env
|
|
66
|
+
Rails.env = ActiveSupport::EnvironmentInquirer.new(name.to_s)
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
Rails.env = original
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "loads each matching file once in non-development environments" do
|
|
73
|
+
Dir.mktmpdir do |dir|
|
|
74
|
+
marker = File.join(dir, "count.txt")
|
|
75
|
+
File.write(File.join(dir, "thing.rb"), "File.write(#{marker.inspect}, File.exist?(#{marker.inspect}) ? File.read(#{marker.inspect}).to_i + 1 : 1)")
|
|
76
|
+
with_globs(:dashboards, [ File.join(dir, "*.rb") ]) do
|
|
77
|
+
2.times { AdminSuite::DefinitionLoader.load!(:dashboards) }
|
|
78
|
+
assert_equal 1, File.read(marker).to_i
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
test "empty globs are a no-op and do not re-scan" do
|
|
84
|
+
with_globs(:dashboards, []) do
|
|
85
|
+
assert_nothing_raised { 2.times { AdminSuite::DefinitionLoader.load!(:dashboards) } }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
test "a raising definition file surfaces the error in test env" do
|
|
90
|
+
Dir.mktmpdir do |dir|
|
|
91
|
+
File.write(File.join(dir, "bad.rb"), "raise 'boom'")
|
|
92
|
+
with_globs(:dashboards, [ File.join(dir, "*.rb") ]) do
|
|
93
|
+
error = assert_raises(RuntimeError) { AdminSuite::DefinitionLoader.load!(:dashboards) }
|
|
94
|
+
assert_equal "boom", error.message
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
test "an unknown kind is rejected" do
|
|
100
|
+
assert_raises(ArgumentError) { AdminSuite::DefinitionLoader.load!(:nonsense) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# --- Development-mode reload coverage ---
|
|
104
|
+
#
|
|
105
|
+
# Per the Task 9 erratum: `load!` itself must NEVER reset a kind's
|
|
106
|
+
# registry automatically (in development or otherwise) -- only
|
|
107
|
+
# `reset_for_new_request!` (wired into Rails' `to_prepare` by the
|
|
108
|
+
# engine, simulating "a new request began") or a test's explicit
|
|
109
|
+
# `reset!`/`with_globs` may do that. These tests simulate the request
|
|
110
|
+
# boundary explicitly rather than assuming `load!` reloads on every call.
|
|
111
|
+
|
|
112
|
+
test "in development, load! does not reload on repeated calls (bounded to the request boundary)" do
|
|
113
|
+
Dir.mktmpdir do |dir|
|
|
114
|
+
marker = File.join(dir, "count.txt")
|
|
115
|
+
File.write(File.join(dir, "thing.rb"), "File.write(#{marker.inspect}, File.exist?(#{marker.inspect}) ? File.read(#{marker.inspect}).to_i + 1 : 1)")
|
|
116
|
+
with_globs(:dashboards, [ File.join(dir, "*.rb") ]) do
|
|
117
|
+
# reset_for_new_request! resets *every* dev_resettable kind, not
|
|
118
|
+
# just :dashboards -- it also touches PortalRegistry and
|
|
119
|
+
# handlers_loaded, which is exactly the shared global state
|
|
120
|
+
# Finding 3 established a snapshot/restore discipline for. Wrap
|
|
121
|
+
# it here too, or this test becomes seed-dependent the day some
|
|
122
|
+
# other test registers a portal at file-load time.
|
|
123
|
+
restore_portals = snapshot_registry(:portals)
|
|
124
|
+
restore_actions = snapshot_registry(:actions)
|
|
125
|
+
|
|
126
|
+
with_rails_env(:development) do
|
|
127
|
+
3.times { AdminSuite::DefinitionLoader.load!(:dashboards) }
|
|
128
|
+
assert_equal 1, File.read(marker).to_i,
|
|
129
|
+
"expected load! to load the file once, not once per call, without an intervening reset"
|
|
130
|
+
assert AdminSuite.config.root_dashboard_loaded,
|
|
131
|
+
"expected load! to mark the kind loaded in development too, not only outside it"
|
|
132
|
+
|
|
133
|
+
AdminSuite::DefinitionLoader.reset_for_new_request!
|
|
134
|
+
AdminSuite::DefinitionLoader.load!(:dashboards)
|
|
135
|
+
assert_equal 2, File.read(marker).to_i,
|
|
136
|
+
"expected reset_for_new_request! to make the next load! call reload"
|
|
137
|
+
end
|
|
138
|
+
ensure
|
|
139
|
+
restore_portals.call
|
|
140
|
+
restore_actions.call
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
test "in development, a portal removed from disk only disappears after reset_for_new_request!, not on a bare repeated load!" do
|
|
146
|
+
Dir.mktmpdir do |dir|
|
|
147
|
+
portal_file = File.join(dir, "temp_portal.rb")
|
|
148
|
+
File.write(portal_file, "AdminSuite.portal(:definition_loader_dev_test) {}")
|
|
149
|
+
|
|
150
|
+
with_globs(:portals, [ File.join(dir, "*.rb") ]) do
|
|
151
|
+
with_rails_env(:development) do
|
|
152
|
+
AdminSuite::DefinitionLoader.load!(:portals)
|
|
153
|
+
assert AdminSuite::PortalRegistry.all.key?(:definition_loader_dev_test),
|
|
154
|
+
"expected the portal defined by the glob'd file to be registered"
|
|
155
|
+
|
|
156
|
+
File.delete(portal_file)
|
|
157
|
+
|
|
158
|
+
# No reset happened -- load! alone must not touch the registry.
|
|
159
|
+
AdminSuite::DefinitionLoader.load!(:portals)
|
|
160
|
+
assert AdminSuite::PortalRegistry.all.key?(:definition_loader_dev_test),
|
|
161
|
+
"expected the portal to remain registered without an intervening reset_for_new_request!"
|
|
162
|
+
|
|
163
|
+
AdminSuite::DefinitionLoader.reset_for_new_request!
|
|
164
|
+
AdminSuite::DefinitionLoader.load!(:portals)
|
|
165
|
+
assert_not AdminSuite::PortalRegistry.all.key?(:definition_loader_dev_test),
|
|
166
|
+
"expected the removed portal to disappear once reset_for_new_request! ran"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
test "reset_for_new_request! resets portals, dashboards and actions, but never resources" do
|
|
173
|
+
# This is the regression this task's erratum exists for: resource
|
|
174
|
+
# registration happens only via Class#inherited, which never refires
|
|
175
|
+
# on a reopened (already-defined) class, so resetting the resource
|
|
176
|
+
# registry between requests would empty it permanently after the
|
|
177
|
+
# first reset+reload cycle. See the :resources KINDS entry.
|
|
178
|
+
Dir.mktmpdir do |dir|
|
|
179
|
+
File.write(File.join(dir, "resource.rb"), <<~RUBY)
|
|
180
|
+
module Admin
|
|
181
|
+
module Resources
|
|
182
|
+
class ResetForNewRequestResource < Admin::Base::Resource
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
RUBY
|
|
187
|
+
|
|
188
|
+
with_globs(:resources, [ File.join(dir, "*.rb") ]) do
|
|
189
|
+
with_globs(:portals, []) do
|
|
190
|
+
with_globs(:dashboards, []) do
|
|
191
|
+
with_globs(:actions, []) do
|
|
192
|
+
AdminSuite.portal(:reset_for_new_request_test) {}
|
|
193
|
+
AdminSuite::DefinitionLoader.load!(:resources)
|
|
194
|
+
Admin::Base::ActionExecutor.handlers_loaded = true
|
|
195
|
+
AdminSuite.config.root_dashboard_loaded = true
|
|
196
|
+
|
|
197
|
+
AdminSuite::DefinitionLoader.reset_for_new_request!
|
|
198
|
+
|
|
199
|
+
assert_not AdminSuite::PortalRegistry.all.key?(:reset_for_new_request_test),
|
|
200
|
+
"expected portals to be reset"
|
|
201
|
+
assert_not AdminSuite.config.root_dashboard_loaded, "expected dashboards to be reset"
|
|
202
|
+
assert_not Admin::Base::ActionExecutor.handlers_loaded, "expected actions to be reset"
|
|
203
|
+
assert Admin::Base::Resource.registered_resources.any? { |r| r.name == "Admin::Resources::ResetForNewRequestResource" },
|
|
204
|
+
"expected resources to survive reset_for_new_request! untouched"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The assertion whose absence let the erratum's bug through: the
|
|
213
|
+
# dev-mode resources test that existed before this fix only ever loaded
|
|
214
|
+
# a brand-new constant exactly once -- the one case where Class#inherited
|
|
215
|
+
# actually fires. Calling load! twice against the *same* file, with no
|
|
216
|
+
# reset in between, is what a single request actually does (base_helper's
|
|
217
|
+
# `portal_color`/`portal_icon` both re-enter `navigation_items`, which
|
|
218
|
+
# calls `ensure_resources_loaded!` again).
|
|
219
|
+
test "load!(:resources) called twice against the same file leaves the registry populated after the second call" do
|
|
220
|
+
Dir.mktmpdir do |dir|
|
|
221
|
+
File.write(File.join(dir, "definition_loader_dev_resource.rb"), <<~RUBY)
|
|
222
|
+
module Admin
|
|
223
|
+
module Resources
|
|
224
|
+
class DefinitionLoaderDevResource < Admin::Base::Resource
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
RUBY
|
|
229
|
+
|
|
230
|
+
with_globs(:resources, [ File.join(dir, "*.rb") ]) do
|
|
231
|
+
with_rails_env(:development) do
|
|
232
|
+
2.times { AdminSuite::DefinitionLoader.load!(:resources) }
|
|
233
|
+
|
|
234
|
+
assert Admin::Base::Resource.registered_resources.any? { |r| r.name == "Admin::Resources::DefinitionLoaderDevResource" },
|
|
235
|
+
"expected the resource to still be registered after a second load! call"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# --- Per-kind wiring sanity (guards against a kind pointed at the wrong globals) ---
|
|
242
|
+
|
|
243
|
+
test "resources kind is backed by Admin::Base::Resource's registry" do
|
|
244
|
+
Dir.mktmpdir do |dir|
|
|
245
|
+
File.write(File.join(dir, "definition_loader_dev_resource.rb"), <<~RUBY)
|
|
246
|
+
module Admin
|
|
247
|
+
module Resources
|
|
248
|
+
class DefinitionLoaderDevResource2 < Admin::Base::Resource
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
RUBY
|
|
253
|
+
|
|
254
|
+
with_globs(:resources, [ File.join(dir, "*.rb") ]) do
|
|
255
|
+
AdminSuite::DefinitionLoader.load!(:resources)
|
|
256
|
+
assert Admin::Base::Resource.registered_resources.any? { |r| r.name == "Admin::Resources::DefinitionLoaderDevResource2" }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
test "actions kind is backed by ActionExecutor.handlers_loaded" do
|
|
262
|
+
with_globs(:actions, []) do
|
|
263
|
+
Admin::Base::ActionExecutor.handlers_loaded = false
|
|
264
|
+
AdminSuite::DefinitionLoader.load!(:actions)
|
|
265
|
+
assert Admin::Base::ActionExecutor.handlers_loaded
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
test "dashboards kind is backed by AdminSuite.config.root_dashboard_loaded" do
|
|
270
|
+
with_globs(:dashboards, []) do
|
|
271
|
+
AdminSuite::DefinitionLoader.load!(:dashboards)
|
|
272
|
+
assert AdminSuite.config.root_dashboard_loaded
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class EngineDefaultsTest < ActiveSupport::TestCase
|
|
7
|
+
def with_fresh_config
|
|
8
|
+
saved = AdminSuite.instance_variable_get(:@config)
|
|
9
|
+
AdminSuite.instance_variable_set(:@config, AdminSuite::Configuration.new)
|
|
10
|
+
yield AdminSuite.config
|
|
11
|
+
ensure
|
|
12
|
+
AdminSuite.instance_variable_set(:@config, saved)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
test "an explicit empty portals hash suppresses the built-in defaults" do
|
|
16
|
+
with_fresh_config do |config|
|
|
17
|
+
config.portals = {}
|
|
18
|
+
assert config.portals_configured?, "assigning portals must mark them configured"
|
|
19
|
+
AdminSuite::Engine.apply_default_portals!(config)
|
|
20
|
+
assert_empty config.portals
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test "untouched portals receive the built-in defaults" do
|
|
25
|
+
with_fresh_config do |config|
|
|
26
|
+
refute config.portals_configured?
|
|
27
|
+
AdminSuite::Engine.apply_default_portals!(config)
|
|
28
|
+
assert_includes config.portals.keys, :ops
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test "dead configuration and DSL members are gone" do
|
|
33
|
+
refute AdminSuite::Configuration.new.respond_to?(:tailwind_cdn)
|
|
34
|
+
assert Admin::Base::Resource.respond_to?(:exportable),
|
|
35
|
+
"exportable is a deprecated no-op, not removed -- see ResourceExportableDeprecationTest"
|
|
36
|
+
refute Admin::Base::Resource::ColumnDefinition.members.include?(:render)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|