import {
type InjectionKey,
type Ref,
defineComponent,
h,
hasInjectionContext,
inject,
nextTick,
onMounted,
provide,
reactive,
readonly,
ref,
} from '../src/index'
import { createApp, nodeOps, render, serialize } from '@vue/runtime-test'
describe('api: provide/inject', () => {
it('string keys', () => {
const Provider = {
setup() {
provide('foo', 1)
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const foo = inject('foo')
return () => foo
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`
1
`)
})
it('symbol keys', () => {
// also verifies InjectionKey type sync
const key: InjectionKey = Symbol()
const Provider = {
setup() {
provide(key, 1)
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const foo = inject(key) || 1
return () => foo + 1
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`2
`)
})
it('default values', () => {
const Provider = {
setup() {
provide('foo', 'foo')
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
// default value should be ignored if value is provided
const foo = inject('foo', 'fooDefault')
// default value should be used if value is not provided
const bar = inject('bar', 'bar')
return () => foo + bar
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`foobar
`)
})
it('bound to instance', () => {
const Provider = {
setup() {
return () => h(Consumer)
},
}
const Consumer = defineComponent({
name: 'Consumer',
inject: {
foo: {
from: 'foo',
default() {
return this!.$options.name
},
},
},
render() {
return this.foo
},
})
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`Consumer
`)
})
it('nested providers', () => {
const ProviderOne = {
setup() {
provide('foo', 'foo')
provide('bar', 'bar')
return () => h(ProviderTwo)
},
}
const ProviderTwo = {
setup() {
// override parent value
provide('foo', 'fooOverride')
provide('baz', 'baz')
return () => h(Consumer)
},
}
const Consumer = {
setup() {
const foo = inject('foo')
const bar = inject('bar')
const baz = inject('baz')
return () => [foo, bar, baz].join(',')
},
}
const root = nodeOps.createElement('div')
render(h(ProviderOne), root)
expect(serialize(root)).toBe(`fooOverride,bar,baz
`)
})
it('reactivity with refs', async () => {
const count = ref(1)
const Provider = {
setup() {
provide('count', count)
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const count = inject[>('count')!
return () => count.value
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`]1
`)
count.value++
await nextTick()
expect(serialize(root)).toBe(`2
`)
})
it('reactivity with readonly refs', async () => {
const count = ref(1)
const Provider = {
setup() {
provide('count', readonly(count))
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const count = inject[>('count')!
// should not work
count.value++
return () => count.value
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`]1
`)
expect(
`Set operation on key "value" failed: target is readonly`,
).toHaveBeenWarned()
// source mutation should still work
count.value++
await nextTick()
expect(serialize(root)).toBe(`2
`)
})
it('reactivity with objects', async () => {
const rootState = reactive({ count: 1 })
const Provider = {
setup() {
provide('state', rootState)
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const state = inject('state')!
return () => state.count
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`1
`)
rootState.count++
await nextTick()
expect(serialize(root)).toBe(`2
`)
})
it('reactivity with readonly objects', async () => {
const rootState = reactive({ count: 1 })
const Provider = {
setup() {
provide('state', readonly(rootState))
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const state = inject('state')!
// should not work
state.count++
return () => state.count
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(`1
`)
expect(
`Set operation on key "count" failed: target is readonly`,
).toHaveBeenWarned()
rootState.count++
await nextTick()
expect(serialize(root)).toBe(`2
`)
})
it('should warn unfound', () => {
const Provider = {
setup() {
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const foo = inject('foo')
expect(foo).toBeUndefined()
return () => foo
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(serialize(root)).toBe(``)
expect(`injection "foo" not found.`).toHaveBeenWarned()
})
it('should not warn when default value is undefined', () => {
const Provider = {
setup() {
return () => h(Middle)
},
}
const Middle = {
render: () => h(Consumer),
}
const Consumer = {
setup() {
const foo = inject('foo', undefined)
return () => foo
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
})
// #2400
it('should not self-inject', () => {
const Comp = {
setup() {
provide('foo', 'foo')
const injection = inject('foo', null)
return () => injection
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serialize(root)).toBe(``)
})
describe('hasInjectionContext', () => {
it('should be false outside of setup', () => {
expect(hasInjectionContext()).toBe(false)
})
it('should be true within setup', () => {
expect.assertions(1)
const Comp = {
setup() {
expect(hasInjectionContext()).toBe(true)
return () => null
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
})
it('should be true within app.runWithContext()', () => {
expect.assertions(1)
createApp({}).runWithContext(() => {
expect(hasInjectionContext()).toBe(true)
})
})
})
describe('warnings for incorrect usage', () => {
it('should warn when inject() is called outside setup', () => {
inject('foo', 'bar')
expect(`inject() can only be used`).toHaveBeenWarned()
})
it('should warn when provide() is called outside setup', () => {
provide('foo', 'bar')
expect(`provide() can only be used`).toHaveBeenWarned()
})
it('should warn when provide() is called from a render function', () => {
const Provider = {
setup() {
return () => {
provide('foo', 'bar')
}
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(`provide() can only be used`).toHaveBeenWarned()
})
it('should warn when provide() is called from onMounted', () => {
const Provider = {
setup() {
onMounted(() => {
provide('foo', 'bar')
})
return () => null
},
}
const root = nodeOps.createElement('div')
render(h(Provider), root)
expect(`provide() can only be used`).toHaveBeenWarned()
})
})
})