component_spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  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>foo</div>',
  103. replace: true,
  104. data: function () {
  105. return { view: 'a' }
  106. }
  107. },
  108. 'view-b': {
  109. template: '<div>bar</div>',
  110. replace: true,
  111. data: function () {
  112. return { view: 'b' }
  113. }
  114. }
  115. }
  116. })
  117. expect(el.innerHTML).toBe('<div view="view-a">foo</div>')
  118. vm.view = 'view-b'
  119. _.nextTick(function () {
  120. expect(el.innerHTML).toBe('<div view="view-b">bar</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:
  132. '<component :is="$options.components.test"></component>' +
  133. '<component :is="$options.components.async"></component>',
  134. components: {
  135. test: {
  136. template: 'foo'
  137. },
  138. async: function (resolve) {
  139. resolve({
  140. template: 'bar'
  141. })
  142. }
  143. }
  144. })
  145. expect(el.textContent).toBe('foobar')
  146. })
  147. it('keep-alive', function (done) {
  148. var spyA = jasmine.createSpy()
  149. var spyB = jasmine.createSpy()
  150. var vm = new Vue({
  151. el: el,
  152. template: '<component :is="view" keep-alive></component>',
  153. data: {
  154. view: 'view-a'
  155. },
  156. components: {
  157. 'view-a': {
  158. created: spyA,
  159. template: '<div>foo</div>',
  160. replace: true
  161. },
  162. 'view-b': {
  163. created: spyB,
  164. template: '<div>bar</div>',
  165. replace: true
  166. }
  167. }
  168. })
  169. expect(el.innerHTML).toBe('<div>foo</div>')
  170. expect(spyA.calls.count()).toBe(1)
  171. expect(spyB.calls.count()).toBe(0)
  172. vm.view = 'view-b'
  173. _.nextTick(function () {
  174. expect(el.innerHTML).toBe('<div>bar</div>')
  175. expect(spyA.calls.count()).toBe(1)
  176. expect(spyB.calls.count()).toBe(1)
  177. vm.view = 'view-a'
  178. _.nextTick(function () {
  179. expect(el.innerHTML).toBe('<div>foo</div>')
  180. expect(spyA.calls.count()).toBe(1)
  181. expect(spyB.calls.count()).toBe(1)
  182. vm.view = 'view-b'
  183. _.nextTick(function () {
  184. expect(el.innerHTML).toBe('<div>bar</div>')
  185. expect(spyA.calls.count()).toBe(1)
  186. expect(spyB.calls.count()).toBe(1)
  187. done()
  188. })
  189. })
  190. })
  191. })
  192. it('should compile parent template directives & content in parent scope', function (done) {
  193. var vm = new Vue({
  194. el: el,
  195. data: {
  196. ok: false,
  197. message: 'hello'
  198. },
  199. template: '<test v-show="ok">{{message}}</test>',
  200. components: {
  201. test: {
  202. template: '<div><slot></slot> {{message}}</div>',
  203. replace: true,
  204. data: function () {
  205. return {
  206. message: 'world'
  207. }
  208. }
  209. }
  210. }
  211. })
  212. expect(el.firstChild.style.display).toBe('none')
  213. expect(el.firstChild.textContent).toBe('hello world')
  214. vm.ok = true
  215. vm.message = 'bye'
  216. _.nextTick(function () {
  217. expect(el.firstChild.style.display).toBe('')
  218. expect(el.firstChild.textContent).toBe('bye world')
  219. done()
  220. })
  221. })
  222. it('parent content + v-if', function (done) {
  223. var vm = new Vue({
  224. el: el,
  225. data: {
  226. ok: false,
  227. message: 'hello'
  228. },
  229. template: '<test v-if="ok">{{message}}</test>',
  230. components: {
  231. test: {
  232. template: '<slot></slot> {{message}}',
  233. data: function () {
  234. return {
  235. message: 'world'
  236. }
  237. }
  238. }
  239. }
  240. })
  241. expect(el.textContent).toBe('')
  242. expect(vm.$children.length).toBe(0)
  243. expect(vm._directives.length).toBe(1) // v-if
  244. vm.ok = true
  245. _.nextTick(function () {
  246. expect(vm.$children.length).toBe(1)
  247. expect(vm._directives.length).toBe(3) // v-if, component, v-text
  248. expect(el.textContent).toBe('hello world')
  249. done()
  250. })
  251. })
  252. it('props', function () {
  253. new Vue({
  254. el: el,
  255. data: {
  256. list: [{a: 1}, {a: 2}]
  257. },
  258. template: '<test :collection="list"></test>',
  259. components: {
  260. test: {
  261. template: '<ul><li v-for="item in collection">{{item.a}}</li></ul>',
  262. replace: true,
  263. props: ['collection']
  264. }
  265. }
  266. })
  267. expect(el.innerHTML).toBe('<ul><li>1</li><li>2</li></ul>')
  268. })
  269. it('activate hook for static component', function (done) {
  270. new Vue({
  271. el: el,
  272. template: '<view-a></view-a>',
  273. components: {
  274. 'view-a': {
  275. template: 'foo',
  276. activate: function (ready) {
  277. setTimeout(function () {
  278. expect(el.textContent).toBe('')
  279. ready()
  280. expect(el.textContent).toBe('foo')
  281. done()
  282. }, 0)
  283. }
  284. }
  285. }
  286. })
  287. })
  288. it('multiple activate hooks', function (done) {
  289. var mixinSpy = jasmine.createSpy('mixin activate')
  290. new Vue({
  291. el: el,
  292. template: '<view-a></view-a>',
  293. components: {
  294. 'view-a': {
  295. template: 'foo',
  296. mixins: [{
  297. activate: function (done) {
  298. expect(el.textContent).toBe('')
  299. mixinSpy()
  300. done()
  301. }
  302. }],
  303. activate: function (ready) {
  304. setTimeout(function () {
  305. expect(mixinSpy).toHaveBeenCalled()
  306. expect(el.textContent).toBe('')
  307. ready()
  308. expect(el.textContent).toBe('foo')
  309. done()
  310. }, 0)
  311. }
  312. }
  313. }
  314. })
  315. })
  316. it('activate hook for dynamic components', function (done) {
  317. var next
  318. var vm = new Vue({
  319. el: el,
  320. data: {
  321. view: 'view-a'
  322. },
  323. template: '<component :is="view"></component>',
  324. components: {
  325. 'view-a': {
  326. template: 'foo',
  327. activate: function (ready) {
  328. next = ready
  329. }
  330. },
  331. 'view-b': {
  332. template: 'bar',
  333. activate: function (ready) {
  334. next = ready
  335. }
  336. }
  337. }
  338. })
  339. expect(next).toBeTruthy()
  340. expect(el.textContent).toBe('')
  341. next()
  342. expect(el.textContent).toBe('foo')
  343. vm.view = 'view-b'
  344. _.nextTick(function () {
  345. expect(el.textContent).toBe('foo')
  346. // old vm is already removed, this is the new vm
  347. expect(vm.$children.length).toBe(1)
  348. next()
  349. expect(el.textContent).toBe('bar')
  350. // ensure switching before ready event correctly
  351. // cleans up the component being waited on.
  352. // see #1152
  353. vm.view = 'view-a'
  354. // store the ready callback for view-a
  355. var callback = next
  356. _.nextTick(function () {
  357. vm.view = 'view-b'
  358. _.nextTick(function () {
  359. expect(vm.$children.length).toBe(1)
  360. expect(vm.$children[0].$el.textContent).toBe('bar')
  361. // calling view-a's ready callback here should not throw
  362. // because it should've been cancelled (#1994)
  363. expect(callback).not.toThrow()
  364. done()
  365. })
  366. })
  367. })
  368. })
  369. it('activate hook + keep-alive', function (done) {
  370. var next
  371. var vm = new Vue({
  372. el: el,
  373. data: {
  374. view: 'view-a'
  375. },
  376. template: '<component :is="view" keep-alive></component>',
  377. components: {
  378. 'view-a': {
  379. template: 'foo',
  380. activate: function (ready) {
  381. next = ready
  382. }
  383. },
  384. 'view-b': {
  385. template: 'bar',
  386. activate: function (ready) {
  387. next = ready
  388. }
  389. }
  390. }
  391. })
  392. next()
  393. expect(el.textContent).toBe('foo')
  394. vm.view = 'view-b'
  395. _.nextTick(function () {
  396. expect(vm.$children.length).toBe(2)
  397. next()
  398. expect(el.textContent).toBe('bar')
  399. vm.view = 'view-a'
  400. _.nextTick(function () {
  401. // should switch without the need to emit
  402. // because of keep-alive
  403. expect(el.textContent).toBe('foo')
  404. done()
  405. })
  406. })
  407. })
  408. it('transition-mode: in-out', function (done) {
  409. var spy1 = jasmine.createSpy('enter')
  410. var spy2 = jasmine.createSpy('leave')
  411. var next
  412. var vm = new Vue({
  413. el: el,
  414. data: {
  415. view: 'view-a'
  416. },
  417. template: '<component :is="view" transition="test" transition-mode="in-out"></component>',
  418. components: {
  419. 'view-a': { template: 'foo' },
  420. 'view-b': { template: 'bar' }
  421. },
  422. transitions: {
  423. test: {
  424. enter: function (el, done) {
  425. spy1()
  426. next = done
  427. },
  428. leave: function (el, done) {
  429. spy2()
  430. _.nextTick(done)
  431. }
  432. }
  433. }
  434. })
  435. expect(el.textContent).toBe('foo')
  436. vm.view = 'view-b'
  437. _.nextTick(function () {
  438. expect(spy1).toHaveBeenCalled()
  439. expect(spy2).not.toHaveBeenCalled()
  440. expect(el.textContent).toBe('foobar')
  441. next()
  442. _.nextTick(function () {
  443. expect(spy2).toHaveBeenCalled()
  444. _.nextTick(function () {
  445. expect(el.textContent).toBe('bar')
  446. done()
  447. })
  448. })
  449. })
  450. })
  451. it('transition-mode: out-in', function (done) {
  452. var spy1 = jasmine.createSpy('enter')
  453. var spy2 = jasmine.createSpy('leave')
  454. var next
  455. var vm = new Vue({
  456. el: el,
  457. data: {
  458. view: 'view-a'
  459. },
  460. template: '<component :is="view" transition="test" transition-mode="out-in"></component>',
  461. components: {
  462. 'view-a': { template: 'foo' },
  463. 'view-b': { template: 'bar' }
  464. },
  465. transitions: {
  466. test: {
  467. enter: function (el, done) {
  468. spy2()
  469. _.nextTick(done)
  470. },
  471. leave: function (el, done) {
  472. spy1()
  473. next = done
  474. }
  475. }
  476. }
  477. })
  478. expect(el.textContent).toBe('foo')
  479. vm.view = 'view-b'
  480. _.nextTick(function () {
  481. expect(spy1).toHaveBeenCalled()
  482. expect(spy2).not.toHaveBeenCalled()
  483. expect(el.textContent).toBe('foo')
  484. next()
  485. expect(spy2).toHaveBeenCalled()
  486. expect(el.textContent).toBe('bar')
  487. done()
  488. })
  489. })
  490. it('teardown', function (done) {
  491. var vm = new Vue({
  492. el: el,
  493. template: '<component :is="view" keep-alive></component>',
  494. data: {
  495. view: 'test'
  496. },
  497. components: {
  498. test: {},
  499. test2: {}
  500. }
  501. })
  502. vm.view = 'test2'
  503. _.nextTick(function () {
  504. expect(vm.$children.length).toBe(2)
  505. var child = vm.$children[0]
  506. var child2 = vm.$children[1]
  507. vm._directives[0].unbind()
  508. expect(vm._directives[0].cache).toBeNull()
  509. expect(vm.$children.length).toBe(0)
  510. expect(child._isDestroyed).toBe(true)
  511. expect(child2._isDestroyed).toBe(true)
  512. done()
  513. })
  514. })
  515. it('already mounted warn', function () {
  516. new Vue({
  517. el: document.createElement('test'),
  518. components: {
  519. test: {}
  520. }
  521. })
  522. expect('cannot mount component "test" on already mounted element').toHaveBeenWarned()
  523. })
  524. it('not found component should not throw', function () {
  525. expect(function () {
  526. new Vue({
  527. el: el,
  528. template: '<div is="non-existent"></div>'
  529. })
  530. }).not.toThrow()
  531. })
  532. it('warn possible camelCase components', function () {
  533. new Vue({
  534. el: document.createElement('div'),
  535. template: '<HelloWorld></HelloWorld>',
  536. components: {
  537. 'hello-world': {}
  538. }
  539. })
  540. expect('did you mean <hello-world>?').toHaveBeenWarned()
  541. })
  542. })