directives.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import {
  2. h,
  3. withDirectives,
  4. ref,
  5. render,
  6. nodeOps,
  7. DirectiveHook,
  8. VNode,
  9. DirectiveBinding,
  10. nextTick,
  11. defineComponent
  12. } from '@vue/runtime-test'
  13. import { currentInstance, ComponentInternalInstance } from '../src/component'
  14. describe('directives', () => {
  15. it('should work', async () => {
  16. const count = ref(0)
  17. function assertBindings(binding: DirectiveBinding) {
  18. expect(binding.value).toBe(count.value)
  19. expect(binding.arg).toBe('foo')
  20. expect(binding.instance).toBe(_instance && _instance.proxy)
  21. expect(binding.modifiers && binding.modifiers.ok).toBe(true)
  22. }
  23. const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
  24. expect(el.tag).toBe('div')
  25. // should not be inserted yet
  26. expect(el.parentNode).toBe(null)
  27. expect(root.children.length).toBe(0)
  28. assertBindings(binding)
  29. expect(vnode).toBe(_vnode)
  30. expect(prevVNode).toBe(null)
  31. }) as DirectiveHook)
  32. const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
  33. expect(el.tag).toBe('div')
  34. // should be inserted now
  35. expect(el.parentNode).toBe(root)
  36. expect(root.children[0]).toBe(el)
  37. assertBindings(binding)
  38. expect(vnode).toBe(_vnode)
  39. expect(prevVNode).toBe(null)
  40. }) as DirectiveHook)
  41. const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
  42. expect(el.tag).toBe('div')
  43. expect(el.parentNode).toBe(root)
  44. expect(root.children[0]).toBe(el)
  45. // node should not have been updated yet
  46. expect(el.children[0].text).toBe(`${count.value - 1}`)
  47. assertBindings(binding)
  48. expect(vnode).toBe(_vnode)
  49. expect(prevVNode).toBe(_prevVnode)
  50. }) as DirectiveHook)
  51. const updated = jest.fn(((el, binding, vnode, prevVNode) => {
  52. expect(el.tag).toBe('div')
  53. expect(el.parentNode).toBe(root)
  54. expect(root.children[0]).toBe(el)
  55. // node should have been updated
  56. expect(el.children[0].text).toBe(`${count.value}`)
  57. assertBindings(binding)
  58. expect(vnode).toBe(_vnode)
  59. expect(prevVNode).toBe(_prevVnode)
  60. }) as DirectiveHook)
  61. const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
  62. expect(el.tag).toBe('div')
  63. // should be removed now
  64. expect(el.parentNode).toBe(root)
  65. expect(root.children[0]).toBe(el)
  66. assertBindings(binding)
  67. expect(vnode).toBe(_vnode)
  68. expect(prevVNode).toBe(null)
  69. }) as DirectiveHook)
  70. const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
  71. expect(el.tag).toBe('div')
  72. // should have been removed
  73. expect(el.parentNode).toBe(null)
  74. expect(root.children.length).toBe(0)
  75. assertBindings(binding)
  76. expect(vnode).toBe(_vnode)
  77. expect(prevVNode).toBe(null)
  78. }) as DirectiveHook)
  79. const dir = {
  80. beforeMount,
  81. mounted,
  82. beforeUpdate,
  83. updated,
  84. beforeUnmount,
  85. unmounted
  86. }
  87. let _instance: ComponentInternalInstance | null = null
  88. let _vnode: VNode | null = null
  89. let _prevVnode: VNode | null = null
  90. const Comp = {
  91. setup() {
  92. _instance = currentInstance
  93. },
  94. render() {
  95. _prevVnode = _vnode
  96. _vnode = withDirectives(h('div', count.value), [
  97. [
  98. dir,
  99. // value
  100. count.value,
  101. // argument
  102. 'foo',
  103. // modifiers
  104. { ok: true }
  105. ]
  106. ])
  107. return _vnode
  108. }
  109. }
  110. const root = nodeOps.createElement('div')
  111. render(h(Comp), root)
  112. expect(beforeMount).toHaveBeenCalledTimes(1)
  113. expect(mounted).toHaveBeenCalledTimes(1)
  114. count.value++
  115. await nextTick()
  116. expect(beforeUpdate).toHaveBeenCalledTimes(1)
  117. expect(updated).toHaveBeenCalledTimes(1)
  118. render(null, root)
  119. expect(beforeUnmount).toHaveBeenCalledTimes(1)
  120. expect(unmounted).toHaveBeenCalledTimes(1)
  121. })
  122. it('should work with a function directive', async () => {
  123. const count = ref(0)
  124. function assertBindings(binding: DirectiveBinding) {
  125. expect(binding.value).toBe(count.value)
  126. expect(binding.arg).toBe('foo')
  127. expect(binding.instance).toBe(_instance && _instance.proxy)
  128. expect(binding.modifiers && binding.modifiers.ok).toBe(true)
  129. }
  130. const fn = jest.fn(((el, binding, vnode, prevVNode) => {
  131. expect(el.tag).toBe('div')
  132. expect(el.parentNode).toBe(root)
  133. assertBindings(binding)
  134. expect(vnode).toBe(_vnode)
  135. expect(prevVNode).toBe(_prevVnode)
  136. }) as DirectiveHook)
  137. let _instance: ComponentInternalInstance | null = null
  138. let _vnode: VNode | null = null
  139. let _prevVnode: VNode | null = null
  140. const Comp = {
  141. setup() {
  142. _instance = currentInstance
  143. },
  144. render() {
  145. _prevVnode = _vnode
  146. _vnode = withDirectives(h('div', count.value), [
  147. [
  148. fn,
  149. // value
  150. count.value,
  151. // argument
  152. 'foo',
  153. // modifiers
  154. { ok: true }
  155. ]
  156. ])
  157. return _vnode
  158. }
  159. }
  160. const root = nodeOps.createElement('div')
  161. render(h(Comp), root)
  162. expect(fn).toHaveBeenCalledTimes(1)
  163. count.value++
  164. await nextTick()
  165. expect(fn).toHaveBeenCalledTimes(2)
  166. })
  167. it('should work on component vnode', async () => {
  168. const count = ref(0)
  169. function assertBindings(binding: DirectiveBinding) {
  170. expect(binding.value).toBe(count.value)
  171. expect(binding.arg).toBe('foo')
  172. expect(binding.instance).toBe(_instance && _instance.proxy)
  173. expect(binding.modifiers && binding.modifiers.ok).toBe(true)
  174. }
  175. const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
  176. expect(el.tag).toBe('div')
  177. // should not be inserted yet
  178. expect(el.parentNode).toBe(null)
  179. expect(root.children.length).toBe(0)
  180. assertBindings(binding)
  181. expect(vnode.type).toBe(_vnode!.type)
  182. expect(prevVNode).toBe(null)
  183. }) as DirectiveHook)
  184. const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
  185. expect(el.tag).toBe('div')
  186. // should be inserted now
  187. expect(el.parentNode).toBe(root)
  188. expect(root.children[0]).toBe(el)
  189. assertBindings(binding)
  190. expect(vnode.type).toBe(_vnode!.type)
  191. expect(prevVNode).toBe(null)
  192. }) as DirectiveHook)
  193. const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
  194. expect(el.tag).toBe('div')
  195. expect(el.parentNode).toBe(root)
  196. expect(root.children[0]).toBe(el)
  197. // node should not have been updated yet
  198. // expect(el.children[0].text).toBe(`${count.value - 1}`)
  199. assertBindings(binding)
  200. expect(vnode.type).toBe(_vnode!.type)
  201. expect(prevVNode!.type).toBe(_prevVnode!.type)
  202. }) as DirectiveHook)
  203. const updated = jest.fn(((el, binding, vnode, prevVNode) => {
  204. expect(el.tag).toBe('div')
  205. expect(el.parentNode).toBe(root)
  206. expect(root.children[0]).toBe(el)
  207. // node should have been updated
  208. expect(el.children[0].text).toBe(`${count.value}`)
  209. assertBindings(binding)
  210. expect(vnode.type).toBe(_vnode!.type)
  211. expect(prevVNode!.type).toBe(_prevVnode!.type)
  212. }) as DirectiveHook)
  213. const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
  214. expect(el.tag).toBe('div')
  215. // should be removed now
  216. expect(el.parentNode).toBe(root)
  217. expect(root.children[0]).toBe(el)
  218. assertBindings(binding)
  219. expect(vnode.type).toBe(_vnode!.type)
  220. expect(prevVNode).toBe(null)
  221. }) as DirectiveHook)
  222. const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
  223. expect(el.tag).toBe('div')
  224. // should have been removed
  225. expect(el.parentNode).toBe(null)
  226. expect(root.children.length).toBe(0)
  227. assertBindings(binding)
  228. expect(vnode.type).toBe(_vnode!.type)
  229. expect(prevVNode).toBe(null)
  230. }) as DirectiveHook)
  231. const dir = {
  232. beforeMount,
  233. mounted,
  234. beforeUpdate,
  235. updated,
  236. beforeUnmount,
  237. unmounted
  238. }
  239. let _instance: ComponentInternalInstance | null = null
  240. let _vnode: VNode | null = null
  241. let _prevVnode: VNode | null = null
  242. const Child = (props: { count: number }) => {
  243. _prevVnode = _vnode
  244. _vnode = h('div', props.count)
  245. return _vnode
  246. }
  247. const Comp = {
  248. setup() {
  249. _instance = currentInstance
  250. },
  251. render() {
  252. return withDirectives(h(Child, { count: count.value }), [
  253. [
  254. dir,
  255. // value
  256. count.value,
  257. // argument
  258. 'foo',
  259. // modifiers
  260. { ok: true }
  261. ]
  262. ])
  263. }
  264. }
  265. const root = nodeOps.createElement('div')
  266. render(h(Comp), root)
  267. expect(beforeMount).toHaveBeenCalledTimes(1)
  268. expect(mounted).toHaveBeenCalledTimes(1)
  269. count.value++
  270. await nextTick()
  271. expect(beforeUpdate).toHaveBeenCalledTimes(1)
  272. expect(updated).toHaveBeenCalledTimes(1)
  273. render(null, root)
  274. expect(beforeUnmount).toHaveBeenCalledTimes(1)
  275. expect(unmounted).toHaveBeenCalledTimes(1)
  276. })
  277. // #2298
  278. it('directive merging on component root', () => {
  279. const d1 = {
  280. mounted: jest.fn()
  281. }
  282. const d2 = {
  283. mounted: jest.fn()
  284. }
  285. const Comp = {
  286. render() {
  287. return withDirectives(h('div'), [[d2]])
  288. }
  289. }
  290. const App = {
  291. name: 'App',
  292. render() {
  293. return h('div', [withDirectives(h(Comp), [[d1]])])
  294. }
  295. }
  296. const root = nodeOps.createElement('div')
  297. render(h(App), root)
  298. expect(d1.mounted).toHaveBeenCalled()
  299. expect(d2.mounted).toHaveBeenCalled()
  300. })
  301. test('should disable tracking inside directive lifecycle hooks', async () => {
  302. const count = ref(0)
  303. const text = ref('')
  304. const beforeUpdate = jest.fn(() => count.value++)
  305. const App = {
  306. render() {
  307. return withDirectives(h('p', text.value), [
  308. [
  309. {
  310. beforeUpdate
  311. }
  312. ]
  313. ])
  314. }
  315. }
  316. const root = nodeOps.createElement('div')
  317. render(h(App), root)
  318. expect(beforeUpdate).toHaveBeenCalledTimes(0)
  319. expect(count.value).toBe(0)
  320. text.value = 'foo'
  321. await nextTick()
  322. expect(beforeUpdate).toHaveBeenCalledTimes(1)
  323. expect(count.value).toBe(1)
  324. })
  325. test('should receive exposeProxy for closed instances', async () => {
  326. let res: string
  327. const App = defineComponent({
  328. setup(_, { expose }) {
  329. expose({
  330. msg: 'Test'
  331. })
  332. return () =>
  333. withDirectives(h('p', 'Lore Ipsum'), [
  334. [
  335. {
  336. mounted(el, { instance }) {
  337. res = (instance as any).msg as string
  338. }
  339. }
  340. ]
  341. ])
  342. }
  343. })
  344. const root = nodeOps.createElement('div')
  345. render(h(App), root)
  346. expect(res!).toBe('Test')
  347. })
  348. })