diff --git a/.changeset/khaki-cooks-develop.md b/.changeset/khaki-cooks-develop.md new file mode 100644 index 0000000000..530d581132 --- /dev/null +++ b/.changeset/khaki-cooks-develop.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve $inspect handling of derived objects diff --git a/.changeset/moody-sheep-type.md b/.changeset/moody-sheep-type.md new file mode 100644 index 0000000000..b218d70bad --- /dev/null +++ b/.changeset/moody-sheep-type.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: make `bind_this` implementation more robust diff --git a/.changeset/pre.json b/.changeset/pre.json index a4ec661a44..0b3bce6575 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -117,6 +117,7 @@ "itchy-kings-deliver", "itchy-lions-wash", "itchy-terms-guess", + "khaki-cooks-develop", "khaki-mails-draw", "khaki-moose-arrive", "kind-baboons-approve", @@ -200,6 +201,7 @@ "rich-waves-mix", "rotten-bags-type", "rotten-buckets-develop", + "rotten-experts-relax", "selfish-dragons-knock", "selfish-tools-hide", "serious-kids-deliver", @@ -242,6 +244,7 @@ "spotty-pens-agree", "spotty-rocks-destroy", "spotty-spiders-compare", + "spotty-turkeys-sparkle", "stale-books-perform", "stale-comics-look", "stale-jeans-refuse", @@ -305,6 +308,7 @@ "witty-camels-warn", "witty-steaks-dream", "witty-tomatoes-care", - "witty-years-crash" + "witty-years-crash", + "yellow-taxis-double" ] } diff --git a/.changeset/quiet-berries-end.md b/.changeset/quiet-berries-end.md new file mode 100644 index 0000000000..155b7915f8 --- /dev/null +++ b/.changeset/quiet-berries-end.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: tweak initial `bind:clientWidth/clientHeight/offsetWidth/offsetHeight` update timing diff --git a/.changeset/rotten-experts-relax.md b/.changeset/rotten-experts-relax.md new file mode 100644 index 0000000000..e5a7e3bbe4 --- /dev/null +++ b/.changeset/rotten-experts-relax.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: permit whitespace within template scripts diff --git a/.changeset/spotty-turkeys-sparkle.md b/.changeset/spotty-turkeys-sparkle.md new file mode 100644 index 0000000000..6464bd1e29 --- /dev/null +++ b/.changeset/spotty-turkeys-sparkle.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: allow boolean `contenteditable` attribute diff --git a/.changeset/yellow-taxis-double.md b/.changeset/yellow-taxis-double.md new file mode 100644 index 0000000000..b8c37c5a89 --- /dev/null +++ b/.changeset/yellow-taxis-double.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve import event handler support diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2672d91a60..16e2323727 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.0.0-next.65 + +### Patch Changes + +- fix: improve $inspect handling of derived objects ([#10584](https://github.com/sveltejs/svelte/pull/10584)) + +- fix: permit whitespace within template scripts ([#10591](https://github.com/sveltejs/svelte/pull/10591)) + +- fix: allow boolean `contenteditable` attribute ([#10590](https://github.com/sveltejs/svelte/pull/10590)) + +- fix: improve import event handler support ([#10592](https://github.com/sveltejs/svelte/pull/10592)) + ## 5.0.0-next.64 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 690c838448..249f407987 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.64", + "version": "5.0.0-next.65", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index f6f70ec3e3..f2207c9336 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -371,10 +371,6 @@ const errors = { // message: // "'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings" // }, - // dynamic_contenteditable_attribute: { - // code: 'dynamic-contenteditable-attribute', - // message: "'contenteditable' attribute cannot be dynamic if element uses two-way binding" - // }, // textarea_duplicate_value: { // code: 'textarea-duplicate-value', // message: diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index fbf6392fba..27f61bf634 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -475,7 +475,7 @@ const validation = { ); if (!contenteditable) { error(node, 'missing-contenteditable-attribute'); - } else if (!is_text_attribute(contenteditable)) { + } else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) { error(contenteditable, 'dynamic-contenteditable-attribute'); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index d33be2fdce..46906b3e98 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -35,6 +35,7 @@ import { import { regex_is_valid_identifier } from '../../../patterns.js'; import { javascript_visitors_runes } from './javascript-runes.js'; import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js'; +import { walk } from 'zimmerframe'; /** * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element @@ -978,24 +979,11 @@ function serialize_inline_component(node, component_name, context) { if (bind_this !== null) { const prev = fn; - const assignment = b.assignment('=', bind_this, b.id('$$value')); - const bind_this_id = /** @type {import('estree').Expression} */ ( - // if expression is not an identifier, we know it can't be a signal - bind_this.type === 'Identifier' - ? bind_this - : bind_this.type === 'MemberExpression' && bind_this.object.type === 'Identifier' - ? bind_this.object - : undefined - ); fn = (node_id) => - b.call( - '$.bind_this', - prev(node_id), - b.arrow( - [b.id('$$value')], - serialize_set_binding(assignment, context, () => context.visit(assignment)) - ), - bind_this_id + serialize_bind_this( + /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (bind_this), + context, + prev(node_id) ); } @@ -1022,6 +1010,66 @@ function serialize_inline_component(node, component_name, context) { return statements.length > 1 ? b.block(statements) : statements[0]; } +/** + * Serializes `bind:this` for components and elements. + * @param {import('estree').Identifier | import('estree').MemberExpression} bind_this + * @param {import('zimmerframe').Context} context + * @param {import('estree').Expression} node + * @returns + */ +function serialize_bind_this(bind_this, context, node) { + let i = 0; + /** @type {Map} */ + const each_ids = new Map(); + // Transform each reference to an each block context variable into a $$value_ variable + // by temporarily changing the `expression` of the corresponding binding. + // These $$value_ variables will be filled in by the bind_this runtime function through its last argument. + // Note that we only do this for each context variables, the consequence is that the value might be stale in + // some scenarios where the value is a member expression with changing computed parts or using a combination of multiple + // variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this. + walk( + bind_this, + {}, + { + Identifier(node) { + const binding = context.state.scope.get(node.name); + if (!binding || each_ids.has(binding)) return; + + const associated_node = Array.from(context.state.scopes.entries()).find( + ([_, scope]) => scope === binding?.scope + )?.[0]; + if (associated_node?.type === 'EachBlock') { + each_ids.set(binding, [ + i, + /** @type {import('estree').Expression} */ (context.visit(node)), + binding.expression + ]); + binding.expression = b.id('$$value_' + i); + i++; + } + } + } + ); + + const bind_this_id = /** @type {import('estree').Expression} */ (context.visit(bind_this)); + const ids = Array.from(each_ids.values()).map((id) => b.id('$$value_' + id[0])); + const assignment = b.assignment('=', bind_this, b.id('$$value')); + const update = serialize_set_binding(assignment, context, () => context.visit(assignment)); + + for (const [binding, [, , expression]] of each_ids) { + // reset expressions to what they were before + binding.expression = expression; + } + + /** @type {import('estree').Expression[]} */ + const args = [node, b.arrow([b.id('$$value'), ...ids], update), b.arrow([...ids], bind_this_id)]; + if (each_ids.size) { + args.push(b.thunk(b.array(Array.from(each_ids.values()).map((id) => id[1])))); + } + + return b.call('$.bind_this', ...args); +} + /** * Creates a new block which looks roughly like this: * ```js @@ -1314,6 +1362,7 @@ function serialize_event_handler(node, { state, visit }) { binding !== null && (binding.kind === 'state' || binding.kind === 'frozen_state' || + binding.declaration_kind === 'import' || binding.kind === 'legacy_reactive' || binding.kind === 'derived' || binding.kind === 'prop' || @@ -2080,7 +2129,7 @@ export const template_visitors = { node.fragment.nodes, context.path, child_metadata.namespace, - state.preserve_whitespace, + node.name === 'script' || state.preserve_whitespace, state.options.preserveComments ); @@ -2748,19 +2797,7 @@ export const template_visitors = { } case 'this': - call_expr = b.call( - `$.bind_this`, - state.node, - setter, - /** @type {import('estree').Expression} */ ( - // if expression is not an identifier, we know it can't be a signal - expression.type === 'Identifier' - ? expression - : expression.type === 'MemberExpression' && expression.object.type === 'Identifier' - ? expression.object - : undefined - ) - ); + call_expr = serialize_bind_this(node.expression, context, state.node); break; case 'textContent': case 'innerHTML': diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index daebed8782..cf48a0cdb6 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -30,7 +30,6 @@ import { } from './reconciler.js'; import { destroy_signal, - is_signal, push_destroy_fn, execute_effect, untrack, @@ -76,7 +75,6 @@ import { run } from '../common.js'; import { bind_transition, trigger_transitions } from './transitions.js'; import { mutable_source, source } from './reactivity/sources.js'; import { safe_equal, safe_not_equal } from './reactivity/equality.js'; -import { STATE_SYMBOL } from './constants.js'; /** @type {Set} */ const all_registerd_events = new Set(); @@ -973,11 +971,11 @@ export function bind_resize_observer(dom, type, update) { * @param {(size: number) => void} update */ export function bind_element_size(dom, type, update) { - // We need to wait a few ticks to be sure that the element has been inserted and rendered - // The alternative would be a mutation observer on the document but that's way to expensive - requestAnimationFrame(() => requestAnimationFrame(() => update(dom[type]))); const unsub = resize_observer_border_box.observe(dom, () => update(dom[type])); - render_effect(() => unsub); + effect(() => { + untrack(() => update(dom[type])); + return unsub; + }); } /** @@ -1322,39 +1320,48 @@ export function bind_prop(props, prop, value) { } } -/** - * @param {unknown} value - */ -function is_state_object(value) { - return value != null && typeof value === 'object' && STATE_SYMBOL in value; -} - /** * @param {Element} element_or_component - * @param {(value: unknown) => void} update - * @param {import('./types.js').MaybeSignal} binding + * @param {(value: unknown, ...parts: unknown[]) => void} update + * @param {(...parts: unknown[]) => unknown} get_value + * @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block, + * returns all the parts of the each block context that are used in the expression * @returns {void} */ -export function bind_this(element_or_component, update, binding) { - render_effect(() => { - // If we are reading from a proxied state binding, then we don't need to untrack - // the update function as it will be fine-grain. - if (is_state_object(binding) || (is_signal(binding) && is_state_object(binding.v))) { - update(element_or_component); - } else { - untrack(() => update(element_or_component)); - } - return () => { - // Defer to the next tick so that all updates can be reconciled first. - // This solves the case where one variable is shared across multiple this-bindings. - render_effect(() => { - untrack(() => { - if (!is_signal(binding) || binding.v === element_or_component) { - update(null); - } - }); - }); - }; +export function bind_this(element_or_component, update, get_value, get_parts) { + /** @type {unknown[]} */ + let old_parts; + /** @type {unknown[]} */ + let parts; + + const e = effect(() => { + old_parts = parts; + // We only track changes to the parts, not the value itself to avoid unnecessary reruns. + parts = get_parts?.() || []; + + untrack(() => { + if (element_or_component !== get_value(...parts)) { + update(element_or_component, ...parts); + // If this is an effect rerun (cause: each block context changes), then nullfiy the binding at + // the previous position if it isn't already taken over by a different effect. + if (old_parts && get_value(...old_parts) === element_or_component) { + update(null, ...old_parts); + } + } + }); + }); + + // Add effect teardown (likely causes: if block became false, each item removed, component unmounted). + // In these cases we need to nullify the binding only if we detect that the value is still the same. + // If not, that means that another effect has now taken over the binding. + push_destroy_fn(e, () => { + // Defer to the next tick so that all updates can be reconciled first. + // This solves the case where one variable is shared across multiple this-bindings. + effect(() => { + if (get_value(...parts) === element_or_component) { + update(null, ...parts); + } + }); }); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5299eb0340..caac577b91 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -43,6 +43,7 @@ let is_micro_task_queued = false; let is_flushing_effect = false; // Used for $inspect export let is_batching_effect = false; +let is_inspecting_signal = false; // Handle effect queues @@ -130,8 +131,15 @@ export function batch_inspect(target, prop, receiver) { return Reflect.apply(value, this, arguments); } finally { is_batching_effect = previously_batching_effect; - if (last_inspected_signal !== null) { - for (const fn of last_inspected_signal.inspect) fn(); + if (last_inspected_signal !== null && !is_inspecting_signal) { + is_inspecting_signal = true; + try { + for (const fn of last_inspected_signal.inspect) { + fn(); + } + } finally { + is_inspecting_signal = false; + } last_inspected_signal = null; } } @@ -426,6 +434,7 @@ export function execute_effect(signal) { function infinite_loop_guard() { if (flush_count > 100) { + flush_count = 0; throw new Error( 'ERR_SVELTE_TOO_MANY_UPDATES' + (DEV diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b49c8ec8b6..9ac8ccda11 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.64'; +export const VERSION = '5.0.0-next.65'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/_config.js b/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/_config.js new file mode 100644 index 0000000000..5df9ca4ba2 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../assert'; +import { log } from './log.js'; + +export default test({ + async test({ assert }) { + await new Promise((fulfil) => setTimeout(fulfil, 0)); + + assert.deepEqual(log, [ + [false, 0, 0], + [true, 100, 100] + ]); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/log.js b/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/log.js new file mode 100644 index 0000000000..d3df521f4d --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/main.svelte b/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/main.svelte new file mode 100644 index 0000000000..7437bbc229 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/binding-width-height-this-timing/main.svelte @@ -0,0 +1,23 @@ + + +
+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js index 33f5c7f4ab..26d22b8c1a 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js @@ -1,3 +1,4 @@ +import { tick } from 'svelte'; import { test } from '../../test'; import { create_deferred } from '../../../helpers.js'; @@ -22,7 +23,8 @@ export default test({ deferred.resolve(42); return deferred.promise - .then(() => { + .then(async () => { + await tick(); assert.htmlEqual(target.innerHTML, ''); const { button } = component; diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-this-with-context/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-this-with-context/_config.js index 6a4abf3edc..1688d15260 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-this-with-context/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-this-with-context/_config.js @@ -46,10 +46,8 @@ export default test({ component.items = ['foo', 'baz']; assert.equal(component.divs.length, 3, 'the divs array is still 3 long'); assert.equal(component.divs[2], null, 'the last div is unregistered'); - // Different from Svelte 3 - assert.equal(component.ps[1], null, 'the second p is unregistered'); - // Different from Svelte 3 - assert.equal(component.spans['-baz2'], null, 'the baz span is unregistered'); + assert.equal(component.ps[2], null, 'the last p is unregistered'); + assert.equal(component.spans['-bar1'], null, 'the bar span is unregistered'); assert.equal(component.hrs.bar, null, 'the bar hr is unregistered'); divs = target.querySelectorAll('div'); @@ -58,22 +56,17 @@ export default test({ assert.equal(e, divs[i], `div ${i} is still correct`); }); - // TODO: unsure if need these two tests to pass, as the logic between Svelte 3 - // and Svelte 5 is different for these cases. - - // elems = target.querySelectorAll('span'); - // component.items.forEach((e, i) => { - // assert.equal( - // component.spans[`-${e}${i}`], - // elems[i], - // `span -${e}${i} is still correct`, - // ); - // }); + spans = target.querySelectorAll('span'); + // @ts-ignore + component.items.forEach((e, i) => { + assert.equal(component.spans[`-${e}${i}`], spans[i], `span -${e}${i} is still correct`); + }); - // elems = target.querySelectorAll('p'); - // component.ps.forEach((e, i) => { - // assert.equal(e, elems[i], `p ${i} is still correct`); - // }); + ps = target.querySelectorAll('p'); + // @ts-ignore + component.ps.forEach((e, i) => { + assert.equal(e, ps[i], `p ${i} is still correct`); + }); hrs = target.querySelectorAll('hr'); // @ts-ignore diff --git a/packages/svelte/tests/runtime-legacy/samples/head-script/_config.js b/packages/svelte/tests/runtime-legacy/samples/head-script/_config.js new file mode 100644 index 0000000000..31acab66a3 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/head-script/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, component, window }) { + document.dispatchEvent(new Event('DOMContentLoaded')); + assert.equal(window.document.querySelector('button')?.textContent, 'Hello world'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/head-script/main.svelte b/packages/svelte/tests/runtime-legacy/samples/head-script/main.svelte new file mode 100644 index 0000000000..8284f84a59 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/head-script/main.svelte @@ -0,0 +1,10 @@ + + + + + + + +
+ + ${selected !== null ? `
${selected}
` : ''} + +
+ +

${selected ?? '...'}

+ `; +} + +export default test({ + html: get_html(null), + + async test({ assert, target }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + + await btn1?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, get_html(1)); + + await btn2?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, get_html(2)); + + await btn1?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, get_html(1)); + + await btn3?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, get_html(3)); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/main.svelte new file mode 100644 index 0000000000..be400d1d9c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/main.svelte @@ -0,0 +1,30 @@ + + +{#each [1, 2, 3] as n, i} + +{/each} + +
+ +{#each [1, 2, 3] as n, i} + {#if selected === i} +
{n}
+ {/if} +{/each} + +
+ +

{current ?? '...'}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-import/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-import/_config.js new file mode 100644 index 0000000000..66bda3def6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-import/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; +import { log, handler, log_a } from './event.js'; + +export default test({ + before_test() { + log.length = 0; + handler.value = log_a; + }, + + async test({ assert, target }) { + const [b1, b2] = target.querySelectorAll('button'); + + b1?.click(); + assert.deepEqual(log, ['a']); + + b2?.click(); + b1?.click(); + assert.deepEqual(log, ['a', 'b']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-import/event.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-import/event.js new file mode 100644 index 0000000000..978f672d30 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-import/event.js @@ -0,0 +1,14 @@ +/** @type {any[]} */ +export const log = []; + +export const log_a = () => { + log.push('a'); +}; + +export const log_b = () => { + log.push('b'); +}; + +export const handler = { + value: log_a +}; diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-import/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-import/main.svelte new file mode 100644 index 0000000000..1b743653a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-import/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js new file mode 100644 index 0000000000..0e6cf85884 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js @@ -0,0 +1,54 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +/** + * @type {any[]} + */ +let log; +/** + * @type {typeof console.log}} + */ +let original_log; + +export default test({ + compileOptions: { + dev: true + }, + before_test() { + log = []; + original_log = console.log; + console.log = (...v) => { + log.push(...v); + }; + }, + after_test() { + console.log = original_log; + }, + async test({ assert, target }) { + const button = target.querySelector('button'); + + flushSync(() => { + button?.click(); + }); + + assert.htmlEqual(target.innerHTML, `\n1`); + assert.deepEqual(log, [ + 'init', + { + data: { + derived: 0, + list: [] + }, + derived: [] + }, + 'update', + { + data: { + derived: 0, + list: [1] + }, + derived: [1] + } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte new file mode 100644 index 0000000000..49dec62f28 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte @@ -0,0 +1,21 @@ + + + + + +{state.data.list} diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index 0f261e1c81..78ae1ffc52 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -11,7 +11,7 @@ export default function Bind_this($$anchor, $$props) { var fragment = $.comment($$anchor); var node = $.child_frag(fragment); - $.bind_this(Foo(node, {}), ($$value) => foo = $$value, foo); + $.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo); $.close_frag($$anchor, fragment); $.pop(); } \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/contenteditable-dynamic/errors.json b/packages/svelte/tests/validator/samples/contenteditable-dynamic/errors.json index 6272bc6d21..27cc3e98c7 100644 --- a/packages/svelte/tests/validator/samples/contenteditable-dynamic/errors.json +++ b/packages/svelte/tests/validator/samples/contenteditable-dynamic/errors.json @@ -3,11 +3,11 @@ "code": "dynamic-contenteditable-attribute", "message": "'contenteditable' attribute cannot be dynamic if element uses two-way binding", "start": { - "line": 6, + "line": 11, "column": 8 }, "end": { - "line": 6, + "line": 11, "column": 32 } } diff --git a/packages/svelte/tests/validator/samples/contenteditable-dynamic/input.svelte b/packages/svelte/tests/validator/samples/contenteditable-dynamic/input.svelte index 8dfa91a354..48b4eadbcc 100644 --- a/packages/svelte/tests/validator/samples/contenteditable-dynamic/input.svelte +++ b/packages/svelte/tests/validator/samples/contenteditable-dynamic/input.svelte @@ -3,4 +3,9 @@ let toggle = false; + + + + +