atomirx 0.0.8 → 0.1.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 +198 -2234
- package/bin/cli.js +90 -0
- package/dist/core/derived.d.ts +2 -2
- package/dist/core/effect.d.ts +3 -2
- package/dist/core/onCreateHook.d.ts +15 -2
- package/dist/core/onErrorHook.d.ts +4 -1
- package/dist/core/pool.d.ts +78 -0
- package/dist/core/pool.test.d.ts +1 -0
- package/dist/core/select-boolean.test.d.ts +1 -0
- package/dist/core/select-pool.test.d.ts +1 -0
- package/dist/core/select.d.ts +278 -86
- package/dist/core/types.d.ts +233 -1
- package/dist/core/withAbort.d.ts +95 -0
- package/dist/core/withReady.d.ts +3 -3
- package/dist/devtools/constants.d.ts +41 -0
- package/dist/devtools/index.cjs +1 -0
- package/dist/devtools/index.d.ts +29 -0
- package/dist/devtools/index.js +429 -0
- package/dist/devtools/registry.d.ts +98 -0
- package/dist/devtools/registry.test.d.ts +1 -0
- package/dist/devtools/setup.d.ts +61 -0
- package/dist/devtools/types.d.ts +311 -0
- package/dist/index-BZEnfIcB.cjs +1 -0
- package/dist/index-BbPZhsDl.js +1653 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +18 -14
- package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
- package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
- package/dist/onErrorHook-BGGy3tqK.js +38 -0
- package/dist/onErrorHook-DHBASmYw.cjs +1 -0
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.js +191 -151
- package/dist/react/onDispatchHook.d.ts +106 -0
- package/dist/react/useAction.d.ts +4 -1
- package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
- package/dist/react-devtools/EntityDetails.d.ts +10 -0
- package/dist/react-devtools/EntityList.d.ts +15 -0
- package/dist/react-devtools/LogList.d.ts +12 -0
- package/dist/react-devtools/hooks.d.ts +50 -0
- package/dist/react-devtools/index.cjs +1 -0
- package/dist/react-devtools/index.d.ts +31 -0
- package/dist/react-devtools/index.js +1589 -0
- package/dist/react-devtools/styles.d.ts +148 -0
- package/package.json +26 -2
- package/skills/atomirx/SKILL.md +456 -0
- package/skills/atomirx/references/async-patterns.md +188 -0
- package/skills/atomirx/references/atom-patterns.md +238 -0
- package/skills/atomirx/references/deferred-loading.md +191 -0
- package/skills/atomirx/references/derived-patterns.md +428 -0
- package/skills/atomirx/references/effect-patterns.md +426 -0
- package/skills/atomirx/references/error-handling.md +140 -0
- package/skills/atomirx/references/hooks.md +322 -0
- package/skills/atomirx/references/pool-patterns.md +229 -0
- package/skills/atomirx/references/react-integration.md +411 -0
- package/skills/atomirx/references/rules.md +407 -0
- package/skills/atomirx/references/select-context.md +309 -0
- package/skills/atomirx/references/service-template.md +172 -0
- package/skills/atomirx/references/store-template.md +205 -0
- package/skills/atomirx/references/testing-patterns.md +431 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -1440
- package/coverage/coverage-final.json +0 -14
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/core/atom.ts.html +0 -889
- package/coverage/src/core/batch.ts.html +0 -223
- package/coverage/src/core/define.ts.html +0 -805
- package/coverage/src/core/emitter.ts.html +0 -919
- package/coverage/src/core/equality.ts.html +0 -631
- package/coverage/src/core/hook.ts.html +0 -460
- package/coverage/src/core/index.html +0 -281
- package/coverage/src/core/isAtom.ts.html +0 -100
- package/coverage/src/core/isPromiseLike.ts.html +0 -133
- package/coverage/src/core/onCreateHook.ts.html +0 -138
- package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
- package/coverage/src/core/types.ts.html +0 -523
- package/coverage/src/core/withUse.ts.html +0 -253
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -106
- package/dist/index-CBVj1kSj.js +0 -1350
- package/dist/index-Cxk9v0um.cjs +0 -1
- package/scripts/publish.js +0 -198
- package/src/core/atom.test.ts +0 -633
- package/src/core/atom.ts +0 -311
- package/src/core/atomState.test.ts +0 -342
- package/src/core/atomState.ts +0 -256
- package/src/core/batch.test.ts +0 -257
- package/src/core/batch.ts +0 -172
- package/src/core/define.test.ts +0 -343
- package/src/core/define.ts +0 -243
- package/src/core/derived.test.ts +0 -1215
- package/src/core/derived.ts +0 -450
- package/src/core/effect.test.ts +0 -802
- package/src/core/effect.ts +0 -188
- package/src/core/emitter.test.ts +0 -364
- package/src/core/emitter.ts +0 -392
- package/src/core/equality.test.ts +0 -392
- package/src/core/equality.ts +0 -182
- package/src/core/getAtomState.ts +0 -69
- package/src/core/hook.test.ts +0 -227
- package/src/core/hook.ts +0 -177
- package/src/core/isAtom.ts +0 -27
- package/src/core/isPromiseLike.test.ts +0 -72
- package/src/core/isPromiseLike.ts +0 -16
- package/src/core/onCreateHook.ts +0 -107
- package/src/core/onErrorHook.test.ts +0 -350
- package/src/core/onErrorHook.ts +0 -52
- package/src/core/promiseCache.test.ts +0 -241
- package/src/core/promiseCache.ts +0 -284
- package/src/core/scheduleNotifyHook.ts +0 -53
- package/src/core/select.ts +0 -729
- package/src/core/selector.test.ts +0 -799
- package/src/core/types.ts +0 -389
- package/src/core/withReady.test.ts +0 -534
- package/src/core/withReady.ts +0 -191
- package/src/core/withUse.test.ts +0 -249
- package/src/core/withUse.ts +0 -56
- package/src/index.test.ts +0 -80
- package/src/index.ts +0 -65
- package/src/react/index.ts +0 -21
- package/src/react/rx.test.tsx +0 -571
- package/src/react/rx.tsx +0 -531
- package/src/react/strictModeTest.tsx +0 -71
- package/src/react/useAction.test.ts +0 -987
- package/src/react/useAction.ts +0 -607
- package/src/react/useSelector.test.ts +0 -182
- package/src/react/useSelector.ts +0 -292
- package/src/react/useStable.test.ts +0 -553
- package/src/react/useStable.ts +0 -288
- package/tsconfig.json +0 -9
- package/v2.md +0 -725
- package/vite.config.ts +0 -42
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Core Principles
|
|
4
|
+
|
|
5
|
+
1. **Isolate** — Each test gets fresh atoms
|
|
6
|
+
2. **Override** — Use `define().override()` for mocks
|
|
7
|
+
3. **Reset hooks** — Clear between tests
|
|
8
|
+
4. **Test behaviors** — Not internals
|
|
9
|
+
|
|
10
|
+
## Testing Atoms
|
|
11
|
+
|
|
12
|
+
### Basic
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
describe("counterAtom", () => {
|
|
16
|
+
it("should initialize to 0", () => {
|
|
17
|
+
const count$ = atom(0, { meta: { key: "test.count" } });
|
|
18
|
+
expect(count$.get()).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should update value", () => {
|
|
22
|
+
const count$ = atom(0);
|
|
23
|
+
count$.set(5);
|
|
24
|
+
expect(count$.get()).toBe(5);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should use reducer", () => {
|
|
28
|
+
const count$ = atom(0);
|
|
29
|
+
count$.set((prev) => prev + 1);
|
|
30
|
+
expect(count$.get()).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should reset to initial", () => {
|
|
34
|
+
const count$ = atom(() => 10);
|
|
35
|
+
count$.set(99);
|
|
36
|
+
count$.reset();
|
|
37
|
+
expect(count$.get()).toBe(10);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should track dirty state", () => {
|
|
41
|
+
const form$ = atom({ name: "" });
|
|
42
|
+
expect(form$.dirty()).toBe(false);
|
|
43
|
+
form$.set({ name: "John" });
|
|
44
|
+
expect(form$.dirty()).toBe(true);
|
|
45
|
+
form$.reset();
|
|
46
|
+
expect(form$.dirty()).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Subscriptions
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
it("should notify on change", () => {
|
|
55
|
+
const count$ = atom(0);
|
|
56
|
+
const values: number[] = [];
|
|
57
|
+
const unsub = count$.on(() => values.push(count$.get()));
|
|
58
|
+
|
|
59
|
+
count$.set(1);
|
|
60
|
+
count$.set(2);
|
|
61
|
+
expect(values).toEqual([1, 2]);
|
|
62
|
+
|
|
63
|
+
unsub();
|
|
64
|
+
count$.set(3);
|
|
65
|
+
expect(values).toEqual([1, 2]); // No 3
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Async Atoms
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
it("should handle async", async () => {
|
|
73
|
+
const user$ = atom(Promise.resolve({ name: "John" }));
|
|
74
|
+
const result = await user$.get();
|
|
75
|
+
expect(result.name).toBe("John");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should refetch on reset", async () => {
|
|
79
|
+
let callCount = 0;
|
|
80
|
+
const data$ = atom(() => {
|
|
81
|
+
callCount++;
|
|
82
|
+
return Promise.resolve(callCount);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(await data$.get()).toBe(1);
|
|
86
|
+
data$.reset();
|
|
87
|
+
expect(await data$.get()).toBe(2);
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Testing Derived
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
describe("derived", () => {
|
|
95
|
+
it("should compute from source", async () => {
|
|
96
|
+
const count$ = atom(5);
|
|
97
|
+
const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
98
|
+
expect(await doubled$.get()).toBe(10);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should update on source change", async () => {
|
|
102
|
+
const count$ = atom(5);
|
|
103
|
+
const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
104
|
+
|
|
105
|
+
count$.set(10);
|
|
106
|
+
expect(await doubled$.get()).toBe(20);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should combine atoms", async () => {
|
|
110
|
+
const a$ = atom(1);
|
|
111
|
+
const b$ = atom(2);
|
|
112
|
+
const sum$ = derived(({ read }) => read(a$) + read(b$));
|
|
113
|
+
|
|
114
|
+
expect(await sum$.get()).toBe(3);
|
|
115
|
+
a$.set(10);
|
|
116
|
+
expect(await sum$.get()).toBe(12);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should handle async sources", async () => {
|
|
120
|
+
const user$ = atom(Promise.resolve({ name: "John" }));
|
|
121
|
+
const greeting$ = derived(({ read }) => `Hello, ${read(user$).name}`);
|
|
122
|
+
expect(await greeting$.get()).toBe("Hello, John");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should use fallback during loading", () => {
|
|
126
|
+
const data$ = derived(({ read }) => read(atom(Promise.resolve(42))), {
|
|
127
|
+
fallback: 0,
|
|
128
|
+
});
|
|
129
|
+
expect(data$.staleValue).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Testing Effects
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
describe("effect", () => {
|
|
138
|
+
it("should run on creation", () => {
|
|
139
|
+
const log: number[] = [];
|
|
140
|
+
const count$ = atom(5);
|
|
141
|
+
effect(({ read }) => log.push(read(count$)));
|
|
142
|
+
expect(log).toEqual([5]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should run on change", () => {
|
|
146
|
+
const log: number[] = [];
|
|
147
|
+
const count$ = atom(0);
|
|
148
|
+
effect(({ read }) => log.push(read(count$)));
|
|
149
|
+
|
|
150
|
+
count$.set(1);
|
|
151
|
+
count$.set(2);
|
|
152
|
+
expect(log).toEqual([0, 1, 2]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should cleanup", () => {
|
|
156
|
+
const cleanups: number[] = [];
|
|
157
|
+
const count$ = atom(0);
|
|
158
|
+
|
|
159
|
+
effect(({ read, onCleanup }) => {
|
|
160
|
+
const val = read(count$);
|
|
161
|
+
onCleanup(() => cleanups.push(val));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
count$.set(1);
|
|
165
|
+
expect(cleanups).toEqual([0]);
|
|
166
|
+
count$.set(2);
|
|
167
|
+
expect(cleanups).toEqual([0, 1]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should dispose", () => {
|
|
171
|
+
const log: number[] = [];
|
|
172
|
+
const count$ = atom(0);
|
|
173
|
+
const e = effect(({ read }) => log.push(read(count$)));
|
|
174
|
+
|
|
175
|
+
count$.set(1);
|
|
176
|
+
expect(log).toEqual([0, 1]);
|
|
177
|
+
|
|
178
|
+
e.dispose();
|
|
179
|
+
count$.set(2);
|
|
180
|
+
expect(log).toEqual([0, 1]); // No 2
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Testing Stores (define)
|
|
186
|
+
|
|
187
|
+
### Mocking Services
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const authService = define((): AuthService => ({
|
|
191
|
+
login: async () => ({ success: true, user: { id: "1", name: "Test" } }),
|
|
192
|
+
logout: async () => {},
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
const authStore = define(() => {
|
|
196
|
+
const auth = authService();
|
|
197
|
+
const user$ = atom<User | null>(null);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...readonly({ user$ }),
|
|
201
|
+
login: async () => {
|
|
202
|
+
const result = await auth.login();
|
|
203
|
+
if (result.success) user$.set(result.user);
|
|
204
|
+
},
|
|
205
|
+
logout: () => user$.set(null),
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("authStore", () => {
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
authService.reset();
|
|
212
|
+
authStore.reset();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should login with mock", async () => {
|
|
216
|
+
authService.override(() => ({
|
|
217
|
+
login: async () => ({ success: true, user: { id: "mock", name: "Mock" } }),
|
|
218
|
+
logout: async () => {},
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
const store = authStore();
|
|
222
|
+
await store.login();
|
|
223
|
+
expect(store.user$.get()).toEqual({ id: "mock", name: "Mock" });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should handle login failure", async () => {
|
|
227
|
+
authService.override(() => ({
|
|
228
|
+
login: async () => ({ success: false, error: "Invalid" }),
|
|
229
|
+
logout: async () => {},
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
const store = authStore();
|
|
233
|
+
await store.login();
|
|
234
|
+
expect(store.user$.get()).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Isolation
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
describe("counterStore", () => {
|
|
243
|
+
const counterStore = define(() => {
|
|
244
|
+
const count$ = atom(0);
|
|
245
|
+
return {
|
|
246
|
+
...readonly({ count$ }),
|
|
247
|
+
increment: () => count$.set((p) => p + 1),
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
beforeEach(() => counterStore.reset());
|
|
252
|
+
|
|
253
|
+
it("test 1", () => {
|
|
254
|
+
const store = counterStore();
|
|
255
|
+
store.increment();
|
|
256
|
+
expect(store.count$.get()).toBe(1);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("test 2 (isolated)", () => {
|
|
260
|
+
const store = counterStore();
|
|
261
|
+
expect(store.count$.get()).toBe(0); // Fresh
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Testing Pools
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
describe("userPool", () => {
|
|
270
|
+
const userPool = pool(
|
|
271
|
+
(id: string) => ({ id, name: `User ${id}`, posts: [] }),
|
|
272
|
+
{ gcTime: 60_000 }
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
afterEach(() => userPool.clear());
|
|
276
|
+
|
|
277
|
+
it("should create entries", () => {
|
|
278
|
+
expect(userPool.has("1")).toBe(false);
|
|
279
|
+
userPool.get("1");
|
|
280
|
+
expect(userPool.has("1")).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should update entries", () => {
|
|
284
|
+
userPool.get("1");
|
|
285
|
+
userPool.set("1", (p) => ({ ...p, name: "Updated" }));
|
|
286
|
+
expect(userPool.get("1").name).toBe("Updated");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should remove entries", () => {
|
|
290
|
+
userPool.get("1");
|
|
291
|
+
userPool.remove("1");
|
|
292
|
+
expect(userPool.has("1")).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should work with derived", async () => {
|
|
296
|
+
userPool.set("1", { id: "1", name: "John", posts: [] });
|
|
297
|
+
|
|
298
|
+
const userName$ = derived(({ read, from }) => {
|
|
299
|
+
const user$ = from(userPool, "1");
|
|
300
|
+
return read(user$).name;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(await userName$.get()).toBe("John");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Testing Hooks
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
describe("onCreateHook", () => {
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
onCreateHook.reset();
|
|
314
|
+
onErrorHook.reset();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
afterEach(() => {
|
|
318
|
+
onCreateHook.reset();
|
|
319
|
+
onErrorHook.reset();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should track atoms", () => {
|
|
323
|
+
const created: string[] = [];
|
|
324
|
+
onCreateHook.override((prev) => (info) => {
|
|
325
|
+
prev?.(info);
|
|
326
|
+
if (info.key) created.push(info.key);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
atom(0, { meta: { key: "test.a" } });
|
|
330
|
+
atom(0, { meta: { key: "test.b" } });
|
|
331
|
+
|
|
332
|
+
expect(created).toContain("test.a");
|
|
333
|
+
expect(created).toContain("test.b");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("onErrorHook", () => {
|
|
338
|
+
beforeEach(() => onErrorHook.reset());
|
|
339
|
+
afterEach(() => onErrorHook.reset());
|
|
340
|
+
|
|
341
|
+
it("should capture errors", async () => {
|
|
342
|
+
const errors: unknown[] = [];
|
|
343
|
+
onErrorHook.override((prev) => (info) => {
|
|
344
|
+
prev?.(info);
|
|
345
|
+
errors.push(info.error);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const buggy$ = derived(() => { throw new Error("test"); });
|
|
349
|
+
|
|
350
|
+
try { await buggy$.get(); } catch {}
|
|
351
|
+
expect(errors).toHaveLength(1);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Testing React
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
import { render, screen, act } from "@testing-library/react";
|
|
360
|
+
import { Suspense } from "react";
|
|
361
|
+
|
|
362
|
+
describe("useSelector", () => {
|
|
363
|
+
const count$ = atom(0, { meta: { key: "test.count" } });
|
|
364
|
+
|
|
365
|
+
beforeEach(() => count$.reset());
|
|
366
|
+
|
|
367
|
+
it("should read value", () => {
|
|
368
|
+
function Counter() {
|
|
369
|
+
const count = useSelector(count$);
|
|
370
|
+
return <span data-testid="count">{count}</span>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
render(<Counter />);
|
|
374
|
+
expect(screen.getByTestId("count")).toHaveTextContent("0");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should update on change", async () => {
|
|
378
|
+
function Counter() {
|
|
379
|
+
const count = useSelector(count$);
|
|
380
|
+
return <span data-testid="count">{count}</span>;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
render(<Counter />);
|
|
384
|
+
await act(() => count$.set(5));
|
|
385
|
+
expect(screen.getByTestId("count")).toHaveTextContent("5");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("rx", () => {
|
|
390
|
+
it("should render inline", () => {
|
|
391
|
+
const name$ = atom("John");
|
|
392
|
+
|
|
393
|
+
render(
|
|
394
|
+
<div data-testid="greeting">
|
|
395
|
+
{rx(({ read }) => <>Hello, {read(name$)}</>)}
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(screen.getByTestId("greeting")).toHaveTextContent("Hello, John");
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("useAction", () => {
|
|
404
|
+
it("should handle async", async () => {
|
|
405
|
+
function Form() {
|
|
406
|
+
const submit = useAction(async () => "done");
|
|
407
|
+
return (
|
|
408
|
+
<>
|
|
409
|
+
<button onClick={() => submit()}>Submit</button>
|
|
410
|
+
<span data-testid="status">{submit.status}</span>
|
|
411
|
+
</>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
render(<Form />);
|
|
416
|
+
expect(screen.getByTestId("status")).toHaveTextContent("idle");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Best Practices
|
|
422
|
+
|
|
423
|
+
| Practice | Description |
|
|
424
|
+
| --------------------- | ----------------------------------- |
|
|
425
|
+
| `reset()` in beforeEach | Fresh state per test |
|
|
426
|
+
| `override()` for mocks | Swap implementations |
|
|
427
|
+
| Test behaviors | Not implementation details |
|
|
428
|
+
| Test subscriptions | Verify reactive updates |
|
|
429
|
+
| Test cleanup | Verify resources released |
|
|
430
|
+
| Use act() | For React state updates |
|
|
431
|
+
| Wrap Suspense | For async atoms in React |
|
package/coverage/base.css
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
body, html {
|
|
2
|
-
margin:0; padding: 0;
|
|
3
|
-
height: 100%;
|
|
4
|
-
}
|
|
5
|
-
body {
|
|
6
|
-
font-family: Helvetica Neue, Helvetica, Arial;
|
|
7
|
-
font-size: 14px;
|
|
8
|
-
color:#333;
|
|
9
|
-
}
|
|
10
|
-
.small { font-size: 12px; }
|
|
11
|
-
*, *:after, *:before {
|
|
12
|
-
-webkit-box-sizing:border-box;
|
|
13
|
-
-moz-box-sizing:border-box;
|
|
14
|
-
box-sizing:border-box;
|
|
15
|
-
}
|
|
16
|
-
h1 { font-size: 20px; margin: 0;}
|
|
17
|
-
h2 { font-size: 14px; }
|
|
18
|
-
pre {
|
|
19
|
-
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
20
|
-
margin: 0;
|
|
21
|
-
padding: 0;
|
|
22
|
-
-moz-tab-size: 2;
|
|
23
|
-
-o-tab-size: 2;
|
|
24
|
-
tab-size: 2;
|
|
25
|
-
}
|
|
26
|
-
a { color:#0074D9; text-decoration:none; }
|
|
27
|
-
a:hover { text-decoration:underline; }
|
|
28
|
-
.strong { font-weight: bold; }
|
|
29
|
-
.space-top1 { padding: 10px 0 0 0; }
|
|
30
|
-
.pad2y { padding: 20px 0; }
|
|
31
|
-
.pad1y { padding: 10px 0; }
|
|
32
|
-
.pad2x { padding: 0 20px; }
|
|
33
|
-
.pad2 { padding: 20px; }
|
|
34
|
-
.pad1 { padding: 10px; }
|
|
35
|
-
.space-left2 { padding-left:55px; }
|
|
36
|
-
.space-right2 { padding-right:20px; }
|
|
37
|
-
.center { text-align:center; }
|
|
38
|
-
.clearfix { display:block; }
|
|
39
|
-
.clearfix:after {
|
|
40
|
-
content:'';
|
|
41
|
-
display:block;
|
|
42
|
-
height:0;
|
|
43
|
-
clear:both;
|
|
44
|
-
visibility:hidden;
|
|
45
|
-
}
|
|
46
|
-
.fl { float: left; }
|
|
47
|
-
@media only screen and (max-width:640px) {
|
|
48
|
-
.col3 { width:100%; max-width:100%; }
|
|
49
|
-
.hide-mobile { display:none!important; }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.quiet {
|
|
53
|
-
color: #7f7f7f;
|
|
54
|
-
color: rgba(0,0,0,0.5);
|
|
55
|
-
}
|
|
56
|
-
.quiet a { opacity: 0.7; }
|
|
57
|
-
|
|
58
|
-
.fraction {
|
|
59
|
-
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
60
|
-
font-size: 10px;
|
|
61
|
-
color: #555;
|
|
62
|
-
background: #E8E8E8;
|
|
63
|
-
padding: 4px 5px;
|
|
64
|
-
border-radius: 3px;
|
|
65
|
-
vertical-align: middle;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
div.path a:link, div.path a:visited { color: #333; }
|
|
69
|
-
table.coverage {
|
|
70
|
-
border-collapse: collapse;
|
|
71
|
-
margin: 10px 0 0 0;
|
|
72
|
-
padding: 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
table.coverage td {
|
|
76
|
-
margin: 0;
|
|
77
|
-
padding: 0;
|
|
78
|
-
vertical-align: top;
|
|
79
|
-
}
|
|
80
|
-
table.coverage td.line-count {
|
|
81
|
-
text-align: right;
|
|
82
|
-
padding: 0 5px 0 20px;
|
|
83
|
-
}
|
|
84
|
-
table.coverage td.line-coverage {
|
|
85
|
-
text-align: right;
|
|
86
|
-
padding-right: 10px;
|
|
87
|
-
min-width:20px;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
table.coverage td span.cline-any {
|
|
91
|
-
display: inline-block;
|
|
92
|
-
padding: 0 5px;
|
|
93
|
-
width: 100%;
|
|
94
|
-
}
|
|
95
|
-
.missing-if-branch {
|
|
96
|
-
display: inline-block;
|
|
97
|
-
margin-right: 5px;
|
|
98
|
-
border-radius: 3px;
|
|
99
|
-
position: relative;
|
|
100
|
-
padding: 0 4px;
|
|
101
|
-
background: #333;
|
|
102
|
-
color: yellow;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.skip-if-branch {
|
|
106
|
-
display: none;
|
|
107
|
-
margin-right: 10px;
|
|
108
|
-
position: relative;
|
|
109
|
-
padding: 0 4px;
|
|
110
|
-
background: #ccc;
|
|
111
|
-
color: white;
|
|
112
|
-
}
|
|
113
|
-
.missing-if-branch .typ, .skip-if-branch .typ {
|
|
114
|
-
color: inherit !important;
|
|
115
|
-
}
|
|
116
|
-
.coverage-summary {
|
|
117
|
-
border-collapse: collapse;
|
|
118
|
-
width: 100%;
|
|
119
|
-
}
|
|
120
|
-
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
|
121
|
-
.keyline-all { border: 1px solid #ddd; }
|
|
122
|
-
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
|
123
|
-
.coverage-summary tbody { border: 1px solid #bbb; }
|
|
124
|
-
.coverage-summary td { border-right: 1px solid #bbb; }
|
|
125
|
-
.coverage-summary td:last-child { border-right: none; }
|
|
126
|
-
.coverage-summary th {
|
|
127
|
-
text-align: left;
|
|
128
|
-
font-weight: normal;
|
|
129
|
-
white-space: nowrap;
|
|
130
|
-
}
|
|
131
|
-
.coverage-summary th.file { border-right: none !important; }
|
|
132
|
-
.coverage-summary th.pct { }
|
|
133
|
-
.coverage-summary th.pic,
|
|
134
|
-
.coverage-summary th.abs,
|
|
135
|
-
.coverage-summary td.pct,
|
|
136
|
-
.coverage-summary td.abs { text-align: right; }
|
|
137
|
-
.coverage-summary td.file { white-space: nowrap; }
|
|
138
|
-
.coverage-summary td.pic { min-width: 120px !important; }
|
|
139
|
-
.coverage-summary tfoot td { }
|
|
140
|
-
|
|
141
|
-
.coverage-summary .sorter {
|
|
142
|
-
height: 10px;
|
|
143
|
-
width: 7px;
|
|
144
|
-
display: inline-block;
|
|
145
|
-
margin-left: 0.5em;
|
|
146
|
-
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
|
147
|
-
}
|
|
148
|
-
.coverage-summary .sorted .sorter {
|
|
149
|
-
background-position: 0 -20px;
|
|
150
|
-
}
|
|
151
|
-
.coverage-summary .sorted-desc .sorter {
|
|
152
|
-
background-position: 0 -10px;
|
|
153
|
-
}
|
|
154
|
-
.status-line { height: 10px; }
|
|
155
|
-
/* yellow */
|
|
156
|
-
.cbranch-no { background: yellow !important; color: #111; }
|
|
157
|
-
/* dark red */
|
|
158
|
-
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
|
159
|
-
.low .chart { border:1px solid #C21F39 }
|
|
160
|
-
.highlighted,
|
|
161
|
-
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
|
162
|
-
background: #C21F39 !important;
|
|
163
|
-
}
|
|
164
|
-
/* medium red */
|
|
165
|
-
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
|
166
|
-
/* light red */
|
|
167
|
-
.low, .cline-no { background:#FCE1E5 }
|
|
168
|
-
/* light green */
|
|
169
|
-
.high, .cline-yes { background:rgb(230,245,208) }
|
|
170
|
-
/* medium green */
|
|
171
|
-
.cstat-yes { background:rgb(161,215,106) }
|
|
172
|
-
/* dark green */
|
|
173
|
-
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
|
174
|
-
.high .chart { border:1px solid rgb(77,146,33) }
|
|
175
|
-
/* dark yellow (gold) */
|
|
176
|
-
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
|
177
|
-
.medium .chart { border:1px solid #f9cd0b; }
|
|
178
|
-
/* light yellow */
|
|
179
|
-
.medium { background: #fff4c2; }
|
|
180
|
-
|
|
181
|
-
.cstat-skip { background: #ddd; color: #111; }
|
|
182
|
-
.fstat-skip { background: #ddd; color: #111 !important; }
|
|
183
|
-
.cbranch-skip { background: #ddd !important; color: #111; }
|
|
184
|
-
|
|
185
|
-
span.cline-neutral { background: #eaeaea; }
|
|
186
|
-
|
|
187
|
-
.coverage-summary td.empty {
|
|
188
|
-
opacity: .5;
|
|
189
|
-
padding-top: 4px;
|
|
190
|
-
padding-bottom: 4px;
|
|
191
|
-
line-height: 1;
|
|
192
|
-
color: #888;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.cover-fill, .cover-empty {
|
|
196
|
-
display:inline-block;
|
|
197
|
-
height: 12px;
|
|
198
|
-
}
|
|
199
|
-
.chart {
|
|
200
|
-
line-height: 0;
|
|
201
|
-
}
|
|
202
|
-
.cover-empty {
|
|
203
|
-
background: white;
|
|
204
|
-
}
|
|
205
|
-
.cover-full {
|
|
206
|
-
border-right: none !important;
|
|
207
|
-
}
|
|
208
|
-
pre.prettyprint {
|
|
209
|
-
border: none !important;
|
|
210
|
-
padding: 0 !important;
|
|
211
|
-
margin: 0 !important;
|
|
212
|
-
}
|
|
213
|
-
.com { color: #999 !important; }
|
|
214
|
-
.ignore-none { color: #999; font-weight: normal; }
|
|
215
|
-
|
|
216
|
-
.wrapper {
|
|
217
|
-
min-height: 100%;
|
|
218
|
-
height: auto !important;
|
|
219
|
-
height: 100%;
|
|
220
|
-
margin: 0 auto -48px;
|
|
221
|
-
}
|
|
222
|
-
.footer, .push {
|
|
223
|
-
height: 48px;
|
|
224
|
-
}
|