apiSetupHelpers.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import {
  2. type ComponentInternalInstance,
  3. type SetupContext,
  4. Suspense,
  5. computed,
  6. createApp,
  7. defineComponent,
  8. getCurrentInstance,
  9. h,
  10. nodeOps,
  11. onMounted,
  12. render,
  13. serializeInner,
  14. shallowReactive,
  15. } from '@vue/runtime-test'
  16. import {
  17. createPropsRestProxy,
  18. defineEmits,
  19. defineExpose,
  20. defineProps,
  21. mergeDefaults,
  22. mergeModels,
  23. useAttrs,
  24. useSlots,
  25. withAsyncContext,
  26. withDefaults,
  27. } from '../src/apiSetupHelpers'
  28. import type { ComputedRefImpl } from '../../reactivity/src/computed'
  29. import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
  30. describe('SFC <script setup> helpers', () => {
  31. test('should warn runtime usage', () => {
  32. defineProps()
  33. expect(`defineProps() is a compiler-hint`).toHaveBeenWarned()
  34. defineEmits()
  35. expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
  36. defineExpose()
  37. expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
  38. withDefaults({}, {})
  39. expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
  40. })
  41. test('useSlots / useAttrs (no args)', () => {
  42. let slots: SetupContext['slots'] | undefined
  43. let attrs: SetupContext['attrs'] | undefined
  44. const Comp = {
  45. setup() {
  46. slots = useSlots()
  47. attrs = useAttrs()
  48. return () => {}
  49. },
  50. }
  51. const passedAttrs = { id: 'foo' }
  52. const passedSlots = {
  53. default: () => {},
  54. x: () => {},
  55. }
  56. render(h(Comp, passedAttrs, passedSlots), nodeOps.createElement('div'))
  57. expect(typeof slots!.default).toBe('function')
  58. expect(typeof slots!.x).toBe('function')
  59. expect(attrs).toMatchObject(passedAttrs)
  60. })
  61. test('useSlots / useAttrs (with args)', () => {
  62. let slots: SetupContext['slots'] | undefined
  63. let attrs: SetupContext['attrs'] | undefined
  64. let ctx: SetupContext | undefined
  65. const Comp = defineComponent({
  66. setup(_, _ctx) {
  67. slots = useSlots()
  68. attrs = useAttrs()
  69. ctx = _ctx
  70. return () => {}
  71. },
  72. })
  73. render(h(Comp), nodeOps.createElement('div'))
  74. expect(slots).toBe(ctx!.slots)
  75. expect(attrs).toBe(ctx!.attrs)
  76. })
  77. describe('mergeDefaults', () => {
  78. test('object syntax', () => {
  79. const merged = mergeDefaults(
  80. {
  81. foo: null,
  82. bar: { type: String, required: false },
  83. baz: String,
  84. },
  85. {
  86. foo: 1,
  87. bar: 'baz',
  88. baz: 'qux',
  89. },
  90. )
  91. expect(merged).toMatchObject({
  92. foo: { default: 1 },
  93. bar: { type: String, required: false, default: 'baz' },
  94. baz: { type: String, default: 'qux' },
  95. })
  96. })
  97. test('array syntax', () => {
  98. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  99. foo: 1,
  100. bar: 'baz',
  101. baz: 'qux',
  102. })
  103. expect(merged).toMatchObject({
  104. foo: { default: 1 },
  105. bar: { default: 'baz' },
  106. baz: { default: 'qux' },
  107. })
  108. })
  109. test('merging with skipFactory', () => {
  110. const fn = () => {}
  111. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  112. foo: fn,
  113. __skip_foo: true,
  114. })
  115. expect(merged).toMatchObject({
  116. foo: { default: fn, skipFactory: true },
  117. })
  118. })
  119. test('should warn missing', () => {
  120. mergeDefaults({}, { foo: 1 })
  121. expect(
  122. `props default key "foo" has no corresponding declaration`,
  123. ).toHaveBeenWarned()
  124. })
  125. })
  126. describe('mergeModels', () => {
  127. test('array syntax', () => {
  128. expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
  129. 'foo',
  130. 'bar',
  131. 'baz',
  132. ])
  133. })
  134. test('object syntax', () => {
  135. expect(
  136. mergeModels({ foo: null, bar: { required: true } }, ['baz']),
  137. ).toMatchObject({
  138. foo: null,
  139. bar: { required: true },
  140. baz: {},
  141. })
  142. expect(
  143. mergeModels(['baz'], { foo: null, bar: { required: true } }),
  144. ).toMatchObject({
  145. foo: null,
  146. bar: { required: true },
  147. baz: {},
  148. })
  149. })
  150. test('overwrite', () => {
  151. expect(
  152. mergeModels(
  153. { foo: null, bar: { required: true } },
  154. { bar: {}, baz: {} },
  155. ),
  156. ).toMatchObject({
  157. foo: null,
  158. bar: {},
  159. baz: {},
  160. })
  161. })
  162. })
  163. test('createPropsRestProxy', () => {
  164. const original = shallowReactive({
  165. foo: 1,
  166. bar: 2,
  167. baz: 3,
  168. })
  169. const rest = createPropsRestProxy(original, ['foo', 'bar'])
  170. expect('foo' in rest).toBe(false)
  171. expect('bar' in rest).toBe(false)
  172. expect(rest.baz).toBe(3)
  173. expect(Object.keys(rest)).toEqual(['baz'])
  174. original.baz = 4
  175. expect(rest.baz).toBe(4)
  176. })
  177. describe('withAsyncContext', () => {
  178. // disable options API because applyOptions() also resets currentInstance
  179. // and we want to ensure the logic works even with Options API disabled.
  180. beforeEach(() => {
  181. __FEATURE_OPTIONS_API__ = false
  182. })
  183. afterEach(() => {
  184. __FEATURE_OPTIONS_API__ = true
  185. })
  186. test('basic', async () => {
  187. const spy = vi.fn()
  188. let beforeInstance: ComponentInternalInstance | null = null
  189. let afterInstance: ComponentInternalInstance | null = null
  190. let resolve: (msg: string) => void
  191. const Comp = defineComponent({
  192. async setup() {
  193. let __temp: any, __restore: any
  194. beforeInstance = getCurrentInstance()
  195. const msg =
  196. (([__temp, __restore] = withAsyncContext(
  197. () =>
  198. new Promise(r => {
  199. resolve = r
  200. }),
  201. )),
  202. (__temp = await __temp),
  203. __restore(),
  204. __temp)
  205. // register the lifecycle after an await statement
  206. onMounted(spy)
  207. afterInstance = getCurrentInstance()
  208. return () => msg
  209. },
  210. })
  211. const root = nodeOps.createElement('div')
  212. render(
  213. h(() => h(Suspense, () => h(Comp))),
  214. root,
  215. )
  216. expect(spy).not.toHaveBeenCalled()
  217. resolve!('hello')
  218. // wait a macro task tick for all micro ticks to resolve
  219. await new Promise(r => setTimeout(r))
  220. // mount hook should have been called
  221. expect(spy).toHaveBeenCalled()
  222. // should retain same instance before/after the await call
  223. expect(beforeInstance).toBe(afterInstance)
  224. expect(serializeInner(root)).toBe('hello')
  225. })
  226. test('should not leak instance to user microtasks after restore', async () => {
  227. let leakedToUserMicrotask = false
  228. const Comp = defineComponent({
  229. async setup() {
  230. let __temp: any, __restore: any
  231. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  232. __temp = await __temp
  233. __restore()
  234. Promise.resolve().then(() => {
  235. leakedToUserMicrotask = getCurrentInstance() !== null
  236. })
  237. return () => ''
  238. },
  239. })
  240. const root = nodeOps.createElement('div')
  241. render(
  242. h(() => h(Suspense, () => h(Comp))),
  243. root,
  244. )
  245. await new Promise(r => setTimeout(r))
  246. expect(leakedToUserMicrotask).toBe(false)
  247. })
  248. test('should not leak sibling instance in concurrent restores', async () => {
  249. let resolveOne: () => void
  250. let resolveTwo: () => void
  251. let done!: () => void
  252. let pending = 2
  253. const ready = new Promise<void>(r => {
  254. done = r
  255. })
  256. const seenUid: Record<'one' | 'two', number | null> = {
  257. one: null,
  258. two: null,
  259. }
  260. const makeComp = (name: 'one' | 'two', wait: Promise<void>) =>
  261. defineComponent({
  262. async setup() {
  263. let __temp: any, __restore: any
  264. ;[__temp, __restore] = withAsyncContext(() => wait)
  265. __temp = await __temp
  266. __restore()
  267. Promise.resolve().then(() => {
  268. seenUid[name] = getCurrentInstance()?.uid ?? null
  269. if (--pending === 0) done()
  270. })
  271. return () => ''
  272. },
  273. })
  274. const oneReady = new Promise<void>(r => {
  275. resolveOne = r
  276. })
  277. const twoReady = new Promise<void>(r => {
  278. resolveTwo = r
  279. })
  280. const CompOne = makeComp('one', oneReady)
  281. const CompTwo = makeComp('two', twoReady)
  282. const root = nodeOps.createElement('div')
  283. render(
  284. h(() => h(Suspense, () => h('div', [h(CompOne), h(CompTwo)]))),
  285. root,
  286. )
  287. resolveOne!()
  288. resolveTwo!()
  289. await ready
  290. expect(seenUid.one).toBeNull()
  291. expect(seenUid.two).toBeNull()
  292. })
  293. test('error handling', async () => {
  294. const spy = vi.fn()
  295. let beforeInstance: ComponentInternalInstance | null = null
  296. let afterInstance: ComponentInternalInstance | null = null
  297. let reject: () => void
  298. const Comp = defineComponent({
  299. async setup() {
  300. let __temp: any, __restore: any
  301. beforeInstance = getCurrentInstance()
  302. try {
  303. ;[__temp, __restore] = withAsyncContext(
  304. () =>
  305. new Promise((_, rj) => {
  306. reject = rj
  307. }),
  308. )
  309. __temp = await __temp
  310. __restore()
  311. } catch (e: any) {
  312. // ignore
  313. }
  314. // register the lifecycle after an await statement
  315. onMounted(spy)
  316. afterInstance = getCurrentInstance()
  317. return () => ''
  318. },
  319. })
  320. const root = nodeOps.createElement('div')
  321. render(
  322. h(() => h(Suspense, () => h(Comp))),
  323. root,
  324. )
  325. expect(spy).not.toHaveBeenCalled()
  326. reject!()
  327. // wait a macro task tick for all micro ticks to resolve
  328. await new Promise(r => setTimeout(r))
  329. // mount hook should have been called
  330. expect(spy).toHaveBeenCalled()
  331. // should retain same instance before/after the await call
  332. expect(beforeInstance).toBe(afterInstance)
  333. // instance scope should be fully restored/cleaned after async ticks
  334. expect((beforeInstance!.scope as any)._on).toBe(0)
  335. })
  336. test('should not leak instance on multiple awaits', async () => {
  337. let resolve: (val?: any) => void
  338. let beforeInstance: ComponentInternalInstance | null = null
  339. let afterInstance: ComponentInternalInstance | null = null
  340. let inBandInstance: ComponentInternalInstance | null = null
  341. let outOfBandInstance: ComponentInternalInstance | null = null
  342. const ready = new Promise(r => {
  343. resolve = r
  344. })
  345. async function doAsyncWork() {
  346. // should still have instance
  347. inBandInstance = getCurrentInstance()
  348. await Promise.resolve()
  349. // should not leak instance
  350. outOfBandInstance = getCurrentInstance()
  351. }
  352. const Comp = defineComponent({
  353. async setup() {
  354. let __temp: any, __restore: any
  355. beforeInstance = getCurrentInstance()
  356. // first await
  357. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  358. __temp = await __temp
  359. __restore()
  360. // setup exit, instance set to null, then resumed
  361. ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
  362. __temp = await __temp
  363. __restore()
  364. afterInstance = getCurrentInstance()
  365. return () => {
  366. resolve()
  367. return ''
  368. }
  369. },
  370. })
  371. const root = nodeOps.createElement('div')
  372. render(
  373. h(() => h(Suspense, () => h(Comp))),
  374. root,
  375. )
  376. await ready
  377. expect(inBandInstance).toBe(beforeInstance)
  378. expect(outOfBandInstance).toBeNull()
  379. expect(afterInstance).toBe(beforeInstance)
  380. expect(getCurrentInstance()).toBeNull()
  381. })
  382. test('should not leak on multiple awaits + error', async () => {
  383. let resolve: (val?: any) => void
  384. const ready = new Promise(r => {
  385. resolve = r
  386. })
  387. const Comp = defineComponent({
  388. async setup() {
  389. let __temp: any, __restore: any
  390. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  391. __temp = await __temp
  392. __restore()
  393. ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
  394. __temp = await __temp
  395. __restore()
  396. },
  397. render() {},
  398. })
  399. const app = createApp(() => h(Suspense, () => h(Comp)))
  400. app.config.errorHandler = () => {
  401. resolve()
  402. return false
  403. }
  404. const root = nodeOps.createElement('div')
  405. app.mount(root)
  406. await ready
  407. expect(getCurrentInstance()).toBeNull()
  408. })
  409. // #4050
  410. test('race conditions', async () => {
  411. const uids = {
  412. one: { before: NaN, after: NaN },
  413. two: { before: NaN, after: NaN },
  414. }
  415. const Comp = defineComponent({
  416. props: ['name'],
  417. async setup(props: { name: 'one' | 'two' }) {
  418. let __temp: any, __restore: any
  419. uids[props.name].before = getCurrentInstance()!.uid
  420. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  421. __temp = await __temp
  422. __restore()
  423. uids[props.name].after = getCurrentInstance()!.uid
  424. return () => ''
  425. },
  426. })
  427. const app = createApp(() =>
  428. h(Suspense, () =>
  429. h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })]),
  430. ),
  431. )
  432. const root = nodeOps.createElement('div')
  433. app.mount(root)
  434. await new Promise(r => setTimeout(r))
  435. expect(uids.one.before).not.toBe(uids.two.before)
  436. expect(uids.one.before).toBe(uids.one.after)
  437. expect(uids.two.before).toBe(uids.two.after)
  438. })
  439. test('should teardown in-scope effects', async () => {
  440. let resolve: (val?: any) => void
  441. const ready = new Promise(r => {
  442. resolve = r
  443. })
  444. let c: ComputedRefImpl
  445. let e: ReactiveEffectRunner
  446. const Comp = defineComponent({
  447. async setup() {
  448. let __temp: any, __restore: any
  449. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  450. __temp = await __temp
  451. __restore()
  452. c = computed(() => {}) as unknown as ComputedRefImpl
  453. e = effect(() => c.value)
  454. // register the lifecycle after an await statement
  455. onMounted(resolve)
  456. return () => c.value
  457. },
  458. })
  459. const app = createApp(() => h(Suspense, () => h(Comp)))
  460. const root = nodeOps.createElement('div')
  461. app.mount(root)
  462. await ready
  463. expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
  464. expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
  465. app.unmount()
  466. expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
  467. expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
  468. })
  469. })
  470. })