// using DOM renderer because this case is mostly DOM-specific import { h, render, nextTick, mergeProps, ref, onUpdated, defineComponent, openBlock, createBlock, FunctionalComponent, createCommentVNode } from '@vue/runtime-dom' import { mockWarn } from '@vue/shared' describe('attribute fallthrough', () => { mockWarn() it('should allow attrs to fallthrough', async () => { const click = jest.fn() const childUpdated = jest.fn() const Hello = { setup() { const count = ref(0) function inc() { count.value++ click() } return () => h(Child, { foo: count.value + 1, id: 'test', class: 'c' + count.value, style: { color: count.value ? 'red' : 'green' }, onClick: inc, 'data-id': count.value + 1 }) } } const Child = { setup(props: any) { onUpdated(childUpdated) return () => h( 'div', { class: 'c2', style: { fontWeight: 'bold' } }, props.foo ) } } const root = document.createElement('div') document.body.appendChild(root) render(h(Hello), root) const node = root.children[0] as HTMLElement expect(node.getAttribute('id')).toBe('test') expect(node.getAttribute('foo')).toBe('1') expect(node.getAttribute('class')).toBe('c2 c0') expect(node.style.color).toBe('green') expect(node.style.fontWeight).toBe('bold') expect(node.dataset.id).toBe('1') node.dispatchEvent(new CustomEvent('click')) expect(click).toHaveBeenCalled() await nextTick() expect(childUpdated).toHaveBeenCalled() expect(node.getAttribute('id')).toBe('test') expect(node.getAttribute('foo')).toBe('2') expect(node.getAttribute('class')).toBe('c2 c1') expect(node.style.color).toBe('red') expect(node.style.fontWeight).toBe('bold') expect(node.dataset.id).toBe('2') }) it('should only allow whitelisted fallthrough on functional component with optional props', async () => { const click = jest.fn() const childUpdated = jest.fn() const count = ref(0) function inc() { count.value++ click() } const Hello = () => h(Child, { foo: count.value + 1, id: 'test', class: 'c' + count.value, style: { color: count.value ? 'red' : 'green' }, onClick: inc }) const Child = (props: any) => { childUpdated() return h( 'div', { class: 'c2', style: { fontWeight: 'bold' } }, props.foo ) } const root = document.createElement('div') document.body.appendChild(root) render(h(Hello), root) const node = root.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 = jest.fn() const childUpdated = jest.fn() const count = ref(0) function inc() { count.value++ click() } const Hello = () => h(Child, { foo: count.value + 1, id: 'test', class: 'c' + count.value, style: { color: count.value ? 'red' : 'green' }, onClick: inc }) const Child = (props: { foo: number }) => { childUpdated() return h( 'div', { class: 'c2', style: { fontWeight: 'bold' } }, props.foo ) } Child.props = ['foo'] const root = document.createElement('div') document.body.appendChild(root) render(h(Hello), root) const node = root.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 = jest.fn() const childUpdated = jest.fn() const grandChildUpdated = jest.fn() const Hello = { setup() { const count = ref(0) function inc() { count.value++ click() } return () => h(Child, { foo: 1, id: 'test', class: 'c' + count.value, style: { color: count.value ? 'red' : 'green' }, onClick: inc }) } } const Child = { 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 () => h(GrandChild, props) } } const GrandChild = defineComponent({ props: { id: String, foo: Number }, setup(props) { onUpdated(grandChildUpdated) return () => h( 'div', { id: props.id, class: 'c2', style: { fontWeight: 'bold' } }, props.foo ) } }) const root = document.createElement('div') document.body.appendChild(root) render(h(Hello), root) const node = root.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() expect(childUpdated).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 = { render() { return h(Child, { foo: 1, class: 'parent' }) } } const Child = defineComponent({ props: ['foo'], inheritAttrs: false, render() { return h('div', this.foo) } }) const root = document.createElement('div') document.body.appendChild(root) render(h(Parent), root) // should not contain class expect(root.innerHTML).toMatch(`