Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 397 additions & 0 deletions packages/runtime-vapor/__tests__/apiInject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,397 @@
// NOTE: This test is implemented based on the case of `runtime-core/__test__/apiInject.spec.ts`.

import {
type InjectionKey,
type Ref,
createComponent,
createTextNode,
createVaporApp,
getCurrentInstance,
hasInjectionContext,
inject,
nextTick,
provide,
reactive,
readonly,
ref,
renderEffect,
setText,
} from '../src'
import { makeRender } from './_utils'

const define = makeRender<any>()

// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
describe('api: provide/inject', () => {
it('string keys', () => {
const Provider = define({
setup() {
provide('foo', 1)
return createComponent(Middle)
},
})

const Middle = {
render() {
return createComponent(Consumer)
},
}

const Consumer = {
setup() {
const foo = inject('foo')
return (() => {
const n0 = createTextNode()
setText(n0, foo)
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('1')
})

it('symbol keys', () => {
// also verifies InjectionKey type sync
const key: InjectionKey<number> = Symbol()

const Provider = define({
setup() {
provide(key, 1)
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const foo = inject(key)
return (() => {
const n0 = createTextNode()
setText(n0, foo)
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('1')
})

it('default values', () => {
const Provider = define({
setup() {
provide('foo', 'foo')
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(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 (() => {
const n0 = createTextNode()
setText(n0, foo + bar)
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('foobar')
})

// NOTE: Options API is not supported
// it('bound to instance', () => {})

it('nested providers', () => {
const ProviderOne = define({
setup() {
provide('foo', 'foo')
provide('bar', 'bar')
return createComponent(ProviderTwo)
},
})

const ProviderTwo = {
setup() {
// override parent value
provide('foo', 'fooOverride')
provide('baz', 'baz')
return createComponent(Consumer)
},
}

const Consumer = {
setup() {
const foo = inject('foo')
const bar = inject('bar')
const baz = inject('baz')
return (() => {
const n0 = createTextNode()
setText(n0, [foo, bar, baz].join(','))
return n0
})()
},
}

ProviderOne.render()
expect(ProviderOne.host.innerHTML).toBe('fooOverride,bar,baz')
})

it('reactivity with refs', async () => {
const count = ref(1)

const Provider = define({
setup() {
provide('count', count)
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const count = inject<Ref<number>>('count')!
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, count.value)
})
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('1')

count.value++
await nextTick()
expect(Provider.host.innerHTML).toBe('2')
})

it('reactivity with readonly refs', async () => {
const count = ref(1)

const Provider = define({
setup() {
provide('count', readonly(count))
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const count = inject<Ref<number>>('count')!
// should not work
count.value++
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, count.value)
})
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('1')

expect(
`Set operation on key "value" failed: target is readonly`,
).toHaveBeenWarned()

count.value++
await nextTick()
expect(Provider.host.innerHTML).toBe('2')
})

it('reactivity with objects', async () => {
const rootState = reactive({ count: 1 })

const Provider = define({
setup() {
provide('state', rootState)
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const state = inject<typeof rootState>('state')!
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, state.count)
})
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('1')

rootState.count++
await nextTick()
expect(Provider.host.innerHTML).toBe('2')
})

it('reactivity with readonly objects', async () => {
const rootState = reactive({ count: 1 })

const Provider = define({
setup() {
provide('state', readonly(rootState))
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const state = inject<typeof rootState>('state')!
// should not work
state.count++
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, state.count)
})
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('1')

expect(
`Set operation on key "count" failed: target is readonly`,
).toHaveBeenWarned()

rootState.count++
await nextTick()
expect(Provider.host.innerHTML).toBe('2')
})

it('should warn unfound', () => {
const Provider = define({
setup() {
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const foo = inject('foo')
expect(foo).toBeUndefined()
return (() => {
const n0 = createTextNode()
setText(n0, foo)
return n0
})()
},
}

Provider.render()
expect(Provider.host.innerHTML).toBe('')
expect(`injection "foo" not found.`).toHaveBeenWarned()
})

it('should not warn when default value is undefined', () => {
const Provider = define({
setup() {
return createComponent(Middle)
},
})

const Middle = {
render: () => createComponent(Consumer),
}

const Consumer = {
setup() {
const foo = inject('foo', undefined)
return (() => {
const n0 = createTextNode()
setText(n0, foo)
return n0
})()
},
}

Provider.render()
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
})

// #2400
it.todo('should not self-inject', () => {
const Comp = define({
setup() {
provide('foo', 'foo')
const injection = inject('foo', null)
return () => injection
},
})

Comp.render()
expect(Comp.host.innerHTML).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 = define({
setup() {
expect(hasInjectionContext()).toBe(true)
return () => null
},
})

Comp.render()
})

it('should be true within app.runWithContext()', () => {
expect.assertions(1)
createVaporApp({}).runWithContext(() => {
expect(hasInjectionContext()).toBe(true)
})
})
})
})
Loading