plutonium 0.46.0 → 0.48.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +4 -0
  3. data/.claude/skills/plutonium-interaction/SKILL.md +23 -0
  4. data/.claude/skills/plutonium-nested-resources/SKILL.md +10 -0
  5. data/.claude/skills/plutonium-testing/SKILL.md +268 -0
  6. data/.yarnrc.yml +1 -0
  7. data/CHANGELOG.md +23 -0
  8. data/Rakefile +10 -1
  9. data/app/assets/plutonium.css +1 -1
  10. data/docs/.vitepress/config.ts +6 -0
  11. data/docs/guides/nested-resources.md +10 -0
  12. data/docs/guides/testing.md +154 -0
  13. data/docs/reference/controller/index.md +9 -4
  14. data/docs/superpowers/plans/2026-04-14-plutonium-testing.md +2046 -0
  15. data/docs/superpowers/plans/2026-04-14-plutonium-testing.md.tasks.json +21 -0
  16. data/docs/superpowers/specs/2026-04-14-plutonium-testing-design.md +364 -0
  17. data/gemfiles/rails_8.1.gemfile.lock +27 -1
  18. data/lib/generators/pu/test/install/install_generator.rb +34 -0
  19. data/lib/generators/pu/test/install/templates/plutonium_testing.rb.tt +14 -0
  20. data/lib/generators/pu/test/scaffold/scaffold_generator.rb +55 -0
  21. data/lib/generators/pu/test/scaffold/templates/integration_test.rb.tt +65 -0
  22. data/lib/plutonium/action/interactive.rb +2 -1
  23. data/lib/plutonium/core/controller.rb +18 -1
  24. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +20 -1
  25. data/lib/plutonium/testing/auth_helpers.rb +62 -0
  26. data/lib/plutonium/testing/dsl.rb +73 -0
  27. data/lib/plutonium/testing/nested_resource.rb +58 -0
  28. data/lib/plutonium/testing/portal_access.rb +49 -0
  29. data/lib/plutonium/testing/resource_crud.rb +104 -0
  30. data/lib/plutonium/testing/resource_definition.rb +61 -0
  31. data/lib/plutonium/testing/resource_interaction.rb +51 -0
  32. data/lib/plutonium/testing/resource_model.rb +53 -0
  33. data/lib/plutonium/testing/resource_policy.rb +72 -0
  34. data/lib/plutonium/testing.rb +16 -0
  35. data/lib/plutonium/ui/action_button.rb +1 -1
  36. data/lib/plutonium/version.rb +1 -1
  37. data/lib/plutonium.rb +2 -0
  38. data/package.json +1 -1
  39. data/plutonium.gemspec +2 -0
  40. data/yarn.lock +6037 -3893
  41. metadata +50 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f603d6d8cf1a941e6880307ac384bf03c4d81a90a3c71fe6be6b2397a509de93
4
- data.tar.gz: 58cf196911c99bb2d91d06f784a6336c3fbf7371189faa9d626eab2b5db4e11e
3
+ metadata.gz: d07d40ddf359fb63528f92bae1e1c10c0ec19d4d8101a7202e1c7ae30a798e90
4
+ data.tar.gz: 30fe5b1d2c89bd08552b3d64eb84795bc12e5417805b7ffd6c18f84f5f05d8d1
5
5
  SHA512:
6
- metadata.gz: 5d9af8748fda86912aad6a2a64deeb9536ad17d08cae17a7fe8d61ab72e8f19c2c0668851e52ba1288a386dbb71eb4e0c2075f8af6955e4b5a491abaa25701d4
7
- data.tar.gz: f99fdb5497ef022daeab3147d556aa6e37ca5c4bb2812a4660afbe449b6666474fd36d1db70f36912778b85f156ec20984bf9a1d02ef6be7357c89b60cb2f46a
6
+ metadata.gz: f5652a759d23236d48265376846ed7269a34ab6a7796bb40c6cc2331a7be308a8de472cda0405e7453ce52ae6452bcb291aaf2c784d305a9dbec451d1be3efa8
7
+ data.tar.gz: 3aee38cfc32329df17558559fcda9f1cae1c0605dc8c5a70d3e8ccd74ec5a4de9a04082e95b7368a07e812fe9a1b7d59e89405077e1dfb8210241710f59b3397
@@ -61,6 +61,7 @@ Optional additions when relevant:
61
61
  | Create a portal or feature package | `plutonium-portal` / `plutonium-package` |
62
62
  | Mount a portal, configure entity strategies, route portal resources | `plutonium-portal` (+ `plutonium-entity-scoping` for tenancy) |
63
63
  | Install Plutonium in a Rails app | `plutonium-installation` |
64
+ | Write tests for a resource, run `pu:test:scaffold`, or include `Plutonium::Testing::*` concerns | `plutonium-testing` |
64
65
 
65
66
  ## Generator catalog
66
67
 
@@ -87,6 +88,8 @@ Every Plutonium generator is discoverable via `rails g pu:<tab>`. Always pass `-
87
88
  | `pu:field:renderer NAME` | Custom display renderer | `plutonium-definition` |
88
89
  | `pu:eject:layout` | Eject the base layout for customization | `plutonium-views` |
89
90
  | `pu:skills:sync` | Sync Plutonium Claude skills into the project | `plutonium` |
91
+ | `pu:test:install` | Install Plutonium::Testing scaffolding | `plutonium-testing` |
92
+ | `pu:test:scaffold NAME --portals=...` | Scaffold integration tests per (resource × portal) | `plutonium-testing` |
90
93
 
91
94
  ## Resource architecture at a glance
92
95
 
@@ -144,3 +147,4 @@ Meta-generators (`pu:saas:setup`) propagate these flags to the generators they c
144
147
  - `plutonium-installation` · `plutonium-create-resource` · `plutonium-model` · `plutonium-policy` · `plutonium-entity-scoping` · `plutonium-portal` · `plutonium-definition`
145
148
  - `plutonium-controller` · `plutonium-interaction` · `plutonium-views` · `plutonium-forms` · `plutonium-assets`
146
149
  - `plutonium-auth` · `plutonium-invites` · `plutonium-package` · `plutonium-nested-resources`
150
+ - `plutonium-testing` — default test concerns and scaffolding
@@ -373,6 +373,29 @@ class Company::InviteUserInteraction < Plutonium::Resource::Interaction
373
373
  end
374
374
  ```
375
375
 
376
+ ## Generating Interaction URLs
377
+
378
+ Use the `interaction:` kwarg on `resource_url_for`. The action type (record/bulk/resource) is inferred from the element and presence of `ids:`:
379
+
380
+ ```ruby
381
+ # Record action — instance argument
382
+ resource_url_for(@post, interaction: :publish)
383
+ # => /posts/:id/record_actions/publish
384
+
385
+ # Resource (class-level) action — class with no ids
386
+ resource_url_for(Post, interaction: :import)
387
+ # => /posts/resource_actions/import
388
+
389
+ # Bulk action — class + ids
390
+ resource_url_for(Post, interaction: :archive, ids: [1, 2, 3])
391
+ # => /posts/bulk_actions/archive?ids[]=1&ids[]=2&ids[]=3
392
+
393
+ # Composes with parent / entity scoping
394
+ resource_url_for(@post, parent: @user, interaction: :publish)
395
+ ```
396
+
397
+ The same URL serves both GET (form/confirmation) and POST (commit) — the verb determines which controller action runs. Passing both `interaction:` and `action:` raises `ArgumentError`.
398
+
376
399
  ## Best Practices
377
400
 
378
401
  1. **Keep interactions focused** - One action per interaction
@@ -178,6 +178,16 @@ resource_url_for(company_profile, parent: company)
178
178
 
179
179
  resource_url_for(CompanyProfile, action: :new, parent: company)
180
180
  # => /companies/123/nested_company_profile/new
181
+
182
+ # Interactions (composes with parent — see plutonium-interaction skill)
183
+ resource_url_for(property, parent: company, interaction: :archive)
184
+ # => /companies/123/nested_properties/456/record_actions/archive
185
+
186
+ resource_url_for(Property, parent: company, interaction: :import)
187
+ # => /companies/123/nested_properties/resource_actions/import
188
+
189
+ resource_url_for(Property, parent: company, interaction: :bulk_delete, ids: [1, 2])
190
+ # => /companies/123/nested_properties/bulk_actions/bulk_delete?ids[]=1&ids[]=2
181
191
  ```
182
192
 
183
193
  ### Cross-Package URL Generation
@@ -0,0 +1,268 @@
1
+ ---
2
+ name: plutonium-testing
3
+ description: Use BEFORE writing tests for a Plutonium resource, running pu:test:scaffold, or including Plutonium::Testing::* concerns. Covers the full testing toolkit — CRUD, policy, definition, interaction, model, nested, portal access, and auth helpers.
4
+ ---
5
+
6
+ # Plutonium Testing
7
+
8
+ ## 🚨 Critical (read first)
9
+
10
+ - **Use the generators.** `pu:test:install` once per app, then `pu:test:scaffold ResourceClass --portals=...` per resource × portal. Hand-written test files drift from conventions.
11
+ - **Tests are opt-in.** `Plutonium::Testing` is only loaded when `require "plutonium/testing"` runs — it's never autoloaded, never present in production.
12
+ - **One file per (resource × portal).** Same model in admin and org portals = two test files. Each portal has different auth, scoping, and allowed actions.
13
+ - **Stub methods are required.** Concerns ship with `NotImplementedError` stubs — your test class supplies the test data via `create_resource!`, `valid_create_params`, `policy_roles`, etc.
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # Once per app
19
+ rails g pu:test:install
20
+
21
+ # Per resource × portal pairing
22
+ rails g pu:test:scaffold Blogging::Post --portals=admin,org
23
+
24
+ # Run
25
+ bin/rails test
26
+ ```
27
+
28
+ `pu:test:install` adds `require "plutonium/testing"` to `test/test_helper.rb` and creates `test/support/plutonium_testing.rb` (a stub for non-Rodauth auth overrides).
29
+
30
+ ## DSL reference
31
+
32
+ Every concern uses the same class-level DSL:
33
+
34
+ ```ruby
35
+ resource_tests_for ResourceClass,
36
+ portal: :admin, # required
37
+ path_prefix: "/admin", # optional override
38
+ parent: :organization, # for nested resources
39
+ actions: %i[index show new create edit update destroy],
40
+ skip: %i[destroy],
41
+ associated_with: :organization, # ResourceModel only
42
+ sgid_routing: true, # ResourceModel only
43
+ has_cents: %i[price] # ResourceModel only
44
+ ```
45
+
46
+ The **portal symbol** drives:
47
+
48
+ | Derived | `:admin` example | `:org` example |
49
+ |---|---|---|
50
+ | `path_prefix` | `/admin` | `/org` |
51
+ | Default sign-in helper | admin Rodauth | user Rodauth |
52
+ | Allowed action set | from definition | from definition |
53
+
54
+ `path_prefix` is auto-resolved from the mounted portal engine. For mounts inside `constraints` (typical Plutonium setup), the resolver walks the route tree and finds the engine.
55
+
56
+ ## Concerns catalog
57
+
58
+ Each concern is `include`d separately. Pick the ones you need.
59
+
60
+ ### `Plutonium::Testing::ResourceCrud`
61
+
62
+ Generates index / show / new / create / edit / update / destroy integration tests against the portal-mounted resource.
63
+
64
+ **Stubs:**
65
+ - `create_resource!` → persisted record
66
+ - `valid_create_params` → Hash for POST
67
+ - `valid_update_params` → Hash for PATCH
68
+
69
+ ```ruby
70
+ class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
71
+ include IntegrationTestHelper
72
+ include Plutonium::Testing::ResourceCrud
73
+
74
+ resource_tests_for Blogging::Post, portal: :admin
75
+
76
+ setup do
77
+ @admin = create_admin!
78
+ @user = create_user!
79
+ @org = create_organization!
80
+ login_as(@admin)
81
+ end
82
+
83
+ def create_resource! = create_post!(user: @user, organization: @org)
84
+ def valid_create_params
85
+ {title: "x", body: "y", status: :draft, user: @user.to_sgid.to_s, organization: @org.to_sgid.to_s}
86
+ end
87
+ def valid_update_params = {title: "Updated"}
88
+ end
89
+ ```
90
+
91
+ ### `Plutonium::Testing::ResourcePolicy`
92
+
93
+ Asserts the `permit?` matrix across action × role and verifies `relation_scope` returns an `ActiveRecord::Relation`.
94
+
95
+ **Stubs:**
96
+ - `policy_roles` → `{role_sym => -> { account }}`
97
+ - `policy_record` → persisted record under test
98
+ - `policy_matrix` → `{action_sym => [allowed_role_syms]}`
99
+ - `policy_context` (optional) → extra kwargs (defaults to `{entity_scope: nil}`)
100
+
101
+ ```ruby
102
+ def policy_roles = {admin: -> { @admin }, member: -> { @user }}
103
+ def policy_record = create_post!(user: @user, organization: @org)
104
+ def policy_matrix = {
105
+ index: %i[admin member], show: %i[admin member],
106
+ create: %i[admin], update: %i[admin], destroy: %i[admin]
107
+ }
108
+ ```
109
+
110
+ ### `Plutonium::Testing::ResourceDefinition`
111
+
112
+ Smoke-tests the resource definition: the class is constantize-able, every defineable prop dictionary (fields/inputs/displays/columns/scopes/filters/sorts/actions) is queryable, and declared fields exist on the model.
113
+
114
+ **No stubs required** for the happy path.
115
+
116
+ ### `Plutonium::Testing::ResourceInteraction`
117
+
118
+ Outcome-assertion helpers for `Plutonium::Interaction::Base` subclasses.
119
+
120
+ **Helpers:**
121
+ - `assert_interaction_success(klass, **input)` → returns the success outcome
122
+ - `assert_interaction_failure(klass, **input)` → returns the failure outcome
123
+ - `interaction_view_context` (overridable) → defaults to a mock view context
124
+
125
+ ```ruby
126
+ test "RebuildSearchInteraction succeeds" do
127
+ outcome = assert_interaction_success(RebuildSearchInteraction, since: 1.day.ago)
128
+ assert_equal 42, outcome.value[:rebuilt_count]
129
+ end
130
+ ```
131
+
132
+ ### `Plutonium::Testing::ResourceModel`
133
+
134
+ Tests `associated_with` scope, SGID routing, and `has_cents` accessors — gated by DSL flags.
135
+
136
+ **Stubs:**
137
+ - `model_test_record` → persisted record
138
+
139
+ ```ruby
140
+ resource_tests_for Catalog::Product, portal: :admin,
141
+ associated_with: :organization,
142
+ sgid_routing: true,
143
+ has_cents: %i[price]
144
+
145
+ def model_test_record = create_product!(user: @user, organization: @org)
146
+ ```
147
+
148
+ Only the flagged features generate tests.
149
+
150
+ ### `Plutonium::Testing::NestedResource`
151
+
152
+ Asserts CRUD under a parent + scope-boundary tests (sibling tenants invisible).
153
+
154
+ **Stubs:**
155
+ - `parent_record!` → current tenant
156
+ - `other_parent_record!` → sibling tenant
157
+ - `create_resource!(parent:)` → persisted record under given parent
158
+
159
+ ### `Plutonium::Testing::PortalAccess`
160
+
161
+ Cross-portal access boundaries. Uses its own DSL — not `resource_tests_for`.
162
+
163
+ ```ruby
164
+ class PortalAccessTest < ActionDispatch::IntegrationTest
165
+ include IntegrationTestHelper
166
+ include Plutonium::Testing::PortalAccess
167
+
168
+ portal_access_for portals: %i[admin org],
169
+ matrix: {admin: %i[admin], member: %i[org]}
170
+
171
+ setup do
172
+ @admin = create_admin!
173
+ @user = create_user!
174
+ @org = create_organization!
175
+ create_membership!(organization: @org, user: @user)
176
+ end
177
+
178
+ def login_as_role(role)
179
+ case role
180
+ when :admin then login_as(@admin, portal: :admin)
181
+ when :member then login_as(@user, portal: :user)
182
+ end
183
+ end
184
+
185
+ def portal_root_path(portal)
186
+ case portal
187
+ when :admin then "/admin"
188
+ when :org then "/org/#{@org.id}"
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ Generates one test per (role × portal). Allowed = `200|302`; blocked = `302|401|403|404`.
195
+
196
+ ## Auth helpers
197
+
198
+ `Plutonium::Testing::AuthHelpers` is included transitively by every concern.
199
+
200
+ ```ruby
201
+ login_as(account) # uses portal from DSL
202
+ login_as(account, portal: :admin) # explicit override
203
+ sign_out # uses portal from DSL
204
+ sign_out(portal: :admin)
205
+ current_account # uses portal from DSL
206
+ current_account(portal: :admin)
207
+ with_portal(:org) { ... } # scoped portal switch
208
+ ```
209
+
210
+ **Override hook for non-Rodauth apps:** define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use). `AuthHelpers` will defer to it.
211
+
212
+ ```ruby
213
+ def sign_in_for_tests(account, portal:)
214
+ # your custom auth flow here
215
+ end
216
+ ```
217
+
218
+ ## Generator reference
219
+
220
+ ### `pu:test:install`
221
+
222
+ ```bash
223
+ rails g pu:test:install
224
+ ```
225
+
226
+ - Adds `require "plutonium/testing"` to `test/test_helper.rb` (idempotent)
227
+ - Creates `test/support/plutonium_testing.rb` with override stub
228
+
229
+ ### `pu:test:scaffold`
230
+
231
+ ```bash
232
+ rails g pu:test:scaffold Blogging::Post --portals=admin,org
233
+ rails g pu:test:scaffold Blogging::Post --portals=admin --concerns=crud,policy,definition
234
+ rails g pu:test:scaffold Blogging::Post --portals=org --parent=organization --dest=blogging
235
+ ```
236
+
237
+ | Flag | Default | Purpose |
238
+ |---|---|---|
239
+ | `--portals=admin,org` | required | Emit one file per portal |
240
+ | `--concerns=...` | `crud,policy,definition` | Concerns to include (`crud,policy,definition,nested,model,interaction,portal_access`) |
241
+ | `--parent=organization` | none | Wires `NestedResource` parent |
242
+ | `--dest=main_app\|<package>` | `main_app` | Output destination |
243
+
244
+ Output path: `test/integration/<portal>_portal/<resource_underscored>_test.rb`.
245
+
246
+ ## Customization & escape hatches
247
+
248
+ - **Skip individual tests:** `resource_tests_for Klass, portal: :admin, skip: %i[destroy]`
249
+ - **Restrict action set:** `resource_tests_for Klass, portal: :admin, actions: %i[index show]`
250
+ - **Custom assertions:** add regular `test "..."` blocks alongside the generated matrix — they coexist.
251
+ - **Non-Rodauth auth:** override `sign_in_for_tests`. See AuthHelpers section.
252
+ - **Custom path prefix:** `path_prefix: "/v2/admin"` overrides portal resolution.
253
+
254
+ ## Common pitfalls
255
+
256
+ - **Forgotten stubs raise `NotImplementedError`** with the stub name. Look for the missing method in your test class.
257
+ - **Portal mismatch:** `:admin` portal expects `AdminPortal::Engine` constant. If your portal is named differently, pass `path_prefix:` explicitly.
258
+ - **Tenant leakage in stubs:** `create_resource!` for an org portal must return a record bound to the test's `@org`. Otherwise scope filtering tests will pass for the wrong reason.
259
+ - **`policy_record` for tenant-scoped resources** must belong to a tenant the role has access to — otherwise even allowed roles will see `false`.
260
+ - **Nested resources need `parent: :foo`** in the DSL AND a real parent record from `parent_record!`. Without both, path interpolation fails.
261
+ - **`PortalAccess` doesn't use `resource_tests_for`** — use `portal_access_for` instead. Mixing them on the same class is undefined behavior.
262
+
263
+ ## See also
264
+
265
+ - `plutonium-policy` — write the policy this concern verifies
266
+ - `plutonium-definition` — definition props the smoke test introspects
267
+ - `plutonium-portal` — portal mounting and entity strategies that drive auth/scoping
268
+ - `plutonium-auth` — Rodauth setup behind the default login flow
data/.yarnrc.yml ADDED
@@ -0,0 +1 @@
1
+ nodeLinker: node-modules
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## [0.48.0] - 2026-04-16
2
+
3
+ ### 🚀 Features
4
+
5
+ - *(turbo)* Preserve scroll by emitting refresh when redirect target matches referer
6
+
7
+ ### 🐛 Bug Fixes
8
+
9
+ - *(action)* Respect `confirmation: false` on interactive actions
10
+
11
+ ### 🧪 Testing
12
+
13
+ - *(system)* Browser coverage for Turbo refresh + scroll preservation
14
+ ## [0.47.0] - 2026-04-15
15
+
16
+ ### 🚀 Features
17
+
18
+ - *(core)* Add `interaction:` kwarg to resource_url_for
19
+ - *(testing)* Add Plutonium::Testing module, generators, skill, docs, and migrate in-repo tests
20
+
21
+ ### ⚙️ Miscellaneous Tasks
22
+
23
+ - Update yarn
1
24
  ## [0.46.0] - 2026-04-11
2
25
 
3
26
  ### 🚀 Features
data/Rakefile CHANGED
@@ -17,7 +17,16 @@ Rake::Task["build"].enhance ["assets"]
17
17
  # Unit + integration tests (safe to run together)
18
18
  Rake::TestTask.new(:test) do |t|
19
19
  t.libs << "test"
20
- t.test_files = FileList["test/**/*_test.rb"].exclude("test/generators/**/*_test.rb")
20
+ t.test_files = FileList["test/**/*_test.rb"]
21
+ .exclude("test/generators/**/*_test.rb")
22
+ .exclude("test/system/**/*_test.rb")
23
+ t.verbose = true
24
+ end
25
+
26
+ # System tests — require a browser (headless Chrome) and run real Turbo/JS.
27
+ Rake::TestTask.new("test:system") do |t|
28
+ t.libs << "test"
29
+ t.test_files = FileList["test/system/**/*_test.rb"]
21
30
  t.verbose = true
22
31
  end
23
32