diff --git a/.changeset/clever-rockets-lose.md b/.changeset/clever-rockets-lose.md new file mode 100644 index 0000000000..f4623573e2 --- /dev/null +++ b/.changeset/clever-rockets-lose.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take into account `nodeName` case sensitivity on XHTML pages diff --git a/.changeset/eleven-fans-show.md b/.changeset/eleven-fans-show.md new file mode 100644 index 0000000000..ccfc48aa90 --- /dev/null +++ b/.changeset/eleven-fans-show.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: render `multiple` and `selected` attributes as empty strings for XHTML compliance diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 451f8191ae..524f35bccf 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -67,6 +67,7 @@ export const STALE_REACTION = new (class StaleReactionError extends Error { message = 'The reaction that called `getAbortSignal()` was re-run or destroyed'; })(); +export const IS_XHTML = /* @__PURE__ */ globalThis.document?.contentType.includes('xml') ?? false; export const ELEMENT_NODE = 1; export const TEXT_NODE = 3; export const COMMENT_NODE = 8; diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 6f1fa7391e..7b55c6dc4b 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -5,7 +5,7 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; import { create_event, delegate } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; -import { LOADING_ATTR_SYMBOL } from '#client/constants'; +import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants'; import { queue_micro_task } from '../task.js'; import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { @@ -30,6 +30,12 @@ export const STYLE = Symbol('style'); const IS_CUSTOM_ELEMENT = Symbol('is custom element'); const IS_HTML = Symbol('is html'); +const LINK_TAG = IS_XHTML ? 'link' : 'LINK'; +const INPUT_TAG = IS_XHTML ? 'input' : 'INPUT'; +const OPTION_TAG = IS_XHTML ? 'option' : 'OPTION'; +const SELECT_TAG = IS_XHTML ? 'select' : 'SELECT'; +const PROGRESS_TAG = IS_XHTML ? 'progress' : 'PROGRESS'; + /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need * to remove it upon hydration to avoid a bug when someone resets the form value. @@ -83,7 +89,7 @@ export function set_value(element, value) { value ?? undefined) || // @ts-expect-error // `progress` elements always need their value set when it's `0` - (element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS')) + (element.value === value && (value !== 0 || element.nodeName !== PROGRESS_TAG)) ) { return; } @@ -168,7 +174,7 @@ export function set_attribute(element, attribute, value, skip_warning) { if ( attribute === 'src' || attribute === 'srcset' || - (attribute === 'href' && element.nodeName === 'LINK') + (attribute === 'href' && element.nodeName === LINK_TAG) ) { if (!skip_warning) { check_src_in_dev_hydration(element, attribute, value ?? ''); @@ -241,7 +247,7 @@ export function set_custom_element_data(node, prop, value) { (setters_cache.has(node.getAttribute('is') || node.nodeName) || // customElements may not be available in browser extension contexts !customElements || - customElements.get(node.getAttribute('is') || node.tagName.toLowerCase()) + customElements.get(node.getAttribute('is') || node.nodeName.toLowerCase()) ? get_setters(node).includes(prop) : value && typeof value === 'object') ) { @@ -280,7 +286,7 @@ function set_attributes( should_remove_defaults = false, skip_warning = false ) { - if (hydrating && should_remove_defaults && element.tagName === 'INPUT') { + if (hydrating && should_remove_defaults && element.nodeName === INPUT_TAG) { var input = /** @type {HTMLInputElement} */ (element); var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue'; @@ -302,7 +308,7 @@ function set_attributes( } var current = prev || {}; - var is_option_element = element.tagName === 'OPTION'; + var is_option_element = element.nodeName === OPTION_TAG; for (var key in prev) { if (!(key in next)) { @@ -505,7 +511,7 @@ export function attribute_effect( /** @type {Record} */ var effects = {}; - var is_select = element.nodeName === 'SELECT'; + var is_select = element.nodeName === SELECT_TAG; var inited = false; managed(() => { diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index b0801d7959..a5d0405d0a 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -22,7 +22,16 @@ import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../constants.js'; -import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, REACTION_RAN, TEXT_NODE } from '#client/constants'; +import { + COMMENT_NODE, + DOCUMENT_FRAGMENT_NODE, + IS_XHTML, + REACTION_RAN, + TEXT_NODE +} from '#client/constants'; + +const TEMPLATE_TAG = IS_XHTML ? 'template' : 'TEMPLATE'; +const SCRIPT_TAG = IS_XHTML ? 'script' : 'SCRIPT'; /** * @param {TemplateNode} start @@ -186,12 +195,12 @@ function fragment_from_tree(structure, ns) { if (children.length > 0) { var target = - element.tagName === 'TEMPLATE' + element.nodeName === TEMPLATE_TAG ? /** @type {HTMLTemplateElement} */ (element).content : element; target.append( - fragment_from_tree(children, element.tagName === 'foreignObject' ? undefined : namespace) + fragment_from_tree(children, element.nodeName === 'foreignObject' ? undefined : namespace) ); } @@ -268,7 +277,7 @@ function run_scripts(node) { const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE; const scripts = - /** @type {HTMLElement} */ (node).tagName === 'SCRIPT' + /** @type {HTMLElement} */ (node).nodeName === SCRIPT_TAG ? [/** @type {HTMLScriptElement} */ (node)] : node.querySelectorAll('script'); diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 62196350bf..d18a48bdb4 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -272,7 +272,7 @@ export class Renderer { } if (value === this.local.select_value) { - renderer.#out.push(' selected'); + renderer.#out.push(' selected=""'); } renderer.#out.push(`>${body}${is_rich ? '' : ''}`); diff --git a/packages/svelte/src/internal/server/renderer.test.ts b/packages/svelte/src/internal/server/renderer.test.ts index 8e9a377a5b..bbbe3f745d 100644 --- a/packages/svelte/src/internal/server/renderer.test.ts +++ b/packages/svelte/src/internal/server/renderer.test.ts @@ -187,7 +187,7 @@ test('selects an option with an explicit value', () => { const { head, body } = Renderer.render(component as unknown as Component); expect(head).toBe(''); expect(body).toBe( - '' + '' ); }); @@ -203,7 +203,7 @@ test('selects an option with an implicit value', () => { const { head, body } = Renderer.render(component as unknown as Component); expect(head).toBe(''); expect(body).toBe( - '' + '' ); }); @@ -221,7 +221,7 @@ test('select merges scoped css hash with static class', () => { const { head, body } = Renderer.render(component as unknown as Component); expect(head).toBe(''); expect(body).toBe( - '' + '' ); }); diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index 4ad550e8d6..8bfa91f80c 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -28,7 +28,7 @@ export function attr(name, value, is_boolean = false) { } if (value == null || (!value && is_boolean)) return ''; const normalized = (name in replacements && replacements[name].get(value)) || value; - const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; + const assignment = is_boolean ? `=""` : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; } diff --git a/packages/svelte/tests/runtime-legacy/samples/select-multiple-spread/_config.js b/packages/svelte/tests/runtime-legacy/samples/select-multiple-spread/_config.js index 0619b1468e..47b38713c8 100644 --- a/packages/svelte/tests/runtime-legacy/samples/select-multiple-spread/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/select-multiple-spread/_config.js @@ -7,9 +7,8 @@ export default test({ assert.equal(options[0].selected, true); assert.equal(options[1].selected, false); - // Shouldn't change the value because the value is not bound. - component.value = ['2']; - assert.equal(options[0].selected, true); - assert.equal(options[1].selected, false); + component.attrs = { value: ['2'] }; + assert.equal(options[0].selected, false); + assert.equal(options[1].selected, true); } }); diff --git a/packages/svelte/tests/runtime-xhtml/samples/bind-value-input-type-dynamic/_config.js b/packages/svelte/tests/runtime-xhtml/samples/bind-value-input-type-dynamic/_config.js new file mode 100644 index 0000000000..0192b48df7 --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/bind-value-input-type-dynamic/_config.js @@ -0,0 +1,72 @@ +import { tick } from 'svelte'; +import { test, ok } from '../../test'; + +export default test({ + html: ` + + +

x / y

+ + + + + `, + ssrHtml: ` + + +

x / y

+ + + + + `, + async test({ assert, target }) { + const [in1, in2] = target.querySelectorAll('input'); + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + const p = target.querySelector('p'); + ok(p); + + in1.value = '0'; + in2.value = '1'; + in1.dispatchEvent(new window.Event('input', { bubbles: true })); + in2.dispatchEvent(new window.Event('input', { bubbles: true })); + await tick(); + btn2?.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '0 / 1'); + + in1.stepUp(); + in1.dispatchEvent(new window.Event('input', { bubbles: true })); + in2.stepUp(); + in2.dispatchEvent(new window.Event('input', { bubbles: true })); + await tick(); + assert.htmlEqual(p.innerHTML, '1 / 2'); + + btn1?.click(); + await tick(); + try { + in1.stepUp(); + assert.fail(); + } catch (e) { + // expected + } + + btn3?.click(); + await tick(); + in1.stepUp(); + in1.dispatchEvent(new window.Event('input', { bubbles: true })); + in2.stepUp(); + in2.dispatchEvent(new window.Event('input', { bubbles: true })); + await tick(); + assert.htmlEqual(p.innerHTML, '2 / 3'); + + btn1?.click(); + await tick(); + in1.value = 'a'; + in2.value = 'b'; + in1.dispatchEvent(new window.Event('input', { bubbles: true })); + in2.dispatchEvent(new window.Event('input', { bubbles: true })); + await tick(); + assert.htmlEqual(p.innerHTML, 'a / b'); + } +}); diff --git a/packages/svelte/tests/runtime-xhtml/samples/bind-value-input-type-dynamic/main.svelte b/packages/svelte/tests/runtime-xhtml/samples/bind-value-input-type-dynamic/main.svelte new file mode 100644 index 0000000000..82b33feb5f --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/bind-value-input-type-dynamic/main.svelte @@ -0,0 +1,14 @@ + + + + +

{dynamic} / {spread}

+ + + + diff --git a/packages/svelte/tests/runtime-xhtml/samples/form-default-value-spread/_config.js b/packages/svelte/tests/runtime-xhtml/samples/form-default-value-spread/_config.js new file mode 100644 index 0000000000..26e90e431b --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/form-default-value-spread/_config.js @@ -0,0 +1,263 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target }) { + /** + * @param {NodeListOf} inputs + * @param {string} field + * @param {any | any[]} value + */ + function check_inputs(inputs, field, value) { + for (let i = 0; i < inputs.length; i++) { + assert.equal(inputs[i][field], Array.isArray(value) ? value[i] : value, `field ${i}`); + } + } + + /** + * @param {any} input + * @param {string} field + * @param {any} value + */ + function set_input(input, field, value) { + input[field] = value; + input.dispatchEvent( + new Event(typeof value === 'boolean' ? 'change' : 'input', { bubbles: true }) + ); + } + + /** + * @param {HTMLOptionElement} option + */ + function select_option(option) { + option.selected = true; + option.dispatchEvent(new Event('change', { bubbles: true })); + } + + const after_reset = []; + + const reset = /** @type {HTMLInputElement} */ (target.querySelector('input[type=reset]')); + const [test1, test2, test3, test4, test5, test6, test7, test14] = + target.querySelectorAll('div'); + const [test8, test9, test10, test11] = target.querySelectorAll('select'); + const [ + test1_span, + test2_span, + test3_span, + test4_span, + test5_span, + test6_span, + test7_span, + test8_span, + test9_span, + test10_span, + test11_span + ] = target.querySelectorAll('span'); + + { + /** @type {NodeListOf} */ + const inputs = test1.querySelectorAll('input, textarea'); + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test1_span.innerHTML, 'x x x x'); + + for (const input of inputs) { + set_input(input, 'value', 'foo'); + } + flushSync(); + check_inputs(inputs, 'value', 'foo'); + assert.htmlEqual(test1_span.innerHTML, 'foo foo foo foo'); + + after_reset.push(() => { + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test1_span.innerHTML, 'x x x x'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test2.querySelectorAll('input, textarea'); + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test2_span.innerHTML, 'x x x x'); + + for (const input of inputs) { + set_input(input, 'value', 'foo'); + } + flushSync(); + check_inputs(inputs, 'value', 'foo'); + assert.htmlEqual(test2_span.innerHTML, 'foo foo foo foo'); + + after_reset.push(() => { + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test2_span.innerHTML, 'x x x x'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test3.querySelectorAll('input, textarea'); + check_inputs(inputs, 'value', 'y'); + assert.htmlEqual(test3_span.innerHTML, 'y y y y'); + + for (const input of inputs) { + set_input(input, 'value', 'foo'); + } + flushSync(); + check_inputs(inputs, 'value', 'foo'); + assert.htmlEqual(test3_span.innerHTML, 'foo foo foo foo'); + + after_reset.push(() => { + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test3_span.innerHTML, 'x x x x'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test4.querySelectorAll('input'); + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test4_span.innerHTML, 'true true'); + + for (const input of inputs) { + set_input(input, 'checked', false); + } + flushSync(); + check_inputs(inputs, 'checked', false); + assert.htmlEqual(test4_span.innerHTML, 'false false'); + + after_reset.push(() => { + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test4_span.innerHTML, 'true true'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test5.querySelectorAll('input'); + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test5_span.innerHTML, 'true true'); + + for (const input of inputs) { + set_input(input, 'checked', false); + } + flushSync(); + check_inputs(inputs, 'checked', false); + assert.htmlEqual(test5_span.innerHTML, 'false false'); + + after_reset.push(() => { + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test5_span.innerHTML, 'true true'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test6.querySelectorAll('input'); + check_inputs(inputs, 'checked', false); + assert.htmlEqual(test6_span.innerHTML, 'false false'); + + after_reset.push(() => { + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test6_span.innerHTML, 'true true'); + }); + } + { + /** @type {NodeListOf} */ + const inputs = test7.querySelectorAll('input'); + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test7_span.innerHTML, 'true'); + + after_reset.push(() => { + check_inputs(inputs, 'checked', false); + assert.htmlEqual(test7_span.innerHTML, 'false'); + }); + } + + { + /** @type {NodeListOf} */ + const options = test8.querySelectorAll('option'); + check_inputs(options, 'selected', [false, true, false]); + assert.htmlEqual(test8_span.innerHTML, 'b'); + + select_option(options[2]); + flushSync(); + check_inputs(options, 'selected', [false, false, true]); + assert.htmlEqual(test8_span.innerHTML, 'c'); + + after_reset.push(() => { + check_inputs(options, 'selected', [false, true, false]); + assert.htmlEqual(test8_span.innerHTML, 'b'); + }); + } + + { + /** @type {NodeListOf} */ + const options = test9.querySelectorAll('option'); + check_inputs(options, 'selected', [false, true, false]); + assert.htmlEqual(test9_span.innerHTML, 'b'); + + select_option(options[2]); + flushSync(); + check_inputs(options, 'selected', [false, false, true]); + assert.htmlEqual(test9_span.innerHTML, 'c'); + + after_reset.push(() => { + check_inputs(options, 'selected', [false, true, false]); + assert.htmlEqual(test9_span.innerHTML, 'b'); + }); + } + + { + /** @type {NodeListOf} */ + const options = test10.querySelectorAll('option'); + check_inputs(options, 'selected', [false, false, true]); + assert.htmlEqual(test10_span.innerHTML, 'c'); + + select_option(options[0]); + flushSync(); + check_inputs(options, 'selected', [true, false, false]); + assert.htmlEqual(test10_span.innerHTML, 'a'); + + after_reset.push(() => { + check_inputs(options, 'selected', [false, true, false]); + assert.htmlEqual(test10_span.innerHTML, 'b'); + }); + } + + { + /** @type {NodeListOf} */ + const options = test11.querySelectorAll('option'); + check_inputs(options, 'selected', [false, false, true]); + assert.htmlEqual(test11_span.innerHTML, 'c'); + + select_option(options[0]); + flushSync(); + check_inputs(options, 'selected', [true, false, false]); + assert.htmlEqual(test11_span.innerHTML, 'a'); + + after_reset.push(() => { + check_inputs(options, 'selected', [false, true, false]); + assert.htmlEqual(test11_span.innerHTML, 'b'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test14.querySelectorAll('input, textarea'); + assert.equal(inputs[0].value, 'x'); + assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, true); + // this is still missing...i have no idea how to fix this lol + // assert.equal(inputs[2].value, 'x'); + + after_reset.push(() => { + assert.equal(inputs[0].value, 'y'); + assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, false); + assert.equal(inputs[2].value, 'y'); + }); + } + + reset.click(); + await Promise.resolve(); + flushSync(); + after_reset.forEach((fn) => fn()); + } +}); diff --git a/packages/svelte/tests/runtime-xhtml/samples/form-default-value-spread/main.svelte b/packages/svelte/tests/runtime-xhtml/samples/form-default-value-spread/main.svelte new file mode 100644 index 0000000000..3fc7ef11a5 --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/form-default-value-spread/main.svelte @@ -0,0 +1,199 @@ + + +
+

Input/Textarea value

+ +
+ + + + + + + + +
+ + +
+ + + + + + + + +
+ + +
+ + + + + + + + +
+ +

Input checked

+ +
+ + + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ + +
+ + +
+ + + +

Select (single)

+ + + + + + + + + + + + +

Select (multiple)

+ + + + + + + + +

Static values

+
+ + + +
+ + +
+ +

+ Bound values: + {value1} {value3} {value6} {value8} + {value9} {value12} {value14} {value16} + {value17} {value20} {value22} {value24} + {checked2} {checked4} + {checked6} {checked8} + {checked10} {checked12} + {checked14} + {selected1} + {selected2} + {selected3} + {selected4} + {selected5} + {selected6} +

diff --git a/packages/svelte/tests/runtime-xhtml/samples/ignore-mismatched-href/_config.js b/packages/svelte/tests/runtime-xhtml/samples/ignore-mismatched-href/_config.js new file mode 100644 index 0000000000..ea044cea64 --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/ignore-mismatched-href/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + server_props: { + browser: false + }, + + props: { + browser: true + }, + + test({ assert, target }) { + assert.equal(target.querySelector('link')?.getAttribute('href'), '/bar'); + }, + + warnings: [ + 'The `href` attribute on `` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value' + ] +}); diff --git a/packages/svelte/tests/runtime-xhtml/samples/ignore-mismatched-href/main.svelte b/packages/svelte/tests/runtime-xhtml/samples/ignore-mismatched-href/main.svelte new file mode 100644 index 0000000000..fd5f2a038a --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/ignore-mismatched-href/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-xhtml/samples/select-falsy-value/_config.js b/packages/svelte/tests/runtime-xhtml/samples/select-falsy-value/_config.js new file mode 100644 index 0000000000..5d535db521 --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/select-falsy-value/_config.js @@ -0,0 +1,32 @@ +import { test } from '../../test'; + +// + + diff --git a/packages/svelte/tests/runtime-xhtml/samples/spread-element-input-select-multiple/_config.js b/packages/svelte/tests/runtime-xhtml/samples/spread-element-input-select-multiple/_config.js new file mode 100644 index 0000000000..d952dc2444 --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/spread-element-input-select-multiple/_config.js @@ -0,0 +1,46 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + test({ assert, component, target, window }) { + const [input1, input2] = target.querySelectorAll('input'); + const select = target.querySelector('select'); + ok(select); + const [option1, option2] = /** @type {NodeListOf} */ (select.childNodes); + + let selections = Array.from(select.selectedOptions); + assert.equal(selections.length, 2); + assert.ok(selections.includes(option1)); + assert.ok(selections.includes(option2)); + + const event = new window.Event('change'); + + input1.checked = false; + input1.dispatchEvent(event); + flushSync(); + + selections = Array.from(select.selectedOptions); + assert.equal(selections.length, 1); + assert.ok(!selections.includes(option1)); + assert.ok(selections.includes(option2)); + + input2.checked = false; + input2.dispatchEvent(event); + flushSync(); + input1.checked = true; + input1.dispatchEvent(event); + flushSync(); + selections = Array.from(select.selectedOptions); + assert.equal(selections.length, 1); + assert.ok(selections.includes(option1)); + assert.ok(!selections.includes(option2)); + + component.spread = { value: ['Hello', 'World'] }; + flushSync(); + + selections = Array.from(select.selectedOptions); + assert.equal(selections.length, 2); + assert.ok(selections.includes(option1)); + assert.ok(selections.includes(option2)); + } +}); diff --git a/packages/svelte/tests/runtime-xhtml/samples/spread-element-input-select-multiple/main.svelte b/packages/svelte/tests/runtime-xhtml/samples/spread-element-input-select-multiple/main.svelte new file mode 100644 index 0000000000..298c7754ce --- /dev/null +++ b/packages/svelte/tests/runtime-xhtml/samples/spread-element-input-select-multiple/main.svelte @@ -0,0 +1,12 @@ + + + + + +