nodeOps.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { markRaw } from '@vue/reactivity'
  2. export enum TestNodeTypes {
  3. TEXT = 'text',
  4. ELEMENT = 'element',
  5. COMMENT = 'comment',
  6. }
  7. export 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: TestNodeTypes.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: TestNodeTypes.TEXT
  27. parentNode: TestElement | null
  28. text: string
  29. }
  30. export interface TestComment {
  31. id: number
  32. type: TestNodeTypes.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?: TestNodeTypes
  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): void {
  52. recordedNodeOps.push(op)
  53. }
  54. export function resetOps(): void {
  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: TestNodeTypes.ELEMENT,
  66. tag,
  67. children: [],
  68. props: {},
  69. parentNode: null,
  70. eventListeners: null,
  71. }
  72. logNodeOp({
  73. type: NodeOpTypes.CREATE,
  74. nodeType: TestNodeTypes.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: TestNodeTypes.TEXT,
  86. text,
  87. parentNode: null,
  88. }
  89. logNodeOp({
  90. type: NodeOpTypes.CREATE,
  91. nodeType: TestNodeTypes.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: TestNodeTypes.COMMENT,
  103. text,
  104. parentNode: null,
  105. }
  106. logNodeOp({
  107. type: NodeOpTypes.CREATE,
  108. nodeType: TestNodeTypes.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): void {
  117. logNodeOp({
  118. type: NodeOpTypes.SET_TEXT,
  119. targetNode: node,
  120. text,
  121. })
  122. node.text = text
  123. }
  124. function insert(
  125. child: TestNode,
  126. parent: TestElement,
  127. ref?: TestNode | null,
  128. ): void {
  129. let refIndex
  130. if (ref) {
  131. refIndex = parent.children.indexOf(ref)
  132. if (refIndex === -1) {
  133. console.error('ref: ', ref)
  134. console.error('parent: ', parent)
  135. throw new Error('ref is not a child of parent')
  136. }
  137. }
  138. logNodeOp({
  139. type: NodeOpTypes.INSERT,
  140. targetNode: child,
  141. parentNode: parent,
  142. refNode: ref,
  143. })
  144. // remove the node first, but don't log it as a REMOVE op
  145. remove(child, false)
  146. // re-calculate the ref index because the child's removal may have affected it
  147. refIndex = ref ? parent.children.indexOf(ref) : -1
  148. if (refIndex === -1) {
  149. parent.children.push(child)
  150. child.parentNode = parent
  151. } else {
  152. parent.children.splice(refIndex, 0, child)
  153. child.parentNode = parent
  154. }
  155. }
  156. function remove(child: TestNode, logOp = true): void {
  157. const parent = child.parentNode
  158. if (parent) {
  159. if (logOp) {
  160. logNodeOp({
  161. type: NodeOpTypes.REMOVE,
  162. targetNode: child,
  163. parentNode: parent,
  164. })
  165. }
  166. const i = parent.children.indexOf(child)
  167. if (i > -1) {
  168. parent.children.splice(i, 1)
  169. } else {
  170. console.error('target: ', child)
  171. console.error('parent: ', parent)
  172. throw Error('target is not a childNode of parent')
  173. }
  174. child.parentNode = null
  175. }
  176. }
  177. function setElementText(el: TestElement, text: string): void {
  178. logNodeOp({
  179. type: NodeOpTypes.SET_ELEMENT_TEXT,
  180. targetNode: el,
  181. text,
  182. })
  183. el.children.forEach(c => {
  184. c.parentNode = null
  185. })
  186. if (!text) {
  187. el.children = []
  188. } else {
  189. el.children = [
  190. {
  191. id: nodeId++,
  192. type: TestNodeTypes.TEXT,
  193. text,
  194. parentNode: el,
  195. },
  196. ]
  197. }
  198. }
  199. function parentNode(node: TestNode): TestElement | null {
  200. return node.parentNode
  201. }
  202. function nextSibling(node: TestNode): TestNode | null {
  203. const parent = node.parentNode
  204. if (!parent) {
  205. return null
  206. }
  207. const i = parent.children.indexOf(node)
  208. return parent.children[i + 1] || null
  209. }
  210. function querySelector(): never {
  211. throw new Error('querySelector not supported in test renderer.')
  212. }
  213. function setScopeId(el: TestElement, id: string): void {
  214. el.props[id] = ''
  215. }
  216. export const nodeOps: {
  217. insert: typeof insert
  218. remove: typeof remove
  219. createElement: typeof createElement
  220. createText: typeof createText
  221. createComment: typeof createComment
  222. setText: typeof setText
  223. setElementText: typeof setElementText
  224. parentNode: typeof parentNode
  225. nextSibling: typeof nextSibling
  226. querySelector: typeof querySelector
  227. setScopeId: typeof setScopeId
  228. } = {
  229. insert,
  230. remove,
  231. createElement,
  232. createText,
  233. createComment,
  234. setText,
  235. setElementText,
  236. parentNode,
  237. nextSibling,
  238. querySelector,
  239. setScopeId,
  240. }