@ripple-ts/compat-react 0.2.180 → 0.2.182
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/package.json +2 -2
- package/tests/index.test.ripple +282 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripple-ts/compat-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.182",
|
|
4
4
|
"description": "Ripple compatibility layer for React",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"author": "Dominic Gannaway",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"react": "^19.2.0",
|
|
19
19
|
"react-dom": "^19.2.0",
|
|
20
|
-
"ripple": "0.2.
|
|
20
|
+
"ripple": "0.2.182"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/react": "^19.2.2",
|
package/tests/index.test.ripple
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { track, flushSync } from 'ripple';
|
|
2
|
-
import { act, createContext, useContext } from 'react';
|
|
2
|
+
import { act, createContext, useContext, Suspense } from 'react';
|
|
3
|
+
import { vi } from 'vitest';
|
|
3
4
|
import { Ripple } from '@ripple-ts/compat-react';
|
|
4
5
|
import { createRoot } from 'react-dom/client';
|
|
5
6
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
@@ -387,7 +388,7 @@ describe('compat-react', () => {
|
|
|
387
388
|
|
|
388
389
|
component App() {
|
|
389
390
|
<tsx:react>
|
|
390
|
-
<DemoContext.Provider value={
|
|
391
|
+
<DemoContext.Provider value={'Hello from Context!'}>
|
|
391
392
|
<ReactChild />
|
|
392
393
|
</DemoContext.Provider>
|
|
393
394
|
</tsx:react>
|
|
@@ -401,6 +402,285 @@ describe('compat-react', () => {
|
|
|
401
402
|
expect(deepChild).toBeTruthy();
|
|
402
403
|
expect(deepChild.textContent).toBe('Deep child, context value is: Hello from Context!');
|
|
403
404
|
});
|
|
405
|
+
|
|
406
|
+
it('should handle React Suspense with Ripple await logic', async () => {
|
|
407
|
+
vi.useFakeTimers();
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
function sleep(ms: number) {
|
|
411
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
component AsyncRippleChild() {
|
|
415
|
+
await sleep(1000);
|
|
416
|
+
<div class="async-content">{'Loaded!'}</div>
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function ReactChild() {
|
|
420
|
+
return jsx(Ripple, { component: AsyncRippleChild });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
component App() {
|
|
424
|
+
<tsx:react>
|
|
425
|
+
<Suspense fallback={<div className="loading">Loading...</div>}>
|
|
426
|
+
<ReactChild />
|
|
427
|
+
</Suspense>
|
|
428
|
+
</tsx:react>
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await act(async () => {
|
|
432
|
+
render(App);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Initially shows loading state
|
|
436
|
+
const loading = container.querySelector('.loading');
|
|
437
|
+
expect(loading).toBeTruthy();
|
|
438
|
+
expect(loading.textContent).toBe('Loading...');
|
|
439
|
+
expect(container.querySelector('.async-content')).toBeFalsy();
|
|
440
|
+
|
|
441
|
+
// Advance timers to resolve the async operation
|
|
442
|
+
await act(async () => {
|
|
443
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Now shows loaded content
|
|
447
|
+
const content = container.querySelector('.async-content');
|
|
448
|
+
expect(content).toBeTruthy();
|
|
449
|
+
expect(content.textContent).toBe('Loaded!');
|
|
450
|
+
expect(container.querySelector('.loading')).toBeFalsy();
|
|
451
|
+
} finally {
|
|
452
|
+
vi.useRealTimers();
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should handle Ripple reactivity updates after Suspense resolves', async () => {
|
|
457
|
+
vi.useFakeTimers();
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
function sleep(ms: number) {
|
|
461
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
component AsyncRippleChild() {
|
|
465
|
+
await sleep(1000);
|
|
466
|
+
|
|
467
|
+
let count = track(0);
|
|
468
|
+
|
|
469
|
+
<div>
|
|
470
|
+
<div class="async-content">{'Loaded!'}</div>
|
|
471
|
+
<div class="count">{@count}</div>
|
|
472
|
+
<button onClick={() => @count++}>{'Increment'}</button>
|
|
473
|
+
</div>
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function ReactChild() {
|
|
477
|
+
return jsx(Ripple, { component: AsyncRippleChild });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
component App() {
|
|
481
|
+
<tsx:react>
|
|
482
|
+
<Suspense fallback={<div className="loading">Loading...</div>}>
|
|
483
|
+
<ReactChild />
|
|
484
|
+
</Suspense>
|
|
485
|
+
</tsx:react>
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await act(async () => {
|
|
489
|
+
render(App);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Initially shows loading state
|
|
493
|
+
expect(container.querySelector('.loading')).toBeTruthy();
|
|
494
|
+
expect(container.querySelector('.async-content')).toBeFalsy();
|
|
495
|
+
|
|
496
|
+
// Advance timers to resolve the async operation
|
|
497
|
+
await act(async () => {
|
|
498
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Now shows loaded content with initial count
|
|
502
|
+
expect(container.querySelector('.async-content')).toBeTruthy();
|
|
503
|
+
expect(container.querySelector('.count').textContent).toBe('0');
|
|
504
|
+
|
|
505
|
+
// Click button to increment count
|
|
506
|
+
const button = container.querySelector('button');
|
|
507
|
+
button.click();
|
|
508
|
+
flushSync();
|
|
509
|
+
|
|
510
|
+
// Count should be updated reactively
|
|
511
|
+
expect(container.querySelector('.count').textContent).toBe('1');
|
|
512
|
+
|
|
513
|
+
// Click again
|
|
514
|
+
button.click();
|
|
515
|
+
flushSync();
|
|
516
|
+
|
|
517
|
+
expect(container.querySelector('.count').textContent).toBe('2');
|
|
518
|
+
} finally {
|
|
519
|
+
vi.useRealTimers();
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should handle tracked async triggering re-suspension on state change', async () => {
|
|
524
|
+
vi.useFakeTimers();
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
function sleep(ms: number) {
|
|
528
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let setPage: (page: number) => void;
|
|
532
|
+
|
|
533
|
+
component AsyncRippleChild({ page }: { page: number }) {
|
|
534
|
+
// Using await track(() => ...) to create a reactive async operation
|
|
535
|
+
// that re-suspends when the tracked dependency (page) changes
|
|
536
|
+
await track(() => {
|
|
537
|
+
page;
|
|
538
|
+
return sleep(1000);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
<div class="content">{`Page ${page} loaded`}</div>
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function ReactChild(props: { page: number }) {
|
|
545
|
+
return jsx(Ripple, {
|
|
546
|
+
component: AsyncRippleChild,
|
|
547
|
+
props: { page: props.page },
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
component App() {
|
|
552
|
+
let page = track(1);
|
|
553
|
+
setPage = (p: number) => {
|
|
554
|
+
@page = p;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
<tsx:react>
|
|
558
|
+
<Suspense fallback={<div className="loading">Loading...</div>}>
|
|
559
|
+
<ReactChild page={@page} />
|
|
560
|
+
</Suspense>
|
|
561
|
+
</tsx:react>
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
await act(async () => {
|
|
565
|
+
render(App);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Initially shows loading state
|
|
569
|
+
expect(container.querySelector('.loading')).toBeTruthy();
|
|
570
|
+
expect(container.querySelector('.content')).toBeFalsy();
|
|
571
|
+
|
|
572
|
+
// Advance timers to resolve the first async operation
|
|
573
|
+
await act(async () => {
|
|
574
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Now shows page 1 content
|
|
578
|
+
expect(container.querySelector('.content')).toBeTruthy();
|
|
579
|
+
expect(container.querySelector('.content').textContent).toBe('Page 1 loaded');
|
|
580
|
+
expect(container.querySelector('.loading')).toBeFalsy();
|
|
581
|
+
|
|
582
|
+
// Change page to trigger re-suspension via tracked dependency
|
|
583
|
+
await act(async () => {
|
|
584
|
+
setPage(2);
|
|
585
|
+
flushSync();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Should show loading again due to reactive async re-run
|
|
589
|
+
expect(container.querySelector('.loading')).toBeTruthy();
|
|
590
|
+
|
|
591
|
+
// Advance timers to resolve the second async operation
|
|
592
|
+
await act(async () => {
|
|
593
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Now shows page 2 content
|
|
597
|
+
expect(container.querySelector('.content')).toBeTruthy();
|
|
598
|
+
expect(container.querySelector('.content').textContent).toBe('Page 2 loaded');
|
|
599
|
+
expect(container.querySelector('.loading')).toBeFalsy();
|
|
600
|
+
} finally {
|
|
601
|
+
vi.useRealTimers();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should handle re-suspension via React key prop forcing remount', async () => {
|
|
606
|
+
vi.useFakeTimers();
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
function sleep(ms: number) {
|
|
610
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let setPage: (page: number) => void;
|
|
614
|
+
|
|
615
|
+
component AsyncRippleChild({ page }: { page: number }) {
|
|
616
|
+
// Simple static await - no tracked dependencies
|
|
617
|
+
await sleep(1000);
|
|
618
|
+
|
|
619
|
+
<div class="content">{`Page ${page} loaded`}</div>
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function ReactChild(props: { page: number }) {
|
|
623
|
+
// Use key to force remount when page changes
|
|
624
|
+
return jsx(Ripple, {
|
|
625
|
+
key: props.page,
|
|
626
|
+
component: AsyncRippleChild,
|
|
627
|
+
props: { page: props.page },
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
component App() {
|
|
632
|
+
let page = track(1);
|
|
633
|
+
setPage = (p: number) => {
|
|
634
|
+
@page = p;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
<tsx:react>
|
|
638
|
+
<Suspense fallback={<div className="loading">Loading...</div>}>
|
|
639
|
+
<ReactChild page={@page} />
|
|
640
|
+
</Suspense>
|
|
641
|
+
</tsx:react>
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
await act(async () => {
|
|
645
|
+
render(App);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Initially shows loading state
|
|
649
|
+
expect(container.querySelector('.loading')).toBeTruthy();
|
|
650
|
+
expect(container.querySelector('.content')).toBeFalsy();
|
|
651
|
+
|
|
652
|
+
// Advance timers to resolve the first async operation
|
|
653
|
+
await act(async () => {
|
|
654
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Now shows page 1 content
|
|
658
|
+
expect(container.querySelector('.content')).toBeTruthy();
|
|
659
|
+
expect(container.querySelector('.content').textContent).toBe('Page 1 loaded');
|
|
660
|
+
expect(container.querySelector('.loading')).toBeFalsy();
|
|
661
|
+
|
|
662
|
+
// Change page to trigger re-suspension via key-based remount
|
|
663
|
+
await act(async () => {
|
|
664
|
+
setPage(2);
|
|
665
|
+
flushSync();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Should show loading again due to component remount
|
|
669
|
+
expect(container.querySelector('.loading')).toBeTruthy();
|
|
670
|
+
|
|
671
|
+
// Advance timers to resolve the second async operation
|
|
672
|
+
await act(async () => {
|
|
673
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Now shows page 2 content
|
|
677
|
+
expect(container.querySelector('.content')).toBeTruthy();
|
|
678
|
+
expect(container.querySelector('.content').textContent).toBe('Page 2 loaded');
|
|
679
|
+
expect(container.querySelector('.loading')).toBeFalsy();
|
|
680
|
+
} finally {
|
|
681
|
+
vi.useRealTimers();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
404
684
|
});
|
|
405
685
|
|
|
406
686
|
describe('Ripple in React app', () => {
|