]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(ssr): tests for utils and props rendering
authorEvan You <yyx990803@gmail.com>
Wed, 29 Jan 2020 20:10:45 +0000 (15:10 -0500)
committerEvan You <yyx990803@gmail.com>
Wed, 29 Jan 2020 20:10:45 +0000 (15:10 -0500)
packages/runtime-core/src/vnode.ts
packages/server-renderer/__tests__/escape.spec.ts [deleted file]
packages/server-renderer/__tests__/renderProps.spec.ts
packages/server-renderer/__tests__/ssrUtils.spec.ts [new file with mode: 0644]
packages/server-renderer/src/index.ts
packages/server-renderer/src/renderProps.ts
packages/server-renderer/src/renderToString.ts
packages/server-renderer/src/ssrUtils.ts [moved from packages/server-renderer/src/escape.ts with 82% similarity]
packages/shared/src/domAttrConfig.ts

index 2552f6c0d7e834697359fba5642b72a8ac930041..97b09eb0d8f78dc7c686e4ca3a501c4f57dbb351 100644 (file)
@@ -223,7 +223,7 @@ export function createVNode(
     if (klass != null && !isString(klass)) {
       props.class = normalizeClass(klass)
     }
-    if (style != null) {
+    if (isObject(style)) {
       // reactive state objects need to be cloned since they are likely to be
       // mutated
       if (isReactive(style) && !isArray(style)) {
diff --git a/packages/server-renderer/__tests__/escape.spec.ts b/packages/server-renderer/__tests__/escape.spec.ts
deleted file mode 100644 (file)
index 6e4c6a2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-test('ssr: escape HTML', () => {})
index b1bbe453e1064a1c9242b6ac16c0f268e5da3173..f5a6f60da28b07ab061347c4cafd152d94093df9 100644 (file)
-describe('ssr: render props', () => {
-  test('class', () => {})
+import { renderProps, renderClass, renderStyle } from '../src'
 
-  test('style', () => {
-    // only render numbers for properties that allow no unit numbers
+describe('ssr: renderProps', () => {
+  test('ignore reserved props', () => {
+    expect(
+      renderProps({
+        key: 1,
+        ref: () => {},
+        onClick: () => {}
+      })
+    ).toBe('')
   })
 
-  test('normal attrs', () => {})
+  test('normal attrs', () => {
+    expect(
+      renderProps({
+        id: 'foo',
+        title: 'bar'
+      })
+    ).toBe(` id="foo" title="bar"`)
+  })
+
+  test('escape attrs', () => {
+    expect(
+      renderProps({
+        id: '"><script'
+      })
+    ).toBe(` id="&quot;&gt;&lt;script"`)
+  })
+
+  test('boolean attrs', () => {
+    expect(
+      renderProps({
+        checked: true,
+        multiple: false
+      })
+    ).toBe(` checked`) // boolean attr w/ false should be ignored
+  })
+
+  test('ignore falsy values', () => {
+    expect(
+      renderProps({
+        foo: false,
+        title: null,
+        baz: undefined
+      })
+    ).toBe(` foo="false"`) // non boolean should render `false` as is
+  })
+
+  test('props to attrs', () => {
+    expect(
+      renderProps({
+        readOnly: true, // simple lower case conversion
+        htmlFor: 'foobar' // special cases
+      })
+    ).toBe(` readonly for="foobar"`)
+  })
+})
 
-  test('boolean attrs', () => {})
+describe('ssr: renderClass', () => {
+  test('via renderProps', () => {
+    expect(
+      renderProps({
+        class: ['foo', 'bar']
+      })
+    ).toBe(` class="foo bar"`)
+  })
+
+  test('standalone', () => {
+    expect(renderClass(`foo`)).toBe(`foo`)
+    expect(renderClass([`foo`, `bar`])).toBe(`foo bar`)
+    expect(renderClass({ foo: true, bar: false })).toBe(`foo`)
+    expect(renderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
+  })
+
+  test('escape class values', () => {
+    expect(renderClass(`"><script`)).toBe(`&quot;&gt;&lt;script`)
+  })
+})
 
-  test('enumerated attrs', () => {})
+describe('ssr: renderStyle', () => {
+  test('via renderProps', () => {
+    expect(
+      renderProps({
+        style: {
+          color: 'red'
+        }
+      })
+    ).toBe(` style="color:red;"`)
+  })
 
-  test('ignore falsy values', () => {})
+  test('standalone', () => {
+    expect(renderStyle(`color:red`)).toBe(`color:red`)
+    expect(
+      renderStyle({
+        color: `red`
+      })
+    ).toBe(`color:red;`)
+    expect(
+      renderStyle([
+        { color: `red` },
+        { fontSize: `15px` } // case conversion
+      ])
+    ).toBe(`color:red;font-size:15px;`)
+  })
 
-  test('props to attrs', () => {})
+  test('number handling', () => {
+    expect(
+      renderStyle({
+        fontSize: 15, // should be ignored since font-size requires unit
+        opacity: 0.5
+      })
+    ).toBe(`opacity:0.5;`)
+  })
 
-  test('ignore non-renderable props', () => {})
+  test('escape inline CSS', () => {
+    expect(renderStyle(`"><script`)).toBe(`&quot;&gt;&lt;script`)
+    expect(
+      renderStyle({
+        color: `"><script`
+      })
+    ).toBe(`color:&quot;&gt;&lt;script;`)
+  })
 })
diff --git a/packages/server-renderer/__tests__/ssrUtils.spec.ts b/packages/server-renderer/__tests__/ssrUtils.spec.ts
new file mode 100644 (file)
index 0000000..191f915
--- /dev/null
@@ -0,0 +1,38 @@
+import { escapeHtml, interpolate } from '../src'
+
+test('ssr: escapeHTML', () => {
+  expect(escapeHtml(`foo`)).toBe(`foo`)
+  expect(escapeHtml(true)).toBe(`true`)
+  expect(escapeHtml(false)).toBe(`false`)
+  expect(escapeHtml(`a && b`)).toBe(`a &amp;&amp; b`)
+  expect(escapeHtml(`"foo"`)).toBe(`&quot;foo&quot;`)
+  expect(escapeHtml(`'bar'`)).toBe(`&#39;bar&#39;`)
+  expect(escapeHtml(`<div>`)).toBe(`&lt;div&gt;`)
+})
+
+test('ssr: interpolate', () => {
+  expect(interpolate(0)).toBe(`0`)
+  expect(interpolate(`foo`)).toBe(`foo`)
+  expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`)
+  // should escape interpolated values
+  expect(interpolate([1, 2, 3])).toBe(
+    escapeHtml(JSON.stringify([1, 2, 3], null, 2))
+  )
+  expect(
+    interpolate({
+      foo: 1,
+      bar: `<div>`
+    })
+  ).toBe(
+    escapeHtml(
+      JSON.stringify(
+        {
+          foo: 1,
+          bar: `<div>`
+        },
+        null,
+        2
+      )
+    )
+  )
+})
index 22885a5824fa726a2bf3891bb3ab39b0edf07a6a..4a4d0fbf3f2a9515a049aa923b867614ef1b9bc8 100644 (file)
@@ -1,11 +1,3 @@
-import { toDisplayString } from 'vue'
-import { escape } from './escape'
-
-export { escape }
-
-export function interpolate(value: unknown) {
-  return escape(toDisplayString(value))
-}
-
 export { renderToString, renderComponent, renderSlot } from './renderToString'
 export { renderClass, renderStyle, renderProps } from './renderProps'
+export { escapeHtml, interpolate } from './ssrUtils'
index ce0abf2b5483234a3774c3c14f0d7846ff944c7a..112055d89780abd965736cf76f1089987ce5e7d4 100644 (file)
@@ -1,4 +1,4 @@
-import { escape } from './escape'
+import { escapeHtml } from './ssrUtils'
 import {
   normalizeClass,
   normalizeStyle,
@@ -9,7 +9,7 @@ import {
   isOn,
   isSSRSafeAttrName,
   isBooleanAttr
-} from '@vue/shared/src'
+} from '@vue/shared'
 
 export function renderProps(
   props: Record<string, unknown>,
@@ -34,7 +34,7 @@ export function renderProps(
           ret += ` ${attrKey}`
         }
       } else if (isSSRSafeAttrName(attrKey)) {
-        ret += ` ${attrKey}="${escape(value)}"`
+        ret += ` ${attrKey}="${escapeHtml(value)}"`
       }
     }
   }
@@ -42,13 +42,16 @@ export function renderProps(
 }
 
 export function renderClass(raw: unknown): string {
-  return escape(normalizeClass(raw))
+  return escapeHtml(normalizeClass(raw))
 }
 
 export function renderStyle(raw: unknown): string {
   if (!raw) {
     return ''
   }
+  if (isString(raw)) {
+    return escapeHtml(raw)
+  }
   const styles = normalizeStyle(raw)
   let ret = ''
   for (const key in styles) {
@@ -62,5 +65,5 @@ export function renderStyle(raw: unknown): string {
       ret += `${normalizedKey}:${value};`
     }
   }
-  return escape(ret)
+  return escapeHtml(ret)
 }
index 291593aa50293c5780160f031e6ecaddf062cf6a..a88c62c353a9a5d85697aed2222f105cff23d954 100644 (file)
@@ -22,7 +22,7 @@ import {
   isVoidTag
 } from '@vue/shared'
 import { renderProps } from './renderProps'
-import { escape } from './escape'
+import { escapeHtml } from './ssrUtils'
 
 const {
   createComponentInstance,
@@ -105,7 +105,7 @@ function renderComponentVNode(
   const instance = createComponentInstance(vnode, parentComponent)
   const res = setupComponent(
     instance,
-    null /* parentSuspense */,
+    null /* parentSuspense (no need to track for SSR) */,
     true /* isSSR */
   )
   if (isPromise(res)) {
@@ -225,15 +225,15 @@ function renderElement(
         push(props.innerHTML)
       } else if (props.textContent) {
         hasChildrenOverride = true
-        push(escape(props.textContent))
+        push(escapeHtml(props.textContent))
       } else if (tag === 'textarea' && props.value) {
         hasChildrenOverride = true
-        push(escape(props.value))
+        push(escapeHtml(props.value))
       }
     }
     if (!hasChildrenOverride) {
       if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
-        push(escape(children as string))
+        push(escapeHtml(children as string))
       } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
         renderVNodeChildren(
           push,
similarity index 82%
rename from packages/server-renderer/src/escape.ts
rename to packages/server-renderer/src/ssrUtils.ts
index 6cfe940393a7909112a1ab7ce714455f82d5f195..9c71e90204b6ddbaeb12f138c3f19debdb8bab4c 100644 (file)
@@ -1,6 +1,8 @@
+import { toDisplayString } from '@vue/shared'
+
 const escapeRE = /["'&<>]/
 
-export function escape(string: unknown) {
+export function escapeHtml(string: unknown) {
   const str = '' + string
   const match = escapeRE.exec(str)
 
@@ -43,3 +45,7 @@ export function escape(string: unknown) {
 
   return lastIndex !== index ? html + str.substring(lastIndex, index) : html
 }
+
+export function interpolate(value: unknown) {
+  return escapeHtml(toDisplayString(value))
+}
index 0187ea289110e2bf1f1837aeaabbe3947d3aad82..5ae2ab7a2d96e3e1dee60ef9a52bba16094af183 100644 (file)
@@ -15,8 +15,8 @@ export const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs)
 // The full list is needed during SSR to produce the correct initial markup.
 export const isBooleanAttr = /*#__PURE__*/ makeMap(
   specialBooleanAttrs +
-    `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,ismap,` +
-    `loop,nomodule,open,required,reversed,scoped,seamless,` +
+    `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
+    `loop,open,required,reversed,scoped,seamless,` +
     `checked,muted,multiple,selected`
 )