apiInject.spec.ts 9.0 KB

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