Skip to content

Commit 153e91d

Browse files
Copilotdummdidumm
andcommitted
Add tests for multiple deriveds interacting and deriveds escaping effect contexts
Co-authored-by: dummdidumm <[email protected]>
1 parent 44f2efd commit 153e91d

File tree

1 file changed

+254
-0
lines changed
  • packages/svelte/tests/signals

1 file changed

+254
-0
lines changed

packages/svelte/tests/signals/test.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,4 +2827,258 @@ describe('signals', () => {
28272827
destroy2();
28282828
};
28292829
});
2830+
2831+
test('multiple deriveds interacting with proxy - one updates, other reacts', () => {
2832+
// Tests multiple deriveds that depend on each other and on a proxy
2833+
const log: any[] = [];
2834+
2835+
return () => {
2836+
const objSignal = state(proxy<{ foo?: number }>({ foo: 1 }));
2837+
2838+
// First derived reads from proxy
2839+
const d1 = derived(() => {
2840+
const obj = $.get(objSignal);
2841+
return obj.foo ?? 0;
2842+
});
2843+
2844+
// Second derived depends on first derived
2845+
const d2 = derived(() => $.get(d1) * 2);
2846+
2847+
// Third derived also depends on first
2848+
const d3 = derived(() => $.get(d1) + 10);
2849+
2850+
// Effect reads all deriveds
2851+
let destroy1 = effect_root(() => {
2852+
render_effect(() => {
2853+
log.push({ d1: $.get(d1), d2: $.get(d2), d3: $.get(d3) });
2854+
});
2855+
});
2856+
2857+
flushSync();
2858+
assert.deepEqual(log, [{ d1: 1, d2: 2, d3: 11 }]);
2859+
2860+
// Destroy effect - all deriveds disconnect
2861+
destroy1();
2862+
flushSync();
2863+
2864+
// Replace proxy with one WITHOUT the property
2865+
set(objSignal, proxy<{ foo?: number }>({}));
2866+
flushSync();
2867+
2868+
// Replace proxy with one WITH the property (new value)
2869+
set(objSignal, proxy<{ foo?: number }>({ foo: 42 }));
2870+
flushSync();
2871+
2872+
// Reconnect with new effect
2873+
const destroy2 = effect_root(() => {
2874+
render_effect(() => {
2875+
log.push({ d1: $.get(d1), d2: $.get(d2), d3: $.get(d3) });
2876+
});
2877+
});
2878+
2879+
flushSync();
2880+
assert.deepEqual(log[log.length - 1], { d1: 42, d2: 84, d3: 52 });
2881+
2882+
// Verify reactivity on new proxy
2883+
$.get(objSignal).foo = 100;
2884+
flushSync();
2885+
assert.deepEqual(log[log.length - 1], { d1: 100, d2: 200, d3: 110 });
2886+
2887+
destroy2();
2888+
};
2889+
});
2890+
2891+
test('derived created in effect context survives after effect destruction', () => {
2892+
// Tests a derived created inside an effect that continues to live
2893+
// after that effect is destroyed
2894+
const log: any[] = [];
2895+
2896+
return () => {
2897+
const objSignal = state(proxy<{ foo?: number }>({ foo: 1 }));
2898+
let escapedDerived: Derived<number>;
2899+
2900+
// Effect 1: creates a derived and "escapes" it
2901+
let destroy1 = effect_root(() => {
2902+
render_effect(() => {
2903+
// Create derived inside effect
2904+
escapedDerived = derived(() => {
2905+
const obj = $.get(objSignal);
2906+
return obj.foo ?? 0;
2907+
});
2908+
log.push('effect1: ' + $.get(escapedDerived));
2909+
});
2910+
});
2911+
2912+
flushSync();
2913+
assert.deepEqual(log, ['effect1: 1']);
2914+
2915+
// Destroy the effect that created the derived
2916+
destroy1();
2917+
flushSync();
2918+
2919+
// The derived should still be usable
2920+
// Replace proxy
2921+
set(objSignal, proxy<{ foo?: number }>({}));
2922+
set(objSignal, proxy<{ foo?: number }>({ foo: 42 }));
2923+
flushSync();
2924+
2925+
// Use the escaped derived in a NEW effect
2926+
const destroy2 = effect_root(() => {
2927+
render_effect(() => {
2928+
log.push('effect2: ' + $.get(escapedDerived!));
2929+
});
2930+
});
2931+
2932+
flushSync();
2933+
assert.deepEqual(log[log.length - 1], 'effect2: 42');
2934+
2935+
// Verify reactivity works on the escaped derived
2936+
$.get(objSignal).foo = 100;
2937+
flushSync();
2938+
assert.deepEqual(log[log.length - 1], 'effect2: 100');
2939+
2940+
destroy2();
2941+
};
2942+
});
2943+
2944+
test('derived moved between effect contexts with proxy changes', () => {
2945+
// Tests a derived that is read in one effect, then that effect is destroyed,
2946+
// and the derived is read in a different effect
2947+
const log: any[] = [];
2948+
2949+
return () => {
2950+
const objSignal = state(proxy<{ foo?: number }>({ foo: 1 }));
2951+
2952+
// Create derived outside effects
2953+
const d = derived(() => {
2954+
const obj = $.get(objSignal);
2955+
return obj.foo ?? 0;
2956+
});
2957+
2958+
// Effect 1: reads the derived
2959+
let destroy1 = effect_root(() => {
2960+
render_effect(() => {
2961+
log.push('effect1: ' + $.get(d));
2962+
});
2963+
});
2964+
2965+
flushSync();
2966+
assert.deepEqual(log, ['effect1: 1']);
2967+
2968+
// Effect 2: also reads the derived (both effects active)
2969+
let destroy2 = effect_root(() => {
2970+
render_effect(() => {
2971+
log.push('effect2: ' + $.get(d));
2972+
});
2973+
});
2974+
2975+
flushSync();
2976+
assert.deepEqual(log[log.length - 1], 'effect2: 1');
2977+
2978+
// Destroy effect 1 - derived still connected via effect 2
2979+
destroy1();
2980+
flushSync();
2981+
2982+
// Change proxy
2983+
set(objSignal, proxy<{ foo?: number }>({ foo: 50 }));
2984+
flushSync();
2985+
2986+
// Effect 2 should still react
2987+
assert.deepEqual(log[log.length - 1], 'effect2: 50');
2988+
2989+
// Destroy effect 2 - derived now fully disconnected
2990+
destroy2();
2991+
flushSync();
2992+
2993+
// Replace proxy while disconnected
2994+
set(objSignal, proxy<{ foo?: number }>({}));
2995+
set(objSignal, proxy<{ foo?: number }>({ foo: 99 }));
2996+
flushSync();
2997+
2998+
// Effect 3: reads the derived (reconnection)
2999+
const destroy3 = effect_root(() => {
3000+
render_effect(() => {
3001+
log.push('effect3: ' + $.get(d));
3002+
});
3003+
});
3004+
3005+
flushSync();
3006+
assert.deepEqual(log[log.length - 1], 'effect3: 99');
3007+
3008+
// Verify reactivity
3009+
$.get(objSignal).foo = 200;
3010+
flushSync();
3011+
assert.deepEqual(log[log.length - 1], 'effect3: 200');
3012+
3013+
destroy3();
3014+
};
3015+
});
3016+
3017+
test('derived created in destroyed effect - verify reactions', () => {
3018+
// Directly verify reactions arrays when derived outlives its creating effect
3019+
return () => {
3020+
const objSignal = state(proxy<{ foo?: number }>({ foo: 1 }));
3021+
let escapedDerived: Derived<number>;
3022+
3023+
// Create derived inside effect
3024+
let destroy1 = effect_root(() => {
3025+
render_effect(() => {
3026+
escapedDerived = derived(() => {
3027+
const obj = $.get(objSignal);
3028+
return obj.foo ?? 0;
3029+
});
3030+
$.get(escapedDerived); // Read it to establish deps
3031+
});
3032+
});
3033+
3034+
flushSync();
3035+
3036+
// Verify initial state
3037+
assert.ok(escapedDerived!.deps !== null, 'derived should have deps');
3038+
3039+
// Destroy creating effect
3040+
destroy1();
3041+
flushSync();
3042+
3043+
// Replace proxy while derived is orphaned
3044+
set(objSignal, proxy<{ foo?: number }>({}));
3045+
set(objSignal, proxy<{ foo?: number }>({ foo: 42 }));
3046+
flushSync();
3047+
3048+
// Use derived in new effect
3049+
const destroy2 = effect_root(() => {
3050+
render_effect(() => {
3051+
$.get(escapedDerived!);
3052+
});
3053+
});
3054+
3055+
flushSync();
3056+
3057+
// Verify deps are updated and reactions are correct
3058+
assert.ok(escapedDerived!.deps !== null, 'derived should still have deps');
3059+
const fooSource = escapedDerived!.deps![1]; // objSignal is [0], foo source is [1]
3060+
assert.ok(fooSource.reactions !== null, 'foo source should have reactions');
3061+
assert.ok(
3062+
fooSource.reactions!.includes(escapedDerived!),
3063+
'derived should be in foo source reactions'
3064+
);
3065+
3066+
// Verify reactivity
3067+
const log: any[] = [];
3068+
const destroy3 = effect_root(() => {
3069+
render_effect(() => {
3070+
log.push($.get(escapedDerived!));
3071+
});
3072+
});
3073+
3074+
flushSync();
3075+
$.get(objSignal).foo = 100;
3076+
flushSync();
3077+
3078+
assert.deepEqual(log, [42, 100], 'should react to changes');
3079+
3080+
destroy2();
3081+
destroy3();
3082+
};
3083+
});
28303084
});

0 commit comments

Comments
 (0)