apiSetupHelpers.spec.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. import {
  2. type ComponentInternalInstance,
  3. type ComputedRef,
  4. Fragment,
  5. type Ref,
  6. type SetupContext,
  7. Suspense,
  8. computed,
  9. createApp,
  10. createBlock,
  11. createElementBlock,
  12. createElementVNode,
  13. createVNode,
  14. defineComponent,
  15. getCurrentInstance,
  16. h,
  17. nextTick,
  18. nodeOps,
  19. onMounted,
  20. openBlock,
  21. ref,
  22. render,
  23. serializeInner,
  24. shallowReactive,
  25. watch,
  26. } from '@vue/runtime-test'
  27. import {
  28. createPropsRestProxy,
  29. defineEmits,
  30. defineExpose,
  31. defineProps,
  32. mergeDefaults,
  33. mergeModels,
  34. useAttrs,
  35. useModel,
  36. useSlots,
  37. withAsyncContext,
  38. withDefaults,
  39. } from '../src/apiSetupHelpers'
  40. describe('SFC <script setup> helpers', () => {
  41. test('should warn runtime usage', () => {
  42. defineProps()
  43. expect(`defineProps() is a compiler-hint`).toHaveBeenWarned()
  44. defineEmits()
  45. expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
  46. defineExpose()
  47. expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
  48. withDefaults({}, {})
  49. expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
  50. })
  51. test('useSlots / useAttrs (no args)', () => {
  52. let slots: SetupContext['slots'] | undefined
  53. let attrs: SetupContext['attrs'] | undefined
  54. const Comp = {
  55. setup() {
  56. slots = useSlots()
  57. attrs = useAttrs()
  58. return () => {}
  59. },
  60. }
  61. const passedAttrs = { id: 'foo' }
  62. const passedSlots = {
  63. default: () => {},
  64. x: () => {},
  65. }
  66. render(h(Comp, passedAttrs, passedSlots), nodeOps.createElement('div'))
  67. expect(typeof slots!.default).toBe('function')
  68. expect(typeof slots!.x).toBe('function')
  69. expect(attrs).toMatchObject(passedAttrs)
  70. })
  71. test('useSlots / useAttrs (with args)', () => {
  72. let slots: SetupContext['slots'] | undefined
  73. let attrs: SetupContext['attrs'] | undefined
  74. let ctx: SetupContext | undefined
  75. const Comp = defineComponent({
  76. setup(_, _ctx) {
  77. slots = useSlots()
  78. attrs = useAttrs()
  79. ctx = _ctx
  80. return () => {}
  81. },
  82. })
  83. render(h(Comp), nodeOps.createElement('div'))
  84. expect(slots).toBe(ctx!.slots)
  85. expect(attrs).toBe(ctx!.attrs)
  86. })
  87. describe('mergeDefaults', () => {
  88. test('object syntax', () => {
  89. const merged = mergeDefaults(
  90. {
  91. foo: null,
  92. bar: { type: String, required: false },
  93. baz: String,
  94. },
  95. {
  96. foo: 1,
  97. bar: 'baz',
  98. baz: 'qux',
  99. },
  100. )
  101. expect(merged).toMatchObject({
  102. foo: { default: 1 },
  103. bar: { type: String, required: false, default: 'baz' },
  104. baz: { type: String, default: 'qux' },
  105. })
  106. })
  107. test('array syntax', () => {
  108. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  109. foo: 1,
  110. bar: 'baz',
  111. baz: 'qux',
  112. })
  113. expect(merged).toMatchObject({
  114. foo: { default: 1 },
  115. bar: { default: 'baz' },
  116. baz: { default: 'qux' },
  117. })
  118. })
  119. test('merging with skipFactory', () => {
  120. const fn = () => {}
  121. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  122. foo: fn,
  123. __skip_foo: true,
  124. })
  125. expect(merged).toMatchObject({
  126. foo: { default: fn, skipFactory: true },
  127. })
  128. })
  129. test('should warn missing', () => {
  130. mergeDefaults({}, { foo: 1 })
  131. expect(
  132. `props default key "foo" has no corresponding declaration`,
  133. ).toHaveBeenWarned()
  134. })
  135. })
  136. describe('mergeModels', () => {
  137. test('array syntax', () => {
  138. expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
  139. 'foo',
  140. 'bar',
  141. 'baz',
  142. ])
  143. })
  144. test('object syntax', () => {
  145. expect(
  146. mergeModels({ foo: null, bar: { required: true } }, ['baz']),
  147. ).toMatchObject({
  148. foo: null,
  149. bar: { required: true },
  150. baz: {},
  151. })
  152. expect(
  153. mergeModels(['baz'], { foo: null, bar: { required: true } }),
  154. ).toMatchObject({
  155. foo: null,
  156. bar: { required: true },
  157. baz: {},
  158. })
  159. })
  160. test('overwrite', () => {
  161. expect(
  162. mergeModels(
  163. { foo: null, bar: { required: true } },
  164. { bar: {}, baz: {} },
  165. ),
  166. ).toMatchObject({
  167. foo: null,
  168. bar: {},
  169. baz: {},
  170. })
  171. })
  172. })
  173. describe('useModel', () => {
  174. test('basic', async () => {
  175. let foo: any
  176. const update = () => {
  177. foo.value = 'bar'
  178. }
  179. const compRender = vi.fn()
  180. const Comp = defineComponent({
  181. props: ['modelValue'],
  182. emits: ['update:modelValue'],
  183. setup(props) {
  184. foo = useModel(props, 'modelValue')
  185. return () => {
  186. compRender()
  187. return foo.value
  188. }
  189. },
  190. })
  191. const msg = ref('')
  192. const setValue = vi.fn(v => (msg.value = v))
  193. const root = nodeOps.createElement('div')
  194. createApp(() =>
  195. h(Comp, {
  196. modelValue: msg.value,
  197. 'onUpdate:modelValue': setValue,
  198. }),
  199. ).mount(root)
  200. expect(foo.value).toBe('')
  201. expect(msg.value).toBe('')
  202. expect(setValue).not.toBeCalled()
  203. expect(compRender).toBeCalledTimes(1)
  204. expect(serializeInner(root)).toBe('')
  205. // update from child
  206. update()
  207. await nextTick()
  208. expect(msg.value).toBe('bar')
  209. expect(foo.value).toBe('bar')
  210. expect(setValue).toBeCalledTimes(1)
  211. expect(compRender).toBeCalledTimes(2)
  212. expect(serializeInner(root)).toBe('bar')
  213. // update from parent
  214. msg.value = 'qux'
  215. expect(msg.value).toBe('qux')
  216. await nextTick()
  217. expect(msg.value).toBe('qux')
  218. expect(foo.value).toBe('qux')
  219. expect(setValue).toBeCalledTimes(1)
  220. expect(compRender).toBeCalledTimes(3)
  221. expect(serializeInner(root)).toBe('qux')
  222. })
  223. test('without parent value (local mutation)', async () => {
  224. let foo: any
  225. const update = () => {
  226. foo.value = 'bar'
  227. }
  228. const compRender = vi.fn()
  229. const Comp = defineComponent({
  230. props: ['foo'],
  231. emits: ['update:foo'],
  232. setup(props) {
  233. foo = useModel(props, 'foo')
  234. return () => {
  235. compRender()
  236. return foo.value
  237. }
  238. },
  239. })
  240. const root = nodeOps.createElement('div')
  241. const updateFoo = vi.fn()
  242. render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
  243. expect(compRender).toBeCalledTimes(1)
  244. expect(serializeInner(root)).toBe('<!---->')
  245. expect(foo.value).toBeUndefined()
  246. update()
  247. // when parent didn't provide value, local mutation is enabled
  248. expect(foo.value).toBe('bar')
  249. await nextTick()
  250. expect(updateFoo).toBeCalledTimes(1)
  251. expect(compRender).toBeCalledTimes(2)
  252. expect(serializeInner(root)).toBe('bar')
  253. })
  254. test('without parent listener (local mutation)', async () => {
  255. let foo: any
  256. const update = () => {
  257. foo.value = 'bar'
  258. }
  259. const compRender = vi.fn()
  260. const Comp = defineComponent({
  261. props: ['foo'],
  262. emits: ['update:foo'],
  263. setup(props) {
  264. foo = useModel(props, 'foo')
  265. return () => {
  266. compRender()
  267. return foo.value
  268. }
  269. },
  270. })
  271. const root = nodeOps.createElement('div')
  272. // provide initial value
  273. render(h(Comp, { foo: 'initial' }), root)
  274. expect(compRender).toBeCalledTimes(1)
  275. expect(serializeInner(root)).toBe('initial')
  276. expect(foo.value).toBe('initial')
  277. update()
  278. // when parent didn't provide value, local mutation is enabled
  279. expect(foo.value).toBe('bar')
  280. await nextTick()
  281. expect(compRender).toBeCalledTimes(2)
  282. expect(serializeInner(root)).toBe('bar')
  283. })
  284. test('kebab-case v-model (should not be local)', async () => {
  285. let foo: any
  286. const compRender = vi.fn()
  287. const Comp = defineComponent({
  288. props: ['fooBar'],
  289. emits: ['update:fooBar'],
  290. setup(props) {
  291. foo = useModel(props, 'fooBar')
  292. return () => {
  293. compRender()
  294. return foo.value
  295. }
  296. },
  297. })
  298. const updateFooBar = vi.fn()
  299. const root = nodeOps.createElement('div')
  300. // v-model:foo-bar compiles to foo-bar and onUpdate:fooBar
  301. render(
  302. h(Comp, { 'foo-bar': 'initial', 'onUpdate:fooBar': updateFooBar }),
  303. root,
  304. )
  305. expect(compRender).toBeCalledTimes(1)
  306. expect(serializeInner(root)).toBe('initial')
  307. expect(foo.value).toBe('initial')
  308. foo.value = 'bar'
  309. // should not be using local mode, so nothing should actually change
  310. expect(foo.value).toBe('initial')
  311. await nextTick()
  312. expect(compRender).toBeCalledTimes(1)
  313. expect(updateFooBar).toBeCalledTimes(1)
  314. expect(updateFooBar).toHaveBeenCalledWith('bar')
  315. expect(foo.value).toBe('initial')
  316. expect(serializeInner(root)).toBe('initial')
  317. })
  318. test('kebab-case update listener (should not be local)', async () => {
  319. let foo: any
  320. const compRender = vi.fn()
  321. const Comp = defineComponent({
  322. props: ['fooBar'],
  323. emits: ['update:fooBar'],
  324. setup(props) {
  325. foo = useModel(props, 'fooBar')
  326. return () => {
  327. compRender()
  328. return foo.value
  329. }
  330. },
  331. })
  332. const updateFooBar = vi.fn()
  333. const root = nodeOps.createElement('div')
  334. // The template compiler won't create hyphenated listeners, but it could have been passed manually
  335. render(
  336. h(Comp, { 'foo-bar': 'initial', 'onUpdate:foo-bar': updateFooBar }),
  337. root,
  338. )
  339. expect(compRender).toBeCalledTimes(1)
  340. expect(serializeInner(root)).toBe('initial')
  341. expect(foo.value).toBe('initial')
  342. foo.value = 'bar'
  343. // should not be using local mode, so nothing should actually change
  344. expect(foo.value).toBe('initial')
  345. await nextTick()
  346. expect(compRender).toBeCalledTimes(1)
  347. expect(updateFooBar).toBeCalledTimes(1)
  348. expect(updateFooBar).toHaveBeenCalledWith('bar')
  349. expect(foo.value).toBe('initial')
  350. expect(serializeInner(root)).toBe('initial')
  351. })
  352. test('default value', async () => {
  353. let count: any
  354. const inc = () => {
  355. count.value++
  356. }
  357. const compRender = vi.fn()
  358. const Comp = defineComponent({
  359. props: { count: { default: 0 } },
  360. emits: ['update:count'],
  361. setup(props) {
  362. count = useModel(props, 'count')
  363. return () => {
  364. compRender()
  365. return count.value
  366. }
  367. },
  368. })
  369. const root = nodeOps.createElement('div')
  370. const updateCount = vi.fn()
  371. render(h(Comp, { 'onUpdate:count': updateCount }), root)
  372. expect(compRender).toBeCalledTimes(1)
  373. expect(serializeInner(root)).toBe('0')
  374. expect(count.value).toBe(0)
  375. inc()
  376. // when parent didn't provide value, local mutation is enabled
  377. expect(count.value).toBe(1)
  378. await nextTick()
  379. expect(updateCount).toBeCalledTimes(1)
  380. expect(compRender).toBeCalledTimes(2)
  381. expect(serializeInner(root)).toBe('1')
  382. })
  383. test('parent limiting child value', async () => {
  384. let childCount: Ref<number>
  385. const compRender = vi.fn()
  386. const Comp = defineComponent({
  387. props: ['count'],
  388. emits: ['update:count'],
  389. setup(props) {
  390. childCount = useModel(props, 'count')
  391. return () => {
  392. compRender()
  393. return childCount.value
  394. }
  395. },
  396. })
  397. const Parent = defineComponent({
  398. setup() {
  399. const count = ref(0)
  400. watch(count, () => {
  401. if (count.value < 0) {
  402. count.value = 0
  403. }
  404. })
  405. return () =>
  406. h(Comp, {
  407. count: count.value,
  408. 'onUpdate:count': val => {
  409. count.value = val
  410. },
  411. })
  412. },
  413. })
  414. const root = nodeOps.createElement('div')
  415. render(h(Parent), root)
  416. expect(serializeInner(root)).toBe('0')
  417. // child update
  418. childCount!.value = 1
  419. // not yet updated
  420. expect(childCount!.value).toBe(0)
  421. await nextTick()
  422. expect(childCount!.value).toBe(1)
  423. expect(serializeInner(root)).toBe('1')
  424. // child update to invalid value
  425. childCount!.value = -1
  426. // not yet updated
  427. expect(childCount!.value).toBe(1)
  428. await nextTick()
  429. // limited to 0 by parent
  430. expect(childCount!.value).toBe(0)
  431. expect(serializeInner(root)).toBe('0')
  432. })
  433. test('has parent value -> no parent value', async () => {
  434. let childCount: Ref<number>
  435. const compRender = vi.fn()
  436. const Comp = defineComponent({
  437. props: ['count'],
  438. emits: ['update:count'],
  439. setup(props) {
  440. childCount = useModel(props, 'count')
  441. return () => {
  442. compRender()
  443. return childCount.value
  444. }
  445. },
  446. })
  447. const toggle = ref(true)
  448. const Parent = defineComponent({
  449. setup() {
  450. const count = ref(0)
  451. return () =>
  452. toggle.value
  453. ? h(Comp, {
  454. count: count.value,
  455. 'onUpdate:count': val => {
  456. count.value = val
  457. },
  458. })
  459. : h(Comp)
  460. },
  461. })
  462. const root = nodeOps.createElement('div')
  463. render(h(Parent), root)
  464. expect(serializeInner(root)).toBe('0')
  465. // child update
  466. childCount!.value = 1
  467. // not yet updated
  468. expect(childCount!.value).toBe(0)
  469. await nextTick()
  470. expect(childCount!.value).toBe(1)
  471. expect(serializeInner(root)).toBe('1')
  472. // parent change
  473. toggle.value = false
  474. await nextTick()
  475. // localValue should be reset
  476. expect(childCount!.value).toBeUndefined()
  477. expect(serializeInner(root)).toBe('<!---->')
  478. // child local mutation should continue to work
  479. childCount!.value = 2
  480. expect(childCount!.value).toBe(2)
  481. await nextTick()
  482. expect(serializeInner(root)).toBe('2')
  483. })
  484. // #9838
  485. test('pass modelValue to slot (optimized mode) ', async () => {
  486. let foo: any
  487. const update = () => {
  488. foo.value = 'bar'
  489. }
  490. const Comp = {
  491. render(this: any) {
  492. return this.$slots.default()
  493. },
  494. }
  495. const childRender = vi.fn()
  496. const slotRender = vi.fn()
  497. const Child = defineComponent({
  498. props: ['modelValue'],
  499. emits: ['update:modelValue'],
  500. setup(props) {
  501. foo = useModel(props, 'modelValue')
  502. return () => {
  503. childRender()
  504. return (
  505. openBlock(),
  506. createElementBlock(Fragment, null, [
  507. createVNode(Comp, null, {
  508. default: () => {
  509. slotRender()
  510. return createElementVNode('div', null, foo.value)
  511. },
  512. _: 1 /* STABLE */,
  513. }),
  514. ])
  515. )
  516. }
  517. },
  518. })
  519. const msg = ref('')
  520. const setValue = vi.fn(v => (msg.value = v))
  521. const root = nodeOps.createElement('div')
  522. createApp({
  523. render() {
  524. return (
  525. openBlock(),
  526. createBlock(
  527. Child,
  528. {
  529. modelValue: msg.value,
  530. 'onUpdate:modelValue': setValue,
  531. },
  532. null,
  533. 8 /* PROPS */,
  534. ['modelValue'],
  535. )
  536. )
  537. },
  538. }).mount(root)
  539. expect(foo.value).toBe('')
  540. expect(msg.value).toBe('')
  541. expect(setValue).not.toBeCalled()
  542. expect(childRender).toBeCalledTimes(1)
  543. expect(slotRender).toBeCalledTimes(1)
  544. expect(serializeInner(root)).toBe('<div></div>')
  545. // update from child
  546. update()
  547. await nextTick()
  548. expect(msg.value).toBe('bar')
  549. expect(foo.value).toBe('bar')
  550. expect(setValue).toBeCalledTimes(1)
  551. expect(childRender).toBeCalledTimes(2)
  552. expect(slotRender).toBeCalledTimes(2)
  553. expect(serializeInner(root)).toBe('<div>bar</div>')
  554. })
  555. test('with modifiers & transformers', async () => {
  556. let childMsg: Ref<string>
  557. let childModifiers: Record<string, true | undefined>
  558. const compRender = vi.fn()
  559. const Comp = defineComponent({
  560. props: ['msg', 'msgModifiers'],
  561. emits: ['update:msg'],
  562. setup(props) {
  563. ;[childMsg, childModifiers] = useModel(props, 'msg', {
  564. get(val) {
  565. return val.toLowerCase()
  566. },
  567. set(val) {
  568. if (childModifiers.upper) {
  569. return val.toUpperCase()
  570. }
  571. },
  572. })
  573. return () => {
  574. compRender()
  575. return childMsg.value
  576. }
  577. },
  578. })
  579. const msg = ref('HI')
  580. const Parent = defineComponent({
  581. setup() {
  582. return () =>
  583. h(Comp, {
  584. msg: msg.value,
  585. msgModifiers: { upper: true },
  586. 'onUpdate:msg': val => {
  587. msg.value = val
  588. },
  589. })
  590. },
  591. })
  592. const root = nodeOps.createElement('div')
  593. render(h(Parent), root)
  594. // should be lowered
  595. expect(serializeInner(root)).toBe('hi')
  596. // child update
  597. childMsg!.value = 'Hmm'
  598. await nextTick()
  599. expect(childMsg!.value).toBe('hmm')
  600. expect(serializeInner(root)).toBe('hmm')
  601. // parent should get uppercase value
  602. expect(msg.value).toBe('HMM')
  603. // parent update
  604. msg.value = 'Ughh'
  605. await nextTick()
  606. expect(serializeInner(root)).toBe('ughh')
  607. expect(msg.value).toBe('Ughh')
  608. // child update again
  609. childMsg!.value = 'ughh'
  610. await nextTick()
  611. expect(msg.value).toBe('UGHH')
  612. })
  613. })
  614. test('createPropsRestProxy', () => {
  615. const original = shallowReactive({
  616. foo: 1,
  617. bar: 2,
  618. baz: 3,
  619. })
  620. const rest = createPropsRestProxy(original, ['foo', 'bar'])
  621. expect('foo' in rest).toBe(false)
  622. expect('bar' in rest).toBe(false)
  623. expect(rest.baz).toBe(3)
  624. expect(Object.keys(rest)).toEqual(['baz'])
  625. original.baz = 4
  626. expect(rest.baz).toBe(4)
  627. })
  628. describe('withAsyncContext', () => {
  629. // disable options API because applyOptions() also resets currentInstance
  630. // and we want to ensure the logic works even with Options API disabled.
  631. beforeEach(() => {
  632. __FEATURE_OPTIONS_API__ = false
  633. })
  634. afterEach(() => {
  635. __FEATURE_OPTIONS_API__ = true
  636. })
  637. test('basic', async () => {
  638. const spy = vi.fn()
  639. let beforeInstance: ComponentInternalInstance | null = null
  640. let afterInstance: ComponentInternalInstance | null = null
  641. let resolve: (msg: string) => void
  642. const Comp = defineComponent({
  643. async setup() {
  644. let __temp: any, __restore: any
  645. beforeInstance = getCurrentInstance()
  646. const msg =
  647. (([__temp, __restore] = withAsyncContext(
  648. () =>
  649. new Promise(r => {
  650. resolve = r
  651. }),
  652. )),
  653. (__temp = await __temp),
  654. __restore(),
  655. __temp)
  656. // register the lifecycle after an await statement
  657. onMounted(spy)
  658. afterInstance = getCurrentInstance()
  659. return () => msg
  660. },
  661. })
  662. const root = nodeOps.createElement('div')
  663. render(
  664. h(() => h(Suspense, () => h(Comp))),
  665. root,
  666. )
  667. expect(spy).not.toHaveBeenCalled()
  668. resolve!('hello')
  669. // wait a macro task tick for all micro ticks to resolve
  670. await new Promise(r => setTimeout(r))
  671. // mount hook should have been called
  672. expect(spy).toHaveBeenCalled()
  673. // should retain same instance before/after the await call
  674. expect(beforeInstance).toBe(afterInstance)
  675. expect(serializeInner(root)).toBe('hello')
  676. })
  677. test('error handling', async () => {
  678. const spy = vi.fn()
  679. let beforeInstance: ComponentInternalInstance | null = null
  680. let afterInstance: ComponentInternalInstance | null = null
  681. let reject: () => void
  682. const Comp = defineComponent({
  683. async setup() {
  684. let __temp: any, __restore: any
  685. beforeInstance = getCurrentInstance()
  686. try {
  687. ;[__temp, __restore] = withAsyncContext(
  688. () =>
  689. new Promise((_, rj) => {
  690. reject = rj
  691. }),
  692. )
  693. __temp = await __temp
  694. __restore()
  695. } catch (e: any) {
  696. // ignore
  697. }
  698. // register the lifecycle after an await statement
  699. onMounted(spy)
  700. afterInstance = getCurrentInstance()
  701. return () => ''
  702. },
  703. })
  704. const root = nodeOps.createElement('div')
  705. render(
  706. h(() => h(Suspense, () => h(Comp))),
  707. root,
  708. )
  709. expect(spy).not.toHaveBeenCalled()
  710. reject!()
  711. // wait a macro task tick for all micro ticks to resolve
  712. await new Promise(r => setTimeout(r))
  713. // mount hook should have been called
  714. expect(spy).toHaveBeenCalled()
  715. // should retain same instance before/after the await call
  716. expect(beforeInstance).toBe(afterInstance)
  717. })
  718. test('should not leak instance on multiple awaits', async () => {
  719. let resolve: (val?: any) => void
  720. let beforeInstance: ComponentInternalInstance | null = null
  721. let afterInstance: ComponentInternalInstance | null = null
  722. let inBandInstance: ComponentInternalInstance | null = null
  723. let outOfBandInstance: ComponentInternalInstance | null = null
  724. const ready = new Promise(r => {
  725. resolve = r
  726. })
  727. async function doAsyncWork() {
  728. // should still have instance
  729. inBandInstance = getCurrentInstance()
  730. await Promise.resolve()
  731. // should not leak instance
  732. outOfBandInstance = getCurrentInstance()
  733. }
  734. const Comp = defineComponent({
  735. async setup() {
  736. let __temp: any, __restore: any
  737. beforeInstance = getCurrentInstance()
  738. // first await
  739. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  740. __temp = await __temp
  741. __restore()
  742. // setup exit, instance set to null, then resumed
  743. ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
  744. __temp = await __temp
  745. __restore()
  746. afterInstance = getCurrentInstance()
  747. return () => {
  748. resolve()
  749. return ''
  750. }
  751. },
  752. })
  753. const root = nodeOps.createElement('div')
  754. render(
  755. h(() => h(Suspense, () => h(Comp))),
  756. root,
  757. )
  758. await ready
  759. expect(inBandInstance).toBe(beforeInstance)
  760. expect(outOfBandInstance).toBeNull()
  761. expect(afterInstance).toBe(beforeInstance)
  762. expect(getCurrentInstance()).toBeNull()
  763. })
  764. test('should not leak on multiple awaits + error', async () => {
  765. let resolve: (val?: any) => void
  766. const ready = new Promise(r => {
  767. resolve = r
  768. })
  769. const Comp = defineComponent({
  770. async setup() {
  771. let __temp: any, __restore: any
  772. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  773. __temp = await __temp
  774. __restore()
  775. ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
  776. __temp = await __temp
  777. __restore()
  778. },
  779. render() {},
  780. })
  781. const app = createApp(() => h(Suspense, () => h(Comp)))
  782. app.config.errorHandler = () => {
  783. resolve()
  784. return false
  785. }
  786. const root = nodeOps.createElement('div')
  787. app.mount(root)
  788. await ready
  789. expect(getCurrentInstance()).toBeNull()
  790. })
  791. // #4050
  792. test('race conditions', async () => {
  793. const uids = {
  794. one: { before: NaN, after: NaN },
  795. two: { before: NaN, after: NaN },
  796. }
  797. const Comp = defineComponent({
  798. props: ['name'],
  799. async setup(props: { name: 'one' | 'two' }) {
  800. let __temp: any, __restore: any
  801. uids[props.name].before = getCurrentInstance()!.uid
  802. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  803. __temp = await __temp
  804. __restore()
  805. uids[props.name].after = getCurrentInstance()!.uid
  806. return () => ''
  807. },
  808. })
  809. const app = createApp(() =>
  810. h(Suspense, () =>
  811. h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })]),
  812. ),
  813. )
  814. const root = nodeOps.createElement('div')
  815. app.mount(root)
  816. await new Promise(r => setTimeout(r))
  817. expect(uids.one.before).not.toBe(uids.two.before)
  818. expect(uids.one.before).toBe(uids.one.after)
  819. expect(uids.two.before).toBe(uids.two.after)
  820. })
  821. test('should teardown in-scope effects', async () => {
  822. let resolve: (val?: any) => void
  823. const ready = new Promise(r => {
  824. resolve = r
  825. })
  826. let c: ComputedRef
  827. const Comp = defineComponent({
  828. async setup() {
  829. let __temp: any, __restore: any
  830. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  831. __temp = await __temp
  832. __restore()
  833. c = computed(() => {})
  834. // register the lifecycle after an await statement
  835. onMounted(resolve)
  836. return () => ''
  837. },
  838. })
  839. const app = createApp(() => h(Suspense, () => h(Comp)))
  840. const root = nodeOps.createElement('div')
  841. app.mount(root)
  842. await ready
  843. expect(c!.effect.active).toBe(true)
  844. app.unmount()
  845. expect(c!.effect.active).toBe(false)
  846. })
  847. })
  848. })