From da20a06a78c9166d89f6410d7fd96e5c637286ca Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 1 Oct 2018 13:15:07 -0400 Subject: [PATCH] feat: implement basic test renderer --- packages/renderer-test/.npmignore | 3 + packages/renderer-test/README.md | 34 +++ .../__tests__/testRenderer.spec.ts | 42 ++++ packages/renderer-test/index.js | 7 + packages/renderer-test/package.json | 24 ++ packages/renderer-test/src/index.ts | 22 ++ packages/renderer-test/src/nodeOps.ts | 231 ++++++++++++++++++ tsconfig.json | 1 + 8 files changed, 364 insertions(+) create mode 100644 packages/renderer-test/.npmignore create mode 100644 packages/renderer-test/README.md create mode 100644 packages/renderer-test/__tests__/testRenderer.spec.ts create mode 100644 packages/renderer-test/index.js create mode 100644 packages/renderer-test/package.json create mode 100644 packages/renderer-test/src/index.ts create mode 100644 packages/renderer-test/src/nodeOps.ts diff --git a/packages/renderer-test/.npmignore b/packages/renderer-test/.npmignore new file mode 100644 index 00000000..bb5c8a54 --- /dev/null +++ b/packages/renderer-test/.npmignore @@ -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 index 00000000..177e2b0a --- /dev/null +++ b/packages/renderer-test/README.md @@ -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 index 00000000..3974388f --- /dev/null +++ b/packages/renderer-test/__tests__/testRenderer.spec.ts @@ -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 index 00000000..462d2e60 --- /dev/null +++ b/packages/renderer-test/index.js @@ -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 index 00000000..8984a70f --- /dev/null +++ b/packages/renderer-test/package.json @@ -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 index 00000000..2c8f648c --- /dev/null +++ b/packages/renderer-test/src/index.ts @@ -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 index 00000000..05b4c508 --- /dev/null +++ b/packages/renderer-test/src/nodeOps.ts @@ -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 +} + +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 +} diff --git a/tsconfig.json b/tsconfig.json index 49817039..15b2076a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] } },