directives.spec.ts 11 KB

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