diff --git a/src/compiler/compile/nodes/Text.ts b/src/compiler/compile/nodes/Text.ts index bfd28a507309..6b7432c22f39 100644 --- a/src/compiler/compile/nodes/Text.ts +++ b/src/compiler/compile/nodes/Text.ts @@ -3,6 +3,18 @@ import Component from '../Component'; import TemplateScope from './shared/TemplateScope'; import { INode } from './interfaces'; +// Whitespace inside one of these elements will not result in +// a whitespace node being created in any circumstances. (This +// list is almost certainly very incomplete) +const elements_without_text = new Set([ + 'audio', + 'datalist', + 'dl', + 'optgroup', + 'select', + 'video', +]); + export default class Text extends Node { type: 'Text'; data: string; @@ -13,4 +25,21 @@ export default class Text extends Node { this.data = info.data; this.synthetic = info.synthetic || false; } + + should_skip() { + if (/\S/.test(this.data)) return false; + + const parent_element = this.find_nearest(/(?:Element|InlineComponent|Head)/); + if (!parent_element) return false; + + if (parent_element.type === 'Head') return true; + if (parent_element.type === 'InlineComponent') return parent_element.children.length === 1 && this === parent_element.children[0]; + + // svg namespace exclusions + if (/svg$/.test(parent_element.namespace)) { + if (this.prev && this.prev.type === "Element" && this.prev.name === "tspan") return false; + } + + return parent_element.namespace || elements_without_text.has(parent_element.name); + } } diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index 85f252f57e23..05bdae973a95 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -55,13 +55,9 @@ export default class AttributeWrapper { const element = this.parent; const name = fix_attribute_casing(this.node.name); - const metadata = this.get_metadata(); - const is_indirectly_bound_value = this.is_indirectly_bound_value(); - const property_name = is_indirectly_bound_value - ? '__value' - : metadata && metadata.property_name; + const property_name = this.get_property_name(); // xlink is a special case... we could maybe extend this to generic // namespaced attributes but I'm not sure that's applicable in @@ -185,6 +181,14 @@ export default class AttributeWrapper { } } + get_property_name() { + const metadata = this.get_metadata(); + const is_indirectly_bound_value = this.is_indirectly_bound_value(); + return is_indirectly_bound_value + ? '__value' + : metadata && metadata.property_name; + } + get_metadata() { if (this.parent.node.namespace) return null; const metadata = attribute_lookup[fix_attribute_casing(this.node.name)]; diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 9291f329b66b..38b91ff679c5 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -384,7 +384,7 @@ export default class ElementWrapper extends Wrapper { this.add_classes(block); this.add_manual_style_scoping(block); - if (nodes && this.renderer.options.hydratable && !this.void) { + if (nodes && this.renderer.options.hydratable && !this.void && !this.can_use_innerhtml) { block.chunks.claim.push( b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);` ); @@ -405,7 +405,7 @@ export default class ElementWrapper extends Wrapper { get_render_statement(block: Block) { const { name, namespace } = this.node; - if (namespace === 'http://www.w3.org/2000/svg') { + if (namespace === namespaces.svg) { return x`@svg_element("${name}")`; } @@ -422,9 +422,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`${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/src/compiler/compile/render_dom/wrappers/Fragment.ts b/src/compiler/compile/render_dom/wrappers/Fragment.ts index 224b17d43f9d..a0984b69b920 100644 --- a/src/compiler/compile/render_dom/wrappers/Fragment.ts +++ b/src/compiler/compile/render_dom/wrappers/Fragment.ts @@ -17,6 +17,7 @@ import { INode } from '../../nodes/interfaces'; import Renderer from '../Renderer'; import Block from '../Block'; import { trim_start, trim_end } from '../../../utils/trim'; +import { link } from '../../../utils/link'; import { Identifier } from 'estree'; const wrappers = { @@ -38,11 +39,6 @@ const wrappers = { Window }; -function link(next: Wrapper, prev: Wrapper) { - prev.next = next; - if (next) next.prev = prev; -} - function trimmable_at(child: INode, next_sibling: Wrapper): boolean { // Whitespace is trimmable if one of the following is true: // The child and its sibling share a common nearest each block (not at an each block boundary) diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts index 3b13e6c68a48..8856f17a5a6d 100644 --- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts @@ -53,7 +53,11 @@ export default class RawMustacheTagWrapper extends Tag { const update_anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null'; - block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${init}, ${update_anchor});`); + block.chunks.create.push(b`${html_tag} = new @HtmlTag(${init});`); + if (this.renderer.options.hydratable) { + block.chunks.claim.push(b`${html_tag} = @claim_html_tag(${init}, ${_parent_nodes});`); + } + block.chunks.hydrate.push(b`${html_tag}.b(${update_anchor})`); block.chunks.mount.push(b`${html_tag}.m(${parent_node || '#target'}, ${parent_node ? null : 'anchor'});`); if (needs_anchor) { diff --git a/src/compiler/compile/render_dom/wrappers/Text.ts b/src/compiler/compile/render_dom/wrappers/Text.ts index 1978cba0d7d0..7ef8aebd7011 100644 --- a/src/compiler/compile/render_dom/wrappers/Text.ts +++ b/src/compiler/compile/render_dom/wrappers/Text.ts @@ -5,36 +5,6 @@ import Wrapper from './shared/Wrapper'; import { x } from 'code-red'; import { Identifier } from 'estree'; -// Whitespace inside one of these elements will not result in -// a whitespace node being created in any circumstances. (This -// list is almost certainly very incomplete) -const elements_without_text = new Set([ - 'audio', - 'datalist', - 'dl', - 'optgroup', - 'select', - 'video', -]); - -// TODO this should probably be in Fragment -function should_skip(node: Text) { - if (/\S/.test(node.data)) return false; - - const parent_element = node.find_nearest(/(?:Element|InlineComponent|Head)/); - if (!parent_element) return false; - - if (parent_element.type === 'Head') return true; - if (parent_element.type === 'InlineComponent') return parent_element.children.length === 1 && node === parent_element.children[0]; - - // svg namespace exclusions - if (/svg$/.test(parent_element.namespace)) { - if (node.prev && node.prev.type === "Element" && node.prev.name === "tspan") return false; - } - - return parent_element.namespace || elements_without_text.has(parent_element.name); -} - export default class TextWrapper extends Wrapper { node: Text; data: string; @@ -50,7 +20,7 @@ export default class TextWrapper extends Wrapper { ) { super(renderer, block, parent, node); - this.skip = should_skip(this.node); + this.skip = this.node.should_skip(); this.data = data; this.var = (this.skip ? null : x`t`) as unknown as Identifier; } diff --git a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts index 4bf8c20bd876..96cf65756d8f 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts @@ -35,7 +35,7 @@ export default class Wrapper { } }); - this.can_use_innerhtml = !renderer.options.hydratable; + this.can_use_innerhtml = true; this.is_static_content = !renderer.options.hydratable; block.wrappers.push(this); diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 6e6a61974a29..2c39ee3ddb25 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -4,12 +4,19 @@ import { get_slot_scope } from './shared/get_slot_scope'; import { boolean_attributes } from './shared/boolean_attributes'; import Renderer, { RenderOptions } from '../Renderer'; import Element from '../../nodes/Element'; +import { INode } from '../../nodes/interfaces'; import { x } from 'code-red'; import Expression from '../../nodes/shared/Expression'; +import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; +import { trim_end, trim_start } from '../../../utils/trim'; +import { link } from '../../../utils/link'; export default function(node: Element, renderer: Renderer, options: RenderOptions & { slot_scopes: Map; }) { + + const children = remove_whitespace_children(node.children, node.next); + // awkward special case let node_contents; @@ -51,16 +58,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)} }`); } } }); @@ -73,7 +80,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 && @@ -81,17 +88,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(`"`); } @@ -133,7 +140,7 @@ export default function(node: Element, renderer: Renderer, options: RenderOption if (node_contents !== undefined) { if (contenteditable) { renderer.push(); - renderer.render(node.children, options); + renderer.render(children, options); const result = renderer.pop(); renderer.add_expression(x`($$value => $$value === void 0 ? ${result} : $$value)(${node_contents})`); @@ -145,7 +152,7 @@ export default function(node: Element, renderer: Renderer, options: RenderOption renderer.add_string(``); } } else if (slot && nearest_inline_component) { - renderer.render(node.children, options); + renderer.render(children, options); if (!is_void(node.name)) { renderer.add_string(``); @@ -163,10 +170,80 @@ export default function(node: Element, renderer: Renderer, options: RenderOption output: renderer.pop() }); } else { - renderer.render(node.children, options); + renderer.render(children, options); if (!is_void(node.name)) { renderer.add_string(``); } } } + +// similar logic from `compile/render_dom/wrappers/Fragment` +// We want to remove trailing whitespace inside an element/component/block, +// *unless* there is no whitespace between this node and its next sibling +function remove_whitespace_children(children: INode[], next?: INode): INode[] { + const nodes: INode[] = []; + let last_child: INode; + let i = children.length; + while (i--) { + const child = children[i]; + + if (child.type === 'Text') { + if (child.should_skip()) { + continue; + } + + let { data } = child; + + if (nodes.length === 0) { + const should_trim = next + ? next.type === 'Text' && + /^\s/.test(next.data) && + trimmable_at(child, next) + : !child.has_ancestor('EachBlock'); + + if (should_trim) { + data = trim_end(data); + if (!data) continue; + } + } + + // glue text nodes (which could e.g. be separated by comments) together + if (last_child && last_child.type === 'Text') { + last_child.data = data + last_child.data; + continue; + } + + nodes.unshift(child); + link(last_child, last_child = child); + } else { + nodes.unshift(child); + link(last_child, last_child = child); + } + } + + const first = nodes[0]; + if (first && first.type === 'Text') { + first.data = trim_start(first.data); + if (!first.data) { + first.var = null; + nodes.shift(); + + if (nodes[0]) { + nodes[0].prev = null; + } + } + } + + return nodes; +} + +function trimmable_at(child: INode, next_sibling: INode): boolean { + // Whitespace is trimmable if one of the following is true: + // The child and its sibling share a common nearest each block (not at an each block boundary) + // The next sibling's previous node is an each block + return ( + next_sibling.find_nearest(/EachBlock/) === + child.find_nearest(/EachBlock/) || next_sibling.prev.type === 'EachBlock' + ); +} diff --git a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts index 2a46da2fc6de..2f658ad7bc76 100644 --- a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts +++ b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts @@ -1,6 +1,8 @@ import Renderer, { RenderOptions } from '../Renderer'; import RawMustacheTag from '../../nodes/RawMustacheTag'; -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); + if (options.hydratable) renderer.add_string(''); } diff --git a/src/compiler/utils/link.ts b/src/compiler/utils/link.ts new file mode 100644 index 000000000000..7cdef75d94a0 --- /dev/null +++ b/src/compiler/utils/link.ts @@ -0,0 +1,4 @@ +export function link(next: T, prev: T) { + prev.next = next; + if (next) next.prev = prev; +} diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 9ab9a4395f40..fb715ff2059e 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -184,6 +184,29 @@ export function claim_space(nodes) { return claim_text(nodes, ' '); } +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(html: string, 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(html); + } + 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(html_tag_nodes.slice(1, html_tag_nodes.length - 1)); +} + export function set_data(text, data) { data = '' + data; if (text.data !== data) text.data = data; @@ -288,10 +311,17 @@ export class HtmlTag { t: HTMLElement; a: HTMLElement; - constructor(html: string, anchor: HTMLElement = null) { + constructor(html: string | ChildNode[]) { this.e = element('div'); + if (Array.isArray(html)) { + this.n = html; + } else { + this.u(html); + } + } + + b(anchor: HTMLElement = null) { this.a = anchor; - this.u(html); } m(target: HTMLElement, anchor: HTMLElement = null) { diff --git a/test/hydration/samples/static-content/_after.html b/test/hydration/samples/static-content/_after.html new file mode 100644 index 000000000000..b422f2de2a55 --- /dev/null +++ b/test/hydration/samples/static-content/_after.html @@ -0,0 +1,5 @@ +
+

Hello world

+

Hello world

+

Hello world

+
\ No newline at end of file diff --git a/test/hydration/samples/static-content/_before.html b/test/hydration/samples/static-content/_before.html new file mode 100644 index 000000000000..b422f2de2a55 --- /dev/null +++ b/test/hydration/samples/static-content/_before.html @@ -0,0 +1,5 @@ +
+

Hello world

+

Hello world

+

Hello world

+
\ No newline at end of file diff --git a/test/hydration/samples/static-content/main.svelte b/test/hydration/samples/static-content/main.svelte new file mode 100644 index 000000000000..0429a0a1d2aa --- /dev/null +++ b/test/hydration/samples/static-content/main.svelte @@ -0,0 +1,5 @@ +
+

Hello world

+

Hello world

+

Hello world

+
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 5d88032b8794..2030fe268d22 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -52,8 +52,9 @@ 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(raw_value, null); + html_tag.b(null); attr(div, "class", "comment"); }, m(target, anchor) { diff --git a/test/js/samples/hydrated-void-element/expected.js b/test/js/samples/hydrated-void-element/expected.js index e53d16d9250e..f09dfbf7757e 100644 --- a/test/js/samples/hydrated-void-element/expected.js +++ b/test/js/samples/hydrated-void-element/expected.js @@ -2,7 +2,6 @@ import { SvelteComponent, attr, - children, claim_element, claim_space, detach, @@ -31,7 +30,6 @@ function create_fragment(ctx) { img = claim_element(nodes, "IMG", { src: true, alt: true }); t = claim_space(nodes); div = claim_element(nodes, "DIV", {}); - children(div).forEach(detach); this.h(); }, h() { diff --git a/test/js/samples/hydrating-static-content/_config.js b/test/js/samples/hydrating-static-content/_config.js new file mode 100644 index 000000000000..758c2f7fdbb3 --- /dev/null +++ b/test/js/samples/hydrating-static-content/_config.js @@ -0,0 +1,5 @@ +module.exports = { + options: { + hydratable: true, + }, +}; diff --git a/test/js/samples/hydrating-static-content/expected.js b/test/js/samples/hydrating-static-content/expected.js new file mode 100644 index 000000000000..34d232121125 --- /dev/null +++ b/test/js/samples/hydrating-static-content/expected.js @@ -0,0 +1,48 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + children, + claim_element, + detach, + element, + init, + insert, + noop, + safe_not_equal +} from "svelte/internal"; + +function create_fragment(ctx) { + let div; + + return { + c() { + div = element("div"); + + div.innerHTML = `

Hello world

+

Hello world

+

Hello world

`; + }, + l(nodes) { + div = claim_element(nodes, "DIV", {}); + var div_nodes = children(div); + }, + m(target, anchor) { + insert(target, div, anchor); + }, + p: noop, + i: noop, + o: noop, + d(detaching) { + if (detaching) detach(div); + } + }; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, null, create_fragment, safe_not_equal, {}); + } +} + +export default Component; \ No newline at end of file diff --git a/test/js/samples/hydrating-static-content/input.svelte b/test/js/samples/hydrating-static-content/input.svelte new file mode 100644 index 000000000000..0429a0a1d2aa --- /dev/null +++ b/test/js/samples/hydrating-static-content/input.svelte @@ -0,0 +1,5 @@ +
+

Hello world

+

Hello world

+

Hello world

+
diff --git a/test/runtime/index.js b/test/runtime/index.js index f070eb818551..885423286c95 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -146,13 +146,23 @@ describe("runtime", () => { throw err; } - if (config.before_test) config.before_test(); - // Put things we need on window for testing window.SvelteComponent = SvelteComponent; 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; + } + + if (config.before_test) config.before_test(); + const warnings = []; const warn = console.warn; console.warn = warning => { diff --git a/test/runtime/samples/attribute-boolean-indeterminate/_config.js b/test/runtime/samples/attribute-boolean-indeterminate/_config.js index 0e24f92c5128..e680abdbfb3b 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 92f6ce196d26..b125c1cdb849 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/binding-textarea/_config.js b/test/runtime/samples/binding-textarea/_config.js index ac092096e6cf..28040b9454c1 100644 --- a/test/runtime/samples/binding-textarea/_config.js +++ b/test/runtime/samples/binding-textarea/_config.js @@ -3,35 +3,27 @@ export default { value: 'some text', }, - html: ` - -

some text

- `, - - ssrHtml: ` - -

some text

- `, - - async test({ assert, component, target, window }) { + async test({ assert, component, target, window, compileOptions }) { const textarea = target.querySelector('textarea'); + const p = target.querySelector('p'); assert.equal(textarea.value, 'some text'); + assert.equal(p.textContent, 'some text'); + assert.equal(textarea.attributes.length, 0); const event = new window.Event('input'); textarea.value = 'hello'; await textarea.dispatchEvent(event); - assert.htmlEqual(target.innerHTML, ` - -

hello

- `); + assert.equal(textarea.value, 'hello'); + assert.equal(p.textContent, 'hello'); + assert.equal(textarea.attributes.length, 0); component.value = 'goodbye'; assert.equal(textarea.value, 'goodbye'); - assert.htmlEqual(target.innerHTML, ` - -

goodbye

- `); + + assert.equal(textarea.value, 'goodbye'); + assert.equal(p.textContent, 'goodbye'); + assert.equal(textarea.attributes.length, 0); }, }; diff --git a/test/runtime/samples/component-namespaced/_config.js b/test/runtime/samples/component-namespaced/_config.js index b91795d6c848..d412a7fda811 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 832d2412ee98..000000000000 --- a/test/runtime/samples/component-namespaced/components.js +++ /dev/null @@ -1,3 +0,0 @@ -import Foo from './Foo.svelte'; - -export default { Foo }; \ No newline at end of file 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 5870ff073b45..3e5aa8f37830 100644 --- a/test/runtime/samples/deconflict-builtins-2/_config.js +++ b/test/runtime/samples/deconflict-builtins-2/_config.js @@ -1,4 +1,8 @@ export default { - html: `hello world`, + html: ` + + hello world + + `, preserveIdentifiers: true, }; \ No newline at end of file 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/if-block-conservative-update/_config.js b/test/runtime/samples/if-block-conservative-update/_config.js index a71166ef81db..aef6ac953837 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 5c90d2ad3adc..5c8ed83eedef 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 033b593aeacb..976d6eb55b18 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, [ @@ -44,6 +46,5 @@ export default { ]); } - 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 35bdcefd9639..3e911ac6041b 100644 --- a/test/runtime/samples/noscript-removal/_config.js +++ b/test/runtime/samples/noscript-removal/_config.js @@ -1,9 +1,33 @@ export default { - skip_if_ssr: true, + ssrHtml: ` + - html: ` -
foo
+
foo
-
foo
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); + }); +} diff --git a/test/runtime/samples/ondestroy-deep/_config.js b/test/runtime/samples/ondestroy-deep/_config.js index 4431a2e16317..ff623ff78c24 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']); diff --git a/test/runtime/samples/raw-mustaches/_config.js b/test/runtime/samples/raw-mustaches/_config.js index cc9999aa3448..17d2b4f90f66 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/'