@vantage-ui/client 0.0.0-stage → 0.4.3
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/CHANGELOG.md +46 -0
- package/bin/vantage-client.js +5 -0
- package/dist/client-connection.d.ts +41 -0
- package/dist/client-connection.js +260 -0
- package/dist/client-core.d.ts +70 -0
- package/dist/client-core.js +140 -0
- package/dist/client-loading.d.ts +13 -0
- package/dist/client-loading.js +229 -0
- package/dist/client-runs.d.ts +18 -0
- package/dist/client-runs.js +88 -0
- package/dist/client-writes.d.ts +14 -0
- package/dist/client-writes.js +114 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +61 -0
- package/dist/codegen/cli.d.ts +8 -0
- package/dist/codegen/cli.js +54 -0
- package/dist/codegen/emit.d.ts +3 -0
- package/dist/codegen/emit.js +190 -0
- package/dist/codegen/fetch.d.ts +4 -0
- package/dist/codegen/fetch.js +41 -0
- package/dist/codegen/meta.d.ts +13 -0
- package/dist/codegen/meta.js +59 -0
- package/dist/codegen/runs.d.ts +20 -0
- package/dist/codegen/runs.js +58 -0
- package/dist/codegen/schema.d.ts +97 -0
- package/dist/codegen/schema.js +23 -0
- package/dist/collection-cell.d.ts +69 -0
- package/dist/collection-cell.js +153 -0
- package/dist/collection.d.ts +32 -0
- package/dist/collection.js +122 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/mock-query.d.ts +9 -0
- package/dist/mock-query.js +75 -0
- package/dist/mock-runs.d.ts +61 -0
- package/dist/mock-runs.js +165 -0
- package/dist/mock-store.d.ts +78 -0
- package/dist/mock-store.js +158 -0
- package/dist/mock-transport.d.ts +16 -0
- package/dist/mock-transport.js +38 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +169 -0
- package/dist/persist.d.ts +47 -0
- package/dist/persist.js +108 -0
- package/dist/protocol.d.ts +236 -0
- package/dist/protocol.js +5 -0
- package/dist/react.d.ts +59 -0
- package/dist/react.js +169 -0
- package/dist/reserved.d.ts +6 -0
- package/dist/reserved.js +13 -0
- package/dist/run/frames.d.ts +6 -0
- package/dist/run/frames.js +24 -0
- package/dist/run/hooks.d.ts +18 -0
- package/dist/run/hooks.js +87 -0
- package/dist/run/messages.d.ts +20 -0
- package/dist/run/messages.js +42 -0
- package/dist/run/outcome.d.ts +12 -0
- package/dist/run/outcome.js +27 -0
- package/dist/run/run.d.ts +82 -0
- package/dist/run/run.js +246 -0
- package/dist/run/storage.d.ts +3 -0
- package/dist/run/storage.js +20 -0
- package/dist/run/types.d.ts +92 -0
- package/dist/run/types.js +3 -0
- package/dist/run/variables.d.ts +26 -0
- package/dist/run/variables.js +70 -0
- package/dist/schema-meta.d.ts +91 -0
- package/dist/schema-meta.js +3 -0
- package/dist/signal.d.ts +45 -0
- package/dist/signal.js +163 -0
- package/dist/store.d.ts +45 -0
- package/dist/store.js +144 -0
- package/dist/table.d.ts +62 -0
- package/dist/table.js +82 -0
- package/dist/testing.d.ts +61 -0
- package/dist/testing.js +195 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +32 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +2 -0
- package/dist/util.d.ts +18 -0
- package/dist/util.js +46 -0
- package/package.json +54 -4
- package/skills/vantage-client/SKILL.md +390 -0
- package/skills/vantage-client/references/protocol.md +67 -0
- package/skills/vantage-client/references/react.md +242 -0
- package/skills/vantage-client/references/testing.md +173 -0
- package/README.md +0 -3
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# React
|
|
2
|
+
|
|
3
|
+
Import the bindings from `@vantage-ui/client/react`. React 18 or later.
|
|
4
|
+
|
|
5
|
+
## The provider, and the connection's states
|
|
6
|
+
|
|
7
|
+
`VantageProvider` makes `db` available to `useVantage()`. With a `fallback`,
|
|
8
|
+
its children wait for `db.ready`: the `instant` entities loaded, or a final
|
|
9
|
+
refusal. One component can serve as the fallback and as a gate that
|
|
10
|
+
explains `refused` and `protocol_mismatch` once the children show:
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
// test: react#provider
|
|
14
|
+
// continues from SKILL.md's Connect: db; App is your app's root component
|
|
15
|
+
import { VantageProvider, useConnection } from "@vantage-ui/client/react";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
17
|
+
|
|
18
|
+
function ConnectionGate({ children }: { children?: ReactNode }) {
|
|
19
|
+
const { status, error } = useConnection();
|
|
20
|
+
if (status === "refused") return <p role="alert">Sign-in refused: {error}</p>;
|
|
21
|
+
if (status === "protocol_mismatch") return <p role="alert">This page is out of date. Reload it. ({error})</p>;
|
|
22
|
+
return children ?? <p>Connecting…</p>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Root() {
|
|
26
|
+
return (
|
|
27
|
+
<VantageProvider db={db} fallback={<ConnectionGate />}>
|
|
28
|
+
<ConnectionGate>
|
|
29
|
+
<App />
|
|
30
|
+
</ConnectionGate>
|
|
31
|
+
</VantageProvider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`useConnection()` returns `{ status, halted, error, seq, principal }`;
|
|
37
|
+
`halted` is true once `refused` or `protocol_mismatch`. `closed` is
|
|
38
|
+
transient (the client reconnects); show it as a hint, not an error. A
|
|
39
|
+
sign-in form shown for `refused` stores the new token and calls
|
|
40
|
+
`useVantage().reconnect()`; the gate shows the children again once the
|
|
41
|
+
connection opens (SKILL.md, Connection states).
|
|
42
|
+
|
|
43
|
+
## observer
|
|
44
|
+
|
|
45
|
+
`observer(Component)` re-renders a component when anything it read during
|
|
46
|
+
render changes: a field, a collection's members, a reference's value. It
|
|
47
|
+
re-renders on nothing else.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// test: react#collection
|
|
51
|
+
import { observer, useCollection, useVantage } from "@vantage-ui/client/react";
|
|
52
|
+
import type { Schema } from "./vantage.gen";
|
|
53
|
+
|
|
54
|
+
const OpenIssues = observer(() => {
|
|
55
|
+
const db = useVantage<Schema>();
|
|
56
|
+
const { items, loadState, error } = useCollection(db.issues.where({ status: "open" }));
|
|
57
|
+
if (error) return <p role="alert">{error}</p>;
|
|
58
|
+
if (loadState === "loading") return <p>Loading…</p>;
|
|
59
|
+
return <ul>{items.map((issue) => <li key={issue.id}>{issue.title}</li>)}</ul>;
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`useCollection` returns `{ items, loadState, total, totalExact, length, error }`.
|
|
64
|
+
Build the collection inline, as above: the same description is the same
|
|
65
|
+
object, so it needs no `useMemo`.
|
|
66
|
+
|
|
67
|
+
## Windows
|
|
68
|
+
|
|
69
|
+
For virtualised lists, `useWindow(collection, start, count)` gives the rows
|
|
70
|
+
`start..start+count`. Moving `start` moves the server's window; rows stay
|
|
71
|
+
on screen until the new ones arrive. Size the virtualiser from
|
|
72
|
+
`page.length`, show it as a count only when `page.lengthExact`, and use
|
|
73
|
+
`page.settled` to tell "still loading" from "no rows here".
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// test: react#window
|
|
77
|
+
import { observer, useVantage, useWindow } from "@vantage-ui/client/react";
|
|
78
|
+
import type { Schema } from "./vantage.gen";
|
|
79
|
+
|
|
80
|
+
const IssueRows = observer(({ start }: { start: number }) => {
|
|
81
|
+
const page = useWindow(useVantage<Schema>().issues.sort("created_at", "desc"), start, 50);
|
|
82
|
+
if (page.error) return <p role="alert">{page.error}</p>;
|
|
83
|
+
return <>{page.items.map((issue, i) => <div key={start + i}>{issue?.title ?? "…"}</div>)}</>;
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## One record
|
|
88
|
+
|
|
89
|
+
`useRecord(reference)` returns `{ value, status, error }`. `status` is
|
|
90
|
+
`none`, `loading`, `loaded`, `missing` or `error`; `error` holds the
|
|
91
|
+
server's message when it refused the record. Unlike collections, each
|
|
92
|
+
`ref()` call is a new reference, so keep it stable with `useMemo`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// test: react#record
|
|
96
|
+
import { useRecord, useVantage } from "@vantage-ui/client/react";
|
|
97
|
+
import { useMemo } from "react";
|
|
98
|
+
import type { Schema } from "./vantage.gen";
|
|
99
|
+
|
|
100
|
+
function IssueTitle({ id }: { id: string }) {
|
|
101
|
+
const db = useVantage<Schema>();
|
|
102
|
+
const ref = useMemo(() => db.issues.ref(id), [db, id]);
|
|
103
|
+
const { value, status, error } = useRecord(ref);
|
|
104
|
+
if (status === "error") return <p role="alert">{error}</p>;
|
|
105
|
+
if (status === "missing") return <p>No issue {id}</p>;
|
|
106
|
+
return <h1>{value?.title ?? "…"}</h1>;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`useHydrated(target, keys?)` suspends until a model's references, a
|
|
111
|
+
reference or a collection have loaded; wrap the component in `<Suspense>`.
|
|
112
|
+
It also stops suspending when the server refuses the load or the
|
|
113
|
+
connection halts: the collection's `error`, or the reference's `error`
|
|
114
|
+
(with `status: "error"`), then holds the message.
|
|
115
|
+
|
|
116
|
+
## Actions
|
|
117
|
+
|
|
118
|
+
`useAction(fn)` wraps an action for a button: `run`, `pending`, and the last
|
|
119
|
+
`error`. Call row actions on the model inside the function, so `this` is
|
|
120
|
+
kept:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// test: react#action
|
|
124
|
+
import { observer, useAction } from "@vantage-ui/client/react";
|
|
125
|
+
import type { Issues } from "./vantage.gen";
|
|
126
|
+
|
|
127
|
+
const CloseButton = observer(({ issue }: { issue: Issues }) => {
|
|
128
|
+
const close = useAction(() => issue.close_issue({ resolution: "fixed" }));
|
|
129
|
+
if (!issue.$actions.includes("close_issue")) return null;
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<button disabled={close.pending} onClick={() => void close.run()}>Close</button>
|
|
133
|
+
{close.error && <span role="alert">{close.error.message}</span>}
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Runs
|
|
140
|
+
|
|
141
|
+
`useRun(db.run.<action>, { key? })` gives the component one run, idle until
|
|
142
|
+
`start`. With `key`, its id is kept in the tab's `sessionStorage`, per
|
|
143
|
+
action: after a page reload the same component attaches to the run again
|
|
144
|
+
and continues from its current state. A stored run the server no longer has
|
|
145
|
+
(expired, or not yours) is forgotten and the run is `idle` again;
|
|
146
|
+
`run.forget()` does the same on purpose, to stop showing a finished run. A
|
|
147
|
+
new `key` is a new run.
|
|
148
|
+
`useShared(run, "name")` reads one variable, or gives `[value, set]` for an
|
|
149
|
+
input; only components that read a variable re-render when it changes.
|
|
150
|
+
`useRunMessage(run, "name", fn)` calls `fn` for each message while mounted,
|
|
151
|
+
in order; `"vantage.notify"` is the body's `notify(text)` (`m.text`).
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// test: react#run
|
|
155
|
+
import { useRun, useRunMessage, useShared, useVantage } from "@vantage-ui/client/react";
|
|
156
|
+
import type { Schema } from "./vantage.gen";
|
|
157
|
+
|
|
158
|
+
// notifyUser is your UI's
|
|
159
|
+
function Triage({ project }: { project: string }) {
|
|
160
|
+
const db = useVantage<Schema>();
|
|
161
|
+
const triage = useRun(db.run.triage, { key: `triage:${project}` });
|
|
162
|
+
const progress = useShared(triage, "progress");
|
|
163
|
+
const total = useShared(triage, "total");
|
|
164
|
+
const [confirmed, setConfirmed] = useShared(triage, "confirmed");
|
|
165
|
+
useRunMessage(triage, "flagged", (m) => notifyUser(`${m.issue} has no assignee`));
|
|
166
|
+
useRunMessage(triage, "vantage.notify", (m) => notifyUser(m.text));
|
|
167
|
+
if (triage.status === "idle") return <button onClick={() => triage.start({ project })}>Triage</button>;
|
|
168
|
+
if (triage.status === "running" && !confirmed) return <button onClick={() => void setConfirmed(true)}>Confirm {total} issues</button>;
|
|
169
|
+
return <progress value={progress} max={total || 1} />;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`useRunMessage` hears only the messages that arrive while the component is
|
|
174
|
+
mounted. After a page reload, the run attaches, and the messages sent
|
|
175
|
+
before then come back as history in `run.messages`, not as events. A list
|
|
176
|
+
that has to survive a reload (the issues a run flagged) is therefore built
|
|
177
|
+
from `run.messages`, in an `observer` component so that it re-renders as
|
|
178
|
+
messages arrive. Each entry is
|
|
179
|
+
`{ name, payload }`, oldest first. The list holds the run's last 100
|
|
180
|
+
messages, the framework's `vantage.notify` and `vantage.log` included.
|
|
181
|
+
Compare `name` to pick out one kind of message; the comparison narrows
|
|
182
|
+
`payload` to that message's type:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// test: react#run-history
|
|
186
|
+
import { observer, useRun, useVantage } from "@vantage-ui/client/react";
|
|
187
|
+
import type { Schema } from "./vantage.gen";
|
|
188
|
+
|
|
189
|
+
const Flagged = observer(() => {
|
|
190
|
+
const db = useVantage<Schema>();
|
|
191
|
+
const triage = useRun(db.run.triage, { key: "triage:JIR" });
|
|
192
|
+
const flagged = triage.messages.flatMap((m) => (m.name === "flagged" ? [m.payload.issue] : []));
|
|
193
|
+
if (triage.status === "idle") return <button onClick={() => triage.start({ project: "JIR" })}>Triage</button>;
|
|
194
|
+
return <ul>{flagged.map((issue) => <li key={issue}>{issue} has no assignee</li>)}</ul>;
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Forms, confirms and multi-step wizards are your components: collect what
|
|
199
|
+
the action needs, then `start` it once with the values.
|
|
200
|
+
|
|
201
|
+
`run.start(args)` always begins a new run with a new id; it never attaches.
|
|
202
|
+
On a finished run (`done`, `failed`, `cancelled`) that is "run it again":
|
|
203
|
+
no `forget()` first. With a `key`, the new id replaces the stored one. A
|
|
204
|
+
page reload shows the last run, finished or not, until the next `start`.
|
|
205
|
+
On a running run, `start` switches the handle to the new run and leaves
|
|
206
|
+
the old one running on the server; `cancel()` it first if it should stop.
|
|
207
|
+
|
|
208
|
+
`check.start()` with no argument type-checks when the action declares no
|
|
209
|
+
args (the generated `args` is `{}`), or when all its args are optional;
|
|
210
|
+
`start({})` means the same. When an arg is required, the argument is
|
|
211
|
+
required too, and leaving it out is a type error.
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// test: react#run-again
|
|
215
|
+
import { useRun, useShared, useVantage } from "@vantage-ui/client/react";
|
|
216
|
+
import type { Schema } from "./vantage.gen";
|
|
217
|
+
|
|
218
|
+
function CheckOverdue() {
|
|
219
|
+
const db = useVantage<Schema>();
|
|
220
|
+
const check = useRun(db.run.check_overdue, { key: "desk" });
|
|
221
|
+
const overdue = useShared(check, "overdue");
|
|
222
|
+
const busy = check.status === "starting" || check.status === "running";
|
|
223
|
+
return (
|
|
224
|
+
<>
|
|
225
|
+
<button disabled={busy} onClick={() => check.start()}>Check overdue loans</button>
|
|
226
|
+
{check.status === "done" && <p>{overdue} overdue</p>}
|
|
227
|
+
</>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Drafts in forms
|
|
233
|
+
|
|
234
|
+
`useServo(model)` gives an edit draft that re-renders the form as it
|
|
235
|
+
changes: set fields, read `$isDirty`, `$status` and `$errors`, then
|
|
236
|
+
`$flash()`.
|
|
237
|
+
|
|
238
|
+
## Other hooks
|
|
239
|
+
|
|
240
|
+
- `usePreloadOnIntent(load)`: starts `load` when the pointer rests on an
|
|
241
|
+
element, so the data is there by the click.
|
|
242
|
+
- `useLive(fn, deps)`: any derived value, recomputed when what it reads changes.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
`@vantage-ui/client/testing` has `MockServer`: an in-memory vantage-server
|
|
4
|
+
that speaks the same frames, with no network. Collections filter, sort,
|
|
5
|
+
window and follow `via` like the real one; writes broadcast `changed` and
|
|
6
|
+
move collection membership. Use it in unit tests and Storybook.
|
|
7
|
+
|
|
8
|
+
## With the generated schema
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// test: testing#generated
|
|
12
|
+
import { createTestVantage } from "@vantage-ui/client/testing";
|
|
13
|
+
import { schema, type Schema } from "./vantage.gen";
|
|
14
|
+
|
|
15
|
+
const { db, server } = createTestVantage<Schema>(schema, {
|
|
16
|
+
tables: { issues: [{ id: "JIR-1", title: "Login fails", status: "open", project: "JIR" }] },
|
|
17
|
+
actions: { "issues.close_issue": () => ({ status: "done" }) },
|
|
18
|
+
});
|
|
19
|
+
await db.ready;
|
|
20
|
+
const issue = await db.issues.ref("JIR-1").load();
|
|
21
|
+
server.upsert("issues", { id: "JIR-1", title: "Login fails on Safari" }); // someone else's edit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Fixtures also take `available` (which row actions a record offers, standing
|
|
25
|
+
in for `when:`), `validate` (refuse writes with a message or field errors),
|
|
26
|
+
`protocol` (the version the mock speaks) and `refuseHello` (refuse every
|
|
27
|
+
token with a message).
|
|
28
|
+
|
|
29
|
+
A third argument passes client options, such as `{ token: "test" }`.
|
|
30
|
+
|
|
31
|
+
## Runs
|
|
32
|
+
|
|
33
|
+
An action's handler is its body: `(row, args, run)`. Its return value (or
|
|
34
|
+
what its promise resolves to) is the run's `result`, and a row action's
|
|
35
|
+
returned object is applied to the row. `run` stands in for the body's run
|
|
36
|
+
vocabulary: `run.set(name, value)` sends a state patch, `run.wait(name)`
|
|
37
|
+
resolves once an input is set, `run.emit(name, payload)` and
|
|
38
|
+
`run.notify(text)` send messages. `server.runs` holds every run by id, with
|
|
39
|
+
its `action`, `args`, `state` and `status`:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// test: testing#runs
|
|
43
|
+
// continues from "With the generated schema": createTestVantage, schema, Schema
|
|
44
|
+
import { expect } from "vitest";
|
|
45
|
+
|
|
46
|
+
const triage = createTestVantage<Schema>(schema, {
|
|
47
|
+
actions: {
|
|
48
|
+
triage: async (_, args, run) => {
|
|
49
|
+
run.set("total", 1);
|
|
50
|
+
await run.wait("confirmed");
|
|
51
|
+
run.emit("flagged", { issue: "JIR-1" });
|
|
52
|
+
return `triaged ${args.project}`;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await triage.db.ready;
|
|
57
|
+
const run = triage.db.run.triage.start({ project: "JIR" });
|
|
58
|
+
await run.set("confirmed", true);
|
|
59
|
+
expect(await run.done).toEqual({ ok: true, value: "triaged JIR" });
|
|
60
|
+
expect(triage.server.runs.get(run.id)?.args).toEqual({ project: "JIR" });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Fixture `actions` are keyed by the action's wire name, the name `db.run`
|
|
64
|
+
uses: a project action's key with dashes turned into underscores
|
|
65
|
+
(`check_overdue` for `action/check-overdue.yaml`), and `<entity>.<name>`
|
|
66
|
+
for a row action. The YAML key with dashes (`"check-overdue"`) works too;
|
|
67
|
+
naming one action both ways throws. Type a handler declared on its own
|
|
68
|
+
with `ActionHandler`, and its `run` parameter with `MockRunControl`:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// test: testing#action-names
|
|
72
|
+
// continues from "With the generated schema": createTestVantage, schema, Schema
|
|
73
|
+
import { type ActionHandler, type MockRunControl } from "@vantage-ui/client/testing";
|
|
74
|
+
import { expect } from "vitest";
|
|
75
|
+
|
|
76
|
+
const checkOverdue: ActionHandler = async (_, __, run: MockRunControl) => {
|
|
77
|
+
run.set("overdue", 3);
|
|
78
|
+
return "3 tools are overdue";
|
|
79
|
+
};
|
|
80
|
+
const desk = createTestVantage<Schema>(schema, { actions: { check_overdue: checkOverdue } });
|
|
81
|
+
await desk.db.ready;
|
|
82
|
+
const run = desk.db.run.check_overdue.start({});
|
|
83
|
+
expect(await run.done).toEqual({ ok: true, value: "3 tools are overdue" });
|
|
84
|
+
expect(run.state.overdue).toBe(3);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`server.refuse("run_set", message)` refuses input writes, so a test can
|
|
88
|
+
check the rollback; `run_set` of a non-input or a wrong type is refused as
|
|
89
|
+
the real server does. `run.update({ a, b })` sends several variables in one
|
|
90
|
+
patch, as the server's throttle does. The fixture `eager: true` starts a
|
|
91
|
+
handler before the invoking socket follows the run, as vantage-server does,
|
|
92
|
+
so what it sends first arrives inside the first snapshot; `features: []`
|
|
93
|
+
is a server without runs.
|
|
94
|
+
|
|
95
|
+
## Without generated types
|
|
96
|
+
|
|
97
|
+
`MockServer.withTable` builds a one-entity schema from the rows' keys.
|
|
98
|
+
`db.table(name)` reaches any entity by name:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// test: testing#one-table
|
|
102
|
+
import { MockServer, createTestVantage } from "@vantage-ui/client/testing";
|
|
103
|
+
|
|
104
|
+
const server = MockServer.withTable("issues", [{ id: "1", title: "First" }]);
|
|
105
|
+
const { db } = createTestVantage(server, { token: "test" });
|
|
106
|
+
const all = await db.table("issues").all.load();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Refusals, held replies and resync
|
|
110
|
+
|
|
111
|
+
The server-side events a test can cause:
|
|
112
|
+
- `server.refuse(op, message)` answers every `op` frame (`open_table`,
|
|
113
|
+
`open_records`, `viewport`, …) with that error; `server.allow(op)` stops it;
|
|
114
|
+
- `server.refuseWrites(message)` refuses every write, whether the client
|
|
115
|
+
sends it alone or batched with others; `server.allowWrites()` stops it;
|
|
116
|
+
- `server.hold(op)` keeps `op` replies pending until `server.release(op)`;
|
|
117
|
+
- `server.resync()` is an API reload: a new journal, every collection gone,
|
|
118
|
+
`resync` sent to every client;
|
|
119
|
+
- `server.reload()` sends `reload` to every client, which calls its
|
|
120
|
+
`onReload`;
|
|
121
|
+
- `server.invalidate(table)` is a coarse change: clients re-read the table;
|
|
122
|
+
- `server.opened` lists every collection a client opened (table, `where`,
|
|
123
|
+
`sort`, …), refused ones too; `server.count(op)` counts the frames
|
|
124
|
+
received, and `server.hellos` keeps every `hello`.
|
|
125
|
+
|
|
126
|
+
Assert on what the page asked for through these, not by spying on the
|
|
127
|
+
transport's frames:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// test: testing#opened
|
|
131
|
+
// continues from "Without generated types": server, db
|
|
132
|
+
import { expect } from "vitest";
|
|
133
|
+
|
|
134
|
+
await db.table("issues").where({ title: "First" }).load();
|
|
135
|
+
expect(server.opened.at(-1)).toEqual({ table: "issues", where: [["title", "eq", "First"]] });
|
|
136
|
+
server.refuseWrites("issues: column `title` is not writable");
|
|
137
|
+
const issue = await db.table("issues").ref("1").load();
|
|
138
|
+
const saved = await issue!.patch({ title: "Renamed" });
|
|
139
|
+
expect(saved.ok).toBe(false);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// test: testing#refusal
|
|
144
|
+
// continues from the previous block: server, db
|
|
145
|
+
import { expect } from "vitest";
|
|
146
|
+
|
|
147
|
+
server.refuse("open_table", "invalid column `x` is not filterable");
|
|
148
|
+
const refused = await db.table("issues").where({ x: 1 }).load();
|
|
149
|
+
expect(refused.error).toBe("invalid column `x` is not filterable");
|
|
150
|
+
expect(server.count("open_table")).toBe(1); // a refusal is not retried
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// test: testing#resync
|
|
155
|
+
// continues from "Without generated types": server, db
|
|
156
|
+
import { expect } from "vitest";
|
|
157
|
+
|
|
158
|
+
const rows = () => db.table("issues").all.map((r) => r.title);
|
|
159
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 5));
|
|
160
|
+
await db.table("issues").all.load();
|
|
161
|
+
server.setRows("issues", [{ id: "1", title: "First, renamed" }]); // the reloaded API's data
|
|
162
|
+
|
|
163
|
+
server.hold("open_table"); // keep the reopen pending
|
|
164
|
+
server.resync();
|
|
165
|
+
await settle();
|
|
166
|
+
expect(rows()).toEqual(["First"]); // still shown, no flash to empty
|
|
167
|
+
server.release("open_table");
|
|
168
|
+
await settle();
|
|
169
|
+
expect(rows()).toEqual(["First, renamed"]);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`transport.drop()` (from `createTestVantage`'s result) simulates a lost
|
|
173
|
+
connection; `transport.drop(1008, reason)` a refused one.
|
package/README.md
DELETED