rendererAttrsFallthrough.spec.ts 9.3 KB

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