// @vitest-environment node import Vue from 'vue' import VM from 'vm' import { createRenderer } from 'server/index' import { _it } from './utils' const { renderToString } = createRenderer() describe('SSR: renderToString', () => { _it('static attributes', done => { renderVmWithOptions( { template: '
' }, result => { expect(result).toContain( '
' ) done() } ) }) _it('unary tags', done => { renderVmWithOptions( { template: '' }, result => { expect(result).toContain( '' ) done() } ) }) _it('dynamic attributes', done => { renderVmWithOptions( { template: '
', data: { foo: 'hi', baz: 123 } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('static class', done => { renderVmWithOptions( { template: '
' }, result => { expect(result).toContain( '
' ) done() } ) }) _it('dynamic class', done => { renderVmWithOptions( { template: '
', data: { a: 'baz', hasQux: true, hasQuux: false } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('custom component class', done => { renderVmWithOptions( { template: '
', components: { cmp: { render: h => h('div', 'test') } } }, result => { expect(result).toContain( '
test
' ) done() } ) }) _it('nested component class', done => { renderVmWithOptions( { template: '', data: { cls: { success: 1 } }, components: { cmp: { render: h => h('div', [ h('nested', { staticClass: 'nested', class: { error: 1 } }) ]), components: { nested: { render: h => h('div', { staticClass: 'inner' }, 'test') } } } } }, result => { expect(result).toContain( '
' + '
test
' + '
' ) done() } ) }) _it('dynamic style', done => { renderVmWithOptions( { template: '
', data: { fontSize: 14, color: 'red' } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('dynamic string style', done => { renderVmWithOptions( { template: '
', data: { style: 'color:red' } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('auto-prefixed style value as array', done => { renderVmWithOptions( { template: '
', data: { style: { display: ['-webkit-box', '-ms-flexbox', 'flex'] } } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('custom component style', done => { renderVmWithOptions( { template: '
', data: { style: 'color:red' }, components: { comp: { template: '
' } } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('nested custom component style', done => { renderVmWithOptions( { template: '', data: { style: 'color:red' }, components: { comp: { template: '', components: { nested: { template: '
' } } } } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('component style not passed to child', done => { renderVmWithOptions( { template: '', data: { style: 'color:red' }, components: { comp: { template: '
' } } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('component style not passed to slot', done => { renderVmWithOptions( { template: '', data: { style: 'color:red' }, components: { comp: { template: '
' } } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('attrs merging on components', done => { const Test = { render: h => h('div', { attrs: { id: 'a' } }) } renderVmWithOptions( { render: h => h(Test, { attrs: { id: 'b', name: 'c' } }) }, res => { expect(res).toContain( '
' ) done() } ) }) _it('domProps merging on components', done => { const Test = { render: h => h('div', { domProps: { innerHTML: 'a' } }) } renderVmWithOptions( { render: h => h(Test, { domProps: { innerHTML: 'b', value: 'c' } }) }, res => { expect(res).toContain( '
b
' ) done() } ) }) _it('v-show directive render', done => { renderVmWithOptions( { template: '
inner
' }, res => { expect(res).toContain( '
inner
' ) done() } ) }) _it('v-show directive merge with style', done => { renderVmWithOptions( { template: '
inner
' }, res => { expect(res).toContain( '
inner
' ) done() } ) }) _it('v-show directive not passed to child', done => { renderVmWithOptions( { template: '', components: { foo: { template: '
inner
' } } }, res => { expect(res).toContain( '
inner
' ) done() } ) }) _it('v-show directive not passed to slot', done => { renderVmWithOptions( { template: 'inner', components: { foo: { template: '
' } } }, res => { expect(res).toContain( '
inner
' ) done() } ) }) _it('v-show directive merging on components', done => { renderVmWithOptions( { template: '', components: { foo: { render: h => h('bar', { directives: [ { name: 'show', value: true } ] }), components: { bar: { render: h => h('div', 'inner') } } } } }, res => { expect(res).toContain( '
inner
' ) done() } ) }) _it('text interpolation', done => { renderVmWithOptions( { template: '
{{ foo }} side {{ bar }}
', data: { foo: 'server', bar: 'rendering' } }, result => { expect(result).toContain( '
server side <span>rendering</span>
' ) done() } ) }) _it('v-html on root', done => { renderVmWithOptions( { template: '
', data: { text: 'foo' } }, result => { expect(result).toContain( '
foo
' ) done() } ) }) _it('v-text on root', done => { renderVmWithOptions( { template: '
', data: { text: 'foo' } }, result => { expect(result).toContain( '
<span>foo</span>
' ) done() } ) }) _it('v-html', done => { renderVmWithOptions( { template: '
', data: { text: 'foo' } }, result => { expect(result).toContain( '
foo
' ) done() } ) }) _it('v-html with null value', done => { renderVmWithOptions( { template: '
', data: { text: null } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('v-text', done => { renderVmWithOptions( { template: '
', data: { text: 'foo' } }, result => { expect(result).toContain( '
<span>foo</span>
' ) done() } ) }) _it('v-text with null value', done => { renderVmWithOptions( { template: '
', data: { text: null } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('child component (hoc)', done => { renderVmWithOptions( { template: '', data: { msg: 'hello' }, components: { child: { props: ['msg'], data() { return { name: 'bar' } }, render() { const h = this.$createElement return h('div', { class: ['bar'] }, [`${this.msg} ${this.name}`]) } } } }, result => { expect(result).toContain( '
hello bar
' ) done() } ) }) _it('has correct lifecycle during render', done => { let lifecycleCount = 1 renderVmWithOptions( { template: '
{{ val }}
', data: { val: 'hi' }, beforeCreate() { expect(lifecycleCount++).toBe(1) }, created() { this.val = 'hello' expect(this.val).toBe('hello') expect(lifecycleCount++).toBe(2) }, components: { test: { beforeCreate() { expect(lifecycleCount++).toBe(3) }, created() { expect(lifecycleCount++).toBe(4) }, render() { expect(lifecycleCount++).toBeGreaterThan(4) return this.$createElement('span', { class: ['b'] }, 'testAsync') } } } }, result => { expect(result).toContain( '
' + 'hello' + 'testAsync' + '
' ) done() } ) }) _it('computed properties', done => { renderVmWithOptions( { template: '
{{ b }}
', data: { a: { b: 1 } }, computed: { b() { return this.a.b + 1 } }, created() { this.a.b = 2 expect(this.b).toBe(3) } }, result => { expect(result).toContain('
3
') done() } ) }) _it('renders async component', done => { renderVmWithOptions( { template: `
`, components: { testAsync(resolve) { setTimeout( () => resolve({ render() { return this.$createElement( 'span', { class: ['b'] }, 'testAsync' ) } }), 1 ) } } }, result => { expect(result).toContain( '
testAsync
' ) done() } ) }) _it('renders async component (Promise, nested)', done => { const Foo = () => Promise.resolve({ render: h => h('div', [h('span', 'foo'), h(Bar)]) }) const Bar = () => ({ component: Promise.resolve({ render: h => h('span', 'bar') }) }) renderVmWithOptions( { render: h => h(Foo) }, res => { expect(res).toContain( `
foobar
` ) done() } ) }) _it('renders async component (ES module)', done => { const Foo = () => Promise.resolve({ __esModule: true, default: { render: h => h('div', [h('span', 'foo'), h(Bar)]) } }) const Bar = () => ({ component: Promise.resolve({ __esModule: true, default: { render: h => h('span', 'bar') } }) }) renderVmWithOptions( { render: h => h(Foo) }, res => { expect(res).toContain( `
foobar
` ) done() } ) }) _it('renders async component (hoc)', done => { renderVmWithOptions( { template: '', components: { testAsync: () => Promise.resolve({ render() { return this.$createElement( 'span', { class: ['b'] }, 'testAsync' ) } }) } }, result => { expect(result).toContain( 'testAsync' ) done() } ) }) _it('renders async component (functional, single node)', done => { renderVmWithOptions( { template: `
`, components: { testAsync(resolve) { setTimeout( () => resolve({ functional: true, render(h) { return h('span', { class: ['b'] }, 'testAsync') } }), 1 ) } } }, result => { expect(result).toContain( '
testAsync
' ) done() } ) }) _it('renders async component (functional, multiple nodes)', done => { renderVmWithOptions( { template: `
`, components: { testAsync(resolve) { setTimeout( () => resolve({ functional: true, render(h) { return [ h('span', { class: ['a'] }, 'foo'), h('span', { class: ['b'] }, 'bar') ] } }), 1 ) } } }, result => { expect(result).toContain( '
' + 'foo' + 'bar' + '
' ) done() } ) }) _it('renders nested async functional component', done => { renderVmWithOptions( { template: `
`, components: { outerAsync(resolve) { setTimeout( () => resolve({ functional: true, render(h) { return h('innerAsync') } }), 1 ) }, innerAsync(resolve) { setTimeout( () => resolve({ functional: true, render(h) { return h('span', { class: ['a'] }, 'inner') } }), 1 ) } } }, result => { expect(result).toContain( '
' + 'inner' + '
' ) done() } ) }) _it('should catch async component error', done => { renderToString( new Vue({ template: '', components: { testAsync: () => Promise.resolve({ render() { throw new Error('foo') } }) } }), (err, result) => { expect(err).toBeTruthy() expect(result).toBeUndefined() expect('foo').toHaveBeenWarned() done() } ) }) // #11963, #10391 _it('renders async children passed in slots', done => { const Parent = { template: `
` } const Child = { template: `

child

` } renderVmWithOptions( { template: ` `, components: { Parent, Child: () => Promise.resolve(Child) } }, result => { expect(result).toContain( `

child

` ) done() } ) }) _it('everything together', done => { renderVmWithOptions( { template: `

yoyo

{{ test }}
`, data: { test: 'hi', isRed: true, imageUrl: 'https://vuejs.org/images/logo.png' }, components: { test: { render() { return this.$createElement('div', { class: ['a'] }, 'test') } }, testAsync(resolve) { resolve({ render() { return this.$createElement( 'span', { class: ['b'] }, 'testAsync' ) } }) } } }, result => { expect(result).toContain( '
' + '

yoyo

' + '
' + 'hi ' + ' ' + ' ' + '
test
' + 'testAsync' + '
' ) done() } ) }) _it('normal attr', done => { renderVmWithOptions( { template: `
hello hello hello hello hello
` }, result => { expect(result).toContain( '
' + 'hello ' + 'hello ' + 'hello ' + 'hello ' + 'hello' + '
' ) done() } ) }) _it('enumerated attr', done => { renderVmWithOptions( { template: `
hello hello hello hello hello hello
` }, result => { expect(result).toContain( '
' + 'hello ' + 'hello ' + 'hello ' + 'hello ' + 'hello ' + 'hello' + '
' ) done() } ) }) _it('boolean attr', done => { renderVmWithOptions( { template: `
hello hello hello hello
` }, result => { expect(result).toContain( '
' + 'hello ' + 'hello ' + 'hello ' + 'hello' + '
' ) done() } ) }) _it('v-bind object', done => { renderVmWithOptions( { data: { test: { id: 'a', class: ['a', 'b'], value: 'c' } }, template: '' }, result => { expect(result).toContain( '' ) done() } ) }) _it('custom directives on raw element', done => { const renderer = createRenderer({ directives: { 'class-prefixer': (node, dir) => { if (node.data.class) { node.data.class = `${dir.value}-${node.data.class}` } if (node.data.staticClass) { node.data.staticClass = `${dir.value}-${node.data.staticClass}` } } } }) renderer.renderToString( new Vue({ render() { const h = this.$createElement return h( 'p', { class: 'class1', staticClass: 'class2', directives: [ { name: 'class-prefixer', value: 'my' } ] }, ['hello world'] ) } }), (err, result) => { expect(err).toBeNull() expect(result).toContain( '

hello world

' ) done() } ) }) _it('custom directives on component', done => { const Test = { template: 'hello world' } const renderer = createRenderer({ directives: { 'class-prefixer': (node, dir) => { if (node.data.class) { node.data.class = `${dir.value}-${node.data.class}` } if (node.data.staticClass) { node.data.staticClass = `${dir.value}-${node.data.staticClass}` } } } }) renderer.renderToString( new Vue({ template: '

', components: { Test } }), (err, result) => { expect(err).toBeNull() expect(result).toContain( '

hello world

' ) done() } ) }) _it('custom directives on element root of a component', done => { const Test = { template: 'hello world' } const renderer = createRenderer({ directives: { 'class-prefixer': (node, dir) => { if (node.data.class) { node.data.class = `${dir.value}-${node.data.class}` } if (node.data.staticClass) { node.data.staticClass = `${dir.value}-${node.data.staticClass}` } } } }) renderer.renderToString( new Vue({ template: '

', components: { Test } }), (err, result) => { expect(err).toBeNull() expect(result).toContain( '

hello world

' ) done() } ) }) _it('custom directives on element with parent element', done => { const renderer = createRenderer({ directives: { 'class-prefixer': (node, dir) => { if (node.data.class) { node.data.class = `${dir.value}-${node.data.class}` } if (node.data.staticClass) { node.data.staticClass = `${dir.value}-${node.data.staticClass}` } } } }) renderer.renderToString( new Vue({ template: '

hello world

' }), (err, result) => { expect(err).toBeNull() expect(result).toContain( '

hello world

' ) done() } ) }) _it( 'should not warn for custom directives that do not have server-side implementation', done => { renderToString( new Vue({ directives: { test: { bind() { // noop } } }, template: '
' }), () => { expect('Failed to resolve directive: test').not.toHaveBeenWarned() done() } ) } ) _it('_scopeId', done => { renderVmWithOptions( { _scopeId: '_v-parent', template: '

', components: { child: { _scopeId: '_v-child', render() { const h = this.$createElement return h('div', null, [h('span', null, ['foo'])]) } } } }, result => { expect(result).toContain( '
' + '

' + '

foo
' + '

' + '
' ) done() } ) }) _it('_scopeId on slot content', done => { renderVmWithOptions( { _scopeId: '_v-parent', template: '

foo

', components: { child: { _scopeId: '_v-child', render() { const h = this.$createElement return h('div', null, this.$slots.default) } } } }, result => { expect(result).toContain( '
' + '

foo

' + '
' ) done() } ) }) _it('comment nodes', done => { renderVmWithOptions( { template: '
' }, result => { expect(result).toContain( `
` ) done() } ) }) _it('should catch error', done => { renderToString( new Vue({ render() { throw new Error('oops') } }), err => { expect(err instanceof Error).toBe(true) expect(`oops`).toHaveBeenWarned() done() } ) }) _it('default value Foreign Function', () => { const FunctionConstructor = VM.runInNewContext('Function') const func = () => 123 const vm = new Vue({ props: { a: { type: FunctionConstructor, default: func } }, propsData: { a: undefined } }) expect(vm.a).toBe(func) }) _it('should prevent xss in attributes', done => { renderVmWithOptions( { data: { xss: '">' }, template: `
foo
` }, res => { expect(res).not.toContain(``) done() } ) }) _it('should prevent xss in attribute names', done => { renderVmWithOptions( { data: { xss: { 'foo="bar">': '' } }, template: `
` }, res => { expect(res).not.toContain(``) done() } ) }) _it('should prevent xss in attribute names (optimized)', done => { renderVmWithOptions( { data: { xss: { 'foo="bar">': '' } }, template: `
foo
` }, res => { expect(res).not.toContain(``) done() } ) }) _it( 'should prevent script xss with v-bind object syntax + array value', done => { renderVmWithOptions( { data: { test: ['">` ) done() } ) }) _it('v-for', done => { renderVmWithOptions( { template: `
foo {{ i }}
` }, res => { expect(res).toContain( `
foo 12
` ) done() } ) }) _it('template v-if', done => { renderVmWithOptions( { template: `
foo
` }, res => { expect(res).toContain( `
foo foo bar baz
` ) done() } ) }) _it('template v-for', done => { renderVmWithOptions( { template: `
foo
` }, res => { expect(res).toContain( `
foo 1bar2bar
` ) done() } ) }) _it('with inheritAttrs: false + $attrs', done => { renderVmWithOptions( { template: ``, components: { foo: { inheritAttrs: false, template: `
` } } }, res => { expect(res).toBe( `
` ) done() } ) }) _it('should escape static strings', done => { renderVmWithOptions( { template: `
<foo>
` }, res => { expect(res).toBe(`
<foo>
`) done() } ) }) _it('should not cache computed properties', done => { renderVmWithOptions( { template: `
{{ foo }}
`, data: () => ({ bar: 1 }), computed: { foo() { return this.bar + 1 } }, created() { this.foo // access this.bar++ // trigger change } }, res => { expect(res).toBe(`
3
`) done() } ) }) // #8977 _it('should call computed properties with vm as first argument', done => { renderToString( new Vue({ data: { firstName: 'Evan', lastName: 'You' }, computed: { fullName: ({ firstName, lastName }) => `${firstName} ${lastName}` }, template: '
{{ fullName }}
' }), (err, result) => { expect(err).toBeNull() expect(result).toContain( '
Evan You
' ) done() } ) }) _it('return Promise', async () => { await renderToString( new Vue({ template: `
{{ foo }}
`, data: { foo: 'bar' } }) )!.then(res => { expect(res).toBe(`
bar
`) }) }) _it('return Promise (error)', async () => { await renderToString( new Vue({ render() { throw new Error('foobar') } }) )!.catch(err => { expect('foobar').toHaveBeenWarned() expect(err.toString()).toContain(`foobar`) }) }) _it('should catch template compilation error', done => { renderToString( new Vue({ template: `
` }), err => { expect(err.toString()).toContain( 'Component template should contain exactly one root element' ) done() } ) }) // #6907 _it('should not optimize root if conditions', done => { renderVmWithOptions( { data: { foo: 123 }, template: `` }, res => { expect(res).toBe( `` ) done() } ) }) _it('render muted properly', done => { renderVmWithOptions( { template: '' }, result => { expect(result).toContain( '' ) done() } ) }) _it('render v-model with textarea', done => { renderVmWithOptions( { data: { foo: 'bar' }, template: '
' }, result => { expect(result).toContain('') done() } ) }) _it('render v-model with textarea (non-optimized)', done => { renderVmWithOptions( { render(h) { return h('textarea', { domProps: { value: 'foo' } }) } }, result => { expect(result).toContain( '' ) done() } ) }) _it('render v-model with ` }, result => { expect(result).toContain( '' ) done() } ) }) _it('render v-model with ` }, result => { expect(result).toContain( '' ) done() } ) }) _it('render v-model with ` }, result => { expect(result).toContain( '' ) done() } ) }) // #7223 _it('should not double escape attribute values', done => { renderVmWithOptions( { template: `
` }, result => { expect(result).toContain(`
`) done() } ) }) // #7859 _it('should not double escape class values', done => { renderVmWithOptions( { template: `
` }, result => { expect(result).toContain(`
`) done() } ) }) _it('should expose ssr helpers on functional context', done => { let called = false renderVmWithOptions( { template: `
`, components: { foo: { functional: true, render(h, ctx) { expect(ctx._ssrNode).toBeTruthy() called = true } } } }, () => { expect(called).toBe(true) done() } ) }) _it('should support serverPrefetch option', done => { renderVmWithOptions( { template: `
{{ count }}
`, data: { count: 0 }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.count = 42 resolve() }, 1) }) } }, result => { expect(result).toContain('
42
') done() } ) }) _it('should support serverPrefetch option (nested)', done => { renderVmWithOptions( { template: `
{{ count }}
`, data: { count: 0 }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.count = 42 resolve() }, 1) }) }, components: { nestedPrefetch: { template: `
{{ message }}
`, data() { return { message: '' } }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.message = 'vue.js' resolve() }, 1) }) } } } }, result => { expect(result).toContain( '
42
vue.js
' ) done() } ) }) _it('should support serverPrefetch option (nested async)', done => { renderVmWithOptions( { template: `
{{ count }}
`, data: { count: 0 }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.count = 42 resolve() }, 1) }) }, components: { nestedPrefetch(resolve) { resolve({ template: `
{{ message }}
`, data() { return { message: '' } }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.message = 'vue.js' resolve() }, 1) }) } }) } } }, result => { expect(result).toContain( '
42
vue.js
' ) done() } ) }) _it('should merge serverPrefetch option', done => { const mixin = { data: { message: '' }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.message = 'vue.js' resolve() }, 1) }) } } renderVmWithOptions( { mixins: [mixin], template: `
{{ count }}
{{ message }}
`, data: { count: 0 }, serverPrefetch() { return new Promise(resolve => { setTimeout(() => { this.count = 42 resolve() }, 1) }) } }, result => { expect(result).toContain( '
42
vue.js
' ) done() } ) }) _it( `should skip serverPrefetch option that doesn't return a promise`, done => { renderVmWithOptions( { template: `
{{ count }}
`, data: { count: 0 }, serverPrefetch() { setTimeout(() => { this.count = 42 }, 1) } }, result => { expect(result).toContain('
0
') done() } ) } ) _it('should call context.rendered', done => { let a = 0 renderToString( new Vue({ template: '
Hello
' }), { rendered: () => { a = 42 } }, (err, res) => { expect(err).toBeNull() expect(res).toContain('
Hello
') expect(a).toBe(42) done() } ) }) _it('invalid style value', done => { renderVmWithOptions( { template: '

', data: { // all invalid, should not even have "style" attribute style: { opacity: {}, color: null }, // mix of valid and invalid style2: { opacity: 0, color: null } } }, result => { expect(result).toContain( '

' ) done() } ) }) _it('numeric style value', done => { renderVmWithOptions( { template: '
', data: { style: { opacity: 0, // valid, opacity is unit-less top: 0, // valid, top requires unit but 0 is allowed left: 10, // invalid, left requires a unit marginTop: '10px' // valid } } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('handling max stack size limit', done => { const vueInstance = new Vue({ template: `
`, components: { child: { template: '
hi
' } }, data: { items: Array(1000).fill(0) } }) renderToString(vueInstance, err => done(err)) }) _it('undefined v-model with textarea', done => { renderVmWithOptions( { render(h) { return h('div', [ h('textarea', { domProps: { value: null } }) ]) } }, result => { expect(result).toContain( '
' ) done() } ) }) _it('Options inheritAttrs in parent component', done => { const childComponent = { template: `
{{ someProp }}
`, props: { someProp: {} } } const parentComponent = { template: ``, components: { childComponent }, inheritAttrs: false } renderVmWithOptions( { template: `
`, components: { parentComponent } }, result => { expect(result).toContain( '
some-val
' ) done() } ) }) }) function renderVmWithOptions(options, cb) { renderToString(new Vue(options), (err, res) => { expect(err).toBeNull() cb(res) }) }