nodeOps.ts 4.9 KB

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