patchProps.spec.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import { vi } from 'vitest'
  2. import { patchProp } from '../src/patchProp'
  3. import { render, h } from '../src'
  4. describe('runtime-dom: props patching', () => {
  5. test('basic', () => {
  6. const el = document.createElement('div')
  7. patchProp(el, 'id', null, 'foo')
  8. expect(el.id).toBe('foo')
  9. // prop with string value should be set to empty string on null values
  10. patchProp(el, 'id', null, null)
  11. expect(el.id).toBe('')
  12. expect(el.getAttribute('id')).toBe(null)
  13. })
  14. test('value', () => {
  15. const el = document.createElement('input')
  16. patchProp(el, 'value', null, 'foo')
  17. expect(el.value).toBe('foo')
  18. patchProp(el, 'value', null, null)
  19. expect(el.value).toBe('')
  20. expect(el.getAttribute('value')).toBe(null)
  21. const obj = {}
  22. patchProp(el, 'value', null, obj)
  23. expect(el.value).toBe(obj.toString())
  24. expect((el as any)._value).toBe(obj)
  25. const option = document.createElement('option')
  26. patchProp(option, 'textContent', null, 'foo')
  27. expect(option.value).toBe('foo')
  28. expect(option.getAttribute('value')).toBe(null)
  29. patchProp(option, 'value', null, 'foo')
  30. expect(option.value).toBe('foo')
  31. expect(option.getAttribute('value')).toBe('foo')
  32. })
  33. test('value for custom elements', () => {
  34. class TestElement extends HTMLElement {
  35. constructor() {
  36. super()
  37. }
  38. // intentionally uses _value because this is used in "normal" HTMLElement for storing the object of the set property value
  39. private _value: any
  40. get value() {
  41. return this._value
  42. }
  43. set value(val) {
  44. this._value = val
  45. this.setterCalled++
  46. }
  47. public setterCalled: number = 0
  48. }
  49. window.customElements.define('patch-props-test-element', TestElement)
  50. const el = document.createElement('patch-props-test-element') as TestElement
  51. patchProp(el, 'value', null, 'foo')
  52. expect(el.value).toBe('foo')
  53. expect(el.setterCalled).toBe(1)
  54. patchProp(el, 'value', null, null)
  55. expect(el.value).toBe('')
  56. expect(el.setterCalled).toBe(2)
  57. expect(el.getAttribute('value')).toBe(null)
  58. const obj = {}
  59. patchProp(el, 'value', null, obj)
  60. expect(el.value).toBe(obj)
  61. expect(el.setterCalled).toBe(3)
  62. })
  63. // For <input type="text">, setting el.value won't create a `value` attribute
  64. // so we need to add tests for other elements
  65. test('value for non-text input', () => {
  66. const el = document.createElement('option')
  67. el.textContent = 'foo' // #4956
  68. patchProp(el, 'value', null, 'foo')
  69. expect(el.getAttribute('value')).toBe('foo')
  70. expect(el.value).toBe('foo')
  71. patchProp(el, 'value', null, null)
  72. el.textContent = ''
  73. expect(el.value).toBe('')
  74. // #3475
  75. expect(el.getAttribute('value')).toBe(null)
  76. })
  77. test('boolean prop', () => {
  78. const el = document.createElement('select')
  79. patchProp(el, 'multiple', null, '')
  80. expect(el.multiple).toBe(true)
  81. patchProp(el, 'multiple', null, null)
  82. expect(el.multiple).toBe(false)
  83. patchProp(el, 'multiple', null, true)
  84. expect(el.multiple).toBe(true)
  85. patchProp(el, 'multiple', null, 0)
  86. expect(el.multiple).toBe(false)
  87. patchProp(el, 'multiple', null, '0')
  88. expect(el.multiple).toBe(true)
  89. patchProp(el, 'multiple', null, false)
  90. expect(el.multiple).toBe(false)
  91. patchProp(el, 'multiple', null, 1)
  92. expect(el.multiple).toBe(true)
  93. patchProp(el, 'multiple', null, undefined)
  94. expect(el.multiple).toBe(false)
  95. })
  96. test('innerHTML unmount prev children', () => {
  97. const fn = vi.fn()
  98. const comp = {
  99. render: () => 'foo',
  100. unmounted: fn
  101. }
  102. const root = document.createElement('div')
  103. render(h('div', null, [h(comp)]), root)
  104. expect(root.innerHTML).toBe(`<div>foo</div>`)
  105. render(h('div', { innerHTML: 'bar' }), root)
  106. expect(root.innerHTML).toBe(`<div>bar</div>`)
  107. expect(fn).toHaveBeenCalled()
  108. })
  109. // #954
  110. test('(svg) innerHTML unmount prev children', () => {
  111. const fn = vi.fn()
  112. const comp = {
  113. render: () => 'foo',
  114. unmounted: fn
  115. }
  116. const root = document.createElement('div')
  117. render(h('div', null, [h(comp)]), root)
  118. expect(root.innerHTML).toBe(`<div>foo</div>`)
  119. render(h('svg', { innerHTML: '<g></g>' }), root)
  120. expect(root.innerHTML).toBe(`<svg><g></g></svg>`)
  121. expect(fn).toHaveBeenCalled()
  122. })
  123. test('textContent unmount prev children', () => {
  124. const fn = vi.fn()
  125. const comp = {
  126. render: () => 'foo',
  127. unmounted: fn
  128. }
  129. const root = document.createElement('div')
  130. render(h('div', null, [h(comp)]), root)
  131. expect(root.innerHTML).toBe(`<div>foo</div>`)
  132. render(h('div', { textContent: 'bar' }), root)
  133. expect(root.innerHTML).toBe(`<div>bar</div>`)
  134. expect(fn).toHaveBeenCalled()
  135. })
  136. // #1049
  137. test('set value as-is for non string-value props', () => {
  138. const el = document.createElement('video')
  139. // jsdom doesn't really support video playback. srcObject in a real browser
  140. // should default to `null`, but in jsdom it's `undefined`.
  141. // anyway, here we just want to make sure Vue doesn't set non-string props
  142. // to an empty string on nullish values - it should reset to its default
  143. // value.
  144. const initialValue = el.srcObject
  145. const fakeObject = {}
  146. patchProp(el, 'srcObject', null, fakeObject)
  147. expect(el.srcObject).not.toBe(fakeObject)
  148. patchProp(el, 'srcObject', null, null)
  149. expect(el.srcObject).toBe(initialValue)
  150. })
  151. test('catch and warn prop set TypeError', () => {
  152. const el = document.createElement('div')
  153. Object.defineProperty(el, 'someProp', {
  154. set() {
  155. throw new TypeError('Invalid type')
  156. }
  157. })
  158. patchProp(el, 'someProp', null, 'foo')
  159. expect(`Failed setting prop "someProp" on <div>`).toHaveBeenWarnedLast()
  160. })
  161. // #1576
  162. test('remove attribute when value is falsy', () => {
  163. const el = document.createElement('div')
  164. patchProp(el, 'id', null, '')
  165. expect(el.hasAttribute('id')).toBe(true)
  166. patchProp(el, 'id', null, null)
  167. expect(el.hasAttribute('id')).toBe(false)
  168. patchProp(el, 'id', null, '')
  169. expect(el.hasAttribute('id')).toBe(true)
  170. patchProp(el, 'id', null, undefined)
  171. expect(el.hasAttribute('id')).toBe(false)
  172. patchProp(el, 'id', null, '')
  173. expect(el.hasAttribute('id')).toBe(true)
  174. // #2677
  175. const img = document.createElement('img')
  176. patchProp(img, 'width', null, '')
  177. expect(el.hasAttribute('width')).toBe(false)
  178. patchProp(img, 'width', null, 0)
  179. expect(img.hasAttribute('width')).toBe(true)
  180. patchProp(img, 'width', null, null)
  181. expect(img.hasAttribute('width')).toBe(false)
  182. patchProp(img, 'width', null, 0)
  183. expect(img.hasAttribute('width')).toBe(true)
  184. patchProp(img, 'width', null, undefined)
  185. expect(img.hasAttribute('width')).toBe(false)
  186. patchProp(img, 'width', null, 0)
  187. expect(img.hasAttribute('width')).toBe(true)
  188. })
  189. test('form attribute', () => {
  190. const el = document.createElement('input')
  191. patchProp(el, 'form', null, 'foo')
  192. // non existent element
  193. expect(el.form).toBe(null)
  194. expect(el.getAttribute('form')).toBe('foo')
  195. // remove attribute
  196. patchProp(el, 'form', 'foo', null)
  197. expect(el.getAttribute('form')).toBe(null)
  198. })
  199. test('readonly type prop on textarea', () => {
  200. const el = document.createElement('textarea')
  201. // just to verify that it doesn't throw when i.e. switching a dynamic :is from an 'input' to a 'textarea'
  202. // see https://github.com/vuejs/core/issues/2766
  203. patchProp(el, 'type', 'text', null)
  204. })
  205. test('force patch as prop', () => {
  206. const el = document.createElement('div') as any
  207. patchProp(el, '.x', null, 1)
  208. expect(el.x).toBe(1)
  209. })
  210. test('force patch as attribute', () => {
  211. const el = document.createElement('div') as any
  212. el.x = 1
  213. patchProp(el, '^x', null, 2)
  214. expect(el.x).toBe(1)
  215. expect(el.getAttribute('x')).toBe('2')
  216. })
  217. test('input with size (number property)', () => {
  218. const el = document.createElement('input')
  219. patchProp(el, 'size', null, 100)
  220. expect(el.size).toBe(100)
  221. patchProp(el, 'size', 100, null)
  222. expect(el.getAttribute('size')).toBe(null)
  223. expect('Failed setting prop "size" on <input>').not.toHaveBeenWarned()
  224. patchProp(el, 'size', null, 'foobar')
  225. expect('Failed setting prop "size" on <input>').toHaveBeenWarnedLast()
  226. })
  227. test('select with type (string property)', () => {
  228. const el = document.createElement('select')
  229. patchProp(el, 'type', null, 'test')
  230. expect(el.type).toBe('select-one')
  231. expect('Failed setting prop "type" on <select>').toHaveBeenWarnedLast()
  232. })
  233. test('select with willValidate (boolean property)', () => {
  234. const el = document.createElement('select')
  235. patchProp(el, 'willValidate', true, null)
  236. expect(el.willValidate).toBe(true)
  237. expect(
  238. 'Failed setting prop "willValidate" on <select>'
  239. ).toHaveBeenWarnedLast()
  240. })
  241. test('patch value for select', () => {
  242. const root = document.createElement('div')
  243. render(
  244. h('select', { value: 'foo' }, [
  245. h('option', { value: 'foo' }, 'foo'),
  246. h('option', { value: 'bar' }, 'bar')
  247. ]),
  248. root
  249. )
  250. const el = root.children[0] as HTMLSelectElement
  251. expect(el.value).toBe('foo')
  252. render(
  253. h('select', { value: 'baz' }, [
  254. h('option', { value: 'foo' }, 'foo'),
  255. h('option', { value: 'baz' }, 'baz')
  256. ]),
  257. root
  258. )
  259. expect(el.value).toBe('baz')
  260. })
  261. test('translate attribute', () => {
  262. const el = document.createElement('div')
  263. patchProp(el, 'translate', null, 'no')
  264. expect(el.translate).toBeFalsy()
  265. expect(el.getAttribute('translate')).toBe('no')
  266. })
  267. })