apiInject.spec.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import {
  2. type InjectionKey,
  3. type Ref,
  4. defineComponent,
  5. h,
  6. hasInjectionContext,
  7. inject,
  8. nextTick,
  9. provide,
  10. reactive,
  11. readonly,
  12. ref,
  13. } from '../src/index'
  14. import { createApp, nodeOps, render, serialize } from '@vue/runtime-test'
  15. describe('api: provide/inject', () => {
  16. it('string keys', () => {
  17. const Provider = {
  18. setup() {
  19. provide('foo', 1)
  20. return () => h(Middle)
  21. },
  22. }
  23. const Middle = {
  24. render: () => h(Consumer),
  25. }
  26. const Consumer = {
  27. setup() {
  28. const foo = inject('foo')
  29. return () => foo
  30. },
  31. }
  32. const root = nodeOps.createElement('div')
  33. render(h(Provider), root)
  34. expect(serialize(root)).toBe(`<div>1</div>`)
  35. })
  36. it('symbol keys', () => {
  37. // also verifies InjectionKey type sync
  38. const key: InjectionKey<number> = Symbol()
  39. const Provider = {
  40. setup() {
  41. provide(key, 1)
  42. return () => h(Middle)
  43. },
  44. }
  45. const Middle = {
  46. render: () => h(Consumer),
  47. }
  48. const Consumer = {
  49. setup() {
  50. const foo = inject(key) || 1
  51. return () => foo + 1
  52. },
  53. }
  54. const root = nodeOps.createElement('div')
  55. render(h(Provider), root)
  56. expect(serialize(root)).toBe(`<div>2</div>`)
  57. })
  58. it('default values', () => {
  59. const Provider = {
  60. setup() {
  61. provide('foo', 'foo')
  62. return () => h(Middle)
  63. },
  64. }
  65. const Middle = {
  66. render: () => h(Consumer),
  67. }
  68. const Consumer = {
  69. setup() {
  70. // default value should be ignored if value is provided
  71. const foo = inject('foo', 'fooDefault')
  72. // default value should be used if value is not provided
  73. const bar = inject('bar', 'bar')
  74. return () => foo + bar
  75. },
  76. }
  77. const root = nodeOps.createElement('div')
  78. render(h(Provider), root)
  79. expect(serialize(root)).toBe(`<div>foobar</div>`)
  80. })
  81. it('bound to instance', () => {
  82. const Provider = {
  83. setup() {
  84. return () => h(Consumer)
  85. },
  86. }
  87. const Consumer = defineComponent({
  88. name: 'Consumer',
  89. inject: {
  90. foo: {
  91. from: 'foo',
  92. default() {
  93. return this!.$options.name
  94. },
  95. },
  96. },
  97. render() {
  98. return this.foo
  99. },
  100. })
  101. const root = nodeOps.createElement('div')
  102. render(h(Provider), root)
  103. expect(serialize(root)).toBe(`<div>Consumer</div>`)
  104. })
  105. it('nested providers', () => {
  106. const ProviderOne = {
  107. setup() {
  108. provide('foo', 'foo')
  109. provide('bar', 'bar')
  110. return () => h(ProviderTwo)
  111. },
  112. }
  113. const ProviderTwo = {
  114. setup() {
  115. // override parent value
  116. provide('foo', 'fooOverride')
  117. provide('baz', 'baz')
  118. return () => h(Consumer)
  119. },
  120. }
  121. const Consumer = {
  122. setup() {
  123. const foo = inject('foo')
  124. const bar = inject('bar')
  125. const baz = inject('baz')
  126. return () => [foo, bar, baz].join(',')
  127. },
  128. }
  129. const root = nodeOps.createElement('div')
  130. render(h(ProviderOne), root)
  131. expect(serialize(root)).toBe(`<div>fooOverride,bar,baz</div>`)
  132. })
  133. it('reactivity with refs', async () => {
  134. const count = ref(1)
  135. const Provider = {
  136. setup() {
  137. provide('count', count)
  138. return () => h(Middle)
  139. },
  140. }
  141. const Middle = {
  142. render: () => h(Consumer),
  143. }
  144. const Consumer = {
  145. setup() {
  146. const count = inject<Ref<number>>('count')!
  147. return () => count.value
  148. },
  149. }
  150. const root = nodeOps.createElement('div')
  151. render(h(Provider), root)
  152. expect(serialize(root)).toBe(`<div>1</div>`)
  153. count.value++
  154. await nextTick()
  155. expect(serialize(root)).toBe(`<div>2</div>`)
  156. })
  157. it('reactivity with readonly refs', async () => {
  158. const count = ref(1)
  159. const Provider = {
  160. setup() {
  161. provide('count', readonly(count))
  162. return () => h(Middle)
  163. },
  164. }
  165. const Middle = {
  166. render: () => h(Consumer),
  167. }
  168. const Consumer = {
  169. setup() {
  170. const count = inject<Ref<number>>('count')!
  171. // should not work
  172. count.value++
  173. return () => count.value
  174. },
  175. }
  176. const root = nodeOps.createElement('div')
  177. render(h(Provider), root)
  178. expect(serialize(root)).toBe(`<div>1</div>`)
  179. expect(
  180. `Set operation on key "value" failed: target is readonly`,
  181. ).toHaveBeenWarned()
  182. // source mutation should still work
  183. count.value++
  184. await nextTick()
  185. expect(serialize(root)).toBe(`<div>2</div>`)
  186. })
  187. it('reactivity with objects', async () => {
  188. const rootState = reactive({ count: 1 })
  189. const Provider = {
  190. setup() {
  191. provide('state', rootState)
  192. return () => h(Middle)
  193. },
  194. }
  195. const Middle = {
  196. render: () => h(Consumer),
  197. }
  198. const Consumer = {
  199. setup() {
  200. const state = inject<typeof rootState>('state')!
  201. return () => state.count
  202. },
  203. }
  204. const root = nodeOps.createElement('div')
  205. render(h(Provider), root)
  206. expect(serialize(root)).toBe(`<div>1</div>`)
  207. rootState.count++
  208. await nextTick()
  209. expect(serialize(root)).toBe(`<div>2</div>`)
  210. })
  211. it('reactivity with readonly objects', async () => {
  212. const rootState = reactive({ count: 1 })
  213. const Provider = {
  214. setup() {
  215. provide('state', readonly(rootState))
  216. return () => h(Middle)
  217. },
  218. }
  219. const Middle = {
  220. render: () => h(Consumer),
  221. }
  222. const Consumer = {
  223. setup() {
  224. const state = inject<typeof rootState>('state')!
  225. // should not work
  226. state.count++
  227. return () => state.count
  228. },
  229. }
  230. const root = nodeOps.createElement('div')
  231. render(h(Provider), root)
  232. expect(serialize(root)).toBe(`<div>1</div>`)
  233. expect(
  234. `Set operation on key "count" failed: target is readonly`,
  235. ).toHaveBeenWarned()
  236. rootState.count++
  237. await nextTick()
  238. expect(serialize(root)).toBe(`<div>2</div>`)
  239. })
  240. it('should warn unfound', () => {
  241. const Provider = {
  242. setup() {
  243. return () => h(Middle)
  244. },
  245. }
  246. const Middle = {
  247. render: () => h(Consumer),
  248. }
  249. const Consumer = {
  250. setup() {
  251. const foo = inject('foo')
  252. expect(foo).toBeUndefined()
  253. return () => foo
  254. },
  255. }
  256. const root = nodeOps.createElement('div')
  257. render(h(Provider), root)
  258. expect(serialize(root)).toBe(`<div><!----></div>`)
  259. expect(`injection "foo" not found.`).toHaveBeenWarned()
  260. })
  261. it('should not warn when default value is undefined', () => {
  262. const Provider = {
  263. setup() {
  264. return () => h(Middle)
  265. },
  266. }
  267. const Middle = {
  268. render: () => h(Consumer),
  269. }
  270. const Consumer = {
  271. setup() {
  272. const foo = inject('foo', undefined)
  273. return () => foo
  274. },
  275. }
  276. const root = nodeOps.createElement('div')
  277. render(h(Provider), root)
  278. expect(`injection "foo" not found.`).not.toHaveBeenWarned()
  279. })
  280. // #2400
  281. it('should not self-inject', () => {
  282. const Comp = {
  283. setup() {
  284. provide('foo', 'foo')
  285. const injection = inject('foo', null)
  286. return () => injection
  287. },
  288. }
  289. const root = nodeOps.createElement('div')
  290. render(h(Comp), root)
  291. expect(serialize(root)).toBe(`<div><!----></div>`)
  292. })
  293. describe('hasInjectionContext', () => {
  294. it('should be false outside of setup', () => {
  295. expect(hasInjectionContext()).toBe(false)
  296. })
  297. it('should be true within setup', () => {
  298. expect.assertions(1)
  299. const Comp = {
  300. setup() {
  301. expect(hasInjectionContext()).toBe(true)
  302. return () => null
  303. },
  304. }
  305. const root = nodeOps.createElement('div')
  306. render(h(Comp), root)
  307. })
  308. it('should be true within app.runWithContext()', () => {
  309. expect.assertions(1)
  310. createApp({}).runWithContext(() => {
  311. expect(hasInjectionContext()).toBe(true)
  312. })
  313. })
  314. })
  315. })