apiSetupHelpers.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  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. nextTick,
  17. ref,
  18. Ref,
  19. watch
  20. } from '@vue/runtime-test'
  21. import {
  22. defineEmits,
  23. defineProps,
  24. defineExpose,
  25. withDefaults,
  26. useAttrs,
  27. useSlots,
  28. mergeDefaults,
  29. withAsyncContext,
  30. createPropsRestProxy,
  31. mergeModels,
  32. useModel
  33. } from '../src/apiSetupHelpers'
  34. describe('SFC <script setup> helpers', () => {
  35. test('should warn runtime usage', () => {
  36. defineProps()
  37. expect(`defineProps() is a compiler-hint`).toHaveBeenWarned()
  38. defineEmits()
  39. expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
  40. defineExpose()
  41. expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
  42. withDefaults({}, {})
  43. expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
  44. })
  45. test('useSlots / useAttrs (no args)', () => {
  46. let slots: SetupContext['slots'] | undefined
  47. let attrs: SetupContext['attrs'] | undefined
  48. const Comp = {
  49. setup() {
  50. slots = useSlots()
  51. attrs = useAttrs()
  52. return () => {}
  53. }
  54. }
  55. const passedAttrs = { id: 'foo' }
  56. const passedSlots = {
  57. default: () => {},
  58. x: () => {}
  59. }
  60. render(h(Comp, passedAttrs, passedSlots), nodeOps.createElement('div'))
  61. expect(typeof slots!.default).toBe('function')
  62. expect(typeof slots!.x).toBe('function')
  63. expect(attrs).toMatchObject(passedAttrs)
  64. })
  65. test('useSlots / useAttrs (with args)', () => {
  66. let slots: SetupContext['slots'] | undefined
  67. let attrs: SetupContext['attrs'] | undefined
  68. let ctx: SetupContext | undefined
  69. const Comp = defineComponent({
  70. setup(_, _ctx) {
  71. slots = useSlots()
  72. attrs = useAttrs()
  73. ctx = _ctx
  74. return () => {}
  75. }
  76. })
  77. render(h(Comp), nodeOps.createElement('div'))
  78. expect(slots).toBe(ctx!.slots)
  79. expect(attrs).toBe(ctx!.attrs)
  80. })
  81. describe('mergeDefaults', () => {
  82. test('object syntax', () => {
  83. const merged = mergeDefaults(
  84. {
  85. foo: null,
  86. bar: { type: String, required: false },
  87. baz: String
  88. },
  89. {
  90. foo: 1,
  91. bar: 'baz',
  92. baz: 'qux'
  93. }
  94. )
  95. expect(merged).toMatchObject({
  96. foo: { default: 1 },
  97. bar: { type: String, required: false, default: 'baz' },
  98. baz: { type: String, default: 'qux' }
  99. })
  100. })
  101. test('array syntax', () => {
  102. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  103. foo: 1,
  104. bar: 'baz',
  105. baz: 'qux'
  106. })
  107. expect(merged).toMatchObject({
  108. foo: { default: 1 },
  109. bar: { default: 'baz' },
  110. baz: { default: 'qux' }
  111. })
  112. })
  113. test('merging with skipFactory', () => {
  114. const fn = () => {}
  115. const merged = mergeDefaults(['foo', 'bar', 'baz'], {
  116. foo: fn,
  117. __skip_foo: true
  118. })
  119. expect(merged).toMatchObject({
  120. foo: { default: fn, skipFactory: true }
  121. })
  122. })
  123. test('should warn missing', () => {
  124. mergeDefaults({}, { foo: 1 })
  125. expect(
  126. `props default key "foo" has no corresponding declaration`
  127. ).toHaveBeenWarned()
  128. })
  129. })
  130. describe('mergeModels', () => {
  131. test('array syntax', () => {
  132. expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
  133. 'foo',
  134. 'bar',
  135. 'baz'
  136. ])
  137. })
  138. test('object syntax', () => {
  139. expect(
  140. mergeModels({ foo: null, bar: { required: true } }, ['baz'])
  141. ).toMatchObject({
  142. foo: null,
  143. bar: { required: true },
  144. baz: {}
  145. })
  146. expect(
  147. mergeModels(['baz'], { foo: null, bar: { required: true } })
  148. ).toMatchObject({
  149. foo: null,
  150. bar: { required: true },
  151. baz: {}
  152. })
  153. })
  154. test('overwrite', () => {
  155. expect(
  156. mergeModels(
  157. { foo: null, bar: { required: true } },
  158. { bar: {}, baz: {} }
  159. )
  160. ).toMatchObject({
  161. foo: null,
  162. bar: {},
  163. baz: {}
  164. })
  165. })
  166. })
  167. describe('useModel', () => {
  168. test('basic', async () => {
  169. let foo: any
  170. const update = () => {
  171. foo.value = 'bar'
  172. }
  173. const compRender = vi.fn()
  174. const Comp = defineComponent({
  175. props: ['modelValue'],
  176. emits: ['update:modelValue'],
  177. setup(props) {
  178. foo = useModel(props, 'modelValue')
  179. return () => {
  180. compRender()
  181. return foo.value
  182. }
  183. }
  184. })
  185. const msg = ref('')
  186. const setValue = vi.fn(v => (msg.value = v))
  187. const root = nodeOps.createElement('div')
  188. createApp(() =>
  189. h(Comp, {
  190. modelValue: msg.value,
  191. 'onUpdate:modelValue': setValue
  192. })
  193. ).mount(root)
  194. expect(foo.value).toBe('')
  195. expect(msg.value).toBe('')
  196. expect(setValue).not.toBeCalled()
  197. expect(compRender).toBeCalledTimes(1)
  198. expect(serializeInner(root)).toBe('')
  199. // update from child
  200. update()
  201. await nextTick()
  202. expect(msg.value).toBe('bar')
  203. expect(foo.value).toBe('bar')
  204. expect(setValue).toBeCalledTimes(1)
  205. expect(compRender).toBeCalledTimes(2)
  206. expect(serializeInner(root)).toBe('bar')
  207. // update from parent
  208. msg.value = 'qux'
  209. expect(msg.value).toBe('qux')
  210. await nextTick()
  211. expect(msg.value).toBe('qux')
  212. expect(foo.value).toBe('qux')
  213. expect(setValue).toBeCalledTimes(1)
  214. expect(compRender).toBeCalledTimes(3)
  215. expect(serializeInner(root)).toBe('qux')
  216. })
  217. test('without parent value (local mutation)', async () => {
  218. let foo: any
  219. const update = () => {
  220. foo.value = 'bar'
  221. }
  222. const compRender = vi.fn()
  223. const Comp = defineComponent({
  224. props: ['foo'],
  225. emits: ['update:foo'],
  226. setup(props) {
  227. foo = useModel(props, 'foo')
  228. return () => {
  229. compRender()
  230. return foo.value
  231. }
  232. }
  233. })
  234. const root = nodeOps.createElement('div')
  235. const updateFoo = vi.fn()
  236. render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
  237. expect(compRender).toBeCalledTimes(1)
  238. expect(serializeInner(root)).toBe('<!---->')
  239. expect(foo.value).toBeUndefined()
  240. update()
  241. // when parent didn't provide value, local mutation is enabled
  242. expect(foo.value).toBe('bar')
  243. await nextTick()
  244. expect(updateFoo).toBeCalledTimes(1)
  245. expect(compRender).toBeCalledTimes(2)
  246. expect(serializeInner(root)).toBe('bar')
  247. })
  248. test('default value', async () => {
  249. let count: any
  250. const inc = () => {
  251. count.value++
  252. }
  253. const compRender = vi.fn()
  254. const Comp = defineComponent({
  255. props: { count: { default: 0 } },
  256. emits: ['update:count'],
  257. setup(props) {
  258. count = useModel(props, 'count')
  259. return () => {
  260. compRender()
  261. return count.value
  262. }
  263. }
  264. })
  265. const root = nodeOps.createElement('div')
  266. const updateCount = vi.fn()
  267. render(h(Comp, { 'onUpdate:count': updateCount }), root)
  268. expect(compRender).toBeCalledTimes(1)
  269. expect(serializeInner(root)).toBe('0')
  270. expect(count.value).toBe(0)
  271. inc()
  272. // when parent didn't provide value, local mutation is enabled
  273. expect(count.value).toBe(1)
  274. await nextTick()
  275. expect(updateCount).toBeCalledTimes(1)
  276. expect(compRender).toBeCalledTimes(2)
  277. expect(serializeInner(root)).toBe('1')
  278. })
  279. test('parent limiting child value', async () => {
  280. let childCount: Ref<number>
  281. const compRender = vi.fn()
  282. const Comp = defineComponent({
  283. props: ['count'],
  284. emits: ['update:count'],
  285. setup(props) {
  286. childCount = useModel(props, 'count')
  287. return () => {
  288. compRender()
  289. return childCount.value
  290. }
  291. }
  292. })
  293. const Parent = defineComponent({
  294. setup() {
  295. const count = ref(0)
  296. watch(count, () => {
  297. if (count.value < 0) {
  298. count.value = 0
  299. }
  300. })
  301. return () =>
  302. h(Comp, {
  303. count: count.value,
  304. 'onUpdate:count': val => {
  305. count.value = val
  306. }
  307. })
  308. }
  309. })
  310. const root = nodeOps.createElement('div')
  311. render(h(Parent), root)
  312. expect(serializeInner(root)).toBe('0')
  313. // child update
  314. childCount!.value = 1
  315. // not yet updated
  316. expect(childCount!.value).toBe(0)
  317. await nextTick()
  318. expect(childCount!.value).toBe(1)
  319. expect(serializeInner(root)).toBe('1')
  320. // child update to invalid value
  321. childCount!.value = -1
  322. // not yet updated
  323. expect(childCount!.value).toBe(1)
  324. await nextTick()
  325. // limited to 0 by parent
  326. expect(childCount!.value).toBe(0)
  327. expect(serializeInner(root)).toBe('0')
  328. })
  329. test('has parent value -> no parent value', async () => {
  330. let childCount: Ref<number>
  331. const compRender = vi.fn()
  332. const Comp = defineComponent({
  333. props: ['count'],
  334. emits: ['update:count'],
  335. setup(props) {
  336. childCount = useModel(props, 'count')
  337. return () => {
  338. compRender()
  339. return childCount.value
  340. }
  341. }
  342. })
  343. const toggle = ref(true)
  344. const Parent = defineComponent({
  345. setup() {
  346. const count = ref(0)
  347. return () =>
  348. toggle.value
  349. ? h(Comp, {
  350. count: count.value,
  351. 'onUpdate:count': val => {
  352. count.value = val
  353. }
  354. })
  355. : h(Comp)
  356. }
  357. })
  358. const root = nodeOps.createElement('div')
  359. render(h(Parent), root)
  360. expect(serializeInner(root)).toBe('0')
  361. // child update
  362. childCount!.value = 1
  363. // not yet updated
  364. expect(childCount!.value).toBe(0)
  365. await nextTick()
  366. expect(childCount!.value).toBe(1)
  367. expect(serializeInner(root)).toBe('1')
  368. // parent change
  369. toggle.value = false
  370. await nextTick()
  371. // localValue should be reset
  372. expect(childCount!.value).toBeUndefined()
  373. expect(serializeInner(root)).toBe('<!---->')
  374. // child local mutation should continue to work
  375. childCount!.value = 2
  376. expect(childCount!.value).toBe(2)
  377. await nextTick()
  378. expect(serializeInner(root)).toBe('2')
  379. })
  380. })
  381. test('createPropsRestProxy', () => {
  382. const original = shallowReactive({
  383. foo: 1,
  384. bar: 2,
  385. baz: 3
  386. })
  387. const rest = createPropsRestProxy(original, ['foo', 'bar'])
  388. expect('foo' in rest).toBe(false)
  389. expect('bar' in rest).toBe(false)
  390. expect(rest.baz).toBe(3)
  391. expect(Object.keys(rest)).toEqual(['baz'])
  392. original.baz = 4
  393. expect(rest.baz).toBe(4)
  394. })
  395. describe('withAsyncContext', () => {
  396. // disable options API because applyOptions() also resets currentInstance
  397. // and we want to ensure the logic works even with Options API disabled.
  398. beforeEach(() => {
  399. __FEATURE_OPTIONS_API__ = false
  400. })
  401. afterEach(() => {
  402. __FEATURE_OPTIONS_API__ = true
  403. })
  404. test('basic', async () => {
  405. const spy = vi.fn()
  406. let beforeInstance: ComponentInternalInstance | null = null
  407. let afterInstance: ComponentInternalInstance | null = null
  408. let resolve: (msg: string) => void
  409. const Comp = defineComponent({
  410. async setup() {
  411. let __temp: any, __restore: any
  412. beforeInstance = getCurrentInstance()
  413. const msg =
  414. (([__temp, __restore] = withAsyncContext(
  415. () =>
  416. new Promise(r => {
  417. resolve = r
  418. })
  419. )),
  420. (__temp = await __temp),
  421. __restore(),
  422. __temp)
  423. // register the lifecycle after an await statement
  424. onMounted(spy)
  425. afterInstance = getCurrentInstance()
  426. return () => msg
  427. }
  428. })
  429. const root = nodeOps.createElement('div')
  430. render(
  431. h(() => h(Suspense, () => h(Comp))),
  432. root
  433. )
  434. expect(spy).not.toHaveBeenCalled()
  435. resolve!('hello')
  436. // wait a macro task tick for all micro ticks to resolve
  437. await new Promise(r => setTimeout(r))
  438. // mount hook should have been called
  439. expect(spy).toHaveBeenCalled()
  440. // should retain same instance before/after the await call
  441. expect(beforeInstance).toBe(afterInstance)
  442. expect(serializeInner(root)).toBe('hello')
  443. })
  444. test('error handling', async () => {
  445. const spy = vi.fn()
  446. let beforeInstance: ComponentInternalInstance | null = null
  447. let afterInstance: ComponentInternalInstance | null = null
  448. let reject: () => void
  449. const Comp = defineComponent({
  450. async setup() {
  451. let __temp: any, __restore: any
  452. beforeInstance = getCurrentInstance()
  453. try {
  454. ;[__temp, __restore] = withAsyncContext(
  455. () =>
  456. new Promise((_, rj) => {
  457. reject = rj
  458. })
  459. )
  460. __temp = await __temp
  461. __restore()
  462. } catch (e: any) {
  463. // ignore
  464. }
  465. // register the lifecycle after an await statement
  466. onMounted(spy)
  467. afterInstance = getCurrentInstance()
  468. return () => ''
  469. }
  470. })
  471. const root = nodeOps.createElement('div')
  472. render(
  473. h(() => h(Suspense, () => h(Comp))),
  474. root
  475. )
  476. expect(spy).not.toHaveBeenCalled()
  477. reject!()
  478. // wait a macro task tick for all micro ticks to resolve
  479. await new Promise(r => setTimeout(r))
  480. // mount hook should have been called
  481. expect(spy).toHaveBeenCalled()
  482. // should retain same instance before/after the await call
  483. expect(beforeInstance).toBe(afterInstance)
  484. })
  485. test('should not leak instance on multiple awaits', async () => {
  486. let resolve: (val?: any) => void
  487. let beforeInstance: ComponentInternalInstance | null = null
  488. let afterInstance: ComponentInternalInstance | null = null
  489. let inBandInstance: ComponentInternalInstance | null = null
  490. let outOfBandInstance: ComponentInternalInstance | null = null
  491. const ready = new Promise(r => {
  492. resolve = r
  493. })
  494. async function doAsyncWork() {
  495. // should still have instance
  496. inBandInstance = getCurrentInstance()
  497. await Promise.resolve()
  498. // should not leak instance
  499. outOfBandInstance = getCurrentInstance()
  500. }
  501. const Comp = defineComponent({
  502. async setup() {
  503. let __temp: any, __restore: any
  504. beforeInstance = getCurrentInstance()
  505. // first await
  506. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  507. __temp = await __temp
  508. __restore()
  509. // setup exit, instance set to null, then resumed
  510. ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
  511. __temp = await __temp
  512. __restore()
  513. afterInstance = getCurrentInstance()
  514. return () => {
  515. resolve()
  516. return ''
  517. }
  518. }
  519. })
  520. const root = nodeOps.createElement('div')
  521. render(
  522. h(() => h(Suspense, () => h(Comp))),
  523. root
  524. )
  525. await ready
  526. expect(inBandInstance).toBe(beforeInstance)
  527. expect(outOfBandInstance).toBeNull()
  528. expect(afterInstance).toBe(beforeInstance)
  529. expect(getCurrentInstance()).toBeNull()
  530. })
  531. test('should not leak on multiple awaits + error', async () => {
  532. let resolve: (val?: any) => void
  533. const ready = new Promise(r => {
  534. resolve = r
  535. })
  536. const Comp = defineComponent({
  537. async setup() {
  538. let __temp: any, __restore: any
  539. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  540. __temp = await __temp
  541. __restore()
  542. ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
  543. __temp = await __temp
  544. __restore()
  545. },
  546. render() {}
  547. })
  548. const app = createApp(() => h(Suspense, () => h(Comp)))
  549. app.config.errorHandler = () => {
  550. resolve()
  551. return false
  552. }
  553. const root = nodeOps.createElement('div')
  554. app.mount(root)
  555. await ready
  556. expect(getCurrentInstance()).toBeNull()
  557. })
  558. // #4050
  559. test('race conditions', async () => {
  560. const uids = {
  561. one: { before: NaN, after: NaN },
  562. two: { before: NaN, after: NaN }
  563. }
  564. const Comp = defineComponent({
  565. props: ['name'],
  566. async setup(props: { name: 'one' | 'two' }) {
  567. let __temp: any, __restore: any
  568. uids[props.name].before = getCurrentInstance()!.uid
  569. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  570. __temp = await __temp
  571. __restore()
  572. uids[props.name].after = getCurrentInstance()!.uid
  573. return () => ''
  574. }
  575. })
  576. const app = createApp(() =>
  577. h(Suspense, () =>
  578. h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })])
  579. )
  580. )
  581. const root = nodeOps.createElement('div')
  582. app.mount(root)
  583. await new Promise(r => setTimeout(r))
  584. expect(uids.one.before).not.toBe(uids.two.before)
  585. expect(uids.one.before).toBe(uids.one.after)
  586. expect(uids.two.before).toBe(uids.two.after)
  587. })
  588. test('should teardown in-scope effects', async () => {
  589. let resolve: (val?: any) => void
  590. const ready = new Promise(r => {
  591. resolve = r
  592. })
  593. let c: ComputedRef
  594. const Comp = defineComponent({
  595. async setup() {
  596. let __temp: any, __restore: any
  597. ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
  598. __temp = await __temp
  599. __restore()
  600. c = computed(() => {})
  601. // register the lifecycle after an await statement
  602. onMounted(resolve)
  603. return () => ''
  604. }
  605. })
  606. const app = createApp(() => h(Suspense, () => h(Comp)))
  607. const root = nodeOps.createElement('div')
  608. app.mount(root)
  609. await ready
  610. expect(c!.effect.active).toBe(true)
  611. app.unmount()
  612. expect(c!.effect.active).toBe(false)
  613. })
  614. })
  615. })