apiSetupHelpers.spec.ts 11 KB

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