apiSetupHelpers.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import { vi } from 'vitest'
  2. import {
  3. ComponentInternalInstance,
  4. createApp,
  5. defineComponent,
  6. getCurrentInstance,
  7. h,
  8. nodeOps,
  9. onMounted,
  10. render,
  11. serializeInner,
  12. SetupContext,
  13. Suspense,
  14. computed,
  15. ComputedRef,
  16. shallowReactive
  17. } from '@vue/runtime-test'
  18. import {
  19. defineEmits,
  20. defineProps,
  21. defineExpose,
  22. withDefaults,
  23. useAttrs,
  24. useSlots,
  25. mergeDefaults,
  26. withAsyncContext,
  27. createPropsRestProxy
  28. } from '../src/apiSetupHelpers'
  29. describe('SFC <script setup> helpers', () => {
  30. test('should warn runtime usage', () => {
  31. defineProps()
  32. expect(`defineProps() is a compiler-hint`).toHaveBeenWarned()
  33. defineEmits()
  34. expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
  35. defineExpose()
  36. expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
  37. withDefaults({}, {})
  38. expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
  39. })
  40. test('useSlots / useAttrs (no args)', () => {
  41. let slots: SetupContext['slots'] | undefined
  42. let attrs: SetupContext['attrs'] | undefined
  43. const Comp = {
  44. setup() {
  45. slots = useSlots()
  46. attrs = useAttrs()
  47. return () => {}
  48. }
  49. }
  50. const passedAttrs = { id: 'foo' }
  51. const passedSlots = {
  52. default: () => {},
  53. x: () => {}
  54. }
  55. render(h(Comp, passedAttrs, passedSlots), nodeOps.createElement('div'))
  56. expect(typeof slots!.default).toBe('function')
  57. expect(typeof slots!.x).toBe('function')
  58. expect(attrs).toMatchObject(passedAttrs)
  59. })
  60. test('useSlots / useAttrs (with args)', () => {
  61. let slots: SetupContext['slots'] | undefined
  62. let attrs: SetupContext['attrs'] | undefined
  63. let ctx: SetupContext | undefined
  64. const Comp = defineComponent({
  65. setup(_, _ctx) {
  66. slots = useSlots()
  67. attrs = useAttrs()
  68. ctx = _ctx
  69. return () => {}
  70. }
  71. })
  72. render(h(Comp), nodeOps.createElement('div'))
  73. expect(slots).toBe(ctx!.slots)
  74. expect(attrs).toBe(ctx!.attrs)
  75. })
  76. describe('mergeDefaults', () => {
  77. test('object syntax', () => {
  78. const merged = mergeDefaults(
  79. {
  80. foo: null,
  81. bar: { type: String, required: false },
  82. baz: String
  83. },
  84. {
  85. foo: 1,
  86. bar: 'baz',
  87. baz: 'qux'
  88. }
  89. )
  90. expect(merged).toMatchObject({
  91. foo: { default: 1 },
  92. bar: { type: String, required: false, default: 'baz' },
  93. baz: { type: String, default: 'qux' }
  94. })
  95. })
  96. test('array syntax', () => {
  97. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  98. foo: 1,
  99. bar: 'baz',
  100. baz: 'qux'
  101. })
  102. expect(merged).toMatchObject({
  103. foo: { default: 1 },
  104. bar: { default: 'baz' },
  105. baz: { default: 'qux' }
  106. })
  107. })
  108. test('should warn missing', () => {
  109. mergeDefaults({}, { foo: 1 })
  110. expect(
  111. `props default key "foo" has no corresponding declaration`
  112. ).toHaveBeenWarned()
  113. })
  114. })
  115. test('createPropsRestProxy', () => {
  116. const original = shallowReactive({
  117. foo: 1,
  118. bar: 2,
  119. baz: 3
  120. })
  121. const rest = createPropsRestProxy(original, ['foo', 'bar'])
  122. expect('foo' in rest).toBe(false)
  123. expect('bar' in rest).toBe(false)
  124. expect(rest.baz).toBe(3)
  125. expect(Object.keys(rest)).toEqual(['baz'])
  126. original.baz = 4
  127. expect(rest.baz).toBe(4)
  128. })
  129. describe('withAsyncContext', () => {
  130. // disable options API because applyOptions() also resets currentInstance
  131. // and we want to ensure the logic works even with Options API disabled.
  132. beforeEach(() => {
  133. __FEATURE_OPTIONS_API__ = false
  134. })
  135. afterEach(() => {
  136. __FEATURE_OPTIONS_API__ = true
  137. })
  138. test('basic', async () => {
  139. const spy = vi.fn()
  140. let beforeInstance: ComponentInternalInstance | null = null
  141. let afterInstance: ComponentInternalInstance | null = null
  142. let resolve: (msg: string) => void
  143. const Comp = defineComponent({
  144. async setup() {
  145. let __temp: any, __restore: any
  146. beforeInstance = getCurrentInstance()
  147. const msg =
  148. (([__temp, __restore] = withAsyncContext(
  149. () =>
  150. new Promise(r => {
  151. resolve = r
  152. })
  153. )),
  154. (__temp = await __temp),
  155. __restore(),
  156. __temp)
  157. // register the lifecycle after an await statement
  158. onMounted(spy)
  159. afterInstance = getCurrentInstance()
  160. return () => msg
  161. }
  162. })
  163. const root = nodeOps.createElement('div')
  164. render(
  165. h(() => h(Suspense, () => h(Comp))),
  166. root
  167. )
  168. expect(spy).not.toHaveBeenCalled()
  169. resolve!('hello')
  170. // wait a macro task tick for all micro ticks to resolve
  171. await new Promise(r => setTimeout(r))
  172. // mount hook should have been called
  173. expect(spy).toHaveBeenCalled()
  174. // should retain same instance before/after the await call
  175. expect(beforeInstance).toBe(afterInstance)
  176. expect(serializeInner(root)).toBe('hello')
  177. })
  178. test('error handling', async () => {
  179. const spy = vi.fn()
  180. let beforeInstance: ComponentInternalInstance | null = null
  181. let afterInstance: ComponentInternalInstance | null = null
  182. let reject: () => void
  183. const Comp = defineComponent({
  184. async setup() {
  185. let __temp: any, __restore: any
  186. beforeInstance = getCurrentInstance()
  187. try {
  188. ;[__temp, __restore] = withAsyncContext(
  189. () =>
  190. new Promise((_, rj) => {
  191. reject = rj
  192. })
  193. )
  194. __temp = await __temp
  195. __restore()
  196. } catch (e: any) {
  197. // ignore
  198. }
  199. // register the lifecycle after an await statement
  200. onMounted(spy)
  201. afterInstance = getCurrentInstance()
  202. return () => ''
  203. }
  204. })
  205. const root = nodeOps.createElement('div')
  206. render(
  207. h(() => h(Suspense, () => h(Comp))),
  208. root
  209. )
  210. expect(spy).not.toHaveBeenCalled()
  211. reject!()
  212. // wait a macro task tick for all micro ticks to resolve
  213. await new Promise(r => setTimeout(r))
  214. // mount hook should have been called
  215. expect(spy).toHaveBeenCalled()
  216. // should retain same instance before/after the await call
  217. expect(beforeInstance).toBe(afterInstance)
  218. })
  219. test('should not leak instance on multiple awaits', async () => {
  220. let resolve: (val?: any) => void
  221. let beforeInstance: ComponentInternalInstance | null = null
  222. let afterInstance: ComponentInternalInstance | null = null
  223. let inBandInstance: ComponentInternalInstance | null = null
  224. let outOfBandInstance: ComponentInternalInstance | null = null
  225. const ready = new Promise(r => {
  226. resolve = r
  227. })
  228. async function doAsyncWork() {
  229. // should still have instance
  230. inBandInstance = getCurrentInstance()
  231. await Promise.resolve()
  232. // should not leak instance
  233. outOfBandInstance = getCurrentInstance()
  234. }
  235. const Comp = defineComponent({
  236. async setup() {
  237. let __temp: any, __restore: any
  238. beforeInstance = getCurrentInstance()
  239. // first await
  240. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  241. __temp = await __temp
  242. __restore()
  243. // setup exit, instance set to null, then resumed
  244. ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
  245. __temp = await __temp
  246. __restore()
  247. afterInstance = getCurrentInstance()
  248. return () => {
  249. resolve()
  250. return ''
  251. }
  252. }
  253. })
  254. const root = nodeOps.createElement('div')
  255. render(
  256. h(() => h(Suspense, () => h(Comp))),
  257. root
  258. )
  259. await ready
  260. expect(inBandInstance).toBe(beforeInstance)
  261. expect(outOfBandInstance).toBeNull()
  262. expect(afterInstance).toBe(beforeInstance)
  263. expect(getCurrentInstance()).toBeNull()
  264. })
  265. test('should not leak on multiple awaits + error', async () => {
  266. let resolve: (val?: any) => void
  267. const ready = new Promise(r => {
  268. resolve = r
  269. })
  270. const Comp = defineComponent({
  271. async setup() {
  272. let __temp: any, __restore: any
  273. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  274. __temp = await __temp
  275. __restore()
  276. ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
  277. __temp = await __temp
  278. __restore()
  279. },
  280. render() {}
  281. })
  282. const app = createApp(() => h(Suspense, () => h(Comp)))
  283. app.config.errorHandler = () => {
  284. resolve()
  285. return false
  286. }
  287. const root = nodeOps.createElement('div')
  288. app.mount(root)
  289. await ready
  290. expect(getCurrentInstance()).toBeNull()
  291. })
  292. // #4050
  293. test('race conditions', async () => {
  294. const uids = {
  295. one: { before: NaN, after: NaN },
  296. two: { before: NaN, after: NaN }
  297. }
  298. const Comp = defineComponent({
  299. props: ['name'],
  300. async setup(props: { name: 'one' | 'two' }) {
  301. let __temp: any, __restore: any
  302. uids[props.name].before = getCurrentInstance()!.uid
  303. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  304. __temp = await __temp
  305. __restore()
  306. uids[props.name].after = getCurrentInstance()!.uid
  307. return () => ''
  308. }
  309. })
  310. const app = createApp(() =>
  311. h(Suspense, () =>
  312. h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })])
  313. )
  314. )
  315. const root = nodeOps.createElement('div')
  316. app.mount(root)
  317. await new Promise(r => setTimeout(r))
  318. expect(uids.one.before).not.toBe(uids.two.before)
  319. expect(uids.one.before).toBe(uids.one.after)
  320. expect(uids.two.before).toBe(uids.two.after)
  321. })
  322. test('should teardown in-scope effects', async () => {
  323. let resolve: (val?: any) => void
  324. const ready = new Promise(r => {
  325. resolve = r
  326. })
  327. let c: ComputedRef
  328. const Comp = defineComponent({
  329. async setup() {
  330. let __temp: any, __restore: any
  331. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  332. __temp = await __temp
  333. __restore()
  334. c = computed(() => {})
  335. // register the lifecycle after an await statement
  336. onMounted(resolve)
  337. return () => ''
  338. }
  339. })
  340. const app = createApp(() => h(Suspense, () => h(Comp)))
  341. const root = nodeOps.createElement('div')
  342. app.mount(root)
  343. await ready
  344. expect(c!.effect.active).toBe(true)
  345. app.unmount()
  346. expect(c!.effect.active).toBe(false)
  347. })
  348. })
  349. })