rendererAttrsFallthrough.spec.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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('should allow whitelisted attrs to fallthrough', 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. {
  44. id: props.id, // id is not whitelisted
  45. class: 'c2',
  46. style: { fontWeight: 'bold' }
  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') // id is not whitelisted, but explicitly bound
  57. expect(node.getAttribute('foo')).toBe(null) // foo is not whitelisted
  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(null)
  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 fallthrough for nested components', async () => {
  73. const click = jest.fn()
  74. const childUpdated = jest.fn()
  75. const grandChildUpdated = jest.fn()
  76. const Hello = {
  77. setup() {
  78. const count = ref(0)
  79. function inc() {
  80. count.value++
  81. click()
  82. }
  83. return () =>
  84. h(Child, {
  85. foo: 1,
  86. id: 'test',
  87. class: 'c' + count.value,
  88. style: { color: count.value ? 'red' : 'green' },
  89. onClick: inc
  90. })
  91. }
  92. }
  93. const Child = {
  94. setup(props: any) {
  95. onUpdated(childUpdated)
  96. // HOC simply passing props down.
  97. // this will result in merging the same attrs, but should be deduped by
  98. // `mergeProps`.
  99. return () => h(GrandChild, props)
  100. }
  101. }
  102. const GrandChild = defineComponent({
  103. props: {
  104. id: String,
  105. foo: Number
  106. },
  107. setup(props) {
  108. onUpdated(grandChildUpdated)
  109. return () =>
  110. h(
  111. 'div',
  112. {
  113. id: props.id,
  114. class: 'c2',
  115. style: { fontWeight: 'bold' }
  116. },
  117. props.foo
  118. )
  119. }
  120. })
  121. const root = document.createElement('div')
  122. document.body.appendChild(root)
  123. render(h(Hello), root)
  124. const node = root.children[0] as HTMLElement
  125. // with declared props, any parent attr that isn't a prop falls through
  126. expect(node.getAttribute('id')).toBe('test')
  127. expect(node.getAttribute('class')).toBe('c2 c0')
  128. expect(node.style.color).toBe('green')
  129. expect(node.style.fontWeight).toBe('bold')
  130. node.dispatchEvent(new CustomEvent('click'))
  131. expect(click).toHaveBeenCalled()
  132. // ...while declared ones remain props
  133. expect(node.hasAttribute('foo')).toBe(false)
  134. await nextTick()
  135. expect(childUpdated).toHaveBeenCalled()
  136. expect(grandChildUpdated).toHaveBeenCalled()
  137. expect(node.getAttribute('id')).toBe('test')
  138. expect(node.getAttribute('class')).toBe('c2 c1')
  139. expect(node.style.color).toBe('red')
  140. expect(node.style.fontWeight).toBe('bold')
  141. expect(node.hasAttribute('foo')).toBe(false)
  142. })
  143. it('should not fallthrough with inheritAttrs: false', () => {
  144. const Parent = {
  145. render() {
  146. return h(Child, { foo: 1, class: 'parent' })
  147. }
  148. }
  149. const Child = defineComponent({
  150. props: ['foo'],
  151. inheritAttrs: false,
  152. render() {
  153. return h('div', this.foo)
  154. }
  155. })
  156. const root = document.createElement('div')
  157. document.body.appendChild(root)
  158. render(h(Parent), root)
  159. // should not contain class
  160. expect(root.innerHTML).toMatch(`<div>1</div>`)
  161. })
  162. it('explicit spreading with inheritAttrs: false', () => {
  163. const Parent = {
  164. render() {
  165. return h(Child, { foo: 1, class: 'parent' })
  166. }
  167. }
  168. const Child = defineComponent({
  169. props: ['foo'],
  170. inheritAttrs: false,
  171. render() {
  172. return h(
  173. 'div',
  174. mergeProps(
  175. {
  176. class: 'child'
  177. },
  178. this.$attrs
  179. ),
  180. this.foo
  181. )
  182. }
  183. })
  184. const root = document.createElement('div')
  185. document.body.appendChild(root)
  186. render(h(Parent), root)
  187. // should merge parent/child classes
  188. expect(root.innerHTML).toMatch(`<div class="child parent">1</div>`)
  189. })
  190. it('should warn when fallthrough fails on non-single-root', () => {
  191. const Parent = {
  192. render() {
  193. return h(Child, { foo: 1, class: 'parent' })
  194. }
  195. }
  196. const Child = defineComponent({
  197. props: ['foo'],
  198. render() {
  199. return [h('div'), h('div')]
  200. }
  201. })
  202. const root = document.createElement('div')
  203. document.body.appendChild(root)
  204. render(h(Parent), root)
  205. expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
  206. })
  207. it('should not warn when $attrs is used during render', () => {
  208. const Parent = {
  209. render() {
  210. return h(Child, { foo: 1, class: 'parent' })
  211. }
  212. }
  213. const Child = defineComponent({
  214. props: ['foo'],
  215. render() {
  216. return [h('div'), h('div', this.$attrs)]
  217. }
  218. })
  219. const root = document.createElement('div')
  220. document.body.appendChild(root)
  221. render(h(Parent), root)
  222. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  223. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  224. })
  225. it('should not warn when context.attrs is used during render', () => {
  226. const Parent = {
  227. render() {
  228. return h(Child, { foo: 1, class: 'parent' })
  229. }
  230. }
  231. const Child = defineComponent({
  232. props: ['foo'],
  233. setup(_props, { attrs }) {
  234. return () => [h('div'), h('div', attrs)]
  235. }
  236. })
  237. const root = document.createElement('div')
  238. document.body.appendChild(root)
  239. render(h(Parent), root)
  240. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  241. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  242. })
  243. // #677
  244. it('should update merged dynamic attrs on optimized child root', async () => {
  245. const aria = ref('true')
  246. const cls = ref('bar')
  247. const Parent = {
  248. render() {
  249. return h(Child, { 'aria-hidden': aria.value, class: cls.value })
  250. }
  251. }
  252. const Child = {
  253. props: [],
  254. render() {
  255. return openBlock(), createBlock('div')
  256. }
  257. }
  258. const root = document.createElement('div')
  259. document.body.appendChild(root)
  260. render(h(Parent), root)
  261. expect(root.innerHTML).toBe(`<div aria-hidden="true" class="bar"></div>`)
  262. aria.value = 'false'
  263. await nextTick()
  264. expect(root.innerHTML).toBe(`<div aria-hidden="false" class="bar"></div>`)
  265. cls.value = 'barr'
  266. await nextTick()
  267. expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
  268. })
  269. })