From 190e64acd9fcd6d86de53f8d1e51242393716772 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 3 Dec 2025 12:29:25 -0500 Subject: [PATCH] fix: destroy each items after siblings are resumed (#17258) * fix: destroy each items after siblings are resumed * changeset * remove unused exports * tidy up * fix: correctly reconcile each blocks after outroing branches are resumed * WIP * add some logging * fix * remove item on destroy * remove * remove * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * tests passing * tidy up * tidy up * note to self * tidy up * fix * add stress test to repo --- .changeset/flat-cars-say.md | 5 + .changeset/great-bikes-listen.md | 5 + .../svelte/src/internal/client/constants.js | 1 + .../src/internal/client/dom/blocks/each.js | 412 +++++++++--------- .../src/internal/client/reactivity/effects.js | 12 +- .../svelte/src/internal/client/types.d.ts | 25 +- .../tests/manual/each-stress-test/main.svelte | 194 +++++++++ .../samples/each-updates-12/_config.js | 33 ++ .../samples/each-updates-12/main.svelte | 19 + .../samples/each-updates-13/_config.js | 23 + .../samples/each-updates-13/main.svelte | 19 + 11 files changed, 526 insertions(+), 222 deletions(-) create mode 100644 .changeset/flat-cars-say.md create mode 100644 .changeset/great-bikes-listen.md create mode 100644 packages/svelte/tests/manual/each-stress-test/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte diff --git a/.changeset/flat-cars-say.md b/.changeset/flat-cars-say.md new file mode 100644 index 0000000000..1caa75a110 --- /dev/null +++ b/.changeset/flat-cars-say.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly reconcile each blocks after outroing branches are resumed diff --git a/.changeset/great-bikes-listen.md b/.changeset/great-bikes-listen.md new file mode 100644 index 0000000000..416e0fcceb --- /dev/null +++ b/.changeset/great-bikes-listen.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: destroy each items after siblings are resumed diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a121e6674a..a1bdb8a985 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -40,6 +40,7 @@ export const EAGER_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; +export const EFFECT_OFFSCREEN = 1 << 25; // Flags exclusive to deriveds /** diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 191152b927..f7c9faf2b1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -1,4 +1,4 @@ -/** @import { EachItem, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ +/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ /** @import { Batch } from '../../reactivity/batch.js'; */ import { EACH_INDEX_REACTIVE, @@ -29,20 +29,22 @@ import { block, branch, destroy_effect, - run_out_transitions, - pause_children, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; -import { COMMENT_NODE, INERT } from '#client/constants'; +import { COMMENT_NODE, EFFECT_OFFSCREEN, INERT } from '#client/constants'; import { queue_micro_task } from '../task.js'; import { get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; import { current_batch } from '../../reactivity/batch.js'; +// When making substantive changes to this file, validate them with the each block stress test: +// https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b +// This test also exists in this repo, as `packages/svelte/tests/manual/each-stress-test` + /** * @param {any} _ * @param {number} i @@ -55,7 +57,7 @@ export function index(_, i) { * Pause multiple effects simultaneously, and coordinate their * subsequent destruction. Used in each blocks * @param {EachState} state - * @param {EachItem[]} to_destroy + * @param {Effect[]} to_destroy * @param {null | Node} controlled_anchor */ function pause_effects(state, to_destroy, controlled_anchor) { @@ -63,19 +65,44 @@ function pause_effects(state, to_destroy, controlled_anchor) { var transitions = []; var length = to_destroy.length; + /** @type {EachOutroGroup} */ + var group; + var remaining = to_destroy.length; + for (var i = 0; i < length; i++) { - pause_children(to_destroy[i].e, transitions, true); + let effect = to_destroy[i]; + + pause_effect( + effect, + () => { + if (group) { + group.pending.delete(effect); + group.done.add(effect); + + if (group.pending.size === 0) { + var groups = /** @type {Set} */ (state.outrogroups); + + destroy_effects(array_from(group.done)); + groups.delete(group); + + if (groups.size === 0) { + state.outrogroups = null; + } + } + } else { + remaining -= 1; + } + }, + false + ); } - run_out_transitions(transitions, () => { + if (remaining === 0) { // If we're in a controlled each block (i.e. the block is the only child of an // element), and we are removing all items, _and_ there are no out transitions, // we can use the fast path — emptying the element and replacing the anchor var fast_path = transitions.length === 0 && controlled_anchor !== null; - // TODO only destroy effects if no pending batch needs them. otherwise, - // just set `item.o` back to `false` - if (fast_path) { var anchor = /** @type {Element} */ (controlled_anchor); var parent_node = /** @type {Element} */ (anchor.parentNode); @@ -84,26 +111,34 @@ function pause_effects(state, to_destroy, controlled_anchor) { parent_node.append(anchor); state.items.clear(); - link(state, to_destroy[0].prev, to_destroy[length - 1].next); } - for (var i = 0; i < length; i++) { - var item = to_destroy[i]; - - if (!fast_path) { - state.items.delete(item.k); - link(state, item.prev, item.next); - } + destroy_effects(to_destroy, !fast_path); + } else { + group = { + pending: new Set(to_destroy), + done: new Set() + }; - destroy_effect(item.e, !fast_path); - } + (state.outrogroups ??= new Set()).add(group); + } +} - if (state.first === to_destroy[0]) { - state.first = to_destroy[0].prev; - } - }); +/** + * @param {Effect[]} to_destroy + * @param {boolean} remove_dom + */ +function destroy_effects(to_destroy, remove_dom = true) { + // TODO only destroy effects if no pending batch needs them. otherwise, + // just re-add the `EFFECT_OFFSCREEN` flag + for (var i = 0; i < to_destroy.length; i++) { + destroy_effect(to_destroy[i], remove_dom); + } } +/** @type {TemplateNode} */ +var offscreen_anchor; + /** * @template V * @param {Element | Comment} node The next sibling node, or the parent node if this is a 'controlled' block @@ -120,12 +155,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {Map} */ var items = new Map(); - /** @type {EachItem | null} */ - var first = null; - var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; - var is_reactive_value = (flags & EACH_ITEM_REACTIVE) !== 0; - var is_reactive_index = (flags & EACH_INDEX_REACTIVE) !== 0; if (is_controlled) { var parent_node = /** @type {Element} */ (node); @@ -139,7 +169,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f hydrate_next(); } - /** @type {{ fragment: DocumentFragment | null, effect: Effect } | null} */ + /** @type {Effect | null} */ var fallback = null; // TODO: ideally we could use derived for runes mode but because of the ability @@ -157,20 +187,19 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var first_run = true; function commit() { + state.fallback = fallback; reconcile(state, array, anchor, flags, get_key); if (fallback !== null) { if (array.length === 0) { - if (fallback.fragment) { - anchor.before(fallback.fragment); - fallback.fragment = null; + if ((fallback.f & EFFECT_OFFSCREEN) === 0) { + resume_effect(fallback); } else { - resume_effect(fallback.effect); + fallback.f ^= EFFECT_OFFSCREEN; + move(fallback, null, anchor); } - - effect.first = fallback.effect; } else { - pause_effect(fallback.effect, () => { + pause_effect(fallback, () => { // TODO only null out if no pending batch needs it, // otherwise re-add `fallback.fragment` and move the // effect into it @@ -202,10 +231,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var keys = new Set(); var batch = /** @type {Batch} */ (current_batch); - var prev = null; var defer = should_defer_append(); - for (var i = 0; i < length; i += 1) { + for (var index = 0; index < length; index += 1) { if ( hydrating && hydrate_node.nodeType === COMMENT_NODE && @@ -218,46 +246,33 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f set_hydrating(false); } - var value = array[i]; - var key = get_key(value, i); + var value = array[index]; + var key = get_key(value, index); var item = first_run ? null : items.get(key); if (item) { // update before reconciliation, to trigger any async updates - if (is_reactive_value) { - internal_set(item.v, value); - } - - if (is_reactive_index) { - internal_set(/** @type {Value} */ (item.i), i); - } + if (item.v) internal_set(item.v, value); + if (item.i) internal_set(item.i, index); if (defer) { batch.skipped_effects.delete(item.e); } } else { item = create_item( - first_run ? anchor : null, - prev, + items, + first_run ? anchor : (offscreen_anchor ??= create_text()), value, key, - i, + index, render_fn, flags, get_collection ); - if (first_run) { - item.o = true; - - if (prev === null) { - first = item; - } else { - prev.next = item; - } - - prev = item; + if (!first_run) { + item.e.f |= EFFECT_OFFSCREEN; } items.set(key, item); @@ -268,19 +283,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f if (length === 0 && fallback_fn && !fallback) { if (first_run) { - fallback = { - fragment: null, - effect: branch(() => fallback_fn(anchor)) - }; + fallback = branch(() => fallback_fn(anchor)); } else { - var fragment = document.createDocumentFragment(); - var target = create_text(); - fragment.append(target); - - fallback = { - fragment, - effect: branch(() => fallback_fn(target)) - }; + fallback = branch(() => fallback_fn((offscreen_anchor ??= create_text()))); + fallback.f |= EFFECT_OFFSCREEN; } } @@ -321,7 +327,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f }); /** @type {EachState} */ - var state = { effect, flags, items, first }; + var state = { effect, flags, items, outrogroups: null, fallback }; first_run = false; @@ -345,21 +351,21 @@ function reconcile(state, array, anchor, flags, get_key) { var length = array.length; var items = state.items; - var current = state.first; + var current = state.effect.first; - /** @type {undefined | Set} */ + /** @type {undefined | Set} */ var seen; - /** @type {EachItem | null} */ + /** @type {Effect | null} */ var prev = null; - /** @type {undefined | Set} */ + /** @type {undefined | Set} */ var to_animate; - /** @type {EachItem[]} */ + /** @type {Effect[]} */ var matched = []; - /** @type {EachItem[]} */ + /** @type {Effect[]} */ var stashed = []; /** @type {V} */ @@ -368,8 +374,8 @@ function reconcile(state, array, anchor, flags, get_key) { /** @type {any} */ var key; - /** @type {EachItem | undefined} */ - var item; + /** @type {Effect | undefined} */ + var effect; /** @type {number} */ var i; @@ -378,13 +384,13 @@ function reconcile(state, array, anchor, flags, get_key) { for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); - item = /** @type {EachItem} */ (items.get(key)); + effect = /** @type {EachItem} */ (items.get(key)).e; // offscreen == coming in now, no animation in that case, // else this would happen https://github.com/sveltejs/svelte/issues/17181 - if (item.o) { - item.e.nodes?.a?.measure(); - (to_animate ??= new Set()).add(item); + if ((effect.f & EFFECT_OFFSCREEN) === 0) { + effect.nodes?.a?.measure(); + (to_animate ??= new Set()).add(effect); } } } @@ -393,38 +399,53 @@ function reconcile(state, array, anchor, flags, get_key) { value = array[i]; key = get_key(value, i); - item = /** @type {EachItem} */ (items.get(key)); + effect = /** @type {EachItem} */ (items.get(key)).e; - state.first ??= item; + if (state.outrogroups !== null) { + for (const group of state.outrogroups) { + group.pending.delete(effect); + group.done.delete(effect); + } + } - if (!item.o) { - item.o = true; + if ((effect.f & EFFECT_OFFSCREEN) !== 0) { + effect.f ^= EFFECT_OFFSCREEN; - var next = prev ? prev.next : current; + if (effect === current) { + move(effect, null, anchor); + } else { + var next = prev ? prev.next : current; - link(state, prev, item); - link(state, item, next); + if (effect === state.effect.last) { + state.effect.last = effect.prev; + } - move(item, next, anchor); - prev = item; + if (effect.prev) effect.prev.next = effect.next; + if (effect.next) effect.next.prev = effect.prev; + link(state, prev, effect); + link(state, effect, next); - matched = []; - stashed = []; + move(effect, next, anchor); + prev = effect; + + matched = []; + stashed = []; - current = prev.next; - continue; + current = prev.next; + continue; + } } - if ((item.e.f & INERT) !== 0) { - resume_effect(item.e); + if ((effect.f & INERT) !== 0) { + resume_effect(effect); if (is_animated) { - item.e.nodes?.a?.unfix(); - (to_animate ??= new Set()).delete(item); + effect.nodes?.a?.unfix(); + (to_animate ??= new Set()).delete(effect); } } - if (item !== current) { - if (seen !== undefined && seen.has(item)) { + if (effect !== current) { + if (seen !== undefined && seen.has(effect)) { if (matched.length < stashed.length) { // more efficient to move later items to the front var start = stashed[0]; @@ -455,14 +476,14 @@ function reconcile(state, array, anchor, flags, get_key) { stashed = []; } else { // more efficient to move earlier items to the back - seen.delete(item); - move(item, current, anchor); + seen.delete(effect); + move(effect, current, anchor); - link(state, item.prev, item.next); - link(state, item, prev === null ? state.first : prev.next); - link(state, prev, item); + link(state, effect.prev, effect.next); + link(state, effect, prev === null ? state.effect.first : prev.next); + link(state, prev, effect); - prev = item; + prev = effect; } continue; @@ -471,12 +492,8 @@ function reconcile(state, array, anchor, flags, get_key) { matched = []; stashed = []; - while (current !== null && current !== item) { - // If the each block isn't inert and an item has an effect that is already inert, - // skip over adding it to our seen Set as the item is already being handled - if ((current.e.f & INERT) === 0) { - (seen ??= new Set()).add(current); - } + while (current !== null && current !== effect) { + (seen ??= new Set()).add(current); stashed.push(current); current = current.next; } @@ -484,42 +501,62 @@ function reconcile(state, array, anchor, flags, get_key) { if (current === null) { continue; } + } - item = current; + if ((effect.f & EFFECT_OFFSCREEN) === 0) { + matched.push(effect); } - matched.push(item); - prev = item; - current = item.next; + prev = effect; + current = effect.next; } - let has_offscreen_items = items.size > length; + if (state.outrogroups !== null) { + for (const group of state.outrogroups) { + if (group.pending.size === 0) { + destroy_effects(array_from(group.done)); + state.outrogroups?.delete(group); + } + } + + if (state.outrogroups.size === 0) { + state.outrogroups = null; + } + } if (current !== null || seen !== undefined) { - var to_destroy = seen === undefined ? [] : array_from(seen); + /** @type {Effect[]} */ + var to_destroy = []; + + if (seen !== undefined) { + for (effect of seen) { + if ((effect.f & INERT) === 0) { + to_destroy.push(effect); + } + } + } while (current !== null) { // If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished - if ((current.e.f & INERT) === 0) { + if ((current.f & INERT) === 0 && current !== state.fallback) { to_destroy.push(current); } + current = current.next; } var destroy_length = to_destroy.length; - has_offscreen_items = items.size - destroy_length > length; - if (destroy_length > 0) { var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null; if (is_animated) { for (i = 0; i < destroy_length; i += 1) { - to_destroy[i].e.nodes?.a?.measure(); + to_destroy[i].nodes?.a?.measure(); } for (i = 0; i < destroy_length; i += 1) { - to_destroy[i].e.nodes?.a?.fix(); + to_destroy[i].nodes?.a?.fix(); } } @@ -527,23 +564,11 @@ function reconcile(state, array, anchor, flags, get_key) { } } - // Append offscreen items at the end - if (has_offscreen_items) { - for (const item of items.values()) { - if (!item.o) { - link(state, prev, item); - prev = item; - } - } - } - - state.effect.last = prev && prev.e; - if (is_animated) { queue_micro_task(() => { if (to_animate === undefined) return; - for (item of to_animate) { - item.e.nodes?.a?.apply(); + for (effect of to_animate) { + effect.nodes?.a?.apply(); } }); } @@ -551,8 +576,8 @@ function reconcile(state, array, anchor, flags, get_key) { /** * @template V - * @param {Node | null} anchor - * @param {EachItem | null} prev + * @param {Map} items + * @param {Node} anchor * @param {V} value * @param {unknown} key * @param {number} index @@ -561,96 +586,81 @@ function reconcile(state, array, anchor, flags, get_key) { * @param {() => V[]} get_collection * @returns {EachItem} */ -function create_item(anchor, prev, value, key, index, render_fn, flags, get_collection) { - var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; - var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0; +function create_item(items, anchor, value, key, index, render_fn, flags, get_collection) { + var v = + (flags & EACH_ITEM_REACTIVE) !== 0 + ? (flags & EACH_ITEM_IMMUTABLE) === 0 + ? mutable_source(value, false, false) + : source(value) + : null; - var v = reactive ? (mutable ? mutable_source(value, false, false) : source(value)) : value; - var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); + var i = (flags & EACH_INDEX_REACTIVE) !== 0 ? source(index) : null; - if (DEV && reactive) { + if (DEV && v) { // For tracing purposes, we need to link the source signal we create with the // collection + index so that tracing works as intended - /** @type {Value} */ (v).trace = () => { - var collection_index = typeof i === 'number' ? index : i.v; + v.trace = () => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - get_collection()[collection_index]; + get_collection()[i?.v ?? index]; }; } - /** @type {EachItem} */ - var item = { - i, + return { v, - k: key, - // @ts-expect-error - e: null, - o: false, - prev, - next: null - }; - - if (anchor === null) { - var fragment = document.createDocumentFragment(); - fragment.append((anchor = create_text())); - } - - item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection)); - - if (prev !== null) { - // we only need to set `prev.next = item`, because - // `item.prev = prev` was set on initialization. - // the effects themselves are already linked - prev.next = item; - } + i, + e: branch(() => { + render_fn(anchor, v ?? value, i ?? index, get_collection); - return item; + return () => { + items.delete(key); + }; + }) + }; } /** - * @param {EachItem} item - * @param {EachItem | null} next + * @param {Effect} effect + * @param {Effect | null} next * @param {Text | Element | Comment} anchor */ -function move(item, next, anchor) { - if (!item.e.nodes) return; +function move(effect, next, anchor) { + if (!effect.nodes) return; - var end = item.next ? /** @type {EffectNodes} */ (item.next.e.nodes).start : anchor; + var node = effect.nodes.start; + var end = effect.nodes.end; - var dest = next ? /** @type {EffectNodes} */ (next.e.nodes).start : anchor; - var node = /** @type {TemplateNode} */ (item.e.nodes.start); + var dest = + next && (next.f & EFFECT_OFFSCREEN) === 0 + ? /** @type {EffectNodes} */ (next.nodes).start + : anchor; - while (node !== null && node !== end) { + while (node !== null) { var next_node = /** @type {TemplateNode} */ (get_next_sibling(node)); dest.before(node); + + if (node === end) { + return; + } + node = next_node; } } /** * @param {EachState} state - * @param {EachItem | null} prev - * @param {EachItem | null} next + * @param {Effect | null} prev + * @param {Effect | null} next */ function link(state, prev, next) { if (prev === null) { - state.first = next; - state.effect.first = next && next.e; + state.effect.first = next; } else { - if (prev.e.next) { - prev.e.next.prev = null; - } - prev.next = next; - prev.e.next = next && next.e; } - if (next !== null) { - if (next.e.prev) { - next.e.prev.next = null; - } - + if (next === null) { + state.effect.last = prev; + } else { next.prev = prev; - next.e.prev = prev && prev.e; } } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index be5ad773e0..717fc35006 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -590,17 +590,11 @@ export function pause_effect(effect, callback, destroy = true) { pause_children(effect, transitions, true); - run_out_transitions(transitions, () => { + var fn = () => { if (destroy) destroy_effect(effect); if (callback) callback(); - }); -} + }; -/** - * @param {TransitionManager[]} transitions - * @param {() => void} fn - */ -export function run_out_transitions(transitions, fn) { var remaining = transitions.length; if (remaining > 0) { var check = () => --remaining || fn(); @@ -617,7 +611,7 @@ export function run_out_transitions(transitions, fn) { * @param {TransitionManager[]} transitions * @param {boolean} local */ -export function pause_children(effect, transitions, local) { +function pause_children(effect, transitions, local) { if ((effect.f & INERT) !== 0) return; effect.f ^= INERT; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 895bfba571..443c21010e 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -72,6 +72,11 @@ export type TemplateNode = Text | Element | Comment; export type Dom = TemplateNode | TemplateNode[]; +export type EachOutroGroup = { + pending: Set; + done: Set; +}; + export type EachState = { /** the each block effect */ effect: Effect; @@ -79,23 +84,19 @@ export type EachState = { flags: number; /** a key -> item lookup */ items: Map; - /** head of the linked list of items */ - first: EachItem | null; + /** all outro groups that this item is a part of */ + outrogroups: Set | null; + /** `{:else}` effect */ + fallback: Effect | null; }; export type EachItem = { + /** value */ + v: Source | null; + /** index */ + i: Source | null; /** effect */ e: Effect; - /** item */ - v: any | Source; - /** index */ - i: number | Source; - /** key */ - k: unknown; - /** true if onscreen */ - o: boolean; - prev: EachItem | null; - next: EachItem | null; }; export interface TransitionManager { diff --git a/packages/svelte/tests/manual/each-stress-test/main.svelte b/packages/svelte/tests/manual/each-stress-test/main.svelte new file mode 100644 index 0000000000..cb69612844 --- /dev/null +++ b/packages/svelte/tests/manual/each-stress-test/main.svelte @@ -0,0 +1,194 @@ + + +

each block stress test

+ + + + + +
+ random + + + +
+ +
+ presets + + {#each presets as preset, index} + + {/each} +
+ +
{ + e.preventDefault(); + test(e.currentTarget.querySelector('input').value); +}}> +
+ input + +
+
+ +
+ {#each list as c (c)} + ({c}:{n}) + {:else} + (fallback) + {/each} +
+ +{#if error} +

{error}

+{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js new file mode 100644 index 0000000000..1fee8ceb67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-12/_config.js @@ -0,0 +1,33 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, raf }) { + const [clear, push] = target.querySelectorAll('button'); + + flushSync(() => clear.click()); + flushSync(() => push.click()); + raf.tick(500); + + assert.htmlEqual( + target.innerHTML, + ` + + + 1 + 2 + ` + ); + + raf.tick(1000); + + assert.htmlEqual( + target.innerHTML, + ` + + + 1 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte new file mode 100644 index 0000000000..a65ebd37a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-12/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#each items as item} + {item} +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js new file mode 100644 index 0000000000..fdf02e486c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-13/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, raf }) { + const [clear, reverse] = target.querySelectorAll('button'); + + flushSync(() => clear.click()); + flushSync(() => reverse.click()); + raf.tick(1); + + assert.htmlEqual( + target.innerHTML, + ` + + + c + b + a + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte new file mode 100644 index 0000000000..3de3382419 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-13/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#each items as item (item)} + {item} +{/each}