component_spec.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. var _ = require('src/util')
  2. var Vue = require('src')
  3. describe('Component', function () {
  4. var el
  5. beforeEach(function () {
  6. el = document.createElement('div')
  7. document.body.appendChild(el)
  8. })
  9. afterEach(function () {
  10. document.body.removeChild(el)
  11. })
  12. it('static', function () {
  13. new Vue({
  14. el: el,
  15. template: '<test></test>',
  16. components: {
  17. test: {
  18. data: function () {
  19. return { a: 123 }
  20. },
  21. template: '{{a}}'
  22. }
  23. }
  24. })
  25. expect(el.innerHTML).toBe('<test>123</test>')
  26. })
  27. it('replace', function () {
  28. new Vue({
  29. el: el,
  30. template: '<test></test>',
  31. components: {
  32. test: {
  33. replace: true,
  34. data: function () {
  35. return { a: 123 }
  36. },
  37. template: '<p>{{a}}</p>'
  38. }
  39. }
  40. })
  41. expect(el.innerHTML).toBe('<p>123</p>')
  42. })
  43. it('"is" on table elements', function () {
  44. var vm = new Vue({
  45. el: el,
  46. template: '<table><tbody><tr is="test"></tr></tbody></table>',
  47. components: {
  48. test: {
  49. data: function () {
  50. return { a: 123 }
  51. },
  52. template: '<td>{{a}}</td>'
  53. }
  54. }
  55. })
  56. expect(el.innerHTML).toBe(vm.$options.template.replace(/<tr.*\/tr>/, '<tr><td>123</td></tr>'))
  57. expect(getWarnCount()).toBe(0)
  58. })
  59. it('inline-template', function () {
  60. new Vue({
  61. el: el,
  62. template: '<test inline-template>{{a}}</test>',
  63. data: {
  64. a: 'parent'
  65. },
  66. components: {
  67. test: {
  68. data: function () {
  69. return { a: 'child' }
  70. },
  71. template: 'child option template'
  72. }
  73. }
  74. })
  75. expect(el.innerHTML).toBe('<test>child</test>')
  76. })
  77. it('block replace', function () {
  78. new Vue({
  79. el: el,
  80. template: '<test></test>',
  81. components: {
  82. test: {
  83. replace: true,
  84. data: function () {
  85. return { a: 123, b: 234 }
  86. },
  87. template: '<p>{{a}}</p><p>{{b}}</p>'
  88. }
  89. }
  90. })
  91. expect(el.innerHTML).toBe('<p>123</p><p>234</p>')
  92. })
  93. it('dynamic', function (done) {
  94. var vm = new Vue({
  95. el: el,
  96. template: '<component :is="view" :view="view"></component>',
  97. data: {
  98. view: 'view-a'
  99. },
  100. components: {
  101. 'view-a': {
  102. template: '<div>AAA</div>',
  103. replace: true,
  104. data: function () {
  105. return { view: 'a' }
  106. }
  107. },
  108. 'view-b': {
  109. template: '<div>BBB</div>',
  110. replace: true,
  111. data: function () {
  112. return { view: 'b' }
  113. }
  114. }
  115. }
  116. })
  117. expect(el.innerHTML).toBe('<div view="view-a">AAA</div>')
  118. vm.view = 'view-b'
  119. _.nextTick(function () {
  120. expect(el.innerHTML).toBe('<div view="view-b">BBB</div>')
  121. vm.view = ''
  122. _.nextTick(function () {
  123. expect(el.innerHTML).toBe('')
  124. done()
  125. })
  126. })
  127. })
  128. it(':is using raw component constructor', function () {
  129. new Vue({
  130. el: el,
  131. template: '<component :is="$options.components.test">',
  132. components: {
  133. test: {
  134. template: 'hi'
  135. }
  136. }
  137. })
  138. expect(el.textContent).toBe('hi')
  139. })
  140. it('keep-alive', function (done) {
  141. var spyA = jasmine.createSpy()
  142. var spyB = jasmine.createSpy()
  143. var vm = new Vue({
  144. el: el,
  145. template: '<component :is="view" keep-alive></component>',
  146. data: {
  147. view: 'view-a'
  148. },
  149. components: {
  150. 'view-a': {
  151. created: spyA,
  152. template: '<div>AAA</div>',
  153. replace: true
  154. },
  155. 'view-b': {
  156. created: spyB,
  157. template: '<div>BBB</div>',
  158. replace: true
  159. }
  160. }
  161. })
  162. expect(el.innerHTML).toBe('<div>AAA</div>')
  163. expect(spyA.calls.count()).toBe(1)
  164. expect(spyB.calls.count()).toBe(0)
  165. vm.view = 'view-b'
  166. _.nextTick(function () {
  167. expect(el.innerHTML).toBe('<div>BBB</div>')
  168. expect(spyA.calls.count()).toBe(1)
  169. expect(spyB.calls.count()).toBe(1)
  170. vm.view = 'view-a'
  171. _.nextTick(function () {
  172. expect(el.innerHTML).toBe('<div>AAA</div>')
  173. expect(spyA.calls.count()).toBe(1)
  174. expect(spyB.calls.count()).toBe(1)
  175. vm.view = 'view-b'
  176. _.nextTick(function () {
  177. expect(el.innerHTML).toBe('<div>BBB</div>')
  178. expect(spyA.calls.count()).toBe(1)
  179. expect(spyB.calls.count()).toBe(1)
  180. done()
  181. })
  182. })
  183. })
  184. })
  185. it('should compile parent template directives & content in parent scope', function (done) {
  186. var vm = new Vue({
  187. el: el,
  188. data: {
  189. ok: false,
  190. message: 'hello'
  191. },
  192. template: '<test v-show="ok">{{message}}</test>',
  193. components: {
  194. test: {
  195. template: '<div><slot></slot> {{message}}</div>',
  196. replace: true,
  197. data: function () {
  198. return {
  199. message: 'world'
  200. }
  201. }
  202. }
  203. }
  204. })
  205. expect(el.firstChild.style.display).toBe('none')
  206. expect(el.firstChild.textContent).toBe('hello world')
  207. vm.ok = true
  208. vm.message = 'bye'
  209. _.nextTick(function () {
  210. expect(el.firstChild.style.display).toBe('')
  211. expect(el.firstChild.textContent).toBe('bye world')
  212. done()
  213. })
  214. })
  215. it('parent content + v-if', function (done) {
  216. var vm = new Vue({
  217. el: el,
  218. data: {
  219. ok: false,
  220. message: 'hello'
  221. },
  222. template: '<test v-if="ok">{{message}}</test>',
  223. components: {
  224. test: {
  225. template: '<slot></slot> {{message}}',
  226. data: function () {
  227. return {
  228. message: 'world'
  229. }
  230. }
  231. }
  232. }
  233. })
  234. expect(el.textContent).toBe('')
  235. expect(vm.$children.length).toBe(0)
  236. expect(vm._directives.length).toBe(1) // v-if
  237. vm.ok = true
  238. _.nextTick(function () {
  239. expect(vm.$children.length).toBe(1)
  240. expect(vm._directives.length).toBe(3) // v-if, component, v-text
  241. expect(el.textContent).toBe('hello world')
  242. done()
  243. })
  244. })
  245. it('props', function () {
  246. new Vue({
  247. el: el,
  248. data: {
  249. list: [{a: 1}, {a: 2}]
  250. },
  251. template: '<test :collection="list"></test>',
  252. components: {
  253. test: {
  254. template: '<ul><li v-for="item in collection">{{item.a}}</li></ul>',
  255. replace: true,
  256. props: ['collection']
  257. }
  258. }
  259. })
  260. expect(el.innerHTML).toBe('<ul><li>1</li><li>2</li></ul>')
  261. })
  262. it('activate hook for static component', function (done) {
  263. new Vue({
  264. el: el,
  265. template: '<view-a></view-a>',
  266. components: {
  267. 'view-a': {
  268. template: 'AAA',
  269. activate: function (ready) {
  270. setTimeout(function () {
  271. expect(el.textContent).toBe('')
  272. ready()
  273. expect(el.textContent).toBe('AAA')
  274. done()
  275. }, 0)
  276. }
  277. }
  278. }
  279. })
  280. })
  281. it('multiple activate hooks', function (done) {
  282. var mixinSpy = jasmine.createSpy('mixin activate')
  283. new Vue({
  284. el: el,
  285. template: '<view-a></view-a>',
  286. components: {
  287. 'view-a': {
  288. template: 'AAA',
  289. mixins: [{
  290. activate: function (done) {
  291. expect(el.textContent).toBe('')
  292. mixinSpy()
  293. done()
  294. }
  295. }],
  296. activate: function (ready) {
  297. setTimeout(function () {
  298. expect(mixinSpy).toHaveBeenCalled()
  299. expect(el.textContent).toBe('')
  300. ready()
  301. expect(el.textContent).toBe('AAA')
  302. done()
  303. }, 0)
  304. }
  305. }
  306. }
  307. })
  308. })
  309. it('activate hook for dynamic components', function (done) {
  310. var next
  311. var vm = new Vue({
  312. el: el,
  313. data: {
  314. view: 'view-a'
  315. },
  316. template: '<component :is="view"></component>',
  317. components: {
  318. 'view-a': {
  319. template: 'AAA',
  320. activate: function (ready) {
  321. next = ready
  322. }
  323. },
  324. 'view-b': {
  325. template: 'BBB',
  326. activate: function (ready) {
  327. next = ready
  328. }
  329. }
  330. }
  331. })
  332. expect(next).toBeTruthy()
  333. expect(el.textContent).toBe('')
  334. next()
  335. expect(el.textContent).toBe('AAA')
  336. vm.view = 'view-b'
  337. _.nextTick(function () {
  338. expect(el.textContent).toBe('AAA')
  339. // old vm is already removed, this is the new vm
  340. expect(vm.$children.length).toBe(1)
  341. next()
  342. expect(el.textContent).toBe('BBB')
  343. // ensure switching before ready event correctly
  344. // cleans up the component being waited on.
  345. // see #1152
  346. vm.view = 'view-a'
  347. // store the ready callback for view-a
  348. var callback = next
  349. _.nextTick(function () {
  350. vm.view = 'view-b'
  351. _.nextTick(function () {
  352. expect(vm.$children.length).toBe(1)
  353. expect(vm.$children[0].$el.textContent).toBe('BBB')
  354. // calling view-a's ready callback here should not throw
  355. // because it should've been cancelled (#1994)
  356. expect(callback).not.toThrow()
  357. done()
  358. })
  359. })
  360. })
  361. })
  362. it('activate hook + keep-alive', function (done) {
  363. var next
  364. var vm = new Vue({
  365. el: el,
  366. data: {
  367. view: 'view-a'
  368. },
  369. template: '<component :is="view" keep-alive></component>',
  370. components: {
  371. 'view-a': {
  372. template: 'AAA',
  373. activate: function (ready) {
  374. next = ready
  375. }
  376. },
  377. 'view-b': {
  378. template: 'BBB',
  379. activate: function (ready) {
  380. next = ready
  381. }
  382. }
  383. }
  384. })
  385. next()
  386. expect(el.textContent).toBe('AAA')
  387. vm.view = 'view-b'
  388. _.nextTick(function () {
  389. expect(vm.$children.length).toBe(2)
  390. next()
  391. expect(el.textContent).toBe('BBB')
  392. vm.view = 'view-a'
  393. _.nextTick(function () {
  394. // should switch without the need to emit
  395. // because of keep-alive
  396. expect(el.textContent).toBe('AAA')
  397. done()
  398. })
  399. })
  400. })
  401. it('transition-mode: in-out', function (done) {
  402. var spy1 = jasmine.createSpy('enter')
  403. var spy2 = jasmine.createSpy('leave')
  404. var next
  405. var vm = new Vue({
  406. el: el,
  407. data: {
  408. view: 'view-a'
  409. },
  410. template: '<component :is="view" transition="test" transition-mode="in-out"></component>',
  411. components: {
  412. 'view-a': { template: 'AAA' },
  413. 'view-b': { template: 'BBB' }
  414. },
  415. transitions: {
  416. test: {
  417. enter: function (el, done) {
  418. spy1()
  419. next = done
  420. },
  421. leave: function (el, done) {
  422. spy2()
  423. _.nextTick(done)
  424. }
  425. }
  426. }
  427. })
  428. expect(el.textContent).toBe('AAA')
  429. vm.view = 'view-b'
  430. _.nextTick(function () {
  431. expect(spy1).toHaveBeenCalled()
  432. expect(spy2).not.toHaveBeenCalled()
  433. expect(el.textContent).toBe('AAABBB')
  434. next()
  435. _.nextTick(function () {
  436. expect(spy2).toHaveBeenCalled()
  437. _.nextTick(function () {
  438. expect(el.textContent).toBe('BBB')
  439. done()
  440. })
  441. })
  442. })
  443. })
  444. it('transition-mode: out-in', function (done) {
  445. var spy1 = jasmine.createSpy('enter')
  446. var spy2 = jasmine.createSpy('leave')
  447. var next
  448. var vm = new Vue({
  449. el: el,
  450. data: {
  451. view: 'view-a'
  452. },
  453. template: '<component :is="view" transition="test" transition-mode="out-in"></component>',
  454. components: {
  455. 'view-a': { template: 'AAA' },
  456. 'view-b': { template: 'BBB' }
  457. },
  458. transitions: {
  459. test: {
  460. enter: function (el, done) {
  461. spy2()
  462. _.nextTick(done)
  463. },
  464. leave: function (el, done) {
  465. spy1()
  466. next = done
  467. }
  468. }
  469. }
  470. })
  471. expect(el.textContent).toBe('AAA')
  472. vm.view = 'view-b'
  473. _.nextTick(function () {
  474. expect(spy1).toHaveBeenCalled()
  475. expect(spy2).not.toHaveBeenCalled()
  476. expect(el.textContent).toBe('AAA')
  477. next()
  478. expect(spy2).toHaveBeenCalled()
  479. expect(el.textContent).toBe('BBB')
  480. done()
  481. })
  482. })
  483. it('teardown', function (done) {
  484. var vm = new Vue({
  485. el: el,
  486. template: '<component :is="view" keep-alive></component>',
  487. data: {
  488. view: 'test'
  489. },
  490. components: {
  491. test: {},
  492. test2: {}
  493. }
  494. })
  495. vm.view = 'test2'
  496. _.nextTick(function () {
  497. expect(vm.$children.length).toBe(2)
  498. var child = vm.$children[0]
  499. var child2 = vm.$children[1]
  500. vm._directives[0].unbind()
  501. expect(vm._directives[0].cache).toBeNull()
  502. expect(vm.$children.length).toBe(0)
  503. expect(child._isDestroyed).toBe(true)
  504. expect(child2._isDestroyed).toBe(true)
  505. done()
  506. })
  507. })
  508. it('already mounted warn', function () {
  509. el.setAttribute('is', 'test')
  510. new Vue({
  511. el: el
  512. })
  513. expect('cannot mount component "test" on already mounted element').toHaveBeenWarned()
  514. })
  515. it('not found component should not throw', function () {
  516. expect(function () {
  517. new Vue({
  518. el: el,
  519. template: '<div is="non-existent"></div>'
  520. })
  521. }).not.toThrow()
  522. })
  523. it('warn possible camelCase components', function () {
  524. new Vue({
  525. el: document.createElement('div'),
  526. template: '<HelloWorld></HelloWorld>',
  527. components: {
  528. 'hello-world': {}
  529. }
  530. })
  531. expect('did you mean <hello-world>?').toHaveBeenWarned()
  532. })
  533. })