import { createApp, h, createCommentVNode, withScopeId, resolveComponent, ComponentOptions } from 'vue' import { escapeHtml } from '@vue/shared' import { renderToString, renderComponent } from '../src/renderToString' import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' describe('ssr: renderToString', () => { test('should apply app context', async () => { const app = createApp({ render() { const Foo = resolveComponent('foo') as ComponentOptions return h(Foo) } }) app.component('foo', { render: () => h('div', 'foo') }) const html = await renderToString(app) expect(html).toBe(`
foo
`) }) describe('components', () => { test('vnode components', async () => { expect( await renderToString( createApp({ data() { return { msg: 'hello' } }, render(this: any) { return h('div', this.msg) } }) ) ).toBe(`
hello
`) }) test('optimized components', async () => { expect( await renderToString( createApp({ data() { return { msg: 'hello' } }, ssrRender(ctx, push) { push(`
${ctx.msg}
`) } }) ) ).toBe(`
hello
`) }) test('nested vnode components', async () => { const Child = { props: ['msg'], render(this: any) { return h('div', this.msg) } } expect( await renderToString( createApp({ render() { return h('div', ['parent', h(Child, { msg: 'hello' })]) } }) ) ).toBe(`
parent
hello
`) }) test('nested optimized components', async () => { const Child = { props: ['msg'], ssrRender(ctx: any, push: any) { push(`
${ctx.msg}
`) } } expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push(renderComponent(Child, { msg: 'hello' }, null, parent)) push(`
`) } }) ) ).toBe(`
parent
hello
`) }) test('mixing optimized / vnode components', async () => { const OptimizedChild = { props: ['msg'], ssrRender(ctx: any, push: any) { push(`
${ctx.msg}
`) } } const VNodeChild = { props: ['msg'], render(this: any) { return h('div', this.msg) } } expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push( renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) ) push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) push(`
`) } }) ) ).toBe(`
parent
opt
vnode
`) }) test('nested components with optimized slots', async () => { const Child = { props: ['msg'], ssrRender(ctx: any, push: any, parent: any) { push(`
`) ssrRenderSlot( ctx.$slots, 'default', { msg: 'from slot' }, () => { push(`fallback`) }, push, parent ) push(`
`) } } expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push( renderComponent( Child, { msg: 'hello' }, { // optimized slot using string push default: ({ msg }: any, push: any, p: any) => { push(`${msg}`) }, // important to avoid slots being normalized _compiled: true as any }, parent ) ) push(`
`) } }) ) ).toBe( `
parent
` + `from slot` + `
` ) // test fallback expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push(renderComponent(Child, { msg: 'hello' }, null, parent)) push(`
`) } }) ) ).toBe(`
parent
fallback
`) }) test('nested components with vnode slots', async () => { const Child = { props: ['msg'], ssrRender(ctx: any, push: any, parent: any) { push(`
`) ssrRenderSlot( ctx.$slots, 'default', { msg: 'from slot' }, null, push, parent ) push(`
`) } } expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push( renderComponent( Child, { msg: 'hello' }, { // bailed slots returning raw vnodes default: ({ msg }: any) => { return h('span', msg) } }, parent ) ) push(`
`) } }) ) ).toBe( `
parent
` + `from slot` + `
` ) }) test('async components', async () => { const Child = { // should wait for resovled render context from setup() async setup() { return { msg: 'hello' } }, ssrRender(ctx: any, push: any) { push(`
${ctx.msg}
`) } } expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push(renderComponent(Child, null, null, parent)) push(`
`) } }) ) ).toBe(`
parent
hello
`) }) test('parallel async components', async () => { const OptimizedChild = { props: ['msg'], async setup(props: any) { return { localMsg: props.msg + '!' } }, ssrRender(ctx: any, push: any) { push(`
${ctx.localMsg}
`) } } const VNodeChild = { props: ['msg'], async setup(props: any) { return { localMsg: props.msg + '!' } }, render(this: any) { return h('div', this.localMsg) } } expect( await renderToString( createApp({ ssrRender(_ctx, push, parent) { push(`
parent`) push( renderComponent(OptimizedChild, { msg: 'opt' }, null, parent) ) push(renderComponent(VNodeChild, { msg: 'vnode' }, null, parent)) push(`
`) } }) ) ).toBe(`
parent
opt!
vnode!
`) }) }) describe('vnode element', () => { test('props', async () => { expect( await renderToString( h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello') ) ).toBe(`
hello
`) }) test('text children', async () => { expect(await renderToString(h('div', 'hello'))).toBe(`
hello
`) }) test('array children', async () => { expect( await renderToString( h('div', [ 'foo', h('span', 'bar'), [h('span', 'baz')], createCommentVNode('qux') ]) ) ).toBe( `
foobarbaz
` ) }) test('void elements', async () => { expect(await renderToString(h('input'))).toBe(``) }) test('innerHTML', async () => { expect( await renderToString( h( 'div', { innerHTML: `hello` }, 'ignored' ) ) ).toBe(`
hello
`) }) test('textContent', async () => { expect( await renderToString( h( 'div', { textContent: `hello` }, 'ignored' ) ) ).toBe(`
${escapeHtml(`hello`)}
`) }) test('textarea value', async () => { expect( await renderToString( h( 'textarea', { value: `hello` }, 'ignored' ) ) ).toBe(``) }) }) describe('scopeId', () => { // note: here we are only testing scopeId handling for vdom serialization. // compiled srr render functions will include scopeId directly in strings. const withId = withScopeId('data-v-test') const withChildId = withScopeId('data-v-child') test('basic', async () => { expect( await renderToString( withId(() => { return h('div') })() ) ).toBe(`
`) }) test('with slots', async () => { const Child = { __scopeId: 'data-v-child', render: withChildId(function(this: any) { return h('div', this.$slots.default()) }) } const Parent = { __scopeId: 'data-v-test', render: withId(() => { return h(Child, null, { default: withId(() => h('span', 'slot')) }) }) } expect(await renderToString(h(Parent))).toBe( `
slot
` ) }) }) })