| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- import { patchProp } from '../src/patchProp'
- import {
- h,
- nextTick,
- ref,
- render,
- vModelCheckbox,
- withDirectives,
- } from '../src'
- describe('runtime-dom: props patching', () => {
- test('basic', () => {
- const el = document.createElement('div')
- patchProp(el, 'id', null, 'foo')
- expect(el.id).toBe('foo')
- // prop with string value should be set to empty string on null values
- patchProp(el, 'id', null, null)
- expect(el.id).toBe('')
- expect(el.getAttribute('id')).toBe(null)
- })
- test('value', () => {
- const el = document.createElement('input')
- patchProp(el, 'value', null, 'foo')
- expect(el.value).toBe('foo')
- patchProp(el, 'value', null, null)
- expect(el.value).toBe('')
- expect(el.getAttribute('value')).toBe(null)
- const obj = {}
- patchProp(el, 'value', null, obj)
- expect(el.value).toBe(obj.toString())
- expect((el as any)._value).toBe(obj)
- const option = document.createElement('option')
- patchProp(option, 'textContent', null, 'foo')
- expect(option.value).toBe('foo')
- expect(option.getAttribute('value')).toBe(null)
- patchProp(option, 'value', null, 'foo')
- expect(option.value).toBe('foo')
- expect(option.getAttribute('value')).toBe('foo')
- })
- test('value for custom elements', () => {
- class TestElement extends HTMLElement {
- constructor() {
- super()
- }
- // intentionally uses _value because this is used in "normal" HTMLElement for storing the object of the set property value
- private _value: any
- get value() {
- return this._value
- }
- set value(val) {
- this._value = val
- this.setterCalled++
- }
- public setterCalled: number = 0
- }
- window.customElements.define('patch-props-test-element', TestElement)
- const el = document.createElement('patch-props-test-element') as TestElement
- patchProp(el, 'value', null, 'foo')
- expect(el.value).toBe('foo')
- expect(el.setterCalled).toBe(1)
- patchProp(el, 'value', null, null)
- expect(el.value).toBe('')
- expect(el.setterCalled).toBe(2)
- expect(el.getAttribute('value')).toBe(null)
- const obj = {}
- patchProp(el, 'value', null, obj)
- expect(el.value).toBe(obj)
- expect(el.setterCalled).toBe(3)
- })
- // For <input type="text">, setting el.value won't create a `value` attribute
- // so we need to add tests for other elements
- test('value for non-text input', () => {
- const el = document.createElement('option')
- el.textContent = 'foo' // #4956
- patchProp(el, 'value', null, 'foo')
- expect(el.getAttribute('value')).toBe('foo')
- expect(el.value).toBe('foo')
- patchProp(el, 'value', null, null)
- el.textContent = ''
- expect(el.value).toBe('')
- // #3475
- expect(el.getAttribute('value')).toBe(null)
- })
- test('boolean prop', () => {
- const el = document.createElement('select')
- patchProp(el, 'multiple', null, '')
- expect(el.multiple).toBe(true)
- patchProp(el, 'multiple', null, null)
- expect(el.multiple).toBe(false)
- patchProp(el, 'multiple', null, true)
- expect(el.multiple).toBe(true)
- patchProp(el, 'multiple', null, 0)
- expect(el.multiple).toBe(false)
- patchProp(el, 'multiple', null, '0')
- expect(el.multiple).toBe(true)
- patchProp(el, 'multiple', null, false)
- expect(el.multiple).toBe(false)
- patchProp(el, 'multiple', null, 1)
- expect(el.multiple).toBe(true)
- patchProp(el, 'multiple', null, undefined)
- expect(el.multiple).toBe(false)
- })
- test('innerHTML unmount prev children', () => {
- const fn = vi.fn()
- const comp = {
- render: () => 'foo',
- unmounted: fn,
- }
- const root = document.createElement('div')
- render(h('div', null, [h(comp)]), root)
- expect(root.innerHTML).toBe(`<div>foo</div>`)
- render(h('div', { innerHTML: 'bar' }), root)
- expect(root.innerHTML).toBe(`<div>bar</div>`)
- expect(fn).toHaveBeenCalled()
- })
- // #954
- test('(svg) innerHTML unmount prev children', () => {
- const fn = vi.fn()
- const comp = {
- render: () => 'foo',
- unmounted: fn,
- }
- const root = document.createElement('div')
- render(h('div', null, [h(comp)]), root)
- expect(root.innerHTML).toBe(`<div>foo</div>`)
- render(h('svg', { innerHTML: '<g></g>' }), root)
- expect(root.innerHTML).toBe(`<svg><g></g></svg>`)
- expect(fn).toHaveBeenCalled()
- })
- test('patch innerHTML porp', async () => {
- const root = document.createElement('div')
- const state = ref(false)
- const Comp = {
- render: () => {
- if (state.value) {
- return h('div', [h('del', null, 'baz')])
- } else {
- return h('div', { innerHTML: 'baz' })
- }
- },
- }
- render(h(Comp), root)
- expect(root.innerHTML).toBe(`<div>baz</div>`)
- state.value = true
- await nextTick()
- expect(root.innerHTML).toBe(`<div><del>baz</del></div>`)
- })
- test('patch innerHTML porp w/ undefined value', async () => {
- const root = document.createElement('div')
- render(h('div', { innerHTML: undefined }), root)
- expect(root.innerHTML).toBe(`<div></div>`)
- })
- test('textContent unmount prev children', () => {
- const fn = vi.fn()
- const comp = {
- render: () => 'foo',
- unmounted: fn,
- }
- const root = document.createElement('div')
- render(h('div', null, [h(comp)]), root)
- expect(root.innerHTML).toBe(`<div>foo</div>`)
- render(h('div', { textContent: 'bar' }), root)
- expect(root.innerHTML).toBe(`<div>bar</div>`)
- expect(fn).toHaveBeenCalled()
- })
- // #1049
- test('set value as-is for non string-value props', () => {
- const el = document.createElement('video')
- // jsdom doesn't really support video playback. srcObject in a real browser
- // should default to `null`, but in jsdom it's `undefined`.
- // anyway, here we just want to make sure Vue doesn't set non-string props
- // to an empty string on nullish values - it should reset to its default
- // value.
- el.srcObject = null
- const initialValue = el.srcObject
- const fakeObject = {}
- patchProp(el, 'srcObject', null, fakeObject)
- expect(el.srcObject).toBe(fakeObject)
- patchProp(el, 'srcObject', null, null)
- expect(el.srcObject).toBe(initialValue)
- })
- test('catch and warn prop set TypeError', () => {
- const el = document.createElement('div')
- Object.defineProperty(el, 'someProp', {
- set() {
- throw new TypeError('Invalid type')
- },
- })
- patchProp(el, 'someProp', null, 'foo')
- expect(`Failed setting prop "someProp" on <div>`).toHaveBeenWarnedLast()
- })
- // #1576
- test('remove attribute when value is falsy', () => {
- const el = document.createElement('div')
- patchProp(el, 'id', null, '')
- expect(el.hasAttribute('id')).toBe(true)
- patchProp(el, 'id', null, null)
- expect(el.hasAttribute('id')).toBe(false)
- patchProp(el, 'id', null, '')
- expect(el.hasAttribute('id')).toBe(true)
- patchProp(el, 'id', null, undefined)
- expect(el.hasAttribute('id')).toBe(false)
- patchProp(el, 'id', null, '')
- expect(el.hasAttribute('id')).toBe(true)
- // #2677
- const img = document.createElement('img')
- patchProp(img, 'width', null, '')
- expect(el.hasAttribute('width')).toBe(false)
- patchProp(img, 'width', null, 0)
- expect(img.hasAttribute('width')).toBe(true)
- patchProp(img, 'width', null, null)
- expect(img.hasAttribute('width')).toBe(false)
- patchProp(img, 'width', null, 0)
- expect(img.hasAttribute('width')).toBe(true)
- patchProp(img, 'width', null, undefined)
- expect(img.hasAttribute('width')).toBe(false)
- patchProp(img, 'width', null, 0)
- expect(img.hasAttribute('width')).toBe(true)
- })
- test('form attribute', () => {
- const el = document.createElement('input')
- patchProp(el, 'form', null, 'foo')
- // non existent element
- expect(el.form).toBe(null)
- expect(el.getAttribute('form')).toBe('foo')
- // remove attribute
- patchProp(el, 'form', 'foo', null)
- expect(el.getAttribute('form')).toBe(null)
- })
- test('readonly type prop on textarea', () => {
- const el = document.createElement('textarea')
- // just to verify that it doesn't throw when i.e. switching a dynamic :is from an 'input' to a 'textarea'
- // see https://github.com/vuejs/core/issues/2766
- patchProp(el, 'type', 'text', null)
- })
- test('force patch as prop', () => {
- const el = document.createElement('div') as any
- patchProp(el, '.x', null, 1)
- expect(el.x).toBe(1)
- })
- test('force patch as attribute', () => {
- const el = document.createElement('div') as any
- el.x = 1
- patchProp(el, '^x', null, 2)
- expect(el.x).toBe(1)
- expect(el.getAttribute('x')).toBe('2')
- })
- test('input with size (number property)', () => {
- const el = document.createElement('input')
- patchProp(el, 'size', null, 100)
- expect(el.size).toBe(100)
- patchProp(el, 'size', 100, null)
- expect(el.getAttribute('size')).toBe(null)
- expect('Failed setting prop "size" on <input>').not.toHaveBeenWarned()
- patchProp(el, 'size', null, 'foobar')
- expect('Failed setting prop "size" on <input>').toHaveBeenWarnedLast()
- })
- test('select with type (string property)', () => {
- const el = document.createElement('select')
- patchProp(el, 'type', null, 'test')
- expect(el.type).toBe('select-one')
- expect('Failed setting prop "type" on <select>').toHaveBeenWarnedLast()
- })
- test('select with willValidate (boolean property)', () => {
- const el = document.createElement('select')
- patchProp(el, 'willValidate', true, null)
- expect(el.willValidate).toBe(true)
- expect(
- 'Failed setting prop "willValidate" on <select>',
- ).toHaveBeenWarnedLast()
- })
- test('patch value for select', () => {
- const root = document.createElement('div')
- render(
- h('select', { value: 'foo' }, [
- h('option', { value: 'foo' }, 'foo'),
- h('option', { value: 'bar' }, 'bar'),
- ]),
- root,
- )
- const el = root.children[0] as HTMLSelectElement
- expect(el.value).toBe('foo')
- render(
- h('select', { value: 'baz' }, [
- h('option', { value: 'foo' }, 'foo'),
- h('option', { value: 'baz' }, 'baz'),
- ]),
- root,
- )
- expect(el.value).toBe('baz')
- })
- test('init empty value for option', () => {
- const root = document.createElement('div')
- render(
- h('select', { value: 'foo' }, [h('option', { value: '' }, 'foo')]),
- root,
- )
- const select = root.children[0] as HTMLSelectElement
- const option = select.children[0] as HTMLOptionElement
- expect(select.value).toBe('')
- expect(option.value).toBe('')
- })
- // #8780
- test('embedded tag with width and height', () => {
- // Width and height of some embedded element such as img、video、source、canvas
- // must be set as attribute
- const el = document.createElement('img')
- patchProp(el, 'width', null, '24px')
- expect(el.getAttribute('width')).toBe('24px')
- })
- // # 9762 should fallthrough to `key in el` logic for non embedded tags
- test('width and height on custom elements', () => {
- const el = document.createElement('foobar')
- patchProp(el, 'width', null, '24px')
- expect(el.getAttribute('width')).toBe('24px')
- })
- test('translate attribute', () => {
- const el = document.createElement('div')
- patchProp(el, 'translate', null, 'no')
- expect(el.translate).toBeFalsy()
- expect(el.getAttribute('translate')).toBe('no')
- })
- // #11647
- test('should not trigger input mutation when `value` is `undefined`', async () => {
- const fn = vi.fn()
- const comp = {
- setup() {
- const checked = ref()
- return () =>
- withDirectives(
- h('input', {
- type: 'checkbox',
- value: undefined,
- 'onUpdate:modelValue': (value: any) => {
- checked.value = value
- },
- }),
- [[vModelCheckbox, checked.value]],
- )
- },
- }
- const root = document.createElement('div')
- render(h(comp), root)
- document.body.append(root)
- const el = root.children[0] as HTMLInputElement
- const observer = new MutationObserver(fn)
- observer.observe(el, {
- attributes: true,
- })
- el.click()
- await nextTick()
- expect(fn).toBeCalledTimes(0)
- })
- })
|