You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

293 lines
9.2 KiB

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