import Vue from '../../dist/vue.runtime.common.js' import VM from 'vm' import { createRenderer } from '../../packages/vue-server-renderer' 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 => { Vue.config.silent = true renderToString(new Vue({ template: '', components: { testAsync: () => Promise.resolve({ render () { throw new Error('foo') } }) } }), (err, result) => { Vue.config.silent = false expect(err).toBeTruthy() expect(result).toBeUndefined() 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', 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('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 => { Vue.config.silent = true renderToString(new Vue({ render () { throw new Error('oops') } }), err => { expect(err instanceof Error).toBe(true) Vue.config.silent = false 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', done => { renderToString(new Vue({ template: `
{{ foo }}
`, data: { foo: 'bar' } })).then(res => { expect(res).toBe(`
bar
`) done() }) }) it('return Promise (error)', done => { Vue.config.silent = true renderToString(new Vue({ render () { throw new Error('foobar') } })).catch(err => { expect(err.toString()).toContain(`foobar`) Vue.config.silent = false done() }) }) 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) }) }