mirror of https://github.com/vuejs/core.git
				
				
				
			
		
			
				
	
	
		
			1202 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			1202 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import {
 | |
|   type ComponentOptions,
 | |
|   KeepAlive,
 | |
|   Transition,
 | |
|   computed,
 | |
|   createApp,
 | |
|   createCommentVNode,
 | |
|   createSSRApp,
 | |
|   createStaticVNode,
 | |
|   createTextVNode,
 | |
|   createVNode,
 | |
|   defineComponent,
 | |
|   getCurrentInstance,
 | |
|   h,
 | |
|   onErrorCaptured,
 | |
|   onServerPrefetch,
 | |
|   reactive,
 | |
|   ref,
 | |
|   renderSlot,
 | |
|   resolveComponent,
 | |
|   resolveDynamicComponent,
 | |
|   watchEffect,
 | |
|   withCtx,
 | |
| } from 'vue'
 | |
| import { escapeHtml } from '@vue/shared'
 | |
| import { renderToString } from '../src/renderToString'
 | |
| import { pipeToNodeWritable, renderToNodeStream } from '../src/renderToStream'
 | |
| import { type SSRSlot, ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
 | |
| import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent'
 | |
| import { type Readable, Transform } from 'node:stream'
 | |
| import { ssrRenderVNode } from '../src'
 | |
| 
 | |
| 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) => {
 | |
|   return promisifyStream(renderToNodeStream(app, context))
 | |
| }
 | |
| 
 | |
| const pipeToWritable = (app: any, context?: any) => {
 | |
|   const stream = new Transform({
 | |
|     transform(data, _encoding, cb) {
 | |
|       this.push(data)
 | |
|       cb()
 | |
|     },
 | |
|   })
 | |
|   pipeToNodeWritable(app, context, stream)
 | |
|   return promisifyStream(stream)
 | |
| }
 | |
| 
 | |
| // we run the same tests twice, once for renderToString, once for renderToStream
 | |
| testRender(`renderToString`, renderToString)
 | |
| testRender(`renderToNodeStream`, renderToStream)
 | |
| testRender(`pipeToNodeWritable`, pipeToWritable)
 | |
| 
 | |
| 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('components using defineComponent with extends option', async () => {
 | |
|         expect(
 | |
|           await render(
 | |
|             createApp(
 | |
|               defineComponent({
 | |
|                 extends: defineComponent({
 | |
|                   data() {
 | |
|                     return { msg: 'hello' }
 | |
|                   },
 | |
|                   render() {
 | |
|                     return h('div', this.msg)
 | |
|                   },
 | |
|                 }),
 | |
|               }),
 | |
|             ),
 | |
|           ),
 | |
|         ).toBe(`<div>hello</div>`)
 | |
|       })
 | |
| 
 | |
|       test('components using defineComponent with mixins option', async () => {
 | |
|         expect(
 | |
|           await render(
 | |
|             createApp(
 | |
|               defineComponent({
 | |
|                 mixins: [
 | |
|                   defineComponent({
 | |
|                     data() {
 | |
|                       return { msg: 'hello' }
 | |
|                     },
 | |
|                     render() {
 | |
|                       return h('div', this.msg)
 | |
|                     },
 | |
|                   }),
 | |
|                 ],
 | |
|               }),
 | |
|             ),
 | |
|           ),
 | |
|         ).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('vnode component', () => {
 | |
|       test('KeepAlive', async () => {
 | |
|         const MyComp = {
 | |
|           render: () => h('p', 'hello'),
 | |
|         }
 | |
|         expect(await render(h(KeepAlive, () => h(MyComp)))).toBe(`<p>hello</p>`)
 | |
|       })
 | |
| 
 | |
|       test('Transition', async () => {
 | |
|         const MyComp = {
 | |
|           render: () => h('p', 'hello'),
 | |
|         }
 | |
|         expect(await render(h(Transition, () => h(MyComp)))).toBe(
 | |
|           `<p>hello</p>`,
 | |
|         )
 | |
|       })
 | |
|     })
 | |
| 
 | |
|     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.
 | |
| 
 | |
|       test('basic', async () => {
 | |
|         const Foo = {
 | |
|           __scopeId: 'data-v-test',
 | |
|           render() {
 | |
|             return h('div')
 | |
|           },
 | |
|         }
 | |
|         expect(await render(h(Foo))).toBe(`<div data-v-test></div>`)
 | |
|       })
 | |
| 
 | |
|       test('with client-compiled vnode slots', async () => {
 | |
|         const Child = {
 | |
|           __scopeId: 'data-v-child',
 | |
|           render: function (this: any) {
 | |
|             return h('div', null, [renderSlot(this.$slots, 'default')])
 | |
|           },
 | |
|         }
 | |
| 
 | |
|         const Parent = {
 | |
|           __scopeId: 'data-v-test',
 | |
|           render: () => {
 | |
|             return h(Child, null, {
 | |
|               default: withCtx(() => [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: `<div>${type}</` }),
 | |
|         )
 | |
| 
 | |
|         expect(
 | |
|           `Template compilation error: Unexpected EOF in tag.`,
 | |
|         ).toHaveBeenWarned()
 | |
|         expect(`Element is missing end tag`).toHaveBeenWarned()
 | |
|       })
 | |
| 
 | |
|       // #6110
 | |
|       test('reset current instance after rendering error', async () => {
 | |
|         const prev = getCurrentInstance()
 | |
|         expect(prev).toBe(null)
 | |
|         try {
 | |
|           await render(
 | |
|             createApp({
 | |
|               data() {
 | |
|                 return { msg: null }
 | |
|               },
 | |
|               template: `<div>{{ msg.text }}</div>`,
 | |
|             }),
 | |
|           )
 | |
|         } catch {}
 | |
|         expect(getCurrentInstance()).toBe(prev)
 | |
|       })
 | |
| 
 | |
|       // #7733
 | |
|       test('reset current instance after error in data', async () => {
 | |
|         const prev = getCurrentInstance()
 | |
|         expect(prev).toBe(null)
 | |
|         try {
 | |
|           await render(
 | |
|             createApp({
 | |
|               data() {
 | |
|                 throw new Error()
 | |
|               },
 | |
|               template: `<div>hello</div>`,
 | |
|             }),
 | |
|           )
 | |
|         } catch {}
 | |
|         expect(getCurrentInstance()).toBe(null)
 | |
|       })
 | |
|     })
 | |
| 
 | |
|     // #7733
 | |
|     test('reset current instance after error in errorCaptured', async () => {
 | |
|       const prev = getCurrentInstance()
 | |
| 
 | |
|       expect(prev).toBe(null)
 | |
| 
 | |
|       const Child = {
 | |
|         created() {
 | |
|           throw new Error()
 | |
|         },
 | |
|       }
 | |
|       try {
 | |
|         await render(
 | |
|           createApp({
 | |
|             errorCaptured() {
 | |
|               throw new Error()
 | |
|             },
 | |
|             render: () => h(Child),
 | |
|           }),
 | |
|         )
 | |
|       } catch {}
 | |
|       expect(
 | |
|         'Unhandled error during execution of errorCaptured hook',
 | |
|       ).toHaveBeenWarned()
 | |
|       expect(getCurrentInstance()).toBe(null)
 | |
|     })
 | |
| 
 | |
|     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>`)
 | |
|     })
 | |
| 
 | |
|     // #2763
 | |
|     test('error handling w/ async setup', async () => {
 | |
|       const fn = vi.fn()
 | |
|       const fn2 = vi.fn()
 | |
| 
 | |
|       const asyncChildren = defineComponent({
 | |
|         async setup() {
 | |
|           return Promise.reject('async child error')
 | |
|         },
 | |
|         template: `<div>asyncChildren</div>`,
 | |
|       })
 | |
|       const app = createApp({
 | |
|         name: 'App',
 | |
|         components: {
 | |
|           asyncChildren,
 | |
|         },
 | |
|         template: `<div class="app"><async-children /></div>`,
 | |
|         errorCaptured(error) {
 | |
|           fn(error)
 | |
|         },
 | |
|       })
 | |
| 
 | |
|       app.config.errorHandler = error => {
 | |
|         fn2(error)
 | |
|       }
 | |
| 
 | |
|       const html = await renderToString(app)
 | |
|       expect(html).toBe(`<div class="app"><div>asyncChildren</div></div>`)
 | |
| 
 | |
|       expect(fn).toHaveBeenCalledTimes(1)
 | |
|       expect(fn).toBeCalledWith('async child error')
 | |
| 
 | |
|       expect(fn2).toHaveBeenCalledTimes(1)
 | |
|       expect(fn2).toBeCalledWith('async child error')
 | |
|     })
 | |
| 
 | |
|     // https://github.com/vuejs/core/issues/3322
 | |
|     test('effect onInvalidate does not error', async () => {
 | |
|       const noop = () => {}
 | |
|       const app = createApp({
 | |
|         setup: () => {
 | |
|           watchEffect(onInvalidate => onInvalidate(noop))
 | |
|         },
 | |
|         render: noop,
 | |
|       })
 | |
|       expect(await render(app)).toBe('<!---->')
 | |
|     })
 | |
| 
 | |
|     // #2863
 | |
|     test('assets should be resolved correctly', async () => {
 | |
|       expect(
 | |
|         await render(
 | |
|           createApp({
 | |
|             components: {
 | |
|               A: {
 | |
|                 ssrRender(_ctx, _push) {
 | |
|                   _push(`<div>A</div>`)
 | |
|                 },
 | |
|               },
 | |
|               B: {
 | |
|                 render: () => h('div', 'B'),
 | |
|               },
 | |
|             },
 | |
|             ssrRender(_ctx, _push, _parent) {
 | |
|               const A: any = resolveComponent('A')
 | |
|               _push(ssrRenderComponent(A, null, null, _parent))
 | |
|               ssrRenderVNode(
 | |
|                 _push,
 | |
|                 createVNode(resolveDynamicComponent('B'), null, null),
 | |
|                 _parent,
 | |
|               )
 | |
|             },
 | |
|           }),
 | |
|         ),
 | |
|       ).toBe(`<div>A</div><div>B</div>`)
 | |
|     })
 | |
| 
 | |
|     test('onServerPrefetch', async () => {
 | |
|       const msg = Promise.resolve('hello')
 | |
|       const app = createApp({
 | |
|         setup() {
 | |
|           const message = ref('')
 | |
|           onServerPrefetch(async () => {
 | |
|             message.value = await msg
 | |
|           })
 | |
|           return {
 | |
|             message,
 | |
|           }
 | |
|         },
 | |
|         render() {
 | |
|           return h('div', this.message)
 | |
|         },
 | |
|       })
 | |
|       const html = await render(app)
 | |
|       expect(html).toBe(`<div>hello</div>`)
 | |
|     })
 | |
| 
 | |
|     test('multiple onServerPrefetch', async () => {
 | |
|       const msg = Promise.resolve('hello')
 | |
|       const msg2 = Promise.resolve('hi')
 | |
|       const msg3 = Promise.resolve('bonjour')
 | |
|       const app = createApp({
 | |
|         setup() {
 | |
|           const message = ref('')
 | |
|           const message2 = ref('')
 | |
|           const message3 = ref('')
 | |
|           onServerPrefetch(async () => {
 | |
|             message.value = await msg
 | |
|           })
 | |
|           onServerPrefetch(async () => {
 | |
|             message2.value = await msg2
 | |
|           })
 | |
|           onServerPrefetch(async () => {
 | |
|             message3.value = await msg3
 | |
|           })
 | |
|           return {
 | |
|             message,
 | |
|             message2,
 | |
|             message3,
 | |
|           }
 | |
|         },
 | |
|         render() {
 | |
|           return h('div', `${this.message} ${this.message2} ${this.message3}`)
 | |
|         },
 | |
|       })
 | |
|       const html = await render(app)
 | |
|       expect(html).toBe(`<div>hello hi bonjour</div>`)
 | |
|     })
 | |
| 
 | |
|     test('onServerPrefetch are run in parallel', async () => {
 | |
|       const first = vi.fn(() => Promise.resolve())
 | |
|       const second = vi.fn(() => Promise.resolve())
 | |
|       let checkOther = [false, false]
 | |
|       let done = [false, false]
 | |
|       const app = createApp({
 | |
|         setup() {
 | |
|           onServerPrefetch(async () => {
 | |
|             checkOther[0] = done[1]
 | |
|             await first()
 | |
|             done[0] = true
 | |
|           })
 | |
|           onServerPrefetch(async () => {
 | |
|             checkOther[1] = done[0]
 | |
|             await second()
 | |
|             done[1] = true
 | |
|           })
 | |
|         },
 | |
|         render() {
 | |
|           return h('div', '')
 | |
|         },
 | |
|       })
 | |
|       await render(app)
 | |
|       expect(first).toHaveBeenCalled()
 | |
|       expect(second).toHaveBeenCalled()
 | |
|       expect(checkOther).toEqual([false, false])
 | |
|       expect(done).toEqual([true, true])
 | |
|     })
 | |
| 
 | |
|     test('onServerPrefetch with serverPrefetch option', async () => {
 | |
|       const msg = Promise.resolve('hello')
 | |
|       const msg2 = Promise.resolve('hi')
 | |
|       const app = createApp({
 | |
|         data() {
 | |
|           return {
 | |
|             message: '',
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         async serverPrefetch() {
 | |
|           this.message = await msg
 | |
|         },
 | |
| 
 | |
|         setup() {
 | |
|           const message2 = ref('')
 | |
|           onServerPrefetch(async () => {
 | |
|             message2.value = await msg2
 | |
|           })
 | |
|           return {
 | |
|             message2,
 | |
|           }
 | |
|         },
 | |
|         render() {
 | |
|           return h('div', `${this.message} ${this.message2}`)
 | |
|         },
 | |
|       })
 | |
|       const html = await render(app)
 | |
|       expect(html).toBe(`<div>hello hi</div>`)
 | |
|     })
 | |
| 
 | |
|     test('mixed in serverPrefetch', async () => {
 | |
|       const msg = Promise.resolve('hello')
 | |
|       const app = createApp({
 | |
|         data() {
 | |
|           return {
 | |
|             msg: '',
 | |
|           }
 | |
|         },
 | |
|         mixins: [
 | |
|           {
 | |
|             async serverPrefetch() {
 | |
|               this.msg = await msg
 | |
|             },
 | |
|           },
 | |
|         ],
 | |
|         render() {
 | |
|           return h('div', this.msg)
 | |
|         },
 | |
|       })
 | |
|       const html = await render(app)
 | |
|       expect(html).toBe(`<div>hello</div>`)
 | |
|     })
 | |
| 
 | |
|     test('many serverPrefetch', async () => {
 | |
|       const foo = Promise.resolve('foo')
 | |
|       const bar = Promise.resolve('bar')
 | |
|       const baz = Promise.resolve('baz')
 | |
|       const app = createApp({
 | |
|         data() {
 | |
|           return {
 | |
|             foo: '',
 | |
|             bar: '',
 | |
|             baz: '',
 | |
|           }
 | |
|         },
 | |
|         mixins: [
 | |
|           {
 | |
|             async serverPrefetch() {
 | |
|               this.foo = await foo
 | |
|             },
 | |
|           },
 | |
|           {
 | |
|             async serverPrefetch() {
 | |
|               this.bar = await bar
 | |
|             },
 | |
|           },
 | |
|         ],
 | |
|         async serverPrefetch() {
 | |
|           this.baz = await baz
 | |
|         },
 | |
|         render() {
 | |
|           return h('div', `${this.foo}${this.bar}${this.baz}`)
 | |
|         },
 | |
|       })
 | |
|       const html = await render(app)
 | |
|       expect(html).toBe(`<div>foobarbaz</div>`)
 | |
|     })
 | |
| 
 | |
|     test('onServerPrefetch throwing error', async () => {
 | |
|       let renderError: Error | null = null
 | |
|       let capturedError: Error | null = null
 | |
| 
 | |
|       const Child = {
 | |
|         setup() {
 | |
|           onServerPrefetch(async () => {
 | |
|             throw new Error('An error')
 | |
|           })
 | |
|         },
 | |
|         render() {
 | |
|           return h('span')
 | |
|         },
 | |
|       }
 | |
| 
 | |
|       const app = createApp({
 | |
|         setup() {
 | |
|           onErrorCaptured(e => {
 | |
|             capturedError = e
 | |
|             return false
 | |
|           })
 | |
|         },
 | |
|         render() {
 | |
|           return h('div', h(Child))
 | |
|         },
 | |
|       })
 | |
| 
 | |
|       try {
 | |
|         await render(app)
 | |
|       } catch (e: any) {
 | |
|         renderError = e
 | |
|       }
 | |
|       expect(renderError).toBe(null)
 | |
|       expect((capturedError as unknown as Error).message).toBe('An error')
 | |
|     })
 | |
| 
 | |
|     test('computed reactivity during SSR with onServerPrefetch', async () => {
 | |
|       const store = {
 | |
|         // initial state could be hydrated
 | |
|         state: reactive({ items: null as null | string[] }),
 | |
| 
 | |
|         // pretend to fetch some data from an api
 | |
|         async fetchData() {
 | |
|           this.state.items = ['hello', 'world']
 | |
|         },
 | |
|       }
 | |
| 
 | |
|       const getterSpy = vi.fn()
 | |
| 
 | |
|       const App = defineComponent(() => {
 | |
|         const msg = computed(() => {
 | |
|           getterSpy()
 | |
|           return store.state.items?.join(' ')
 | |
|         })
 | |
| 
 | |
|         // If msg value is falsy then we are either in ssr context or on the client
 | |
|         // and the initial state was not modified/hydrated.
 | |
|         // In both cases we need to fetch data.
 | |
|         onServerPrefetch(() => store.fetchData())
 | |
| 
 | |
|         // simulate the read from a composable (e.g. filtering a list of results)
 | |
|         msg.value
 | |
| 
 | |
|         return () => h('div', null, msg.value)
 | |
|       })
 | |
| 
 | |
|       const app = createSSRApp(App)
 | |
| 
 | |
|       // in real world serve this html and append store state for hydration on client
 | |
|       const html = await renderToString(app)
 | |
| 
 | |
|       expect(html).toMatch('hello world')
 | |
| 
 | |
|       // should only be called twice since access should be cached
 | |
|       // during the render phase
 | |
|       expect(getterSpy).toHaveBeenCalledTimes(2)
 | |
|     })
 | |
|   })
 | |
| }
 |