Skip to content

Commit d2ae711

Browse files
committed
add test case for #17352
1 parent d117320 commit d2ae711

File tree

1 file changed

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

1 file changed

+260
-0
lines changed

packages/svelte/tests/signals/test.ts

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)