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,491 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "tmpdir"
5
+
6
+ module AdminSuite
7
+ class ChartPanelTest < ActionDispatch::IntegrationTest
8
+ # Renders a root dashboard from a temp file. Mirrors the recipe in
9
+ # dashboard_test.rb, which is the only reliable way to exercise panels.
10
+ def with_dashboard(body)
11
+ Dir.mktmpdir do |dir|
12
+ File.write(File.join(dir, "dashboard.rb"), body)
13
+ saved = AdminSuite.config.dashboard_globs
14
+ AdminSuite.config.dashboard_globs = [ File.join(dir, "*.rb") ]
15
+ AdminSuite.reset_root_dashboard!
16
+ AdminSuite::DefinitionLoader.reset!(:dashboards)
17
+ yield
18
+ ensure
19
+ AdminSuite.config.dashboard_globs = saved
20
+ AdminSuite.reset_root_dashboard!
21
+ AdminSuite::DefinitionLoader.reset!(:dashboards)
22
+ end
23
+ end
24
+
25
+ test "a chart panel emits the chart controller with serialized series" do
26
+ with_dashboard(<<~RUBY) do
27
+ AdminSuite.root_dashboard do
28
+ row do
29
+ chart_panel "Daily Cost", data: -> { [ { label: "Mon", value: 3 }, { label: "Tue", value: 7 } ] }
30
+ end
31
+ end
32
+ RUBY
33
+ get "/internal/admin_suite"
34
+ assert_response :success
35
+ assert_includes response.body, "admin-suite--chart"
36
+ assert_includes response.body, "Mon"
37
+ assert_includes response.body, "Tue"
38
+ end
39
+ end
40
+
41
+ test "string-keyed chart data renders values, not blank bars" do
42
+ with_dashboard(<<~RUBY) do
43
+ AdminSuite.root_dashboard do
44
+ row do
45
+ chart_panel "From JSONB", data: -> { [ { "label" => "Alpha", "value" => 5 } ] }
46
+ end
47
+ end
48
+ RUBY
49
+ get "/internal/admin_suite"
50
+ assert_response :success
51
+ assert_includes response.body, "Alpha"
52
+ document = Nokogiri::HTML(response.body)
53
+ # Scoped to the chart's own card: the layout's topbar partial also has
54
+ # h-16/h-full classes (unrelated to the chart), so an unscoped
55
+ # "[data-admin-suite--chart-target='bars'] > .h-full > div" selector
56
+ # is deliberately anchored under the chart's own card rather than
57
+ # matching against the whole page.
58
+ chart = document.at_xpath("//h3[normalize-space()='From JSONB']/ancestor::div[contains(@class, 'rounded-xl')]")
59
+ assert chart, "expected the chart panel card"
60
+ bar = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div").first
61
+ assert bar, "expected a rendered bar"
62
+ assert_equal "height: 100%", bar["style"], "string-keyed value must normalize like a symbol-keyed one"
63
+ end
64
+ end
65
+
66
+ test "a raising data proc degrades to the empty state instead of a 500" do
67
+ with_dashboard(<<~RUBY) do
68
+ AdminSuite.root_dashboard do
69
+ row do
70
+ chart_panel "Broken", data: -> { raise "boom" }
71
+ end
72
+ end
73
+ RUBY
74
+ get "/internal/admin_suite"
75
+ assert_response :success
76
+ assert_includes response.body, "Broken"
77
+ end
78
+ end
79
+
80
+ test "a malformed row in an otherwise-good data array degrades instead of a 500" do
81
+ with_dashboard(<<~RUBY) do
82
+ AdminSuite.root_dashboard do
83
+ row do
84
+ chart_panel "Mixed", data: -> { [ { label: "ok", value: 3 }, 5, nil ] }
85
+ end
86
+ end
87
+ RUBY
88
+ get "/internal/admin_suite"
89
+ assert_response :success
90
+ assert_includes response.body, "Mixed"
91
+ document = Nokogiri::HTML(response.body)
92
+ chart = document.at_xpath("//h3[normalize-space()='Mixed']/ancestor::div[contains(@class, 'rounded-xl')]")
93
+ assert chart, "expected the chart panel card"
94
+ # The good row still renders as a full-height bar; the junk rows
95
+ # (an Integer and nil) are silently dropped rather than raising.
96
+ assert_includes chart.text, "ok"
97
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
98
+ assert_equal 1, bars.size
99
+ assert_equal "height: 100%", bars.first["style"]
100
+ end
101
+ end
102
+
103
+ test "a Hash row with a non-numeric value degrades to a zero-height bar instead of a 500" do
104
+ with_dashboard(<<~RUBY) do
105
+ AdminSuite.root_dashboard do
106
+ row do
107
+ chart_panel "Junk Values", data: -> {
108
+ [
109
+ { label: "bool", value: true },
110
+ { label: "hash", value: {} },
111
+ { label: "array", value: [ 1, 2 ] },
112
+ { label: "good", value: 5 }
113
+ ]
114
+ }
115
+ end
116
+ end
117
+ RUBY
118
+ get "/internal/admin_suite"
119
+ assert_response :success
120
+ assert_includes response.body, "Junk Values"
121
+ document = Nokogiri::HTML(response.body)
122
+ chart = document.at_xpath("//h3[normalize-space()='Junk Values']/ancestor::div[contains(@class, 'rounded-xl')]")
123
+ assert chart, "expected the chart panel card"
124
+ # Boolean/Hash/Array values used to raise via #to_f (e.g. `true.to_f`
125
+ # is a NoMethodError). They now coerce to 0 instead of 500ing, and
126
+ # the one well-formed row alongside them still renders at full height.
127
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
128
+ assert_equal 4, bars.size
129
+ assert_equal [ "height: 0%", "height: 0%", "height: 0%", "height: 100%" ], bars.map { |bar| bar["style"] }
130
+ assert_includes chart.text, "good"
131
+ end
132
+ end
133
+
134
+ test "a numeric-string value still displays without a trailing .0 in the tooltip" do
135
+ with_dashboard(<<~RUBY) do
136
+ AdminSuite.root_dashboard do
137
+ row do
138
+ chart_panel "String Value", data: -> { [ { label: "Str", value: "3" } ] }
139
+ end
140
+ end
141
+ RUBY
142
+ get "/internal/admin_suite"
143
+ assert_response :success
144
+ document = Nokogiri::HTML(response.body)
145
+ chart = document.at_xpath("//h3[normalize-space()='String Value']/ancestor::div[contains(@class, 'rounded-xl')]")
146
+ assert chart, "expected the chart panel card"
147
+ bar = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div").first
148
+ assert bar, "expected a rendered bar"
149
+ # Display keeps the original value ("3"), even though the height math
150
+ # underneath uses the coerced numeric (3.0) — no display regression
151
+ # from the total-coercion fix.
152
+ assert_equal "Str: 3", bar["title"]
153
+ assert_equal "height: 100%", bar["style"]
154
+ end
155
+ end
156
+
157
+ test "chart height defaults to a real chart size and is configurable" do
158
+ with_dashboard(<<~RUBY) do
159
+ AdminSuite.root_dashboard do
160
+ row do
161
+ chart_panel "Daily Cost", data: -> { [ { label: "Mon", value: 3 } ] }
162
+ chart_panel "Tall One", data: -> { [ { label: "Mon", value: 3 } ] }, height: 300
163
+ end
164
+ end
165
+ RUBY
166
+ get "/internal/admin_suite"
167
+ assert_response :success
168
+ document = Nokogiri::HTML(response.body)
169
+
170
+ default_chart = document.at_xpath("//h3[normalize-space()='Daily Cost']/ancestor::div[contains(@class, 'rounded-xl')]")
171
+ default_bars = default_chart.css("[data-admin-suite--chart-target='bars']").first
172
+ assert_equal "height: 192px;", default_bars["style"]
173
+ assert_includes default_chart.to_s, 'data-admin-suite--chart-height-value="192"'
174
+
175
+ tall_chart = document.at_xpath("//h3[normalize-space()='Tall One']/ancestor::div[contains(@class, 'rounded-xl')]")
176
+ tall_bars = tall_chart.css("[data-admin-suite--chart-target='bars']").first
177
+ assert_equal "height: 300px;", tall_bars["style"]
178
+ assert_includes tall_chart.to_s, 'data-admin-suite--chart-height-value="300"'
179
+ end
180
+ end
181
+
182
+ # Final review Finding 1: `chart_height.presence || 192` lets any value
183
+ # that survives `presence` (true, Symbols, non-empty Arrays/Hashes)
184
+ # through to `#to_i`, which none of those implement -- NoMethodError,
185
+ # 500ing the whole dashboard. Regression tests for the total-coercion fix
186
+ # (`Integer(Float(...))` rescuing ArgumentError/TypeError), mirroring the
187
+ # `type:` junk-value tests above.
188
+ #
189
+ # The Float::INFINITY/NAN cases come from the fix's own re-review: those
190
+ # parse as Floats but have no Integer form, so `Integer()` raises
191
+ # FloatDomainError -- a RangeError, which the first cut of the rescue did
192
+ # not name. They were the one junk value still 500ing the dashboard, and a
193
+ # host reaches them by computing a height from a ratio that divides by zero.
194
+ [ "true", ":tall", "[200]", "{ px: 200 }", '"abc"', "nil", "0", "-5",
195
+ "Float::INFINITY", "-Float::INFINITY", "Float::NAN" ].each do |literal|
196
+ test "height: #{literal} does not raise and falls back to the default 192px" do
197
+ with_dashboard(<<~RUBY) do
198
+ AdminSuite.root_dashboard do
199
+ row do
200
+ chart_panel "Junk Height", height: #{literal}, data: -> { [ { label: "Mon", value: 3 } ] }
201
+ end
202
+ end
203
+ RUBY
204
+ get "/internal/admin_suite"
205
+ assert_response :success
206
+ document = Nokogiri::HTML(response.body)
207
+ chart = document.at_xpath("//h3[normalize-space()='Junk Height']/ancestor::div[contains(@class, 'rounded-xl')]")
208
+ assert chart, "expected the chart panel card"
209
+ bars = chart.css("[data-admin-suite--chart-target='bars']").first
210
+ assert_equal "height: 192px;", bars["style"]
211
+ assert_includes chart.to_s, 'data-admin-suite--chart-height-value="192"'
212
+ end
213
+ end
214
+ end
215
+
216
+ [ [ '"240"', "240" ], [ "240.7", "240" ] ].each do |literal, expected|
217
+ test "height: #{literal} is coerced to a real pixel height instead of the default" do
218
+ with_dashboard(<<~RUBY) do
219
+ AdminSuite.root_dashboard do
220
+ row do
221
+ chart_panel "Parsed Height", height: #{literal}, data: -> { [ { label: "Mon", value: 3 } ] }
222
+ end
223
+ end
224
+ RUBY
225
+ get "/internal/admin_suite"
226
+ assert_response :success
227
+ document = Nokogiri::HTML(response.body)
228
+ chart = document.at_xpath("//h3[normalize-space()='Parsed Height']/ancestor::div[contains(@class, 'rounded-xl')]")
229
+ assert chart, "expected the chart panel card"
230
+ bars = chart.css("[data-admin-suite--chart-target='bars']").first
231
+ assert_equal "height: #{expected}px;", bars["style"]
232
+ assert_includes chart.to_s, %(data-admin-suite--chart-height-value="#{expected}")
233
+ end
234
+ end
235
+ end
236
+
237
+ # Final review Finding 2: `(panel.options[:color] || theme_primary).to_sym`
238
+ # raises `NoMethodError` for any `color:` value that doesn't implement
239
+ # `#to_sym` (e.g. an Integer or Boolean). Pre-existing since 0.4.0.
240
+ # Regression tests for the `.to_s.presence&.to_sym` fix -- unknown colors
241
+ # already fall through to indigo in both the ERB `case` and the JS
242
+ # `COLOR_HEX` map, so an unrecognized coerced value is expected to render
243
+ # the indigo bar, not raise.
244
+ test "a non-Symbol, non-String color: (an Integer) falls back to indigo instead of raising" do
245
+ with_dashboard(<<~RUBY) do
246
+ AdminSuite.root_dashboard do
247
+ row do
248
+ chart_panel "Integer Color", color: 42, data: -> { [ { label: "Mon", value: 3 } ] }
249
+ end
250
+ end
251
+ RUBY
252
+ get "/internal/admin_suite"
253
+ assert_response :success
254
+ document = Nokogiri::HTML(response.body)
255
+ chart = document.at_xpath("//h3[normalize-space()='Integer Color']/ancestor::div[contains(@class, 'rounded-xl')]")
256
+ assert chart, "expected the chart panel card"
257
+ assert_includes chart.to_s, 'data-admin-suite--chart-color-value="42"'
258
+ bar = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div").first
259
+ assert_includes bar["class"], "bg-indigo-500"
260
+ end
261
+ end
262
+
263
+ test "a Boolean color: (true) falls back to indigo instead of raising" do
264
+ with_dashboard(<<~RUBY) do
265
+ AdminSuite.root_dashboard do
266
+ row do
267
+ chart_panel "Boolean Color", color: true, data: -> { [ { label: "Mon", value: 3 } ] }
268
+ end
269
+ end
270
+ RUBY
271
+ get "/internal/admin_suite"
272
+ assert_response :success
273
+ document = Nokogiri::HTML(response.body)
274
+ chart = document.at_xpath("//h3[normalize-space()='Boolean Color']/ancestor::div[contains(@class, 'rounded-xl')]")
275
+ assert chart, "expected the chart panel card"
276
+ assert_includes chart.to_s, 'data-admin-suite--chart-color-value="true"'
277
+ bar = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div").first
278
+ assert_includes bar["class"], "bg-indigo-500"
279
+ end
280
+ end
281
+
282
+ test "a known color: as a Symbol still renders its matching bar color" do
283
+ with_dashboard(<<~RUBY) do
284
+ AdminSuite.root_dashboard do
285
+ row do
286
+ chart_panel "Amber Color", color: :amber, data: -> { [ { label: "Mon", value: 3 } ] }
287
+ end
288
+ end
289
+ RUBY
290
+ get "/internal/admin_suite"
291
+ assert_response :success
292
+ document = Nokogiri::HTML(response.body)
293
+ chart = document.at_xpath("//h3[normalize-space()='Amber Color']/ancestor::div[contains(@class, 'rounded-xl')]")
294
+ assert chart, "expected the chart panel card"
295
+ assert_includes chart.to_s, 'data-admin-suite--chart-color-value="amber"'
296
+ bar = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div").first
297
+ assert_includes bar["class"], "bg-amber-500"
298
+ end
299
+ end
300
+
301
+ test "chart assets load only on pages that render a chart" do
302
+ # Self-contained via `with_dashboard` (defined above in this file)
303
+ # rather than hitting a fixture route registered by a *different* test
304
+ # file (`/ops/read_only_widgets`, from read_only_resource_test.rb):
305
+ # that made this test pass in the full suite but 404 under
306
+ # `TEST=test/integration/chart_panel_test.rb` isolation, since that
307
+ # other file's resource/route never gets registered. A `stat_panel`
308
+ # renders no chart at all, so this is guaranteed chart-free regardless
309
+ # of what else has (or hasn't) been loaded.
310
+ with_dashboard(<<~RUBY) do
311
+ AdminSuite.root_dashboard do
312
+ row do
313
+ stat_panel "Active Users", 42
314
+ end
315
+ end
316
+ RUBY
317
+ get "/internal/admin_suite"
318
+ assert_response :success
319
+ refute_includes response.body, "vendor/chart"
320
+ end
321
+ end
322
+
323
+ test "chart_panel with no type: still renders exactly as a bar chart" do
324
+ with_dashboard(<<~RUBY) do
325
+ AdminSuite.root_dashboard do
326
+ row do
327
+ chart_panel "Bar Default", data: -> { [ { label: "Mon", value: 3 } ] }
328
+ end
329
+ end
330
+ RUBY
331
+ get "/internal/admin_suite"
332
+ assert_response :success
333
+ assert_includes response.body, 'data-admin-suite--chart-type-value="bar"'
334
+ document = Nokogiri::HTML(response.body)
335
+ chart = document.at_xpath("//h3[normalize-space()='Bar Default']/ancestor::div[contains(@class, 'rounded-xl')]")
336
+ assert chart, "expected the chart panel card"
337
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
338
+ assert_equal 1, bars.size
339
+ end
340
+ end
341
+
342
+ test "chart_panel type: :line serializes to the DOM and degrades to CSS bars" do
343
+ with_dashboard(<<~RUBY) do
344
+ AdminSuite.root_dashboard do
345
+ row do
346
+ chart_panel "Line Chart", type: :line, data: -> { [ { label: "Mon", value: 3 } ] }
347
+ end
348
+ end
349
+ RUBY
350
+ get "/internal/admin_suite"
351
+ assert_response :success
352
+ assert_includes response.body, 'data-admin-suite--chart-type-value="line"'
353
+ document = Nokogiri::HTML(response.body)
354
+ chart = document.at_xpath("//h3[normalize-space()='Line Chart']/ancestor::div[contains(@class, 'rounded-xl')]")
355
+ assert chart, "expected the chart panel card"
356
+ # line's degraded (no-JS) rendering is the same CSS bars as bar/area —
357
+ # a line chart with one data point has no sensible line-only degrade.
358
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
359
+ assert_equal 1, bars.size
360
+ end
361
+ end
362
+
363
+ test "chart_panel type: :area serializes to the DOM and degrades to CSS bars" do
364
+ with_dashboard(<<~RUBY) do
365
+ AdminSuite.root_dashboard do
366
+ row do
367
+ chart_panel "Area Chart", type: :area, data: -> { [ { label: "Mon", value: 3 } ] }
368
+ end
369
+ end
370
+ RUBY
371
+ get "/internal/admin_suite"
372
+ assert_response :success
373
+ assert_includes response.body, 'data-admin-suite--chart-type-value="area"'
374
+ document = Nokogiri::HTML(response.body)
375
+ chart = document.at_xpath("//h3[normalize-space()='Area Chart']/ancestor::div[contains(@class, 'rounded-xl')]")
376
+ assert chart, "expected the chart panel card"
377
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
378
+ assert_equal 1, bars.size
379
+ end
380
+ end
381
+
382
+ test "chart_panel type: :doughnut serializes to the DOM and degrades to a labelled value list, not bars" do
383
+ with_dashboard(<<~RUBY) do
384
+ AdminSuite.root_dashboard do
385
+ row do
386
+ chart_panel "Doughnut Chart", type: :doughnut, data: -> {
387
+ [ { label: "Mon", value: 3 }, { label: "Tue", value: 7 } ]
388
+ }
389
+ end
390
+ end
391
+ RUBY
392
+ get "/internal/admin_suite"
393
+ assert_response :success
394
+ assert_includes response.body, 'data-admin-suite--chart-type-value="doughnut"'
395
+ document = Nokogiri::HTML(response.body)
396
+ chart = document.at_xpath("//h3[normalize-space()='Doughnut Chart']/ancestor::div[contains(@class, 'rounded-xl')]")
397
+ assert chart, "expected the chart panel card"
398
+ # A stacked bar makes no sense for a doughnut's degraded state — assert
399
+ # there are no percentage-height bars, and instead a plain label/value
400
+ # list carries both rows' data.
401
+ assert_empty chart.css("[data-admin-suite--chart-target='bars']")
402
+ assert_includes chart.text, "Mon"
403
+ assert_includes chart.text, "3"
404
+ assert_includes chart.text, "Tue"
405
+ assert_includes chart.text, "7"
406
+ end
407
+ end
408
+
409
+ test "an unknown chart type falls back to :bar and does not raise" do
410
+ with_dashboard(<<~RUBY) do
411
+ AdminSuite.root_dashboard do
412
+ row do
413
+ chart_panel "Weird Chart", type: :bogus, data: -> { [ { label: "Mon", value: 3 } ] }
414
+ end
415
+ end
416
+ RUBY
417
+ get "/internal/admin_suite"
418
+ assert_response :success
419
+ assert_includes response.body, 'data-admin-suite--chart-type-value="bar"'
420
+ refute_includes response.body, 'data-admin-suite--chart-type-value="bogus"'
421
+ document = Nokogiri::HTML(response.body)
422
+ chart = document.at_xpath("//h3[normalize-space()='Weird Chart']/ancestor::div[contains(@class, 'rounded-xl')]")
423
+ assert chart, "expected the chart panel card"
424
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
425
+ assert_equal 1, bars.size
426
+ end
427
+ end
428
+
429
+ test "a non-Symbol, non-String type: (an Integer) falls back to :bar instead of raising" do
430
+ # `type: 42` used to reach `.presence&.to_sym` directly -- `presence`
431
+ # passes an Integer through unchanged, and Integer has no `#to_sym`,
432
+ # so this 500ed the whole dashboard rather than degrading. Regression
433
+ # test for that total-coercion fix.
434
+ with_dashboard(<<~RUBY) do
435
+ AdminSuite.root_dashboard do
436
+ row do
437
+ chart_panel "Integer Type", type: 42, data: -> { [ { label: "Mon", value: 3 } ] }
438
+ end
439
+ end
440
+ RUBY
441
+ get "/internal/admin_suite"
442
+ assert_response :success
443
+ assert_includes response.body, 'data-admin-suite--chart-type-value="bar"'
444
+ document = Nokogiri::HTML(response.body)
445
+ chart = document.at_xpath("//h3[normalize-space()='Integer Type']/ancestor::div[contains(@class, 'rounded-xl')]")
446
+ assert chart, "expected the chart panel card"
447
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
448
+ assert_equal 1, bars.size
449
+ end
450
+ end
451
+
452
+ test "a Boolean type: (true) falls back to :bar instead of raising" do
453
+ with_dashboard(<<~RUBY) do
454
+ AdminSuite.root_dashboard do
455
+ row do
456
+ chart_panel "Boolean Type", type: true, data: -> { [ { label: "Mon", value: 3 } ] }
457
+ end
458
+ end
459
+ RUBY
460
+ get "/internal/admin_suite"
461
+ assert_response :success
462
+ assert_includes response.body, 'data-admin-suite--chart-type-value="bar"'
463
+ document = Nokogiri::HTML(response.body)
464
+ chart = document.at_xpath("//h3[normalize-space()='Boolean Type']/ancestor::div[contains(@class, 'rounded-xl')]")
465
+ assert chart, "expected the chart panel card"
466
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
467
+ assert_equal 1, bars.size
468
+ end
469
+ end
470
+
471
+ test "a valid type: given as a String (not a Symbol) is honored, not just tolerated" do
472
+ with_dashboard(<<~RUBY) do
473
+ AdminSuite.root_dashboard do
474
+ row do
475
+ chart_panel "String Type", type: "line", data: -> { [ { label: "Mon", value: 3 } ] }
476
+ end
477
+ end
478
+ RUBY
479
+ get "/internal/admin_suite"
480
+ assert_response :success
481
+ assert_includes response.body, 'data-admin-suite--chart-type-value="line"'
482
+ document = Nokogiri::HTML(response.body)
483
+ chart = document.at_xpath("//h3[normalize-space()='String Type']/ancestor::div[contains(@class, 'rounded-xl')]")
484
+ assert chart, "expected the chart panel card"
485
+ # "line" still degrades to CSS bars, same as the Symbol form.
486
+ bars = chart.css("[data-admin-suite--chart-target='bars'] > .h-full > div")
487
+ assert_equal 1, bars.size
488
+ end
489
+ end
490
+ end
491
+ end
@@ -96,8 +96,15 @@ module AdminSuite
96
96
  document = Nokogiri::HTML(response.body)
97
97
  chart = document.at_xpath("//h3[normalize-space()='Daily Cost']/ancestor::div[contains(@class, 'rounded-xl')]")
98
98
  assert chart
99
- assert_equal 2, chart.css(".h-16 > .h-full").size
100
- assert_equal ["height: 2%", "height: 100%"], chart.css(".h-16 > .h-full > div").map { |bar| bar["style"] }
99
+ # The bars container carries an explicit inline height (not a
100
+ # Tailwind class) so the degraded CSS bars and the upgraded canvas
101
+ # share an identical, non-content-derived height — see
102
+ # app/views/admin_suite/panels/_chart.html.erb.
103
+ bars = chart.css("[data-admin-suite--chart-target='bars']").first
104
+ assert bars, "expected the bars wrapper"
105
+ assert_equal "height: 192px;", bars["style"], "bars wrapper must carry the default definite chart height"
106
+ assert_equal 2, bars.css(".h-full").size
107
+ assert_equal ["height: 2%", "height: 100%"], bars.css(".h-full > div").map { |bar| bar["style"] }
101
108
  assert_includes chart.text, "Jul 30"
102
109
  assert_includes chart.text, "Thursday"
103
110
  end