vue3-core/packages/compiler-ssr/__tests__/ssrElement.spec.ts

556 lines
19 KiB
TypeScript

import { getCompiledString } from './utils'
import { compile } from '../src'
import { renderToString } from '@vue/server-renderer'
import { createApp } from '@vue/runtime-dom'
describe('ssr: element', () => {
test('basic elements', () => {
expect(getCompiledString(`<div></div>`)).toMatchInlineSnapshot(
`"\`<div></div>\`"`,
)
expect(getCompiledString(`<div/>`)).toMatchInlineSnapshot(
`"\`<div></div>\`"`,
)
})
test('nested elements', () => {
expect(
getCompiledString(`<div><span></span><span></span></div>`),
).toMatchInlineSnapshot(`"\`<div><span></span><span></span></div>\`"`)
})
test('void element', () => {
expect(getCompiledString(`<input>`)).toMatchInlineSnapshot(`"\`<input>\`"`)
})
describe('children override', () => {
test('v-html', () => {
expect(getCompiledString(`<div v-html="foo"/>`)).toMatchInlineSnapshot(`
"\`<div>\${
(_ctx.foo) ?? ''
}</div>\`"
`)
})
test('v-text', () => {
expect(getCompiledString(`<div v-text="foo"/>`)).toMatchInlineSnapshot(`
"\`<div>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('<textarea> with dynamic value', () => {
expect(getCompiledString(`<textarea :value="foo"/>`))
.toMatchInlineSnapshot(`
"\`<textarea>\${
_ssrInterpolate(_ctx.foo)
}</textarea>\`"
`)
})
test('<textarea> with static value', () => {
expect(
getCompiledString(`<textarea value="fo&gt;o"/>`),
).toMatchInlineSnapshot(`"\`<textarea>fo&gt;o</textarea>\`"`)
})
test('<textarea> with dynamic v-bind', () => {
expect(compile(`<textarea v-bind="obj">fallback</textarea>`).code)
.toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
let _temp0
_push(\`<textarea\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, _attrs), "textarea")
}>\${
_ssrInterpolate(("value" in _temp0) ? _temp0.value : "fallback")
}</textarea>\`)
}"
`)
})
test('<select> with dynamic value assigns `selected` option attribute', async () => {
expect(
getCompiledString(
`<select :value="selectValue"><option value="1"></option></select>`,
),
).toMatchInlineSnapshot(`
"\`<select><option value="1"\${
(_ssrIncludeBooleanAttr((Array.isArray(_ctx.selectValue))
? _ssrLooseContain(_ctx.selectValue, "1")
: _ssrLooseEqual(_ctx.selectValue, "1"))) ? " selected" : ""
}></option></select>\`"
`)
expect(
await renderToString(
createApp({
data: () => ({ selected: 2 }),
template: `<div><select :value="selected"><option value="1">1</option><option value="2">2</option></select></div>`,
}),
),
).toMatchInlineSnapshot(
`"<div><select><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
)
})
test('<select> with static value assigns `selected` option attribute', async () => {
expect(
getCompiledString(
`<select value="selectValue"><option value="1"></option></select>`,
),
).toMatchInlineSnapshot(`
"\`<select><option value="1"\${
(_ssrIncludeBooleanAttr(_ssrLooseEqual("selectValue", "1"))) ? " selected" : ""
}></option></select>\`"
`)
expect(
await renderToString(
createApp({
template: `<div><select value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
}),
),
).toMatchInlineSnapshot(
`"<div><select><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
)
})
test('<select> with dynamic v-bind assigns `selected` option attribute', async () => {
expect(
compile(`<select v-bind="obj"><option value="1"></option></select>`)
.code,
).toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
let _temp0
_push(\`<select\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, _attrs))
}><option value="1"\${
(_ssrIncludeBooleanAttr(("value" in _temp0)
? (Array.isArray(_temp0.value))
? _ssrLooseContain(_temp0.value, "1")
: _ssrLooseEqual(_temp0.value, "1")
: false)) ? " selected" : ""
}></option></select>\`)
}"
`)
expect(
await renderToString(
createApp({
data: () => ({ obj: { value: 2 } }),
template: `<div><select v-bind="obj"><option value="1">1</option><option value="2">2</option></select></div>`,
}),
),
).toMatchInlineSnapshot(
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
)
})
test('<select> with dynamic v-bind and dynamic value bind assigns `selected` option attribute', async () => {
expect(
compile(
`<select v-bind="obj" :value="selectValue"><option value="1"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
let _temp0
_push(\`<select\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, { value: _ctx.selectValue }, _attrs))
}><option value="1"\${
(_ssrIncludeBooleanAttr(("value" in _temp0)
? (Array.isArray(_temp0.value))
? _ssrLooseContain(_temp0.value, "1")
: _ssrLooseEqual(_temp0.value, "1")
: false)) ? " selected" : ""
}></option></select>\`)
}"
`)
expect(
await renderToString(
createApp({
data: () => ({ obj: { value: 1 } }),
template: `<div><select v-bind="obj" :value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
}),
),
).toMatchInlineSnapshot(
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
)
})
test('<select> with dynamic v-bind and static value bind assigns `selected` option attribute', async () => {
expect(
compile(
`<select v-bind="obj" value="selectValue"><option value="1"></option></select>`,
).code,
).toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs, ssrIncludeBooleanAttr: _ssrIncludeBooleanAttr, ssrLooseContain: _ssrLooseContain, ssrLooseEqual: _ssrLooseEqual } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
let _temp0
_push(\`<select\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.obj, { value: "selectValue" }, _attrs))
}><option value="1"\${
(_ssrIncludeBooleanAttr(("value" in _temp0)
? (Array.isArray(_temp0.value))
? _ssrLooseContain(_temp0.value, "1")
: _ssrLooseEqual(_temp0.value, "1")
: false)) ? " selected" : ""
}></option></select>\`)
}"
`)
expect(
await renderToString(
createApp({
data: () => ({ obj: { value: 1 } }),
template: `<div><select v-bind="obj" value="2"><option value="1">1</option><option value="2">2</option></select></div>`,
}),
),
).toMatchInlineSnapshot(
`"<div><select value="2"><option value="1">1</option><option value="2" selected>2</option></select></div>"`,
)
})
test('multiple _ssrInterpolate at parent and child import dependency once', () => {
expect(
compile(`<div>{{ hello }}<textarea v-bind="a"></textarea></div>`).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
let _temp0
_push(\`<div\${
_ssrRenderAttrs(_attrs)
}>\${
_ssrInterpolate(_ctx.hello)
}<textarea\${
_ssrRenderAttrs(_temp0 = _ctx.a, "textarea")
}>\${
_ssrInterpolate(("value" in _temp0) ? _temp0.value : "")
}</textarea></div>\`)
}"
`)
})
test('should pass tag to custom elements w/ dynamic v-bind', () => {
expect(
compile(`<my-foo v-bind="obj"></my-foo>`, {
isCustomElement: () => true,
}).code,
).toMatchInlineSnapshot(`
"const { mergeProps: _mergeProps } = require("vue")
const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<my-foo\${_ssrRenderAttrs(_mergeProps(_ctx.obj, _attrs), "my-foo")}></my-foo>\`)
}"
`)
})
})
describe('attrs', () => {
test('static attrs', () => {
expect(
getCompiledString(`<div id="foo" class="bar"></div>`),
).toMatchInlineSnapshot(`"\`<div id="foo" class="bar"></div>\`"`)
})
test('ignore static key/ref', () => {
expect(
getCompiledString(`<div key="1" ref="el"></div>`),
).toMatchInlineSnapshot(`"\`<div></div>\`"`)
})
test('ignore v-bind key/ref', () => {
expect(
getCompiledString(`<div :key="1" :ref="el"></div>`),
).toMatchInlineSnapshot(`"\`<div></div>\`"`)
})
test('v-bind:class', () => {
expect(getCompiledString(`<div id="foo" :class="bar"></div>`))
.toMatchInlineSnapshot(`
"\`<div id="foo" class="\${
_ssrRenderClass(_ctx.bar)
}"></div>\`"
`)
})
test('static class + v-bind:class', () => {
expect(getCompiledString(`<div class="foo" :class="bar"></div>`))
.toMatchInlineSnapshot(`
"\`<div class="\${
_ssrRenderClass([_ctx.bar, "foo"])
}"></div>\`"
`)
})
test('v-bind:class + static class', () => {
expect(getCompiledString(`<div :class="bar" class="foo"></div>`))
.toMatchInlineSnapshot(`
"\`<div class="\${
_ssrRenderClass([_ctx.bar, "foo"])
}"></div>\`"
`)
})
test('v-bind:style', () => {
expect(getCompiledString(`<div id="foo" :style="bar"></div>`))
.toMatchInlineSnapshot(`
"\`<div id="foo" style="\${
_ssrRenderStyle(_ctx.bar)
}"></div>\`"
`)
})
test('static style + v-bind:style', () => {
expect(getCompiledString(`<div style="color:red;" :style="bar"></div>`))
.toMatchInlineSnapshot(`
"\`<div style="\${
_ssrRenderStyle([{"color":"red"}, _ctx.bar])
}"></div>\`"
`)
})
test('v-bind:arg (boolean)', () => {
expect(getCompiledString(`<input type="checkbox" :checked="checked">`))
.toMatchInlineSnapshot(`
"\`<input type="checkbox"\${
(_ssrIncludeBooleanAttr(_ctx.checked)) ? " checked" : ""
}>\`"
`)
})
test('v-bind:arg (non-boolean)', () => {
expect(getCompiledString(`<div :id="id" class="bar"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttr("id", _ctx.id)
} class="bar"></div>\`"
`)
})
test('v-bind:[arg]', () => {
expect(getCompiledString(`<div v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs({ [_ctx.key || ""]: _ctx.value })
}></div>\`"
`)
expect(getCompiledString(`<div class="foo" v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs({
class: "foo",
[_ctx.key || ""]: _ctx.value
})
}></div>\`"
`)
expect(getCompiledString(`<div :id="id" v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs({
id: _ctx.id,
[_ctx.key || ""]: _ctx.value
})
}></div>\`"
`)
})
test('v-bind="obj"', () => {
expect(getCompiledString(`<div v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ctx.obj)
}></div>\`"
`)
expect(getCompiledString(`<div class="foo" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ class: "foo" }, _ctx.obj))
}></div>\`"
`)
expect(getCompiledString(`<div :id="id" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ id: _ctx.id }, _ctx.obj))
}></div>\`"
`)
// dynamic key + v-bind="object"
expect(getCompiledString(`<div :[key]="id" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ [_ctx.key || ""]: _ctx.id }, _ctx.obj))
}></div>\`"
`)
// should merge class and :class
expect(getCompiledString(`<div class="a" :class="b" v-bind="obj"></div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({
class: ["a", _ctx.b]
}, _ctx.obj))
}></div>\`"
`)
// should merge style and :style
expect(
getCompiledString(
`<div style="color:red;" :style="b" v-bind="obj"></div>`,
),
).toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({
style: [{"color":"red"}, _ctx.b]
}, _ctx.obj))
}></div>\`"
`)
})
test('should ignore v-on', () => {
expect(
getCompiledString(`<div id="foo" @click="bar"/>`),
).toMatchInlineSnapshot(`"\`<div id="foo"></div>\`"`)
expect(
getCompiledString(`<div id="foo" v-on="bar"/>`),
).toMatchInlineSnapshot(`"\`<div id="foo"></div>\`"`)
expect(getCompiledString(`<div v-bind="foo" v-on="bar"/>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ctx.foo)
}></div>\`"
`)
})
})
describe('custom directives', () => {
// #8112 should respect textContent / innerHTML from directive getSSRProps
// if the element has no children
test('custom dir without children', () => {
expect(getCompiledString(`<div v-xxx:x.y="z" />`)).toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_temp0 = _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, "x", { y: true }))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with children', () => {
expect(getCompiledString(`<div v-xxx:x.y="z">hello</div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, "x", { y: true }))
}>hello</div>\`"
`)
})
test('custom dir with normal attrs', () => {
expect(getCompiledString(`<div class="foo" v-xxx />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_temp0 = _mergeProps({ class: "foo" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with v-bind', () => {
expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_temp0 = _mergeProps({
title: _ctx.foo,
class: _ctx.bar
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with v-text', () => {
expect(getCompiledString(`<div v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with v-text and normal attrs', () => {
expect(getCompiledString(`<div class="test" v-xxx v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ class: "test" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('mulptiple custom dirs with v-text', () => {
expect(getCompiledString(`<div v-xxx v-yyy v-text="foo" />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps(_ssrGetDirectiveProps(_ctx, _directive_xxx), _ssrGetDirectiveProps(_ctx, _directive_yyy)))
}>\${
_ssrInterpolate(_ctx.foo)
}</div>\`"
`)
})
test('custom dir with object v-bind', () => {
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with object v-bind + normal bindings', () => {
expect(
getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`),
).toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.x, {
class: "foo",
title: "bar"
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
})
})