apiSetupHelpers.spec.ts 20 KB

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