@@ -1563,4 +1563,264 @@ describe('signals', () => {
15631563 assert . ok ( isClean , 'derived with no deps should be CLEAN' ) ;
15641564 } ;
15651565 } ) ;
1566+
1567+ // Simpler test: nested derived inside parent derived should react to source changes
1568+ test ( 'nested derived inside parent derived reacts to source changes' , ( ) => {
1569+ const log : any [ ] = [ ] ;
1570+
1571+ return ( ) => {
1572+ const flag = state ( false ) ;
1573+
1574+ const destroy = effect_root ( ( ) => {
1575+ // Parent derived creates a nested derived
1576+ const items = derived ( ( ) => {
1577+ const nested = derived ( ( ) => $ . get ( flag ) ) ;
1578+ return {
1579+ get value ( ) {
1580+ return $ . get ( nested ) ;
1581+ }
1582+ } ;
1583+ } ) ;
1584+
1585+ render_effect ( ( ) => {
1586+ log . push ( $ . get ( items ) . value ) ;
1587+ } ) ;
1588+ } ) ;
1589+
1590+ flushSync ( ) ;
1591+ assert . deepEqual ( log , [ false ] , 'initial value' ) ;
1592+
1593+ set ( flag , true ) ;
1594+ flushSync ( ) ;
1595+ assert . deepEqual ( log , [ false , true ] , 'nested derived should react to flag change' ) ;
1596+
1597+ destroy ( ) ;
1598+ } ;
1599+ } ) ;
1600+
1601+ // Test with SvelteSet like the original issue
1602+ test ( 'nested derived with SvelteSet reacts to changes' , ( ) => {
1603+ const log : any [ ] = [ ] ;
1604+
1605+ return ( ) => {
1606+ const expanded_ids = new SvelteSet < string > ( ) ;
1607+
1608+ const destroy = effect_root ( ( ) => {
1609+ const items = derived ( ( ) => {
1610+ const expanded = derived ( ( ) => expanded_ids . has ( 'a' ) ) ;
1611+ return {
1612+ get expanded ( ) {
1613+ return $ . get ( expanded ) ;
1614+ }
1615+ } ;
1616+ } ) ;
1617+
1618+ render_effect ( ( ) => {
1619+ log . push ( $ . get ( items ) . expanded ) ;
1620+ } ) ;
1621+ } ) ;
1622+
1623+ flushSync ( ) ;
1624+ assert . deepEqual ( log , [ false ] , 'initial: a not expanded' ) ;
1625+
1626+ expanded_ids . add ( 'a' ) ;
1627+ flushSync ( ) ;
1628+ assert . deepEqual ( log , [ false , true ] , 'after add: a expanded' ) ;
1629+
1630+ expanded_ids . delete ( 'a' ) ;
1631+ flushSync ( ) ;
1632+ assert . deepEqual ( log , [ false , true , false ] , 'after delete: a not expanded' ) ;
1633+
1634+ destroy ( ) ;
1635+ } ;
1636+ } ) ;
1637+
1638+ // This matches the App.svelte pattern more closely - array of items with nested deriveds
1639+ // and reading visible_items outside effect context after mutation
1640+ test ( 'nested deriveds in array items lose reactivity after reading outside effect' , ( ) => {
1641+ const log : any [ ] = [ ] ;
1642+
1643+ return ( ) => {
1644+ const expanded_ids = new SvelteSet < string > ( ) ;
1645+ const nodes = proxy ( [ { id : 'a' } , { id : 'b' } ] ) ;
1646+
1647+ let items_derived : Derived < any [ ] > ;
1648+ let visible_items_derived : Derived < any [ ] > ;
1649+
1650+ const destroy = effect_root ( ( ) => {
1651+ items_derived = derived ( ( ) => {
1652+ const result : any [ ] = [ ] ;
1653+ for ( const node of nodes ) {
1654+ const expanded = derived ( ( ) => expanded_ids . has ( node . id ) ) ;
1655+ result . push ( {
1656+ node,
1657+ get expanded ( ) {
1658+ return $ . get ( expanded ) ;
1659+ }
1660+ } ) ;
1661+ }
1662+ return result ;
1663+ } ) ;
1664+
1665+ visible_items_derived = derived ( ( ) => $ . get ( items_derived ) ) ;
1666+
1667+ render_effect ( ( ) => {
1668+ const items = $ . get ( visible_items_derived ) ;
1669+ // Log which items are expanded
1670+ log . push ( items . map ( ( i : any ) => i . expanded ) ) ;
1671+ } ) ;
1672+ } ) ;
1673+
1674+ flushSync ( ) ;
1675+ assert . deepEqual ( log , [ [ false , false ] ] , 'initial: none expanded' ) ;
1676+
1677+ expanded_ids . add ( 'a' ) ;
1678+ flushSync ( ) ;
1679+ assert . deepEqual (
1680+ log ,
1681+ [
1682+ [ false , false ] ,
1683+ [ true , false ]
1684+ ] ,
1685+ 'a expanded'
1686+ ) ;
1687+
1688+ expanded_ids . delete ( 'a' ) ;
1689+ flushSync ( ) ;
1690+ assert . deepEqual (
1691+ log ,
1692+ [
1693+ [ false , false ] ,
1694+ [ true , false ] ,
1695+ [ false , false ]
1696+ ] ,
1697+ 'a collapsed'
1698+ ) ;
1699+
1700+ // Now simulate the delete scenario - mutate nodes then read visible_items outside effect
1701+ nodes . splice ( 1 , 1 ) ; // Remove 'b'
1702+
1703+ // This read happens outside effect context (simulating event handler reading visible_items)
1704+ const snapshot = $ . get ( visible_items_derived ) ;
1705+ assert . equal ( snapshot . length , 1 ) ;
1706+
1707+ flushSync ( ) ;
1708+ assert . deepEqual (
1709+ log ,
1710+ [ [ false , false ] , [ true , false ] , [ false , false ] , [ false ] ] ,
1711+ 'after delete'
1712+ ) ;
1713+
1714+ // Now the bug: expanding 'a' should work
1715+ expanded_ids . add ( 'a' ) ;
1716+ flushSync ( ) ;
1717+ assert . deepEqual (
1718+ log ,
1719+ [ [ false , false ] , [ true , false ] , [ false , false ] , [ false ] , [ true ] ] ,
1720+ 'after expand post-delete: nested deriveds should still be reactive'
1721+ ) ;
1722+
1723+ destroy ( ) ;
1724+ } ;
1725+ } ) ;
1726+
1727+ // Reproduces the exact App.svelte bug pattern:
1728+ // - Parent derived creates items with nested deriveds
1729+ // - Child item's `visible` derived depends on parent item's `expanded` derived
1730+ // - After mutation + read outside effect, nested deriveds lose reactivity
1731+ test ( 'tree-like nested deriveds with parent-child dependencies stay reactive' , ( ) => {
1732+ const log : any [ ] = [ ] ;
1733+
1734+ return ( ) => {
1735+ const expanded_ids = new SvelteSet < string > ( ) ;
1736+ const nodes = proxy ( [
1737+ {
1738+ id : 'folder' ,
1739+ children : [ { id : 'file' } ]
1740+ } ,
1741+ { id : 'other' }
1742+ ] ) ;
1743+
1744+ let items_derived : Derived < any [ ] > ;
1745+ let visible_items_derived : Derived < any [ ] > ;
1746+
1747+ const destroy = effect_root ( ( ) => {
1748+ // Mimics the App.svelte pattern exactly
1749+ items_derived = derived ( function create_items (
1750+ list : any [ ] = nodes ,
1751+ parent ?: any ,
1752+ result : any [ ] = [ ]
1753+ ) {
1754+ for ( const node of list ) {
1755+ // Each item has its own expanded derived
1756+ const expanded_d = derived ( ( ) => expanded_ids . has ( node . id ) ) ;
1757+ // Child's visible depends on parent's expanded (via getter)
1758+ const visible_d = derived ( ( ) =>
1759+ parent === undefined ? true : $ . get ( parent . expanded_d ) && $ . get ( parent . visible_d )
1760+ ) ;
1761+
1762+ const item = {
1763+ node,
1764+ expanded_d,
1765+ visible_d,
1766+ get expanded ( ) {
1767+ return $ . get ( expanded_d ) ;
1768+ } ,
1769+ get visible ( ) {
1770+ return $ . get ( visible_d ) ;
1771+ }
1772+ } ;
1773+ result . push ( item ) ;
1774+
1775+ if ( node . children ) {
1776+ create_items ( node . children , item , result ) ;
1777+ }
1778+ }
1779+ return result ;
1780+ } ) ;
1781+
1782+ visible_items_derived = derived ( ( ) => $ . get ( items_derived ) . filter ( ( item ) => item . visible ) ) ;
1783+
1784+ render_effect ( ( ) => {
1785+ log . push ( $ . get ( visible_items_derived ) . length ) ;
1786+ } ) ;
1787+ } ) ;
1788+
1789+ flushSync ( ) ;
1790+ // folder (visible), file (NOT visible - parent not expanded), other (visible)
1791+ assert . deepEqual ( log , [ 2 ] , 'initial: folder and other visible' ) ;
1792+
1793+ // Expand folder - file should become visible
1794+ expanded_ids . add ( 'folder' ) ;
1795+ flushSync ( ) ;
1796+ assert . deepEqual ( log , [ 2 , 3 ] , 'after expand: folder, file, other visible' ) ;
1797+
1798+ // Collapse folder
1799+ expanded_ids . delete ( 'folder' ) ;
1800+ flushSync ( ) ;
1801+ assert . deepEqual ( log , [ 2 , 3 , 2 ] , 'after collapse' ) ;
1802+
1803+ // KEY BUG SCENARIO: delete 'other' then read visible_items outside effect
1804+ nodes . splice ( 1 , 1 ) ; // Remove 'other'
1805+
1806+ // Read outside effect context (simulates event handler reading visible_items)
1807+ const snapshot = $ . get ( visible_items_derived ) ;
1808+ assert . equal ( snapshot . length , 1 , 'after delete: only folder visible' ) ;
1809+
1810+ flushSync ( ) ;
1811+ assert . deepEqual ( log , [ 2 , 3 , 2 , 1 ] , 'effect ran after delete' ) ;
1812+
1813+ // THE BUG: expand folder - file should become visible again
1814+ // Without fix: nested deriveds created during outside-effect read lost reactivity
1815+ expanded_ids . add ( 'folder' ) ;
1816+ flushSync ( ) ;
1817+ assert . deepEqual (
1818+ log ,
1819+ [ 2 , 3 , 2 , 1 , 2 ] ,
1820+ 'after expand post-delete: folder and file should be visible'
1821+ ) ;
1822+
1823+ destroy ( ) ;
1824+ } ;
1825+ } ) ;
15661826} ) ;
0 commit comments