component-async.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import Vue from 'vue'
  2. function wait(): [() => void, Promise<void>] {
  3. let done
  4. const p = new Promise<void>((resolve, reject) => {
  5. done = resolve
  6. done.fail = reject
  7. })
  8. return [done, p]
  9. }
  10. describe('Component async', () => {
  11. const oldSetTimeout = setTimeout
  12. const oldClearTimeout = clearTimeout
  13. // will contain pending timeouts set during the test iteration
  14. // will contain the id of the timeout as the key, and the millisecond timeout as the value
  15. // this helps to identify the timeout that is still pending
  16. let timeoutsPending = {}
  17. beforeAll(function () {
  18. // @ts-expect-error
  19. global.setTimeout = function (func, delay) {
  20. if (delay >= 1000) {
  21. // skip vitest internal timeouts
  22. return
  23. }
  24. const id = oldSetTimeout(function () {
  25. delete timeoutsPending[id]
  26. func()
  27. }, delay) as any
  28. timeoutsPending[id] = delay
  29. return id
  30. }
  31. global.clearTimeout = function (id) {
  32. oldClearTimeout(id)
  33. delete timeoutsPending[id]
  34. }
  35. })
  36. afterAll(function () {
  37. global.setTimeout = oldSetTimeout
  38. global.clearTimeout = oldClearTimeout
  39. })
  40. beforeEach(() => {
  41. // reset the timeouts for this iteration
  42. timeoutsPending = {}
  43. })
  44. afterEach(() => {
  45. // after the test is complete no timeouts that have been set up during the test should still be active
  46. // compare stringified JSON for better error message containing ID and millisecond timeout
  47. expect(JSON.stringify(timeoutsPending)).toEqual(JSON.stringify({}))
  48. })
  49. it('normal', () => {
  50. const vm = new Vue({
  51. template: '<div><test></test></div>',
  52. components: {
  53. test: (resolve) => {
  54. setTimeout(() => {
  55. resolve({
  56. template: '<div>hi</div>'
  57. })
  58. // wait for parent update
  59. Vue.nextTick(next)
  60. }, 0)
  61. }
  62. }
  63. }).$mount()
  64. expect(vm.$el.innerHTML).toBe('<!---->')
  65. expect(vm.$children.length).toBe(0)
  66. const [done, p] = wait()
  67. function next() {
  68. expect(vm.$el.innerHTML).toBe('<div>hi</div>')
  69. expect(vm.$children.length).toBe(1)
  70. done()
  71. }
  72. return p
  73. })
  74. it('resolve ES module default', () => {
  75. const vm = new Vue({
  76. template: '<div><test></test></div>',
  77. components: {
  78. test: (resolve) => {
  79. setTimeout(() => {
  80. resolve({
  81. __esModule: true,
  82. default: {
  83. template: '<div>hi</div>'
  84. }
  85. })
  86. // wait for parent update
  87. Vue.nextTick(next)
  88. }, 0)
  89. }
  90. }
  91. }).$mount()
  92. expect(vm.$el.innerHTML).toBe('<!---->')
  93. expect(vm.$children.length).toBe(0)
  94. const [done, p] = wait()
  95. function next() {
  96. expect(vm.$el.innerHTML).toBe('<div>hi</div>')
  97. expect(vm.$children.length).toBe(1)
  98. done()
  99. }
  100. return p
  101. })
  102. it('as root', () => {
  103. const vm = new Vue({
  104. template: '<test></test>',
  105. components: {
  106. test: (resolve) => {
  107. setTimeout(() => {
  108. resolve({
  109. template: '<div>hi</div>'
  110. })
  111. // wait for parent update
  112. Vue.nextTick(next)
  113. }, 0)
  114. }
  115. }
  116. }).$mount()
  117. expect(vm.$el.nodeType).toBe(8)
  118. expect(vm.$children.length).toBe(0)
  119. const [done, p] = wait()
  120. function next() {
  121. expect(vm.$el.nodeType).toBe(1)
  122. expect(vm.$el.outerHTML).toBe('<div>hi</div>')
  123. expect(vm.$children.length).toBe(1)
  124. done()
  125. }
  126. return p
  127. })
  128. it('dynamic', () => {
  129. const vm = new Vue({
  130. template: '<component :is="view"></component>',
  131. data: {
  132. view: 'view-a'
  133. },
  134. components: {
  135. 'view-a': (resolve) => {
  136. setTimeout(() => {
  137. resolve({
  138. template: '<div>A</div>'
  139. })
  140. Vue.nextTick(step1)
  141. }, 0)
  142. },
  143. 'view-b': (resolve) => {
  144. setTimeout(() => {
  145. resolve({
  146. template: '<p>B</p>'
  147. })
  148. Vue.nextTick(step2)
  149. }, 0)
  150. }
  151. }
  152. }).$mount()
  153. let aCalled = false
  154. function step1() {
  155. // ensure A is resolved only once
  156. expect(aCalled).toBe(false)
  157. aCalled = true
  158. expect(vm.$el.tagName).toBe('DIV')
  159. expect(vm.$el.textContent).toBe('A')
  160. vm.view = 'view-b'
  161. }
  162. const [done, p] = wait()
  163. function step2() {
  164. expect(vm.$el.tagName).toBe('P')
  165. expect(vm.$el.textContent).toBe('B')
  166. vm.view = 'view-a'
  167. waitForUpdate(() => {
  168. expect(vm.$el.tagName).toBe('DIV')
  169. expect(vm.$el.textContent).toBe('A')
  170. }).then(done)
  171. }
  172. return p
  173. })
  174. it('warn reject', () => {
  175. new Vue({
  176. template: '<test></test>',
  177. components: {
  178. test: (resolve, reject) => {
  179. reject('nooooo')
  180. }
  181. }
  182. }).$mount()
  183. expect('Reason: nooooo').toHaveBeenWarned()
  184. })
  185. it('with v-for', () => {
  186. const vm = new Vue({
  187. template: '<div><test v-for="n in list" :key="n" :n="n"></test></div>',
  188. data: {
  189. list: [1, 2, 3]
  190. },
  191. components: {
  192. test: (resolve) => {
  193. setTimeout(() => {
  194. resolve({
  195. props: ['n'],
  196. template: '<div>{{n}}</div>'
  197. })
  198. Vue.nextTick(next)
  199. }, 0)
  200. }
  201. }
  202. }).$mount()
  203. const [done, p] = wait()
  204. function next() {
  205. expect(vm.$el.innerHTML).toBe('<div>1</div><div>2</div><div>3</div>')
  206. done()
  207. }
  208. return p
  209. })
  210. it('returning Promise', () => {
  211. const vm = new Vue({
  212. template: '<div><test></test></div>',
  213. components: {
  214. test: () => {
  215. return new Promise((resolve) => {
  216. setTimeout(() => {
  217. resolve({
  218. template: '<div>hi</div>'
  219. })
  220. // wait for promise resolve and then parent update
  221. Promise.resolve().then(() => {
  222. Vue.nextTick(next)
  223. })
  224. }, 0)
  225. })
  226. }
  227. }
  228. }).$mount()
  229. expect(vm.$el.innerHTML).toBe('<!---->')
  230. expect(vm.$children.length).toBe(0)
  231. const [done, p] = wait()
  232. function next() {
  233. expect(vm.$el.innerHTML).toBe('<div>hi</div>')
  234. expect(vm.$children.length).toBe(1)
  235. done()
  236. }
  237. return p
  238. })
  239. describe('loading/error/timeout', () => {
  240. it('with loading component', () => {
  241. const vm = new Vue({
  242. template: `<div><test/></div>`,
  243. components: {
  244. test: () => ({
  245. component: new Promise((resolve) => {
  246. setTimeout(() => {
  247. resolve({ template: '<div>hi</div>' })
  248. // wait for promise resolve and then parent update
  249. Promise.resolve().then(() => {
  250. Vue.nextTick(next)
  251. })
  252. }, 50)
  253. }),
  254. loading: { template: `<div>loading</div>` },
  255. delay: 1
  256. })
  257. }
  258. }).$mount()
  259. expect(vm.$el.innerHTML).toBe('<!---->')
  260. let loadingAsserted = false
  261. setTimeout(() => {
  262. Vue.nextTick(() => {
  263. loadingAsserted = true
  264. expect(vm.$el.textContent).toBe('loading')
  265. })
  266. }, 1)
  267. const [done, p] = wait()
  268. function next() {
  269. expect(loadingAsserted).toBe(true)
  270. expect(vm.$el.textContent).toBe('hi')
  271. done()
  272. }
  273. return p
  274. })
  275. it('with loading component (0 delay)', () => {
  276. const vm = new Vue({
  277. template: `<div><test/></div>`,
  278. components: {
  279. test: () => ({
  280. component: new Promise((resolve) => {
  281. setTimeout(() => {
  282. resolve({ template: '<div>hi</div>' })
  283. // wait for promise resolve and then parent update
  284. Promise.resolve().then(() => {
  285. Vue.nextTick(next)
  286. })
  287. }, 50)
  288. }),
  289. loading: { template: `<div>loading</div>` },
  290. delay: 0
  291. })
  292. }
  293. }).$mount()
  294. expect(vm.$el.textContent).toBe('loading')
  295. const [done, p] = wait()
  296. function next() {
  297. expect(vm.$el.textContent).toBe('hi')
  298. done()
  299. }
  300. return p
  301. })
  302. it('with error component', () => {
  303. const vm = new Vue({
  304. template: `<div><test/></div>`,
  305. components: {
  306. test: () => ({
  307. component: new Promise((resolve, reject) => {
  308. setTimeout(() => {
  309. reject()
  310. // wait for promise resolve and then parent update
  311. Promise.resolve().then(() => {
  312. Vue.nextTick(next)
  313. })
  314. }, 50)
  315. }),
  316. loading: { template: `<div>loading</div>` },
  317. error: { template: `<div>error</div>` },
  318. delay: 0
  319. })
  320. }
  321. }).$mount()
  322. expect(vm.$el.textContent).toBe('loading')
  323. const [done, p] = wait()
  324. function next() {
  325. expect(`Failed to resolve async component`).toHaveBeenWarned()
  326. expect(vm.$el.textContent).toBe('error')
  327. done()
  328. }
  329. return p
  330. })
  331. it('with error component + timeout', () => {
  332. const vm = new Vue({
  333. template: `<div><test/></div>`,
  334. components: {
  335. test: () => ({
  336. component: new Promise((resolve, reject) => {
  337. setTimeout(() => {
  338. resolve({ template: '<div>hi</div>' })
  339. // wait for promise resolve and then parent update
  340. Promise.resolve().then(() => {
  341. Vue.nextTick(next)
  342. })
  343. }, 50)
  344. }),
  345. loading: { template: `<div>loading</div>` },
  346. error: { template: `<div>error</div>` },
  347. delay: 0,
  348. timeout: 1
  349. })
  350. }
  351. }).$mount()
  352. expect(vm.$el.textContent).toBe('loading')
  353. setTimeout(() => {
  354. Vue.nextTick(() => {
  355. expect(`Failed to resolve async component`).toHaveBeenWarned()
  356. expect(vm.$el.textContent).toBe('error')
  357. })
  358. }, 1)
  359. const [done, p] = wait()
  360. function next() {
  361. expect(vm.$el.textContent).toBe('error') // late resolve ignored
  362. done()
  363. }
  364. return p
  365. })
  366. it('should not trigger timeout if resolved', () => {
  367. const vm = new Vue({
  368. template: `<div><test/></div>`,
  369. components: {
  370. test: () => ({
  371. component: new Promise((resolve, reject) => {
  372. setTimeout(() => {
  373. resolve({ template: '<div>hi</div>' })
  374. }, 10)
  375. }),
  376. error: { template: `<div>error</div>` },
  377. timeout: 20
  378. })
  379. }
  380. }).$mount()
  381. const [done, p] = wait()
  382. setTimeout(() => {
  383. expect(vm.$el.textContent).toBe('hi')
  384. expect(`Failed to resolve async component`).not.toHaveBeenWarned()
  385. done()
  386. }, 50)
  387. return p
  388. })
  389. it('should not have running timeout/loading if resolved', () => {
  390. const vm = new Vue({
  391. template: `<div><test/></div>`,
  392. components: {
  393. test: () => ({
  394. component: new Promise((resolve, reject) => {
  395. setTimeout(() => {
  396. resolve({ template: '<div>hi</div>' })
  397. Promise.resolve().then(() => {
  398. Vue.nextTick(next)
  399. })
  400. }, 10)
  401. }),
  402. loading: { template: `<div>loading</div>` },
  403. delay: 30,
  404. error: { template: `<div>error</div>` },
  405. timeout: 40
  406. })
  407. }
  408. }).$mount()
  409. const [done, p] = wait()
  410. function next() {
  411. expect(vm.$el.textContent).toBe('hi')
  412. // the afterEach() will ensure that the timeouts for delay and timeout have been cleared
  413. done()
  414. }
  415. return p
  416. })
  417. // #7107
  418. it(`should work when resolving sync in sibling component's mounted hook`, () => {
  419. let resolveTwo
  420. const vm = new Vue({
  421. template: `<div><one/> <two/></div>`,
  422. components: {
  423. one: {
  424. template: `<div>one</div>`,
  425. mounted() {
  426. resolveTwo()
  427. }
  428. },
  429. two: (resolve) => {
  430. resolveTwo = () => {
  431. resolve({
  432. template: `<div>two</div>`
  433. })
  434. }
  435. }
  436. }
  437. }).$mount()
  438. expect(vm.$el.textContent).toBe('one ')
  439. const [done, p] = wait()
  440. waitForUpdate(() => {
  441. expect(vm.$el.textContent).toBe('one two')
  442. }).then(done)
  443. return p
  444. })
  445. })
  446. })