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() }) }) })