]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: implement basic test renderer
authorEvan You <yyx990803@gmail.com>
Mon, 1 Oct 2018 17:15:07 +0000 (13:15 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 1 Oct 2018 17:15:07 +0000 (13:15 -0400)
packages/renderer-test/.npmignore [new file with mode: 0644]
packages/renderer-test/README.md [new file with mode: 0644]
packages/renderer-test/__tests__/testRenderer.spec.ts [new file with mode: 0644]
packages/renderer-test/index.js [new file with mode: 0644]
packages/renderer-test/package.json [new file with mode: 0644]
packages/renderer-test/src/index.ts [new file with mode: 0644]
packages/renderer-test/src/nodeOps.ts [new file with mode: 0644]
tsconfig.json

diff --git a/packages/renderer-test/.npmignore b/packages/renderer-test/.npmignore
new file mode 100644 (file)
index 0000000..bb5c8a5
--- /dev/null
@@ -0,0 +1,3 @@
+__tests__/
+__mocks__/
+dist/packages
\ No newline at end of file
diff --git a/packages/renderer-test/README.md b/packages/renderer-test/README.md
new file mode 100644 (file)
index 0000000..177e2b0
--- /dev/null
@@ -0,0 +1,34 @@
+# @vue/renderer-test
+
+``` js
+import {
+  h,
+  render,
+  Component,
+  nodeOps,
+  startRecordingOps,
+  dumpOps
+} from '@vue/renderer-test'
+
+class App extends Component {
+  data () {
+    return {
+      msg: 'Hello World!'
+    }
+  }
+  render () {
+    return h('div', this.msg)
+  }
+}
+
+// root is of type `TestElement` as defined in src/nodeOps.ts
+const root = nodeOps.createElement('div')
+
+startRecordingOps()
+
+render(h(App), root)
+
+const ops = dumpOps()
+
+console.log(ops)
+```
diff --git a/packages/renderer-test/__tests__/testRenderer.spec.ts b/packages/renderer-test/__tests__/testRenderer.spec.ts
new file mode 100644 (file)
index 0000000..3974388
--- /dev/null
@@ -0,0 +1,42 @@
+import {
+  h,
+  render,
+  Component,
+  nodeOps,
+  NodeTypes,
+  TestElement,
+  TestText
+} from '../src'
+
+describe('test renderer', () => {
+  it('should work', () => {
+    class App extends Component {
+      render() {
+        return h(
+          'div',
+          {
+            id: 'test'
+          },
+          'hello'
+        )
+      }
+    }
+    const root = nodeOps.createElement('div')
+    render(h(App), root)
+
+    expect(root.children.length).toBe(1)
+
+    const el = root.children[0] as TestElement
+    expect(el.type).toBe(NodeTypes.ELEMENT)
+    expect(el.props.id).toBe('test')
+    expect(el.children.length).toBe(1)
+
+    const text = el.children[0] as TestText
+    expect(text.type).toBe(NodeTypes.TEXT)
+    expect(text.text).toBe('hello')
+  })
+
+  it('should record ops', () => {
+    // TODO
+  })
+})
diff --git a/packages/renderer-test/index.js b/packages/renderer-test/index.js
new file mode 100644 (file)
index 0000000..462d2e6
--- /dev/null
@@ -0,0 +1,7 @@
+'use strict'
+
+if (process.env.NODE_ENV === 'production') {
+  module.exports = require('./dist/renderer-test.cjs.prod.js')
+} else {
+  module.exports = require('./dist/renderer-test.cjs.js')
+}
diff --git a/packages/renderer-test/package.json b/packages/renderer-test/package.json
new file mode 100644 (file)
index 0000000..8984a70
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "name": "@vue/renderer-test",
+  "version": "3.0.0-alpha.1",
+  "description": "@vue/renderer-test",
+  "main": "index.js",
+  "module": "dist/renderer-test.esm-bundler.js",
+  "typings": "dist/index.d.ts",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vuejs/vue.git"
+  },
+  "keywords": [
+    "vue"
+  ],
+  "author": "Evan You",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/vuejs/vue/issues"
+  },
+  "homepage": "https://github.com/vuejs/vue/tree/dev/packages/renderer-test#readme",
+  "dependencies": {
+    "@vue/core": "3.0.0-alpha.1"
+  }
+}
diff --git a/packages/renderer-test/src/index.ts b/packages/renderer-test/src/index.ts
new file mode 100644 (file)
index 0000000..2c8f648
--- /dev/null
@@ -0,0 +1,22 @@
+import { createRenderer, VNode } from '@vue/core'
+import { nodeOps, TestElement } from './nodeOps'
+
+function patchData(
+  el: TestElement,
+  key: string,
+  prevValue: any,
+  nextValue: any
+) {
+  el.props[key] = nextValue
+}
+
+const { render: _render } = createRenderer({
+  nodeOps,
+  patchData
+})
+
+type publicRender = (node: VNode | null, container: TestElement) => void
+export const render = _render as publicRender
+
+export * from './nodeOps'
+export * from '@vue/core'
diff --git a/packages/renderer-test/src/nodeOps.ts b/packages/renderer-test/src/nodeOps.ts
new file mode 100644 (file)
index 0000000..05b4c50
--- /dev/null
@@ -0,0 +1,231 @@
+export const enum NodeTypes {
+  TEXT = 'text',
+  ELEMENT = 'element'
+}
+
+export interface TestElement {
+  id: number
+  type: NodeTypes.ELEMENT
+  parentNode: TestElement | null
+  tag: string
+  children: TestNode[]
+  props: Record<string, any>
+}
+
+export interface TestText {
+  id: number
+  type: NodeTypes.TEXT
+  parentNode: TestElement | null
+  text: string
+}
+
+export type TestNode = TestElement | TestText
+
+const enum OpTypes {
+  CREATE = 'create',
+  INSERT = 'insert',
+  APPEND = 'append',
+  REMOVE = 'remove',
+  SET_TEXT = 'setText',
+  CLEAR = 'clearContent',
+  NEXT_SIBLING = 'nextSibling',
+  PARENT_NODE = 'parentNode'
+}
+
+interface Op {
+  type: OpTypes
+  nodeType?: NodeTypes
+  tag?: string
+  text?: string
+  targetNode?: TestNode
+  parentNode?: TestElement
+  refNode?: TestNode
+}
+
+let nodeId: number = 0
+let isRecording: boolean = false
+let recordedOps: Op[] = []
+
+function logOp(op: Op) {
+  if (isRecording) {
+    recordedOps.push(op)
+  }
+}
+
+export function startRecordingOps() {
+  if (!isRecording) {
+    isRecording = true
+    recordedOps = []
+  } else {
+    throw new Error(
+      '`startRecordingOps` called when there is already an active session.'
+    )
+  }
+}
+
+export function dumpOps(): Op[] {
+  if (!isRecording) {
+    throw new Error(
+      '`dumpOps` called without a recording session. ' +
+        'Call `startRecordingOps` first to start a session.'
+    )
+  }
+  isRecording = false
+  return recordedOps.slice()
+}
+
+function createElement(tag: string): TestElement {
+  const node: TestElement = {
+    id: nodeId++,
+    type: NodeTypes.ELEMENT,
+    tag,
+    children: [],
+    props: {},
+    parentNode: null
+  }
+  logOp({
+    type: OpTypes.CREATE,
+    nodeType: NodeTypes.ELEMENT,
+    targetNode: node,
+    tag
+  })
+  return node
+}
+
+function createText(text: string): TestText {
+  const node: TestText = {
+    id: nodeId++,
+    type: NodeTypes.TEXT,
+    text,
+    parentNode: null
+  }
+  logOp({
+    type: OpTypes.CREATE,
+    nodeType: NodeTypes.TEXT,
+    targetNode: node,
+    text
+  })
+  return node
+}
+
+function setText(node: TestText, text: string) {
+  logOp({
+    type: OpTypes.SET_TEXT,
+    targetNode: node,
+    text
+  })
+  node.text = text
+}
+
+function appendChild(parent: TestElement, child: TestNode) {
+  logOp({
+    type: OpTypes.APPEND,
+    targetNode: child,
+    parentNode: parent
+  })
+  if (child.parentNode) {
+    removeChild(child.parentNode, child)
+  }
+  parent.children.push(child)
+  child.parentNode = parent
+}
+
+function insertBefore(parent: TestElement, child: TestNode, ref: TestNode) {
+  if (child.parentNode) {
+    removeChild(child.parentNode, child)
+  }
+  const refIndex = parent.children.indexOf(ref)
+  if (refIndex === -1) {
+    console.error('ref: ', ref)
+    console.error('parent: ', parent)
+    throw new Error('ref is not a child of parent')
+  }
+  logOp({
+    type: OpTypes.INSERT,
+    targetNode: child,
+    parentNode: parent,
+    refNode: ref
+  })
+  parent.children.splice(refIndex, 0, child)
+  child.parentNode = parent
+}
+
+function replaceChild(
+  parent: TestElement,
+  oldChild: TestNode,
+  newChild: TestNode
+) {
+  insertBefore(parent, newChild, oldChild)
+  removeChild(parent, oldChild)
+}
+
+function removeChild(parent: TestElement, child: TestNode) {
+  logOp({
+    type: OpTypes.REMOVE,
+    targetNode: child,
+    parentNode: parent
+  })
+  const i = parent.children.indexOf(child)
+  if (i > -1) {
+    parent.children.splice(i, 1)
+  } else {
+    console.error('target: ', child)
+    console.error('parent: ', parent)
+    throw Error('target is not a childNode of parent')
+  }
+  child.parentNode = null
+}
+
+function clearContent(node: TestNode) {
+  logOp({
+    type: OpTypes.CLEAR,
+    targetNode: node
+  })
+  if (node.type === NodeTypes.ELEMENT) {
+    node.children.forEach(c => {
+      c.parentNode = null
+    })
+    node.children = []
+  } else {
+    node.text = ''
+  }
+}
+
+function parentNode(node: TestNode): TestElement | null {
+  logOp({
+    type: OpTypes.PARENT_NODE,
+    targetNode: node
+  })
+  return node.parentNode
+}
+
+function nextSibling(node: TestNode): TestNode | null {
+  logOp({
+    type: OpTypes.NEXT_SIBLING,
+    targetNode: node
+  })
+  const parent = node.parentNode
+  if (!parent) {
+    return null
+  }
+  const i = parent.children.indexOf(node)
+  return parent.children[i + 1] || null
+}
+
+function querySelector() {
+  throw new Error('querySelector not supported in test renderer.')
+}
+
+export const nodeOps = {
+  createElement,
+  createText,
+  setText,
+  appendChild,
+  insertBefore,
+  replaceChild,
+  removeChild,
+  clearContent,
+  parentNode,
+  nextSibling,
+  querySelector
+}
index 49817039d4ddd9a3fc8066826e00e594c24d9ea1..15b2076a0f5be73ba024b297f3971a4bb5721d79 100644 (file)
@@ -23,6 +23,7 @@
       "@vue/scheduler": ["packages/scheduler/src"],
       "@vue/renderer-dom": ["packages/renderer-dom/src"],
       "@vue/renderer-server": ["packages/renderer-server/src"],
+      "@vue/renderer-test": ["packages/renderer-test/src"],
       "@vue/compiler": ["packages/compiler-core/src"]
     }
   },