nodeOps.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. export const enum NodeTypes {
  2. TEXT = 'text',
  3. ELEMENT = 'element',
  4. COMMENT = 'comment'
  5. }
  6. export const enum NodeOpTypes {
  7. CREATE = 'create',
  8. INSERT = 'insert',
  9. REMOVE = 'remove',
  10. SET_TEXT = 'setText',
  11. SET_ELEMENT_TEXT = 'setElementText',
  12. PATCH = 'patch'
  13. }
  14. export interface TestElement {
  15. id: number
  16. type: NodeTypes.ELEMENT
  17. parentNode: TestElement | null
  18. tag: string
  19. children: TestNode[]
  20. props: Record<string, any>
  21. eventListeners: Record<string, Function | Function[]> | null
  22. }
  23. export interface TestText {
  24. id: number
  25. type: NodeTypes.TEXT
  26. parentNode: TestElement | null
  27. text: string
  28. }
  29. export interface TestComment {
  30. id: number
  31. type: NodeTypes.COMMENT
  32. parentNode: TestElement | null
  33. text: string
  34. }
  35. export type TestNode = TestElement | TestText | TestComment
  36. export interface NodeOp {
  37. type: NodeOpTypes
  38. nodeType?: NodeTypes
  39. tag?: string
  40. text?: string
  41. targetNode?: TestNode
  42. parentNode?: TestElement
  43. refNode?: TestNode | null
  44. propKey?: string
  45. propPrevValue?: any
  46. propNextValue?: any
  47. }
  48. let nodeId: number = 0
  49. let recordedNodeOps: NodeOp[] = []
  50. export function logNodeOp(op: NodeOp) {
  51. recordedNodeOps.push(op)
  52. }
  53. export function resetOps() {
  54. recordedNodeOps = []
  55. }
  56. export function dumpOps(): NodeOp[] {
  57. const ops = recordedNodeOps.slice()
  58. resetOps()
  59. return ops
  60. }
  61. function createElement(tag: string): TestElement {
  62. const node: TestElement = {
  63. id: nodeId++,
  64. type: NodeTypes.ELEMENT,
  65. tag,
  66. children: [],
  67. props: {},
  68. parentNode: null,
  69. eventListeners: null
  70. }
  71. logNodeOp({
  72. type: NodeOpTypes.CREATE,
  73. nodeType: NodeTypes.ELEMENT,
  74. targetNode: node,
  75. tag
  76. })
  77. return node
  78. }
  79. function createText(text: string): TestText {
  80. const node: TestText = {
  81. id: nodeId++,
  82. type: NodeTypes.TEXT,
  83. text,
  84. parentNode: null
  85. }
  86. logNodeOp({
  87. type: NodeOpTypes.CREATE,
  88. nodeType: NodeTypes.TEXT,
  89. targetNode: node,
  90. text
  91. })
  92. return node
  93. }
  94. function createComment(text: string): TestComment {
  95. const node: TestComment = {
  96. id: nodeId++,
  97. type: NodeTypes.COMMENT,
  98. text,
  99. parentNode: null
  100. }
  101. logNodeOp({
  102. type: NodeOpTypes.CREATE,
  103. nodeType: NodeTypes.COMMENT,
  104. targetNode: node,
  105. text
  106. })
  107. return node
  108. }
  109. function setText(node: TestText, text: string) {
  110. logNodeOp({
  111. type: NodeOpTypes.SET_TEXT,
  112. targetNode: node,
  113. text
  114. })
  115. node.text = text
  116. }
  117. function insert(child: TestNode, parent: TestElement, ref?: TestNode | null) {
  118. let refIndex
  119. if (ref != null) {
  120. refIndex = parent.children.indexOf(ref)
  121. if (refIndex === -1) {
  122. console.error('ref: ', ref)
  123. console.error('parent: ', parent)
  124. throw new Error('ref is not a child of parent')
  125. }
  126. }
  127. logNodeOp({
  128. type: NodeOpTypes.INSERT,
  129. targetNode: child,
  130. parentNode: parent,
  131. refNode: ref
  132. })
  133. remove(child)
  134. if (refIndex === undefined) {
  135. parent.children.push(child)
  136. child.parentNode = parent
  137. } else {
  138. parent.children.splice(refIndex, 0, child)
  139. child.parentNode = parent
  140. }
  141. }
  142. function remove(child: TestNode) {
  143. const parent = child.parentNode
  144. if (parent != null) {
  145. logNodeOp({
  146. type: NodeOpTypes.REMOVE,
  147. targetNode: child,
  148. parentNode: parent
  149. })
  150. const i = parent.children.indexOf(child)
  151. if (i > -1) {
  152. parent.children.splice(i, 1)
  153. } else {
  154. console.error('target: ', child)
  155. console.error('parent: ', parent)
  156. throw Error('target is not a childNode of parent')
  157. }
  158. child.parentNode = null
  159. }
  160. }
  161. function setElementText(el: TestElement, text: string) {
  162. logNodeOp({
  163. type: NodeOpTypes.SET_ELEMENT_TEXT,
  164. targetNode: el,
  165. text
  166. })
  167. el.children.forEach(c => {
  168. c.parentNode = null
  169. })
  170. el.children = [
  171. {
  172. id: nodeId++,
  173. type: NodeTypes.TEXT,
  174. text,
  175. parentNode: el
  176. }
  177. ]
  178. }
  179. function parentNode(node: TestNode): TestElement | null {
  180. return node.parentNode
  181. }
  182. function nextSibling(node: TestNode): TestNode | null {
  183. const parent = node.parentNode
  184. if (!parent) {
  185. return null
  186. }
  187. const i = parent.children.indexOf(node)
  188. return parent.children[i + 1] || null
  189. }
  190. function querySelector() {
  191. throw new Error('querySelector not supported in test renderer.')
  192. }
  193. export const nodeOps = {
  194. insert,
  195. remove,
  196. createElement,
  197. createText,
  198. createComment,
  199. setText,
  200. setElementText,
  201. parentNode,
  202. nextSibling,
  203. querySelector
  204. }