Skip to content

Commit 6e06810

Browse files
committed
test(ssr): tests for utils and props rendering
1 parent 730d329 commit 6e06810

File tree

9 files changed

+177
-34
lines changed

9 files changed

+177
-34
lines changed

packages/runtime-core/src/vnode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function createVNode(
223223
if (klass != null && !isString(klass)) {
224224
props.class = normalizeClass(klass)
225225
}
226-
if (style != null) {
226+
if (isObject(style)) {
227227
// reactive state objects need to be cloned since they are likely to be
228228
// mutated
229229
if (isReactive(style) && !isArray(style)) {

packages/server-renderer/__tests__/escape.spec.ts

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,124 @@
1-
describe('ssr: render props', () => {
2-
test('class', () => {})
1+
import { renderProps, renderClass, renderStyle } from '../src'
32

4-
test('style', () => {
5-
// only render numbers for properties that allow no unit numbers
3+
describe('ssr: renderProps', () => {
4+
test('ignore reserved props', () => {
5+
expect(
6+
renderProps({
7+
key: 1,
8+
ref: () => {},
9+
onClick: () => {}
10+
})
11+
).toBe('')
612
})
713

8-
test('normal attrs', () => {})
14+
test('normal attrs', () => {
15+
expect(
16+
renderProps({
17+
id: 'foo',
18+
title: 'bar'
19+
})
20+
).toBe(` id="foo" title="bar"`)
21+
})
22+
23+
test('escape attrs', () => {
24+
expect(
25+
renderProps({
26+
id: '"><script'
27+
})
28+
).toBe(` id="&quot;&gt;&lt;script"`)
29+
})
30+
31+
test('boolean attrs', () => {
32+
expect(
33+
renderProps({
34+
checked: true,
35+
multiple: false
36+
})
37+
).toBe(` checked`) // boolean attr w/ false should be ignored
38+
})
39+
40+
test('ignore falsy values', () => {
41+
expect(
42+
renderProps({
43+
foo: false,
44+
title: null,
45+
baz: undefined
46+
})
47+
).toBe(` foo="false"`) // non boolean should render `false` as is
48+
})
49+
50+
test('props to attrs', () => {
51+
expect(
52+
renderProps({
53+
readOnly: true, // simple lower case conversion
54+
htmlFor: 'foobar' // special cases
55+
})
56+
).toBe(` readonly for="foobar"`)
57+
})
58+
})
959

10-
test('boolean attrs', () => {})
60+
describe('ssr: renderClass', () => {
61+
test('via renderProps', () => {
62+
expect(
63+
renderProps({
64+
class: ['foo', 'bar']
65+
})
66+
).toBe(` class="foo bar"`)
67+
})
68+
69+
test('standalone', () => {
70+
expect(renderClass(`foo`)).toBe(`foo`)
71+
expect(renderClass([`foo`, `bar`])).toBe(`foo bar`)
72+
expect(renderClass({ foo: true, bar: false })).toBe(`foo`)
73+
expect(renderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
74+
})
75+
76+
test('escape class values', () => {
77+
expect(renderClass(`"><script`)).toBe(`&quot;&gt;&lt;script`)
78+
})
79+
})
1180

12-
test('enumerated attrs', () => {})
81+
describe('ssr: renderStyle', () => {
82+
test('via renderProps', () => {
83+
expect(
84+
renderProps({
85+
style: {
86+
color: 'red'
87+
}
88+
})
89+
).toBe(` style="color:red;"`)
90+
})
1391

14-
test('ignore falsy values', () => {})
92+
test('standalone', () => {
93+
expect(renderStyle(`color:red`)).toBe(`color:red`)
94+
expect(
95+
renderStyle({
96+
color: `red`
97+
})
98+
).toBe(`color:red;`)
99+
expect(
100+
renderStyle([
101+
{ color: `red` },
102+
{ fontSize: `15px` } // case conversion
103+
])
104+
).toBe(`color:red;font-size:15px;`)
105+
})
15106

16-
test('props to attrs', () => {})
107+
test('number handling', () => {
108+
expect(
109+
renderStyle({
110+
fontSize: 15, // should be ignored since font-size requires unit
111+
opacity: 0.5
112+
})
113+
).toBe(`opacity:0.5;`)
114+
})
17115

18-
test('ignore non-renderable props', () => {})
116+
test('escape inline CSS', () => {
117+
expect(renderStyle(`"><script`)).toBe(`&quot;&gt;&lt;script`)
118+
expect(
119+
renderStyle({
120+
color: `"><script`
121+
})
122+
).toBe(`color:&quot;&gt;&lt;script;`)
123+
})
19124
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { escapeHtml, interpolate } from '../src'
2+
3+
test('ssr: escapeHTML', () => {
4+
expect(escapeHtml(`foo`)).toBe(`foo`)
5+
expect(escapeHtml(true)).toBe(`true`)
6+
expect(escapeHtml(false)).toBe(`false`)
7+
expect(escapeHtml(`a && b`)).toBe(`a &amp;&amp; b`)
8+
expect(escapeHtml(`"foo"`)).toBe(`&quot;foo&quot;`)
9+
expect(escapeHtml(`'bar'`)).toBe(`&#39;bar&#39;`)
10+
expect(escapeHtml(`<div>`)).toBe(`&lt;div&gt;`)
11+
})
12+
13+
test('ssr: interpolate', () => {
14+
expect(interpolate(0)).toBe(`0`)
15+
expect(interpolate(`foo`)).toBe(`foo`)
16+
expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`)
17+
// should escape interpolated values
18+
expect(interpolate([1, 2, 3])).toBe(
19+
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
20+
)
21+
expect(
22+
interpolate({
23+
foo: 1,
24+
bar: `<div>`
25+
})
26+
).toBe(
27+
escapeHtml(
28+
JSON.stringify(
29+
{
30+
foo: 1,
31+
bar: `<div>`
32+
},
33+
null,
34+
2
35+
)
36+
)
37+
)
38+
})

packages/server-renderer/src/index.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
import { toDisplayString } from 'vue'
2-
import { escape } from './escape'
3-
4-
export { escape }
5-
6-
export function interpolate(value: unknown) {
7-
return escape(toDisplayString(value))
8-
}
9-
101
export { renderToString, renderComponent, renderSlot } from './renderToString'
112
export { renderClass, renderStyle, renderProps } from './renderProps'
3+
export { escapeHtml, interpolate } from './ssrUtils'

packages/server-renderer/src/renderProps.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { escape } from './escape'
1+
import { escapeHtml } from './ssrUtils'
22
import {
33
normalizeClass,
44
normalizeStyle,
@@ -9,7 +9,7 @@ import {
99
isOn,
1010
isSSRSafeAttrName,
1111
isBooleanAttr
12-
} from '@vue/shared/src'
12+
} from '@vue/shared'
1313

1414
export function renderProps(
1515
props: Record<string, unknown>,
@@ -34,21 +34,24 @@ export function renderProps(
3434
ret += ` ${attrKey}`
3535
}
3636
} else if (isSSRSafeAttrName(attrKey)) {
37-
ret += ` ${attrKey}="${escape(value)}"`
37+
ret += ` ${attrKey}="${escapeHtml(value)}"`
3838
}
3939
}
4040
}
4141
return ret
4242
}
4343

4444
export function renderClass(raw: unknown): string {
45-
return escape(normalizeClass(raw))
45+
return escapeHtml(normalizeClass(raw))
4646
}
4747

4848
export function renderStyle(raw: unknown): string {
4949
if (!raw) {
5050
return ''
5151
}
52+
if (isString(raw)) {
53+
return escapeHtml(raw)
54+
}
5255
const styles = normalizeStyle(raw)
5356
let ret = ''
5457
for (const key in styles) {
@@ -62,5 +65,5 @@ export function renderStyle(raw: unknown): string {
6265
ret += `${normalizedKey}:${value};`
6366
}
6467
}
65-
return escape(ret)
68+
return escapeHtml(ret)
6669
}

packages/server-renderer/src/renderToString.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
isVoidTag
2323
} from '@vue/shared'
2424
import { renderProps } from './renderProps'
25-
import { escape } from './escape'
25+
import { escapeHtml } from './ssrUtils'
2626

2727
const {
2828
createComponentInstance,
@@ -105,7 +105,7 @@ function renderComponentVNode(
105105
const instance = createComponentInstance(vnode, parentComponent)
106106
const res = setupComponent(
107107
instance,
108-
null /* parentSuspense */,
108+
null /* parentSuspense (no need to track for SSR) */,
109109
true /* isSSR */
110110
)
111111
if (isPromise(res)) {
@@ -225,15 +225,15 @@ function renderElement(
225225
push(props.innerHTML)
226226
} else if (props.textContent) {
227227
hasChildrenOverride = true
228-
push(escape(props.textContent))
228+
push(escapeHtml(props.textContent))
229229
} else if (tag === 'textarea' && props.value) {
230230
hasChildrenOverride = true
231-
push(escape(props.value))
231+
push(escapeHtml(props.value))
232232
}
233233
}
234234
if (!hasChildrenOverride) {
235235
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
236-
push(escape(children as string))
236+
push(escapeHtml(children as string))
237237
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
238238
renderVNodeChildren(
239239
push,

packages/server-renderer/src/escape.ts renamed to packages/server-renderer/src/ssrUtils.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { toDisplayString } from '@vue/shared'
2+
13
const escapeRE = /["'&<>]/
24

3-
export function escape(string: unknown) {
5+
export function escapeHtml(string: unknown) {
46
const str = '' + string
57
const match = escapeRE.exec(str)
68

@@ -43,3 +45,7 @@ export function escape(string: unknown) {
4345

4446
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
4547
}
48+
49+
export function interpolate(value: unknown) {
50+
return escapeHtml(toDisplayString(value))
51+
}

packages/shared/src/domAttrConfig.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs)
1515
// The full list is needed during SSR to produce the correct initial markup.
1616
export const isBooleanAttr = /*#__PURE__*/ makeMap(
1717
specialBooleanAttrs +
18-
`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,ismap,` +
19-
`loop,nomodule,open,required,reversed,scoped,seamless,` +
18+
`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
19+
`loop,open,required,reversed,scoped,seamless,` +
2020
`checked,muted,multiple,selected`
2121
)
2222

0 commit comments

Comments
 (0)