@spree/docs 0.1.240 → 0.1.242

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 (28) hide show
  1. package/dist/api-reference/store-api/metadata.md +6 -6
  2. package/dist/developer/core-concepts/architecture.md +15 -19
  3. package/dist/developer/core-concepts/carts.md +71 -1
  4. package/dist/developer/core-concepts/delivery-setup.md +3 -1
  5. package/dist/developer/core-concepts/freight.md +279 -0
  6. package/dist/developer/core-concepts/metafields.md +0 -2
  7. package/dist/developer/core-concepts/payments.md +123 -72
  8. package/dist/developer/core-concepts/sellers.md +50 -5
  9. package/dist/developer/create-spree-app/quickstart.md +22 -19
  10. package/dist/developer/customization/api.md +2 -2
  11. package/dist/developer/customization/checkout.md +117 -0
  12. package/dist/developer/customization/configuration.md +76 -175
  13. package/dist/developer/customization/decorators.md +120 -130
  14. package/dist/developer/customization/metadata.md +9 -9
  15. package/dist/developer/customization/model-preferences.md +34 -68
  16. package/dist/developer/customization/permissions.md +13 -28
  17. package/dist/developer/customization/validations.md +16 -43
  18. package/dist/developer/getting-started/quickstart.md +25 -24
  19. package/dist/developer/how-to/custom-promotion.md +1 -1
  20. package/dist/developer/how-to/custom-search-provider.md +2 -2
  21. package/dist/developer/how-to/custom-stock-splitter.md +2 -2
  22. package/dist/developer/sdk/admin/extending.md +1 -1
  23. package/dist/developer/sdk/admin/quickstart.md +1 -1
  24. package/dist/developer/sdk/admin/resources.md +1 -1
  25. package/dist/developer/sdk/configuration.md +1 -1
  26. package/dist/developer/sdk/extending.md +1 -1
  27. package/dist/developer/tutorial/testing.md +45 -91
  28. package/package.json +1 -1
@@ -11,15 +11,17 @@ Every grantable capability is a flat key of the form `read_<resource>` or `write
11
11
 
12
12
  Keys are per resource, not per action. The money-adjacent areas (`payments`, `refunds`, `gift_cards`, `store_credits`) are separate resources from `orders`, so "view orders but don't refund" needs no special casing.
13
13
 
14
- Roles as code is plain ActiveRecord:
15
-
16
- ```ruby
17
- # db/seeds.rb
18
- Spree::Role.find_or_create_by!(name: 'support')
19
- .update!(permissions: %w[read_orders read_customers], description: 'Read-only support')
14
+ Roles are data, so create them where you manage the rest of your store's data: **Settings → Staff → Roles** in the dashboard, or the Admin API when you want it scripted.
15
+
16
+ ```bash
17
+ spree api post roles --data '{
18
+ "name": "Support",
19
+ "description": "Read-only support",
20
+ "permissions": ["read_orders", "read_customers"]
21
+ }'
20
22
  ```
21
23
 
22
- A role with `mutable: false` (set from seeds or the console) renders read-only in the dashboard — for roles the application does not want store staff to edit. The `admin` role is always protected.
24
+ A role marked immutable renders read-only in the dashboard, for roles the application does not want store staff editing. The `admin` role is always protected.
23
25
 
24
26
  ## Registering extension permissions
25
27
 
@@ -56,30 +58,13 @@ Naming another audience exposes the scope to a principal outside the store's own
56
58
 
57
59
  A key never decides *which rows* its holder reaches. That is the controller's job, through scope fetching — a seller holding `write_products` manages their own products because the seller API reads through `current_seller`, not because the key says so.
58
60
 
59
- ## Beyond the catalog
61
+ ## What the catalog does not cover
60
62
 
61
- The catalog is deliberately flat: it answers "may this role touch this kind of record", and that is the whole grant vocabulary. Two rules that do *not* belong in it:
63
+ The catalog is deliberately flat: it answers "may this role touch this kind of record", and that is the whole grant vocabulary.
62
64
 
63
- - **Record-state rules** ("a completed order cannot be deleted") live on models and workflows, so they bind every caller — including secret API keys, which never consult CanCanCan.
64
- - **Row-level restrictions** (a support agent limited to one market) are not expressible as keys. Replace the ability class outright:
65
+ **Record-state rules** — "a completed order cannot be deleted" — belong on models and workflows, not on roles. Put them there and they bind every caller, including secret API keys, which never consult the role system at all. A rule enforced only in the permission layer is a rule the API can walk straight past.
65
66
 
66
- ```ruby
67
- # config/initializers/spree.rb
68
- Spree::Dependencies.ability_class = 'MyApp::Ability'
69
- ```
70
-
71
- ```ruby
72
- # app/models/my_app/ability.rb
73
- class MyApp::Ability < Spree::Ability
74
- def initialize(user, options = {})
75
- super
76
- return unless user.respond_to?(:market_id) && user.market_id
77
-
78
- cannot :manage, Spree::Order
79
- can [:read, :admin], Spree::Order, market_id: user.market_id
80
- end
81
- end
82
- ```
67
+ **Row-level restrictions**, such as a support agent limited to one market, are not expressible as permission keys. Scope them where the records are read: a controller reaching through `current_store.orders` rather than `Spree::Order` already restricts by tenant, and the same technique narrows by market or seller. See [extending the API](api.md).
83
68
 
84
69
  ## How enforcement works
85
70
 
@@ -52,27 +52,6 @@ Both cart keys appear above because adding to a cart and setting a quantity are
52
52
  different flows. See [Services & Workflows](workflows.md)
53
53
  for the full hook list.
54
54
 
55
- ## Adding a rule to a model
56
-
57
- For rules about the shape of a record, a decorator is the supported route:
58
-
59
- ```ruby
60
- # app/models/spree/product_decorator.rb
61
- module MyStore
62
- module ProductDecorator
63
- def self.prepended(base)
64
- base.validates :external_id, presence: true, uniqueness: true
65
- end
66
- end
67
- end
68
-
69
- Spree::Product.prepend(MyStore::ProductDecorator)
70
- ```
71
-
72
- Decorators couple you to Spree internals, so prefer a hook when the rule is
73
- really about an operation. See
74
- [Decorators](decorators.md).
75
-
76
55
  ## Relaxing a rule Spree ships
77
56
 
78
57
  Never call `clear_validators!` — it wipes every validation on the model,
@@ -81,7 +60,8 @@ cause. Use one of these instead.
81
60
 
82
61
  ### Store preferences
83
62
 
84
- Some rules are already switchable, per store, from the dashboard or a console:
63
+ Some rules are switchable per store, with no code at all. Change them in the
64
+ dashboard under **Settings → Store**, or through the Admin API:
85
65
 
86
66
  | Preference | Effect |
87
67
  |---|---|
@@ -90,30 +70,23 @@ Some rules are already switchable, per store, from the dashboard or a console:
90
70
  | `address_requires_company` | Requires it — only meaningful with the field shown |
91
71
  | `disable_sku_validation` | Turns off SKU uniqueness |
92
72
 
93
- ```ruby
94
- store.update!(preferred_address_requires_company: true)
95
- ```
96
-
97
- ### Overriding the gating predicate
98
73
 
99
- Most conditional validations in `Spree::Address` are gated on a predicate you
100
- can override, which is far safer than removing the validation:
101
-
102
- ```ruby
103
- module MyStore
104
- module AddressDecorator
105
- # Business addresses only in this store.
106
- def require_company?
107
- !quick_checkout
108
- end
109
- end
110
- end
74
+ ```typescript Admin SDK
75
+ await adminClient.store.update({
76
+ preferred_address_requires_phone: false,
77
+ })
78
+ ```
111
79
 
112
- Spree::Address.prepend(MyStore::AddressDecorator)
80
+ ```bash cURL
81
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
82
+ -H 'X-Spree-API-Key: sk_xxx' \
83
+ -H 'Content-Type: application/json' \
84
+ -d '{ "preferred_address_requires_phone": false }'
113
85
  ```
114
86
 
115
- `require_name?`, `require_street?`, `require_postal_code?`, `require_phone?` and
116
- `require_company?` all work this way.
87
+
88
+ > **NOTE:** All four are editable in the dashboard except `address_requires_company`, which
89
+ > is API-only for now.
117
90
 
118
91
  ### The address validator registry
119
92
 
@@ -121,7 +94,7 @@ Address rules are regional and business-specific, so addresses carry a registry
121
94
  of extra validator classes you can add to and remove from:
122
95
 
123
96
  ```ruby
124
- # config/initializers/spree.rb — or config.to_prepare for your own classes
97
+ # server/config/initializers/spree.rb — or config.to_prepare for your own classes
125
98
  Spree.validators.addresses.register(MyStore::PostBoxValidator)
126
99
 
127
100
  # Drop a rule Spree ships
@@ -6,35 +6,39 @@ description: "Install Spree Commerce locally, run your first store, and deploy i
6
6
 
7
7
  ## Using `create-spree-app`
8
8
 
9
- The quickest way to get started. Scaffolds a full Spree project with Next.js Storefront (optional).
9
+ The quickest way to get started. Scaffolds a full Spree project with React admin dashboard and Next.js Storefront (optional).
10
10
 
11
- **Requirements:** [Node.js](https://nodejs.org/) 20+ and [Docker](https://docs.docker.com/get-docker/) running.
11
+ **Requirements:** [Node.js](https://nodejs.org/) 24+ and [Docker](https://docs.docker.com/get-docker/) running.
12
12
 
13
13
  ```bash
14
14
  npx create-spree-app@latest my-store
15
15
  ```
16
16
 
17
- The CLI walks you through an interactive setup:
17
+ The CLI walks you through an interactive setup where you choose the project name and whether to include the Next.js Storefront. It then builds a Docker stack with Postgres, and Spree API.
18
18
 
19
- 1. Choose **Full-stack** (Backend + Next.js Storefront) or **Backend only**
20
- 2. Optionally load **sample data** (products, categories, images)
19
+ Once complete it will open a browser window so you can finalize the store setup (pick name, currency, your email/password).
21
20
 
22
- Once complete, your store should be running at [http://localhost:3000/admin](http://localhost:3000/admin). The port can be different if `3000` is already used on your system.
21
+ <video autoPlay muted loop playsInline controls className="block w-auto max-w-full h-auto rounded-xl" src="/videos/store-setup.mp4"></video>
23
22
 
24
- See the full [create-spree-app documentation](../create-spree-app/quickstart.md) for all CLI flags and options.
23
+ ## Project structure and overview
25
24
 
26
- ## Accessing Admin Panel
25
+ `create-spree-app` creates a project folder like this:
27
26
 
28
- Navigate to [http://localhost:3000/admin](http://localhost:3000/admin) and log in with the default credentials:
29
-
30
- | | |
31
- |---|---|
32
- | **Email** | `spree@example.com` |
33
- | **Password** | `spree123` |
34
-
35
- Upon successful authentication, you should see the admin screen:
36
-
37
- <img src="/images/spree_admin_dashboard.png" />
27
+ ```
28
+ my-store/
29
+ ├── server/ # The Spree API
30
+ ├── apps/
31
+ │ ├── dashboard/ # Admin dashboard (Vite/React)
32
+ │ ├── seller-dashboard/ # Seller panel, for marketplaces (Vite/React)
33
+ │ └── storefront/ # Next.js storefront (if selected)
34
+ ├── .env # Ports, secrets and service URLs
35
+ ├── .spree/ # CLI credentials — gitignored, created on first run
36
+ ├── docker-compose.yml # The local stack
37
+ ├── package.json # Scripts and the pinned @spree/cli
38
+ ├── AGENTS.md # Instructions for coding agents
39
+ ├── CLAUDE.md # Project layout and conventions
40
+ └── README.md # How to run everything
41
+ ```
38
42
 
39
43
  ## Using the Spree CLI
40
44
 
@@ -58,18 +62,15 @@ See the [Spree CLI guide](../cli/quickstart.md) for all commands and the [Admin
58
62
 
59
63
  ## Next Steps
60
64
 
65
+ - [How Spree fits together](../core-concepts/architecture.md) — The catalog, the cart, the order and fulfillment — what each one is for, and the two APIs you build against. Twenty minutes here saves a lot of guessing later, and it ends by pointing you at whichever path suits you.
61
66
 
62
- - [Build with AI Agents](../agentic/overview.md) — Install the Spree agent skills and connect the docs MCP server — Claude Code, Cursor, Copilot, and 60+ other tools learn Spree's conventions and build features with you.
63
- - [Customization Tutorial](../tutorial/introduction.md) — Build a complete custom feature — model, admin UI, API, and TypeScript SDK — step by step.
64
- - [Core Concepts](../core-concepts/architecture.md) — Understand how Spree models commerce — stores, products, orders, payments.
65
- - [Next.js Storefront](../storefront/nextjs/quickstart.md) — Customize and extend the headless storefront.
66
-
67
+ In a hurry? Jump straight to the [tutorial](../tutorial/introduction.md) to build a feature end to end, the [Next.js Storefront](../storefront/nextjs/quickstart.md) to put a storefront in front of it, or the [API reference](../../api-reference/admin-api/introduction.md) to see what you can do with the Admin API.
67
68
 
68
69
  > **TIP:** Using an AI coding agent? Run `npx skills add spree/agent-skills` in your new project — it teaches your agent the customization patterns before you write a line of code. See [Agentic Development](../agentic/overview.md).
69
70
 
70
71
  ---
71
72
 
72
- Congrats! You've set up your Spree Commerce and it's looking amazing!
73
+ Congrats! You've set up your Spree application and it's looking amazing!
73
74
 
74
75
  Give Spree a [GitHub Star](https://github.com/spree/spree), why don't ya? Thank you for supporting Spree open-source!
75
76
 
@@ -150,7 +150,7 @@ module Spree
150
150
  private
151
151
 
152
152
  def matches_brand?(line_item)
153
- brand = line_item.product.get_metafield('details.brand')&.value
153
+ brand = line_item.product.get_custom_field('details.brand')&.value
154
154
  preferred_brand_names.include?(brand)
155
155
  end
156
156
  end
@@ -224,9 +224,9 @@ Background jobs (`IndexJob`, `RemoveJob`) fire on `after_commit` when `indexing_
224
224
 
225
225
  Always use prefixed IDs (`ctg_abc`, `prod_xyz`, `optval_abc`) when indexing. Never use raw database IDs — Spree supports UUID primary keys.
226
226
 
227
- ### Metafield search schema
227
+ ### Custom Field search schema
228
228
 
229
- Product metafields marked `searchable` / `sortable` on `MetafieldDefinition` are projected through `Spree::SearchProvider::MetafieldSchema` — shared by providers and `ProductPresenter`. Toggle participation with definition flags; run `rake spree:search:reindex` after changes when using Meilisearch.
229
+ Product custom fields marked `searchable` / `sortable` on `CustomFieldDefinition` are projected through `Spree::SearchProvider::CustomFieldSchema` — shared by providers and `ProductPresenter`. Toggle participation with definition flags; run `rake spree:search:reindex` after changes when using Meilisearch.
230
230
 
231
231
  Document shaping for `cf_*` fields stays on `search_product_presenter` — the only search-related dependency.
232
232
 
@@ -170,12 +170,12 @@ end
170
170
 
171
171
  ### Step 4 (optional): Add a Variant-side Predicate
172
172
 
173
- The example above relies on `variant.refrigerated?`. In a real plugin you'd back that with a [Custom Field](../core-concepts/metafields.md) on `Spree::Variant` — say a boolean metafield with key `refrigerated` — and define the predicate as a thin reader:
173
+ The example above relies on `variant.refrigerated?`. In a real plugin you'd back that with a [Custom Field](../core-concepts/metafields.md) on `Spree::Variant` — say a boolean custom field with key `refrigerated` — and define the predicate as a thin reader:
174
174
 
175
175
  ```ruby app/models/spree/variant_decorator.rb
176
176
  Spree::Variant.class_eval do
177
177
  def refrigerated?
178
- metafields.find_by(namespace: 'logistics', key: 'refrigerated')&.value == 'true'
178
+ custom_fields.find_by(namespace: 'logistics', key: 'refrigerated')&.value == 'true'
179
179
  end
180
180
  end
181
181
  ```
@@ -66,5 +66,5 @@ The SDK transforms `filter`/`sort` into the Ransack-flavored query string the Ad
66
66
  ## See also
67
67
 
68
68
  - The [Store SDK extending guide](../extending.md) covers the same pattern for storefront endpoints — the API is identical, only the base path (`/api/v3/store`) and auth header differ.
69
- - The [API](../../tutorial/admin-api.md) and [SDK](../../tutorial/storefront.md) tutorials walk through creating new Spree endpoints end-to-end (Rails controller, serializer, TypeScript types). Everything they show for the Store API applies equivalently for the Admin API.
69
+ - The [API](../../tutorial/model-and-api.md) and [SDK](../../tutorial/storefront.md) tutorials walk through creating new Spree endpoints end-to-end (Rails controller, serializer, TypeScript types). Everything they show for the Store API applies equivalently for the Admin API.
70
70
  - For building dashboard UI on top of your custom endpoint, see the [Dashboard](../../dashboard/overview.md) section — start with [Customization Quickstart](../../dashboard/customization/quickstart.md) for in-app changes or [Plugin Overview](../../dashboard/plugins/overview.md) for redistributable packages.
@@ -66,4 +66,4 @@ const page: PaginatedResponse<Order> = await client.orders.list()
66
66
  - [Admin API Reference](../../../api-reference/admin-api/introduction.md) — The underlying REST API the SDK wraps — every endpoint, parameter, and response shape.
67
67
 
68
68
 
69
- > **TIP:** Adding your own resources? The [`spree generate api_resource`](../../tutorial/admin-api.md) generator scaffolds Admin API endpoints (model, serializers, controllers, specs) in one command, and the [extending guide](../extending.md) shows how to call custom endpoints from the SDK.
69
+ > **TIP:** Adding your own resources? The [`spree generate api_resource`](../../tutorial/model-and-api.md) generator scaffolds Admin API endpoints (model, serializers, controllers, specs) in one command, and the [extending guide](../extending.md) shows how to call custom endpoints from the SDK.
@@ -127,4 +127,4 @@ await client.customFields('Spree::Product', 'prod_xxx').list()
127
127
 
128
128
  ## Adding your own resources
129
129
 
130
- The resource map grows with your store. The [`spree generate api_resource`](../../tutorial/admin-api.md) generator scaffolds new Admin API endpoints (model, serializers, controllers, routes, specs) in one command — then call them from the SDK with a [custom fetch or raw request](../extending.md), or generate a typed client for them.
130
+ The resource map grows with your store. The [`spree generate api_resource`](../../tutorial/model-and-api.md) generator scaffolds new Admin API endpoints (model, serializers, controllers, routes, specs) in one command — then call them from the SDK with a [custom fetch or raw request](../extending.md), or generate a typed client for them.
@@ -162,7 +162,7 @@ All types are exported as unprefixed names (e.g., `Product`, `Order`). Legacy `S
162
162
  - `OptionType` - Option type (e.g., Size, Color)
163
163
  - `OptionValue` - Option value (e.g., Small, Red)
164
164
  - `DigitalLink` - Digital download link
165
- - `Metafield` - Custom metafield data
165
+ - `CustomField` - Custom field data
166
166
 
167
167
  ### Wishlist Types
168
168
  - `Wishlist` - Wishlist
@@ -30,4 +30,4 @@ const nike = await client.request<Brand>('GET', '/brands/nike')
30
30
 
31
31
  `client.request` uses the same auth headers, retry logic, and locale/currency defaults as `client.products.list()` or any other built-in resource.
32
32
 
33
- For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/admin-api.md) and [SDK](../tutorial/storefront.md) tutorials.
33
+ For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/model-and-api.md) and [SDK](../tutorial/storefront.md) tutorials.
@@ -1,129 +1,83 @@
1
1
  ---
2
2
  title: Testing
3
- description: Test the Brands feature at every layer — model and API specs with RSpec, dashboard tests with Vitest, and one end-to-end Playwright pass.
3
+ description: Test the Brands feature — running the generated backend specs, unit-testing the dashboard logic you wrote, and one Playwright pass through the browser.
4
4
  ---
5
5
 
6
- Each layer gets the test that suits it: fast backend specs for the model and both APIs, component tests for the dashboard, and one end-to-end pass proving the whole path works.
6
+ Most of the backend is generated, so most of the testing worth doing is on the code you wrote yourself — which is mostly in the dashboard. This chapter covers the backend briefly, spends longer on the dashboard, and ends with one browser pass proving the layers are actually connected.
7
7
 
8
- Your project arrives with testing already configured — RSpec, FactoryBot, and Spree's own test helpers, including every factory core ships. You can write a spec against `create(:product)` without setting anything up.
8
+ ## The backend
9
9
 
10
- ## Step 1: Run what the generator wrote
11
-
12
- The generator in step 1 already wrote a factory and a controller spec per API side, so this step is mostly filling in rather than starting over:
10
+ Your project arrives with a fully configured testing environment for backend testing. The generator wrote all the files needed for running API tests:
13
11
 
14
12
  ```text
15
- server/spec/factories/spree/brand_factory.rb
16
13
  server/spec/controllers/spree/api/v3/store/brands_controller_spec.rb
17
14
  server/spec/controllers/spree/api/v3/admin/brands_controller_spec.rb
18
15
  ```
19
16
 
20
- Run them:
17
+ You can now run them, a directory or a single file at a time:
21
18
 
22
19
  ```bash
23
20
  spree rspec spec/controllers/spree/api/v3/
21
+ spree rspec spec/models/spree/brand_spec.rb
24
22
  ```
25
23
 
26
- The generated factory is the starting point for every other spec — always build records through factories rather than creating them directly, so a change to the model does not mean editing fifty specs.
27
-
28
- ## Step 2: Model specs
24
+ ## The dashboard
29
25
 
30
- Test what you wrote, not what the framework already guarantees. A presence validation on a required column needs no spec; the association behavior you chose does:
26
+ This is where most of your own code lives, so it is where most of your tests belong. Vitest and Playwright come configured, so there is nothing to set up:
31
27
 
32
- ```ruby server/spec/models/spree/brand_spec.rb
33
- RSpec.describe Spree::Brand, type: :model do
34
- it 'nullifies the reference instead of deleting products' do
35
- brand = create(:brand)
36
- product = create(:product, brand: brand)
37
-
38
- brand.destroy
39
-
40
- expect(product.reload.brand_id).to be_nil
41
- expect(product).to be_persisted
42
- end
43
- end
28
+ ```bash
29
+ cd apps/dashboard && pnpm test
44
30
  ```
45
31
 
46
- Prefer `build` over `create` when a record does not need to hit the database — it is markedly faster across a suite.
47
-
48
- ## Step 3: Controller specs
49
-
50
- Controller specs are where edge cases belong: authorization, scoping, filtering, error shapes. Always `render_views`, so a broken serializer fails the spec rather than passing silently:
32
+ ### What is worth a unit test
51
33
 
52
- ```ruby server/spec/controllers/spree/api/v3/store/brands_controller_spec.rb
53
- RSpec.describe Spree::Api::V3::Store::BrandsController, type: :controller do
54
- render_views
34
+ Not components. Rendering a card to assert it shows a brand name tests React, not your feature, and breaks every time the markup moves. Test the **logic between the UI and the API** — the parts with decisions in them.
55
35
 
56
- it 'exposes public fields only' do
57
- create(:brand, name: 'Wilson')
36
+ Query keys are the clearest example. Every key must be scoped to the current store, or one store's rows leak into another after a switch. That is a rule a test can hold you to:
58
37
 
59
- get :index, params: { format: :json }
38
+ ```ts apps/dashboard/src/brands/client.test.ts
39
+ import { withStoreScope } from '@spree/dashboard'
40
+ import { QueryClient } from '@tanstack/react-query'
41
+ import { describe, expect, it } from 'vitest'
42
+ import { brandInvalidations } from './client'
60
43
 
61
- record = json_response['data'].first
62
- expect(record['id']).to start_with('brand_')
63
- expect(record).not_to have_key('created_at')
64
- end
44
+ const STORE = 'store_abc123'
65
45
 
66
- it 'filters by name' do
67
- create(:brand, name: 'Wilson')
68
- create(:brand, name: 'Penn')
46
+ describe('brand cache invalidation', () => {
47
+ it('refreshes the brand list without touching another store', () => {
48
+ const client = new QueryClient()
49
+ client.setQueryData(withStoreScope(['brands'], STORE), [])
50
+ client.setQueryData(withStoreScope(['brands'], 'store_other'), [])
69
51
 
70
- get :index, params: { q: { name_cont: 'wil' }, format: :json }
52
+ for (const key of brandInvalidations()) {
53
+ client.invalidateQueries({ queryKey: withStoreScope(key, STORE) })
54
+ }
71
55
 
72
- expect(json_response['data'].map { |b| b['name'] }).to eq(['Wilson'])
73
- end
74
- end
56
+ const stale = client.getQueryCache().getAll().filter((q) => q.state.isInvalidated)
57
+ expect(stale).toHaveLength(1)
58
+ })
59
+ })
75
60
  ```
76
61
 
77
- That second example is worth keeping: it proves the Ransack allowlist from step 1 actually permits `name_cont`. Remove `name` from `whitelisted_ransackable_attributes` and this spec fails, which is exactly what you want.
62
+ Driving a real `QueryClient` means the assertion goes through TanStack's own prefix matching rather than a hand-rolled key comparison — so it stays true if the matching rules change.
78
63
 
79
- > **NOTE:** Specs run against a store the suite sets up for you, available as `@default_store`. Use it rather than creating another store marked as the default — two of them in one run produce failures that surface far from their cause.
64
+ The same applies to anything else with a decision in it: a function mapping form values to an API payload, a permission predicate deciding whether a nav entry shows, a filter translating table state into Ransack params. Each is a pure function, each is a two-line test.
80
65
 
81
- ## Step 4: Dashboard tests
66
+ ### What is not worth a unit test
82
67
 
83
- The dashboard is a TypeScript app, so its code tests with Vitest:
68
+ - **Components.** Asserting a card renders text is testing React.
69
+ - **`BrandsClient.list` itself.** It is a one-line wrapper around `adminClient.request`. A test would mock the thing it wraps and assert the mock was called — proving nothing about whether the endpoint exists.
70
+ - **Anything the compiler already catches.** A wrong prop type is a build failure, not a test case.
84
71
 
85
- ```bash
86
- cd apps/dashboard && pnpm test
87
- ```
88
-
89
- Test the client wrappers and any logic in your pages. Mock the HTTP layer rather than the client module, so the test exercises the real request path:
90
-
91
- ```ts apps/dashboard/src/brands/client.test.ts
92
- import { http, HttpResponse } from 'msw'
93
- import { setupServer } from 'msw/node'
94
- import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
95
- import { BrandsClient } from './client'
96
-
97
- const server = setupServer(
98
- http.get('*/api/v3/admin/brands', () =>
99
- HttpResponse.json({ data: [{ id: 'brand_k5nR8xLq', name: 'Wilson' }] }),
100
- ),
101
- )
102
-
103
- beforeAll(() => server.listen())
104
- afterEach(() => server.resetHandlers())
105
- afterAll(() => server.close())
106
-
107
- describe('BrandsClient', () => {
108
- it('lists brands from the admin endpoint', async () => {
109
- const result = await BrandsClient.list({ per_page: 10 })
110
-
111
- expect(result.data).toHaveLength(1)
112
- expect(result.data[0].name).toBe('Wilson')
113
- })
114
- })
115
- ```
72
+ That second one is worth dwelling on: the question "does `/brands` actually return brands" cannot be answered by a unit test at all. It needs the real API, which is the next section.
116
73
 
117
- Mocking the HTTP layer rather than the client module means the test exercises the real request path — the URL, the params and the response shape — so a wrong endpoint fails here instead of in the browser.
118
-
119
- ## Step 5: One end-to-end pass
74
+ ## End to end, through the browser
120
75
 
121
76
  Everything above tests a layer in isolation. One Playwright spec proves the layers are actually connected — a brand created through the Admin API reaches the dashboard screen you built:
122
77
 
123
78
  ```ts apps/dashboard/e2e/brands.spec.ts
124
79
  import { expect, test } from '@playwright/test'
125
- import { createBrand } from './helpers/api'
126
- import { login } from './helpers/login'
80
+ import { createBrand, login } from './helpers'
127
81
 
128
82
  test('a brand appears on the brands screen', async ({ page }) => {
129
83
  const name = `Wilson ${Date.now()}`
@@ -147,21 +101,21 @@ Two conventions matter here:
147
101
 
148
102
  ## What to run before you push
149
103
 
150
- Run the specs for what you changed rather than the whole suite:
104
+ Run what you changed, not everything:
151
105
 
152
106
  ```bash
153
- spree rspec spec/models/spree/brand_spec.rb
154
- spree rspec spec/controllers/spree/api/v3/
107
+ spree rspec spec/models/spree/brand_spec.rb # the backend
108
+ cd apps/dashboard && pnpm test # the dashboard
155
109
  ```
156
110
 
157
- Pass a file, a directory, or a single example with a line number (`spec/models/spree/brand_spec.rb:12`). Keep the full run for continuous integration, where waiting costs you nothing.
111
+ Keep the full suite and the end-to-end pass for continuous integration, where waiting costs you nothing.
158
112
 
159
113
  ## You're done
160
114
 
161
115
  You have built one feature through every layer of Spree:
162
116
 
163
117
  - A model with a Store API and an Admin API, generated in one command
164
- - A dashboard plugin giving staff a place to manage it
118
+ - A dashboard screen giving staff a place to manage it
165
119
  - Storefront pages rendering it through typed SDK calls
166
120
  - Lifecycle events other systems react to
167
121
  - Tests at each layer, plus one pass across all of them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.240",
3
+ "version": "0.1.242",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",