Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime-vapor): provide and inject #158

Merged
merged 8 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Loading