|
|
@@ -1,13 +1,22 @@
|
|
|
-import { type Ref, nextTick, ref } from '@vue/runtime-dom'
|
|
|
import {
|
|
|
+ type Ref,
|
|
|
+ nextTick,
|
|
|
+ onUpdated,
|
|
|
+ ref,
|
|
|
+ withModifiers,
|
|
|
+} from '@vue/runtime-dom'
|
|
|
+import {
|
|
|
+ VaporTeleport,
|
|
|
createComponent,
|
|
|
createDynamicComponent,
|
|
|
createIf,
|
|
|
createSlot,
|
|
|
defineVaporComponent,
|
|
|
+ delegateEvents,
|
|
|
renderEffect,
|
|
|
setClass,
|
|
|
setDynamicProps,
|
|
|
+ setInsertionState,
|
|
|
setProp,
|
|
|
setStyle,
|
|
|
template,
|
|
|
@@ -18,8 +27,7 @@ import { stringifyStyle } from '@vue/shared'
|
|
|
import { setElementText } from '../src/dom/prop'
|
|
|
|
|
|
const define = makeRender<any>()
|
|
|
-
|
|
|
-// TODO: port more tests from rendererAttrsFallthrough.spec.ts
|
|
|
+delegateEvents('click')
|
|
|
|
|
|
describe('attribute fallthrough', () => {
|
|
|
it('should allow attrs to fallthrough', async () => {
|
|
|
@@ -59,6 +67,662 @@ describe('attribute fallthrough', () => {
|
|
|
expect(host.innerHTML).toBe('<div id="b">2</div>')
|
|
|
})
|
|
|
|
|
|
+ it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
|
|
|
+ const click = vi.fn()
|
|
|
+ const childUpdated = vi.fn()
|
|
|
+
|
|
|
+ const count = ref(0)
|
|
|
+
|
|
|
+ function inc() {
|
|
|
+ count.value++
|
|
|
+ click()
|
|
|
+ }
|
|
|
+
|
|
|
+ const Hello = () =>
|
|
|
+ createComponent(Child, {
|
|
|
+ foo: () => count.value + 1,
|
|
|
+ id: () => 'test',
|
|
|
+ class: () => 'c' + count.value,
|
|
|
+ style: () => ({
|
|
|
+ color: count.value ? 'red' : 'green',
|
|
|
+ }),
|
|
|
+ onClick: () => inc,
|
|
|
+ })
|
|
|
+
|
|
|
+ const { component: Child } = define((props: any) => {
|
|
|
+ childUpdated()
|
|
|
+ const n0 = template(
|
|
|
+ '<div class="c2" style="font-weight: bold"></div>',
|
|
|
+ true,
|
|
|
+ )() as Element
|
|
|
+ renderEffect(() => setElementText(n0, props.foo))
|
|
|
+ return n0
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(Hello).render()
|
|
|
+ expect(host.innerHTML).toBe(
|
|
|
+ '<div class="c2 c0" style="font-weight: bold; color: green;">1</div>',
|
|
|
+ )
|
|
|
+
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+
|
|
|
+ // not whitelisted
|
|
|
+ expect(node.getAttribute('id')).toBe(null)
|
|
|
+ expect(node.getAttribute('foo')).toBe(null)
|
|
|
+
|
|
|
+ // whitelisted: style, class, event listeners
|
|
|
+ expect(node.getAttribute('class')).toBe('c2 c0')
|
|
|
+ expect(node.style.color).toBe('green')
|
|
|
+ expect(node.style.fontWeight).toBe('bold')
|
|
|
+ node.dispatchEvent(new CustomEvent('click'))
|
|
|
+ expect(click).toHaveBeenCalled()
|
|
|
+
|
|
|
+ await nextTick()
|
|
|
+ expect(childUpdated).toHaveBeenCalled()
|
|
|
+ expect(node.getAttribute('id')).toBe(null)
|
|
|
+ expect(node.getAttribute('foo')).toBe(null)
|
|
|
+ expect(node.getAttribute('class')).toBe('c2 c1')
|
|
|
+ expect(node.style.color).toBe('red')
|
|
|
+ expect(node.style.fontWeight).toBe('bold')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should allow all attrs on functional component with declared props', async () => {
|
|
|
+ const click = vi.fn()
|
|
|
+ const childUpdated = vi.fn()
|
|
|
+ const count = ref(0)
|
|
|
+
|
|
|
+ function inc() {
|
|
|
+ count.value++
|
|
|
+ click()
|
|
|
+ }
|
|
|
+
|
|
|
+ const Hello = () =>
|
|
|
+ createComponent(Child, {
|
|
|
+ foo: () => count.value + 1,
|
|
|
+ id: () => 'test',
|
|
|
+ class: () => 'c' + count.value,
|
|
|
+ style: () => ({ color: count.value ? 'red' : 'green' }),
|
|
|
+ onClick: () => inc,
|
|
|
+ })
|
|
|
+
|
|
|
+ const Child = defineVaporComponent((props: any) => {
|
|
|
+ childUpdated()
|
|
|
+ const n0 = template(
|
|
|
+ '<div class="c2" style="font-weight: bold"></div>',
|
|
|
+ true,
|
|
|
+ )() as Element
|
|
|
+ renderEffect(() => setElementText(n0, props.foo))
|
|
|
+ return n0
|
|
|
+ })
|
|
|
+
|
|
|
+ Child.props = ['foo']
|
|
|
+
|
|
|
+ const { host } = define(Hello).render()
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+
|
|
|
+ expect(node.getAttribute('id')).toBe('test')
|
|
|
+ expect(node.getAttribute('foo')).toBe(null) // declared as prop
|
|
|
+ expect(node.getAttribute('class')).toBe('c2 c0')
|
|
|
+ expect(node.style.color).toBe('green')
|
|
|
+ expect(node.style.fontWeight).toBe('bold')
|
|
|
+ node.dispatchEvent(new CustomEvent('click'))
|
|
|
+ expect(click).toHaveBeenCalled()
|
|
|
+
|
|
|
+ await nextTick()
|
|
|
+ expect(childUpdated).toHaveBeenCalled()
|
|
|
+ expect(node.getAttribute('id')).toBe('test')
|
|
|
+ expect(node.getAttribute('foo')).toBe(null)
|
|
|
+ expect(node.getAttribute('class')).toBe('c2 c1')
|
|
|
+ expect(node.style.color).toBe('red')
|
|
|
+ expect(node.style.fontWeight).toBe('bold')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should fallthrough for nested components', async () => {
|
|
|
+ const click = vi.fn()
|
|
|
+ const childUpdated = vi.fn()
|
|
|
+ const grandChildUpdated = vi.fn()
|
|
|
+
|
|
|
+ const Hello = {
|
|
|
+ setup() {
|
|
|
+ const count = ref(0)
|
|
|
+
|
|
|
+ function inc() {
|
|
|
+ count.value++
|
|
|
+ click()
|
|
|
+ }
|
|
|
+
|
|
|
+ return createComponent(Child, {
|
|
|
+ foo: () => count.value + 1,
|
|
|
+ id: () => 'test',
|
|
|
+ class: () => 'c' + count.value,
|
|
|
+ style: () => ({
|
|
|
+ color: count.value ? 'red' : 'green',
|
|
|
+ }),
|
|
|
+ onClick: () => inc,
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ setup(props: any) {
|
|
|
+ onUpdated(childUpdated)
|
|
|
+ // HOC simply passing props down.
|
|
|
+ // this will result in merging the same attrs, but should be deduped by
|
|
|
+ // `mergeProps`.
|
|
|
+ return createComponent(GrandChild, props, null, true)
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const GrandChild = defineVaporComponent({
|
|
|
+ props: {
|
|
|
+ id: String,
|
|
|
+ foo: Number,
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ onUpdated(grandChildUpdated)
|
|
|
+ const n0 = template(
|
|
|
+ '<div class="c2" style="font-weight: bold"></div>',
|
|
|
+ true,
|
|
|
+ )() as Element
|
|
|
+ renderEffect(() => {
|
|
|
+ setProp(n0, 'id', props.id)
|
|
|
+ setElementText(n0, props.foo)
|
|
|
+ })
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(Hello).render()
|
|
|
+ expect(host.innerHTML).toBe(
|
|
|
+ '<div class="c2 c0" style="font-weight: bold; color: green;" id="test">1</div>',
|
|
|
+ )
|
|
|
+
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+
|
|
|
+ // with declared props, any parent attr that isn't a prop falls through
|
|
|
+ expect(node.getAttribute('id')).toBe('test')
|
|
|
+ expect(node.getAttribute('class')).toBe('c2 c0')
|
|
|
+ expect(node.style.color).toBe('green')
|
|
|
+ expect(node.style.fontWeight).toBe('bold')
|
|
|
+ node.dispatchEvent(new CustomEvent('click'))
|
|
|
+ expect(click).toHaveBeenCalled()
|
|
|
+
|
|
|
+ // ...while declared ones remain props
|
|
|
+ expect(node.hasAttribute('foo')).toBe(false)
|
|
|
+
|
|
|
+ await nextTick()
|
|
|
+ // child should not update, due to it not accessing props
|
|
|
+ // this is a optimization in vapor mode
|
|
|
+ expect(childUpdated).not.toHaveBeenCalled()
|
|
|
+ expect(grandChildUpdated).toHaveBeenCalled()
|
|
|
+ expect(node.getAttribute('id')).toBe('test')
|
|
|
+ expect(node.getAttribute('class')).toBe('c2 c1')
|
|
|
+ expect(node.style.color).toBe('red')
|
|
|
+ expect(node.style.fontWeight).toBe('bold')
|
|
|
+
|
|
|
+ expect(node.hasAttribute('foo')).toBe(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not fallthrough with inheritAttrs: false', () => {
|
|
|
+ const Parent = defineVaporComponent({
|
|
|
+ setup() {
|
|
|
+ return createComponent(Child, { foo: () => 1, class: () => 'parent' })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ props: ['foo'],
|
|
|
+ inheritAttrs: false,
|
|
|
+ setup(props) {
|
|
|
+ const n0 = template('<div></div>', true)() as Element
|
|
|
+ renderEffect(() => setElementText(n0, props.foo))
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { html } = define(Parent).render()
|
|
|
+
|
|
|
+ // should not contain class
|
|
|
+ expect(html()).toMatch(`<div>1</div>`)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('explicit spreading with inheritAttrs: false', () => {
|
|
|
+ const Parent = defineVaporComponent({
|
|
|
+ setup() {
|
|
|
+ return createComponent(Child, { foo: () => 1, class: () => 'parent' })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ props: ['foo'],
|
|
|
+ inheritAttrs: false,
|
|
|
+ setup(props, { attrs }) {
|
|
|
+ const n0 = template('<div>', true)() as Element
|
|
|
+ renderEffect(() => {
|
|
|
+ setElementText(n0, props.foo)
|
|
|
+ setDynamicProps(n0, [{ class: 'child' }, attrs])
|
|
|
+ })
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { html } = define(Parent).render()
|
|
|
+
|
|
|
+ // should merge parent/child classes
|
|
|
+ expect(html()).toMatch(`<div class="child parent">1</div>`)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should warn when fallthrough fails on non-single-root', () => {
|
|
|
+ const Parent = {
|
|
|
+ setup() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ foo: () => 1,
|
|
|
+ class: () => 'parent',
|
|
|
+ onBar: () => () => {},
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ props: ['foo'],
|
|
|
+ render() {
|
|
|
+ return [template('<div></div>')(), template('<div></div>')()]
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ define(Parent).render()
|
|
|
+
|
|
|
+ expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
|
|
|
+ expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should warn when fallthrough fails on teleport root node', () => {
|
|
|
+ const Parent = {
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, { class: () => 'parent' })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const target = document.createElement('div')
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createComponent(
|
|
|
+ VaporTeleport,
|
|
|
+ { to: () => target },
|
|
|
+ {
|
|
|
+ default: () => template('<div></div>')(),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ document.body.appendChild(target)
|
|
|
+ define(Parent).render()
|
|
|
+
|
|
|
+ expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should dedupe same listeners when $attrs is used during render', () => {
|
|
|
+ const click = vi.fn()
|
|
|
+ const count = ref(0)
|
|
|
+
|
|
|
+ function inc() {
|
|
|
+ count.value++
|
|
|
+ click()
|
|
|
+ }
|
|
|
+
|
|
|
+ const Parent = {
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, { onClick: () => inc })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ setup(_, { attrs }) {
|
|
|
+ const n0 = template('<div></div>', true)() as any
|
|
|
+ n0.$evtclick = withModifiers(() => {}, ['prevent', 'stop'])
|
|
|
+ renderEffect(() => setDynamicProps(n0, [attrs]))
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(Parent).render()
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+ node.dispatchEvent(new CustomEvent('click'))
|
|
|
+ expect(click).toHaveBeenCalledTimes(1)
|
|
|
+ expect(count.value).toBe(1)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not warn when context.attrs is used during render', () => {
|
|
|
+ const Parent = {
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ foo: () => 1,
|
|
|
+ class: () => 'parent',
|
|
|
+ onBar: () => () => {},
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ props: ['foo'],
|
|
|
+ render(_ctx, $props, $emit, $attrs, $slots) {
|
|
|
+ const n0 = template('<div></div>')() as Element
|
|
|
+ const n1 = template('<div></div>')() as Element
|
|
|
+ renderEffect(() => {
|
|
|
+ setDynamicProps(n1, [$attrs])
|
|
|
+ })
|
|
|
+ return [n0, n1]
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { html } = define(Parent).render()
|
|
|
+
|
|
|
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
|
|
|
+ expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
|
|
|
+
|
|
|
+ expect(html()).toBe(`<div></div><div class="parent"></div>`)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not warn when context.attrs is used during render (functional)', () => {
|
|
|
+ const Parent = {
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ foo: () => 1,
|
|
|
+ class: () => 'parent',
|
|
|
+ onBar: () => () => {},
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const { component: Child } = define((_: any, { attrs }: any) => {
|
|
|
+ const n0 = template('<div></div>')() as Element
|
|
|
+ const n1 = template('<div></div>')() as Element
|
|
|
+ renderEffect(() => {
|
|
|
+ setDynamicProps(n1, [attrs])
|
|
|
+ })
|
|
|
+ return [n0, n1]
|
|
|
+ })
|
|
|
+
|
|
|
+ Child.props = ['foo']
|
|
|
+
|
|
|
+ const { html } = define(Parent).render()
|
|
|
+
|
|
|
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
|
|
|
+ expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
|
|
|
+ expect(html()).toBe(`<div></div><div class="parent"></div>`)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not warn when functional component has optional props', () => {
|
|
|
+ const Parent = {
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ foo: () => 1,
|
|
|
+ class: () => 'parent',
|
|
|
+ onBar: () => () => {},
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const { component: Child } = define((props: any) => {
|
|
|
+ const n0 = template('<div></div>')() as Element
|
|
|
+ const n1 = template('<div></div>')() as Element
|
|
|
+ renderEffect(() => {
|
|
|
+ setClass(n1, props.class)
|
|
|
+ })
|
|
|
+ return [n0, n1]
|
|
|
+ })
|
|
|
+
|
|
|
+ const { html } = define(Parent).render()
|
|
|
+
|
|
|
+ expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
|
|
|
+ expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
|
|
|
+ expect(html()).toBe(`<div></div><div class="parent"></div>`)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should warn when functional component has props and does not use attrs', () => {
|
|
|
+ const Parent = {
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ foo: () => 1,
|
|
|
+ class: () => 'parent',
|
|
|
+ onBar: () => () => {},
|
|
|
+ })
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const { component: Child } = define(() => [
|
|
|
+ template('<div></div>')(),
|
|
|
+ template('<div></div>')(),
|
|
|
+ ])
|
|
|
+
|
|
|
+ Child.props = ['foo']
|
|
|
+
|
|
|
+ const { html } = define(Parent).render()
|
|
|
+
|
|
|
+ expect(`Extraneous non-props attributes`).toHaveBeenWarned()
|
|
|
+ expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
|
|
|
+ expect(html()).toBe(`<div></div><div></div>`)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not let listener fallthrough when declared in emits (stateful)', () => {
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ emits: ['click'],
|
|
|
+ render(_ctx, $props, $emit, $attrs, $slots) {
|
|
|
+ const n0 = template('<button>hello</button>')() as any
|
|
|
+ n0.$evtclick = () => {
|
|
|
+ // @ts-expect-error
|
|
|
+ $emit('click', 'custom')
|
|
|
+ }
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const onClick = vi.fn()
|
|
|
+ const App = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createComponent(
|
|
|
+ Child,
|
|
|
+ {
|
|
|
+ onClick: () => onClick,
|
|
|
+ },
|
|
|
+ null,
|
|
|
+ true,
|
|
|
+ )
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(App).render()
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+ node.click()
|
|
|
+ expect(onClick).toHaveBeenCalledTimes(1)
|
|
|
+ expect(onClick).toHaveBeenCalledWith('custom')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not let listener fallthrough when declared in emits (functional)', () => {
|
|
|
+ const { component: Child } = define((_: any, { emit }: any) => {
|
|
|
+ // should not be in props
|
|
|
+ expect((_ as any).onClick).toBeUndefined()
|
|
|
+ const n0 = template('<button></button>')() as any
|
|
|
+ n0.$evtclick = () => {
|
|
|
+ emit('click', 'custom')
|
|
|
+ }
|
|
|
+ return n0
|
|
|
+ })
|
|
|
+ Child.emits = ['click']
|
|
|
+
|
|
|
+ const onClick = vi.fn()
|
|
|
+ const App = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ onClick: () => onClick,
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(App).render()
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+ node.click()
|
|
|
+ expect(onClick).toHaveBeenCalledTimes(1)
|
|
|
+ expect(onClick).toHaveBeenCalledWith('custom')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should support fallthrough for single element + comments', () => {
|
|
|
+ const click = vi.fn()
|
|
|
+
|
|
|
+ const Hello = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ class: () => 'foo',
|
|
|
+ onClick: () => click,
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return [
|
|
|
+ template('<!--hello-->')(),
|
|
|
+ template('<button></button>')(),
|
|
|
+ template('<!--world-->')(),
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(Hello).render()
|
|
|
+
|
|
|
+ expect(host.innerHTML).toBe(
|
|
|
+ `<!--hello--><button class="foo"></button><!--world-->`,
|
|
|
+ )
|
|
|
+ const button = host.children[0] as HTMLElement
|
|
|
+ button.dispatchEvent(new CustomEvent('click'))
|
|
|
+ expect(click).toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should support fallthrough for nested element + comments', async () => {
|
|
|
+ const toggle = ref(false)
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ setup() {
|
|
|
+ const n0 = template('<!-- comment A -->')() as any
|
|
|
+ const n1 = createIf(
|
|
|
+ () => toggle.value,
|
|
|
+ () => template('<span>Foo</span>')(),
|
|
|
+ () => {
|
|
|
+ const n2 = template('<!-- comment B -->')() as any
|
|
|
+ const n3 = template('<div>Bar</div>')() as any
|
|
|
+ return [n2, n3]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return [n0, n1]
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const Root = defineVaporComponent({
|
|
|
+ setup() {
|
|
|
+ return createComponent(Child, { class: () => 'red' })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(Root).render()
|
|
|
+
|
|
|
+ expect(host.innerHTML).toBe(
|
|
|
+ `<!-- comment A --><!-- comment B --><div class="red">Bar</div><!--if-->`,
|
|
|
+ )
|
|
|
+
|
|
|
+ toggle.value = true
|
|
|
+ await nextTick()
|
|
|
+ expect(host.innerHTML).toBe(
|
|
|
+ `<!-- comment A --><span class=\"red\">Foo</span><!--if-->`,
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should not fallthrough v-model listeners with corresponding declared prop', () => {
|
|
|
+ let textFoo = ''
|
|
|
+ let textBar = ''
|
|
|
+ const click = vi.fn()
|
|
|
+
|
|
|
+ const App = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, {
|
|
|
+ modelValue: () => textFoo,
|
|
|
+ 'onUpdate:modelValue': () => (val: string) => {
|
|
|
+ textFoo = val
|
|
|
+ },
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ props: ['modelValue'],
|
|
|
+ setup(_props, { emit }) {
|
|
|
+ return createComponent(GrandChild, {
|
|
|
+ modelValue: () => textBar,
|
|
|
+ 'onUpdate:modelValue': () => (val: string) => {
|
|
|
+ textBar = val
|
|
|
+ emit('update:modelValue', 'from Child')
|
|
|
+ },
|
|
|
+ })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const GrandChild = defineVaporComponent({
|
|
|
+ props: ['modelValue'],
|
|
|
+ setup(_props, { emit }) {
|
|
|
+ const n0 = template('<button></button>')() as any
|
|
|
+ n0.$evtclick = () => {
|
|
|
+ click()
|
|
|
+ emit('update:modelValue', 'from GrandChild')
|
|
|
+ }
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { host } = define(App).render()
|
|
|
+ const node = host.children[0] as HTMLElement
|
|
|
+ node.click()
|
|
|
+ expect(click).toHaveBeenCalled()
|
|
|
+ expect(textBar).toBe('from GrandChild')
|
|
|
+ expect(textFoo).toBe('from Child')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should track this.$attrs access in slots', async () => {
|
|
|
+ const GrandChild = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createSlot('default')
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const Child = defineVaporComponent({
|
|
|
+ // @ts-expect-error
|
|
|
+ components: { GrandChild },
|
|
|
+ render(_ctx, $props, $emit, $attrs, $slots) {
|
|
|
+ const n0 = template('<div></div>')() as any
|
|
|
+ setInsertionState(n0)
|
|
|
+ createComponent(GrandChild, null, {
|
|
|
+ default: () => {
|
|
|
+ const n1 = template(' ')()
|
|
|
+ renderEffect(() => setElementText(n1, $attrs.foo))
|
|
|
+ return n1
|
|
|
+ },
|
|
|
+ })
|
|
|
+ return n0
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const obj = ref(1)
|
|
|
+ const App = defineVaporComponent({
|
|
|
+ render() {
|
|
|
+ return createComponent(Child, { foo: () => obj.value })
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const { html } = define(App).render()
|
|
|
+ expect(html()).toBe('<div foo="1">1<!--slot--></div>')
|
|
|
+
|
|
|
+ obj.value = 2
|
|
|
+ await nextTick()
|
|
|
+ expect(html()).toBe('<div foo="2">2<!--slot--></div>')
|
|
|
+ })
|
|
|
+
|
|
|
it('should allow attrs to fallthrough on component with comment at root', async () => {
|
|
|
const t0 = template('<!--comment-->')
|
|
|
const t1 = template('<div>')
|
|
|
@@ -172,6 +836,7 @@ describe('attribute fallthrough', () => {
|
|
|
},
|
|
|
}).render()
|
|
|
expect(host.innerHTML).toBe('<span></span><div>1</div>')
|
|
|
+ expect(`Extraneous non-props attributes (id)`).toHaveBeenWarned()
|
|
|
})
|
|
|
|
|
|
it('should not allow attrs to fallthrough on component with single comment root', async () => {
|
|
|
@@ -190,6 +855,7 @@ describe('attribute fallthrough', () => {
|
|
|
},
|
|
|
}).render()
|
|
|
expect(host.innerHTML).toBe('<!--comment-->')
|
|
|
+ expect(`Extraneous non-props attributes (id)`).toHaveBeenWarned()
|
|
|
})
|
|
|
|
|
|
it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
|