rask-ui 0.28.2 → 0.28.4
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/component.d.ts +1 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +7 -2
- package/dist/tests/batch.test.js +202 -12
- package/dist/tests/createContext.test.js +50 -37
- package/dist/tests/error.test.js +25 -12
- package/dist/tests/renderCount.test.d.ts +2 -0
- package/dist/tests/renderCount.test.d.ts.map +1 -0
- package/dist/tests/renderCount.test.js +95 -0
- package/dist/tests/scopeEnforcement.test.d.ts +2 -0
- package/dist/tests/scopeEnforcement.test.d.ts.map +1 -0
- package/dist/tests/scopeEnforcement.test.js +157 -0
- package/dist/tests/useAction.test.d.ts +2 -0
- package/dist/tests/useAction.test.d.ts.map +1 -0
- package/dist/tests/useAction.test.js +132 -0
- package/dist/tests/useAsync.test.d.ts +2 -0
- package/dist/tests/useAsync.test.d.ts.map +1 -0
- package/dist/tests/useAsync.test.js +499 -0
- package/dist/tests/useDerived.test.d.ts +2 -0
- package/dist/tests/useDerived.test.d.ts.map +1 -0
- package/dist/tests/useDerived.test.js +407 -0
- package/dist/tests/useEffect.test.d.ts +2 -0
- package/dist/tests/useEffect.test.d.ts.map +1 -0
- package/dist/tests/useEffect.test.js +600 -0
- package/dist/tests/useLookup.test.d.ts +2 -0
- package/dist/tests/useLookup.test.d.ts.map +1 -0
- package/dist/tests/useLookup.test.js +299 -0
- package/dist/tests/useRef.test.d.ts +2 -0
- package/dist/tests/useRef.test.d.ts.map +1 -0
- package/dist/tests/useRef.test.js +189 -0
- package/dist/tests/useState.test.d.ts +2 -0
- package/dist/tests/useState.test.d.ts.map +1 -0
- package/dist/tests/useState.test.js +178 -0
- package/dist/tests/useSuspend.test.d.ts +2 -0
- package/dist/tests/useSuspend.test.d.ts.map +1 -0
- package/dist/tests/useSuspend.test.js +752 -0
- package/dist/tests/useView.test.d.ts +2 -0
- package/dist/tests/useView.test.d.ts.map +1 -0
- package/dist/tests/useView.test.js +305 -0
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +1 -5
- package/dist/useState.js +4 -2
- package/package.json +1 -1
package/dist/component.d.ts
CHANGED
package/dist/component.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAsB,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAMrE,wBAAgB,mBAAmB,mCAElC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,QAM5C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,QAMxC;AAED,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC3D,CAAC,MAAM,KAAK,CAAC,GACb,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;AAE1B,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC1D,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;IACpD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAC1C,OAAO,CAAC,aAAa,CAAc;IAwBnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAsB,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAMrE,wBAAgB,mBAAmB,mCAElC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,QAM5C;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,QAMxC;AAED,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC3D,CAAC,MAAM,KAAK,CAAC,GACb,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;AAE1B,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC1D,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;IACpD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAC1C,OAAO,CAAC,aAAa,CAAc;IAwBnC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,mBAAmB,CAAQ;IAEnC,QAAQ,WAOL;IAEH,WAAW,UAAS;IACpB,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,UAAU,CAAC,OAAO,EAAE,OAAO;IAS3B,eAAe;IAMf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IAEnC,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAI5B,yBAAyB,CACvB,SAAS,EAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,CAAC,CAAC,GAClD,IAAI;IAgBP,qBAAqB,IAAI,OAAO;IAMhC,MAAM;CAkDP"}
|
package/dist/component.js
CHANGED
|
@@ -45,6 +45,7 @@ export class RaskComponent extends Component {
|
|
|
45
45
|
// calling forceUpdate() at the wrong time during Inferno's reconciliation.
|
|
46
46
|
isNotified = false;
|
|
47
47
|
isReconciling = false;
|
|
48
|
+
hasChangedComponent = true;
|
|
48
49
|
observer = new Observer(() => {
|
|
49
50
|
if (this.isReconciling) {
|
|
50
51
|
this.isNotified = true;
|
|
@@ -81,6 +82,8 @@ export class RaskComponent extends Component {
|
|
|
81
82
|
this.isReconciling = true;
|
|
82
83
|
const prevProps = this.props;
|
|
83
84
|
this.props = nextProps;
|
|
85
|
+
this.hasChangedComponent =
|
|
86
|
+
prevProps.__component !== this.props.__component;
|
|
84
87
|
syncBatch(() => {
|
|
85
88
|
for (const prop in this.propsSignals) {
|
|
86
89
|
if (prevProps[prop] === nextProps[prop]) {
|
|
@@ -91,7 +94,7 @@ export class RaskComponent extends Component {
|
|
|
91
94
|
});
|
|
92
95
|
}
|
|
93
96
|
shouldComponentUpdate() {
|
|
94
|
-
const shouldRender = this.isNotified;
|
|
97
|
+
const shouldRender = this.isNotified || this.hasChangedComponent;
|
|
95
98
|
this.isNotified = false;
|
|
96
99
|
this.isReconciling = false;
|
|
97
100
|
return shouldRender;
|
|
@@ -100,7 +103,9 @@ export class RaskComponent extends Component {
|
|
|
100
103
|
currentComponent = this;
|
|
101
104
|
const stopObserving = this.observer.observe();
|
|
102
105
|
try {
|
|
103
|
-
if (
|
|
106
|
+
if (this.hasChangedComponent) {
|
|
107
|
+
this.hasChangedComponent = false;
|
|
108
|
+
this.componentWillUnmount();
|
|
104
109
|
this.reactiveProps = createReactiveProps(this);
|
|
105
110
|
const component = this.props.__component;
|
|
106
111
|
const renderFn = component(this.reactiveProps);
|
package/dist/tests/batch.test.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { syncBatch } from "../batch";
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "../useState";
|
|
4
4
|
import { Observer } from "../observation";
|
|
5
5
|
describe("syncBatch", () => {
|
|
6
6
|
it("should batch multiple state changes into a single notification", () => {
|
|
7
|
-
const state =
|
|
7
|
+
const state = useState({ count: 0, name: "Alice" });
|
|
8
8
|
let notifyCount = 0;
|
|
9
9
|
const observer = new Observer(() => {
|
|
10
10
|
notifyCount++;
|
|
@@ -26,7 +26,7 @@ describe("syncBatch", () => {
|
|
|
26
26
|
observer.dispose();
|
|
27
27
|
});
|
|
28
28
|
it("should handle nested batches correctly", () => {
|
|
29
|
-
const state =
|
|
29
|
+
const state = useState({ count: 0 });
|
|
30
30
|
let notifyCount = 0;
|
|
31
31
|
const observer = new Observer(() => {
|
|
32
32
|
notifyCount++;
|
|
@@ -47,7 +47,7 @@ describe("syncBatch", () => {
|
|
|
47
47
|
observer.dispose();
|
|
48
48
|
});
|
|
49
49
|
it("should handle multiple observers with syncBatch", () => {
|
|
50
|
-
const state =
|
|
50
|
+
const state = useState({ count: 0 });
|
|
51
51
|
let notifyCount1 = 0;
|
|
52
52
|
let notifyCount2 = 0;
|
|
53
53
|
const observer1 = new Observer(() => {
|
|
@@ -74,7 +74,7 @@ describe("syncBatch", () => {
|
|
|
74
74
|
observer2.dispose();
|
|
75
75
|
});
|
|
76
76
|
it("should maintain correct state values after syncBatch", () => {
|
|
77
|
-
const state =
|
|
77
|
+
const state = useState({
|
|
78
78
|
count: 0,
|
|
79
79
|
name: "Alice",
|
|
80
80
|
items: [1, 2, 3],
|
|
@@ -90,7 +90,7 @@ describe("syncBatch", () => {
|
|
|
90
90
|
expect(state.items).toEqual([100, 2, 3, 4]);
|
|
91
91
|
});
|
|
92
92
|
it("should not flush if exception thrown within syncBatch", () => {
|
|
93
|
-
const state =
|
|
93
|
+
const state = useState({ count: 0 });
|
|
94
94
|
let notifyCount = 0;
|
|
95
95
|
const observer = new Observer(() => {
|
|
96
96
|
notifyCount++;
|
|
@@ -114,7 +114,7 @@ describe("syncBatch", () => {
|
|
|
114
114
|
observer.dispose();
|
|
115
115
|
});
|
|
116
116
|
it("should deduplicate notifications for the same observer", () => {
|
|
117
|
-
const state =
|
|
117
|
+
const state = useState({ count: 0, name: "Alice" });
|
|
118
118
|
let notifyCount = 0;
|
|
119
119
|
const observer = new Observer(() => {
|
|
120
120
|
notifyCount++;
|
|
@@ -135,7 +135,7 @@ describe("syncBatch", () => {
|
|
|
135
135
|
});
|
|
136
136
|
describe("queue (async batching)", () => {
|
|
137
137
|
it("should queue updates and flush on microtask", async () => {
|
|
138
|
-
const state =
|
|
138
|
+
const state = useState({ count: 0 });
|
|
139
139
|
let notifyCount = 0;
|
|
140
140
|
const observer = new Observer(() => {
|
|
141
141
|
notifyCount++;
|
|
@@ -157,7 +157,7 @@ describe("queue (async batching)", () => {
|
|
|
157
157
|
observer.dispose();
|
|
158
158
|
});
|
|
159
159
|
it("should batch multiple async updates into one notification", async () => {
|
|
160
|
-
const state =
|
|
160
|
+
const state = useState({ count: 0, name: "Alice" });
|
|
161
161
|
let notifyCount = 0;
|
|
162
162
|
const observer = new Observer(() => {
|
|
163
163
|
notifyCount++;
|
|
@@ -175,7 +175,7 @@ describe("queue (async batching)", () => {
|
|
|
175
175
|
observer.dispose();
|
|
176
176
|
});
|
|
177
177
|
it("should handle separate async batches", async () => {
|
|
178
|
-
const state =
|
|
178
|
+
const state = useState({ count: 0 });
|
|
179
179
|
let notifyCount = 0;
|
|
180
180
|
const observer = new Observer(() => {
|
|
181
181
|
notifyCount++;
|
|
@@ -196,7 +196,7 @@ describe("queue (async batching)", () => {
|
|
|
196
196
|
});
|
|
197
197
|
describe("syncBatch with nested async updates", () => {
|
|
198
198
|
it("should handle syncBatch inside async context", async () => {
|
|
199
|
-
const state =
|
|
199
|
+
const state = useState({ count: 0 });
|
|
200
200
|
let notifyCount = 0;
|
|
201
201
|
const observer = new Observer(() => {
|
|
202
202
|
notifyCount++;
|
|
@@ -217,7 +217,7 @@ describe("syncBatch with nested async updates", () => {
|
|
|
217
217
|
observer.dispose();
|
|
218
218
|
});
|
|
219
219
|
it("should handle async updates inside syncBatch callback", async () => {
|
|
220
|
-
const state =
|
|
220
|
+
const state = useState({ count: 0 });
|
|
221
221
|
let notifyCount = 0;
|
|
222
222
|
const observer = new Observer(() => {
|
|
223
223
|
notifyCount++;
|
|
@@ -242,3 +242,193 @@ describe("syncBatch with nested async updates", () => {
|
|
|
242
242
|
observer.dispose();
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
|
+
describe("syncBatch with cascading updates", () => {
|
|
246
|
+
it("should handle cascading observer notifications within the same batch", () => {
|
|
247
|
+
const state = useState({ count: 0 });
|
|
248
|
+
const derived = useState({ doubled: 0 });
|
|
249
|
+
let stateNotifyCount = 0;
|
|
250
|
+
let derivedNotifyCount = 0;
|
|
251
|
+
let componentNotifyCount = 0;
|
|
252
|
+
// Observer 1: Watches state, updates derived (simulates useDerived)
|
|
253
|
+
const derivedObserver = new Observer(() => {
|
|
254
|
+
stateNotifyCount++;
|
|
255
|
+
// When state changes, update derived synchronously
|
|
256
|
+
derived.doubled = state.count * 2;
|
|
257
|
+
});
|
|
258
|
+
const dispose1 = derivedObserver.observe();
|
|
259
|
+
state.count; // Track state
|
|
260
|
+
dispose1();
|
|
261
|
+
// Observer 2: Watches derived (simulates component)
|
|
262
|
+
const componentObserver = new Observer(() => {
|
|
263
|
+
derivedNotifyCount++;
|
|
264
|
+
});
|
|
265
|
+
const dispose2 = componentObserver.observe();
|
|
266
|
+
derived.doubled; // Track derived
|
|
267
|
+
dispose2();
|
|
268
|
+
// Observer 3: Also watches derived (another component)
|
|
269
|
+
const component2Observer = new Observer(() => {
|
|
270
|
+
componentNotifyCount++;
|
|
271
|
+
});
|
|
272
|
+
const dispose3 = component2Observer.observe();
|
|
273
|
+
derived.doubled; // Track derived
|
|
274
|
+
dispose3();
|
|
275
|
+
// Make a change in a batch
|
|
276
|
+
syncBatch(() => {
|
|
277
|
+
state.count = 5;
|
|
278
|
+
});
|
|
279
|
+
// All observers should have been notified exactly once
|
|
280
|
+
expect(stateNotifyCount).toBe(1);
|
|
281
|
+
expect(derivedNotifyCount).toBe(1);
|
|
282
|
+
expect(componentNotifyCount).toBe(1);
|
|
283
|
+
expect(state.count).toBe(5);
|
|
284
|
+
expect(derived.doubled).toBe(10);
|
|
285
|
+
derivedObserver.dispose();
|
|
286
|
+
componentObserver.dispose();
|
|
287
|
+
component2Observer.dispose();
|
|
288
|
+
});
|
|
289
|
+
it("should handle multi-level cascading updates", () => {
|
|
290
|
+
const state = useState({ value: 0 });
|
|
291
|
+
const derived1 = useState({ level1: 0 });
|
|
292
|
+
const derived2 = useState({ level2: 0 });
|
|
293
|
+
const derived3 = useState({ level3: 0 });
|
|
294
|
+
const notifyCounts = [0, 0, 0, 0];
|
|
295
|
+
// Level 1: state -> derived1
|
|
296
|
+
const observer1 = new Observer(() => {
|
|
297
|
+
notifyCounts[0]++;
|
|
298
|
+
derived1.level1 = state.value + 1;
|
|
299
|
+
});
|
|
300
|
+
const dispose1 = observer1.observe();
|
|
301
|
+
state.value;
|
|
302
|
+
dispose1();
|
|
303
|
+
// Level 2: derived1 -> derived2
|
|
304
|
+
const observer2 = new Observer(() => {
|
|
305
|
+
notifyCounts[1]++;
|
|
306
|
+
derived2.level2 = derived1.level1 + 1;
|
|
307
|
+
});
|
|
308
|
+
const dispose2 = observer2.observe();
|
|
309
|
+
derived1.level1;
|
|
310
|
+
dispose2();
|
|
311
|
+
// Level 3: derived2 -> derived3
|
|
312
|
+
const observer3 = new Observer(() => {
|
|
313
|
+
notifyCounts[2]++;
|
|
314
|
+
derived3.level3 = derived2.level2 + 1;
|
|
315
|
+
});
|
|
316
|
+
const dispose3 = observer3.observe();
|
|
317
|
+
derived2.level2;
|
|
318
|
+
dispose3();
|
|
319
|
+
// Final observer: watches derived3
|
|
320
|
+
const observer4 = new Observer(() => {
|
|
321
|
+
notifyCounts[3]++;
|
|
322
|
+
});
|
|
323
|
+
const dispose4 = observer4.observe();
|
|
324
|
+
derived3.level3;
|
|
325
|
+
dispose4();
|
|
326
|
+
// Update state in a batch
|
|
327
|
+
syncBatch(() => {
|
|
328
|
+
state.value = 10;
|
|
329
|
+
});
|
|
330
|
+
// All levels should have cascaded and each observer notified exactly once
|
|
331
|
+
expect(notifyCounts).toEqual([1, 1, 1, 1]);
|
|
332
|
+
expect(state.value).toBe(10);
|
|
333
|
+
expect(derived1.level1).toBe(11);
|
|
334
|
+
expect(derived2.level2).toBe(12);
|
|
335
|
+
expect(derived3.level3).toBe(13);
|
|
336
|
+
observer1.dispose();
|
|
337
|
+
observer2.dispose();
|
|
338
|
+
observer3.dispose();
|
|
339
|
+
observer4.dispose();
|
|
340
|
+
});
|
|
341
|
+
it("should handle diamond dependency pattern", () => {
|
|
342
|
+
// Diamond: state -> [derived1, derived2] -> derived3
|
|
343
|
+
const state = useState({ value: 0 });
|
|
344
|
+
const derived1 = useState({ path1: 0 });
|
|
345
|
+
const derived2 = useState({ path2: 0 });
|
|
346
|
+
const derived3 = useState({ combined: 0 });
|
|
347
|
+
let derived3NotifyCount = 0;
|
|
348
|
+
// State -> derived1
|
|
349
|
+
const obs1 = new Observer(() => {
|
|
350
|
+
derived1.path1 = state.value * 2;
|
|
351
|
+
});
|
|
352
|
+
const d1 = obs1.observe();
|
|
353
|
+
state.value;
|
|
354
|
+
d1();
|
|
355
|
+
// State -> derived2
|
|
356
|
+
const obs2 = new Observer(() => {
|
|
357
|
+
derived2.path2 = state.value * 3;
|
|
358
|
+
});
|
|
359
|
+
const d2 = obs2.observe();
|
|
360
|
+
state.value;
|
|
361
|
+
d2();
|
|
362
|
+
// [derived1, derived2] -> derived3
|
|
363
|
+
const obs3 = new Observer(() => {
|
|
364
|
+
derived3.combined = derived1.path1 + derived2.path2;
|
|
365
|
+
});
|
|
366
|
+
const d3 = obs3.observe();
|
|
367
|
+
derived1.path1;
|
|
368
|
+
derived2.path2;
|
|
369
|
+
d3();
|
|
370
|
+
// Watch derived3
|
|
371
|
+
const obs4 = new Observer(() => {
|
|
372
|
+
derived3NotifyCount++;
|
|
373
|
+
});
|
|
374
|
+
const d4 = obs4.observe();
|
|
375
|
+
derived3.combined;
|
|
376
|
+
d4();
|
|
377
|
+
syncBatch(() => {
|
|
378
|
+
state.value = 5;
|
|
379
|
+
});
|
|
380
|
+
// derived3 should only be notified once despite two paths updating
|
|
381
|
+
expect(derived3NotifyCount).toBe(1);
|
|
382
|
+
expect(derived1.path1).toBe(10);
|
|
383
|
+
expect(derived2.path2).toBe(15);
|
|
384
|
+
expect(derived3.combined).toBe(25);
|
|
385
|
+
obs1.dispose();
|
|
386
|
+
obs2.dispose();
|
|
387
|
+
obs3.dispose();
|
|
388
|
+
obs4.dispose();
|
|
389
|
+
});
|
|
390
|
+
it("should not create infinite loops with circular dependencies", () => {
|
|
391
|
+
const state1 = useState({ value: 0 });
|
|
392
|
+
const state2 = useState({ value: 0 });
|
|
393
|
+
let notify1Count = 0;
|
|
394
|
+
let notify2Count = 0;
|
|
395
|
+
// Observer 1: watches state1, updates state2
|
|
396
|
+
const obs1 = new Observer(() => {
|
|
397
|
+
notify1Count++;
|
|
398
|
+
if (notify1Count > 10) {
|
|
399
|
+
throw new Error("Infinite loop detected");
|
|
400
|
+
}
|
|
401
|
+
// Only update if different to break the cycle
|
|
402
|
+
if (state2.value !== state1.value + 1) {
|
|
403
|
+
state2.value = state1.value + 1;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const d1 = obs1.observe();
|
|
407
|
+
state1.value;
|
|
408
|
+
d1();
|
|
409
|
+
// Observer 2: watches state2, updates state1
|
|
410
|
+
const obs2 = new Observer(() => {
|
|
411
|
+
notify2Count++;
|
|
412
|
+
if (notify2Count > 10) {
|
|
413
|
+
throw new Error("Infinite loop detected");
|
|
414
|
+
}
|
|
415
|
+
// Only update if different to break the cycle
|
|
416
|
+
if (state1.value !== state2.value - 1) {
|
|
417
|
+
state1.value = state2.value - 1;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
const d2 = obs2.observe();
|
|
421
|
+
state2.value;
|
|
422
|
+
d2();
|
|
423
|
+
syncBatch(() => {
|
|
424
|
+
state1.value = 5;
|
|
425
|
+
});
|
|
426
|
+
// Should stabilize without infinite loop
|
|
427
|
+
expect(notify1Count).toBeLessThan(10);
|
|
428
|
+
expect(notify2Count).toBeLessThan(10);
|
|
429
|
+
expect(state1.value).toBe(5);
|
|
430
|
+
expect(state2.value).toBe(6);
|
|
431
|
+
obs1.dispose();
|
|
432
|
+
obs2.dispose();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "rask-ui/jsx-runtime";
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
3
|
import { createContext } from "../createContext";
|
|
4
4
|
import { render } from "../";
|
|
5
5
|
describe("createContext", () => {
|
|
6
|
-
it("should create a context
|
|
7
|
-
const
|
|
8
|
-
expect(
|
|
9
|
-
expect(
|
|
6
|
+
it("should create a context with use and inject methods", () => {
|
|
7
|
+
const MyContext = createContext(() => ({ value: "test" }));
|
|
8
|
+
expect(typeof MyContext.use).toBe("function");
|
|
9
|
+
expect(typeof MyContext.inject).toBe("function");
|
|
10
10
|
});
|
|
11
11
|
it("should allow setting and getting context values", () => {
|
|
12
|
-
const ThemeContext = createContext();
|
|
12
|
+
const ThemeContext = createContext(() => ({ theme: "dark" }));
|
|
13
13
|
function Parent() {
|
|
14
|
-
ThemeContext.inject(
|
|
14
|
+
ThemeContext.inject();
|
|
15
15
|
return () => _jsx(Child, {});
|
|
16
16
|
}
|
|
17
17
|
function Child() {
|
|
18
|
-
const theme = ThemeContext.
|
|
18
|
+
const theme = ThemeContext.use();
|
|
19
19
|
return () => _jsx("div", { children: theme.theme });
|
|
20
20
|
}
|
|
21
21
|
const container = document.createElement("div");
|
|
@@ -25,16 +25,16 @@ describe("createContext", () => {
|
|
|
25
25
|
document.body.removeChild(container);
|
|
26
26
|
});
|
|
27
27
|
it("should traverse parent components to find context", () => {
|
|
28
|
-
const ThemeContext = createContext();
|
|
28
|
+
const ThemeContext = createContext(() => ({ theme: "light" }));
|
|
29
29
|
function GrandParent() {
|
|
30
|
-
ThemeContext.inject(
|
|
30
|
+
ThemeContext.inject();
|
|
31
31
|
return () => _jsx(Parent, {});
|
|
32
32
|
}
|
|
33
33
|
function Parent() {
|
|
34
34
|
return () => _jsx(Child, {});
|
|
35
35
|
}
|
|
36
36
|
function Child() {
|
|
37
|
-
const theme = ThemeContext.
|
|
37
|
+
const theme = ThemeContext.use();
|
|
38
38
|
return () => _jsx("div", { children: theme.theme });
|
|
39
39
|
}
|
|
40
40
|
const container = document.createElement("div");
|
|
@@ -44,11 +44,11 @@ describe("createContext", () => {
|
|
|
44
44
|
document.body.removeChild(container);
|
|
45
45
|
});
|
|
46
46
|
it("should throw error when context is not found", () => {
|
|
47
|
-
const ThemeContext = createContext();
|
|
47
|
+
const ThemeContext = createContext(() => ({ theme: "dark" }));
|
|
48
48
|
function Child() {
|
|
49
49
|
expect(() => {
|
|
50
|
-
ThemeContext.
|
|
51
|
-
}).toThrow("
|
|
50
|
+
ThemeContext.use();
|
|
51
|
+
}).toThrow("No context available");
|
|
52
52
|
return () => _jsx("div", { children: "Child" });
|
|
53
53
|
}
|
|
54
54
|
const container = document.createElement("div");
|
|
@@ -57,33 +57,33 @@ describe("createContext", () => {
|
|
|
57
57
|
document.body.removeChild(container);
|
|
58
58
|
});
|
|
59
59
|
it("should throw error when setting context outside component", () => {
|
|
60
|
-
const ThemeContext = createContext();
|
|
60
|
+
const ThemeContext = createContext(() => ({ theme: "dark" }));
|
|
61
61
|
expect(() => {
|
|
62
|
-
ThemeContext.inject(
|
|
63
|
-
}).toThrow("
|
|
62
|
+
ThemeContext.inject();
|
|
63
|
+
}).toThrow("Only use useInjectContext in component setup");
|
|
64
64
|
});
|
|
65
65
|
it("should throw error when getting context outside component", () => {
|
|
66
|
-
const ThemeContext = createContext();
|
|
66
|
+
const ThemeContext = createContext(() => ({ theme: "dark" }));
|
|
67
67
|
expect(() => {
|
|
68
|
-
ThemeContext.
|
|
69
|
-
}).toThrow("
|
|
68
|
+
ThemeContext.use();
|
|
69
|
+
}).toThrow("Only use useContext in component setup");
|
|
70
70
|
});
|
|
71
71
|
it("should allow overriding context in nested components", () => {
|
|
72
|
-
const ThemeContext = createContext();
|
|
72
|
+
const ThemeContext = createContext((theme) => ({ theme }));
|
|
73
73
|
function GrandParent() {
|
|
74
|
-
ThemeContext.inject(
|
|
74
|
+
ThemeContext.inject("light");
|
|
75
75
|
return () => (_jsxs("div", { children: [_jsx(Parent, {}), _jsx(ChildOfGrandParent, {})] }));
|
|
76
76
|
}
|
|
77
77
|
function Parent() {
|
|
78
|
-
ThemeContext.inject(
|
|
78
|
+
ThemeContext.inject("dark");
|
|
79
79
|
return () => _jsx(ChildOfParent, {});
|
|
80
80
|
}
|
|
81
81
|
function ChildOfParent() {
|
|
82
|
-
const theme = ThemeContext.
|
|
82
|
+
const theme = ThemeContext.use();
|
|
83
83
|
return () => _jsx("div", { class: "child-of-parent", children: theme.theme });
|
|
84
84
|
}
|
|
85
85
|
function ChildOfGrandParent() {
|
|
86
|
-
const theme = ThemeContext.
|
|
86
|
+
const theme = ThemeContext.use();
|
|
87
87
|
return () => _jsx("div", { class: "child-of-grandparent", children: theme.theme });
|
|
88
88
|
}
|
|
89
89
|
const container = document.createElement("div");
|
|
@@ -96,16 +96,16 @@ describe("createContext", () => {
|
|
|
96
96
|
document.body.removeChild(container);
|
|
97
97
|
});
|
|
98
98
|
it("should support multiple different contexts", () => {
|
|
99
|
-
const ThemeContext = createContext();
|
|
100
|
-
const UserContext = createContext();
|
|
99
|
+
const ThemeContext = createContext(() => ({ theme: "dark" }));
|
|
100
|
+
const UserContext = createContext(() => ({ name: "Alice" }));
|
|
101
101
|
function Parent() {
|
|
102
|
-
ThemeContext.inject(
|
|
103
|
-
UserContext.inject(
|
|
102
|
+
ThemeContext.inject();
|
|
103
|
+
UserContext.inject();
|
|
104
104
|
return () => _jsx(Child, {});
|
|
105
105
|
}
|
|
106
106
|
function Child() {
|
|
107
|
-
const theme = ThemeContext.
|
|
108
|
-
const user = UserContext.
|
|
107
|
+
const theme = ThemeContext.use();
|
|
108
|
+
const user = UserContext.use();
|
|
109
109
|
return () => _jsx("div", { children: `${theme.theme} - ${user.name}` });
|
|
110
110
|
}
|
|
111
111
|
const container = document.createElement("div");
|
|
@@ -115,16 +115,16 @@ describe("createContext", () => {
|
|
|
115
115
|
document.body.removeChild(container);
|
|
116
116
|
});
|
|
117
117
|
it("should handle context values of different types", () => {
|
|
118
|
-
const NumberContext = createContext();
|
|
119
|
-
const ArrayContext = createContext();
|
|
118
|
+
const NumberContext = createContext(() => 42);
|
|
119
|
+
const ArrayContext = createContext(() => ["a", "b", "c"]);
|
|
120
120
|
function Parent() {
|
|
121
|
-
NumberContext.inject(
|
|
122
|
-
ArrayContext.inject(
|
|
121
|
+
NumberContext.inject();
|
|
122
|
+
ArrayContext.inject();
|
|
123
123
|
return () => _jsx(Child, {});
|
|
124
124
|
}
|
|
125
125
|
function Child() {
|
|
126
|
-
const num = NumberContext.
|
|
127
|
-
const arr = ArrayContext.
|
|
126
|
+
const num = NumberContext.use();
|
|
127
|
+
const arr = ArrayContext.use();
|
|
128
128
|
return () => _jsx("div", { children: `${num} - ${arr.join(",")}` });
|
|
129
129
|
}
|
|
130
130
|
const container = document.createElement("div");
|
|
@@ -133,4 +133,17 @@ describe("createContext", () => {
|
|
|
133
133
|
expect(container.textContent).toContain("42 - a,b,c");
|
|
134
134
|
document.body.removeChild(container);
|
|
135
135
|
});
|
|
136
|
+
it("should allow using context in the root component that injects it", () => {
|
|
137
|
+
const ThemeContext = createContext(() => ({ theme: "root-theme" }));
|
|
138
|
+
function RootComponent() {
|
|
139
|
+
ThemeContext.inject();
|
|
140
|
+
const theme = ThemeContext.use();
|
|
141
|
+
return () => _jsx("div", { children: theme.theme });
|
|
142
|
+
}
|
|
143
|
+
const container = document.createElement("div");
|
|
144
|
+
document.body.appendChild(container);
|
|
145
|
+
render(_jsx(RootComponent, {}), container);
|
|
146
|
+
expect(container.textContent).toContain("root-theme");
|
|
147
|
+
document.body.removeChild(container);
|
|
148
|
+
});
|
|
136
149
|
});
|
package/dist/tests/error.test.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "rask-ui/jsx-runtime";
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { useCatchError } from "../useCatchError";
|
|
4
4
|
import { render } from "../";
|
|
5
|
-
describe("
|
|
5
|
+
describe("useCatchError", () => {
|
|
6
6
|
it("should render children when no error occurs", async () => {
|
|
7
7
|
function SafeChild() {
|
|
8
8
|
return () => _jsx("div", { children: "Safe content" });
|
|
9
9
|
}
|
|
10
10
|
function TestComponent() {
|
|
11
|
-
|
|
11
|
+
const errorState = useCatchError();
|
|
12
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(SafeChild, {}));
|
|
12
13
|
}
|
|
13
14
|
const container = document.createElement("div");
|
|
14
15
|
document.body.appendChild(container);
|
|
@@ -26,7 +27,8 @@ describe("ErrorBoundary", () => {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
function TestComponent() {
|
|
29
|
-
|
|
30
|
+
const errorState = useCatchError();
|
|
31
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(ThrowingChild, {}));
|
|
30
32
|
}
|
|
31
33
|
const container = document.createElement("div");
|
|
32
34
|
document.body.appendChild(container);
|
|
@@ -44,7 +46,8 @@ describe("ErrorBoundary", () => {
|
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
function TestComponent() {
|
|
47
|
-
|
|
49
|
+
const errorState = useCatchError();
|
|
50
|
+
return () => errorState.error ? (_jsxs("div", { class: "error-ui", children: [_jsx("h1", { children: "Oops!" }), _jsx("p", { children: String(errorState.error) })] })) : (_jsx(ThrowingChild, {}));
|
|
48
51
|
}
|
|
49
52
|
const container = document.createElement("div");
|
|
50
53
|
document.body.appendChild(container);
|
|
@@ -64,7 +67,8 @@ describe("ErrorBoundary", () => {
|
|
|
64
67
|
return () => _jsx("div", { children: "Child 2" });
|
|
65
68
|
}
|
|
66
69
|
function TestComponent() {
|
|
67
|
-
|
|
70
|
+
const errorState = useCatchError();
|
|
71
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsxs(_Fragment, { children: [_jsx(SafeChild1, {}), _jsx(SafeChild2, {})] }));
|
|
68
72
|
}
|
|
69
73
|
const container = document.createElement("div");
|
|
70
74
|
document.body.appendChild(container);
|
|
@@ -85,7 +89,8 @@ describe("ErrorBoundary", () => {
|
|
|
85
89
|
return () => _jsx(DeepChild, {});
|
|
86
90
|
}
|
|
87
91
|
function TestComponent() {
|
|
88
|
-
|
|
92
|
+
const errorState = useCatchError();
|
|
93
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Caught: ", String(errorState.error)] })) : (_jsx(MiddleChild, {}));
|
|
89
94
|
}
|
|
90
95
|
const container = document.createElement("div");
|
|
91
96
|
document.body.appendChild(container);
|
|
@@ -102,8 +107,13 @@ describe("ErrorBoundary", () => {
|
|
|
102
107
|
return _jsx("div", {});
|
|
103
108
|
};
|
|
104
109
|
}
|
|
110
|
+
function InnerBoundary() {
|
|
111
|
+
const errorState = useCatchError();
|
|
112
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Inner: ", String(errorState.error)] })) : (_jsx(ThrowingChild, {}));
|
|
113
|
+
}
|
|
105
114
|
function TestComponent() {
|
|
106
|
-
|
|
115
|
+
const errorState = useCatchError();
|
|
116
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Outer: ", String(errorState.error)] })) : (_jsx(InnerBoundary, {}));
|
|
107
117
|
}
|
|
108
118
|
const container = document.createElement("div");
|
|
109
119
|
document.body.appendChild(container);
|
|
@@ -122,7 +132,8 @@ describe("ErrorBoundary", () => {
|
|
|
122
132
|
};
|
|
123
133
|
}
|
|
124
134
|
function TestComponent() {
|
|
125
|
-
|
|
135
|
+
const errorState = useCatchError();
|
|
136
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(ThrowingChild, {}));
|
|
126
137
|
}
|
|
127
138
|
const container = document.createElement("div");
|
|
128
139
|
document.body.appendChild(container);
|
|
@@ -139,7 +150,8 @@ describe("ErrorBoundary", () => {
|
|
|
139
150
|
};
|
|
140
151
|
}
|
|
141
152
|
function TestComponent() {
|
|
142
|
-
|
|
153
|
+
const errorState = useCatchError();
|
|
154
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Error: ", errorState.error.message, " (Code:", " ", errorState.error.code, ")"] })) : (_jsx(ThrowingChild, {}));
|
|
143
155
|
}
|
|
144
156
|
const container = document.createElement("div");
|
|
145
157
|
document.body.appendChild(container);
|
|
@@ -156,7 +168,8 @@ describe("ErrorBoundary", () => {
|
|
|
156
168
|
return () => _jsx("div", { children: "Safe content" });
|
|
157
169
|
}
|
|
158
170
|
function TestComponent() {
|
|
159
|
-
|
|
171
|
+
const errorState = useCatchError();
|
|
172
|
+
return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(SafeChild, {}));
|
|
160
173
|
}
|
|
161
174
|
const container = document.createElement("div");
|
|
162
175
|
document.body.appendChild(container);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderCount.test.d.ts","sourceRoot":"","sources":["../../src/tests/renderCount.test.tsx"],"names":[],"mappings":""}
|