rendererAttrsFallthrough.spec.ts 7.9 KB

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