patchProps.spec.ts 11 KB

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