// @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: '<div id="foo" bar="123"></div>'
      },
      result => {
        expect(result).toContain(
          '<div id="foo" bar="123" data-server-rendered="true"></div>'
        )
        done()
      }
    )
  })

  _it('unary tags', done => {
    renderVmWithOptions(
      {
        template: '<input value="123">'
      },
      result => {
        expect(result).toContain(
          '<input value="123" data-server-rendered="true">'
        )
        done()
      }
    )
  })

  _it('dynamic attributes', done => {
    renderVmWithOptions(
      {
        template: '<div qux="quux" :id="foo" :bar="baz"></div>',
        data: {
          foo: 'hi',
          baz: 123
        }
      },
      result => {
        expect(result).toContain(
          '<div qux="quux" id="hi" bar="123" data-server-rendered="true"></div>'
        )
        done()
      }
    )
  })

  _it('static class', done => {
    renderVmWithOptions(
      {
        template: '<div class="foo bar"></div>'
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" class="foo bar"></div>'
        )
        done()
      }
    )
  })

  _it('dynamic class', done => {
    renderVmWithOptions(
      {
        template:
          '<div class="foo bar" :class="[a, { qux: hasQux, quux: hasQuux }]"></div>',
        data: {
          a: 'baz',
          hasQux: true,
          hasQuux: false
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" class="foo bar baz qux"></div>'
        )
        done()
      }
    )
  })

  _it('custom component class', done => {
    renderVmWithOptions(
      {
        template: '<div><cmp class="cmp"></cmp></div>',
        components: {
          cmp: {
            render: h => h('div', 'test')
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><div class="cmp">test</div></div>'
        )
        done()
      }
    )
  })

  _it('nested component class', done => {
    renderVmWithOptions(
      {
        template: '<cmp class="outer" :class="cls"></cmp>',
        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(
          '<div data-server-rendered="true" class="outer success">' +
            '<div class="inner nested error">test</div>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('dynamic style', done => {
    renderVmWithOptions(
      {
        template:
          '<div style="background-color:black" :style="{ fontSize: fontSize + \'px\', color: color }"></div>',
        data: {
          fontSize: 14,
          color: 'red'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" style="background-color:black;font-size:14px;color:red;"></div>'
        )
        done()
      }
    )
  })

  _it('dynamic string style', done => {
    renderVmWithOptions(
      {
        template: '<div :style="style"></div>',
        data: {
          style: 'color:red'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" style="color:red;"></div>'
        )
        done()
      }
    )
  })

  _it('auto-prefixed style value as array', done => {
    renderVmWithOptions(
      {
        template: '<div :style="style"></div>',
        data: {
          style: {
            display: ['-webkit-box', '-ms-flexbox', 'flex']
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" style="display:-webkit-box;display:-ms-flexbox;display:flex;"></div>'
        )
        done()
      }
    )
  })

  _it('custom component style', done => {
    renderVmWithOptions(
      {
        template: '<section><comp :style="style"></comp></section>',
        data: {
          style: 'color:red'
        },
        components: {
          comp: {
            template: '<div></div>'
          }
        }
      },
      result => {
        expect(result).toContain(
          '<section data-server-rendered="true"><div style="color:red;"></div></section>'
        )
        done()
      }
    )
  })

  _it('nested custom component style', done => {
    renderVmWithOptions(
      {
        template: '<comp style="color: blue" :style="style"></comp>',
        data: {
          style: 'color:red'
        },
        components: {
          comp: {
            template:
              '<nested style="text-align: left;" :style="{fontSize:\'520rem\'}"></nested>',
            components: {
              nested: {
                template: '<div></div>'
              }
            }
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" style="text-align:left;font-size:520rem;color:red;"></div>'
        )
        done()
      }
    )
  })

  _it('component style not passed to child', done => {
    renderVmWithOptions(
      {
        template: '<comp :style="style"></comp>',
        data: {
          style: 'color:red'
        },
        components: {
          comp: {
            template: '<div><div></div></div>'
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" style="color:red;"><div></div></div>'
        )
        done()
      }
    )
  })

  _it('component style not passed to slot', done => {
    renderVmWithOptions(
      {
        template:
          '<comp :style="style"><span style="color:black"></span></comp>',
        data: {
          style: 'color:red'
        },
        components: {
          comp: {
            template: '<div><slot></slot></div>'
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" style="color:red;"><span style="color:black;"></span></div>'
        )
        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(
          '<div id="b" data-server-rendered="true" name="c"></div>'
        )
        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(
          '<div data-server-rendered="true" value="c">b</div>'
        )
        done()
      }
    )
  })

  _it('v-show directive render', done => {
    renderVmWithOptions(
      {
        template: '<div v-show="false"><span>inner</span></div>'
      },
      res => {
        expect(res).toContain(
          '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
        )
        done()
      }
    )
  })

  _it('v-show directive merge with style', done => {
    renderVmWithOptions(
      {
        template:
          '<div :style="[{lineHeight: 1}]" v-show="false"><span>inner</span></div>'
      },
      res => {
        expect(res).toContain(
          '<div data-server-rendered="true" style="line-height:1;display:none;"><span>inner</span></div>'
        )
        done()
      }
    )
  })

  _it('v-show directive not passed to child', done => {
    renderVmWithOptions(
      {
        template: '<foo v-show="false"></foo>',
        components: {
          foo: {
            template: '<div><span>inner</span></div>'
          }
        }
      },
      res => {
        expect(res).toContain(
          '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
        )
        done()
      }
    )
  })

  _it('v-show directive not passed to slot', done => {
    renderVmWithOptions(
      {
        template: '<foo v-show="false"><span>inner</span></foo>',
        components: {
          foo: {
            template: '<div><slot></slot></div>'
          }
        }
      },
      res => {
        expect(res).toContain(
          '<div data-server-rendered="true" style="display:none;"><span>inner</span></div>'
        )
        done()
      }
    )
  })

  _it('v-show directive merging on components', done => {
    renderVmWithOptions(
      {
        template: '<foo v-show="false"></foo>',
        components: {
          foo: {
            render: h =>
              h('bar', {
                directives: [
                  {
                    name: 'show',
                    value: true
                  }
                ]
              }),
            components: {
              bar: {
                render: h => h('div', 'inner')
              }
            }
          }
        }
      },
      res => {
        expect(res).toContain(
          '<div data-server-rendered="true" style="display:none;">inner</div>'
        )
        done()
      }
    )
  })

  _it('text interpolation', done => {
    renderVmWithOptions(
      {
        template: '<div>{{ foo }} side {{ bar }}</div>',
        data: {
          foo: 'server',
          bar: '<span>rendering</span>'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true">server side &lt;span&gt;rendering&lt;/span&gt;</div>'
        )
        done()
      }
    )
  })

  _it('v-html on root', done => {
    renderVmWithOptions(
      {
        template: '<div v-html="text"></div>',
        data: {
          text: '<span>foo</span>'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><span>foo</span></div>'
        )
        done()
      }
    )
  })

  _it('v-text on root', done => {
    renderVmWithOptions(
      {
        template: '<div v-text="text"></div>',
        data: {
          text: '<span>foo</span>'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true">&lt;span&gt;foo&lt;/span&gt;</div>'
        )
        done()
      }
    )
  })

  _it('v-html', done => {
    renderVmWithOptions(
      {
        template: '<div><div v-html="text"></div></div>',
        data: {
          text: '<span>foo</span>'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><div><span>foo</span></div></div>'
        )
        done()
      }
    )
  })

  _it('v-html with null value', done => {
    renderVmWithOptions(
      {
        template: '<div><div v-html="text"></div></div>',
        data: {
          text: null
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><div></div></div>'
        )
        done()
      }
    )
  })

  _it('v-text', done => {
    renderVmWithOptions(
      {
        template: '<div><div v-text="text"></div></div>',
        data: {
          text: '<span>foo</span>'
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><div>&lt;span&gt;foo&lt;/span&gt;</div></div>'
        )
        done()
      }
    )
  })

  _it('v-text with null value', done => {
    renderVmWithOptions(
      {
        template: '<div><div v-text="text"></div></div>',
        data: {
          text: null
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><div></div></div>'
        )
        done()
      }
    )
  })

  _it('child component (hoc)', done => {
    renderVmWithOptions(
      {
        template: '<child class="foo" :msg="msg"></child>',
        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(
          '<div data-server-rendered="true" class="foo bar">hello bar</div>'
        )
        done()
      }
    )
  })

  _it('has correct lifecycle during render', done => {
    let lifecycleCount = 1
    renderVmWithOptions(
      {
        template: '<div><span>{{ val }}</span><test></test></div>',
        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(
          '<div data-server-rendered="true">' +
            '<span>hello</span>' +
            '<span class="b">testAsync</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('computed properties', done => {
    renderVmWithOptions(
      {
        template: '<div>{{ b }}</div>',
        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('<div data-server-rendered="true">3</div>')
        done()
      }
    )
  })

  _it('renders async component', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <test-async></test-async>
        </div>
      `,
        components: {
          testAsync(resolve) {
            setTimeout(
              () =>
                resolve({
                  render() {
                    return this.$createElement(
                      'span',
                      { class: ['b'] },
                      'testAsync'
                    )
                  }
                }),
              1
            )
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><span class="b">testAsync</span></div>'
        )
        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(
          `<div data-server-rendered="true"><span>foo</span><span>bar</span></div>`
        )
        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(
          `<div data-server-rendered="true"><span>foo</span><span>bar</span></div>`
        )
        done()
      }
    )
  })

  _it('renders async component (hoc)', done => {
    renderVmWithOptions(
      {
        template: '<test-async></test-async>',
        components: {
          testAsync: () =>
            Promise.resolve({
              render() {
                return this.$createElement(
                  'span',
                  { class: ['b'] },
                  'testAsync'
                )
              }
            })
        }
      },
      result => {
        expect(result).toContain(
          '<span data-server-rendered="true" class="b">testAsync</span>'
        )
        done()
      }
    )
  })

  _it('renders async component (functional, single node)', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <test-async></test-async>
        </div>
      `,
        components: {
          testAsync(resolve) {
            setTimeout(
              () =>
                resolve({
                  functional: true,
                  render(h) {
                    return h('span', { class: ['b'] }, 'testAsync')
                  }
                }),
              1
            )
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><span class="b">testAsync</span></div>'
        )
        done()
      }
    )
  })

  _it('renders async component (functional, multiple nodes)', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <test-async></test-async>
        </div>
      `,
        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(
          '<div data-server-rendered="true">' +
            '<span class="a">foo</span>' +
            '<span class="b">bar</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('renders nested async functional component', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <outer-async></outer-async>
        </div>
      `,
        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(
          '<div data-server-rendered="true">' +
            '<span class="a">inner</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('should catch async component error', done => {
    renderToString(
      new Vue({
        template: '<test-async></test-async>',
        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: `<div><slot name="child"/></div>`
    }
    const Child = {
      template: `<p>child</p>`
    }
    renderVmWithOptions(
      {
        template: `
      <Parent>
        <template #child>
          <Child/>
        </template>
      </Parent>
      `,
        components: {
          Parent,
          Child: () => Promise.resolve(Child)
        }
      },
      result => {
        expect(result).toContain(
          `<div data-server-rendered="true"><p>child</p></div>`
        )
        done()
      }
    )
  })

  _it('everything together', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <p class="hi">yoyo</p>
          <div id="ho" :class="{ red: isRed }"></div>
          <span>{{ test }}</span>
          <input :value="test">
          <img :src="imageUrl">
          <test></test>
          <test-async></test-async>
        </div>
      `,
        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(
          '<div data-server-rendered="true">' +
            '<p class="hi">yoyo</p> ' +
            '<div id="ho" class="red"></div> ' +
            '<span>hi</span> ' +
            '<input value="hi"> ' +
            '<img src="https://vuejs.org/images/logo.png"> ' +
            '<div class="a">test</div> ' +
            '<span class="b">testAsync</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('normal attr', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span :test="'ok'">hello</span>
          <span :test="null">hello</span>
          <span :test="false">hello</span>
          <span :test="true">hello</span>
          <span :test="0">hello</span>
        </div>
      `
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true">' +
            '<span test="ok">hello</span> ' +
            '<span>hello</span> ' +
            '<span>hello</span> ' +
            '<span test="true">hello</span> ' +
            '<span test="0">hello</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('enumerated attr', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span :draggable="true">hello</span>
          <span :draggable="'ok'">hello</span>
          <span :draggable="null">hello</span>
          <span :draggable="false">hello</span>
          <span :draggable="''">hello</span>
          <span :draggable="'false'">hello</span>
        </div>
      `
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true">' +
            '<span draggable="true">hello</span> ' +
            '<span draggable="true">hello</span> ' +
            '<span draggable="false">hello</span> ' +
            '<span draggable="false">hello</span> ' +
            '<span draggable="true">hello</span> ' +
            '<span draggable="false">hello</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('boolean attr', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span :disabled="true">hello</span>
          <span :disabled="'ok'">hello</span>
          <span :disabled="null">hello</span>
          <span :disabled="''">hello</span>
        </div>
      `
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true">' +
            '<span disabled="disabled">hello</span> ' +
            '<span disabled="disabled">hello</span> ' +
            '<span>hello</span> ' +
            '<span disabled="disabled">hello</span>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('v-bind object', done => {
    renderVmWithOptions(
      {
        data: {
          test: { id: 'a', class: ['a', 'b'], value: 'c' }
        },
        template: '<input v-bind="test">'
      },
      result => {
        expect(result).toContain(
          '<input id="a" data-server-rendered="true" value="c" class="a b">'
        )
        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(
          '<p data-server-rendered="true" class="my-class2 my-class1">hello world</p>'
        )
        done()
      }
    )
  })

  _it('custom directives on component', done => {
    const Test = {
      template: '<span>hello world</span>'
    }
    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:
          '<p><Test v-class-prefixer="\'my\'" class="class1" :class="\'class2\'" /></p>',
        components: { Test }
      }),
      (err, result) => {
        expect(err).toBeNull()
        expect(result).toContain(
          '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
        )
        done()
      }
    )
  })

  _it('custom directives on element root of a component', done => {
    const Test = {
      template:
        '<span v-class-prefixer="\'my\'" class="class1" :class="\'class2\'">hello world</span>'
    }
    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: '<p><Test /></p>',
        components: { Test }
      }),
      (err, result) => {
        expect(err).toBeNull()
        expect(result).toContain(
          '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
        )
        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:
          '<p><span v-class-prefixer="\'my\'" class="class1" :class="\'class2\'">hello world</span></p>'
      }),
      (err, result) => {
        expect(err).toBeNull()
        expect(result).toContain(
          '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
        )
        done()
      }
    )
  })

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

  _it('_scopeId', done => {
    renderVmWithOptions(
      {
        _scopeId: '_v-parent',
        template: '<div id="foo"><p><child></child></p></div>',
        components: {
          child: {
            _scopeId: '_v-child',
            render() {
              const h = this.$createElement
              return h('div', null, [h('span', null, ['foo'])])
            }
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div id="foo" data-server-rendered="true" _v-parent>' +
            '<p _v-parent>' +
            '<div _v-child _v-parent><span _v-child>foo</span></div>' +
            '</p>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('_scopeId on slot content', done => {
    renderVmWithOptions(
      {
        _scopeId: '_v-parent',
        template: '<div><child><p>foo</p></child></div>',
        components: {
          child: {
            _scopeId: '_v-child',
            render() {
              const h = this.$createElement
              return h('div', null, this.$slots.default)
            }
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true" _v-parent>' +
            '<div _v-child _v-parent><p _v-child _v-parent>foo</p></div>' +
            '</div>'
        )
        done()
      }
    )
  })

  _it('comment nodes', done => {
    renderVmWithOptions(
      {
        template: '<div><transition><div v-if="false"></div></transition></div>'
      },
      result => {
        expect(result).toContain(
          `<div data-server-rendered="true"><!----></div>`
        )
        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: '"><script>alert(1)</script>'
        },
        template: `
        <div>
          <a :title="xss" :style="{ color: xss }" :class="[xss]">foo</a>
        </div>
      `
      },
      res => {
        expect(res).not.toContain(`<script>alert(1)</script>`)
        done()
      }
    )
  })

  _it('should prevent xss in attribute names', done => {
    renderVmWithOptions(
      {
        data: {
          xss: {
            'foo="bar"></div><script>alert(1)</script>': ''
          }
        },
        template: `
        <div v-bind="xss"></div>
      `
      },
      res => {
        expect(res).not.toContain(`<script>alert(1)</script>`)
        done()
      }
    )
  })

  _it('should prevent xss in attribute names (optimized)', done => {
    renderVmWithOptions(
      {
        data: {
          xss: {
            'foo="bar"></div><script>alert(1)</script>': ''
          }
        },
        template: `
        <div>
          <a v-bind="xss">foo</a>
        </div>
      `
      },
      res => {
        expect(res).not.toContain(`<script>alert(1)</script>`)
        done()
      }
    )
  })

  _it(
    'should prevent script xss with v-bind object syntax + array value',
    done => {
      renderVmWithOptions(
        {
          data: {
            test: ['"><script>alert(1)</script><!--"']
          },
          template: `<div v-bind="{ test }"></div>`
        },
        res => {
          expect(res).not.toContain(`<script>alert(1)</script>`)
          done()
        }
      )
    }
  )

  _it('v-if', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span v-if="true">foo</span>
          <span v-if="false">bar</span>
        </div>
      `
      },
      res => {
        expect(res).toContain(
          `<div data-server-rendered="true"><span>foo</span> <!----></div>`
        )
        done()
      }
    )
  })

  _it('v-for', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span>foo</span>
          <span v-for="i in 2">{{ i }}</span>
        </div>
      `
      },
      res => {
        expect(res).toContain(
          `<div data-server-rendered="true"><span>foo</span> <span>1</span><span>2</span></div>`
        )
        done()
      }
    )
  })

  _it('template v-if', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span>foo</span>
          <template v-if="true">
            <span>foo</span> bar <span>baz</span>
          </template>
        </div>
      `
      },
      res => {
        expect(res).toContain(
          `<div data-server-rendered="true"><span>foo</span> <span>foo</span> bar <span>baz</span></div>`
        )
        done()
      }
    )
  })

  _it('template v-for', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span>foo</span>
          <template v-for="i in 2">
            <span>{{ i }}</span><span>bar</span>
          </template>
        </div>
      `
      },
      res => {
        expect(res).toContain(
          `<div data-server-rendered="true"><span>foo</span> <span>1</span><span>bar</span><span>2</span><span>bar</span></div>`
        )
        done()
      }
    )
  })

  _it('with inheritAttrs: false + $attrs', done => {
    renderVmWithOptions(
      {
        template: `<foo id="a"/>`,
        components: {
          foo: {
            inheritAttrs: false,
            template: `<div><div v-bind="$attrs"></div></div>`
          }
        }
      },
      res => {
        expect(res).toBe(
          `<div data-server-rendered="true"><div id="a"></div></div>`
        )
        done()
      }
    )
  })

  _it('should escape static strings', done => {
    renderVmWithOptions(
      {
        template: `<div>&lt;foo&gt;</div>`
      },
      res => {
        expect(res).toBe(`<div data-server-rendered="true">&lt;foo&gt;</div>`)
        done()
      }
    )
  })

  _it('should not cache computed properties', done => {
    renderVmWithOptions(
      {
        template: `<div>{{ foo }}</div>`,
        data: () => ({ bar: 1 }),
        computed: {
          foo() {
            return this.bar + 1
          }
        },
        created() {
          this.foo // access
          this.bar++ // trigger change
        }
      },
      res => {
        expect(res).toBe(`<div data-server-rendered="true">3</div>`)
        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: '<div>{{ fullName }}</div>'
      }),
      (err, result) => {
        expect(err).toBeNull()
        expect(result).toContain(
          '<div data-server-rendered="true">Evan You</div>'
        )
        done()
      }
    )
  })

  _it('return Promise', async () => {
    await renderToString(
      new Vue({
        template: `<div>{{ foo }}</div>`,
        data: { foo: 'bar' }
      })
    )!.then(res => {
      expect(res).toBe(`<div data-server-rendered="true">bar</div>`)
    })
  })

  _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: `<div></div><div></div>`
      }),
      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: `<input :type="'text'" v-model="foo">`
      },
      res => {
        expect(res).toBe(
          `<input type="text" data-server-rendered="true" value="123">`
        )
        done()
      }
    )
  })

  _it('render muted properly', done => {
    renderVmWithOptions(
      {
        template: '<video muted></video>'
      },
      result => {
        expect(result).toContain(
          '<video muted="muted" data-server-rendered="true"></video>'
        )
        done()
      }
    )
  })

  _it('render v-model with textarea', done => {
    renderVmWithOptions(
      {
        data: { foo: 'bar' },
        template: '<div><textarea v-model="foo"></textarea></div>'
      },
      result => {
        expect(result).toContain('<textarea>bar</textarea>')
        done()
      }
    )
  })

  _it('render v-model with textarea (non-optimized)', done => {
    renderVmWithOptions(
      {
        render(h) {
          return h('textarea', {
            domProps: {
              value: 'foo'
            }
          })
        }
      },
      result => {
        expect(result).toContain(
          '<textarea data-server-rendered="true">foo</textarea>'
        )
        done()
      }
    )
  })

  _it('render v-model with <select> (value binding)', done => {
    renderVmWithOptions(
      {
        data: {
          selected: 2,
          options: [
            { id: 1, label: 'one' },
            { id: 2, label: 'two' }
          ]
        },
        template: `
      <div>
        <select v-model="selected">
          <option v-for="o in options" :value="o.id">{{ o.label }}</option>
        </select>
      </div>
      `
      },
      result => {
        expect(result).toContain(
          '<select>' +
            '<option value="1">one</option>' +
            '<option selected="selected" value="2">two</option>' +
            '</select>'
        )
        done()
      }
    )
  })

  _it('render v-model with <select> (static value)', done => {
    renderVmWithOptions(
      {
        data: {
          selected: 2
        },
        template: `
      <div>
        <select v-model="selected">
          <option value="1">one</option>
          <option value="2">two</option>
        </select>
      </div>
      `
      },
      result => {
        expect(result).toContain(
          '<select>' +
            '<option value="1">one</option> ' +
            '<option value="2" selected="selected">two</option>' +
            '</select>'
        )
        done()
      }
    )
  })

  _it('render v-model with <select> (text as value)', done => {
    renderVmWithOptions(
      {
        data: {
          selected: 2,
          options: [
            { id: 1, label: 'one' },
            { id: 2, label: 'two' }
          ]
        },
        template: `
      <div>
        <select v-model="selected">
          <option v-for="o in options">{{ o.id }}</option>
        </select>
      </div>
      `
      },
      result => {
        expect(result).toContain(
          '<select>' +
            '<option>1</option>' +
            '<option selected="selected">2</option>' +
            '</select>'
        )
        done()
      }
    )
  })

  // #7223
  _it('should not double escape attribute values', done => {
    renderVmWithOptions(
      {
        template: `
      <div>
        <div id="a\nb"></div>
      </div>
      `
      },
      result => {
        expect(result).toContain(`<div id="a\nb"></div>`)
        done()
      }
    )
  })

  // #7859
  _it('should not double escape class values', done => {
    renderVmWithOptions(
      {
        template: `
      <div>
        <div class="a\nb"></div>
      </div>
      `
      },
      result => {
        expect(result).toContain(`<div class="a b"></div>`)
        done()
      }
    )
  })

  _it('should expose ssr helpers on functional context', done => {
    let called = false
    renderVmWithOptions(
      {
        template: `<div><foo/></div>`,
        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: `
        <div>{{ count }}</div>
      `,
        data: {
          count: 0
        },
        serverPrefetch() {
          return new Promise<void>(resolve => {
            setTimeout(() => {
              this.count = 42
              resolve()
            }, 1)
          })
        }
      },
      result => {
        expect(result).toContain('<div data-server-rendered="true">42</div>')
        done()
      }
    )
  })

  _it('should support serverPrefetch option (nested)', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span>{{ count }}</span>
          <nested-prefetch></nested-prefetch>
        </div>
      `,
        data: {
          count: 0
        },
        serverPrefetch() {
          return new Promise<void>(resolve => {
            setTimeout(() => {
              this.count = 42
              resolve()
            }, 1)
          })
        },
        components: {
          nestedPrefetch: {
            template: `
            <div>{{ message }}</div>
          `,
            data() {
              return {
                message: ''
              }
            },
            serverPrefetch() {
              return new Promise<void>(resolve => {
                setTimeout(() => {
                  this.message = 'vue.js'
                  resolve()
                }, 1)
              })
            }
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>'
        )
        done()
      }
    )
  })

  _it('should support serverPrefetch option (nested async)', done => {
    renderVmWithOptions(
      {
        template: `
        <div>
          <span>{{ count }}</span>
          <nested-prefetch></nested-prefetch>
        </div>
      `,
        data: {
          count: 0
        },
        serverPrefetch() {
          return new Promise<void>(resolve => {
            setTimeout(() => {
              this.count = 42
              resolve()
            }, 1)
          })
        },
        components: {
          nestedPrefetch(resolve) {
            resolve({
              template: `
              <div>{{ message }}</div>
            `,
              data() {
                return {
                  message: ''
                }
              },
              serverPrefetch() {
                return new Promise<void>(resolve => {
                  setTimeout(() => {
                    this.message = 'vue.js'
                    resolve()
                  }, 1)
                })
              }
            })
          }
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>'
        )
        done()
      }
    )
  })

  _it('should merge serverPrefetch option', done => {
    const mixin = {
      data: {
        message: ''
      },
      serverPrefetch() {
        return new Promise<void>(resolve => {
          setTimeout(() => {
            this.message = 'vue.js'
            resolve()
          }, 1)
        })
      }
    }
    renderVmWithOptions(
      {
        mixins: [mixin],
        template: `
        <div>
          <span>{{ count }}</span>
          <div>{{ message }}</div>
        </div>
      `,
        data: {
          count: 0
        },
        serverPrefetch() {
          return new Promise<void>(resolve => {
            setTimeout(() => {
              this.count = 42
              resolve()
            }, 1)
          })
        }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><span>42</span> <div>vue.js</div></div>'
        )
        done()
      }
    )
  })

  _it(
    `should skip serverPrefetch option that doesn't return a promise`,
    done => {
      renderVmWithOptions(
        {
          template: `
        <div>{{ count }}</div>
      `,
          data: {
            count: 0
          },
          serverPrefetch() {
            setTimeout(() => {
              this.count = 42
            }, 1)
          }
        },
        result => {
          expect(result).toContain('<div data-server-rendered="true">0</div>')
          done()
        }
      )
    }
  )

  _it('should call context.rendered', done => {
    let a = 0
    renderToString(
      new Vue({
        template: '<div>Hello</div>'
      }),
      {
        rendered: () => {
          a = 42
        }
      },
      (err, res) => {
        expect(err).toBeNull()
        expect(res).toContain('<div data-server-rendered="true">Hello</div>')
        expect(a).toBe(42)
        done()
      }
    )
  })

  _it('invalid style value', done => {
    renderVmWithOptions(
      {
        template: '<div :style="style"><p :style="style2"/></div>',
        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(
          '<div data-server-rendered="true"><p style="opacity:0;"></p></div>'
        )
        done()
      }
    )
  })

  _it('numeric style value', done => {
    renderVmWithOptions(
      {
        template: '<div :style="style"></div>',
        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(
          '<div data-server-rendered="true" style="opacity:0;top:0;margin-top:10px;"></div>'
        )
        done()
      }
    )
  })

  _it('handling max stack size limit', done => {
    const vueInstance = new Vue({
      template: `<div class="root">
        <child v-for="(x, i) in items" :key="i"></child>
      </div>`,
      components: {
        child: {
          template: '<div class="child"><span class="child">hi</span></div>'
        }
      },
      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(
          '<div data-server-rendered="true"><textarea></textarea></div>'
        )
        done()
      }
    )
  })

  _it('Options inheritAttrs in parent component', done => {
    const childComponent = {
      template: `<div>{{ someProp }}</div>`,
      props: {
        someProp: {}
      }
    }
    const parentComponent = {
      template: `<childComponent v-bind="$attrs" />`,
      components: { childComponent },
      inheritAttrs: false
    }
    renderVmWithOptions(
      {
        template: `
        <div>
          <parentComponent some-prop="some-val" />
        </div>
        `,
        components: { parentComponent }
      },
      result => {
        expect(result).toContain(
          '<div data-server-rendered="true"><div>some-val</div></div>'
        )
        done()
      }
    )
  })
})

function renderVmWithOptions(options, cb) {
  renderToString(new Vue(options), (err, res) => {
    expect(err).toBeNull()
    cb(res)
  })
}