Skip to content

Commit 2475ea0

Browse files
committed
test: enhance hydration tests for CSS variable handling and mismatch scenarios
1 parent cfb5bef commit 2475ea0

File tree

2 files changed

+164
-80
lines changed

2 files changed

+164
-80
lines changed

packages/runtime-vapor/__tests__/hydration.spec.ts

Lines changed: 71 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import {
2+
child,
3+
createComponent,
24
createVaporSSRApp,
35
defineVaporAsyncComponent,
6+
defineVaporComponent,
47
delegateEvents,
8+
renderEffect,
9+
setStyle,
10+
template,
11+
useVaporCssVars,
512
} from '../src'
613
import { defineAsyncComponent, nextTick, reactive, ref } from '@vue/runtime-dom'
714
import { isString } from '@vue/shared'
@@ -4437,58 +4444,55 @@ describe('mismatch handling', () => {
44374444
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
44384445
})
44394446

4440-
test.todo('should not warn css v-bind', () => {
4441-
// const container = document.createElement('div')
4442-
// container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
4443-
// const app = createSSRApp({
4444-
// setup() {
4445-
// useCssVars(() => ({
4446-
// foo: 'red',
4447-
// }))
4448-
// return () => h('div', { style: { color: 'var(--foo)' } })
4449-
// },
4450-
// })
4451-
// app.mount(container)
4452-
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
4447+
test('should not warn css v-bind', async () => {
4448+
const container = document.createElement('div')
4449+
container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
4450+
const app = createVaporSSRApp({
4451+
setup() {
4452+
useVaporCssVars(() => ({ foo: 'red' }))
4453+
const n0 = template('<div></div>', true)() as any
4454+
renderEffect(() => setStyle(n0, { color: 'var(--foo)' }))
4455+
return n0
4456+
},
4457+
})
4458+
app.mount(container)
4459+
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
44534460
})
44544461

4455-
test.todo(
4456-
'css vars should only be added to expected on component root dom',
4457-
() => {
4458-
// const container = document.createElement('div')
4459-
// container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
4460-
// const app = createSSRApp({
4461-
// setup() {
4462-
// useCssVars(() => ({
4463-
// foo: 'red',
4464-
// }))
4465-
// return () =>
4466-
// h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
4467-
// },
4468-
// })
4469-
// app.mount(container)
4470-
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
4471-
},
4472-
)
4462+
test('css vars should only be added to expected on component root dom', () => {
4463+
const container = document.createElement('div')
4464+
container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
4465+
const app = createVaporSSRApp({
4466+
setup() {
4467+
useVaporCssVars(() => ({ foo: 'red' }))
4468+
const n0 = template('<div><div></div></div>', true)() as any
4469+
const n1 = child(n0) as any
4470+
renderEffect(() => setStyle(n1, { color: 'var(--foo)' }))
4471+
return n0
4472+
},
4473+
})
4474+
app.mount(container)
4475+
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
4476+
})
44734477

4474-
test.todo('css vars support fallthrough', () => {
4475-
// const container = document.createElement('div')
4476-
// container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
4477-
// const app = createSSRApp({
4478-
// setup() {
4479-
// useCssVars(() => ({
4480-
// foo: 'red',
4481-
// }))
4482-
// return () => h(Child)
4483-
// },
4484-
// })
4485-
// const Child = {
4486-
// setup() {
4487-
// return () => h('div', { style: 'padding: 4px' })
4488-
// },
4489-
// }
4490-
// app.mount(container)
4491-
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
4478+
test('css vars support fallthrough', () => {
4479+
const container = document.createElement('div')
4480+
container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
4481+
const app = createVaporSSRApp({
4482+
setup() {
4483+
useVaporCssVars(() => ({ foo: 'red' }))
4484+
return createComponent(Child)
4485+
},
4486+
})
4487+
const Child = defineVaporComponent({
4488+
setup() {
4489+
const n0 = template('<div></div>', true)() as any
4490+
renderEffect(() => setStyle(n0, { padding: '4px' }))
4491+
return n0
4492+
},
4493+
})
4494+
app.mount(container)
4495+
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
44924496
})
44934497

44944498
// vapor directive does not have a created hook
@@ -4510,24 +4514,24 @@ describe('mismatch handling', () => {
45104514
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
45114515
})
45124516

4513-
test.todo('escape css var name', () => {
4514-
// const container = document.createElement('div')
4515-
// container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
4516-
// const app = createSSRApp({
4517-
// setup() {
4518-
// useCssVars(() => ({
4519-
// 'foo.bar': 'red',
4520-
// }))
4521-
// return () => h(Child)
4522-
// },
4523-
// })
4524-
// const Child = {
4525-
// setup() {
4526-
// return () => h('div', { style: 'padding: 4px' })
4527-
// },
4528-
// }
4529-
// app.mount(container)
4530-
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
4517+
test('escape css var name', () => {
4518+
const container = document.createElement('div')
4519+
container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
4520+
const app = createVaporSSRApp({
4521+
setup() {
4522+
useVaporCssVars(() => ({ 'foo.bar': 'red' }))
4523+
return createComponent(Child)
4524+
},
4525+
})
4526+
const Child = defineVaporComponent({
4527+
setup() {
4528+
const n0 = template('<div></div>', true)() as any
4529+
renderEffect(() => setStyle(n0, { padding: '4px' }))
4530+
return n0
4531+
},
4532+
})
4533+
app.mount(container)
4534+
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
45314535
})
45324536
})
45334537

packages/runtime-vapor/src/dom/prop.ts

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ import {
22
type NormalizedStyle,
33
camelize,
44
canSetValueDirectly,
5+
getEscapedCssVarName,
56
includeBooleanAttr,
67
isArray,
78
isOn,
89
isString,
910
normalizeClass,
11+
normalizeCssVarValue,
1012
normalizeStyle,
1113
parseStringStyle,
1214
stringifyStyle,
1315
toDisplayString,
1416
} from '@vue/shared'
1517
import { on } from './event'
1618
import {
19+
type GenericComponentInstance,
1720
MismatchTypes,
1821
currentInstance,
1922
getAttributeMismatch,
@@ -23,6 +26,7 @@ import {
2326
isValidHtmlOrSvgAttribute,
2427
mergeProps,
2528
patchStyle,
29+
queuePostFlushCb,
2630
shouldSetAsProp,
2731
toClassSet,
2832
toStyleMap,
@@ -38,7 +42,7 @@ import {
3842
isVaporComponent,
3943
} from '../component'
4044
import { isHydrating, logMismatchError } from './hydration'
41-
import type { Block } from '../block'
45+
import { type Block, normalizeBlock } from '../block'
4246
import type { VaporElement } from '../apiDefineVaporCustomElement'
4347

4448
type TargetElement = Element & {
@@ -224,18 +228,44 @@ function setClassIncremental(el: any, value: any): void {
224228
}
225229
}
226230

231+
/**
232+
* dev only
233+
* if a component uses style v-bind, or an el's style contains style vars (potentially
234+
* fallthrough from parent components), then style matching checks must be deferred until
235+
* after hydration (when instance.block is set). At this point, it can be confirmed
236+
* whether the el is at the root level of the component.
237+
*/
238+
function shouldDeferCheckStyleMismatch(el: TargetElement): boolean {
239+
return (
240+
__DEV__ &&
241+
(!!currentInstance!.getCssVars ||
242+
Object.values((el as HTMLElement).style).some(v => v.startsWith('--')))
243+
)
244+
}
245+
227246
export function setStyle(el: TargetElement, value: any): void {
228247
if (el.$root) {
229248
setStyleIncremental(el, value)
230249
} else {
231250
const normalizedValue = normalizeStyle(value)
232251
if (
233252
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
234-
isHydrating &&
235-
!styleHasMismatch(el, value, normalizedValue, false)
253+
isHydrating
236254
) {
237-
el.$sty = normalizedValue
238-
return
255+
if (shouldDeferCheckStyleMismatch(el)) {
256+
const instance = currentInstance as VaporComponentInstance
257+
queuePostFlushCb(() => {
258+
if (!styleHasMismatch(el, value, normalizedValue, false, instance)) {
259+
el.$sty = normalizedValue
260+
return
261+
}
262+
patchStyle(el, el.$sty, (el.$sty = normalizedValue))
263+
})
264+
return
265+
} else if (!styleHasMismatch(el, value, normalizedValue, false)) {
266+
el.$sty = normalizedValue
267+
return
268+
}
239269
}
240270

241271
patchStyle(el, el.$sty, (el.$sty = normalizedValue))
@@ -248,13 +278,21 @@ function setStyleIncremental(el: any, value: any): NormalizedStyle | undefined {
248278
? parseStringStyle(value)
249279
: (normalizeStyle(value) as NormalizedStyle | undefined)
250280

251-
if (
252-
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
253-
isHydrating &&
254-
!styleHasMismatch(el, value, normalizedValue, true)
255-
) {
256-
el[cacheKey] = normalizedValue
257-
return
281+
if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) {
282+
if (shouldDeferCheckStyleMismatch(el)) {
283+
const instance = currentInstance as VaporComponentInstance
284+
queuePostFlushCb(() => {
285+
if (!styleHasMismatch(el, value, normalizedValue, true, instance)) {
286+
el[cacheKey] = normalizedValue
287+
return
288+
}
289+
patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
290+
})
291+
return
292+
} else if (!styleHasMismatch(el, value, normalizedValue, true)) {
293+
el[cacheKey] = normalizedValue
294+
return
295+
}
258296
}
259297

260298
patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
@@ -554,6 +592,7 @@ function styleHasMismatch(
554592
value: any,
555593
normalizedValue: string | NormalizedStyle | undefined,
556594
isIncremental: boolean,
595+
instance = currentInstance,
557596
): boolean {
558597
const actual = el.getAttribute('style')
559598
const actualStyleMap = toStyleMap(actual || '')
@@ -565,7 +604,10 @@ function styleHasMismatch(
565604
expectedStyleMap.set('display', 'none')
566605
}
567606

568-
// TODO: handle css vars
607+
// handle css vars
608+
if (instance) {
609+
resolveCssVars(instance as VaporComponentInstance, el, expectedStyleMap)
610+
}
569611

570612
let hasMismatch: boolean = false
571613
if (isIncremental) {
@@ -588,6 +630,44 @@ function styleHasMismatch(
588630
return false
589631
}
590632

633+
/**
634+
* dev only
635+
*/
636+
function resolveCssVars(
637+
instance: VaporComponentInstance,
638+
block: Block,
639+
expectedMap: Map<string, string>,
640+
): void {
641+
if (__DEV__ && !instance.isMounted) {
642+
throw new Error(
643+
'resolveCssVars should NOT be called before component is mounted',
644+
)
645+
}
646+
647+
const rootBlocks = normalizeBlock(instance)
648+
if (
649+
(instance as GenericComponentInstance).getCssVars &&
650+
normalizeBlock(block).every(b => rootBlocks.includes(b))
651+
) {
652+
const cssVars = (instance as GenericComponentInstance).getCssVars!()
653+
for (const key in cssVars) {
654+
const value = normalizeCssVarValue(cssVars[key])
655+
expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value)
656+
}
657+
}
658+
659+
if (
660+
normalizeBlock(block).every(b => rootBlocks.includes(b)) &&
661+
instance.parent
662+
) {
663+
resolveCssVars(
664+
instance.parent as VaporComponentInstance,
665+
instance.block,
666+
expectedMap,
667+
)
668+
}
669+
}
670+
591671
function attributeHasMismatch(el: any, key: string, value: any): boolean {
592672
if (isValidHtmlOrSvgAttribute(el, key)) {
593673
const { actual, expected } = getAttributeMismatch(el, key, value)

0 commit comments

Comments
 (0)