fix: take into account `nodeName` case sensitivity on XHTML pages (#17689)

It should be the last piece of XHTML compliance.
We missed fixing `mutltiple` and `selected` attributes on `<select>` and
`<option>`.
Also, it seems the test runtime-legacy/select-multiple-spread was
broken.
I just copied to runtime-xhtml and updated tests that fail when a
`nodeName` comparison is broken. For `<progress>` no test due to JSDOM
quirks (at least in the past), and for `<template>` and `<script>`
nothing got broken 🤔

### Before submitting the PR, please make sure you do the following

- [x] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/17681/head
7nik 2 days ago committed by GitHub
parent 4eee2f7c92
commit b657029687
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take into account `nodeName` case sensitivity on XHTML pages

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: render `multiple` and `selected` attributes as empty strings for XHTML compliance

@ -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;

@ -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<symbol, Effect>} */
var effects = {};
var is_select = element.nodeName === 'SELECT';
var is_select = element.nodeName === SELECT_TAG;
var inited = false;
managed(() => {

@ -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');

@ -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 ? '<!>' : ''}</option>`);

@ -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(
'<!--[--><select><option value="1">one</option><option value="2" selected>two</option><option value="3">three</option></select><!--]-->'
'<!--[--><select><option value="1">one</option><option value="2" selected="">two</option><option value="3">three</option></select><!--]-->'
);
});
@ -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(
'<!--[--><select><option>one</option><option selected>two</option><option>three</option></select><!--]-->'
'<!--[--><select><option>one</option><option selected="">two</option><option>three</option></select><!--]-->'
);
});
@ -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(
'<!--[--><select class="foo svelte-hash"><option value="foo" selected>foo</option></select><!--]-->'
'<!--[--><select class="foo svelte-hash"><option value="foo" selected="">foo</option></select><!--]-->'
);
});

@ -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}`;
}

@ -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);
}
});

@ -0,0 +1,72 @@
import { tick } from 'svelte';
import { test, ok } from '../../test';
export default test({
html: `
<input type="text"/>
<input type="text"/>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
ssrHtml: `
<input type="text" value="x"/>
<input type="text" value="y"/>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
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');
}
});

@ -0,0 +1,14 @@
<script>
let dynamic = $state('x');
let spread = $state('y');
let inputType = $state('text');
let props = $derived({type: inputType});
</script>
<input bind:value={dynamic} type={inputType}>
<input bind:value={spread} {...props}>
<p>{dynamic} / {spread}</p>
<button onclick={() => inputType = 'text'}>change to text</button>
<button onclick={() => inputType = 'number'}>change to number</button>
<button onclick={() => inputType = 'range'}>change to range</button>

@ -0,0 +1,263 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target }) {
/**
* @param {NodeListOf<any>} 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<HTMLInputElement | HTMLTextAreaElement>} */
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<HTMLInputElement | HTMLTextAreaElement>} */
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<HTMLInputElement | HTMLTextAreaElement>} */
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<HTMLInputElement>} */
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<HTMLInputElement>} */
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<HTMLInputElement>} */
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<HTMLInputElement>} */
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<HTMLOptionElement>} */
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<HTMLOptionElement>} */
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<HTMLOptionElement>} */
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<HTMLOptionElement>} */
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<HTMLInputElement | HTMLTextAreaElement>} */
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());
}
});

@ -0,0 +1,199 @@
<script>
let spread = {};
let value1 = $state();
let value2 = $state();
let value3 = $state();
let value4 = $state();
let value5 = $state();
let value6 = $state();
let value7 = $state();
let value8 = $state();
let value9 = $state(null);
let value10 = $state(null);
let value11 = $state(null);
let value12 = $state(null);
let value13 = $state(null);
let value14 = $state(null);
let value15 = $state(null);
let value16 = $state(null);
let value17 = $state('y');
let value18 = $state('y');
let value19 = $state('y');
let value20 = $state('y');
let value21 = $state('y');
let value22 = $state('y');
let value23 = $state('y');
let value24 = $state('y');
let checked1 = $state();
let checked2 = $state();
let checked3 = $state();
let checked4 = $state();
let checked5 = $state(null);
let checked6 = $state(null);
let checked7 = $state(null);
let checked8 = $state(null);
let checked9 = $state(false);
let checked10 = $state(false);
let checked11 = $state(false);
let checked12 = $state(false);
let checked13 = $state(true);
let checked14 = $state(true);
let selected1 = $state();
let selected2 = $state();
let selected3 = $state('c');
let selected4 = $state('c');
let selected5 = $state(['c']);
let selected6 = $state(['c']);
let defaultValue = $state('x');
let defaultChecked = $state(true);
</script>
<form>
<p>Input/Textarea value</p>
<!-- defaultValue=x, value=undefined -->
<div class="test-1">
<input {defaultValue} bind:value={value1} {...spread} />
<input {defaultValue} value={value2} {...spread} />
<input defaultValue="x" bind:value={value3} {...spread} />
<input defaultValue="x" value={value4} {...spread} />
<textarea {defaultValue} value={value5} {...spread}></textarea>
<textarea {defaultValue} bind:value={value6} {...spread}></textarea>
<textarea defaultValue="x" value={value7} {...spread}></textarea>
<textarea defaultValue="x" bind:value={value8} {...spread}></textarea>
</div>
<!-- defaultValue=x, value=null -->
<div class="test-2">
<input {defaultValue} bind:value={value9} {...spread} />
<input {defaultValue} value={value10} {...spread} />
<input defaultValue="x" value={value11} {...spread} />
<input defaultValue="x" bind:value={value12} {...spread} />
<textarea {defaultValue} value={value13} {...spread}></textarea>
<textarea {defaultValue} bind:value={value14} {...spread}></textarea>
<textarea defaultValue="x" value={value15} {...spread}></textarea>
<textarea defaultValue="x" bind:value={value16} {...spread}></textarea>
</div>
<!-- defaultValue=x, value=y -->
<div class="test-3">
<input {defaultValue} bind:value={value17} {...spread} />
<input {defaultValue} value={value18} {...spread} />
<input defaultValue="x" value={value19} {...spread} />
<input defaultValue="x" bind:value={value20} {...spread} />
<textarea {defaultValue} value={value21} {...spread}></textarea>
<textarea {defaultValue} bind:value={value22} {...spread}></textarea>
<textarea defaultValue="x" value={value23} {...spread}></textarea>
<textarea defaultValue="x" bind:value={value24} {...spread}></textarea>
</div>
<p>Input checked</p>
<!-- defaultChecked=true, checked=undefined -->
<div class="test-4">
<input type="checkbox" {defaultChecked} checked={checked1} {...spread} />
<input type="checkbox" {defaultChecked} bind:checked={checked2} {...spread} />
<input type="checkbox" defaultChecked checked={checked3} {...spread} />
<input type="checkbox" defaultChecked bind:checked={checked4} {...spread} />
</div>
<!-- defaultChecked=true, checked=null -->
<div class="test-5">
<input type="checkbox" {defaultChecked} checked={checked5} {...spread} />
<input type="checkbox" {defaultChecked} bind:checked={checked6} {...spread} />
<input type="checkbox" defaultChecked checked={checked7} {...spread} />
<input type="checkbox" defaultChecked bind:checked={checked8} {...spread} />
</div>
<!-- defaultChecked=true, checked=false -->
<div class="test-6">
<input type="checkbox" {defaultChecked} checked={checked9} {...spread} />
<input type="checkbox" {defaultChecked} bind:checked={checked10} {...spread} />
<input type="checkbox" defaultChecked checked={checked11} {...spread} />
<input type="checkbox" defaultChecked bind:checked={checked12} {...spread} />
</div>
<!-- defaultChecked=false, checked=true -->
<div class="test-7">
<input type="checkbox" defaultChecked={false} checked={checked13} {...spread} />
<input type="checkbox" defaultChecked={false} bind:checked={checked14} {...spread} />
</div>
<!-- no support for bind:group; too complex + we may deprecate it in favor of bind:checked={get,set} -->
<p>Select (single)</p>
<!-- select with static checked, value=undefined-->
<select bind:value={selected1}>
<option value="a">A</option>
<option value="b" selected {...spread}>B</option>
<option value="c">C</option>
</select>
<!-- select with dynamic checked, value=undefined-->
<select bind:value={selected2}>
<option value="a">A</option>
<option value="b" selected={defaultChecked}>B</option>
<option value="c">C</option>
</select>
<!-- select with static checked, value=something else than default-->
<select bind:value={selected3}>
<option value="a">A</option>
<option value="b" selected {...spread}>B</option>
<option value="c">C</option>
</select>
<!-- select with dynamic checked, value=something else than default-->
<select bind:value={selected4}>
<option value="a">A</option>
<option value="b" selected={defaultChecked}>B</option>
<option value="c">C</option>
</select>
<p>Select (multiple)</p>
<!-- There's no possibility to have the selected attribute influence a multi select initially,
because we require the value to be an array -->
<!-- select with static checked, value=something else than default-->
<select multiple bind:value={selected5}>
<option value="a">A</option>
<option value="b" selected {...spread}>B</option>
<option value="c">C</option>
</select>
<!-- select with dynamic checked, value=something else than default-->
<select multiple bind:value={selected6}>
<option value="a">A</option>
<option value="b" selected={defaultChecked}>B</option>
<option value="c">C</option>
</select>
<p>Static values</p>
<div class="test-14">
<input value="x" defaultValue="y" {...spread} />
<input type="checkbox" checked defaultChecked={false} {...spread} />
<textarea defaultValue="y" {...spread}>x</textarea>
</div>
<input type="reset" value="Reset" />
</form>
<p>
Bound values:
<span class="test-1">{value1} {value3} {value6} {value8}</span>
<span class="test-2">{value9} {value12} {value14} {value16}</span>
<span class="test-3">{value17} {value20} {value22} {value24}</span>
<span class="test-4">{checked2} {checked4}</span>
<span class="test-5">{checked6} {checked8}</span>
<span class="test-6">{checked10} {checked12}</span>
<span class="test-7">{checked14}</span>
<span class="test-8">{selected1}</span>
<span class="test-9">{selected2}</span>
<span class="test-10">{selected3}</span>
<span class="test-11">{selected4}</span>
<span class="test-12">{selected5}</span>
<span class="test-13">{selected6}</span>
</p>

@ -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 `<link xmlns="http://www.w3.org/1999/xhtml" href="/bar" />` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value'
]
});

@ -0,0 +1,5 @@
<script>
let { browser } = $props();
</script>
<link href={browser ? '/foo' : '/bar'} />

@ -0,0 +1,32 @@
import { test } from '../../test';
// <option value> is special because falsy values should result in an empty string value attribute
export default test({
mode: ['client'],
test({ assert, target }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
`
);
}
});

@ -0,0 +1,26 @@
<script>
let nonreactive = undefined;
let reactive = $state();
let nonreactive_spread = { value: undefined };
let reactive_spread = $state({ value: undefined });
</script>
<select>
<option value={undefined}>Default</option>
</select>
<select>
<option value={nonreactive}>Default</option>
</select>
<select>
<option value={reactive}>Default</option>
</select>
<select>
<option {...nonreactive_spread}>Default</option>
</select>
<select>
<option {...reactive_spread}>Default</option>
</select>

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, component, target }) {
const options = target.querySelectorAll('option');
assert.equal(options[0].selected, true);
assert.equal(options[1].selected, false);
// Shouldn't change the value because the value is not bound.
component.attrs = { value: ['2'] };
flushSync();
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
}
});

@ -0,0 +1,7 @@
<script>
import Select from './select.svelte';
let { attrs = { value: ['1'] } } = $props();
</script>
<Select {attrs} />

@ -0,0 +1,8 @@
<script>
let { attrs } = $props();
</script>
<select multiple {...attrs}>
<option value="1">option 1</option>
<option value="2">option 2</option>
</select>

@ -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<HTMLOptionElement>} */ (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));
}
});

@ -0,0 +1,12 @@
<script>
let { spread } = $props();
let value = $state(['Hello', 'World']);
</script>
<select multiple {value} {...spread}>
<option>Hello</option>
<option>World</option>
</select>
<input type="checkbox" value="Hello" bind:group={value}>
<input type="checkbox" value="World" bind:group={value}>
Loading…
Cancel
Save