admin_suite 0.3.0 → 0.4.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/app/assets/vendor/easymde.min.css +7 -0
  4. data/app/assets/vendor/easymde.min.js +7 -0
  5. data/app/controllers/admin_suite/application_controller.rb +26 -47
  6. data/app/controllers/admin_suite/portals_controller.rb +2 -2
  7. data/app/controllers/admin_suite/resources_controller.rb +8 -1
  8. data/app/helpers/admin_suite/base_helper.rb +49 -385
  9. data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
  10. data/app/views/admin_suite/panels/_chart.html.erb +3 -2
  11. data/app/views/admin_suite/panels/_stat.html.erb +10 -0
  12. data/app/views/admin_suite/resources/index.html.erb +7 -77
  13. data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
  14. data/app/views/admin_suite/shared/_sidebar.html.erb +1 -1
  15. data/app/views/layouts/admin_suite/application.html.erb +5 -3
  16. data/lib/admin/base/action_executor.rb +19 -51
  17. data/lib/admin/base/resource.rb +53 -14
  18. data/lib/admin_suite/configuration.rb +28 -3
  19. data/lib/admin_suite/definition_loader.rb +194 -0
  20. data/lib/admin_suite/deprecation.rb +48 -0
  21. data/lib/admin_suite/engine.rb +55 -76
  22. data/lib/admin_suite/host_autoload_policy.rb +132 -0
  23. data/lib/admin_suite/legacy_custom_renderer_procs.rb +29 -0
  24. data/lib/admin_suite/portal_definition.rb +11 -0
  25. data/lib/admin_suite/renderer.rb +133 -0
  26. data/lib/admin_suite/renderer_registry.rb +70 -0
  27. data/lib/admin_suite/renderers/code_renderer.rb +15 -0
  28. data/lib/admin_suite/renderers/json_renderer.rb +15 -0
  29. data/lib/admin_suite/renderers/key_value_renderer.rb +39 -0
  30. data/lib/admin_suite/renderers/legacy_gleania.rb +230 -0
  31. data/lib/admin_suite/renderers/table_from_renderer.rb +22 -0
  32. data/lib/admin_suite/section_definition.rb +42 -0
  33. data/lib/admin_suite/ui/field_renderer_registry.rb +31 -4
  34. data/lib/admin_suite/ui/form_field_renderer.rb +1 -7
  35. data/lib/admin_suite/ui/show_formatter_registry.rb +9 -0
  36. data/lib/admin_suite/ui/show_value_formatter.rb +7 -3
  37. data/lib/admin_suite/version.rb +1 -1
  38. data/lib/admin_suite.rb +37 -0
  39. data/lib/generators/admin_suite/install/templates/admin_suite.rb +0 -4
  40. data/test/controllers/resources_controller_test.rb +76 -1
  41. data/test/integration/dashboard_test.rb +40 -0
  42. data/test/integration/layout_assets_test.rb +112 -0
  43. data/test/integration/navigation_sections_test.rb +41 -0
  44. data/test/integration/pagination_and_stats_test.rb +152 -0
  45. data/test/lib/action_executor_redirect_test.rb +41 -0
  46. data/test/lib/builtin_renderers_test.rb +202 -0
  47. data/test/lib/definition_loader_test.rb +264 -0
  48. data/test/lib/engine_defaults_test.rb +39 -0
  49. data/test/lib/form_field_renderer_test.rb +64 -0
  50. data/test/lib/legacy_renderer_deprecation_test.rb +35 -0
  51. data/test/lib/renderer_test.rb +221 -0
  52. data/test/lib/resource_exportable_deprecation_test.rb +39 -0
  53. data/test/lib/show_value_formatter_test.rb +88 -0
  54. data/test/lib/zeitwerk_integration_test.rb +28 -64
  55. data/test/test_helper.rb +77 -0
  56. metadata +28 -1
@@ -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,264 @@
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
+ -> { AdminSuite::DefinitionLoader.reset!(kind) }
45
+ end
46
+ end
47
+
48
+ # Stubs Rails.env for the duration of the block, restoring it afterward.
49
+ # Used to exercise DefinitionLoader's `load` (vs `require`) file-loading
50
+ # mode, which never runs under the dummy app's normal (test-env) CI
51
+ # suite.
52
+ def with_rails_env(name)
53
+ original = Rails.env
54
+ Rails.env = ActiveSupport::EnvironmentInquirer.new(name.to_s)
55
+ yield
56
+ ensure
57
+ Rails.env = original
58
+ end
59
+
60
+ test "loads each matching file once in non-development environments" do
61
+ Dir.mktmpdir do |dir|
62
+ marker = File.join(dir, "count.txt")
63
+ File.write(File.join(dir, "thing.rb"), "File.write(#{marker.inspect}, File.exist?(#{marker.inspect}) ? File.read(#{marker.inspect}).to_i + 1 : 1)")
64
+ with_globs(:dashboards, [ File.join(dir, "*.rb") ]) do
65
+ 2.times { AdminSuite::DefinitionLoader.load!(:dashboards) }
66
+ assert_equal 1, File.read(marker).to_i
67
+ end
68
+ end
69
+ end
70
+
71
+ test "empty globs are a no-op and do not re-scan" do
72
+ with_globs(:dashboards, []) do
73
+ assert_nothing_raised { 2.times { AdminSuite::DefinitionLoader.load!(:dashboards) } }
74
+ end
75
+ end
76
+
77
+ test "a raising definition file surfaces the error in test env" do
78
+ Dir.mktmpdir do |dir|
79
+ File.write(File.join(dir, "bad.rb"), "raise 'boom'")
80
+ with_globs(:dashboards, [ File.join(dir, "*.rb") ]) do
81
+ error = assert_raises(RuntimeError) { AdminSuite::DefinitionLoader.load!(:dashboards) }
82
+ assert_equal "boom", error.message
83
+ end
84
+ end
85
+ end
86
+
87
+ test "an unknown kind is rejected" do
88
+ assert_raises(ArgumentError) { AdminSuite::DefinitionLoader.load!(:nonsense) }
89
+ end
90
+
91
+ # --- Development-mode reload coverage ---
92
+ #
93
+ # Per the Task 9 erratum: `load!` itself must NEVER reset a kind's
94
+ # registry automatically (in development or otherwise) -- only
95
+ # `reset_for_new_request!` (wired into Rails' `to_prepare` by the
96
+ # engine, simulating "a new request began") or a test's explicit
97
+ # `reset!`/`with_globs` may do that. These tests simulate the request
98
+ # boundary explicitly rather than assuming `load!` reloads on every call.
99
+
100
+ test "in development, load! does not reload on repeated calls (bounded to the request boundary)" do
101
+ Dir.mktmpdir do |dir|
102
+ marker = File.join(dir, "count.txt")
103
+ File.write(File.join(dir, "thing.rb"), "File.write(#{marker.inspect}, File.exist?(#{marker.inspect}) ? File.read(#{marker.inspect}).to_i + 1 : 1)")
104
+ with_globs(:dashboards, [ File.join(dir, "*.rb") ]) do
105
+ # reset_for_new_request! resets *every* dev_resettable kind, not
106
+ # just :dashboards -- it also touches PortalRegistry and
107
+ # handlers_loaded, which is exactly the shared global state
108
+ # Finding 3 established a snapshot/restore discipline for. Wrap
109
+ # it here too, or this test becomes seed-dependent the day some
110
+ # other test registers a portal at file-load time.
111
+ restore_portals = snapshot_registry(:portals)
112
+ restore_actions = snapshot_registry(:actions)
113
+
114
+ with_rails_env(:development) do
115
+ 3.times { AdminSuite::DefinitionLoader.load!(:dashboards) }
116
+ assert_equal 1, File.read(marker).to_i,
117
+ "expected load! to load the file once, not once per call, without an intervening reset"
118
+ assert AdminSuite.config.root_dashboard_loaded,
119
+ "expected load! to mark the kind loaded in development too, not only outside it"
120
+
121
+ AdminSuite::DefinitionLoader.reset_for_new_request!
122
+ AdminSuite::DefinitionLoader.load!(:dashboards)
123
+ assert_equal 2, File.read(marker).to_i,
124
+ "expected reset_for_new_request! to make the next load! call reload"
125
+ end
126
+ ensure
127
+ restore_portals.call
128
+ restore_actions.call
129
+ end
130
+ end
131
+ end
132
+
133
+ test "in development, a portal removed from disk only disappears after reset_for_new_request!, not on a bare repeated load!" do
134
+ Dir.mktmpdir do |dir|
135
+ portal_file = File.join(dir, "temp_portal.rb")
136
+ File.write(portal_file, "AdminSuite.portal(:definition_loader_dev_test) {}")
137
+
138
+ with_globs(:portals, [ File.join(dir, "*.rb") ]) do
139
+ with_rails_env(:development) do
140
+ AdminSuite::DefinitionLoader.load!(:portals)
141
+ assert AdminSuite::PortalRegistry.all.key?(:definition_loader_dev_test),
142
+ "expected the portal defined by the glob'd file to be registered"
143
+
144
+ File.delete(portal_file)
145
+
146
+ # No reset happened -- load! alone must not touch the registry.
147
+ AdminSuite::DefinitionLoader.load!(:portals)
148
+ assert AdminSuite::PortalRegistry.all.key?(:definition_loader_dev_test),
149
+ "expected the portal to remain registered without an intervening reset_for_new_request!"
150
+
151
+ AdminSuite::DefinitionLoader.reset_for_new_request!
152
+ AdminSuite::DefinitionLoader.load!(:portals)
153
+ assert_not AdminSuite::PortalRegistry.all.key?(:definition_loader_dev_test),
154
+ "expected the removed portal to disappear once reset_for_new_request! ran"
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ test "reset_for_new_request! resets portals, dashboards and actions, but never resources" do
161
+ # This is the regression this task's erratum exists for: resource
162
+ # registration happens only via Class#inherited, which never refires
163
+ # on a reopened (already-defined) class, so resetting the resource
164
+ # registry between requests would empty it permanently after the
165
+ # first reset+reload cycle. See the :resources KINDS entry.
166
+ Dir.mktmpdir do |dir|
167
+ File.write(File.join(dir, "resource.rb"), <<~RUBY)
168
+ module Admin
169
+ module Resources
170
+ class ResetForNewRequestResource < Admin::Base::Resource
171
+ end
172
+ end
173
+ end
174
+ RUBY
175
+
176
+ with_globs(:resources, [ File.join(dir, "*.rb") ]) do
177
+ with_globs(:portals, []) do
178
+ with_globs(:dashboards, []) do
179
+ with_globs(:actions, []) do
180
+ AdminSuite.portal(:reset_for_new_request_test) {}
181
+ AdminSuite::DefinitionLoader.load!(:resources)
182
+ Admin::Base::ActionExecutor.handlers_loaded = true
183
+ AdminSuite.config.root_dashboard_loaded = true
184
+
185
+ AdminSuite::DefinitionLoader.reset_for_new_request!
186
+
187
+ assert_not AdminSuite::PortalRegistry.all.key?(:reset_for_new_request_test),
188
+ "expected portals to be reset"
189
+ assert_not AdminSuite.config.root_dashboard_loaded, "expected dashboards to be reset"
190
+ assert_not Admin::Base::ActionExecutor.handlers_loaded, "expected actions to be reset"
191
+ assert Admin::Base::Resource.registered_resources.any? { |r| r.name == "Admin::Resources::ResetForNewRequestResource" },
192
+ "expected resources to survive reset_for_new_request! untouched"
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ # The assertion whose absence let the erratum's bug through: the
201
+ # dev-mode resources test that existed before this fix only ever loaded
202
+ # a brand-new constant exactly once -- the one case where Class#inherited
203
+ # actually fires. Calling load! twice against the *same* file, with no
204
+ # reset in between, is what a single request actually does (base_helper's
205
+ # `portal_color`/`portal_icon` both re-enter `navigation_items`, which
206
+ # calls `ensure_resources_loaded!` again).
207
+ test "load!(:resources) called twice against the same file leaves the registry populated after the second call" do
208
+ Dir.mktmpdir do |dir|
209
+ File.write(File.join(dir, "definition_loader_dev_resource.rb"), <<~RUBY)
210
+ module Admin
211
+ module Resources
212
+ class DefinitionLoaderDevResource < Admin::Base::Resource
213
+ end
214
+ end
215
+ end
216
+ RUBY
217
+
218
+ with_globs(:resources, [ File.join(dir, "*.rb") ]) do
219
+ with_rails_env(:development) do
220
+ 2.times { AdminSuite::DefinitionLoader.load!(:resources) }
221
+
222
+ assert Admin::Base::Resource.registered_resources.any? { |r| r.name == "Admin::Resources::DefinitionLoaderDevResource" },
223
+ "expected the resource to still be registered after a second load! call"
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ # --- Per-kind wiring sanity (guards against a kind pointed at the wrong globals) ---
230
+
231
+ test "resources kind is backed by Admin::Base::Resource's registry" do
232
+ Dir.mktmpdir do |dir|
233
+ File.write(File.join(dir, "definition_loader_dev_resource.rb"), <<~RUBY)
234
+ module Admin
235
+ module Resources
236
+ class DefinitionLoaderDevResource2 < Admin::Base::Resource
237
+ end
238
+ end
239
+ end
240
+ RUBY
241
+
242
+ with_globs(:resources, [ File.join(dir, "*.rb") ]) do
243
+ AdminSuite::DefinitionLoader.load!(:resources)
244
+ assert Admin::Base::Resource.registered_resources.any? { |r| r.name == "Admin::Resources::DefinitionLoaderDevResource2" }
245
+ end
246
+ end
247
+ end
248
+
249
+ test "actions kind is backed by ActionExecutor.handlers_loaded" do
250
+ with_globs(:actions, []) do
251
+ Admin::Base::ActionExecutor.handlers_loaded = false
252
+ AdminSuite::DefinitionLoader.load!(:actions)
253
+ assert Admin::Base::ActionExecutor.handlers_loaded
254
+ end
255
+ end
256
+
257
+ test "dashboards kind is backed by AdminSuite.config.root_dashboard_loaded" do
258
+ with_globs(:dashboards, []) do
259
+ AdminSuite::DefinitionLoader.load!(:dashboards)
260
+ assert AdminSuite.config.root_dashboard_loaded
261
+ end
262
+ end
263
+ end
264
+ 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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class FormFieldRendererTest < ActionView::TestCase
7
+ include AdminSuite::BaseHelper
8
+
9
+ Record = Struct.new(:name, :body, :enabled) do
10
+ def to_model = self
11
+ def model_name = ActiveModel::Name.new(self.class, nil, "Record")
12
+ def persisted? = false
13
+ def to_key = nil
14
+
15
+ # render_form_field unconditionally calls `resource.errors[field.name]`
16
+ # for every field type (to add the error border class / message), so
17
+ # the double needs an `errors` object supporting `[]` -> Array-like
18
+ # (`.any?`, `.first`). No test here exercises an actual validation
19
+ # error, so a Hash defaulting to `[]` is sufficient.
20
+ def errors = Hash.new([])
21
+ end
22
+
23
+ def field(name, type)
24
+ Admin::Base::Resource::FieldDefinition.new(
25
+ name: name, type: type, required: false, label: name.to_s.humanize,
26
+ readonly: false, multiple: false, creatable: false, preview: true
27
+ )
28
+ end
29
+
30
+ def render_field(type, name: :name)
31
+ record = Record.new("x", "y", true)
32
+ html = nil
33
+ form_with(model: record, url: "/", scope: :record) { |f| html = render_form_field(f, field(name, type), record) }
34
+ html.to_s
35
+ end
36
+
37
+ test "textarea type renders a textarea" do
38
+ assert_includes render_field(:textarea, name: :body), "<textarea"
39
+ end
40
+
41
+ test "email and url types render matching inputs" do
42
+ assert_includes render_field(:email), 'type="email"'
43
+ assert_includes render_field(:url), 'type="url"'
44
+ end
45
+
46
+ test "date and datetime types render matching inputs" do
47
+ assert_includes render_field(:date), 'type="date"'
48
+ assert_includes render_field(:datetime), 'type="datetime-local"'
49
+ end
50
+
51
+ test "markdown type wires the markdown editor controller" do
52
+ assert_includes render_field(:markdown, name: :body), "admin-suite--markdown-editor"
53
+ end
54
+
55
+ test "an unregistered type falls back to a text field" do
56
+ html = render_field(:totally_unknown_type)
57
+ assert_includes html, 'type="text"'
58
+ end
59
+
60
+ test "the label is rendered for every field" do
61
+ assert_includes render_field(:text), "Name"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class LegacyRendererDeprecationTest < ActionView::TestCase
7
+ include ::ERB::Util
8
+ include AdminSuite::BaseHelper
9
+
10
+ Prompt = Struct.new(:prompt_template)
11
+
12
+ setup { AdminSuite::Renderers::LegacyGleania.reset_deprecation_notices! }
13
+
14
+ test "the legacy prompt renderer still renders its template" do
15
+ html = render_custom_section(Prompt.new("Hello {{name}}"), :prompt_template_preview)
16
+ assert_includes html, "Hello"
17
+ assert_includes html, "name"
18
+ end
19
+
20
+ test "using a legacy renderer logs a deprecation once per key" do
21
+ logged = []
22
+ AdminSuite::Renderers::LegacyGleania.stub(:warn_once_sink, ->(msg) { logged << msg }) do
23
+ 2.times { render_custom_section(Prompt.new("x"), :prompt_template_preview) }
24
+ end
25
+ assert_equal 1, logged.size
26
+ assert_includes logged.first, "deprecated"
27
+ assert_includes logged.first, "0.5.0"
28
+ end
29
+
30
+ test "the gem no longer includes the host CustomRenderersHelper constant" do
31
+ source = AdminSuite::Engine.root.join("app/helpers/admin_suite/base_helper.rb").read
32
+ refute_includes source, "Internal::Developer::CustomRenderersHelper"
33
+ end
34
+ end
35
+ end