patchProps.spec.ts 10 KB

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