test(ssr): test rendering vnode elements

This commit is contained in:
Evan You 2020-01-29 17:36:06 -05:00
parent 8cdaf28515
commit eaf414f063
5 changed files with 118 additions and 27 deletions

View File

@ -104,13 +104,14 @@ export { registerRuntimeCompiler } from './component'
// SSR ------------------------------------------------------------------------- // SSR -------------------------------------------------------------------------
import { createComponentInstance, setupComponent } from './component' import { createComponentInstance, setupComponent } from './component'
import { renderComponentRoot } from './componentRenderUtils' import { renderComponentRoot } from './componentRenderUtils'
import { normalizeVNode } from './vnode' import { isVNode, normalizeVNode } from './vnode'
// SSR utils are only exposed in cjs builds. // SSR utils are only exposed in cjs builds.
const _ssrUtils = { const _ssrUtils = {
createComponentInstance, createComponentInstance,
setupComponent, setupComponent,
renderComponentRoot, renderComponentRoot,
isVNode,
normalizeVNode normalizeVNode
} }

View File

@ -55,6 +55,17 @@ describe('ssr: renderProps', () => {
}) })
).toBe(` readonly for="foobar"`) ).toBe(` readonly for="foobar"`)
}) })
test('preserve name on custom element', () => {
expect(
renderProps(
{
fooBar: 'ok'
},
'my-el'
)
).toBe(` fooBar="ok"`)
})
}) })
describe('ssr: renderClass', () => { describe('ssr: renderClass', () => {

View File

@ -1,5 +1,5 @@
import { createApp, h } from 'vue' import { createApp, h, createCommentVNode } from 'vue'
import { renderToString, renderComponent, renderSlot } from '../src' import { renderToString, renderComponent, renderSlot, escapeHtml } from '../src'
describe('ssr: renderToString', () => { describe('ssr: renderToString', () => {
describe('components', () => { describe('components', () => {
@ -251,21 +251,82 @@ describe('ssr: renderToString', () => {
}) })
}) })
describe('vnode element', () => {
test('props', async () => {
expect(
await renderToString(
h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
)
).toBe(`<div id="foo&amp;" class="bar baz">hello</div>`)
})
test('text children', async () => {
expect(await renderToString(h('div', 'hello'))).toBe(`<div>hello</div>`)
})
test('array children', async () => {
expect(
await renderToString(
h('div', [
'foo',
h('span', 'bar'),
[h('span', 'baz')],
createCommentVNode('qux')
])
)
).toBe(
`<div>foo<span>bar</span><!----><span>baz</span><!----><!--qux--></div>`
)
})
test('void elements', async () => {
expect(await renderToString(h('input'))).toBe(`<input>`)
})
test('innerHTML', async () => {
expect(
await renderToString(
h(
'div',
{
innerHTML: `<span>hello</span>`
},
'ignored'
)
)
).toBe(`<div><span>hello</span></div>`)
})
test('textContent', async () => {
expect(
await renderToString(
h(
'div',
{
textContent: `<span>hello</span>`
},
'ignored'
)
)
).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
})
test('textarea value', async () => {
expect(
await renderToString(
h(
'textarea',
{
value: `<span>hello</span>`
},
'ignored'
)
)
).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
})
})
describe('scopeId', () => { describe('scopeId', () => {
// TODO // TODO
}) })
describe('vnode', () => {
test('text children', () => {})
test('array children', () => {})
test('void elements', () => {})
test('innerHTML', () => {})
test('textContent', () => {})
test('textarea value', () => {})
})
}) })

View File

@ -8,16 +8,23 @@ import {
isNoUnitNumericStyleProp, isNoUnitNumericStyleProp,
isOn, isOn,
isSSRSafeAttrName, isSSRSafeAttrName,
isBooleanAttr isBooleanAttr,
makeMap
} from '@vue/shared' } from '@vue/shared'
const shouldIgnoreProp = makeMap(`key,ref,innerHTML,textContent`)
export function renderProps( export function renderProps(
props: Record<string, unknown>, props: Record<string, unknown>,
isCustomElement: boolean = false tag?: string
): string { ): string {
let ret = '' let ret = ''
for (const key in props) { for (const key in props) {
if (key === 'key' || key === 'ref' || isOn(key)) { if (
shouldIgnoreProp(key) ||
isOn(key) ||
(tag === 'textarea' && key === 'value')
) {
continue continue
} }
const value = props[key] const value = props[key]
@ -26,8 +33,9 @@ export function renderProps(
} else if (key === 'style') { } else if (key === 'style') {
ret += ` style="${renderStyle(value)}"` ret += ` style="${renderStyle(value)}"`
} else if (value != null) { } else if (value != null) {
const attrKey = isCustomElement const attrKey =
? key tag && tag.indexOf('-') > 0
? key // preserve raw name on custom elements
: propsToAttrMap[key] || key.toLowerCase() : propsToAttrMap[key] || key.toLowerCase()
if (isBooleanAttr(attrKey)) { if (isBooleanAttr(attrKey)) {
if (value !== false) { if (value !== false) {

View File

@ -12,7 +12,8 @@ import {
Portal, Portal,
ShapeFlags, ShapeFlags,
ssrUtils, ssrUtils,
Slot Slot,
createApp
} from 'vue' } from 'vue'
import { import {
isString, isString,
@ -25,6 +26,7 @@ import { renderProps } from './renderProps'
import { escapeHtml } from './ssrUtils' import { escapeHtml } from './ssrUtils'
const { const {
isVNode,
createComponentInstance, createComponentInstance,
setupComponent, setupComponent,
renderComponentRoot, renderComponentRoot,
@ -81,7 +83,15 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
return ret return ret
} }
export async function renderToString(app: App): Promise<string> { export async function renderToString(input: App | VNode): Promise<string> {
if (isVNode(input)) {
return renderAppToString(createApp({ render: () => input }))
} else {
return renderAppToString(input)
}
}
async function renderAppToString(app: App): Promise<string> {
const resolvedBuffer = await renderComponent(app._component, app._props, null) const resolvedBuffer = await renderComponent(app._component, app._props, null)
return unrollBuffer(resolvedBuffer) return unrollBuffer(resolvedBuffer)
} }
@ -143,7 +153,7 @@ function renderComponentSubTree(
return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer) return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
} }
export function renderVNode( function renderVNode(
push: PushFn, push: PushFn,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance | null = null
@ -203,7 +213,7 @@ function renderElement(
// TODO directives // TODO directives
if (props !== null) { if (props !== null) {
openTag += renderProps(props, tag.indexOf(`-`) > 0) openTag += renderProps(props, tag)
} }
if (scopeId !== null) { if (scopeId !== null) {