From 94da90ff15ef8288aa5b490bf938b67bf3cbf627 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Fri, 21 Feb 2020 09:23:03 +0800 Subject: [PATCH 01/11] fix some hydration bug, more to go --- .../render_dom/wrappers/Element/index.ts | 2 +- .../compile/render_ssr/handlers/Element.ts | 17 +++++++++-------- test/runtime/index.ts | 15 ++++++++++++++- .../attribute-boolean-indeterminate/_config.js | 4 ++++ .../samples/attribute-dynamic-type/_config.js | 3 +-- test/runtime/samples/ondestroy-deep/_config.js | 3 +++ 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index b21a1aa24b59..c3d9f9c5607e 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -374,7 +374,7 @@ export default class ElementWrapper extends Wrapper { get_claim_statement(nodes: Identifier) { const attributes = this.node.attributes .filter((attr) => attr.type === 'Attribute') - .map((attr) => p`${attr.name}: true`); + .map((attr) => p`${fix_attribute_casing(attr.name)}: true`); const name = this.node.namespace ? this.node.name diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index d10c16519876..1948b4e3eaa8 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -6,6 +6,7 @@ import Element from '../../nodes/Element'; import { x } from 'code-red'; import Expression from '../../nodes/shared/Expression'; import remove_whitespace_children from './utils/remove_whitespace_children'; +import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; export default function(node: Element, renderer: Renderer, options: RenderOptions) { @@ -45,16 +46,16 @@ export default function(node: Element, renderer: Renderer, options: RenderOption if (name === 'value' && node.name.toLowerCase() === 'textarea') { node_contents = get_attribute_value(attribute); } else if (attribute.is_true) { - args.push(x`{ ${attribute.name}: true }`); + args.push(x`{ ${fix_attribute_casing(attribute.name)}: true }`); } else if ( boolean_attributes.has(name) && attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text' ) { // a boolean attribute with one non-Text chunk - args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} || null }`); + args.push(x`{ ${fix_attribute_casing(attribute.name)}: ${(attribute.chunks[0] as Expression).node} || null }`); } else { - args.push(x`{ ${attribute.name}: ${get_attribute_value(attribute)} }`); + args.push(x`{ ${fix_attribute_casing(attribute.name)}: ${get_attribute_value(attribute)} }`); } } }); @@ -67,7 +68,7 @@ export default function(node: Element, renderer: Renderer, options: RenderOption if (name === 'value' && node.name.toLowerCase() === 'textarea') { node_contents = get_attribute_value(attribute); } else if (attribute.is_true) { - renderer.add_string(` ${attribute.name}`); + renderer.add_string(` ${fix_attribute_casing(attribute.name)}`); } else if ( boolean_attributes.has(name) && attribute.chunks.length === 1 && @@ -75,17 +76,17 @@ export default function(node: Element, renderer: Renderer, options: RenderOption ) { // a boolean attribute with one non-Text chunk renderer.add_string(' '); - renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attribute.name}" : ""`); + renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${fix_attribute_casing(attribute.name)}" : ""`); } else if (name === 'class' && class_expression) { add_class_attribute = false; - renderer.add_string(` ${attribute.name}="`); + renderer.add_string(` ${fix_attribute_casing(attribute.name)}="`); renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`); renderer.add_string('"'); } else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') { const snippet = (attribute.chunks[0] as Expression).node; - renderer.add_expression(x`@add_attribute("${attribute.name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); + renderer.add_expression(x`@add_attribute("${fix_attribute_casing(attribute.name)}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); } else { - renderer.add_string(` ${attribute.name}="`); + renderer.add_string(` ${fix_attribute_casing(attribute.name)}="`); renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute)); renderer.add_string('"'); } diff --git a/test/runtime/index.ts b/test/runtime/index.ts index 153ac9dbb813..265c2285ff88 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -153,6 +153,17 @@ describe('runtime', () => { const target = window.document.querySelector('main'); + if (hydrate) { + // ssr into target + compileOptions.generate = 'ssr'; + cleanRequireCache(); + const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default; + const { html } = SsrSvelteComponent.render(config.props); + target.innerHTML = html; + + delete compileOptions.generate; + } + const warnings = []; const warn = console.warn; console.warn = warning => { @@ -182,7 +193,9 @@ describe('runtime', () => { throw new Error('Received unexpected warnings'); } - if (config.html) { + if (hydrate && config.ssrHtml) { + assert.htmlEqual(target.innerHTML, config.ssrHtml); + } else if (config.html) { assert.htmlEqual(target.innerHTML, config.html); } diff --git a/test/runtime/samples/attribute-boolean-indeterminate/_config.js b/test/runtime/samples/attribute-boolean-indeterminate/_config.js index d16cb5f96bec..63ecefb2215a 100644 --- a/test/runtime/samples/attribute-boolean-indeterminate/_config.js +++ b/test/runtime/samples/attribute-boolean-indeterminate/_config.js @@ -11,6 +11,10 @@ export default { `, + // somehow ssr will render indeterminate="" + // the hydrated html will still contain that attribute + ssrHtml: ``, + test({ assert, component, target }) { const input = target.querySelector('input'); diff --git a/test/runtime/samples/attribute-dynamic-type/_config.js b/test/runtime/samples/attribute-dynamic-type/_config.js index 19aa33d65ece..fa00cb6bc3ed 100644 --- a/test/runtime/samples/attribute-dynamic-type/_config.js +++ b/test/runtime/samples/attribute-dynamic-type/_config.js @@ -1,12 +1,11 @@ export default { - skip_if_ssr: true, - props: { inputType: 'text', inputValue: 42 }, html: '', + ssrHtml: '', test({ assert, component, target }) { const input = target.querySelector('input'); diff --git a/test/runtime/samples/ondestroy-deep/_config.js b/test/runtime/samples/ondestroy-deep/_config.js index 3f82e8b601a1..397d8c5684e3 100644 --- a/test/runtime/samples/ondestroy-deep/_config.js +++ b/test/runtime/samples/ondestroy-deep/_config.js @@ -2,6 +2,9 @@ import { destroyed, reset } from './destroyed.js'; export default { test({ assert, component }) { + // for hydration, ssr may have pushed to `destroyed` + reset(); + component.visible = false; assert.deepEqual(destroyed, ['A', 'B', 'C']); From e1f7c60ea9e88ec943a707469553096d770ca837 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Sat, 22 Feb 2020 09:46:21 +0800 Subject: [PATCH 02/11] fix more tests --- .../compile/render_dom/wrappers/Element/index.ts | 6 +++--- test/runtime/index.ts | 9 +++------ .../samples/if-block-conservative-update/_config.js | 3 +++ .../if-block-else-conservative-update/_config.js | 5 +++++ .../lifecycle-render-order-for-children/_config.js | 6 +++--- .../runtime/samples/lifecycle-render-order/_config.js | 5 +++-- test/runtime/samples/noscript-removal/_config.js | 11 ++++++++--- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index c3d9f9c5607e..4e69c7369a00 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -372,9 +372,9 @@ export default class ElementWrapper extends Wrapper { } get_claim_statement(nodes: Identifier) { - const attributes = this.node.attributes - .filter((attr) => attr.type === 'Attribute') - .map((attr) => p`${fix_attribute_casing(attr.name)}: true`); + const attributes = this.attributes + .filter((attr) => attr.node.type === 'Attribute' && !attr.get_property_name()) + .map((attr) => p`${fix_attribute_casing(attr.node.name)}: true`); const name = this.node.namespace ? this.node.name diff --git a/test/runtime/index.ts b/test/runtime/index.ts index 265c2285ff88..da2228472b74 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -146,8 +146,6 @@ describe('runtime', () => { throw err; } - if (config.before_test) config.before_test(); - // Put things we need on window for testing window.SvelteComponent = SvelteComponent; @@ -160,10 +158,11 @@ describe('runtime', () => { const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default; const { html } = SsrSvelteComponent.render(config.props); target.innerHTML = html; - delete compileOptions.generate; } + if (config.before_test) config.before_test(); + const warnings = []; const warn = console.warn; console.warn = warning => { @@ -193,9 +192,7 @@ describe('runtime', () => { throw new Error('Received unexpected warnings'); } - if (hydrate && config.ssrHtml) { - assert.htmlEqual(target.innerHTML, config.ssrHtml); - } else if (config.html) { + if (config.html) { assert.htmlEqual(target.innerHTML, config.html); } diff --git a/test/runtime/samples/if-block-conservative-update/_config.js b/test/runtime/samples/if-block-conservative-update/_config.js index f84ccb6b43e6..f7ba01a8fdfe 100644 --- a/test/runtime/samples/if-block-conservative-update/_config.js +++ b/test/runtime/samples/if-block-conservative-update/_config.js @@ -11,6 +11,9 @@ export default { html: '

potato

', + before_test() { + count = 0; + }, test({ assert, component, target }) { assert.equal(count, 1); diff --git a/test/runtime/samples/if-block-else-conservative-update/_config.js b/test/runtime/samples/if-block-else-conservative-update/_config.js index da83385dd593..b3db0bca549e 100644 --- a/test/runtime/samples/if-block-else-conservative-update/_config.js +++ b/test/runtime/samples/if-block-else-conservative-update/_config.js @@ -17,6 +17,11 @@ export default { html: '

potato

', + before_test() { + count_a = 0; + count_b = 0; + }, + test({ assert, component, target }) { assert.equal(count_a, 1); assert.equal(count_b, 0); diff --git a/test/runtime/samples/lifecycle-render-order-for-children/_config.js b/test/runtime/samples/lifecycle-render-order-for-children/_config.js index d182d5a97eb8..b85c91d07fbe 100644 --- a/test/runtime/samples/lifecycle-render-order-for-children/_config.js +++ b/test/runtime/samples/lifecycle-render-order-for-children/_config.js @@ -2,7 +2,9 @@ import order from './order.js'; export default { skip_if_ssr: true, - + before_test() { + order.length = 0; + }, test({ assert, component, target, compileOptions }) { if (compileOptions.hydratable) { assert.deepEqual(order, [ @@ -43,7 +45,5 @@ export default { '0: afterUpdate' ]); } - - order.length = 0; } }; diff --git a/test/runtime/samples/lifecycle-render-order/_config.js b/test/runtime/samples/lifecycle-render-order/_config.js index 5080973cefbc..2bbab7a838cc 100644 --- a/test/runtime/samples/lifecycle-render-order/_config.js +++ b/test/runtime/samples/lifecycle-render-order/_config.js @@ -3,6 +3,9 @@ import order from './order.js'; export default { skip_if_ssr: true, + before_test() { + order.length = 0; + }, test({ assert }) { assert.deepEqual(order, [ 'beforeUpdate', @@ -10,7 +13,5 @@ export default { 'onMount', 'afterUpdate' ]); - - order.length = 0; } }; diff --git a/test/runtime/samples/noscript-removal/_config.js b/test/runtime/samples/noscript-removal/_config.js index 709792c14a65..9ba7d55d5ced 100644 --- a/test/runtime/samples/noscript-removal/_config.js +++ b/test/runtime/samples/noscript-removal/_config.js @@ -1,9 +1,14 @@ export default { - skip_if_ssr: true, - html: `
foo
foo
foo
-` + `, + ssrHtml: ` + + +
foo
+ +
foo
foo
+ ` }; From e7efc3e568d839c5472f57ef25cf823e7c3da33d Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Sat, 22 Feb 2020 13:10:37 +0800 Subject: [PATCH 03/11] fix rest of the failed tests --- .../render_dom/wrappers/Element/index.ts | 7 ++- .../render_dom/wrappers/RawMustacheTag.ts | 7 ++- .../compile/render_ssr/handlers/HtmlTag.ts | 2 + src/runtime/internal/dom.ts | 44 +++++++++++++++++-- .../each-block-changed-check/expected.js | 1 + .../samples/component-namespaced/_config.js | 4 -- .../component-namespaced/components.js | 3 -- .../component-namespaced/components.svelte | 5 +++ .../samples/component-namespaced/main.svelte | 2 +- .../samples/deconflict-builtins-2/_config.js | 2 +- .../samples/deconflict-builtins-2/main.svelte | 5 ++- test/runtime/samples/raw-mustaches/_config.js | 1 - 12 files changed, 65 insertions(+), 18 deletions(-) delete mode 100644 test/runtime/samples/component-namespaced/components.js create mode 100644 test/runtime/samples/component-namespaced/components.svelte diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 4e69c7369a00..d40c17b631d8 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -232,7 +232,12 @@ export default class ElementWrapper extends Wrapper { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { const { renderer } = this; - if (this.node.name === 'noscript') return; + if (this.node.name === 'noscript') { + if (renderer.options.hydratable) { + block.chunks.claim.push(b`@claim_noscript(${parent_nodes});`); + } + return; + } const node = this.var; const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`); // if we're in unclaimable territory, i.e. , parent_nodes is null diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts index fd43ffcc326d..51e7213cd91a 100644 --- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts @@ -51,8 +51,11 @@ export default class RawMustacheTagWrapper extends Tag { const update_anchor = needs_anchor ? html_anchor : this.next ? this.next.var : 'null'; - block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${update_anchor});`); - block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`); + block.chunks.create.push(b`${html_tag} = new @HtmlTag(${update_anchor});`); + if (this.renderer.options.hydratable) { + block.chunks.claim.push(b`${html_tag} = @claim_html_tag(${update_anchor}, ${_parent_nodes});`); + } + block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : 'anchor'});`); if (needs_anchor) { block.add_element(html_anchor, x`@empty()`, x`@empty()`, parent_node); diff --git a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts index c0a7952e65bf..13744d0759ac 100644 --- a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts +++ b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts @@ -3,5 +3,7 @@ import RawMustacheTag from '../../nodes/RawMustacheTag'; import { Expression } from 'estree'; export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) { + renderer.add_string(''); renderer.add_expression(node.expression.node as Expression); + renderer.add_string(''); } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 40471c298058..8151882c9e62 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -191,6 +191,33 @@ export function claim_space(nodes) { return claim_text(nodes, ' '); } +export function claim_noscript(nodes) { + detach(claim_element(nodes, 'NOSCRIPT', {}, false)); +} + +function find_comment(nodes, text, start) { + for (let i = start; i < nodes.length; i += 1) { + const node = nodes[i]; + if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) { + return i; + } + } + return nodes.length; +} + +export function claim_html_tag(update_anchor, nodes) { + // find html opening tag + const start_index = find_comment(nodes, 'HTML_TAG_START', 0); + const end_index = find_comment(nodes, 'HTML_TAG_END', start_index); + if (start_index === end_index) { + return new HtmlTag(update_anchor); + } + const html_tag_nodes = nodes.splice(start_index, end_index + 1); + detach(html_tag_nodes[0]); + detach(html_tag_nodes[html_tag_nodes.length - 1]); + return new HtmlTag(update_anchor, html_tag_nodes.slice(1, html_tag_nodes.length - 1)); +} + export function set_data(text, data) { data = '' + data; if (text.wholeText !== data) text.data = data; @@ -318,27 +345,38 @@ export function query_selector_all(selector: string, parent: HTMLElement = docum } export class HtmlTag { + // parent for creating node e: HTMLElement; + // html tag nodes n: ChildNode[]; + // hydration claimed nodes + l: ChildNode[] | void; + // target t: HTMLElement; + // anchor a: HTMLElement; - constructor(anchor: HTMLElement = null) { + constructor(anchor: HTMLElement = null, claimed_nodes?: ChildNode[]) { this.a = anchor; this.e = this.n = null; + this.l = claimed_nodes; } m(html: string, target: HTMLElement, anchor: HTMLElement = null) { if (!this.e) { this.e = element(target.nodeName as keyof HTMLElementTagNameMap); this.t = target; - this.h(html); + if (this.l) { + this.n = this.l; + } else { + this.h(html); + } } this.i(anchor); } - h(html) { + h(html: string) { this.e.innerHTML = html; this.n = Array.from(this.e.childNodes); } diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 63bc1d8607b8..6ae6f81dccec 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -52,6 +52,7 @@ function create_each_block(ctx) { t4 = text(t4_value); t5 = text(" ago:"); t6 = space(); + html_tag = new HtmlTag(raw_value); attr(span, "class", "meta"); html_tag = new HtmlTag(null); attr(div, "class", "comment"); diff --git a/test/runtime/samples/component-namespaced/_config.js b/test/runtime/samples/component-namespaced/_config.js index 35a5e46d47e6..9ee6b5e7882f 100644 --- a/test/runtime/samples/component-namespaced/_config.js +++ b/test/runtime/samples/component-namespaced/_config.js @@ -9,10 +9,6 @@ export default {

foo 1

`, - before_test() { - delete require.cache[path.resolve(__dirname, 'components.js')]; - }, - test({ assert, component, target }) { component.a = 2; assert.htmlEqual(target.innerHTML, ` diff --git a/test/runtime/samples/component-namespaced/components.js b/test/runtime/samples/component-namespaced/components.js deleted file mode 100644 index 7dcfcf157c63..000000000000 --- a/test/runtime/samples/component-namespaced/components.js +++ /dev/null @@ -1,3 +0,0 @@ -import Foo from './Foo.svelte'; - -export default { Foo }; diff --git a/test/runtime/samples/component-namespaced/components.svelte b/test/runtime/samples/component-namespaced/components.svelte new file mode 100644 index 000000000000..5b9a6c5167a8 --- /dev/null +++ b/test/runtime/samples/component-namespaced/components.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/runtime/samples/component-namespaced/main.svelte b/test/runtime/samples/component-namespaced/main.svelte index 541b68e47e8f..25862cf6f2b1 100644 --- a/test/runtime/samples/component-namespaced/main.svelte +++ b/test/runtime/samples/component-namespaced/main.svelte @@ -1,5 +1,5 @@ diff --git a/test/runtime/samples/deconflict-builtins-2/_config.js b/test/runtime/samples/deconflict-builtins-2/_config.js index e136b0410dff..fba811b8807e 100644 --- a/test/runtime/samples/deconflict-builtins-2/_config.js +++ b/test/runtime/samples/deconflict-builtins-2/_config.js @@ -1,4 +1,4 @@ export default { - html: 'hello world', + html: 'hello world', preserveIdentifiers: true }; diff --git a/test/runtime/samples/deconflict-builtins-2/main.svelte b/test/runtime/samples/deconflict-builtins-2/main.svelte index 82f9213045c8..db10a81c74a5 100644 --- a/test/runtime/samples/deconflict-builtins-2/main.svelte +++ b/test/runtime/samples/deconflict-builtins-2/main.svelte @@ -1,5 +1,6 @@ - -{foo} \ No newline at end of file + + {foo} + \ No newline at end of file diff --git a/test/runtime/samples/raw-mustaches/_config.js b/test/runtime/samples/raw-mustaches/_config.js index 9eda8289cebd..a69b91d2d8e8 100644 --- a/test/runtime/samples/raw-mustaches/_config.js +++ b/test/runtime/samples/raw-mustaches/_config.js @@ -1,5 +1,4 @@ export default { - skip_if_ssr: true, props: { raw: 'raw html!!!\\o/' From 81a89239dd5e7a76f7f30cbcc59c245e9641869e Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Sat, 22 Feb 2020 13:36:48 +0800 Subject: [PATCH 04/11] add comment tag only when hydratable --- src/compiler/compile/render_ssr/handlers/HtmlTag.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts index 13744d0759ac..8f85f2fc90b4 100644 --- a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts +++ b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts @@ -3,7 +3,7 @@ import RawMustacheTag from '../../nodes/RawMustacheTag'; import { Expression } from 'estree'; export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) { - renderer.add_string(''); + if (options.hydratable) renderer.add_string(''); renderer.add_expression(node.expression.node as Expression); - renderer.add_string(''); + if (options.hydratable) renderer.add_string(''); } From b503b7c354bad858e02b98a18016aa3824b94d34 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Sat, 22 Feb 2020 20:16:06 +0800 Subject: [PATCH 05/11] ignore noscript when claiming --- .../render_dom/wrappers/Element/index.ts | 7 +---- .../compile/render_ssr/handlers/HtmlTag.ts | 2 +- src/runtime/internal/dom.ts | 4 --- .../samples/noscript-removal/_config.js | 31 +++++++++++++++---- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index d40c17b631d8..4e69c7369a00 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -232,12 +232,7 @@ export default class ElementWrapper extends Wrapper { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { const { renderer } = this; - if (this.node.name === 'noscript') { - if (renderer.options.hydratable) { - block.chunks.claim.push(b`@claim_noscript(${parent_nodes});`); - } - return; - } + if (this.node.name === 'noscript') return; const node = this.var; const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`); // if we're in unclaimable territory, i.e. , parent_nodes is null diff --git a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts index 8f85f2fc90b4..cd62b959818e 100644 --- a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts +++ b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts @@ -2,7 +2,7 @@ import Renderer, { RenderOptions } from '../Renderer'; import RawMustacheTag from '../../nodes/RawMustacheTag'; import { Expression } from 'estree'; -export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) { +export default function(node: RawMustacheTag, renderer: Renderer, options: RenderOptions) { if (options.hydratable) renderer.add_string(''); renderer.add_expression(node.expression.node as Expression); if (options.hydratable) renderer.add_string(''); diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 8151882c9e62..921f009f5d4f 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -191,10 +191,6 @@ export function claim_space(nodes) { return claim_text(nodes, ' '); } -export function claim_noscript(nodes) { - detach(claim_element(nodes, 'NOSCRIPT', {}, false)); -} - function find_comment(nodes, text, start) { for (let i = start; i < nodes.length; i += 1) { const node = nodes[i]; diff --git a/test/runtime/samples/noscript-removal/_config.js b/test/runtime/samples/noscript-removal/_config.js index 9ba7d55d5ced..0fd03ea22332 100644 --- a/test/runtime/samples/noscript-removal/_config.js +++ b/test/runtime/samples/noscript-removal/_config.js @@ -1,14 +1,33 @@ export default { - html: ` -
foo
- -
foo
foo
- `, ssrHtml: `
foo
foo
foo
- ` + `, + test({ assert, target, compileOptions }) { + // if created on client side, should not build noscript + if (!compileOptions.hydratable) { + assert.equal(target.querySelectorAll('noscript').length, 0); + } + + // it's okay not to remove the node during hydration + // will not be seen by user anyway + removeNoScript(target); + + assert.htmlEqual( + target.innerHTML, + ` +
foo
+
foo
foo
+ ` + ); + } }; + +function removeNoScript(target) { + target.querySelectorAll('noscript').forEach(elem => { + elem.parentNode.removeChild(elem); + }); +} From 57b5552b01e6b0e7b045aa14d2bf478e085d8839 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Mon, 20 Apr 2020 21:46:33 +0800 Subject: [PATCH 06/11] make test handle both ssr html and initial html --- test/runtime/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/runtime/index.ts b/test/runtime/index.ts index da2228472b74..a8e1b145292f 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -49,7 +49,7 @@ describe('runtime', () => { const failed = new Set(); - function runTest(dir, hydrate) { + function runTest(dir, hydrate, from_ssr_html) { if (dir[0] === '.') return; const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`); @@ -61,7 +61,8 @@ describe('runtime', () => { throw new Error('Forgot to remove `solo: true` from test'); } - (config.skip ? it.skip : solo ? it.only : it)(`${dir} ${hydrate ? '(with hydration)' : ''}`, () => { + const testName = `${dir} ${hydrate ? `(with hydration ${from_ssr_html ? 'from ssr rendered html' : ''})` : ''}`; + (config.skip ? it.skip : solo ? it.only : it)(testName, () => { if (failed.has(dir)) { // this makes debugging easier, by only printing compiled output once throw new Error('skipping test, already failed'); @@ -151,7 +152,7 @@ describe('runtime', () => { const target = window.document.querySelector('main'); - if (hydrate) { + if (hydrate && from_ssr_html) { // ssr into target compileOptions.generate = 'ssr'; cleanRequireCache(); @@ -255,7 +256,8 @@ describe('runtime', () => { fs.readdirSync(`${__dirname}/samples`).forEach(dir => { runTest(dir, false); - runTest(dir, true); + runTest(dir, true, false); + runTest(dir, true, true); }); async function create_component(src = '
') { From 3707b9abf98a54040a70f1bec8034bc664d901e0 Mon Sep 17 00:00:00 2001 From: tanhauhau Date: Tue, 15 Jun 2021 11:02:30 +0800 Subject: [PATCH 07/11] fix ts --- src/compiler/compile/render_dom/wrappers/Element/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 4e69c7369a00..51af8b065595 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -373,7 +373,7 @@ export default class ElementWrapper extends Wrapper { get_claim_statement(nodes: Identifier) { const attributes = this.attributes - .filter((attr) => attr.node.type === 'Attribute' && !attr.get_property_name()) + .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) .map((attr) => p`${fix_attribute_casing(attr.node.name)}: true`); const name = this.node.namespace From bb6fac8fbc8e9f70a11280db3d77007056a57cb6 Mon Sep 17 00:00:00 2001 From: tanhauhau Date: Wed, 16 Jun 2021 20:43:54 +0800 Subject: [PATCH 08/11] fix test --- .../render_dom/wrappers/Element/index.ts | 2 +- .../render_dom/wrappers/RawMustacheTag.ts | 7 ++++--- .../compile/render_ssr/handlers/Element.ts | 19 +++++++++++-------- src/runtime/internal/dom.ts | 9 ++++----- .../each-block-changed-check/expected.js | 4 ++-- test/runtime/index.ts | 3 +++ .../_config.js | 1 + .../_config.js | 1 + .../_config.js | 2 +- .../each-block-keyed-dyanmic-key/_config.js | 4 ++++ .../samples/raw-anchor-first-child/_config.js | 1 + .../_config.js | 3 +++ .../samples/store-unreferenced/_config.js | 6 ++++-- 13 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 51af8b065595..f1ca4e993d46 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -374,7 +374,7 @@ export default class ElementWrapper extends Wrapper { get_claim_statement(nodes: Identifier) { const attributes = this.attributes .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) - .map((attr) => p`${fix_attribute_casing(attr.node.name)}: true`); + .map((attr) => p`${attr.name}: true`); const name = this.node.namespace ? this.node.name diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts index 51e7213cd91a..1315f1e14447 100644 --- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts @@ -51,11 +51,12 @@ export default class RawMustacheTagWrapper extends Tag { const update_anchor = needs_anchor ? html_anchor : this.next ? this.next.var : 'null'; - block.chunks.create.push(b`${html_tag} = new @HtmlTag(${update_anchor});`); + block.chunks.create.push(b`${html_tag} = new @HtmlTag();`); if (this.renderer.options.hydratable) { - block.chunks.claim.push(b`${html_tag} = @claim_html_tag(${update_anchor}, ${_parent_nodes});`); + block.chunks.claim.push(b`${html_tag} = @claim_html_tag(${_parent_nodes});`); } - block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : 'anchor'});`); + block.chunks.hydrate.push(b`${html_tag}.a = ${update_anchor};`); + block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`); if (needs_anchor) { block.add_element(html_anchor, x`@empty()`, x`@empty()`, parent_node); diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 1948b4e3eaa8..a0bae4044aa5 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -7,6 +7,7 @@ import { x } from 'code-red'; import Expression from '../../nodes/shared/Expression'; import remove_whitespace_children from './utils/remove_whitespace_children'; import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; +import { namespaces } from '../../../utils/namespaces'; export default function(node: Element, renderer: Renderer, options: RenderOptions) { @@ -42,20 +43,21 @@ export default function(node: Element, renderer: Renderer, options: RenderOption if (attribute.is_spread) { args.push(attribute.expression.node); } else { + const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name); const name = attribute.name.toLowerCase(); if (name === 'value' && node.name.toLowerCase() === 'textarea') { node_contents = get_attribute_value(attribute); } else if (attribute.is_true) { - args.push(x`{ ${fix_attribute_casing(attribute.name)}: true }`); + args.push(x`{ ${attr_name}: true }`); } else if ( boolean_attributes.has(name) && attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text' ) { // a boolean attribute with one non-Text chunk - args.push(x`{ ${fix_attribute_casing(attribute.name)}: ${(attribute.chunks[0] as Expression).node} || null }`); + args.push(x`{ ${attr_name}: ${(attribute.chunks[0] as Expression).node} || null }`); } else { - args.push(x`{ ${fix_attribute_casing(attribute.name)}: ${get_attribute_value(attribute)} }`); + args.push(x`{ ${attr_name}: ${get_attribute_value(attribute)} }`); } } }); @@ -65,10 +67,11 @@ export default function(node: Element, renderer: Renderer, options: RenderOption let add_class_attribute = !!class_expression; node.attributes.forEach(attribute => { const name = attribute.name.toLowerCase(); + const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name); if (name === 'value' && node.name.toLowerCase() === 'textarea') { node_contents = get_attribute_value(attribute); } else if (attribute.is_true) { - renderer.add_string(` ${fix_attribute_casing(attribute.name)}`); + renderer.add_string(` ${attr_name}`); } else if ( boolean_attributes.has(name) && attribute.chunks.length === 1 && @@ -76,17 +79,17 @@ export default function(node: Element, renderer: Renderer, options: RenderOption ) { // a boolean attribute with one non-Text chunk renderer.add_string(' '); - renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${fix_attribute_casing(attribute.name)}" : ""`); + renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attr_name}" : ""`); } else if (name === 'class' && class_expression) { add_class_attribute = false; - renderer.add_string(` ${fix_attribute_casing(attribute.name)}="`); + renderer.add_string(` ${attr_name}="`); renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`); renderer.add_string('"'); } else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') { const snippet = (attribute.chunks[0] as Expression).node; - renderer.add_expression(x`@add_attribute("${fix_attribute_casing(attribute.name)}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); + renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`); } else { - renderer.add_string(` ${fix_attribute_casing(attribute.name)}="`); + renderer.add_string(` ${attr_name}="`); renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute)); renderer.add_string('"'); } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 921f009f5d4f..1b4c4451bcb8 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -201,17 +201,17 @@ function find_comment(nodes, text, start) { return nodes.length; } -export function claim_html_tag(update_anchor, nodes) { +export function claim_html_tag(nodes) { // find html opening tag const start_index = find_comment(nodes, 'HTML_TAG_START', 0); const end_index = find_comment(nodes, 'HTML_TAG_END', start_index); if (start_index === end_index) { - return new HtmlTag(update_anchor); + return new HtmlTag(); } const html_tag_nodes = nodes.splice(start_index, end_index + 1); detach(html_tag_nodes[0]); detach(html_tag_nodes[html_tag_nodes.length - 1]); - return new HtmlTag(update_anchor, html_tag_nodes.slice(1, html_tag_nodes.length - 1)); + return new HtmlTag(html_tag_nodes.slice(1, html_tag_nodes.length - 1)); } export function set_data(text, data) { @@ -352,8 +352,7 @@ export class HtmlTag { // anchor a: HTMLElement; - constructor(anchor: HTMLElement = null, claimed_nodes?: ChildNode[]) { - this.a = anchor; + constructor(claimed_nodes?: ChildNode[]) { this.e = this.n = null; this.l = claimed_nodes; } diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 6ae6f81dccec..0020235d44ef 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -52,9 +52,9 @@ function create_each_block(ctx) { t4 = text(t4_value); t5 = text(" ago:"); t6 = space(); - html_tag = new HtmlTag(raw_value); + html_tag = new HtmlTag(); attr(span, "class", "meta"); - html_tag = new HtmlTag(null); + html_tag.a = null; attr(div, "class", "comment"); }, m(target, anchor) { diff --git a/test/runtime/index.ts b/test/runtime/index.ts index a8e1b145292f..c72d9ef55504 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -56,6 +56,7 @@ describe('runtime', () => { const solo = config.solo || /\.solo/.test(dir); if (hydrate && config.skip_if_hydrate) return; + if (hydrate && from_ssr_html && config.skip_if_hydrate_from_ssr) return; if (solo && process.env.CI) { throw new Error('Forgot to remove `solo: true` from test'); @@ -160,6 +161,8 @@ describe('runtime', () => { const { html } = SsrSvelteComponent.render(config.props); target.innerHTML = html; delete compileOptions.generate; + } else { + target.innerHTML = ''; } if (config.before_test) config.before_test(); diff --git a/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js b/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js index 0439aca06a24..9f8ee61c7a20 100644 --- a/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js +++ b/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js @@ -11,6 +11,7 @@ export default { options: { hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements. }, + skip_if_hydrate_from_ssr: true, compileOptions: { namespace: 'foreign' }, diff --git a/test/runtime/samples/attribute-casing-foreign-namespace/_config.js b/test/runtime/samples/attribute-casing-foreign-namespace/_config.js index d7eca6aba96a..f74f62252409 100644 --- a/test/runtime/samples/attribute-casing-foreign-namespace/_config.js +++ b/test/runtime/samples/attribute-casing-foreign-namespace/_config.js @@ -9,6 +9,7 @@ export default { options: { hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements. }, + skip_if_hydrate_from_ssr: true, test({ assert, target }) { const attr = sel => target.querySelector(sel).attributes[0].name; diff --git a/test/runtime/samples/binding-this-each-block-property-2/_config.js b/test/runtime/samples/binding-this-each-block-property-2/_config.js index b67d27050efd..49131d6635a2 100644 --- a/test/runtime/samples/binding-this-each-block-property-2/_config.js +++ b/test/runtime/samples/binding-this-each-block-property-2/_config.js @@ -7,7 +7,7 @@ export default { props: { callback }, - after_test() { + before_test() { calls = []; }, async test({ assert, component, target }) { diff --git a/test/runtime/samples/each-block-keyed-dyanmic-key/_config.js b/test/runtime/samples/each-block-keyed-dyanmic-key/_config.js index 0aed1b0e0701..949235ee4891 100644 --- a/test/runtime/samples/each-block-keyed-dyanmic-key/_config.js +++ b/test/runtime/samples/each-block-keyed-dyanmic-key/_config.js @@ -9,6 +9,10 @@ export default { } }, + before_test() { + count = 0; + }, + html: `
foo
foo
diff --git a/test/runtime/samples/raw-anchor-first-child/_config.js b/test/runtime/samples/raw-anchor-first-child/_config.js index 02297675c58c..f9e6b7662622 100644 --- a/test/runtime/samples/raw-anchor-first-child/_config.js +++ b/test/runtime/samples/raw-anchor-first-child/_config.js @@ -8,5 +8,6 @@ export default { assert.ok(!span.previousSibling); component.raw = 'bar'; + assert.htmlEqual(target.innerHTML, '
bar
'); } }; diff --git a/test/runtime/samples/reactive-function-called-reassigned/_config.js b/test/runtime/samples/reactive-function-called-reassigned/_config.js index 7b2a8b72fe08..b4d50a3e84d8 100644 --- a/test/runtime/samples/reactive-function-called-reassigned/_config.js +++ b/test/runtime/samples/reactive-function-called-reassigned/_config.js @@ -9,6 +9,9 @@ export default { props: { callback }, + before_test() { + called = 0; + }, async test({ assert, component, target, window }) { assert.equal(called, 1); diff --git a/test/runtime/samples/store-unreferenced/_config.js b/test/runtime/samples/store-unreferenced/_config.js index e15f202492e5..ce416bca5dbd 100644 --- a/test/runtime/samples/store-unreferenced/_config.js +++ b/test/runtime/samples/store-unreferenced/_config.js @@ -3,11 +3,13 @@ import { count } from './store.js'; export default { html: '

count: 0

', + before_test() { + count.set(0); + }, + async test({ assert, component, target }) { await component.increment(); assert.htmlEqual(target.innerHTML, '

count: 1

'); - - count.set(0); } }; From d28f22668806afcd15eff01cc2e2240ab914a362 Mon Sep 17 00:00:00 2001 From: tanhauhau Date: Wed, 16 Jun 2021 20:48:35 +0800 Subject: [PATCH 09/11] fix ts --- src/compiler/compile/render_dom/wrappers/Element/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index f1ca4e993d46..9363015b1c26 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -374,7 +374,7 @@ export default class ElementWrapper extends Wrapper { get_claim_statement(nodes: Identifier) { const attributes = this.attributes .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) - .map((attr) => p`${attr.name}: true`); + .map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`); const name = this.node.namespace ? this.node.name From 0622e23f606fe070307c4bb2dd4919d474867fe5 Mon Sep 17 00:00:00 2001 From: tanhauhau Date: Thu, 17 Jun 2021 00:08:03 +0800 Subject: [PATCH 10/11] fix lint --- .../samples/attribute-boolean-indeterminate/_config.js | 6 ++---- test/runtime/samples/component-namespaced/_config.js | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/runtime/samples/attribute-boolean-indeterminate/_config.js b/test/runtime/samples/attribute-boolean-indeterminate/_config.js index 63ecefb2215a..38a6006feefc 100644 --- a/test/runtime/samples/attribute-boolean-indeterminate/_config.js +++ b/test/runtime/samples/attribute-boolean-indeterminate/_config.js @@ -7,13 +7,11 @@ export default { indeterminate: true }, - html: ` - - `, + html: '', // somehow ssr will render indeterminate="" // the hydrated html will still contain that attribute - ssrHtml: ``, + ssrHtml: '', test({ assert, component, target }) { const input = target.querySelector('input'); diff --git a/test/runtime/samples/component-namespaced/_config.js b/test/runtime/samples/component-namespaced/_config.js index 9ee6b5e7882f..7ec4a35c6d66 100644 --- a/test/runtime/samples/component-namespaced/_config.js +++ b/test/runtime/samples/component-namespaced/_config.js @@ -1,5 +1,3 @@ -import * as path from 'path'; - export default { props: { a: 1 From 841137afa646a1151d0261d7ff1324152bb6db0c Mon Sep 17 00:00:00 2001 From: tanhauhau Date: Sat, 19 Jun 2021 00:48:05 +0800 Subject: [PATCH 11/11] fix code review --- test/runtime/index.ts | 2 +- .../samples/attribute-boolean-indeterminate/_config.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtime/index.ts b/test/runtime/index.ts index c72d9ef55504..ff6d1039d3bd 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -62,7 +62,7 @@ describe('runtime', () => { throw new Error('Forgot to remove `solo: true` from test'); } - const testName = `${dir} ${hydrate ? `(with hydration ${from_ssr_html ? 'from ssr rendered html' : ''})` : ''}`; + const testName = `${dir} ${hydrate ? `(with hydration${from_ssr_html ? ' from ssr rendered html' : ''})` : ''}`; (config.skip ? it.skip : solo ? it.only : it)(testName, () => { if (failed.has(dir)) { // this makes debugging easier, by only printing compiled output once diff --git a/test/runtime/samples/attribute-boolean-indeterminate/_config.js b/test/runtime/samples/attribute-boolean-indeterminate/_config.js index 38a6006feefc..d6e97ffc0e7f 100644 --- a/test/runtime/samples/attribute-boolean-indeterminate/_config.js +++ b/test/runtime/samples/attribute-boolean-indeterminate/_config.js @@ -7,11 +7,11 @@ export default { indeterminate: true }, - html: '', + html: "", // somehow ssr will render indeterminate="" // the hydrated html will still contain that attribute - ssrHtml: '', + ssrHtml: "", test({ assert, component, target }) { const input = target.querySelector('input');