Skip to content

Commit 5c9a151

Browse files
ubugeeeisxzz
andauthored
feat(runtime-vapor): provide and inject (#158)
Co-authored-by: 三咲智子 Kevin Deng <[email protected]>
1 parent ed6b171 commit 5c9a151

File tree

5 files changed

+541
-6
lines changed

5 files changed

+541
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
// NOTE: This test is implemented based on the case of `runtime-core/__test__/apiInject.spec.ts`.
2+
3+
import {
4+
type InjectionKey,
5+
type Ref,
6+
createComponent,
7+
createTextNode,
8+
createVaporApp,
9+
getCurrentInstance,
10+
hasInjectionContext,
11+
inject,
12+
nextTick,
13+
provide,
14+
reactive,
15+
readonly,
16+
ref,
17+
renderEffect,
18+
setText,
19+
} from '../src'
20+
import { makeRender } from './_utils'
21+
22+
const define = makeRender<any>()
23+
24+
// reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
25+
describe('api: provide/inject', () => {
26+
it('string keys', () => {
27+
const Provider = define({
28+
setup() {
29+
provide('foo', 1)
30+
return createComponent(Middle)
31+
},
32+
})
33+
34+
const Middle = {
35+
render() {
36+
return createComponent(Consumer)
37+
},
38+
}
39+
40+
const Consumer = {
41+
setup() {
42+
const foo = inject('foo')
43+
return (() => {
44+
const n0 = createTextNode()
45+
setText(n0, foo)
46+
return n0
47+
})()
48+
},
49+
}
50+
51+
Provider.render()
52+
expect(Provider.host.innerHTML).toBe('1')
53+
})
54+
55+
it('symbol keys', () => {
56+
// also verifies InjectionKey type sync
57+
const key: InjectionKey<number> = Symbol()
58+
59+
const Provider = define({
60+
setup() {
61+
provide(key, 1)
62+
return createComponent(Middle)
63+
},
64+
})
65+
66+
const Middle = {
67+
render: () => createComponent(Consumer),
68+
}
69+
70+
const Consumer = {
71+
setup() {
72+
const foo = inject(key)
73+
return (() => {
74+
const n0 = createTextNode()
75+
setText(n0, foo)
76+
return n0
77+
})()
78+
},
79+
}
80+
81+
Provider.render()
82+
expect(Provider.host.innerHTML).toBe('1')
83+
})
84+
85+
it('default values', () => {
86+
const Provider = define({
87+
setup() {
88+
provide('foo', 'foo')
89+
return createComponent(Middle)
90+
},
91+
})
92+
93+
const Middle = {
94+
render: () => createComponent(Consumer),
95+
}
96+
97+
const Consumer = {
98+
setup() {
99+
// default value should be ignored if value is provided
100+
const foo = inject('foo', 'fooDefault')
101+
// default value should be used if value is not provided
102+
const bar = inject('bar', 'bar')
103+
return (() => {
104+
const n0 = createTextNode()
105+
setText(n0, foo + bar)
106+
return n0
107+
})()
108+
},
109+
}
110+
111+
Provider.render()
112+
expect(Provider.host.innerHTML).toBe('foobar')
113+
})
114+
115+
// NOTE: Options API is not supported
116+
// it('bound to instance', () => {})
117+
118+
it('nested providers', () => {
119+
const ProviderOne = define({
120+
setup() {
121+
provide('foo', 'foo')
122+
provide('bar', 'bar')
123+
return createComponent(ProviderTwo)
124+
},
125+
})
126+
127+
const ProviderTwo = {
128+
setup() {
129+
// override parent value
130+
provide('foo', 'fooOverride')
131+
provide('baz', 'baz')
132+
return createComponent(Consumer)
133+
},
134+
}
135+
136+
const Consumer = {
137+
setup() {
138+
const foo = inject('foo')
139+
const bar = inject('bar')
140+
const baz = inject('baz')
141+
return (() => {
142+
const n0 = createTextNode()
143+
setText(n0, [foo, bar, baz].join(','))
144+
return n0
145+
})()
146+
},
147+
}
148+
149+
ProviderOne.render()
150+
expect(ProviderOne.host.innerHTML).toBe('fooOverride,bar,baz')
151+
})
152+
153+
it('reactivity with refs', async () => {
154+
const count = ref(1)
155+
156+
const Provider = define({
157+
setup() {
158+
provide('count', count)
159+
return createComponent(Middle)
160+
},
161+
})
162+
163+
const Middle = {
164+
render: () => createComponent(Consumer),
165+
}
166+
167+
const Consumer = {
168+
setup() {
169+
const count = inject<Ref<number>>('count')!
170+
return (() => {
171+
const n0 = createTextNode()
172+
renderEffect(() => {
173+
setText(n0, count.value)
174+
})
175+
return n0
176+
})()
177+
},
178+
}
179+
180+
Provider.render()
181+
expect(Provider.host.innerHTML).toBe('1')
182+
183+
count.value++
184+
await nextTick()
185+
expect(Provider.host.innerHTML).toBe('2')
186+
})
187+
188+
it('reactivity with readonly refs', async () => {
189+
const count = ref(1)
190+
191+
const Provider = define({
192+
setup() {
193+
provide('count', readonly(count))
194+
return createComponent(Middle)
195+
},
196+
})
197+
198+
const Middle = {
199+
render: () => createComponent(Consumer),
200+
}
201+
202+
const Consumer = {
203+
setup() {
204+
const count = inject<Ref<number>>('count')!
205+
// should not work
206+
count.value++
207+
return (() => {
208+
const n0 = createTextNode()
209+
renderEffect(() => {
210+
setText(n0, count.value)
211+
})
212+
return n0
213+
})()
214+
},
215+
}
216+
217+
Provider.render()
218+
expect(Provider.host.innerHTML).toBe('1')
219+
220+
expect(
221+
`Set operation on key "value" failed: target is readonly`,
222+
).toHaveBeenWarned()
223+
224+
count.value++
225+
await nextTick()
226+
expect(Provider.host.innerHTML).toBe('2')
227+
})
228+
229+
it('reactivity with objects', async () => {
230+
const rootState = reactive({ count: 1 })
231+
232+
const Provider = define({
233+
setup() {
234+
provide('state', rootState)
235+
return createComponent(Middle)
236+
},
237+
})
238+
239+
const Middle = {
240+
render: () => createComponent(Consumer),
241+
}
242+
243+
const Consumer = {
244+
setup() {
245+
const state = inject<typeof rootState>('state')!
246+
return (() => {
247+
const n0 = createTextNode()
248+
renderEffect(() => {
249+
setText(n0, state.count)
250+
})
251+
return n0
252+
})()
253+
},
254+
}
255+
256+
Provider.render()
257+
expect(Provider.host.innerHTML).toBe('1')
258+
259+
rootState.count++
260+
await nextTick()
261+
expect(Provider.host.innerHTML).toBe('2')
262+
})
263+
264+
it('reactivity with readonly objects', async () => {
265+
const rootState = reactive({ count: 1 })
266+
267+
const Provider = define({
268+
setup() {
269+
provide('state', readonly(rootState))
270+
return createComponent(Middle)
271+
},
272+
})
273+
274+
const Middle = {
275+
render: () => createComponent(Consumer),
276+
}
277+
278+
const Consumer = {
279+
setup() {
280+
const state = inject<typeof rootState>('state')!
281+
// should not work
282+
state.count++
283+
return (() => {
284+
const n0 = createTextNode()
285+
renderEffect(() => {
286+
setText(n0, state.count)
287+
})
288+
return n0
289+
})()
290+
},
291+
}
292+
293+
Provider.render()
294+
expect(Provider.host.innerHTML).toBe('1')
295+
296+
expect(
297+
`Set operation on key "count" failed: target is readonly`,
298+
).toHaveBeenWarned()
299+
300+
rootState.count++
301+
await nextTick()
302+
expect(Provider.host.innerHTML).toBe('2')
303+
})
304+
305+
it('should warn unfound', () => {
306+
const Provider = define({
307+
setup() {
308+
return createComponent(Middle)
309+
},
310+
})
311+
312+
const Middle = {
313+
render: () => createComponent(Consumer),
314+
}
315+
316+
const Consumer = {
317+
setup() {
318+
const foo = inject('foo')
319+
expect(foo).toBeUndefined()
320+
return (() => {
321+
const n0 = createTextNode()
322+
setText(n0, foo)
323+
return n0
324+
})()
325+
},
326+
}
327+
328+
Provider.render()
329+
expect(Provider.host.innerHTML).toBe('')
330+
expect(`injection "foo" not found.`).toHaveBeenWarned()
331+
})
332+
333+
it('should not warn when default value is undefined', () => {
334+
const Provider = define({
335+
setup() {
336+
return createComponent(Middle)
337+
},
338+
})
339+
340+
const Middle = {
341+
render: () => createComponent(Consumer),
342+
}
343+
344+
const Consumer = {
345+
setup() {
346+
const foo = inject('foo', undefined)
347+
return (() => {
348+
const n0 = createTextNode()
349+
setText(n0, foo)
350+
return n0
351+
})()
352+
},
353+
}
354+
355+
Provider.render()
356+
expect(`injection "foo" not found.`).not.toHaveBeenWarned()
357+
})
358+
359+
// #2400
360+
it.todo('should not self-inject', () => {
361+
const Comp = define({
362+
setup() {
363+
provide('foo', 'foo')
364+
const injection = inject('foo', null)
365+
return () => injection
366+
},
367+
})
368+
369+
Comp.render()
370+
expect(Comp.host.innerHTML).toBe('')
371+
})
372+
373+
describe('hasInjectionContext', () => {
374+
it('should be false outside of setup', () => {
375+
expect(hasInjectionContext()).toBe(false)
376+
})
377+
378+
it('should be true within setup', () => {
379+
expect.assertions(1)
380+
const Comp = define({
381+
setup() {
382+
expect(hasInjectionContext()).toBe(true)
383+
return () => null
384+
},
385+
})
386+
387+
Comp.render()
388+
})
389+
390+
it('should be true within app.runWithContext()', () => {
391+
expect.assertions(1)
392+
createVaporApp({}).runWithContext(() => {
393+
expect(hasInjectionContext()).toBe(true)
394+
})
395+
})
396+
})
397+
})

0 commit comments

Comments
 (0)