component.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import Vue from 'vue'
  2. describe('Component', () => {
  3. it('static', () => {
  4. const vm = new Vue({
  5. template: '<test></test>',
  6. components: {
  7. test: {
  8. data() {
  9. return { a: 123 }
  10. },
  11. template: '<span>{{a}}</span>'
  12. }
  13. }
  14. }).$mount()
  15. expect(vm.$el.tagName).toBe('SPAN')
  16. expect(vm.$el.innerHTML).toBe('123')
  17. })
  18. it('using component in restricted elements', () => {
  19. const vm = new Vue({
  20. template: '<div><table><tbody><test></test></tbody></table></div>',
  21. components: {
  22. test: {
  23. data() {
  24. return { a: 123 }
  25. },
  26. template: '<tr><td>{{a}}</td></tr>'
  27. }
  28. }
  29. }).$mount()
  30. expect(vm.$el.innerHTML).toBe(
  31. '<table><tbody><tr><td>123</td></tr></tbody></table>'
  32. )
  33. })
  34. it('"is" attribute', () => {
  35. const vm = new Vue({
  36. template: '<div><table><tbody><tr is="test"></tr></tbody></table></div>',
  37. components: {
  38. test: {
  39. data() {
  40. return { a: 123 }
  41. },
  42. template: '<tr><td>{{a}}</td></tr>'
  43. }
  44. }
  45. }).$mount()
  46. expect(vm.$el.innerHTML).toBe(
  47. '<table><tbody><tr><td>123</td></tr></tbody></table>'
  48. )
  49. })
  50. it('inline-template', () => {
  51. const vm = new Vue({
  52. template: '<div><test inline-template><span>{{a}}</span></test></div>',
  53. data: {
  54. a: 'parent'
  55. },
  56. components: {
  57. test: {
  58. data() {
  59. return { a: 'child' }
  60. }
  61. }
  62. }
  63. }).$mount()
  64. expect(vm.$el.innerHTML).toBe('<span>child</span>')
  65. })
  66. it('fragment instance warning', () => {
  67. new Vue({
  68. template: '<test></test>',
  69. components: {
  70. test: {
  71. data() {
  72. return { a: 123, b: 234 }
  73. },
  74. template: '<p>{{a}}</p><p>{{b}}</p>'
  75. }
  76. }
  77. }).$mount()
  78. expect(
  79. 'Component template should contain exactly one root element'
  80. ).toHaveBeenWarned()
  81. })
  82. it('dynamic', done => {
  83. const vm = new Vue({
  84. template: '<component :is="view" :view="view"></component>',
  85. data: {
  86. view: 'view-a'
  87. },
  88. components: {
  89. 'view-a': {
  90. template: '<div>foo {{view}}</div>',
  91. data() {
  92. return { view: 'a' }
  93. }
  94. },
  95. 'view-b': {
  96. template: '<div>bar {{view}}</div>',
  97. data() {
  98. return { view: 'b' }
  99. }
  100. }
  101. }
  102. }).$mount()
  103. expect(vm.$el.outerHTML).toBe('<div view="view-a">foo a</div>')
  104. vm.view = 'view-b'
  105. waitForUpdate(() => {
  106. expect(vm.$el.outerHTML).toBe('<div view="view-b">bar b</div>')
  107. vm.view = ''
  108. })
  109. .then(() => {
  110. expect(vm.$el.nodeType).toBe(8)
  111. expect(vm.$el.data).toBe('')
  112. })
  113. .then(done)
  114. })
  115. it('dynamic with props', done => {
  116. const vm = new Vue({
  117. template: '<component :is="view" :view="view"></component>',
  118. data: {
  119. view: 'view-a'
  120. },
  121. components: {
  122. 'view-a': {
  123. template: '<div>foo {{view}}</div>',
  124. props: ['view']
  125. },
  126. 'view-b': {
  127. template: '<div>bar {{view}}</div>',
  128. props: ['view']
  129. }
  130. }
  131. }).$mount()
  132. expect(vm.$el.outerHTML).toBe('<div>foo view-a</div>')
  133. vm.view = 'view-b'
  134. waitForUpdate(() => {
  135. expect(vm.$el.outerHTML).toBe('<div>bar view-b</div>')
  136. vm.view = ''
  137. })
  138. .then(() => {
  139. expect(vm.$el.nodeType).toBe(8)
  140. expect(vm.$el.data).toBe('')
  141. })
  142. .then(done)
  143. })
  144. it(':is using raw component constructor', () => {
  145. const vm = new Vue({
  146. template:
  147. '<div>' +
  148. '<component :is="$options.components.test"></component>' +
  149. '<component :is="$options.components.async"></component>' +
  150. '</div>',
  151. components: {
  152. test: {
  153. template: '<span>foo</span>'
  154. },
  155. async: function (resolve) {
  156. resolve({
  157. template: '<span>bar</span>'
  158. })
  159. }
  160. }
  161. }).$mount()
  162. expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
  163. })
  164. it('dynamic combined with v-for', done => {
  165. const vm = new Vue({
  166. template:
  167. '<div>' +
  168. '<component v-for="(c, i) in comps" :key="i" :is="c.type"></component>' +
  169. '</div>',
  170. data: {
  171. comps: [{ type: 'one' }, { type: 'two' }]
  172. },
  173. components: {
  174. one: {
  175. template: '<span>one</span>'
  176. },
  177. two: {
  178. template: '<span>two</span>'
  179. }
  180. }
  181. }).$mount()
  182. expect(vm.$el.innerHTML).toBe('<span>one</span><span>two</span>')
  183. vm.comps[1].type = 'one'
  184. waitForUpdate(() => {
  185. expect(vm.$el.innerHTML).toBe('<span>one</span><span>one</span>')
  186. }).then(done)
  187. })
  188. it('dynamic elements with domProps', done => {
  189. const vm = new Vue({
  190. template: '<component :is="view" :value.prop="val"></component>',
  191. data: {
  192. view: 'input',
  193. val: 'hello'
  194. }
  195. }).$mount()
  196. expect(vm.$el.tagName).toBe('INPUT')
  197. expect(vm.$el.value).toBe('hello')
  198. vm.view = 'textarea'
  199. vm.val += ' world'
  200. waitForUpdate(() => {
  201. expect(vm.$el.tagName).toBe('TEXTAREA')
  202. expect(vm.$el.value).toBe('hello world')
  203. vm.view = ''
  204. }).then(done)
  205. })
  206. it('should compile parent template directives & content in parent scope', done => {
  207. const vm = new Vue({
  208. data: {
  209. ok: false,
  210. message: 'hello'
  211. },
  212. template: '<test v-show="ok">{{message}}</test>',
  213. components: {
  214. test: {
  215. template: '<div><slot></slot> {{message}}</div>',
  216. data() {
  217. return {
  218. message: 'world'
  219. }
  220. }
  221. }
  222. }
  223. }).$mount()
  224. expect(vm.$el.style.display).toBe('none')
  225. expect(vm.$el.textContent).toBe('hello world')
  226. vm.ok = true
  227. vm.message = 'bye'
  228. waitForUpdate(() => {
  229. expect(vm.$el.style.display).toBe('')
  230. expect(vm.$el.textContent).toBe('bye world')
  231. }).then(done)
  232. })
  233. it('parent content + v-if', done => {
  234. const vm = new Vue({
  235. data: {
  236. ok: false,
  237. message: 'hello'
  238. },
  239. template: '<test v-if="ok">{{message}}</test>',
  240. components: {
  241. test: {
  242. template: '<div><slot></slot> {{message}}</div>',
  243. data() {
  244. return {
  245. message: 'world'
  246. }
  247. }
  248. }
  249. }
  250. }).$mount()
  251. expect(vm.$el.textContent).toBe('')
  252. expect(vm.$children.length).toBe(0)
  253. vm.ok = true
  254. waitForUpdate(() => {
  255. expect(vm.$children.length).toBe(1)
  256. expect(vm.$el.textContent).toBe('hello world')
  257. }).then(done)
  258. })
  259. it('props', () => {
  260. const vm = new Vue({
  261. data: {
  262. list: [{ a: 1 }, { a: 2 }]
  263. },
  264. template: '<test :collection="list"></test>',
  265. components: {
  266. test: {
  267. template: '<ul><li v-for="item in collection">{{item.a}}</li></ul>',
  268. props: ['collection']
  269. }
  270. }
  271. }).$mount()
  272. expect(vm.$el.outerHTML).toBe('<ul><li>1</li><li>2</li></ul>')
  273. })
  274. it('should warn when using camelCased props in in-DOM template', () => {
  275. new Vue({
  276. data: {
  277. list: [{ a: 1 }, { a: 2 }]
  278. },
  279. template: '<test :somecollection="list"></test>', // <-- simulate lowercased template
  280. components: {
  281. test: {
  282. template:
  283. '<ul><li v-for="item in someCollection">{{item.a}}</li></ul>',
  284. props: ['someCollection']
  285. }
  286. }
  287. }).$mount()
  288. expect(
  289. 'You should probably use "some-collection" instead of "someCollection".'
  290. ).toHaveBeenTipped()
  291. })
  292. it('should warn when using camelCased events in in-DOM template', () => {
  293. new Vue({
  294. template: '<test @foobar="a++"></test>', // <-- simulate lowercased template
  295. components: {
  296. test: {
  297. template: '<div></div>',
  298. created() {
  299. this.$emit('fooBar')
  300. }
  301. }
  302. }
  303. }).$mount()
  304. expect(
  305. 'You should probably use "foo-bar" instead of "fooBar".'
  306. ).toHaveBeenTipped()
  307. })
  308. it('not found component should not throw', () => {
  309. expect(function () {
  310. new Vue({
  311. template: '<div is="non-existent"></div>'
  312. })
  313. }).not.toThrow()
  314. })
  315. it('properly update replaced higher-order component root node', done => {
  316. const vm = new Vue({
  317. data: {
  318. color: 'red'
  319. },
  320. template: '<test id="foo" :class="color"></test>',
  321. components: {
  322. test: {
  323. data() {
  324. return { tag: 'div' }
  325. },
  326. render(h) {
  327. return h(this.tag, { class: 'test' }, 'hi')
  328. }
  329. }
  330. }
  331. }).$mount()
  332. expect(vm.$el.tagName).toBe('DIV')
  333. expect(vm.$el.id).toBe('foo')
  334. expect(vm.$el.className).toBe('test red')
  335. vm.color = 'green'
  336. waitForUpdate(() => {
  337. expect(vm.$el.tagName).toBe('DIV')
  338. expect(vm.$el.id).toBe('foo')
  339. expect(vm.$el.className).toBe('test green')
  340. vm.$children[0].tag = 'p'
  341. })
  342. .then(() => {
  343. expect(vm.$el.tagName).toBe('P')
  344. expect(vm.$el.id).toBe('foo')
  345. expect(vm.$el.className).toBe('test green')
  346. vm.color = 'red'
  347. })
  348. .then(() => {
  349. expect(vm.$el.tagName).toBe('P')
  350. expect(vm.$el.id).toBe('foo')
  351. expect(vm.$el.className).toBe('test red')
  352. })
  353. .then(done)
  354. })
  355. it('catch component render error and preserve previous vnode', done => {
  356. const spy = vi.fn()
  357. Vue.config.errorHandler = spy
  358. const vm = new Vue({
  359. data: {
  360. a: {
  361. b: 123
  362. }
  363. },
  364. render(h) {
  365. return h('div', [this.a.b])
  366. }
  367. }).$mount()
  368. expect(vm.$el.textContent).toBe('123')
  369. expect(spy).not.toHaveBeenCalled()
  370. vm.a = null
  371. waitForUpdate(() => {
  372. expect(spy).toHaveBeenCalled()
  373. expect(vm.$el.textContent).toBe('123') // should preserve rendered DOM
  374. vm.a = { b: 234 }
  375. })
  376. .then(() => {
  377. expect(vm.$el.textContent).toBe('234') // should be able to recover
  378. Vue.config.errorHandler = undefined
  379. })
  380. .then(done)
  381. })
  382. it('relocates node without error', done => {
  383. const el = document.createElement('div')
  384. document.body.appendChild(el)
  385. const target = document.createElement('div')
  386. document.body.appendChild(target)
  387. const Test = {
  388. render(h) {
  389. return h('div', { class: 'test' }, this.$slots.default)
  390. },
  391. mounted() {
  392. target.appendChild(this.$el)
  393. },
  394. beforeDestroy() {
  395. const parent = this.$el.parentNode
  396. if (parent) {
  397. parent.removeChild(this.$el)
  398. }
  399. }
  400. }
  401. const vm = new Vue({
  402. data() {
  403. return {
  404. view: true
  405. }
  406. },
  407. template: `<div><test v-if="view">Test</test></div>`,
  408. components: {
  409. test: Test
  410. }
  411. }).$mount(el)
  412. expect(el.outerHTML).toBe('<div></div>')
  413. expect(target.outerHTML).toBe('<div><div class="test">Test</div></div>')
  414. vm.view = false
  415. waitForUpdate(() => {
  416. expect(el.outerHTML).toBe('<div></div>')
  417. expect(target.outerHTML).toBe('<div></div>')
  418. vm.$destroy()
  419. }).then(done)
  420. })
  421. it('render vnode with <script> tag as root element', () => {
  422. const vm = new Vue({
  423. template: '<scriptTest></scriptTest>',
  424. components: {
  425. scriptTest: {
  426. template: '<script>console.log(1)</script>'
  427. }
  428. }
  429. }).$mount()
  430. expect(vm.$el.nodeName).toBe('#comment')
  431. expect(
  432. 'Templates should only be responsible for mapping the state'
  433. ).toHaveBeenWarned()
  434. })
  435. })