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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +210 -1
  3. data/app/assets/vendor/chart.umd.min.js +14 -0
  4. data/app/assets/vendor/easymde.min.css +7 -0
  5. data/app/assets/vendor/easymde.min.js +7 -0
  6. data/app/controllers/admin_suite/application_controller.rb +26 -47
  7. data/app/controllers/admin_suite/portals_controller.rb +2 -2
  8. data/app/controllers/admin_suite/resources_controller.rb +138 -6
  9. data/app/helpers/admin_suite/base_helper.rb +232 -391
  10. data/app/javascript/admin_suite_application.js +3 -0
  11. data/app/javascript/controllers/admin_suite/chart_controller.js +173 -0
  12. data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
  13. data/app/views/admin_suite/panels/_chart.html.erb +158 -18
  14. data/app/views/admin_suite/panels/_stat.html.erb +10 -0
  15. data/app/views/admin_suite/resources/index.html.erb +31 -82
  16. data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
  17. data/app/views/admin_suite/shared/_sidebar.html.erb +15 -9
  18. data/app/views/layouts/admin_suite/application.html.erb +11 -3
  19. data/config/routes.rb +3 -0
  20. data/lib/admin/base/action_executor.rb +19 -51
  21. data/lib/admin/base/filter_builder.rb +48 -5
  22. data/lib/admin/base/resource.rb +92 -17
  23. data/lib/admin_suite/configuration.rb +32 -3
  24. data/lib/admin_suite/definition_loader.rb +194 -0
  25. data/lib/admin_suite/deprecation.rb +48 -0
  26. data/lib/admin_suite/engine.rb +55 -76
  27. data/lib/admin_suite/host_autoload_policy.rb +132 -0
  28. data/lib/admin_suite/legacy_custom_renderer_procs.rb +29 -0
  29. data/lib/admin_suite/portal_definition.rb +11 -0
  30. data/lib/admin_suite/renderer.rb +133 -0
  31. data/lib/admin_suite/renderer_registry.rb +81 -0
  32. data/lib/admin_suite/renderers/code_renderer.rb +15 -0
  33. data/lib/admin_suite/renderers/json_renderer.rb +15 -0
  34. data/lib/admin_suite/renderers/key_value_renderer.rb +39 -0
  35. data/lib/admin_suite/renderers/legacy_gleania.rb +232 -0
  36. data/lib/admin_suite/renderers/table_from_renderer.rb +22 -0
  37. data/lib/admin_suite/section_definition.rb +42 -0
  38. data/lib/admin_suite/ui/dashboard_definition.rb +6 -0
  39. data/lib/admin_suite/ui/field_renderer_registry.rb +31 -4
  40. data/lib/admin_suite/ui/form_field_renderer.rb +1 -7
  41. data/lib/admin_suite/ui/show_formatter_registry.rb +9 -0
  42. data/lib/admin_suite/ui/show_value_formatter.rb +7 -3
  43. data/lib/admin_suite/version.rb +1 -1
  44. data/lib/admin_suite.rb +48 -0
  45. data/lib/generators/admin_suite/install/templates/admin_suite.rb +0 -4
  46. data/test/controllers/resources_controller_test.rb +76 -1
  47. data/test/integration/association_linking_test.rb +292 -0
  48. data/test/integration/chart_panel_test.rb +491 -0
  49. data/test/integration/dashboard_test.rb +9 -2
  50. data/test/integration/index_table_test.rb +289 -0
  51. data/test/integration/layout_assets_test.rb +112 -0
  52. data/test/integration/navigation_sections_test.rb +62 -0
  53. data/test/integration/pagination_and_stats_test.rb +152 -0
  54. data/test/integration/searchable_select_search_test.rb +368 -0
  55. data/test/integration/show_hide_blank_test.rb +195 -0
  56. data/test/integration/toggle_test.rb +106 -0
  57. data/test/lib/action_executor_redirect_test.rb +41 -0
  58. data/test/lib/builtin_renderers_test.rb +202 -0
  59. data/test/lib/definition_loader_test.rb +276 -0
  60. data/test/lib/engine_defaults_test.rb +39 -0
  61. data/test/lib/form_field_renderer_test.rb +136 -0
  62. data/test/lib/format_table_cell_test.rb +49 -0
  63. data/test/lib/index_includes_test.rb +223 -0
  64. data/test/lib/legacy_renderer_deprecation_test.rb +42 -0
  65. data/test/lib/renderer_test.rb +237 -0
  66. data/test/lib/resource_exportable_deprecation_test.rb +47 -0
  67. data/test/lib/show_value_formatter_test.rb +88 -0
  68. data/test/lib/zeitwerk_integration_test.rb +28 -64
  69. data/test/test_helper.rb +121 -5
  70. 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 &quot; pattern), so a literal `"id"` never appears verbatim.
53
+ assert_includes build(:json), "&quot;id&quot;"
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