rendererAttrsFallthrough.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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 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: count.value + 1,
  29. id: 'test',
  30. class: 'c' + count.value,
  31. style: { color: count.value ? 'red' : 'green' },
  32. onClick: inc,
  33. 'data-id': count.value + 1
  34. })
  35. }
  36. }
  37. const Child = {
  38. setup(props: any) {
  39. onUpdated(childUpdated)
  40. return () =>
  41. h(
  42. 'div',
  43. {
  44. class: 'c2',
  45. style: { fontWeight: 'bold' }
  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. expect(node.dataset.id).toBe('1')
  61. node.dispatchEvent(new CustomEvent('click'))
  62. expect(click).toHaveBeenCalled()
  63. await nextTick()
  64. expect(childUpdated).toHaveBeenCalled()
  65. expect(node.getAttribute('id')).toBe('test')
  66. expect(node.getAttribute('foo')).toBe('2')
  67. expect(node.getAttribute('class')).toBe('c2 c1')
  68. expect(node.style.color).toBe('red')
  69. expect(node.style.fontWeight).toBe('bold')
  70. expect(node.dataset.id).toBe('2')
  71. })
  72. it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
  73. const click = jest.fn()
  74. const childUpdated = jest.fn()
  75. const count = ref(0)
  76. function inc() {
  77. count.value++
  78. click()
  79. }
  80. const Hello = () =>
  81. h(Child, {
  82. foo: count.value + 1,
  83. id: 'test',
  84. class: 'c' + count.value,
  85. style: { color: count.value ? 'red' : 'green' },
  86. onClick: inc
  87. })
  88. const Child = (props: any) => {
  89. childUpdated()
  90. return h(
  91. 'div',
  92. {
  93. class: 'c2',
  94. style: { fontWeight: 'bold' }
  95. },
  96. props.foo
  97. )
  98. }
  99. const root = document.createElement('div')
  100. document.body.appendChild(root)
  101. render(h(Hello), root)
  102. const node = root.children[0] as HTMLElement
  103. // not whitelisted
  104. expect(node.getAttribute('id')).toBe(null)
  105. expect(node.getAttribute('foo')).toBe(null)
  106. // whitelisted: style, class, event listeners
  107. expect(node.getAttribute('class')).toBe('c2 c0')
  108. expect(node.style.color).toBe('green')
  109. expect(node.style.fontWeight).toBe('bold')
  110. node.dispatchEvent(new CustomEvent('click'))
  111. expect(click).toHaveBeenCalled()
  112. await nextTick()
  113. expect(childUpdated).toHaveBeenCalled()
  114. expect(node.getAttribute('id')).toBe(null)
  115. expect(node.getAttribute('foo')).toBe(null)
  116. expect(node.getAttribute('class')).toBe('c2 c1')
  117. expect(node.style.color).toBe('red')
  118. expect(node.style.fontWeight).toBe('bold')
  119. })
  120. it('should allow all attrs on functional component with declared props', async () => {
  121. const click = jest.fn()
  122. const childUpdated = jest.fn()
  123. const count = ref(0)
  124. function inc() {
  125. count.value++
  126. click()
  127. }
  128. const Hello = () =>
  129. h(Child, {
  130. foo: count.value + 1,
  131. id: 'test',
  132. class: 'c' + count.value,
  133. style: { color: count.value ? 'red' : 'green' },
  134. onClick: inc
  135. })
  136. const Child = (props: { foo: number }) => {
  137. childUpdated()
  138. return h(
  139. 'div',
  140. {
  141. class: 'c2',
  142. style: { fontWeight: 'bold' }
  143. },
  144. props.foo
  145. )
  146. }
  147. Child.props = ['foo']
  148. const root = document.createElement('div')
  149. document.body.appendChild(root)
  150. render(h(Hello), root)
  151. const node = root.children[0] as HTMLElement
  152. expect(node.getAttribute('id')).toBe('test')
  153. expect(node.getAttribute('foo')).toBe(null) // declared as prop
  154. expect(node.getAttribute('class')).toBe('c2 c0')
  155. expect(node.style.color).toBe('green')
  156. expect(node.style.fontWeight).toBe('bold')
  157. node.dispatchEvent(new CustomEvent('click'))
  158. expect(click).toHaveBeenCalled()
  159. await nextTick()
  160. expect(childUpdated).toHaveBeenCalled()
  161. expect(node.getAttribute('id')).toBe('test')
  162. expect(node.getAttribute('foo')).toBe(null)
  163. expect(node.getAttribute('class')).toBe('c2 c1')
  164. expect(node.style.color).toBe('red')
  165. expect(node.style.fontWeight).toBe('bold')
  166. })
  167. it('should fallthrough for nested components', async () => {
  168. const click = jest.fn()
  169. const childUpdated = jest.fn()
  170. const grandChildUpdated = jest.fn()
  171. const Hello = {
  172. setup() {
  173. const count = ref(0)
  174. function inc() {
  175. count.value++
  176. click()
  177. }
  178. return () =>
  179. h(Child, {
  180. foo: 1,
  181. id: 'test',
  182. class: 'c' + count.value,
  183. style: { color: count.value ? 'red' : 'green' },
  184. onClick: inc
  185. })
  186. }
  187. }
  188. const Child = {
  189. setup(props: any) {
  190. onUpdated(childUpdated)
  191. // HOC simply passing props down.
  192. // this will result in merging the same attrs, but should be deduped by
  193. // `mergeProps`.
  194. return () => h(GrandChild, props)
  195. }
  196. }
  197. const GrandChild = defineComponent({
  198. props: {
  199. id: String,
  200. foo: Number
  201. },
  202. setup(props) {
  203. onUpdated(grandChildUpdated)
  204. return () =>
  205. h(
  206. 'div',
  207. {
  208. id: props.id,
  209. class: 'c2',
  210. style: { fontWeight: 'bold' }
  211. },
  212. props.foo
  213. )
  214. }
  215. })
  216. const root = document.createElement('div')
  217. document.body.appendChild(root)
  218. render(h(Hello), root)
  219. const node = root.children[0] as HTMLElement
  220. // with declared props, any parent attr that isn't a prop falls through
  221. expect(node.getAttribute('id')).toBe('test')
  222. expect(node.getAttribute('class')).toBe('c2 c0')
  223. expect(node.style.color).toBe('green')
  224. expect(node.style.fontWeight).toBe('bold')
  225. node.dispatchEvent(new CustomEvent('click'))
  226. expect(click).toHaveBeenCalled()
  227. // ...while declared ones remain props
  228. expect(node.hasAttribute('foo')).toBe(false)
  229. await nextTick()
  230. expect(childUpdated).toHaveBeenCalled()
  231. expect(grandChildUpdated).toHaveBeenCalled()
  232. expect(node.getAttribute('id')).toBe('test')
  233. expect(node.getAttribute('class')).toBe('c2 c1')
  234. expect(node.style.color).toBe('red')
  235. expect(node.style.fontWeight).toBe('bold')
  236. expect(node.hasAttribute('foo')).toBe(false)
  237. })
  238. it('should not fallthrough with inheritAttrs: false', () => {
  239. const Parent = {
  240. render() {
  241. return h(Child, { foo: 1, class: 'parent' })
  242. }
  243. }
  244. const Child = defineComponent({
  245. props: ['foo'],
  246. inheritAttrs: false,
  247. render() {
  248. return h('div', this.foo)
  249. }
  250. })
  251. const root = document.createElement('div')
  252. document.body.appendChild(root)
  253. render(h(Parent), root)
  254. // should not contain class
  255. expect(root.innerHTML).toMatch(`<div>1</div>`)
  256. })
  257. it('explicit spreading with inheritAttrs: false', () => {
  258. const Parent = {
  259. render() {
  260. return h(Child, { foo: 1, class: 'parent' })
  261. }
  262. }
  263. const Child = defineComponent({
  264. props: ['foo'],
  265. inheritAttrs: false,
  266. render() {
  267. return h(
  268. 'div',
  269. mergeProps(
  270. {
  271. class: 'child'
  272. },
  273. this.$attrs
  274. ),
  275. this.foo
  276. )
  277. }
  278. })
  279. const root = document.createElement('div')
  280. document.body.appendChild(root)
  281. render(h(Parent), root)
  282. // should merge parent/child classes
  283. expect(root.innerHTML).toMatch(`<div class="child parent">1</div>`)
  284. })
  285. it('should warn when fallthrough fails on non-single-root', () => {
  286. const Parent = {
  287. render() {
  288. return h(Child, { foo: 1, class: 'parent' })
  289. }
  290. }
  291. const Child = defineComponent({
  292. props: ['foo'],
  293. render() {
  294. return [h('div'), h('div')]
  295. }
  296. })
  297. const root = document.createElement('div')
  298. document.body.appendChild(root)
  299. render(h(Parent), root)
  300. expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
  301. })
  302. it('should not warn when $attrs is used during render', () => {
  303. const Parent = {
  304. render() {
  305. return h(Child, { foo: 1, class: 'parent' })
  306. }
  307. }
  308. const Child = defineComponent({
  309. props: ['foo'],
  310. render() {
  311. return [h('div'), h('div', this.$attrs)]
  312. }
  313. })
  314. const root = document.createElement('div')
  315. document.body.appendChild(root)
  316. render(h(Parent), root)
  317. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  318. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  319. })
  320. it('should not warn when context.attrs is used during render', () => {
  321. const Parent = {
  322. render() {
  323. return h(Child, { foo: 1, class: 'parent' })
  324. }
  325. }
  326. const Child = defineComponent({
  327. props: ['foo'],
  328. setup(_props, { attrs }) {
  329. return () => [h('div'), h('div', attrs)]
  330. }
  331. })
  332. const root = document.createElement('div')
  333. document.body.appendChild(root)
  334. render(h(Parent), root)
  335. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  336. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  337. })
  338. // #677
  339. it('should update merged dynamic attrs on optimized child root', async () => {
  340. const aria = ref('true')
  341. const cls = ref('bar')
  342. const Parent = {
  343. render() {
  344. return h(Child, { 'aria-hidden': aria.value, class: cls.value })
  345. }
  346. }
  347. const Child = {
  348. props: [],
  349. render() {
  350. return openBlock(), createBlock('div')
  351. }
  352. }
  353. const root = document.createElement('div')
  354. document.body.appendChild(root)
  355. render(h(Parent), root)
  356. expect(root.innerHTML).toBe(`<div aria-hidden="true" class="bar"></div>`)
  357. aria.value = 'false'
  358. await nextTick()
  359. expect(root.innerHTML).toBe(`<div aria-hidden="false" class="bar"></div>`)
  360. cls.value = 'barr'
  361. await nextTick()
  362. expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
  363. })
  364. })