apiSetupHelpers.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import {
  2. type ComponentInternalInstance,
  3. type ComputedRef,
  4. type SetupContext,
  5. Suspense,
  6. computed,
  7. createApp,
  8. defineComponent,
  9. getCurrentInstance,
  10. h,
  11. nodeOps,
  12. onMounted,
  13. render,
  14. serializeInner,
  15. shallowReactive,
  16. } from '@vue/runtime-test'
  17. import {
  18. createPropsRestProxy,
  19. defineEmits,
  20. defineExpose,
  21. defineProps,
  22. mergeDefaults,
  23. mergeModels,
  24. useAttrs,
  25. useSlots,
  26. withAsyncContext,
  27. withDefaults,
  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('merging with skipFactory', () => {
  109. const fn = () => {}
  110. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  111. foo: fn,
  112. __skip_foo: true,
  113. })
  114. expect(merged).toMatchObject({
  115. foo: { default: fn, skipFactory: true },
  116. })
  117. })
  118. test('should warn missing', () => {
  119. mergeDefaults({}, { foo: 1 })
  120. expect(
  121. `props default key "foo" has no corresponding declaration`,
  122. ).toHaveBeenWarned()
  123. })
  124. })
  125. describe('mergeModels', () => {
  126. test('array syntax', () => {
  127. expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
  128. 'foo',
  129. 'bar',
  130. 'baz',
  131. ])
  132. })
  133. test('object syntax', () => {
  134. expect(
  135. mergeModels({ foo: null, bar: { required: true } }, ['baz']),
  136. ).toMatchObject({
  137. foo: null,
  138. bar: { required: true },
  139. baz: {},
  140. })
  141. expect(
  142. mergeModels(['baz'], { foo: null, bar: { required: true } }),
  143. ).toMatchObject({
  144. foo: null,
  145. bar: { required: true },
  146. baz: {},
  147. })
  148. })
  149. test('overwrite', () => {
  150. expect(
  151. mergeModels(
  152. { foo: null, bar: { required: true } },
  153. { bar: {}, baz: {} },
  154. ),
  155. ).toMatchObject({
  156. foo: null,
  157. bar: {},
  158. baz: {},
  159. })
  160. })
  161. })
  162. test('createPropsRestProxy', () => {
  163. const original = shallowReactive({
  164. foo: 1,
  165. bar: 2,
  166. baz: 3,
  167. })
  168. const rest = createPropsRestProxy(original, ['foo', 'bar'])
  169. expect('foo' in rest).toBe(false)
  170. expect('bar' in rest).toBe(false)
  171. expect(rest.baz).toBe(3)
  172. expect(Object.keys(rest)).toEqual(['baz'])
  173. original.baz = 4
  174. expect(rest.baz).toBe(4)
  175. })
  176. describe('withAsyncContext', () => {
  177. // disable options API because applyOptions() also resets currentInstance
  178. // and we want to ensure the logic works even with Options API disabled.
  179. beforeEach(() => {
  180. __FEATURE_OPTIONS_API__ = false
  181. })
  182. afterEach(() => {
  183. __FEATURE_OPTIONS_API__ = true
  184. })
  185. test('basic', async () => {
  186. const spy = vi.fn()
  187. let beforeInstance: ComponentInternalInstance | null = null
  188. let afterInstance: ComponentInternalInstance | null = null
  189. let resolve: (msg: string) => void
  190. const Comp = defineComponent({
  191. async setup() {
  192. let __temp: any, __restore: any
  193. beforeInstance = getCurrentInstance()
  194. const msg =
  195. (([__temp, __restore] = withAsyncContext(
  196. () =>
  197. new Promise(r => {
  198. resolve = r
  199. }),
  200. )),
  201. (__temp = await __temp),
  202. __restore(),
  203. __temp)
  204. // register the lifecycle after an await statement
  205. onMounted(spy)
  206. afterInstance = getCurrentInstance()
  207. return () => msg
  208. },
  209. })
  210. const root = nodeOps.createElement('div')
  211. render(
  212. h(() => h(Suspense, () => h(Comp))),
  213. root,
  214. )
  215. expect(spy).not.toHaveBeenCalled()
  216. resolve!('hello')
  217. // wait a macro task tick for all micro ticks to resolve
  218. await new Promise(r => setTimeout(r))
  219. // mount hook should have been called
  220. expect(spy).toHaveBeenCalled()
  221. // should retain same instance before/after the await call
  222. expect(beforeInstance).toBe(afterInstance)
  223. expect(serializeInner(root)).toBe('hello')
  224. })
  225. test('error handling', async () => {
  226. const spy = vi.fn()
  227. let beforeInstance: ComponentInternalInstance | null = null
  228. let afterInstance: ComponentInternalInstance | null = null
  229. let reject: () => void
  230. const Comp = defineComponent({
  231. async setup() {
  232. let __temp: any, __restore: any
  233. beforeInstance = getCurrentInstance()
  234. try {
  235. ;[__temp, __restore] = withAsyncContext(
  236. () =>
  237. new Promise((_, rj) => {
  238. reject = rj
  239. }),
  240. )
  241. __temp = await __temp
  242. __restore()
  243. } catch (e: any) {
  244. // ignore
  245. }
  246. // register the lifecycle after an await statement
  247. onMounted(spy)
  248. afterInstance = getCurrentInstance()
  249. return () => ''
  250. },
  251. })
  252. const root = nodeOps.createElement('div')
  253. render(
  254. h(() => h(Suspense, () => h(Comp))),
  255. root,
  256. )
  257. expect(spy).not.toHaveBeenCalled()
  258. reject!()
  259. // wait a macro task tick for all micro ticks to resolve
  260. await new Promise(r => setTimeout(r))
  261. // mount hook should have been called
  262. expect(spy).toHaveBeenCalled()
  263. // should retain same instance before/after the await call
  264. expect(beforeInstance).toBe(afterInstance)
  265. })
  266. test('should not leak instance on multiple awaits', async () => {
  267. let resolve: (val?: any) => void
  268. let beforeInstance: ComponentInternalInstance | null = null
  269. let afterInstance: ComponentInternalInstance | null = null
  270. let inBandInstance: ComponentInternalInstance | null = null
  271. let outOfBandInstance: ComponentInternalInstance | null = null
  272. const ready = new Promise(r => {
  273. resolve = r
  274. })
  275. async function doAsyncWork() {
  276. // should still have instance
  277. inBandInstance = getCurrentInstance()
  278. await Promise.resolve()
  279. // should not leak instance
  280. outOfBandInstance = getCurrentInstance()
  281. }
  282. const Comp = defineComponent({
  283. async setup() {
  284. let __temp: any, __restore: any
  285. beforeInstance = getCurrentInstance()
  286. // first await
  287. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  288. __temp = await __temp
  289. __restore()
  290. // setup exit, instance set to null, then resumed
  291. ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
  292. __temp = await __temp
  293. __restore()
  294. afterInstance = getCurrentInstance()
  295. return () => {
  296. resolve()
  297. return ''
  298. }
  299. },
  300. })
  301. const root = nodeOps.createElement('div')
  302. render(
  303. h(() => h(Suspense, () => h(Comp))),
  304. root,
  305. )
  306. await ready
  307. expect(inBandInstance).toBe(beforeInstance)
  308. expect(outOfBandInstance).toBeNull()
  309. expect(afterInstance).toBe(beforeInstance)
  310. expect(getCurrentInstance()).toBeNull()
  311. })
  312. test('should not leak on multiple awaits + error', async () => {
  313. let resolve: (val?: any) => void
  314. const ready = new Promise(r => {
  315. resolve = r
  316. })
  317. const Comp = defineComponent({
  318. async setup() {
  319. let __temp: any, __restore: any
  320. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  321. __temp = await __temp
  322. __restore()
  323. ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
  324. __temp = await __temp
  325. __restore()
  326. },
  327. render() {},
  328. })
  329. const app = createApp(() => h(Suspense, () => h(Comp)))
  330. app.config.errorHandler = () => {
  331. resolve()
  332. return false
  333. }
  334. const root = nodeOps.createElement('div')
  335. app.mount(root)
  336. await ready
  337. expect(getCurrentInstance()).toBeNull()
  338. })
  339. // #4050
  340. test('race conditions', async () => {
  341. const uids = {
  342. one: { before: NaN, after: NaN },
  343. two: { before: NaN, after: NaN },
  344. }
  345. const Comp = defineComponent({
  346. props: ['name'],
  347. async setup(props: { name: 'one' | 'two' }) {
  348. let __temp: any, __restore: any
  349. uids[props.name].before = getCurrentInstance()!.uid
  350. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  351. __temp = await __temp
  352. __restore()
  353. uids[props.name].after = getCurrentInstance()!.uid
  354. return () => ''
  355. },
  356. })
  357. const app = createApp(() =>
  358. h(Suspense, () =>
  359. h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })]),
  360. ),
  361. )
  362. const root = nodeOps.createElement('div')
  363. app.mount(root)
  364. await new Promise(r => setTimeout(r))
  365. expect(uids.one.before).not.toBe(uids.two.before)
  366. expect(uids.one.before).toBe(uids.one.after)
  367. expect(uids.two.before).toBe(uids.two.after)
  368. })
  369. test('should teardown in-scope effects', async () => {
  370. let resolve: (val?: any) => void
  371. const ready = new Promise(r => {
  372. resolve = r
  373. })
  374. let c: ComputedRef
  375. const Comp = defineComponent({
  376. async setup() {
  377. let __temp: any, __restore: any
  378. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  379. __temp = await __temp
  380. __restore()
  381. c = computed(() => {})
  382. // register the lifecycle after an await statement
  383. onMounted(resolve)
  384. return () => ''
  385. },
  386. })
  387. const app = createApp(() => h(Suspense, () => h(Comp)))
  388. const root = nodeOps.createElement('div')
  389. app.mount(root)
  390. await ready
  391. expect(c!.effect.active).toBe(true)
  392. app.unmount()
  393. expect(c!.effect.active).toBe(false)
  394. })
  395. })
  396. })