apiSetupHelpers.spec.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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 = (([__temp, __restore] = withAsyncContext(
  112. () =>
  113. new Promise(r => {
  114. resolve = r
  115. })
  116. )),
  117. (__temp = await __temp),
  118. __restore(),
  119. __temp)
  120. // register the lifecycle after an await statement
  121. onMounted(spy)
  122. afterInstance = getCurrentInstance()
  123. return () => msg
  124. }
  125. })
  126. const root = nodeOps.createElement('div')
  127. render(h(() => h(Suspense, () => h(Comp))), root)
  128. expect(spy).not.toHaveBeenCalled()
  129. resolve!('hello')
  130. // wait a macro task tick for all micro ticks to resolve
  131. await new Promise(r => setTimeout(r))
  132. // mount hook should have been called
  133. expect(spy).toHaveBeenCalled()
  134. // should retain same instance before/after the await call
  135. expect(beforeInstance).toBe(afterInstance)
  136. expect(serializeInner(root)).toBe('hello')
  137. })
  138. test('error handling', async () => {
  139. const spy = jest.fn()
  140. let beforeInstance: ComponentInternalInstance | null = null
  141. let afterInstance: ComponentInternalInstance | null = null
  142. let reject: () => void
  143. const Comp = defineComponent({
  144. async setup() {
  145. let __temp: any, __restore: any
  146. beforeInstance = getCurrentInstance()
  147. try {
  148. ;[__temp, __restore] = withAsyncContext(
  149. () =>
  150. new Promise((_, rj) => {
  151. reject = rj
  152. })
  153. )
  154. __temp = await __temp
  155. __restore()
  156. } catch (e) {
  157. // ignore
  158. }
  159. // register the lifecycle after an await statement
  160. onMounted(spy)
  161. afterInstance = getCurrentInstance()
  162. return () => ''
  163. }
  164. })
  165. const root = nodeOps.createElement('div')
  166. render(h(() => h(Suspense, () => h(Comp))), root)
  167. expect(spy).not.toHaveBeenCalled()
  168. reject!()
  169. // wait a macro task tick for all micro ticks to resolve
  170. await new Promise(r => setTimeout(r))
  171. // mount hook should have been called
  172. expect(spy).toHaveBeenCalled()
  173. // should retain same instance before/after the await call
  174. expect(beforeInstance).toBe(afterInstance)
  175. })
  176. test('should not leak instance on multiple awaits', async () => {
  177. let resolve: (val?: any) => void
  178. let beforeInstance: ComponentInternalInstance | null = null
  179. let afterInstance: ComponentInternalInstance | null = null
  180. let inBandInstance: ComponentInternalInstance | null = null
  181. let outOfBandInstance: ComponentInternalInstance | null = null
  182. const ready = new Promise(r => {
  183. resolve = r
  184. })
  185. async function doAsyncWork() {
  186. // should still have instance
  187. inBandInstance = getCurrentInstance()
  188. await Promise.resolve()
  189. // should not leak instance
  190. outOfBandInstance = getCurrentInstance()
  191. }
  192. const Comp = defineComponent({
  193. async setup() {
  194. let __temp: any, __restore: any
  195. beforeInstance = getCurrentInstance()
  196. // first await
  197. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  198. __temp = await __temp
  199. __restore()
  200. // setup exit, instance set to null, then resumed
  201. ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
  202. __temp = await __temp
  203. __restore()
  204. afterInstance = getCurrentInstance()
  205. return () => {
  206. resolve()
  207. return ''
  208. }
  209. }
  210. })
  211. const root = nodeOps.createElement('div')
  212. render(h(() => h(Suspense, () => h(Comp))), root)
  213. await ready
  214. expect(inBandInstance).toBe(beforeInstance)
  215. expect(outOfBandInstance).toBeNull()
  216. expect(afterInstance).toBe(beforeInstance)
  217. expect(getCurrentInstance()).toBeNull()
  218. })
  219. test('should not leak on multiple awaits + error', async () => {
  220. let resolve: (val?: any) => void
  221. const ready = new Promise(r => {
  222. resolve = r
  223. })
  224. const Comp = defineComponent({
  225. async setup() {
  226. let __temp: any, __restore: any
  227. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  228. __temp = await __temp
  229. __restore()
  230. ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
  231. __temp = await __temp
  232. __restore()
  233. },
  234. render() {}
  235. })
  236. const app = createApp(() => h(Suspense, () => h(Comp)))
  237. app.config.errorHandler = () => {
  238. resolve()
  239. return false
  240. }
  241. const root = nodeOps.createElement('div')
  242. app.mount(root)
  243. await ready
  244. expect(getCurrentInstance()).toBeNull()
  245. })
  246. // #4050
  247. test('race conditions', async () => {
  248. const uids = {
  249. one: { before: NaN, after: NaN },
  250. two: { before: NaN, after: NaN }
  251. }
  252. const Comp = defineComponent({
  253. props: ['name'],
  254. async setup(props: { name: 'one' | 'two' }) {
  255. let __temp: any, __restore: any
  256. uids[props.name].before = getCurrentInstance()!.uid
  257. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  258. __temp = await __temp
  259. __restore()
  260. uids[props.name].after = getCurrentInstance()!.uid
  261. return () => ''
  262. }
  263. })
  264. const app = createApp(() =>
  265. h(Suspense, () =>
  266. h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })])
  267. )
  268. )
  269. const root = nodeOps.createElement('div')
  270. app.mount(root)
  271. await new Promise(r => setTimeout(r))
  272. expect(uids.one.before).not.toBe(uids.two.before)
  273. expect(uids.one.before).toBe(uids.one.after)
  274. expect(uids.two.before).toBe(uids.two.after)
  275. })
  276. test('should teardown in-scope effects', async () => {
  277. let resolve: (val?: any) => void
  278. const ready = new Promise(r => {
  279. resolve = r
  280. })
  281. let c: ComputedRef
  282. const Comp = defineComponent({
  283. async setup() {
  284. let __temp: any, __restore: any
  285. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  286. __temp = await __temp
  287. __restore()
  288. c = computed(() => {})
  289. // register the lifecycle after an await statement
  290. onMounted(resolve)
  291. return () => ''
  292. }
  293. })
  294. const app = createApp(() => h(Suspense, () => h(Comp)))
  295. const root = nodeOps.createElement('div')
  296. app.mount(root)
  297. await ready
  298. expect(c!.effect.active).toBe(true)
  299. app.unmount()
  300. expect(c!.effect.active).toBe(false)
  301. })
  302. })
  303. })