2
0

directives.spec.ts 11 KB

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