| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- import {
- type ComponentInternalInstance,
- type SetupContext,
- Suspense,
- computed,
- createApp,
- defineComponent,
- getCurrentInstance,
- h,
- nodeOps,
- onMounted,
- render,
- serializeInner,
- shallowReactive,
- } from '@vue/runtime-test'
- import {
- createPropsRestProxy,
- defineEmits,
- defineExpose,
- defineProps,
- mergeDefaults,
- mergeModels,
- useAttrs,
- useSlots,
- withAsyncContext,
- withDefaults,
- } from '../src/apiSetupHelpers'
- import type { ComputedRefImpl } from '../../reactivity/src/computed'
- import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
- describe('SFC <script setup> helpers', () => {
- test('should warn runtime usage', () => {
- defineProps()
- expect(`defineProps() is a compiler-hint`).toHaveBeenWarned()
- defineEmits()
- expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
- defineExpose()
- expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
- withDefaults({}, {})
- expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
- })
- test('useSlots / useAttrs (no args)', () => {
- let slots: SetupContext['slots'] | undefined
- let attrs: SetupContext['attrs'] | undefined
- const Comp = {
- setup() {
- slots = useSlots()
- attrs = useAttrs()
- return () => {}
- },
- }
- const passedAttrs = { id: 'foo' }
- const passedSlots = {
- default: () => {},
- x: () => {},
- }
- render(h(Comp, passedAttrs, passedSlots), nodeOps.createElement('div'))
- expect(typeof slots!.default).toBe('function')
- expect(typeof slots!.x).toBe('function')
- expect(attrs).toMatchObject(passedAttrs)
- })
- test('useSlots / useAttrs (with args)', () => {
- let slots: SetupContext['slots'] | undefined
- let attrs: SetupContext['attrs'] | undefined
- let ctx: SetupContext | undefined
- const Comp = defineComponent({
- setup(_, _ctx) {
- slots = useSlots()
- attrs = useAttrs()
- ctx = _ctx
- return () => {}
- },
- })
- render(h(Comp), nodeOps.createElement('div'))
- expect(slots).toBe(ctx!.slots)
- expect(attrs).toBe(ctx!.attrs)
- })
- describe('mergeDefaults', () => {
- test('object syntax', () => {
- const merged = mergeDefaults(
- {
- foo: null,
- bar: { type: String, required: false },
- baz: String,
- },
- {
- foo: 1,
- bar: 'baz',
- baz: 'qux',
- },
- )
- expect(merged).toMatchObject({
- foo: { default: 1 },
- bar: { type: String, required: false, default: 'baz' },
- baz: { type: String, default: 'qux' },
- })
- })
- test('array syntax', () => {
- const merged = mergeDefaults(['foo', 'bar', 'baz'], {
- foo: 1,
- bar: 'baz',
- baz: 'qux',
- })
- expect(merged).toMatchObject({
- foo: { default: 1 },
- bar: { default: 'baz' },
- baz: { default: 'qux' },
- })
- })
- test('merging with skipFactory', () => {
- const fn = () => {}
- const merged = mergeDefaults(['foo', 'bar', 'baz'], {
- foo: fn,
- __skip_foo: true,
- })
- expect(merged).toMatchObject({
- foo: { default: fn, skipFactory: true },
- })
- })
- test('should warn missing', () => {
- mergeDefaults({}, { foo: 1 })
- expect(
- `props default key "foo" has no corresponding declaration`,
- ).toHaveBeenWarned()
- })
- })
- describe('mergeModels', () => {
- test('array syntax', () => {
- expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
- 'foo',
- 'bar',
- 'baz',
- ])
- })
- test('object syntax', () => {
- expect(
- mergeModels({ foo: null, bar: { required: true } }, ['baz']),
- ).toMatchObject({
- foo: null,
- bar: { required: true },
- baz: {},
- })
- expect(
- mergeModels(['baz'], { foo: null, bar: { required: true } }),
- ).toMatchObject({
- foo: null,
- bar: { required: true },
- baz: {},
- })
- })
- test('overwrite', () => {
- expect(
- mergeModels(
- { foo: null, bar: { required: true } },
- { bar: {}, baz: {} },
- ),
- ).toMatchObject({
- foo: null,
- bar: {},
- baz: {},
- })
- })
- })
- test('createPropsRestProxy', () => {
- const original = shallowReactive({
- foo: 1,
- bar: 2,
- baz: 3,
- })
- const rest = createPropsRestProxy(original, ['foo', 'bar'])
- expect('foo' in rest).toBe(false)
- expect('bar' in rest).toBe(false)
- expect(rest.baz).toBe(3)
- expect(Object.keys(rest)).toEqual(['baz'])
- original.baz = 4
- expect(rest.baz).toBe(4)
- })
- describe('withAsyncContext', () => {
- // disable options API because applyOptions() also resets currentInstance
- // and we want to ensure the logic works even with Options API disabled.
- beforeEach(() => {
- __FEATURE_OPTIONS_API__ = false
- })
- afterEach(() => {
- __FEATURE_OPTIONS_API__ = true
- })
- test('basic', async () => {
- const spy = vi.fn()
- let beforeInstance: ComponentInternalInstance | null = null
- let afterInstance: ComponentInternalInstance | null = null
- let resolve: (msg: string) => void
- const Comp = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- beforeInstance = getCurrentInstance()
- const msg =
- (([__temp, __restore] = withAsyncContext(
- () =>
- new Promise(r => {
- resolve = r
- }),
- )),
- (__temp = await __temp),
- __restore(),
- __temp)
- // register the lifecycle after an await statement
- onMounted(spy)
- afterInstance = getCurrentInstance()
- return () => msg
- },
- })
- const root = nodeOps.createElement('div')
- render(
- h(() => h(Suspense, () => h(Comp))),
- root,
- )
- expect(spy).not.toHaveBeenCalled()
- resolve!('hello')
- // wait a macro task tick for all micro ticks to resolve
- await new Promise(r => setTimeout(r))
- // mount hook should have been called
- expect(spy).toHaveBeenCalled()
- // should retain same instance before/after the await call
- expect(beforeInstance).toBe(afterInstance)
- expect(serializeInner(root)).toBe('hello')
- })
- test('should not leak instance to user microtasks after restore', async () => {
- let leakedToUserMicrotask = false
- const Comp = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- Promise.resolve().then(() => {
- leakedToUserMicrotask = getCurrentInstance() !== null
- })
- return () => ''
- },
- })
- const root = nodeOps.createElement('div')
- render(
- h(() => h(Suspense, () => h(Comp))),
- root,
- )
- await new Promise(r => setTimeout(r))
- expect(leakedToUserMicrotask).toBe(false)
- })
- test('should not leak sibling instance in concurrent restores', async () => {
- let resolveOne: () => void
- let resolveTwo: () => void
- let done!: () => void
- let pending = 2
- const ready = new Promise<void>(r => {
- done = r
- })
- const seenUid: Record<'one' | 'two', number | null> = {
- one: null,
- two: null,
- }
- const makeComp = (name: 'one' | 'two', wait: Promise<void>) =>
- defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => wait)
- __temp = await __temp
- __restore()
- Promise.resolve().then(() => {
- seenUid[name] = getCurrentInstance()?.uid ?? null
- if (--pending === 0) done()
- })
- return () => ''
- },
- })
- const oneReady = new Promise<void>(r => {
- resolveOne = r
- })
- const twoReady = new Promise<void>(r => {
- resolveTwo = r
- })
- const CompOne = makeComp('one', oneReady)
- const CompTwo = makeComp('two', twoReady)
- const root = nodeOps.createElement('div')
- render(
- h(() => h(Suspense, () => h('div', [h(CompOne), h(CompTwo)]))),
- root,
- )
- resolveOne!()
- resolveTwo!()
- await ready
- expect(seenUid.one).toBeNull()
- expect(seenUid.two).toBeNull()
- })
- test('should not leak currentInstance to sibling slot render', async () => {
- let done!: () => void
- const ready = new Promise<void>(r => {
- done = r
- })
- let innerUid: number | null = null
- let innerRenderUid: number | null = null
- const Inner = defineComponent({
- setup(_, { slots }) {
- innerUid = getCurrentInstance()!.uid
- return () => {
- innerRenderUid = getCurrentInstance()!.uid
- done()
- return h('div', slots.default?.())
- }
- },
- })
- const Outer = defineComponent({
- setup(_, { slots }) {
- return () => h(Inner, null, () => [slots.default?.()])
- },
- })
- const AsyncA = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() =>
- Promise.resolve()
- .then(() => {})
- .then(() => {}),
- )
- __temp = await __temp
- __restore()
- return () => h('div', 'A')
- },
- })
- const AsyncB = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- return () => h(Outer, null, () => 'B')
- },
- })
- const root = nodeOps.createElement('div')
- render(
- h(() => h(Suspense, () => h('div', [h(AsyncA), h(AsyncB)]))),
- root,
- )
- await ready
- expect(
- 'Slot "default" invoked outside of the render function',
- ).not.toHaveBeenWarned()
- expect(innerRenderUid).toBe(innerUid)
- await Promise.resolve()
- expect(serializeInner(root)).toBe(`<div><div>A</div><div>B</div></div>`)
- })
- test('error handling', async () => {
- const spy = vi.fn()
- let beforeInstance: ComponentInternalInstance | null = null
- let afterInstance: ComponentInternalInstance | null = null
- let reject: () => void
- const Comp = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- beforeInstance = getCurrentInstance()
- try {
- ;[__temp, __restore] = withAsyncContext(
- () =>
- new Promise((_, rj) => {
- reject = rj
- }),
- )
- __temp = await __temp
- __restore()
- } catch (e: any) {
- // ignore
- }
- // register the lifecycle after an await statement
- onMounted(spy)
- afterInstance = getCurrentInstance()
- return () => ''
- },
- })
- const root = nodeOps.createElement('div')
- render(
- h(() => h(Suspense, () => h(Comp))),
- root,
- )
- expect(spy).not.toHaveBeenCalled()
- reject!()
- // wait a macro task tick for all micro ticks to resolve
- await new Promise(r => setTimeout(r))
- // mount hook should have been called
- expect(spy).toHaveBeenCalled()
- // should retain same instance before/after the await call
- expect(beforeInstance).toBe(afterInstance)
- // instance scope should be fully restored/cleaned after async ticks
- expect((beforeInstance!.scope as any)._on).toBe(0)
- })
- test('should not leak instance on multiple awaits', async () => {
- let resolve: (val?: any) => void
- let beforeInstance: ComponentInternalInstance | null = null
- let afterInstance: ComponentInternalInstance | null = null
- let inBandInstance: ComponentInternalInstance | null = null
- let outOfBandInstance: ComponentInternalInstance | null = null
- const ready = new Promise(r => {
- resolve = r
- })
- async function doAsyncWork() {
- // should still have instance
- inBandInstance = getCurrentInstance()
- await Promise.resolve()
- // should not leak instance
- outOfBandInstance = getCurrentInstance()
- }
- const Comp = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- beforeInstance = getCurrentInstance()
- // first await
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- // setup exit, instance set to null, then resumed
- ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
- __temp = await __temp
- __restore()
- afterInstance = getCurrentInstance()
- return () => {
- resolve()
- return ''
- }
- },
- })
- const root = nodeOps.createElement('div')
- render(
- h(() => h(Suspense, () => h(Comp))),
- root,
- )
- await ready
- expect(inBandInstance).toBe(beforeInstance)
- expect(outOfBandInstance).toBeNull()
- expect(afterInstance).toBe(beforeInstance)
- expect(getCurrentInstance()).toBeNull()
- })
- test('should not leak on multiple awaits + error', async () => {
- let resolve: (val?: any) => void
- const ready = new Promise(r => {
- resolve = r
- })
- const Comp = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
- __temp = await __temp
- __restore()
- },
- render() {},
- })
- const app = createApp(() => h(Suspense, () => h(Comp)))
- app.config.errorHandler = () => {
- resolve()
- return false
- }
- const root = nodeOps.createElement('div')
- app.mount(root)
- await ready
- expect(getCurrentInstance()).toBeNull()
- })
- // #4050
- test('race conditions', async () => {
- const uids = {
- one: { before: NaN, after: NaN },
- two: { before: NaN, after: NaN },
- }
- const Comp = defineComponent({
- props: ['name'],
- async setup(props: { name: 'one' | 'two' }) {
- let __temp: any, __restore: any
- uids[props.name].before = getCurrentInstance()!.uid
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- uids[props.name].after = getCurrentInstance()!.uid
- return () => ''
- },
- })
- const app = createApp(() =>
- h(Suspense, () =>
- h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })]),
- ),
- )
- const root = nodeOps.createElement('div')
- app.mount(root)
- await new Promise(r => setTimeout(r))
- expect(uids.one.before).not.toBe(uids.two.before)
- expect(uids.one.before).toBe(uids.one.after)
- expect(uids.two.before).toBe(uids.two.after)
- })
- test('should teardown in-scope effects', async () => {
- let resolve: (val?: any) => void
- const ready = new Promise(r => {
- resolve = r
- })
- let c: ComputedRefImpl
- let e: ReactiveEffectRunner
- const Comp = defineComponent({
- async setup() {
- let __temp: any, __restore: any
- ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
- __temp = await __temp
- __restore()
- c = computed(() => {}) as unknown as ComputedRefImpl
- e = effect(() => c.value)
- // register the lifecycle after an await statement
- onMounted(resolve)
- return () => c.value
- },
- })
- const app = createApp(() => h(Suspense, () => h(Comp)))
- const root = nodeOps.createElement('div')
- app.mount(root)
- await ready
- expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
- expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
- app.unmount()
- expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
- expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
- })
- })
- })
|