apiSetupHelpers.spec.ts 9.8 KB

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