test(ssr): tests for utils and props rendering

This commit is contained in:
Evan You 2020-01-29 15:10:45 -05:00
parent 730d329f79
commit 6e06810add
9 changed files with 178 additions and 35 deletions

View File

@ -223,7 +223,7 @@ export function createVNode(
if (klass != null && !isString(klass)) { if (klass != null && !isString(klass)) {
props.class = normalizeClass(klass) props.class = normalizeClass(klass)
} }
if (style != null) { if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be // reactive state objects need to be cloned since they are likely to be
// mutated // mutated
if (isReactive(style) && !isArray(style)) { if (isReactive(style) && !isArray(style)) {

View File

@ -1 +0,0 @@
test('ssr: escape HTML', () => {})

View File

@ -1,19 +1,124 @@
describe('ssr: render props', () => { import { renderProps, renderClass, renderStyle } from '../src'
test('class', () => {})
test('style', () => { describe('ssr: renderProps', () => {
// only render numbers for properties that allow no unit numbers test('ignore reserved props', () => {
expect(
renderProps({
key: 1,
ref: () => {},
onClick: () => {}
})
).toBe('')
}) })
test('normal attrs', () => {}) test('normal attrs', () => {
expect(
renderProps({
id: 'foo',
title: 'bar'
})
).toBe(` id="foo" title="bar"`)
})
test('boolean attrs', () => {}) test('escape attrs', () => {
expect(
renderProps({
id: '"><script'
})
).toBe(` id="&quot;&gt;&lt;script"`)
})
test('enumerated attrs', () => {}) test('boolean attrs', () => {
expect(
renderProps({
checked: true,
multiple: false
})
).toBe(` checked`) // boolean attr w/ false should be ignored
})
test('ignore falsy values', () => {}) test('ignore falsy values', () => {
expect(
renderProps({
foo: false,
title: null,
baz: undefined
})
).toBe(` foo="false"`) // non boolean should render `false` as is
})
test('props to attrs', () => {}) test('props to attrs', () => {
expect(
test('ignore non-renderable props', () => {}) renderProps({
readOnly: true, // simple lower case conversion
htmlFor: 'foobar' // special cases
})
).toBe(` readonly for="foobar"`)
})
})
describe('ssr: renderClass', () => {
test('via renderProps', () => {
expect(
renderProps({
class: ['foo', 'bar']
})
).toBe(` class="foo bar"`)
})
test('standalone', () => {
expect(renderClass(`foo`)).toBe(`foo`)
expect(renderClass([`foo`, `bar`])).toBe(`foo bar`)
expect(renderClass({ foo: true, bar: false })).toBe(`foo`)
expect(renderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
})
test('escape class values', () => {
expect(renderClass(`"><script`)).toBe(`&quot;&gt;&lt;script`)
})
})
describe('ssr: renderStyle', () => {
test('via renderProps', () => {
expect(
renderProps({
style: {
color: 'red'
}
})
).toBe(` style="color:red;"`)
})
test('standalone', () => {
expect(renderStyle(`color:red`)).toBe(`color:red`)
expect(
renderStyle({
color: `red`
})
).toBe(`color:red;`)
expect(
renderStyle([
{ color: `red` },
{ fontSize: `15px` } // case conversion
])
).toBe(`color:red;font-size:15px;`)
})
test('number handling', () => {
expect(
renderStyle({
fontSize: 15, // should be ignored since font-size requires unit
opacity: 0.5
})
).toBe(`opacity:0.5;`)
})
test('escape inline CSS', () => {
expect(renderStyle(`"><script`)).toBe(`&quot;&gt;&lt;script`)
expect(
renderStyle({
color: `"><script`
})
).toBe(`color:&quot;&gt;&lt;script;`)
})
}) })

View File

@ -0,0 +1,38 @@
import { escapeHtml, interpolate } from '../src'
test('ssr: escapeHTML', () => {
expect(escapeHtml(`foo`)).toBe(`foo`)
expect(escapeHtml(true)).toBe(`true`)
expect(escapeHtml(false)).toBe(`false`)
expect(escapeHtml(`a && b`)).toBe(`a &amp;&amp; b`)
expect(escapeHtml(`"foo"`)).toBe(`&quot;foo&quot;`)
expect(escapeHtml(`'bar'`)).toBe(`&#39;bar&#39;`)
expect(escapeHtml(`<div>`)).toBe(`&lt;div&gt;`)
})
test('ssr: interpolate', () => {
expect(interpolate(0)).toBe(`0`)
expect(interpolate(`foo`)).toBe(`foo`)
expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`)
// should escape interpolated values
expect(interpolate([1, 2, 3])).toBe(
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
)
expect(
interpolate({
foo: 1,
bar: `<div>`
})
).toBe(
escapeHtml(
JSON.stringify(
{
foo: 1,
bar: `<div>`
},
null,
2
)
)
)
})

View File

@ -1,11 +1,3 @@
import { toDisplayString } from 'vue'
import { escape } from './escape'
export { escape }
export function interpolate(value: unknown) {
return escape(toDisplayString(value))
}
export { renderToString, renderComponent, renderSlot } from './renderToString' export { renderToString, renderComponent, renderSlot } from './renderToString'
export { renderClass, renderStyle, renderProps } from './renderProps' export { renderClass, renderStyle, renderProps } from './renderProps'
export { escapeHtml, interpolate } from './ssrUtils'

View File

@ -1,4 +1,4 @@
import { escape } from './escape' import { escapeHtml } from './ssrUtils'
import { import {
normalizeClass, normalizeClass,
normalizeStyle, normalizeStyle,
@ -9,7 +9,7 @@ import {
isOn, isOn,
isSSRSafeAttrName, isSSRSafeAttrName,
isBooleanAttr isBooleanAttr
} from '@vue/shared/src' } from '@vue/shared'
export function renderProps( export function renderProps(
props: Record<string, unknown>, props: Record<string, unknown>,
@ -34,7 +34,7 @@ export function renderProps(
ret += ` ${attrKey}` ret += ` ${attrKey}`
} }
} else if (isSSRSafeAttrName(attrKey)) { } else if (isSSRSafeAttrName(attrKey)) {
ret += ` ${attrKey}="${escape(value)}"` ret += ` ${attrKey}="${escapeHtml(value)}"`
} }
} }
} }
@ -42,13 +42,16 @@ export function renderProps(
} }
export function renderClass(raw: unknown): string { export function renderClass(raw: unknown): string {
return escape(normalizeClass(raw)) return escapeHtml(normalizeClass(raw))
} }
export function renderStyle(raw: unknown): string { export function renderStyle(raw: unknown): string {
if (!raw) { if (!raw) {
return '' return ''
} }
if (isString(raw)) {
return escapeHtml(raw)
}
const styles = normalizeStyle(raw) const styles = normalizeStyle(raw)
let ret = '' let ret = ''
for (const key in styles) { for (const key in styles) {
@ -62,5 +65,5 @@ export function renderStyle(raw: unknown): string {
ret += `${normalizedKey}:${value};` ret += `${normalizedKey}:${value};`
} }
} }
return escape(ret) return escapeHtml(ret)
} }

View File

@ -22,7 +22,7 @@ import {
isVoidTag isVoidTag
} from '@vue/shared' } from '@vue/shared'
import { renderProps } from './renderProps' import { renderProps } from './renderProps'
import { escape } from './escape' import { escapeHtml } from './ssrUtils'
const { const {
createComponentInstance, createComponentInstance,
@ -105,7 +105,7 @@ function renderComponentVNode(
const instance = createComponentInstance(vnode, parentComponent) const instance = createComponentInstance(vnode, parentComponent)
const res = setupComponent( const res = setupComponent(
instance, instance,
null /* parentSuspense */, null /* parentSuspense (no need to track for SSR) */,
true /* isSSR */ true /* isSSR */
) )
if (isPromise(res)) { if (isPromise(res)) {
@ -225,15 +225,15 @@ function renderElement(
push(props.innerHTML) push(props.innerHTML)
} else if (props.textContent) { } else if (props.textContent) {
hasChildrenOverride = true hasChildrenOverride = true
push(escape(props.textContent)) push(escapeHtml(props.textContent))
} else if (tag === 'textarea' && props.value) { } else if (tag === 'textarea' && props.value) {
hasChildrenOverride = true hasChildrenOverride = true
push(escape(props.value)) push(escapeHtml(props.value))
} }
} }
if (!hasChildrenOverride) { if (!hasChildrenOverride) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
push(escape(children as string)) push(escapeHtml(children as string))
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
renderVNodeChildren( renderVNodeChildren(
push, push,

View File

@ -1,6 +1,8 @@
import { toDisplayString } from '@vue/shared'
const escapeRE = /["'&<>]/ const escapeRE = /["'&<>]/
export function escape(string: unknown) { export function escapeHtml(string: unknown) {
const str = '' + string const str = '' + string
const match = escapeRE.exec(str) const match = escapeRE.exec(str)
@ -43,3 +45,7 @@ export function escape(string: unknown) {
return lastIndex !== index ? html + str.substring(lastIndex, index) : html return lastIndex !== index ? html + str.substring(lastIndex, index) : html
} }
export function interpolate(value: unknown) {
return escapeHtml(toDisplayString(value))
}

View File

@ -15,8 +15,8 @@ export const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs)
// The full list is needed during SSR to produce the correct initial markup. // The full list is needed during SSR to produce the correct initial markup.
export const isBooleanAttr = /*#__PURE__*/ makeMap( export const isBooleanAttr = /*#__PURE__*/ makeMap(
specialBooleanAttrs + specialBooleanAttrs +
`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,ismap,` + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
`loop,nomodule,open,required,reversed,scoped,seamless,` + `loop,open,required,reversed,scoped,seamless,` +
`checked,muted,multiple,selected` `checked,muted,multiple,selected`
) )