@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.
- package/dist/api-reference/store-api/metadata.md +6 -6
- package/dist/developer/core-concepts/architecture.md +15 -19
- package/dist/developer/core-concepts/carts.md +71 -1
- package/dist/developer/core-concepts/delivery-setup.md +3 -1
- package/dist/developer/core-concepts/freight.md +279 -0
- package/dist/developer/core-concepts/metafields.md +0 -2
- package/dist/developer/core-concepts/payments.md +123 -72
- package/dist/developer/core-concepts/sellers.md +50 -5
- package/dist/developer/create-spree-app/quickstart.md +22 -19
- package/dist/developer/customization/api.md +2 -2
- package/dist/developer/customization/checkout.md +117 -0
- package/dist/developer/customization/configuration.md +76 -175
- package/dist/developer/customization/decorators.md +120 -130
- package/dist/developer/customization/metadata.md +9 -9
- package/dist/developer/customization/model-preferences.md +34 -68
- package/dist/developer/customization/permissions.md +13 -28
- package/dist/developer/customization/validations.md +16 -43
- package/dist/developer/getting-started/quickstart.md +25 -24
- package/dist/developer/how-to/custom-promotion.md +1 -1
- package/dist/developer/how-to/custom-search-provider.md +2 -2
- package/dist/developer/how-to/custom-stock-splitter.md +2 -2
- package/dist/developer/sdk/admin/extending.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +1 -1
- package/dist/developer/sdk/admin/resources.md +1 -1
- package/dist/developer/sdk/configuration.md +1 -1
- package/dist/developer/sdk/extending.md +1 -1
- package/dist/developer/tutorial/testing.md +45 -91
- 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
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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/)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
## Project structure and overview
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
`create-spree-app` creates a project folder like this:
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
###
|
|
227
|
+
### Custom Field search schema
|
|
228
228
|
|
|
229
|
-
Product
|
|
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
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
- `
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8
|
+
## The backend
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
## Step 2: Model specs
|
|
24
|
+
## The dashboard
|
|
29
25
|
|
|
30
|
-
|
|
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
|
-
```
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
52
|
+
for (const key of brandInvalidations()) {
|
|
53
|
+
client.invalidateQueries({ queryKey: withStoreScope(key, STORE) })
|
|
54
|
+
}
|
|
71
55
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
56
|
+
const stale = client.getQueryCache().getAll().filter((q) => q.state.isInvalidated)
|
|
57
|
+
expect(stale).toHaveLength(1)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
75
60
|
```
|
|
76
61
|
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
### What is not worth a unit test
|
|
82
67
|
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
104
|
+
Run what you changed, not everything:
|
|
151
105
|
|
|
152
106
|
```bash
|
|
153
|
-
spree rspec spec/models/spree/brand_spec.rb
|
|
154
|
-
|
|
107
|
+
spree rspec spec/models/spree/brand_spec.rb # the backend
|
|
108
|
+
cd apps/dashboard && pnpm test # the dashboard
|
|
155
109
|
```
|
|
156
110
|
|
|
157
|
-
|
|
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
|
|
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
|