directives.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import { SpyInstanceFn } from 'vitest'
  2. import Vue from 'vue'
  3. describe('Options directives', () => {
  4. it('basic usage', done => {
  5. const bindSpy = vi.fn()
  6. const insertedSpy = vi.fn()
  7. const updateSpy = vi.fn()
  8. const componentUpdatedSpy = vi.fn()
  9. const unbindSpy = vi.fn()
  10. const assertContext = (el, binding, vnode) => {
  11. expect(vnode.context).toBe(vm)
  12. expect(binding.arg).toBe('arg')
  13. expect(binding.modifiers).toEqual({ hello: true })
  14. }
  15. const vm = new Vue({
  16. template:
  17. '<div class="hi"><div v-if="ok" v-test:arg.hello="a">{{ msg }}</div></div>',
  18. data: {
  19. msg: 'hi',
  20. a: 'foo',
  21. ok: true
  22. },
  23. directives: {
  24. test: {
  25. bind(el, binding, vnode) {
  26. bindSpy()
  27. assertContext(el, binding, vnode)
  28. expect(binding.value).toBe('foo')
  29. expect(binding.expression).toBe('a')
  30. expect(binding.oldValue).toBeUndefined()
  31. expect(el.parentNode).toBeNull()
  32. },
  33. inserted(el, binding, vnode) {
  34. insertedSpy()
  35. assertContext(el, binding, vnode)
  36. expect(binding.value).toBe('foo')
  37. expect(binding.expression).toBe('a')
  38. expect(binding.oldValue).toBeUndefined()
  39. expect(el.parentNode.className).toBe('hi')
  40. },
  41. update(el, binding, vnode, oldVnode) {
  42. updateSpy()
  43. assertContext(el, binding, vnode)
  44. expect(el).toBe(vm.$el.children[0])
  45. expect(oldVnode).not.toBe(vnode)
  46. expect(binding.expression).toBe('a')
  47. if (binding.value !== binding.oldValue) {
  48. expect(binding.value).toBe('bar')
  49. expect(binding.oldValue).toBe('foo')
  50. }
  51. },
  52. componentUpdated(el, binding, vnode) {
  53. componentUpdatedSpy()
  54. assertContext(el, binding, vnode)
  55. },
  56. unbind(el, binding, vnode) {
  57. unbindSpy()
  58. assertContext(el, binding, vnode)
  59. }
  60. }
  61. }
  62. })
  63. vm.$mount()
  64. expect(bindSpy).toHaveBeenCalled()
  65. expect(insertedSpy).toHaveBeenCalled()
  66. expect(updateSpy).not.toHaveBeenCalled()
  67. expect(componentUpdatedSpy).not.toHaveBeenCalled()
  68. expect(unbindSpy).not.toHaveBeenCalled()
  69. vm.a = 'bar'
  70. waitForUpdate(() => {
  71. expect(updateSpy).toHaveBeenCalled()
  72. expect(componentUpdatedSpy).toHaveBeenCalled()
  73. expect(unbindSpy).not.toHaveBeenCalled()
  74. vm.msg = 'bye'
  75. })
  76. .then(() => {
  77. expect(componentUpdatedSpy.mock.calls.length).toBe(2)
  78. vm.ok = false
  79. })
  80. .then(() => {
  81. expect(unbindSpy).toHaveBeenCalled()
  82. })
  83. .then(done)
  84. })
  85. it('function shorthand', done => {
  86. const spy = vi.fn()
  87. const vm = new Vue({
  88. template: '<div v-test:arg.hello="a"></div>',
  89. data: { a: 'foo' },
  90. directives: {
  91. test(el, binding, vnode) {
  92. expect(vnode.context).toBe(vm)
  93. expect(binding.arg).toBe('arg')
  94. expect(binding.modifiers).toEqual({ hello: true })
  95. spy(binding.value, binding.oldValue)
  96. }
  97. }
  98. })
  99. vm.$mount()
  100. expect(spy).toHaveBeenCalledWith('foo', undefined)
  101. vm.a = 'bar'
  102. waitForUpdate(() => {
  103. expect(spy).toHaveBeenCalledWith('bar', 'foo')
  104. }).then(done)
  105. })
  106. it('function shorthand (global)', done => {
  107. const spy = vi.fn()
  108. Vue.directive('test', function (el, binding, vnode) {
  109. expect(vnode.context).toBe(vm)
  110. expect(binding.arg).toBe('arg')
  111. expect(binding.modifiers).toEqual({ hello: true })
  112. spy(binding.value, binding.oldValue)
  113. })
  114. const vm = new Vue({
  115. template: '<div v-test:arg.hello="a"></div>',
  116. data: { a: 'foo' }
  117. })
  118. vm.$mount()
  119. expect(spy).toHaveBeenCalledWith('foo', undefined)
  120. vm.a = 'bar'
  121. waitForUpdate(() => {
  122. expect(spy).toHaveBeenCalledWith('bar', 'foo')
  123. delete Vue.options.directives.test
  124. }).then(done)
  125. })
  126. it('should teardown directives on old vnodes when new vnodes have none', done => {
  127. const vm = new Vue({
  128. data: {
  129. ok: true
  130. },
  131. template: `
  132. <div>
  133. <div v-if="ok" v-test>a</div>
  134. <div v-else class="b">b</div>
  135. </div>
  136. `,
  137. directives: {
  138. test: {
  139. bind: el => {
  140. el.id = 'a'
  141. },
  142. unbind: el => {
  143. el.id = ''
  144. }
  145. }
  146. }
  147. }).$mount()
  148. expect(vm.$el.children[0].id).toBe('a')
  149. vm.ok = false
  150. waitForUpdate(() => {
  151. expect(vm.$el.children[0].id).toBe('')
  152. expect(vm.$el.children[0].className).toBe('b')
  153. }).then(done)
  154. })
  155. it('should properly handle same node with different directive sets', done => {
  156. const spies: Record<string, SpyInstanceFn> = {}
  157. const createSpy = name => (spies[name] = vi.fn())
  158. const vm = new Vue({
  159. data: {
  160. ok: true,
  161. val: 123
  162. },
  163. template: `
  164. <div>
  165. <div v-if="ok" v-test="val" v-test.hi="val"></div>
  166. <div v-if="!ok" v-test.hi="val" v-test2="val"></div>
  167. </div>
  168. `,
  169. directives: {
  170. test: {
  171. bind: createSpy('bind1'),
  172. inserted: createSpy('inserted1'),
  173. update: createSpy('update1'),
  174. componentUpdated: createSpy('componentUpdated1'),
  175. unbind: createSpy('unbind1')
  176. },
  177. test2: {
  178. bind: createSpy('bind2'),
  179. inserted: createSpy('inserted2'),
  180. update: createSpy('update2'),
  181. componentUpdated: createSpy('componentUpdated2'),
  182. unbind: createSpy('unbind2')
  183. }
  184. }
  185. }).$mount()
  186. expect(spies.bind1.mock.calls.length).toBe(2)
  187. expect(spies.inserted1.mock.calls.length).toBe(2)
  188. expect(spies.bind2.mock.calls.length).toBe(0)
  189. expect(spies.inserted2.mock.calls.length).toBe(0)
  190. vm.ok = false
  191. waitForUpdate(() => {
  192. // v-test with modifier should be updated
  193. expect(spies.update1.mock.calls.length).toBe(1)
  194. expect(spies.componentUpdated1.mock.calls.length).toBe(1)
  195. // v-test without modifier should be unbound
  196. expect(spies.unbind1.mock.calls.length).toBe(1)
  197. // v-test2 should be bound
  198. expect(spies.bind2.mock.calls.length).toBe(1)
  199. expect(spies.inserted2.mock.calls.length).toBe(1)
  200. vm.ok = true
  201. })
  202. .then(() => {
  203. // v-test without modifier should be bound again
  204. expect(spies.bind1.mock.calls.length).toBe(3)
  205. expect(spies.inserted1.mock.calls.length).toBe(3)
  206. // v-test2 should be unbound
  207. expect(spies.unbind2.mock.calls.length).toBe(1)
  208. // v-test with modifier should be updated again
  209. expect(spies.update1.mock.calls.length).toBe(2)
  210. expect(spies.componentUpdated1.mock.calls.length).toBe(2)
  211. vm.val = 234
  212. })
  213. .then(() => {
  214. expect(spies.update1.mock.calls.length).toBe(4)
  215. expect(spies.componentUpdated1.mock.calls.length).toBe(4)
  216. })
  217. .then(done)
  218. })
  219. it('warn non-existent', () => {
  220. new Vue({
  221. template: '<div v-test></div>'
  222. }).$mount()
  223. expect('Failed to resolve directive: test').toHaveBeenWarned()
  224. })
  225. // #6513
  226. it('should invoke unbind & inserted on inner component root element change', done => {
  227. const dir = {
  228. bind: vi.fn(),
  229. inserted: vi.fn(),
  230. unbind: vi.fn()
  231. }
  232. const Child = {
  233. template: `<div v-if="ok"/><span v-else/>`,
  234. data: () => ({ ok: true })
  235. }
  236. const vm = new Vue({
  237. template: `<child ref="child" v-test />`,
  238. directives: { test: dir },
  239. components: { Child }
  240. }).$mount()
  241. const oldEl = vm.$el
  242. expect(dir.bind.mock.calls.length).toBe(1)
  243. expect(dir.bind.mock.calls[0][0]).toBe(oldEl)
  244. expect(dir.inserted.mock.calls.length).toBe(1)
  245. expect(dir.inserted.mock.calls[0][0]).toBe(oldEl)
  246. expect(dir.unbind).not.toHaveBeenCalled()
  247. vm.$refs.child.ok = false
  248. waitForUpdate(() => {
  249. expect(vm.$el.tagName).toBe('SPAN')
  250. expect(dir.bind.mock.calls.length).toBe(2)
  251. expect(dir.bind.mock.calls[1][0]).toBe(vm.$el)
  252. expect(dir.inserted.mock.calls.length).toBe(2)
  253. expect(dir.inserted.mock.calls[1][0]).toBe(vm.$el)
  254. expect(dir.unbind.mock.calls.length).toBe(1)
  255. expect(dir.unbind.mock.calls[0][0]).toBe(oldEl)
  256. }).then(done)
  257. })
  258. it('dynamic arguments', done => {
  259. const vm = new Vue({
  260. template: `<div v-my:[key]="1"/>`,
  261. data: {
  262. key: 'foo'
  263. },
  264. directives: {
  265. my: {
  266. bind(el, binding) {
  267. expect(binding.arg).toBe('foo')
  268. },
  269. update(el, binding) {
  270. expect(binding.arg).toBe('bar')
  271. expect(binding.oldArg).toBe('foo')
  272. done()
  273. }
  274. }
  275. }
  276. }).$mount()
  277. vm.key = 'bar'
  278. })
  279. it('deep object like `deep.a` as dynamic arguments', done => {
  280. const vm = new Vue({
  281. template: `<div v-my:[deep.a]="1"/>`,
  282. data: {
  283. deep: {
  284. a: 'foo'
  285. }
  286. },
  287. directives: {
  288. my: {
  289. bind(el, binding) {
  290. expect(binding.arg).toBe('foo')
  291. },
  292. update(el, binding) {
  293. expect(binding.arg).toBe('bar')
  294. expect(binding.oldArg).toBe('foo')
  295. done()
  296. }
  297. }
  298. }
  299. }).$mount()
  300. vm.deep.a = 'bar'
  301. })
  302. it('deep object like `deep.a.b` as dynamic arguments', done => {
  303. const vm = new Vue({
  304. template: `<div v-my:[deep.a.b]="1"/>`,
  305. data: {
  306. deep: {
  307. a: {
  308. b: 'foo'
  309. }
  310. }
  311. },
  312. directives: {
  313. my: {
  314. bind(el, binding) {
  315. expect(binding.arg).toBe('foo')
  316. },
  317. update(el, binding) {
  318. expect(binding.arg).toBe('bar')
  319. expect(binding.oldArg).toBe('foo')
  320. done()
  321. }
  322. }
  323. }
  324. }).$mount()
  325. vm.deep.a.b = 'bar'
  326. })
  327. it('deep object as dynamic arguments with modifiers', done => {
  328. const vm = new Vue({
  329. template: `<div v-my:[deep.a.b].x.y="1"/>`,
  330. data: {
  331. deep: {
  332. a: {
  333. b: 'foo'
  334. }
  335. }
  336. },
  337. directives: {
  338. my: {
  339. bind(el, binding) {
  340. expect(binding.arg).toBe('foo')
  341. expect(binding.modifiers.x).toBe(true)
  342. expect(binding.modifiers.y).toBe(true)
  343. },
  344. update(el, binding) {
  345. expect(binding.arg).toBe('bar')
  346. expect(binding.oldArg).toBe('foo')
  347. done()
  348. }
  349. }
  350. }
  351. }).$mount()
  352. vm.deep.a.b = 'bar'
  353. })
  354. })