Skip to content

Commit

Permalink
feat(runtime-vapor): provide and inject (#158)
Browse files Browse the repository at this point in the history
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
  • Loading branch information
ubugeeei and sxzz authored Mar 22, 2024
1 parent ed6b171 commit 5c9a151
Show file tree
Hide file tree
Showing 5 changed files with 541 additions and 6 deletions.
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

0 comments on commit 5c9a151

Please sign in to comment.