test(ssr): refactor ssr render tests
Merge renderToString and renderToStream to run the same tests
This commit is contained in:
		
							parent
							
								
									88f6b33d05
								
							
						
					
					
						commit
						8ad1aab068
					
				
							
								
								
									
										719
									
								
								packages/server-renderer/__tests__/render.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										719
									
								
								packages/server-renderer/__tests__/render.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,719 @@
 | 
			
		||||
import {
 | 
			
		||||
  createApp,
 | 
			
		||||
  h,
 | 
			
		||||
  createCommentVNode,
 | 
			
		||||
  withScopeId,
 | 
			
		||||
  resolveComponent,
 | 
			
		||||
  ComponentOptions,
 | 
			
		||||
  ref,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
  createTextVNode,
 | 
			
		||||
  createStaticVNode
 | 
			
		||||
} from 'vue'
 | 
			
		||||
import { escapeHtml } from '@vue/shared'
 | 
			
		||||
import { renderToString } from '../src/renderToString'
 | 
			
		||||
import { renderToStream as _renderToStream } from '../src/renderToStream'
 | 
			
		||||
import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
 | 
			
		||||
import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
 | 
			
		||||
import { Readable } from 'stream'
 | 
			
		||||
 | 
			
		||||
const promisifyStream = (stream: Readable) => {
 | 
			
		||||
  return new Promise<string>((resolve, reject) => {
 | 
			
		||||
    let result = ''
 | 
			
		||||
    stream.on('data', data => {
 | 
			
		||||
      result += data
 | 
			
		||||
    })
 | 
			
		||||
    stream.on('error', () => {
 | 
			
		||||
      reject(result)
 | 
			
		||||
    })
 | 
			
		||||
    stream.on('end', () => {
 | 
			
		||||
      resolve(result)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const renderToStream = (app: any, context?: any) =>
 | 
			
		||||
  promisifyStream(_renderToStream(app, context))
 | 
			
		||||
 | 
			
		||||
// we run the same tests twice, once for renderToString, once for renderToStream
 | 
			
		||||
testRender(`renderToString`, renderToString)
 | 
			
		||||
testRender(`renderToStream`, renderToStream)
 | 
			
		||||
 | 
			
		||||
function testRender(type: string, render: typeof renderToString) {
 | 
			
		||||
  describe(`ssr: ${type}`, () => {
 | 
			
		||||
    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 render(app)
 | 
			
		||||
      expect(html).toBe(`<div>foo</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('components', () => {
 | 
			
		||||
      test('vnode components', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              data() {
 | 
			
		||||
                return { msg: 'hello' }
 | 
			
		||||
              },
 | 
			
		||||
              render(this: any) {
 | 
			
		||||
                return h('div', this.msg)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('option components returning render from setup', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              setup() {
 | 
			
		||||
                const msg = ref('hello')
 | 
			
		||||
                return () => h('div', msg.value)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('setup components returning render from setup', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp(
 | 
			
		||||
              defineComponent(() => {
 | 
			
		||||
                const msg = ref('hello')
 | 
			
		||||
                return () => h('div', msg.value)
 | 
			
		||||
              })
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('optimized components', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              data() {
 | 
			
		||||
                return { msg: 'hello' }
 | 
			
		||||
              },
 | 
			
		||||
              ssrRender(ctx, push) {
 | 
			
		||||
                push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('nested vnode components', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          render(this: any) {
 | 
			
		||||
            return h('div', this.msg)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              render() {
 | 
			
		||||
                return h('div', ['parent', h(Child, { msg: 'hello' })])
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('nested optimized components', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          ssrRender(ctx: any, push: any) {
 | 
			
		||||
            push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('nested template components', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          template: `<div>{{ msg }}</div>`
 | 
			
		||||
        }
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          template: `<div>parent<Child msg="hello" /></div>`
 | 
			
		||||
        })
 | 
			
		||||
        app.component('Child', Child)
 | 
			
		||||
 | 
			
		||||
        expect(await render(app)).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('template components with dynamic class attribute after static', async () => {
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          template: `<div><div class="child" :class="'dynamic'"></div></div>`
 | 
			
		||||
        })
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          `<div><div class="dynamic child"></div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('template components with dynamic class attribute before static', async () => {
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          template: `<div><div :class="'dynamic'" class="child"></div></div>`
 | 
			
		||||
        })
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          `<div><div class="dynamic child"></div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('mixing optimized / vnode / template components', async () => {
 | 
			
		||||
        const OptimizedChild = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          ssrRender(ctx: any, push: any) {
 | 
			
		||||
            push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const VNodeChild = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          render(this: any) {
 | 
			
		||||
            return h('div', this.msg)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const TemplateChild = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          template: `<div>{{ msg }}</div>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(
 | 
			
		||||
                    OptimizedChild,
 | 
			
		||||
                    { msg: 'opt' },
 | 
			
		||||
                    null,
 | 
			
		||||
                    parent
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
 | 
			
		||||
                )
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(
 | 
			
		||||
                    TemplateChild,
 | 
			
		||||
                    { msg: 'template' },
 | 
			
		||||
                    null,
 | 
			
		||||
                    parent
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(
 | 
			
		||||
          `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('async components', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          // should wait for resolved render context from setup()
 | 
			
		||||
          async setup() {
 | 
			
		||||
            return {
 | 
			
		||||
              msg: 'hello'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          ssrRender(ctx: any, push: any) {
 | 
			
		||||
            push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(ssrRenderComponent(Child, null, null, parent))
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('parallel async components', async () => {
 | 
			
		||||
        const OptimizedChild = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          async setup(props: any) {
 | 
			
		||||
            return {
 | 
			
		||||
              localMsg: props.msg + '!'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          ssrRender(ctx: any, push: any) {
 | 
			
		||||
            push(`<div>${ctx.localMsg}</div>`)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const VNodeChild = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          async setup(props: any) {
 | 
			
		||||
            return {
 | 
			
		||||
              localMsg: props.msg + '!'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          render(this: any) {
 | 
			
		||||
            return h('div', this.localMsg)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(
 | 
			
		||||
                    OptimizedChild,
 | 
			
		||||
                    { msg: 'opt' },
 | 
			
		||||
                    null,
 | 
			
		||||
                    parent
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
 | 
			
		||||
                )
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('slots', () => {
 | 
			
		||||
      test('nested components with optimized slots', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          ssrRender(ctx: any, push: any, parent: any) {
 | 
			
		||||
            push(`<div class="child">`)
 | 
			
		||||
            ssrRenderSlot(
 | 
			
		||||
              ctx.$slots,
 | 
			
		||||
              'default',
 | 
			
		||||
              { msg: 'from slot' },
 | 
			
		||||
              () => {
 | 
			
		||||
                push(`fallback`)
 | 
			
		||||
              },
 | 
			
		||||
              push,
 | 
			
		||||
              parent
 | 
			
		||||
            )
 | 
			
		||||
            push(`</div>`)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(
 | 
			
		||||
                    Child,
 | 
			
		||||
                    { msg: 'hello' },
 | 
			
		||||
                    {
 | 
			
		||||
                      // optimized slot using string push
 | 
			
		||||
                      default: (({ msg }, push, _p) => {
 | 
			
		||||
                        push(`<span>${msg}</span>`)
 | 
			
		||||
                      }) as SSRSlot,
 | 
			
		||||
                      // important to avoid slots being normalized
 | 
			
		||||
                      _: 1 as any
 | 
			
		||||
                    },
 | 
			
		||||
                    parent
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(
 | 
			
		||||
          `<div>parent<div class="child">` +
 | 
			
		||||
            `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
            `</div></div>`
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        // test fallback
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(
 | 
			
		||||
          `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('nested components with vnode slots', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          ssrRender(ctx: any, push: any, parent: any) {
 | 
			
		||||
            push(`<div class="child">`)
 | 
			
		||||
            ssrRenderSlot(
 | 
			
		||||
              ctx.$slots,
 | 
			
		||||
              'default',
 | 
			
		||||
              { msg: 'from slot' },
 | 
			
		||||
              null,
 | 
			
		||||
              push,
 | 
			
		||||
              parent
 | 
			
		||||
            )
 | 
			
		||||
            push(`</div>`)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              ssrRender(_ctx, push, parent) {
 | 
			
		||||
                push(`<div>parent`)
 | 
			
		||||
                push(
 | 
			
		||||
                  ssrRenderComponent(
 | 
			
		||||
                    Child,
 | 
			
		||||
                    { msg: 'hello' },
 | 
			
		||||
                    {
 | 
			
		||||
                      // bailed slots returning raw vnodes
 | 
			
		||||
                      default: ({ msg }: any) => {
 | 
			
		||||
                        return h('span', msg)
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                    parent
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
                push(`</div>`)
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(
 | 
			
		||||
          `<div>parent<div class="child">` +
 | 
			
		||||
            `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
            `</div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('nested components with template slots', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          template: `<div class="child"><slot msg="from slot"></slot></div>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          components: { Child },
 | 
			
		||||
          template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          `<div>parent<div class="child">` +
 | 
			
		||||
            `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
            `</div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('nested render fn components with template slots', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          props: ['msg'],
 | 
			
		||||
          render(this: any) {
 | 
			
		||||
            return h(
 | 
			
		||||
              'div',
 | 
			
		||||
              {
 | 
			
		||||
                class: 'child'
 | 
			
		||||
              },
 | 
			
		||||
              this.$slots.default({ msg: 'from slot' })
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
 | 
			
		||||
        })
 | 
			
		||||
        app.component('Child', Child)
 | 
			
		||||
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          `<div>parent<div class="child">` +
 | 
			
		||||
            // no comment anchors because slot is used directly as element children
 | 
			
		||||
            `<span>from slot</span>` +
 | 
			
		||||
            `</div></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('template slots forwarding', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          template: `<div><slot/></div>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const Parent = {
 | 
			
		||||
          components: { Child },
 | 
			
		||||
          template: `<Child><slot/></Child>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          components: { Parent },
 | 
			
		||||
          template: `<Parent>hello</Parent>`
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('template slots forwarding, empty slot', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          template: `<div><slot/></div>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const Parent = {
 | 
			
		||||
          components: { Child },
 | 
			
		||||
          template: `<Child><slot/></Child>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          components: { Parent },
 | 
			
		||||
          template: `<Parent></Parent>`
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          // should only have a single fragment
 | 
			
		||||
          `<div><!--[--><!--]--></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('template slots forwarding, empty slot w/ fallback', async () => {
 | 
			
		||||
        const Child = {
 | 
			
		||||
          template: `<div><slot>fallback</slot></div>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const Parent = {
 | 
			
		||||
          components: { Child },
 | 
			
		||||
          template: `<Child><slot/></Child>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const app = createApp({
 | 
			
		||||
          components: { Parent },
 | 
			
		||||
          template: `<Parent></Parent>`
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        expect(await render(app)).toBe(
 | 
			
		||||
          // should only have a single fragment
 | 
			
		||||
          `<div><!--[-->fallback<!--]--></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('vnode element', () => {
 | 
			
		||||
      test('props', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello'))
 | 
			
		||||
        ).toBe(`<div id="foo&" class="bar baz">hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('text children', async () => {
 | 
			
		||||
        expect(await render(h('div', 'hello'))).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('array children', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            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 render(h('input'))).toBe(`<input>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('innerHTML', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            h(
 | 
			
		||||
              'div',
 | 
			
		||||
              {
 | 
			
		||||
                innerHTML: `<span>hello</span>`
 | 
			
		||||
              },
 | 
			
		||||
              'ignored'
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div><span>hello</span></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('textContent', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            h(
 | 
			
		||||
              'div',
 | 
			
		||||
              {
 | 
			
		||||
                textContent: `<span>hello</span>`
 | 
			
		||||
              },
 | 
			
		||||
              'ignored'
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('textarea value', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            h(
 | 
			
		||||
              'textarea',
 | 
			
		||||
              {
 | 
			
		||||
                value: `<span>hello</span>`
 | 
			
		||||
              },
 | 
			
		||||
              'ignored'
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('raw vnode types', () => {
 | 
			
		||||
      test('Text', async () => {
 | 
			
		||||
        expect(await render(createTextVNode('hello <div>'))).toBe(
 | 
			
		||||
          `hello <div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('Comment', async () => {
 | 
			
		||||
        // https://www.w3.org/TR/html52/syntax.html#comments
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            h('div', [
 | 
			
		||||
              createCommentVNode('>foo'),
 | 
			
		||||
              createCommentVNode('->foo'),
 | 
			
		||||
              createCommentVNode('<!--foo-->'),
 | 
			
		||||
              createCommentVNode('--!>foo<!-')
 | 
			
		||||
            ])
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('Static', async () => {
 | 
			
		||||
        const content = `<div id="ok">hello<span>world</span></div>`
 | 
			
		||||
        expect(await render(createStaticVNode(content, 1))).toBe(content)
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    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 render(
 | 
			
		||||
            withId(() => {
 | 
			
		||||
              return h('div')
 | 
			
		||||
            })()
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div data-v-test></div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      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 render(h(Parent))).toBe(
 | 
			
		||||
          `<div data-v-child data-v-test><span data-v-test data-v-child-s>slot</span></div>`
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('integration w/ compiled template', () => {
 | 
			
		||||
      test('render', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await render(
 | 
			
		||||
            createApp({
 | 
			
		||||
              data() {
 | 
			
		||||
                return { msg: 'hello' }
 | 
			
		||||
              },
 | 
			
		||||
              template: `<div>{{ msg }}</div>`
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('handle compiler errors', async () => {
 | 
			
		||||
        await render(
 | 
			
		||||
          // render different content since compilation is cached
 | 
			
		||||
          createApp({ template: `<${type === 'renderToString' ? 'div' : 'p'}` })
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          `Template compilation error: Unexpected EOF in tag.`
 | 
			
		||||
        ).toHaveBeenWarned()
 | 
			
		||||
        expect(`Element is missing end tag`).toHaveBeenWarned()
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('serverPrefetch', async () => {
 | 
			
		||||
      const msg = Promise.resolve('hello')
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        data() {
 | 
			
		||||
          return {
 | 
			
		||||
            msg: ''
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        async serverPrefetch() {
 | 
			
		||||
          this.msg = await msg
 | 
			
		||||
        },
 | 
			
		||||
        render() {
 | 
			
		||||
          return h('div', this.msg)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const html = await render(app)
 | 
			
		||||
      expect(html).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
@ -1,622 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  createApp,
 | 
			
		||||
  h,
 | 
			
		||||
  createCommentVNode,
 | 
			
		||||
  withScopeId,
 | 
			
		||||
  resolveComponent,
 | 
			
		||||
  ComponentOptions,
 | 
			
		||||
  ref,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
  createTextVNode,
 | 
			
		||||
  createStaticVNode
 | 
			
		||||
} from 'vue'
 | 
			
		||||
import { escapeHtml } from '@vue/shared'
 | 
			
		||||
import { renderToStream as _renderToStream } from '../src/renderToStream'
 | 
			
		||||
import { Readable } from 'stream'
 | 
			
		||||
import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
 | 
			
		||||
import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
 | 
			
		||||
 | 
			
		||||
const promisifyStream = (stream: Readable) => {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let result = ''
 | 
			
		||||
    stream.on('data', data => {
 | 
			
		||||
      result += data
 | 
			
		||||
    })
 | 
			
		||||
    stream.on('error', () => {
 | 
			
		||||
      reject(result)
 | 
			
		||||
    })
 | 
			
		||||
    stream.on('end', () => {
 | 
			
		||||
      resolve(result)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const renderToStream = (app: any, context?: any) =>
 | 
			
		||||
  promisifyStream(_renderToStream(app, context))
 | 
			
		||||
 | 
			
		||||
describe('ssr: renderToStream', () => {
 | 
			
		||||
  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 renderToStream(app)
 | 
			
		||||
    expect(html).toBe(`<div>foo</div>`)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('components', () => {
 | 
			
		||||
    test('vnode components', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            data() {
 | 
			
		||||
              return { msg: 'hello' }
 | 
			
		||||
            },
 | 
			
		||||
            render(this: any) {
 | 
			
		||||
              return h('div', this.msg)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('option components returning render from setup', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            setup() {
 | 
			
		||||
              const msg = ref('hello')
 | 
			
		||||
              return () => h('div', msg.value)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('setup components returning render from setup', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp(
 | 
			
		||||
            defineComponent(() => {
 | 
			
		||||
              const msg = ref('hello')
 | 
			
		||||
              return () => h('div', msg.value)
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('optimized components', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            data() {
 | 
			
		||||
              return { msg: 'hello' }
 | 
			
		||||
            },
 | 
			
		||||
            ssrRender(ctx, push) {
 | 
			
		||||
              push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('template components', () => {
 | 
			
		||||
      test('render', async () => {
 | 
			
		||||
        expect(
 | 
			
		||||
          await renderToStream(
 | 
			
		||||
            createApp({
 | 
			
		||||
              data() {
 | 
			
		||||
                return { msg: 'hello' }
 | 
			
		||||
              },
 | 
			
		||||
              template: `<div>{{ msg }}</div>`
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        ).toBe(`<div>hello</div>`)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      test('handle compiler errors', async () => {
 | 
			
		||||
        await renderToStream(createApp({ template: `<` }))
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
          'Template compilation error: Unexpected EOF in tag.\n' +
 | 
			
		||||
            '1  |  <\n' +
 | 
			
		||||
            '   |   ^'
 | 
			
		||||
        ).toHaveBeenWarned()
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested vnode components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        render(this: any) {
 | 
			
		||||
          return h('div', this.msg)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            render() {
 | 
			
		||||
              return h('div', ['parent', h(Child, { msg: 'hello' })])
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested optimized components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested template components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        template: `<div>{{ msg }}</div>`
 | 
			
		||||
      }
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        template: `<div>parent<Child msg="hello" /></div>`
 | 
			
		||||
      })
 | 
			
		||||
      app.component('Child', Child)
 | 
			
		||||
 | 
			
		||||
      expect(await renderToStream(app)).toBe(
 | 
			
		||||
        `<div>parent<div>hello</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('mixing optimized / vnode / template components', async () => {
 | 
			
		||||
      const OptimizedChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const VNodeChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        render(this: any) {
 | 
			
		||||
          return h('div', this.msg)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const TemplateChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        template: `<div>{{ msg }}</div>`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(
 | 
			
		||||
                  TemplateChild,
 | 
			
		||||
                  { msg: 'template' },
 | 
			
		||||
                  null,
 | 
			
		||||
                  parent
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested components with optimized slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any, parent: any) {
 | 
			
		||||
          push(`<div class="child">`)
 | 
			
		||||
          ssrRenderSlot(
 | 
			
		||||
            ctx.$slots,
 | 
			
		||||
            'default',
 | 
			
		||||
            { msg: 'from slot' },
 | 
			
		||||
            () => {
 | 
			
		||||
              push(`fallback`)
 | 
			
		||||
            },
 | 
			
		||||
            push,
 | 
			
		||||
            parent
 | 
			
		||||
          )
 | 
			
		||||
          push(`</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(
 | 
			
		||||
                  Child,
 | 
			
		||||
                  { msg: 'hello' },
 | 
			
		||||
                  {
 | 
			
		||||
                    // optimized slot using string push
 | 
			
		||||
                    default: ({ msg }: any, push: any) => {
 | 
			
		||||
                      push(`<span>${msg}</span>`)
 | 
			
		||||
                    },
 | 
			
		||||
                    // important to avoid slots being normalized
 | 
			
		||||
                    _: 1 as any
 | 
			
		||||
                  },
 | 
			
		||||
                  parent
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      // test fallback
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested components with vnode slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any, parent: any) {
 | 
			
		||||
          push(`<div class="child">`)
 | 
			
		||||
          ssrRenderSlot(
 | 
			
		||||
            ctx.$slots,
 | 
			
		||||
            'default',
 | 
			
		||||
            { msg: 'from slot' },
 | 
			
		||||
            null,
 | 
			
		||||
            push,
 | 
			
		||||
            parent
 | 
			
		||||
          )
 | 
			
		||||
          push(`</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(
 | 
			
		||||
                  Child,
 | 
			
		||||
                  { msg: 'hello' },
 | 
			
		||||
                  {
 | 
			
		||||
                    // bailed slots returning raw vnodes
 | 
			
		||||
                    default: ({ msg }: any) => {
 | 
			
		||||
                      return h('span', msg)
 | 
			
		||||
                    }
 | 
			
		||||
                  },
 | 
			
		||||
                  parent
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested components with template slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        template: `<div class="child"><slot msg="from slot"></slot></div>`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        components: { Child },
 | 
			
		||||
        template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      expect(await renderToStream(app)).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested render fn components with template slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        render(this: any) {
 | 
			
		||||
          return h(
 | 
			
		||||
            'div',
 | 
			
		||||
            {
 | 
			
		||||
              class: 'child'
 | 
			
		||||
            },
 | 
			
		||||
            this.$slots.default({ msg: 'from slot' })
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
 | 
			
		||||
      })
 | 
			
		||||
      app.component('Child', Child)
 | 
			
		||||
 | 
			
		||||
      expect(await renderToStream(app)).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          // no comment anchors because slot is used directly as element children
 | 
			
		||||
          `<span>from slot</span>` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('async components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        // should wait for resolved render context from setup()
 | 
			
		||||
        async setup() {
 | 
			
		||||
          return {
 | 
			
		||||
            msg: 'hello'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(ssrRenderComponent(Child, null, null, parent))
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('parallel async components', async () => {
 | 
			
		||||
      const OptimizedChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        async setup(props: any) {
 | 
			
		||||
          return {
 | 
			
		||||
            localMsg: props.msg + '!'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.localMsg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const VNodeChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        async setup(props: any) {
 | 
			
		||||
          return {
 | 
			
		||||
            localMsg: props.msg + '!'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        render(this: any) {
 | 
			
		||||
          return h('div', this.localMsg)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('vnode element', () => {
 | 
			
		||||
    test('props', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div id="foo&" class="bar baz">hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('text children', async () => {
 | 
			
		||||
      expect(await renderToStream(h('div', 'hello'))).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('array children', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          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 renderToStream(h('input'))).toBe(`<input>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('innerHTML', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          h(
 | 
			
		||||
            'div',
 | 
			
		||||
            {
 | 
			
		||||
              innerHTML: `<span>hello</span>`
 | 
			
		||||
            },
 | 
			
		||||
            'ignored'
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div><span>hello</span></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('textContent', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          h(
 | 
			
		||||
            'div',
 | 
			
		||||
            {
 | 
			
		||||
              textContent: `<span>hello</span>`
 | 
			
		||||
            },
 | 
			
		||||
            'ignored'
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>${escapeHtml(`<span>hello</span>`)}</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('textarea value', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          h(
 | 
			
		||||
            'textarea',
 | 
			
		||||
            {
 | 
			
		||||
              value: `<span>hello</span>`
 | 
			
		||||
            },
 | 
			
		||||
            'ignored'
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<textarea>${escapeHtml(`<span>hello</span>`)}</textarea>`)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('raw vnode types', () => {
 | 
			
		||||
    test('Text', async () => {
 | 
			
		||||
      expect(await renderToStream(createTextVNode('hello <div>'))).toBe(
 | 
			
		||||
        `hello <div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('Comment', async () => {
 | 
			
		||||
      // https://www.w3.org/TR/html52/syntax.html#comments
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToStream(
 | 
			
		||||
          h('div', [
 | 
			
		||||
            createCommentVNode('>foo'),
 | 
			
		||||
            createCommentVNode('->foo'),
 | 
			
		||||
            createCommentVNode('<!--foo-->'),
 | 
			
		||||
            createCommentVNode('--!>foo<!-')
 | 
			
		||||
          ])
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('Static', async () => {
 | 
			
		||||
      const content = `<div id="ok">hello<span>world</span></div>`
 | 
			
		||||
      expect(await renderToStream(createStaticVNode(content, 1))).toBe(content)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  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 renderToStream(
 | 
			
		||||
          withId(() => {
 | 
			
		||||
            return h('div')
 | 
			
		||||
          })()
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div data-v-test></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    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 renderToStream(h(Parent))).toBe(
 | 
			
		||||
        `<div data-v-child data-v-test><span data-v-test data-v-child-s>slot</span></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('serverPrefetch', async () => {
 | 
			
		||||
    const msg = Promise.resolve('hello')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      data() {
 | 
			
		||||
        return {
 | 
			
		||||
          msg: ''
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      async serverPrefetch() {
 | 
			
		||||
        this.msg = await msg
 | 
			
		||||
      },
 | 
			
		||||
      render() {
 | 
			
		||||
        return h('div', this.msg)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    const html = await renderToStream(app)
 | 
			
		||||
    expect(html).toBe(`<div>hello</div>`)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@ -1,621 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  createApp,
 | 
			
		||||
  h,
 | 
			
		||||
  createCommentVNode,
 | 
			
		||||
  withScopeId,
 | 
			
		||||
  resolveComponent,
 | 
			
		||||
  ComponentOptions,
 | 
			
		||||
  ref,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
  createTextVNode,
 | 
			
		||||
  createStaticVNode
 | 
			
		||||
} from 'vue'
 | 
			
		||||
import { escapeHtml } from '@vue/shared'
 | 
			
		||||
import { renderToString } from '../src/renderToString'
 | 
			
		||||
import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot'
 | 
			
		||||
import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
 | 
			
		||||
 | 
			
		||||
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(`<div>foo</div>`)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('components', () => {
 | 
			
		||||
    test('vnode components', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            data() {
 | 
			
		||||
              return { msg: 'hello' }
 | 
			
		||||
            },
 | 
			
		||||
            render(this: any) {
 | 
			
		||||
              return h('div', this.msg)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('option components returning render from setup', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            setup() {
 | 
			
		||||
              const msg = ref('hello')
 | 
			
		||||
              return () => h('div', msg.value)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('setup components returning render from setup', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp(
 | 
			
		||||
            defineComponent(() => {
 | 
			
		||||
              const msg = ref('hello')
 | 
			
		||||
              return () => h('div', msg.value)
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('optimized components', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            data() {
 | 
			
		||||
              return { msg: 'hello' }
 | 
			
		||||
            },
 | 
			
		||||
            ssrRender(ctx, push) {
 | 
			
		||||
              push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    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(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested optimized components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested template components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        template: `<div>{{ msg }}</div>`
 | 
			
		||||
      }
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        template: `<div>parent<Child msg="hello" /></div>`
 | 
			
		||||
      })
 | 
			
		||||
      app.component('Child', Child)
 | 
			
		||||
 | 
			
		||||
      expect(await renderToString(app)).toBe(
 | 
			
		||||
        `<div>parent<div>hello</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('template components with dynamic class attribute after static', async () => {
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        template: `<div><div class="child" :class="'dynamic'"></div></div>`
 | 
			
		||||
      })
 | 
			
		||||
      expect(await renderToString(app)).toBe(
 | 
			
		||||
        `<div><div class="dynamic child"></div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('template components with dynamic class attribute before static', async () => {
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        template: `<div><div :class="'dynamic'" class="child"></div></div>`
 | 
			
		||||
      })
 | 
			
		||||
      expect(await renderToString(app)).toBe(
 | 
			
		||||
        `<div><div class="dynamic child"></div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('mixing optimized / vnode / template components', async () => {
 | 
			
		||||
      const OptimizedChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const VNodeChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        render(this: any) {
 | 
			
		||||
          return h('div', this.msg)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const TemplateChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        template: `<div>{{ msg }}</div>`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(
 | 
			
		||||
                  TemplateChild,
 | 
			
		||||
                  { msg: 'template' },
 | 
			
		||||
                  null,
 | 
			
		||||
                  parent
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div>opt</div><div>vnode</div><div>template</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested components with optimized slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any, parent: any) {
 | 
			
		||||
          push(`<div class="child">`)
 | 
			
		||||
          ssrRenderSlot(
 | 
			
		||||
            ctx.$slots,
 | 
			
		||||
            'default',
 | 
			
		||||
            { msg: 'from slot' },
 | 
			
		||||
            () => {
 | 
			
		||||
              push(`fallback`)
 | 
			
		||||
            },
 | 
			
		||||
            push,
 | 
			
		||||
            parent
 | 
			
		||||
          )
 | 
			
		||||
          push(`</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(
 | 
			
		||||
                  Child,
 | 
			
		||||
                  { msg: 'hello' },
 | 
			
		||||
                  {
 | 
			
		||||
                    // optimized slot using string push
 | 
			
		||||
                    default: (({ msg }, push, _p) => {
 | 
			
		||||
                      push(`<span>${msg}</span>`)
 | 
			
		||||
                    }) as SSRSlot,
 | 
			
		||||
                    // important to avoid slots being normalized
 | 
			
		||||
                    _: 1 as any
 | 
			
		||||
                  },
 | 
			
		||||
                  parent
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      // test fallback
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(ssrRenderComponent(Child, { msg: 'hello' }, null, parent))
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested components with vnode slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        ssrRender(ctx: any, push: any, parent: any) {
 | 
			
		||||
          push(`<div class="child">`)
 | 
			
		||||
          ssrRenderSlot(
 | 
			
		||||
            ctx.$slots,
 | 
			
		||||
            'default',
 | 
			
		||||
            { msg: 'from slot' },
 | 
			
		||||
            null,
 | 
			
		||||
            push,
 | 
			
		||||
            parent
 | 
			
		||||
          )
 | 
			
		||||
          push(`</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(
 | 
			
		||||
                  Child,
 | 
			
		||||
                  { msg: 'hello' },
 | 
			
		||||
                  {
 | 
			
		||||
                    // bailed slots returning raw vnodes
 | 
			
		||||
                    default: ({ msg }: any) => {
 | 
			
		||||
                      return h('span', msg)
 | 
			
		||||
                    }
 | 
			
		||||
                  },
 | 
			
		||||
                  parent
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested components with template slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        template: `<div class="child"><slot msg="from slot"></slot></div>`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        components: { Child },
 | 
			
		||||
        template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      expect(await renderToString(app)).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          `<!--[--><span>from slot</span><!--]-->` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('nested render fn components with template slots', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        render(this: any) {
 | 
			
		||||
          return h(
 | 
			
		||||
            'div',
 | 
			
		||||
            {
 | 
			
		||||
              class: 'child'
 | 
			
		||||
            },
 | 
			
		||||
            this.$slots.default({ msg: 'from slot' })
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const app = createApp({
 | 
			
		||||
        template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
 | 
			
		||||
      })
 | 
			
		||||
      app.component('Child', Child)
 | 
			
		||||
 | 
			
		||||
      expect(await renderToString(app)).toBe(
 | 
			
		||||
        `<div>parent<div class="child">` +
 | 
			
		||||
          // no comment anchors because slot is used directly as element children
 | 
			
		||||
          `<span>from slot</span>` +
 | 
			
		||||
          `</div></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('async components', async () => {
 | 
			
		||||
      const Child = {
 | 
			
		||||
        // should wait for resolved render context from setup()
 | 
			
		||||
        async setup() {
 | 
			
		||||
          return {
 | 
			
		||||
            msg: 'hello'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.msg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            ssrRender(_ctx, push, parent) {
 | 
			
		||||
              push(`<div>parent`)
 | 
			
		||||
              push(ssrRenderComponent(Child, null, null, parent))
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>hello</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('parallel async components', async () => {
 | 
			
		||||
      const OptimizedChild = {
 | 
			
		||||
        props: ['msg'],
 | 
			
		||||
        async setup(props: any) {
 | 
			
		||||
          return {
 | 
			
		||||
            localMsg: props.msg + '!'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        ssrRender(ctx: any, push: any) {
 | 
			
		||||
          push(`<div>${ctx.localMsg}</div>`)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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(`<div>parent`)
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(OptimizedChild, { msg: 'opt' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(
 | 
			
		||||
                ssrRenderComponent(VNodeChild, { msg: 'vnode' }, null, parent)
 | 
			
		||||
              )
 | 
			
		||||
              push(`</div>`)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>parent<div>opt!</div><div>vnode!</div></div>`)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('vnode element', () => {
 | 
			
		||||
    test('props', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          h('div', { id: 'foo&', class: ['bar', 'baz'] }, 'hello')
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div id="foo&" 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('raw vnode types', () => {
 | 
			
		||||
    test('Text', async () => {
 | 
			
		||||
      expect(await renderToString(createTextVNode('hello <div>'))).toBe(
 | 
			
		||||
        `hello <div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('Comment', async () => {
 | 
			
		||||
      // https://www.w3.org/TR/html52/syntax.html#comments
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          h('div', [
 | 
			
		||||
            createCommentVNode('>foo'),
 | 
			
		||||
            createCommentVNode('->foo'),
 | 
			
		||||
            createCommentVNode('<!--foo-->'),
 | 
			
		||||
            createCommentVNode('--!>foo<!-')
 | 
			
		||||
          ])
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div><!--foo--><!--foo--><!--foo--><!--foo--></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('Static', async () => {
 | 
			
		||||
      const content = `<div id="ok">hello<span>world</span></div>`
 | 
			
		||||
      expect(await renderToString(createStaticVNode(content, 1))).toBe(content)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  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(`<div data-v-test></div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
        `<div data-v-child data-v-test><span data-v-test data-v-child-s>slot</span></div>`
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('integration w/ compiled template', () => {
 | 
			
		||||
    test('render', async () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        await renderToString(
 | 
			
		||||
          createApp({
 | 
			
		||||
            data() {
 | 
			
		||||
              return { msg: 'hello' }
 | 
			
		||||
            },
 | 
			
		||||
            template: `<div>{{ msg }}</div>`
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      ).toBe(`<div>hello</div>`)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('handle compiler errors', async () => {
 | 
			
		||||
      await renderToString(createApp({ template: `<` }))
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        'Template compilation error: Unexpected EOF in tag.\n' +
 | 
			
		||||
          '1  |  <\n' +
 | 
			
		||||
          '   |   ^'
 | 
			
		||||
      ).toHaveBeenWarned()
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('serverPrefetch', async () => {
 | 
			
		||||
    const msg = Promise.resolve('hello')
 | 
			
		||||
    const app = createApp({
 | 
			
		||||
      data() {
 | 
			
		||||
        return {
 | 
			
		||||
          msg: ''
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      async serverPrefetch() {
 | 
			
		||||
        this.msg = await msg
 | 
			
		||||
      },
 | 
			
		||||
      render() {
 | 
			
		||||
        return h('div', this.msg)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    const html = await renderToString(app)
 | 
			
		||||
    expect(html).toBe(`<div>hello</div>`)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user