@sanity-labs/client-fake-for-test 0.2.0 → 0.3.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.
- package/README.md +103 -26
- package/dist/index.cjs +450 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +290 -10
- package/dist/index.d.ts +290 -10
- package/dist/index.js +451 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -65,18 +65,36 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
65
65
|
|
|
66
66
|
| Area | Method | Notes |
|
|
67
67
|
| --- | --- | --- |
|
|
68
|
-
| Read | `fetch<T>(query, params?)` | GROQ via groq-js
|
|
69
|
-
| Read | `getDocument<T>(id)` | Returns `
|
|
70
|
-
| Read | `getDocuments<T>(ids)` | Element-wise null for missing |
|
|
71
|
-
| Write | `create(doc,
|
|
72
|
-
| Write | `createOrReplace(doc,
|
|
73
|
-
| Write | `createIfNotExists(doc,
|
|
74
|
-
| Write | `delete(id,
|
|
68
|
+
| Read | `fetch<T>(query, params?, { filterResponse?, returnQuery? })` | GROQ via groq-js. `filterResponse: false` → `{ result, ms, query }` (`RawQueryResponse`), like the real client |
|
|
69
|
+
| Read | `getDocument<T>(id)` | Returns `undefined` if missing (or hidden by ACL) — matches `@sanity/client` |
|
|
70
|
+
| Read | `getDocuments<T>(ids)` | Element-wise `null` for missing |
|
|
71
|
+
| Write | `create(doc, options?)` | Throws if `_id` already exists |
|
|
72
|
+
| Write | `createOrReplace(doc, options?)` | Upsert |
|
|
73
|
+
| Write | `createIfNotExists(doc, options?)` | No-op if `_id` exists |
|
|
74
|
+
| Write | `delete(id, options?)` | Default `{ _id }` or `null`; id/array shapes via options |
|
|
75
|
+
| Mutate | `mutate(operations, options?)` | Raw mutation array (`{create}`/`{patch}`/`{delete}`/…), or a `patch()` / `transaction()` builder. Atomic |
|
|
75
76
|
| Patch | `patch(id)` chain → `.commit()` | Ops: `.set`, `.setIfMissing`, `.unset`, `.inc`, `.dec`, `.insert(position, anchor, items)`, `.ifRevisionId` |
|
|
76
|
-
| Transaction | `transaction()` chain → `.commit()` | Mutations: `.create`, `.createOrReplace`, `.createIfNotExists`, `.delete`, `.patch(id, recipe)`. Atomic — any throw rolls back the whole batch |
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
77
|
+
| Transaction | `transaction()` chain → `.commit()` | Mutations: `.create`, `.createOrReplace`, `.createIfNotExists`, `.delete`, `.patch(id, recipe)`. Atomic — any throw rolls back the whole batch. Default result is a `MultipleMutationResult` (`{ transactionId, documentIds, results }`) |
|
|
78
|
+
| Actions | `action(action \| action[])` | Content Lake release / version actions, applied atomically |
|
|
79
|
+
| Releases | `releases.*`, `createVersion`, `unpublishVersion` | Content Releases lifecycle — create / edit / schedule / publish / archive / delete, plus version create / replace / discard / unpublish |
|
|
80
|
+
| Config | `config()` | Returns `{ projectId, dataset, perspective }` |
|
|
81
|
+
| Config | `withConfig({ projectId?, dataset?, perspective? })` | Sibling handle into the same multi-dataset registry |
|
|
82
|
+
| Perspectives | `raw` / `published` / `drafts` / release stacks | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
|
|
83
|
+
| HTTP | `request({ url? \| uri? })` | Present **only** when the client is created with `accessControl`. Serves `/users/me` + the configured ACL path. Calling it without `accessControl` throws a guidance error. |
|
|
84
|
+
|
|
85
|
+
#### Mutation return shapes
|
|
86
|
+
|
|
87
|
+
`create` / `createOrReplace` / `createIfNotExists` / `delete` / `mutate` / `transaction().commit()` honor `returnDocuments` / `returnFirst`, mirroring `@sanity/client`:
|
|
88
|
+
|
|
89
|
+
| `returnDocuments` | `returnFirst` | Resolves to |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| (default for create-family) | | the mutated document |
|
|
92
|
+
| `true` | `false` | `SanityDocument[]` |
|
|
93
|
+
| `false` | `true` (or default) | `SingleMutationResult` `{ transactionId, documentId, results }` |
|
|
94
|
+
| `false` | `false` (default for `transaction` / `mutate`) | `MultipleMutationResult` `{ transactionId, documentIds, results }` |
|
|
95
|
+
|
|
96
|
+
> [!NOTE]
|
|
97
|
+
> The static return type of `create` / `delete` reflects the **default** shape (the document / `{ _id } | null`). When you pass `returnDocuments: false` to get an id-shaped result, supply a type argument or cast — the runtime value is correct either way.
|
|
80
98
|
|
|
81
99
|
### Test-only helpers (not on real `SanityClient`)
|
|
82
100
|
|
|
@@ -90,29 +108,88 @@ Useful in test bodies, but a giveaway that the code under test isn't using the r
|
|
|
90
108
|
| `client.reset()` | Drop everything in the current dataset |
|
|
91
109
|
| `client.store()` | Direct access to the underlying `TestStore` |
|
|
92
110
|
| `client.datasets()` | `{ projectId, dataset }[]` known to the registry |
|
|
111
|
+
| `client.mutations()` | Every committed mutation, flattened and in order — assert exactly what writes your code submitted |
|
|
112
|
+
| `client.transactions()` | Every committed transaction (a direct mutation is a one-mutation transaction), each tagged with `transactionId` + `(projectId, dataset)` |
|
|
113
|
+
| `client.clearLog()` | Reset the mutation / transaction log |
|
|
114
|
+
| `client.calls(method?)` | Every recorded method call, with args + result — a framework-agnostic spy (like `vi.fn().mock.calls`) |
|
|
115
|
+
| `client.callCount(method?)` | Number of recorded calls, optionally filtered by method |
|
|
116
|
+
| `client.clearCalls()` | Reset the call log |
|
|
117
|
+
| `client.stub(name, handler)` | Register a handler for any unimplemented `@sanity/client` member (see below) |
|
|
118
|
+
| `client.stubs()` | The currently-registered stubs |
|
|
93
119
|
| `createTestClient({ datasets: { "p:d": [...] } })` | Seed multiple `(projectId, dataset)` stores up front |
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
#### Two layers of observability
|
|
122
|
+
|
|
123
|
+
The fake records at two levels, so you can assert on either *what your code did to the data* or *how it called the client*:
|
|
124
|
+
|
|
125
|
+
- **Write effects** — `client.mutations()` / `client.transactions()`: the create/patch/delete/mutate operations that actually landed (rolled-back and `dryRun` writes excluded).
|
|
126
|
+
- **API calls** — `client.calls()` / `client.callCount()`: every invocation of a `@sanity/client`-surface method (`fetch`, `getDocument`, `create`, `patch`, `mutate`, `releases.*`, …) with its `args`, resolved `result`, or thrown `error`. This is the spy layer — reach for it to assert read patterns (`fetch` called once, with these params) that the write log can't show.
|
|
96
127
|
|
|
97
|
-
|
|
128
|
+
```ts
|
|
129
|
+
const client = createTestClient({ documents: [{ _id: "a", _type: "article" }] });
|
|
130
|
+
await myLoader(client);
|
|
131
|
+
|
|
132
|
+
expect(client.callCount("fetch")).toBe(1);
|
|
133
|
+
expect(client.calls("fetch")[0]?.args).toEqual([`*[_type=="article"]`, {}]);
|
|
134
|
+
expect(client.calls("getDocument")[0]?.result).toMatchObject({ _id: "a" });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> [!NOTE]
|
|
138
|
+
> Builder-returning methods (`patch`, `transaction`) record the call at builder-creation time (`patch("a")` → `args: ["a"]`); the chained ops/commit show up in the mutation log, not as extra calls. Test-only helpers (`snapshot`, `mutations`, `calls`, …) are never recorded.
|
|
139
|
+
|
|
140
|
+
#### Inspecting what your code wrote
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const client = createTestClient();
|
|
144
|
+
await myFeature(client); // code under test issues some writes
|
|
145
|
+
|
|
146
|
+
expect(client.mutations()).toEqual([
|
|
147
|
+
{ kind: "create", doc: { _id: "draft.x", _type: "article" } },
|
|
148
|
+
{ kind: "patch", documentId: "draft.x", ops: [{ kind: "set", props: { status: "ready" } }] },
|
|
149
|
+
]);
|
|
150
|
+
// or assert on whole transactions:
|
|
151
|
+
expect(client.transactions()).toHaveLength(1);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Not supported — but fails gracefully
|
|
155
|
+
|
|
156
|
+
The client is wrapped in a `Proxy`. Reaching for a member the fake doesn't implement doesn't give you a bare `undefined is not a function` — it throws an actionable error naming the member, what it is on the real client, and what to do instead:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
[client-fake-for-test] `client.listen()` is not implemented (real-time listener …).
|
|
160
|
+
|
|
161
|
+
This is an in-memory fake of @sanity/client. It implements:
|
|
162
|
+
fetch, getDocument, getDocuments, create, createOrReplace, …
|
|
163
|
+
|
|
164
|
+
→ Test your subscription logic in isolation, or register a fake: client.stub("listen", () => myFakeObservable).
|
|
165
|
+
|
|
166
|
+
Register a stub to fill any gap: client.stub("listen", handler)
|
|
167
|
+
If this should be supported, open an issue: …
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Nested access reports the full path (`client.assets.upload(...)` → `client.assets.upload`). If you type the test variable as `TestClient`, TypeScript also catches the misuse at compile time.
|
|
98
171
|
|
|
99
172
|
| Area | Status | Workaround |
|
|
100
173
|
| --- | --- | --- |
|
|
101
|
-
| `listen(
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `agent.*` — Agent Actions | ❌ Absent | Stub the namespace |
|
|
108
|
-
| `assets.*` — uploads, metadata | ❌ Absent | Seed asset docs manually |
|
|
109
|
-
| `users.*` / `projects.*` / `datasets.*` / `releases.*` | ❌ Absent | Stub per call site |
|
|
110
|
-
| `observable.*` — RxJS API | ❌ Absent | Wrap the Promise API with `from()` if needed |
|
|
111
|
-
| Perspectives (`drafts.*` / `published.*` / `raw` config) | ❌ All docs visible | Treat IDs literally; no draft/published split |
|
|
174
|
+
| `listen(...)` — real-time | ❌ Guarded | Test subscription logic separately, or `client.stub("listen", fn)` |
|
|
175
|
+
| `clone()` | ❌ Guarded | `withConfig({})` returns a sibling handle |
|
|
176
|
+
| `getUrl(uri)` / `getDataUrl(...)` — URL builders | ❌ Guarded | Hard-code in tests, or stub |
|
|
177
|
+
| `request(...)` — generic HTTP | ⚠️ Only with `accessControl` | Configure `accessControl`, stub it, or use [MSW](https://mswjs.io) |
|
|
178
|
+
| `agent.*` / `assets.*` / `projects.*` / `users.*` / `live.*` / `mediaLibrary.*` | ❌ Guarded | Seed docs directly, or `client.stub(name, fn)` |
|
|
179
|
+
| `observable.*` — RxJS API | ❌ Guarded | Wrap the Promise API with `from()` if needed |
|
|
112
180
|
| Patch ops: `.diffMatchPatch`, `.merge`, `.append`, `.prepend`, `.replace` | ❌ Absent | `.insert("after", "field[-1]", items)` for append; `.insert("before", "field[0]", items)` for prepend |
|
|
113
181
|
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
#### Registering stubs
|
|
183
|
+
|
|
184
|
+
Fill any gap without forking — `client.stub(name, handler)` makes that member resolve to your handler (shared across `withConfig` siblings):
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const client = createTestClient();
|
|
188
|
+
client.stub("listen", () => fakeObservable);
|
|
189
|
+
client.stub("assets", { upload: vi.fn() });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Stubs fill gaps only — they don't shadow implemented methods like `fetch` or `patch`.
|
|
116
193
|
|
|
117
194
|
## Design constraints
|
|
118
195
|
|