mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: `color:red;` }),
)
+ mountWithHydration(
+ `<div style="color:red; font-size: 12px;"></div>`,
+ () => h('div', { style: `font-size: 12px; color:red;` }),
+ )
+ mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
+ withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+ [vShow, false],
+ ]),
+ )
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div style="color:red;"></div>`, () =>
h('div', { style: { color: 'green' } }),
)
- expect(`Hydration style mismatch`).toHaveBeenWarned()
+ expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+ })
+
+ test('style mismatch w/ v-show', () => {
+ mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
+ withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+ [vShow, false],
+ ]),
+ )
+ expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+ mountWithHydration(`<div style="color:red;"></div>`, () =>
+ withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+ [vShow, false],
+ ]),
+ )
+ expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
})
test('attr mismatch', () => {
mountWithHydration(`<select multiple></div>`, () =>
h('select', { multiple: 'multiple' }),
)
+ mountWithHydration(`<textarea>foo</textarea>`, () =>
+ h('textarea', { value: 'foo' }),
+ )
+ mountWithHydration(`<textarea></textarea>`, () =>
+ h('textarea', { value: '' }),
+ )
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
) {
for (const key in props) {
// check hydration mismatch
- if (__DEV__ && propHasMismatch(el, key, props[key])) {
+ if (__DEV__ && propHasMismatch(el, key, props[key], vnode)) {
hasMismatch = true
}
if (
/**
* Dev only
*/
-function propHasMismatch(el: Element, key: string, clientValue: any): boolean {
+function propHasMismatch(
+ el: Element,
+ key: string,
+ clientValue: any,
+ vnode: VNode,
+): boolean {
let mismatchType: string | undefined
let mismatchKey: string | undefined
let actual: any
mismatchType = mismatchKey = `class`
}
} else if (key === 'style') {
- actual = el.getAttribute('style')
- expected = isString(clientValue)
- ? clientValue
- : stringifyStyle(normalizeStyle(clientValue))
- if (actual !== expected) {
+ // style might be in different order, but that doesn't affect cascade
+ actual = toStyleMap(el.getAttribute('style') || '')
+ expected = toStyleMap(
+ isString(clientValue)
+ ? clientValue
+ : stringifyStyle(normalizeStyle(clientValue)),
+ )
+ // If `v-show=false`, `display: 'none'` should be added to expected
+ if (vnode.dirs) {
+ for (const { dir, value } of vnode.dirs) {
+ // @ts-expect-error only vShow has this internal name
+ if (dir.name === 'show' && !value) {
+ expected.set('display', 'none')
+ }
+ }
+ }
+ if (!isMapEqual(actual, expected)) {
mismatchType = mismatchKey = 'style'
}
} else if (
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
) {
- actual = el.hasAttribute(key) && el.getAttribute(key)
+ // #10000 some attrs such as textarea.value can't be get by `hasAttribute`
+ actual = el.hasAttribute(key)
+ ? el.getAttribute(key)
+ : key in el
+ ? el[key as keyof typeof el]
+ : ''
expected = isBooleanAttr(key)
? includeBooleanAttr(clientValue)
? ''
: false
: clientValue == null
- ? false
+ ? ''
: String(clientValue)
if (actual !== expected) {
mismatchType = `attribute`
}
return true
}
+
+function toStyleMap(str: string): Map<string, string> {
+ const styleMap: Map<string, string> = new Map()
+ for (const item of str.split(';')) {
+ let [key, value] = item.split(':')
+ key = key?.trim()
+ value = value?.trim()
+ if (key && value) {
+ styleMap.set(key, value)
+ }
+ }
+ return styleMap
+}
+
+function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
+ if (a.size !== b.size) {
+ return false
+ }
+ for (const [key, value] of a) {
+ if (value !== b.get(key)) {
+ return false
+ }
+ }
+ return true
+}