From 2475ea0cb771a631f0e53807ce10eabc9d0ec3a3 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 13 Nov 2025 16:24:01 +0800 Subject: [PATCH] test: enhance hydration tests for CSS variable handling and mismatch scenarios --- .../runtime-vapor/__tests__/hydration.spec.ts | 138 +++++++++--------- packages/runtime-vapor/src/dom/prop.ts | 106 ++++++++++++-- 2 files changed, 164 insertions(+), 80 deletions(-) diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 7094a0f60a..00faacb009 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1,7 +1,14 @@ import { + child, + createComponent, createVaporSSRApp, defineVaporAsyncComponent, + defineVaporComponent, delegateEvents, + renderEffect, + setStyle, + template, + useVaporCssVars, } from '../src' import { defineAsyncComponent, nextTick, reactive, ref } from '@vue/runtime-dom' import { isString } from '@vue/shared' @@ -4437,58 +4444,55 @@ describe('mismatch handling', () => { expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() }) - test.todo('should not warn css v-bind', () => { - // const container = document.createElement('div') - // container.innerHTML = `
` - // const app = createSSRApp({ - // setup() { - // useCssVars(() => ({ - // foo: 'red', - // })) - // return () => h('div', { style: { color: 'var(--foo)' } }) - // }, - // }) - // app.mount(container) - // expect(`Hydration style mismatch`).not.toHaveBeenWarned() + test('should not warn css v-bind', async () => { + const container = document.createElement('div') + container.innerHTML = `
` + const app = createVaporSSRApp({ + setup() { + useVaporCssVars(() => ({ foo: 'red' })) + const n0 = template('
', true)() as any + renderEffect(() => setStyle(n0, { color: 'var(--foo)' })) + return n0 + }, + }) + app.mount(container) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() }) - test.todo( - 'css vars should only be added to expected on component root dom', - () => { - // const container = document.createElement('div') - // container.innerHTML = `
` - // const app = createSSRApp({ - // setup() { - // useCssVars(() => ({ - // foo: 'red', - // })) - // return () => - // h('div', null, [h('div', { style: { color: 'var(--foo)' } })]) - // }, - // }) - // app.mount(container) - // expect(`Hydration style mismatch`).not.toHaveBeenWarned() - }, - ) + test('css vars should only be added to expected on component root dom', () => { + const container = document.createElement('div') + container.innerHTML = `
` + const app = createVaporSSRApp({ + setup() { + useVaporCssVars(() => ({ foo: 'red' })) + const n0 = template('
', true)() as any + const n1 = child(n0) as any + renderEffect(() => setStyle(n1, { color: 'var(--foo)' })) + return n0 + }, + }) + app.mount(container) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() + }) - test.todo('css vars support fallthrough', () => { - // const container = document.createElement('div') - // container.innerHTML = `
` - // const app = createSSRApp({ - // setup() { - // useCssVars(() => ({ - // foo: 'red', - // })) - // return () => h(Child) - // }, - // }) - // const Child = { - // setup() { - // return () => h('div', { style: 'padding: 4px' }) - // }, - // } - // app.mount(container) - // expect(`Hydration style mismatch`).not.toHaveBeenWarned() + test('css vars support fallthrough', () => { + const container = document.createElement('div') + container.innerHTML = `
` + const app = createVaporSSRApp({ + setup() { + useVaporCssVars(() => ({ foo: 'red' })) + return createComponent(Child) + }, + }) + const Child = defineVaporComponent({ + setup() { + const n0 = template('
', true)() as any + renderEffect(() => setStyle(n0, { padding: '4px' })) + return n0 + }, + }) + app.mount(container) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() }) // vapor directive does not have a created hook @@ -4510,24 +4514,24 @@ describe('mismatch handling', () => { // expect(`Hydration style mismatch`).not.toHaveBeenWarned() }) - test.todo('escape css var name', () => { - // const container = document.createElement('div') - // container.innerHTML = `
` - // const app = createSSRApp({ - // setup() { - // useCssVars(() => ({ - // 'foo.bar': 'red', - // })) - // return () => h(Child) - // }, - // }) - // const Child = { - // setup() { - // return () => h('div', { style: 'padding: 4px' }) - // }, - // } - // app.mount(container) - // expect(`Hydration style mismatch`).not.toHaveBeenWarned() + test('escape css var name', () => { + const container = document.createElement('div') + container.innerHTML = `
` + const app = createVaporSSRApp({ + setup() { + useVaporCssVars(() => ({ 'foo.bar': 'red' })) + return createComponent(Child) + }, + }) + const Child = defineVaporComponent({ + setup() { + const n0 = template('
', true)() as any + renderEffect(() => setStyle(n0, { padding: '4px' })) + return n0 + }, + }) + app.mount(container) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() }) }) diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index c7f9631509..4b067fa240 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -2,11 +2,13 @@ import { type NormalizedStyle, camelize, canSetValueDirectly, + getEscapedCssVarName, includeBooleanAttr, isArray, isOn, isString, normalizeClass, + normalizeCssVarValue, normalizeStyle, parseStringStyle, stringifyStyle, @@ -14,6 +16,7 @@ import { } from '@vue/shared' import { on } from './event' import { + type GenericComponentInstance, MismatchTypes, currentInstance, getAttributeMismatch, @@ -23,6 +26,7 @@ import { isValidHtmlOrSvgAttribute, mergeProps, patchStyle, + queuePostFlushCb, shouldSetAsProp, toClassSet, toStyleMap, @@ -38,7 +42,7 @@ import { isVaporComponent, } from '../component' import { isHydrating, logMismatchError } from './hydration' -import type { Block } from '../block' +import { type Block, normalizeBlock } from '../block' import type { VaporElement } from '../apiDefineVaporCustomElement' type TargetElement = Element & { @@ -224,6 +228,21 @@ function setClassIncremental(el: any, value: any): void { } } +/** + * dev only + * if a component uses style v-bind, or an el's style contains style vars (potentially + * fallthrough from parent components), then style matching checks must be deferred until + * after hydration (when instance.block is set). At this point, it can be confirmed + * whether the el is at the root level of the component. + */ +function shouldDeferCheckStyleMismatch(el: TargetElement): boolean { + return ( + __DEV__ && + (!!currentInstance!.getCssVars || + Object.values((el as HTMLElement).style).some(v => v.startsWith('--'))) + ) +} + export function setStyle(el: TargetElement, value: any): void { if (el.$root) { setStyleIncremental(el, value) @@ -231,11 +250,22 @@ export function setStyle(el: TargetElement, value: any): void { const normalizedValue = normalizeStyle(value) if ( (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && - isHydrating && - !styleHasMismatch(el, value, normalizedValue, false) + isHydrating ) { - el.$sty = normalizedValue - return + if (shouldDeferCheckStyleMismatch(el)) { + const instance = currentInstance as VaporComponentInstance + queuePostFlushCb(() => { + if (!styleHasMismatch(el, value, normalizedValue, false, instance)) { + el.$sty = normalizedValue + return + } + patchStyle(el, el.$sty, (el.$sty = normalizedValue)) + }) + return + } else if (!styleHasMismatch(el, value, normalizedValue, false)) { + el.$sty = normalizedValue + return + } } patchStyle(el, el.$sty, (el.$sty = normalizedValue)) @@ -248,13 +278,21 @@ function setStyleIncremental(el: any, value: any): NormalizedStyle | undefined { ? parseStringStyle(value) : (normalizeStyle(value) as NormalizedStyle | undefined) - if ( - (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && - isHydrating && - !styleHasMismatch(el, value, normalizedValue, true) - ) { - el[cacheKey] = normalizedValue - return + if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) { + if (shouldDeferCheckStyleMismatch(el)) { + const instance = currentInstance as VaporComponentInstance + queuePostFlushCb(() => { + if (!styleHasMismatch(el, value, normalizedValue, true, instance)) { + el[cacheKey] = normalizedValue + return + } + patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue)) + }) + return + } else if (!styleHasMismatch(el, value, normalizedValue, true)) { + el[cacheKey] = normalizedValue + return + } } patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue)) @@ -554,6 +592,7 @@ function styleHasMismatch( value: any, normalizedValue: string | NormalizedStyle | undefined, isIncremental: boolean, + instance = currentInstance, ): boolean { const actual = el.getAttribute('style') const actualStyleMap = toStyleMap(actual || '') @@ -565,7 +604,10 @@ function styleHasMismatch( expectedStyleMap.set('display', 'none') } - // TODO: handle css vars + // handle css vars + if (instance) { + resolveCssVars(instance as VaporComponentInstance, el, expectedStyleMap) + } let hasMismatch: boolean = false if (isIncremental) { @@ -588,6 +630,44 @@ function styleHasMismatch( return false } +/** + * dev only + */ +function resolveCssVars( + instance: VaporComponentInstance, + block: Block, + expectedMap: Map, +): void { + if (__DEV__ && !instance.isMounted) { + throw new Error( + 'resolveCssVars should NOT be called before component is mounted', + ) + } + + const rootBlocks = normalizeBlock(instance) + if ( + (instance as GenericComponentInstance).getCssVars && + normalizeBlock(block).every(b => rootBlocks.includes(b)) + ) { + const cssVars = (instance as GenericComponentInstance).getCssVars!() + for (const key in cssVars) { + const value = normalizeCssVarValue(cssVars[key]) + expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value) + } + } + + if ( + normalizeBlock(block).every(b => rootBlocks.includes(b)) && + instance.parent + ) { + resolveCssVars( + instance.parent as VaporComponentInstance, + instance.block, + expectedMap, + ) + } +} + function attributeHasMismatch(el: any, key: string, value: any): boolean { if (isValidHtmlOrSvgAttribute(el, key)) { const { actual, expected } = getAttributeMismatch(el, key, value) -- 2.47.3