From 52f911cae4fdbd4fb4b2d52ded05c012ff90b89b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 5 Apr 2023 18:25:46 +0200 Subject: [PATCH] wip --- src/compiler/compile/Component.ts | 2 +- src/compiler/compile/css/Stylesheet.ts | 12 +- src/compiler/compile/render_dom/index.ts | 97 +++---- .../compile/render_dom/wrappers/Slot.ts | 4 + src/compiler/compile/render_ssr/index.ts | 2 +- src/compiler/parse/state/tag.ts | 2 +- src/runtime/internal/Component.ts | 252 ++++++++++++++---- .../samples/action/main.svelte | 20 ++ test/custom-elements/samples/action/test.js | 14 + .../samples/escaped-css/test.js | 7 +- .../samples/events/main.svelte | 9 + test/custom-elements/samples/events/test.js | 18 ++ .../samples/extended-builtin/test.js | 7 +- .../samples/html-slots/test.js | 2 +- .../samples/nested.skip/Counter.svelte | 7 - .../samples/nested.skip/main.svelte | 10 - .../samples/nested.skip/test.js | 17 -- .../samples/nested/Counter.svelte | 14 + .../samples/nested/main.svelte | 13 + test/custom-elements/samples/nested/test.js | 20 ++ .../samples/new-styled/test.js | 7 +- test/custom-elements/samples/new/main.svelte | 7 - test/custom-elements/samples/new/test.js | 18 -- .../samples/no-svelte-options/test.js | 3 +- .../samples/no-tag-warning/test.js | 3 +- test/custom-elements/samples/no-tag/test.js | 3 +- .../samples/oncreate/main.svelte | 18 +- test/custom-elements/samples/oncreate/test.js | 5 +- .../custom-elements/samples/ondestroy/test.js | 7 +- test/custom-elements/samples/props/test.js | 6 +- .../samples/reflect-attributes/main.svelte | 20 ++ .../reflect-attributes/my-widget.svelte | 18 ++ .../samples/reflect-attributes/test.js | 19 ++ test/tsconfig.json | 1 + 34 files changed, 447 insertions(+), 217 deletions(-) create mode 100644 test/custom-elements/samples/action/main.svelte create mode 100644 test/custom-elements/samples/action/test.js create mode 100644 test/custom-elements/samples/events/main.svelte create mode 100644 test/custom-elements/samples/events/test.js delete mode 100644 test/custom-elements/samples/nested.skip/Counter.svelte delete mode 100644 test/custom-elements/samples/nested.skip/main.svelte delete mode 100644 test/custom-elements/samples/nested.skip/test.js create mode 100644 test/custom-elements/samples/nested/Counter.svelte create mode 100644 test/custom-elements/samples/nested/main.svelte create mode 100644 test/custom-elements/samples/nested/test.js delete mode 100644 test/custom-elements/samples/new/main.svelte delete mode 100644 test/custom-elements/samples/new/test.js create mode 100644 test/custom-elements/samples/reflect-attributes/main.svelte create mode 100644 test/custom-elements/samples/reflect-attributes/my-widget.svelte create mode 100644 test/custom-elements/samples/reflect-attributes/test.js diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index e87cf6218a..b2c2fff248 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -192,7 +192,7 @@ export default class Component { this.pop_ignores(); this.elements.forEach(element => this.stylesheet.apply(element)); - if (!compile_options.customElement) this.stylesheet.reify(); + this.stylesheet.reify(); this.stylesheet.warn_on_unused_selectors(this); } diff --git a/src/compiler/compile/css/Stylesheet.ts b/src/compiler/compile/css/Stylesheet.ts index 7cb1af3635..9355fb816c 100644 --- a/src/compiler/compile/css/Stylesheet.ts +++ b/src/compiler/compile/css/Stylesheet.ts @@ -407,7 +407,7 @@ export default class Stylesheet { }); } - render(file: string, should_transform_selectors: boolean) { + render(file: string) { if (!this.has_styles) { return { code: null, map: null }; } @@ -421,12 +421,10 @@ export default class Stylesheet { } }); - if (should_transform_selectors) { - const max = Math.max(...this.children.map(rule => rule.get_max_amount_class_specificity_increased())); - this.children.forEach((child: (Atrule | Rule)) => { - child.transform(code, this.id, this.keyframes, max); - }); - } + const max = Math.max(...this.children.map(rule => rule.get_max_amount_class_specificity_increased())); + this.children.forEach((child: (Atrule | Rule)) => { + child.transform(code, this.id, this.keyframes, max); + }); let c = 0; this.children.forEach(child => { diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 58b7a8317b..058c132281 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -6,7 +6,7 @@ import { walk } from 'estree-walker'; import { extract_names, Scope } from 'periscopic'; import { invalidate } from './invalidate'; import Block from './Block'; -import { ImportDeclaration, ClassDeclaration, FunctionExpression, Node, Statement, ObjectExpression, Expression } from 'estree'; +import { ImportDeclaration, ClassDeclaration, Node, Statement, ObjectExpression, Expression } from 'estree'; import { apply_preprocessor_sourcemap } from '../../utils/mapped_code'; import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types'; import { flatten } from '../../utils/flatten'; @@ -25,9 +25,6 @@ export default function dom( block.has_outro_method = true; - // prevent fragment being created twice (#1063) - if (options.customElement) block.chunks.create.push(b`this.c = @noop;`); - const body = []; if (renderer.file_var) { @@ -35,7 +32,7 @@ export default function dom( body.push(b`const ${renderer.file_var} = ${file};`); } - const css = component.stylesheet.render(options.filename, !options.customElement); + const css = component.stylesheet.render(options.filename); const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css'); @@ -52,7 +49,6 @@ export default function dom( const add_css = component.get_unique_name('add_css'); const should_add_css = ( - !options.customElement && !!styles && options.css === 'injected' ); @@ -519,8 +515,35 @@ export default function dom( } } - if (options.customElement) { + const superclass = { + type: 'Identifier', + name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent' + }; + + const optional_parameters = []; + if (should_add_css) { + optional_parameters.push(add_css); + } else if (dirty) { + optional_parameters.push(x`null`); + } + if (dirty) { + optional_parameters.push(dirty); + } + + const declaration = b` + class ${name} extends ${superclass} { + constructor(options) { + super(${options.dev && 'options'}); + @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters}); + ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} + } + } + `[0] as ClassDeclaration; + + push_array(declaration.body.body, accessors); + body.push(declaration); + if (options.customElement && component.tag != null) { let init_props = x`@attribute_to_object(this.attributes)`; if (uses_slots) { init_props = x`{ ...${init_props}, $$slots: @get_custom_elements_slots(this) }`; @@ -553,57 +576,15 @@ export default function dom( } `[0] as ClassDeclaration; - if (props.length > 0) { - declaration.body.body.push({ - type: 'MethodDefinition', - kind: 'get', - static: true, - computed: false, - key: { type: 'Identifier', name: 'observedAttributes' }, - value: x`function() { - return [${props.map(prop => x`"${prop.export_name}"`)}]; - }` as FunctionExpression - }); - } - - push_array(declaration.body.body, accessors); - - body.push(declaration); - - if (component.tag != null) { - body.push(b` - @_customElements.define("${component.tag}", ${name}); - `); - } - } else { - const superclass = { - type: 'Identifier', - name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent' - }; - - const optional_parameters = []; - if (should_add_css) { - optional_parameters.push(add_css); - } else if (dirty) { - optional_parameters.push(x`null`); - } - if (dirty) { - optional_parameters.push(dirty); - } - - const declaration = b` - class ${name} extends ${superclass} { - constructor(options) { - super(${options.dev && 'options'}); - @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters}); - ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} - } - } - `[0] as ClassDeclaration; - - push_array(declaration.body.body, accessors); - - body.push(declaration); + const props_str = writable_props.map(prop => `"${prop.export_name}"`).join(','); + const slots_str = [...component.slots.keys()].map(key => `"${key}"`).join(','); + const accessors_str = accessors + .filter(accessor => !writable_props.some(prop => prop.export_name === accessor.key.name)) + .map(accessor => `"${accessor.key.name}"`) + .join(','); + body.push( + b`@_customElements.define("${component.tag}", @create_custom_element(${name}, [${props_str}], [${slots_str}], [${accessors_str}]));` + ); } return { js: flatten(body), css }; diff --git a/src/compiler/compile/render_dom/wrappers/Slot.ts b/src/compiler/compile/render_dom/wrappers/Slot.ts index 0a589e3394..380d7527b3 100644 --- a/src/compiler/compile/render_dom/wrappers/Slot.ts +++ b/src/compiler/compile/render_dom/wrappers/Slot.ts @@ -132,6 +132,10 @@ export default class SlotWrapper extends Wrapper { const ${slot_definition} = ${renderer.reference('#slots')}.${slot_name}; const ${slot} = @create_slot(${slot_definition}, #ctx, ${renderer.reference('$$scope')}, ${get_slot_context_fn}); ${has_fallback ? b`const ${slot_or_fallback} = ${slot} || ${this.fallback.name}(#ctx);` : null} + ${has_fallback && this.renderer.options.customElement && this.renderer.options.tag + // This ensures that fallback content is rendered into the element given by the custom element wrapper + ? b`if (${slot_or_fallback}.$$c_e) ${this.fallback.name}(#ctx);` + : null} `); block.chunks.create.push( diff --git a/src/compiler/compile/render_ssr/index.ts b/src/compiler/compile/render_ssr/index.ts index e256ba78fb..d1fc816cde 100644 --- a/src/compiler/compile/render_ssr/index.ts +++ b/src/compiler/compile/render_ssr/index.ts @@ -33,7 +33,7 @@ export default function ssr( // TODO concatenate CSS maps const css = options.customElement ? { code: null, map: null } : - component.stylesheet.render(options.filename, true); + component.stylesheet.render(options.filename); const uses_rest = component.var_lookup.has('$$restProps'); const props = component.vars.filter(variable => !variable.module && variable.export_name); diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 90adfb8e79..f7d02a0950 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -115,7 +115,7 @@ export default function tag(parser: Parser) { : (regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent' : name === 'svelte:fragment' ? 'SlotTemplate' : name === 'title' && parent_is_head(parser.stack) ? 'Title' - : name === 'slot' && !parser.customElement ? 'Slot' : 'Element'; + : name === 'slot' ? 'Slot' : 'Element'; const element: TemplateNode = { start, diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index a8a500b25b..2a1655a896 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,9 +1,10 @@ -import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler'; +import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components, tick } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; -import { children, detach, start_hydrating, end_hydrating } from './dom'; +import { children, detach, start_hydrating, end_hydrating, set_custom_element_data, get_custom_elements_slots, insert } from './dom'; import { transition_in } from './transitions'; import { T$$ } from './types'; +import { ComponentType } from './dev'; export function bind(component, name, callback) { const index = component.$$.props[name]; @@ -21,29 +22,27 @@ export function claim_component(block, parent_nodes) { block && block.l(parent_nodes); } -export function mount_component(component, target, anchor, customElement) { +export function mount_component(component, target, anchor) { const { fragment, after_update } = component.$$; fragment && fragment.m(target, anchor); - if (!customElement) { - // onMount happens before the initial afterUpdate - add_render_callback(() => { - - const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); - // if the component was destroyed immediately - // it will update the `$$.on_destroy` reference to `null`. - // the destructured on_destroy may still reference to the old array - if (component.$$.on_destroy) { - component.$$.on_destroy.push(...new_on_destroy); - } else { - // Edge case - component was destroyed immediately, - // most likely as a result of a binding initialising - run_all(new_on_destroy); - } - component.$$.on_mount = []; - }); - } + // onMount happens before the initial afterUpdate + add_render_callback(() => { + + const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); + // if the component was destroyed immediately + // it will update the `$$.on_destroy` reference to `null`. + // the destructured on_destroy may still reference to the old array + if (component.$$.on_destroy) { + component.$$.on_destroy.push(...new_on_destroy); + } else { + // Edge case - component was destroyed immediately, + // most likely as a result of a binding initialising + run_all(new_on_destroy); + } + component.$$.on_mount = []; + }); after_update.forEach(add_render_callback); } @@ -137,7 +136,7 @@ export function init(component, options, instance, create_fragment, not_equal, p } if (options.intro) transition_in(component.$$.fragment); - mount_component(component, options.target, options.anchor, options.customElement); + mount_component(component, options.target, options.anchor); end_hydrating(); flush(); } @@ -148,59 +147,202 @@ export function init(component, options, instance, create_fragment, not_equal, p export let SvelteElement; if (typeof HTMLElement === 'function') { SvelteElement = class extends HTMLElement { - $$: T$$; - $$set?: ($$props: any) => void; - constructor() { + private $$component?: SvelteComponent; + private $$connected = false; + private $$data = {}; + private $$reflecting = false; + + constructor( + private $$componentCtor: ComponentType, + private $$slots: string[], + ) { super(); this.attachShadow({ mode: 'open' }); } - connectedCallback() { - const { on_mount } = this.$$; - this.$$.on_disconnect = on_mount.map(run).filter(is_function); + addEventListener(type: string, listener: any, options?: any): void { + // We can't determine upfront if the event is a custom event or not, so we have to + // listen to both. If someone uses a custom event with the same name as a regular + // browser event, this fires twice - we can't avoid that. + this.$$component!.$on(type, listener); + super.addEventListener(type, listener, options); + } - // @ts-ignore todo: improve typings - for (const key in this.$$.slotted) { - // @ts-ignore todo: improve typings - this.appendChild(this.$$.slotted[key]); + connectedCallback() { + this.$$connected = true; + if (!this.$$component) { + for (const attribute of this.attributes) { + // this.$$data takes precedence over this.attributes + if (!(attribute.name in this.$$data)) { + this.$$data[attribute.name] = attribute.value; + } + } + + function create_slot(name: string) { + return () => { + let node: HTMLSlotElement; + return { + c: function create() { + node = document.createElement('slot'); + if (name !== 'default') { + node.setAttribute('name', name); + } + }, + m: function mount(target: HTMLElement, anchor?: HTMLElement) { + insert(target, node, anchor); + }, + d: function destroy(detaching: boolean) { + if (detaching) { + detach(node) + } + }, + $$c_e: true + }; + }; + } + + let $$slots: Record = {}; + const existing_slots = get_custom_elements_slots(this); + for (const name of this.$$slots) { + if (name in existing_slots) { + $$slots[name] = [create_slot(name)]; + } + } + + // Dilemma: We need to set the component props eagerly or they have the wrong value for actions/onMount etc. + // Boolean attributes are represented by the empty string, and we don't know if they represent boolean or string props. + this.$$component = new this.$$componentCtor({ + target: this.shadowRoot!, + props: { + $$slots, + $$scope: { + ctx: [] + } + } + }); + // ensures that works correctly + Object.keys(this.$$data).forEach(key => { + set_custom_element_data(this, key, this.$$data[key]); + this.$$data[key] = this[key]; // "" -> true for boolean attributes + }); } } - attributeChangedCallback(attr, _oldValue, newValue) { - this[attr] = newValue; + // TODO we don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte + // and setting attributes through setAttribute etc, this is probably helpful + attributeChangedCallback(attr: string, _oldValue: any, newValue: any) { + if (this.$$reflecting) return; + + set_custom_element_data(this.$$data, attr, newValue); + this.$$component![attr] = this.$$data; } disconnectedCallback() { - run_all(this.$$.on_disconnect); + this.$$connected = false; + // In a microtask, because this could be a move within the DOM + tick().then(() => { + if (!this.$$connected) { + this.$$component!.$destroy(); + this.$$component = undefined; + } + }); } + }; +} - $destroy() { - destroy_component(this, 1); - this.$destroy = noop; - } +/** + * Attribute value types that should be reflected to the DOM. Helpful + * for people relying on the custom element's attributes to be present, + * for example when using a CSS selector which relies on an attribute. + */ +const should_reflect = ['string', 'number', 'boolean']; - $on(type, callback) { - // TODO should this delegate to addEventListener? - if (!is_function(callback)) { - return noop; +function camelToHyphen(str: string) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} + +/** + * Turn a Svelte component into a custom element. + * @param Component A Svelte component constructor + * @param props The props to observe + * @param slots The slots to create + * @param accessors Other accessors besides the ones for props the component has + * @param styles Additional styles to apply to the shadow root (not needed for Svelte components compiled with `customElement: true`) + * @returns A custom element class + */ +export function create_custom_element( + Component: ComponentType, + props: string[], + slots: string[], + accessors: string[], + styles?: string, +) { + const Class = class extends SvelteElement { + constructor() { + super(Component, slots); + if (styles) { + const style = document.createElement('style'); + style.textContent = styles; + this.shadowRoot!.appendChild(style); } - const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - callbacks.push(callback); + } - return () => { - const index = callbacks.indexOf(callback); - if (index !== -1) callbacks.splice(index, 1); - }; + static get observedAttributes() { + return props; } + }; - $set($$props) { - if (this.$$set && !is_empty($$props)) { - this.$$.skip_bound = true; - this.$$set($$props); - this.$$.skip_bound = false; + function createProperty(name: string, prop: string) { + Object.defineProperty(Class.prototype, name, { + get() { + return this.$$component && prop in this.$$component + ? this.$$component[prop] + : this.$$data[prop]; + }, + + set(value) { + this.$$data[prop] = value; + + if (this.$$component) { + if(should_reflect.indexOf(typeof value) !== -1) { + this.$$reflecting = true; + if (value === false || value == null) { + this.removeAttribute(prop); + } else { + this.setAttribute(prop, value); + } + this.$$reflecting = false; + } + + this.$$component[prop] = value; + } } + }) + } + + props.forEach((prop) => { + createProperty(prop, prop); + // will be ce.camcelcase = "foo" + const lower = prop.toLowerCase(); + if (lower !== prop) { + createProperty(lower, prop); } - }; + // also support hyphenated version where will be ce['camel-case'] = "foo" + const hyphen = camelToHyphen(prop); + if (hyphen !== lower) { + createProperty(hyphen, prop) + } + }); + + accessors.forEach(accessor => { + Object.defineProperty(Class.prototype, accessor, { + get() { + return this.$$component?.[accessor]; + }, + }) + }); + + return Class; } /** diff --git a/test/custom-elements/samples/action/main.svelte b/test/custom-elements/samples/action/main.svelte new file mode 100644 index 0000000000..1f8f5fe7e6 --- /dev/null +++ b/test/custom-elements/samples/action/main.svelte @@ -0,0 +1,20 @@ + + + + +
action
diff --git a/test/custom-elements/samples/action/test.js b/test/custom-elements/samples/action/test.js new file mode 100644 index 0000000000..6bf1d3850a --- /dev/null +++ b/test/custom-elements/samples/action/test.js @@ -0,0 +1,14 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ''; + const el = target.querySelector('custom-element'); + assert.deepEqual(el.events, ['foo']); + + el.name = 'bar'; + assert.deepEqual(el.events, ['foo', 'bar']); + + target.innerHTML = ''; + assert.deepEqual(el.events, ['foo', 'bar', 'destroy']); +} diff --git a/test/custom-elements/samples/escaped-css/test.js b/test/custom-elements/samples/escaped-css/test.js index 6277ccba32..e7df08da1c 100644 --- a/test/custom-elements/samples/escaped-css/test.js +++ b/test/custom-elements/samples/escaped-css/test.js @@ -1,11 +1,8 @@ import * as assert from 'assert'; -import CustomElement from './main.svelte'; +import './main.svelte'; export default function (target) { - new CustomElement({ - target - }); - + target.innerHTML = ''; const icon = target.querySelector('custom-element').shadowRoot.querySelector('.icon'); const before = getComputedStyle(icon, '::before'); diff --git a/test/custom-elements/samples/events/main.svelte b/test/custom-elements/samples/events/main.svelte new file mode 100644 index 0000000000..1c8355b01e --- /dev/null +++ b/test/custom-elements/samples/events/main.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/test/custom-elements/samples/events/test.js b/test/custom-elements/samples/events/test.js new file mode 100644 index 0000000000..2c8b7832d0 --- /dev/null +++ b/test/custom-elements/samples/events/test.js @@ -0,0 +1,18 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ''; + const el = target.querySelector('custom-element'); + + const events = []; + el.addEventListener('custom', e => { + events.push(e.detail); + }); + el.addEventListener('click', () => { + events.push('click'); + }); + + el.shadowRoot.querySelector('button').click(); + assert.deepEqual(events, ['foo', 'click']); +} diff --git a/test/custom-elements/samples/extended-builtin/test.js b/test/custom-elements/samples/extended-builtin/test.js index a2f253e5d4..1eac8d852e 100644 --- a/test/custom-elements/samples/extended-builtin/test.js +++ b/test/custom-elements/samples/extended-builtin/test.js @@ -1,11 +1,8 @@ import * as assert from 'assert'; -import CustomElement from './main.svelte'; +import './main.svelte'; export default function (target) { - new CustomElement({ - target - }); - + target.innerHTML = ''; assert.equal(target.innerHTML, ''); const el = target.querySelector('custom-element'); diff --git a/test/custom-elements/samples/html-slots/test.js b/test/custom-elements/samples/html-slots/test.js index 06d18d9944..c82a2d24ad 100644 --- a/test/custom-elements/samples/html-slots/test.js +++ b/test/custom-elements/samples/html-slots/test.js @@ -13,5 +13,5 @@ export default function (target) { const [slot0, slot1] = div.children; assert.equal(slot0.assignedNodes()[1], target.querySelector('strong')); - assert.equal(slot1.assignedNodes().length, 0); + assert.equal(slot1.innerHTML, 'foo fallback content'); } diff --git a/test/custom-elements/samples/nested.skip/Counter.svelte b/test/custom-elements/samples/nested.skip/Counter.svelte deleted file mode 100644 index 87cde48466..0000000000 --- a/test/custom-elements/samples/nested.skip/Counter.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/test/custom-elements/samples/nested.skip/main.svelte b/test/custom-elements/samples/nested.skip/main.svelte deleted file mode 100644 index cb26008061..0000000000 --- a/test/custom-elements/samples/nested.skip/main.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - -

clicked {count} times

diff --git a/test/custom-elements/samples/nested.skip/test.js b/test/custom-elements/samples/nested.skip/test.js deleted file mode 100644 index 09edc38f54..0000000000 --- a/test/custom-elements/samples/nested.skip/test.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as assert from 'assert'; -import './main.svelte'; - -export default async function (target) { - target.innerHTML = ''; - const el = target.querySelector('my-app'); - const counter = el.shadowRoot.querySelector('my-counter'); - const button = counter.shadowRoot.querySelector('button'); - - assert.equal(counter.count, 0); - assert.equal(counter.shadowRoot.innerHTML, ''); - - await button.dispatchEvent(new MouseEvent('click')); - - assert.equal(counter.count, 1); - assert.equal(counter.shadowRoot.innerHTML, ''); -} diff --git a/test/custom-elements/samples/nested/Counter.svelte b/test/custom-elements/samples/nested/Counter.svelte new file mode 100644 index 0000000000..45003ac724 --- /dev/null +++ b/test/custom-elements/samples/nested/Counter.svelte @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/test/custom-elements/samples/nested/main.svelte b/test/custom-elements/samples/nested/main.svelte new file mode 100644 index 0000000000..3170fe22fa --- /dev/null +++ b/test/custom-elements/samples/nested/main.svelte @@ -0,0 +1,13 @@ + + + + + + slot {count} + +

clicked {count} times

diff --git a/test/custom-elements/samples/nested/test.js b/test/custom-elements/samples/nested/test.js new file mode 100644 index 0000000000..29029f3d2c --- /dev/null +++ b/test/custom-elements/samples/nested/test.js @@ -0,0 +1,20 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default async function (target) { + target.innerHTML = ''; + const el = target.querySelector('my-app'); + const button = el.shadowRoot.querySelector('button'); + const span = el.shadowRoot.querySelector('span'); + + assert.equal(el.counter.count, 0); + assert.equal(button.innerHTML, 'count: 0'); + assert.equal(span.innerHTML, 'slot 0'); + assert.equal(getComputedStyle(button).color, 'rgb(255, 0, 0)'); + + await button.dispatchEvent(new MouseEvent('click')); + + assert.equal(el.counter.count, 1); + assert.equal(button.innerHTML, 'count: 1'); + assert.equal(span.innerHTML, 'slot 1'); +} diff --git a/test/custom-elements/samples/new-styled/test.js b/test/custom-elements/samples/new-styled/test.js index 72c2cecd10..3433451900 100644 --- a/test/custom-elements/samples/new-styled/test.js +++ b/test/custom-elements/samples/new-styled/test.js @@ -1,12 +1,9 @@ import * as assert from 'assert'; -import CustomElement from './main.svelte'; +import './main.svelte'; export default function (target) { target.innerHTML = '

unstyled

'; - - new CustomElement({ - target - }); + target.appendChild(document.createElement('custom-element')); const unstyled = target.querySelector('p'); const styled = target.querySelector('custom-element').shadowRoot.querySelector('p'); diff --git a/test/custom-elements/samples/new/main.svelte b/test/custom-elements/samples/new/main.svelte deleted file mode 100644 index 0931535a18..0000000000 --- a/test/custom-elements/samples/new/main.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - -

Hello {name}!

diff --git a/test/custom-elements/samples/new/test.js b/test/custom-elements/samples/new/test.js deleted file mode 100644 index 88ba69ab69..0000000000 --- a/test/custom-elements/samples/new/test.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as assert from 'assert'; -import CustomElement from './main.svelte'; - -export default function (target) { - new CustomElement({ - target, - props: { - name: 'world' - } - }); - - assert.equal(target.innerHTML, ''); - - const el = target.querySelector('custom-element'); - const h1 = el.shadowRoot.querySelector('h1'); - - assert.equal(h1.textContent, 'Hello world!'); -} diff --git a/test/custom-elements/samples/no-svelte-options/test.js b/test/custom-elements/samples/no-svelte-options/test.js index e6ce82d1a4..a3c2a48574 100644 --- a/test/custom-elements/samples/no-svelte-options/test.js +++ b/test/custom-elements/samples/no-svelte-options/test.js @@ -1,8 +1,9 @@ import * as assert from 'assert'; import CustomElement from './main.svelte'; +import { create_custom_element } from 'svelte/internal'; export default function (target) { - customElements.define('no-tag', CustomElement); + customElements.define('no-tag', create_custom_element(CustomElement, ['name'], [], [])); target.innerHTML = ''; const el = target.querySelector('no-tag'); diff --git a/test/custom-elements/samples/no-tag-warning/test.js b/test/custom-elements/samples/no-tag-warning/test.js index e6ce82d1a4..a3c2a48574 100644 --- a/test/custom-elements/samples/no-tag-warning/test.js +++ b/test/custom-elements/samples/no-tag-warning/test.js @@ -1,8 +1,9 @@ import * as assert from 'assert'; import CustomElement from './main.svelte'; +import { create_custom_element } from 'svelte/internal'; export default function (target) { - customElements.define('no-tag', CustomElement); + customElements.define('no-tag', create_custom_element(CustomElement, ['name'], [], [])); target.innerHTML = ''; const el = target.querySelector('no-tag'); diff --git a/test/custom-elements/samples/no-tag/test.js b/test/custom-elements/samples/no-tag/test.js index e6ce82d1a4..a3c2a48574 100644 --- a/test/custom-elements/samples/no-tag/test.js +++ b/test/custom-elements/samples/no-tag/test.js @@ -1,8 +1,9 @@ import * as assert from 'assert'; import CustomElement from './main.svelte'; +import { create_custom_element } from 'svelte/internal'; export default function (target) { - customElements.define('no-tag', CustomElement); + customElements.define('no-tag', create_custom_element(CustomElement, ['name'], [], [])); target.innerHTML = ''; const el = target.querySelector('no-tag'); diff --git a/test/custom-elements/samples/oncreate/main.svelte b/test/custom-elements/samples/oncreate/main.svelte index 23819e660f..e22d101eca 100644 --- a/test/custom-elements/samples/oncreate/main.svelte +++ b/test/custom-elements/samples/oncreate/main.svelte @@ -1,14 +1,14 @@ - + diff --git a/test/custom-elements/samples/oncreate/test.js b/test/custom-elements/samples/oncreate/test.js index f451979976..cd27d9de86 100644 --- a/test/custom-elements/samples/oncreate/test.js +++ b/test/custom-elements/samples/oncreate/test.js @@ -1,10 +1,13 @@ import * as assert from 'assert'; +import { tick } from 'svelte'; import './main.svelte'; -export default function (target) { +export default async function (target) { target.innerHTML = ''; const el = target.querySelector('my-app'); + await tick(); + assert.ok(el.wasCreated); assert.ok(el.propsInitialized); } diff --git a/test/custom-elements/samples/ondestroy/test.js b/test/custom-elements/samples/ondestroy/test.js index 61375bfa96..3092613fc3 100644 --- a/test/custom-elements/samples/ondestroy/test.js +++ b/test/custom-elements/samples/ondestroy/test.js @@ -1,11 +1,14 @@ import * as assert from 'assert'; +import { tick } from 'svelte'; import './main.svelte'; -export default function (target) { +export default async function (target) { target.innerHTML = ''; const el = target.querySelector('my-app'); target.removeChild(el); + await tick(); + assert.ok(target.dataset.onMountDestroyed); - assert.equal(target.dataset.destroyed, undefined); + assert.ok(target.dataset.destroyed); } diff --git a/test/custom-elements/samples/props/test.js b/test/custom-elements/samples/props/test.js index 41ca77d29d..3d93c60b83 100644 --- a/test/custom-elements/samples/props/test.js +++ b/test/custom-elements/samples/props/test.js @@ -1,10 +1,8 @@ import * as assert from 'assert'; -import CustomElement from './main.svelte'; +import './main.svelte'; export default function (target) { - new CustomElement({ - target - }); + target.innerHTML = ''; assert.equal(target.innerHTML, ''); diff --git a/test/custom-elements/samples/reflect-attributes/main.svelte b/test/custom-elements/samples/reflect-attributes/main.svelte new file mode 100644 index 0000000000..4aadb8bc06 --- /dev/null +++ b/test/custom-elements/samples/reflect-attributes/main.svelte @@ -0,0 +1,20 @@ + + + + +
hi
+

hi

+ + + diff --git a/test/custom-elements/samples/reflect-attributes/my-widget.svelte b/test/custom-elements/samples/reflect-attributes/my-widget.svelte new file mode 100644 index 0000000000..ef6d071d2c --- /dev/null +++ b/test/custom-elements/samples/reflect-attributes/my-widget.svelte @@ -0,0 +1,18 @@ + + + + +
hi
+

hi

+ + diff --git a/test/custom-elements/samples/reflect-attributes/test.js b/test/custom-elements/samples/reflect-attributes/test.js new file mode 100644 index 0000000000..4b392868c6 --- /dev/null +++ b/test/custom-elements/samples/reflect-attributes/test.js @@ -0,0 +1,19 @@ +import * as assert from 'assert'; +import './main.svelte'; + +export default function (target) { + target.innerHTML = ''; + const ceRoot = target.querySelector('custom-element').shadowRoot; + const div = ceRoot.querySelector('div'); + const p = ceRoot.querySelector('p'); + + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + assert.equal(getComputedStyle(p).color, 'rgb(255, 255, 255)'); + + const innerRoot = ceRoot.querySelector('my-widget').shadowRoot; + const innerDiv = innerRoot.querySelector('div'); + const innerP = innerRoot.querySelector('p'); + + assert.equal(getComputedStyle(innerDiv).color, 'rgb(255, 0, 0)'); + assert.equal(getComputedStyle(innerP).color, 'rgb(255, 255, 255)'); +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 82eaf0245e..83eecc51dc 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.json", "include": ["."], + "exclude": ["./**/_output/**/*"], "compilerOptions": { "allowJs": true,