rendererAttrsFallthrough.spec.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. // using DOM renderer because this case is mostly DOM-specific
  2. import {
  3. h,
  4. render,
  5. nextTick,
  6. mergeProps,
  7. ref,
  8. onUpdated,
  9. defineComponent
  10. } from '@vue/runtime-dom'
  11. import { mockWarn } from '@vue/runtime-test'
  12. describe('attribute fallthrough', () => {
  13. mockWarn()
  14. it('everything should be in props when component has no declared props', async () => {
  15. const click = jest.fn()
  16. const childUpdated = jest.fn()
  17. const Hello = {
  18. setup() {
  19. const count = ref(0)
  20. function inc() {
  21. count.value++
  22. click()
  23. }
  24. return () =>
  25. h(Child, {
  26. foo: 1,
  27. id: 'test',
  28. class: 'c' + count.value,
  29. style: { color: count.value ? 'red' : 'green' },
  30. onClick: inc
  31. })
  32. }
  33. }
  34. const Child = {
  35. setup(props: any) {
  36. onUpdated(childUpdated)
  37. return () =>
  38. h(
  39. 'div',
  40. mergeProps(
  41. {
  42. class: 'c2',
  43. style: { fontWeight: 'bold' }
  44. },
  45. props
  46. ),
  47. props.foo
  48. )
  49. }
  50. }
  51. const root = document.createElement('div')
  52. document.body.appendChild(root)
  53. render(h(Hello), root)
  54. const node = root.children[0] as HTMLElement
  55. expect(node.getAttribute('id')).toBe('test')
  56. expect(node.getAttribute('foo')).toBe('1')
  57. expect(node.getAttribute('class')).toBe('c2 c0')
  58. expect(node.style.color).toBe('green')
  59. expect(node.style.fontWeight).toBe('bold')
  60. node.dispatchEvent(new CustomEvent('click'))
  61. expect(click).toHaveBeenCalled()
  62. await nextTick()
  63. expect(childUpdated).toHaveBeenCalled()
  64. expect(node.getAttribute('id')).toBe('test')
  65. expect(node.getAttribute('foo')).toBe('1')
  66. expect(node.getAttribute('class')).toBe('c2 c1')
  67. expect(node.style.color).toBe('red')
  68. expect(node.style.fontWeight).toBe('bold')
  69. })
  70. it('should implicitly fallthrough on single root nodes', async () => {
  71. const click = jest.fn()
  72. const childUpdated = jest.fn()
  73. const Hello = {
  74. setup() {
  75. const count = ref(0)
  76. function inc() {
  77. count.value++
  78. click()
  79. }
  80. return () =>
  81. h(Child, {
  82. foo: 1,
  83. id: 'test',
  84. class: 'c' + count.value,
  85. style: { color: count.value ? 'red' : 'green' },
  86. onClick: inc
  87. })
  88. }
  89. }
  90. const Child = defineComponent({
  91. props: {
  92. foo: Number
  93. },
  94. setup(props) {
  95. onUpdated(childUpdated)
  96. return () =>
  97. h(
  98. 'div',
  99. {
  100. class: 'c2',
  101. style: { fontWeight: 'bold' }
  102. },
  103. props.foo
  104. )
  105. }
  106. })
  107. const root = document.createElement('div')
  108. document.body.appendChild(root)
  109. render(h(Hello), root)
  110. const node = root.children[0] as HTMLElement
  111. // with declared props, any parent attr that isn't a prop falls through
  112. expect(node.getAttribute('id')).toBe('test')
  113. expect(node.getAttribute('class')).toBe('c2 c0')
  114. expect(node.style.color).toBe('green')
  115. expect(node.style.fontWeight).toBe('bold')
  116. node.dispatchEvent(new CustomEvent('click'))
  117. expect(click).toHaveBeenCalled()
  118. // ...while declared ones remain props
  119. expect(node.hasAttribute('foo')).toBe(false)
  120. await nextTick()
  121. expect(childUpdated).toHaveBeenCalled()
  122. expect(node.getAttribute('id')).toBe('test')
  123. expect(node.getAttribute('class')).toBe('c2 c1')
  124. expect(node.style.color).toBe('red')
  125. expect(node.style.fontWeight).toBe('bold')
  126. expect(node.hasAttribute('foo')).toBe(false)
  127. })
  128. it('should fallthrough for nested components', async () => {
  129. const click = jest.fn()
  130. const childUpdated = jest.fn()
  131. const grandChildUpdated = jest.fn()
  132. const Hello = {
  133. setup() {
  134. const count = ref(0)
  135. function inc() {
  136. count.value++
  137. click()
  138. }
  139. return () =>
  140. h(Child, {
  141. foo: 1,
  142. id: 'test',
  143. class: 'c' + count.value,
  144. style: { color: count.value ? 'red' : 'green' },
  145. onClick: inc
  146. })
  147. }
  148. }
  149. const Child = {
  150. setup(props: any) {
  151. onUpdated(childUpdated)
  152. return () => h(GrandChild, props)
  153. }
  154. }
  155. const GrandChild = defineComponent({
  156. props: {
  157. foo: Number
  158. },
  159. setup(props) {
  160. onUpdated(grandChildUpdated)
  161. return () =>
  162. h(
  163. 'div',
  164. {
  165. class: 'c2',
  166. style: { fontWeight: 'bold' }
  167. },
  168. props.foo
  169. )
  170. }
  171. })
  172. const root = document.createElement('div')
  173. document.body.appendChild(root)
  174. render(h(Hello), root)
  175. const node = root.children[0] as HTMLElement
  176. // with declared props, any parent attr that isn't a prop falls through
  177. expect(node.getAttribute('id')).toBe('test')
  178. expect(node.getAttribute('class')).toBe('c2 c0')
  179. expect(node.style.color).toBe('green')
  180. expect(node.style.fontWeight).toBe('bold')
  181. node.dispatchEvent(new CustomEvent('click'))
  182. expect(click).toHaveBeenCalled()
  183. // ...while declared ones remain props
  184. expect(node.hasAttribute('foo')).toBe(false)
  185. await nextTick()
  186. expect(childUpdated).toHaveBeenCalled()
  187. expect(grandChildUpdated).toHaveBeenCalled()
  188. expect(node.getAttribute('id')).toBe('test')
  189. expect(node.getAttribute('class')).toBe('c2 c1')
  190. expect(node.style.color).toBe('red')
  191. expect(node.style.fontWeight).toBe('bold')
  192. expect(node.hasAttribute('foo')).toBe(false)
  193. })
  194. it('should not fallthrough with inheritAttrs: false', () => {
  195. const Parent = {
  196. render() {
  197. return h(Child, { foo: 1, class: 'parent' })
  198. }
  199. }
  200. const Child = defineComponent({
  201. props: ['foo'],
  202. inheritAttrs: false,
  203. render() {
  204. return h('div', this.foo)
  205. }
  206. })
  207. const root = document.createElement('div')
  208. document.body.appendChild(root)
  209. render(h(Parent), root)
  210. // should not contain class
  211. expect(root.innerHTML).toMatch(`<div>1</div>`)
  212. })
  213. it('explicit spreading with inheritAttrs: false', () => {
  214. const Parent = {
  215. render() {
  216. return h(Child, { foo: 1, class: 'parent' })
  217. }
  218. }
  219. const Child = defineComponent({
  220. props: ['foo'],
  221. inheritAttrs: false,
  222. render() {
  223. return h(
  224. 'div',
  225. mergeProps(
  226. {
  227. class: 'child'
  228. },
  229. this.$attrs
  230. ),
  231. this.foo
  232. )
  233. }
  234. })
  235. const root = document.createElement('div')
  236. document.body.appendChild(root)
  237. render(h(Parent), root)
  238. // should merge parent/child classes
  239. expect(root.innerHTML).toMatch(`<div class="child parent">1</div>`)
  240. })
  241. it('should warn when fallthrough fails on non-single-root', () => {
  242. const Parent = {
  243. render() {
  244. return h(Child, { foo: 1, class: 'parent' })
  245. }
  246. }
  247. const Child = defineComponent({
  248. props: ['foo'],
  249. render() {
  250. return [h('div'), h('div')]
  251. }
  252. })
  253. const root = document.createElement('div')
  254. document.body.appendChild(root)
  255. render(h(Parent), root)
  256. expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
  257. })
  258. it('should not warn when $attrs is used during render', () => {
  259. const Parent = {
  260. render() {
  261. return h(Child, { foo: 1, class: 'parent' })
  262. }
  263. }
  264. const Child = defineComponent({
  265. props: ['foo'],
  266. render() {
  267. return [h('div'), h('div', this.$attrs)]
  268. }
  269. })
  270. const root = document.createElement('div')
  271. document.body.appendChild(root)
  272. render(h(Parent), root)
  273. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  274. expect(root.innerHTML).toBe(
  275. `<!----><div></div><div class="parent"></div><!---->`
  276. )
  277. })
  278. })