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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -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 +8 -1
- data/app/helpers/admin_suite/base_helper.rb +49 -385
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
- data/app/views/admin_suite/panels/_chart.html.erb +3 -2
- data/app/views/admin_suite/panels/_stat.html.erb +10 -0
- data/app/views/admin_suite/resources/index.html.erb +7 -77
- data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +1 -1
- data/app/views/layouts/admin_suite/application.html.erb +5 -3
- data/lib/admin/base/action_executor.rb +19 -51
- data/lib/admin/base/resource.rb +53 -14
- data/lib/admin_suite/configuration.rb +28 -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 +70 -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 +230 -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/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 +37 -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/dashboard_test.rb +40 -0
- data/test/integration/layout_assets_test.rb +112 -0
- data/test/integration/navigation_sections_test.rb +41 -0
- data/test/integration/pagination_and_stats_test.rb +152 -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 +264 -0
- data/test/lib/engine_defaults_test.rb +39 -0
- data/test/lib/form_field_renderer_test.rb +64 -0
- data/test/lib/legacy_renderer_deprecation_test.rb +35 -0
- data/test/lib/renderer_test.rb +221 -0
- data/test/lib/resource_exportable_deprecation_test.rb +39 -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 +77 -0
- 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 " 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,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
|