apiInject.spec.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import {
  2. h,
  3. provide,
  4. inject,
  5. InjectionKey,
  6. ref,
  7. nextTick,
  8. Ref,
  9. readonly,
  10. reactive,
  11. defineComponent,
  12. hasInjectionContext
  13. } from '../src/index'
  14. import { render, nodeOps, serialize, createApp } from '@vue/runtime-test'
  15. // reference: https://vue-composition-api-rfc.netlify.com/api.html#provide-inject
  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. // @ts-ignore
  100. return this.foo
  101. }
  102. })
  103. const root = nodeOps.createElement('div')
  104. render(h(Provider), root)
  105. expect(serialize(root)).toBe(`<div>Consumer</div>`)
  106. })
  107. it('nested providers', () => {
  108. const ProviderOne = {
  109. setup() {
  110. provide('foo', 'foo')
  111. provide('bar', 'bar')
  112. return () => h(ProviderTwo)
  113. }
  114. }
  115. const ProviderTwo = {
  116. setup() {
  117. // override parent value
  118. provide('foo', 'fooOverride')
  119. provide('baz', 'baz')
  120. return () => h(Consumer)
  121. }
  122. }
  123. const Consumer = {
  124. setup() {
  125. const foo = inject('foo')
  126. const bar = inject('bar')
  127. const baz = inject('baz')
  128. return () => [foo, bar, baz].join(',')
  129. }
  130. }
  131. const root = nodeOps.createElement('div')
  132. render(h(ProviderOne), root)
  133. expect(serialize(root)).toBe(`<div>fooOverride,bar,baz</div>`)
  134. })
  135. it('reactivity with refs', async () => {
  136. const count = ref(1)
  137. const Provider = {
  138. setup() {
  139. provide('count', count)
  140. return () => h(Middle)
  141. }
  142. }
  143. const Middle = {
  144. render: () => h(Consumer)
  145. }
  146. const Consumer = {
  147. setup() {
  148. const count = inject<Ref<number>>('count')!
  149. return () => count.value
  150. }
  151. }
  152. const root = nodeOps.createElement('div')
  153. render(h(Provider), root)
  154. expect(serialize(root)).toBe(`<div>1</div>`)
  155. count.value++
  156. await nextTick()
  157. expect(serialize(root)).toBe(`<div>2</div>`)
  158. })
  159. it('reactivity with readonly refs', async () => {
  160. const count = ref(1)
  161. const Provider = {
  162. setup() {
  163. provide('count', readonly(count))
  164. return () => h(Middle)
  165. }
  166. }
  167. const Middle = {
  168. render: () => h(Consumer)
  169. }
  170. const Consumer = {
  171. setup() {
  172. const count = inject<Ref<number>>('count')!
  173. // should not work
  174. count.value++
  175. return () => count.value
  176. }
  177. }
  178. const root = nodeOps.createElement('div')
  179. render(h(Provider), root)
  180. expect(serialize(root)).toBe(`<div>1</div>`)
  181. expect(
  182. `Set operation on key "value" failed: target is readonly`
  183. ).toHaveBeenWarned()
  184. // source mutation should still work
  185. count.value++
  186. await nextTick()
  187. expect(serialize(root)).toBe(`<div>2</div>`)
  188. })
  189. it('reactivity with objects', async () => {
  190. const rootState = reactive({ count: 1 })
  191. const Provider = {
  192. setup() {
  193. provide('state', rootState)
  194. return () => h(Middle)
  195. }
  196. }
  197. const Middle = {
  198. render: () => h(Consumer)
  199. }
  200. const Consumer = {
  201. setup() {
  202. const state = inject<typeof rootState>('state')!
  203. return () => state.count
  204. }
  205. }
  206. const root = nodeOps.createElement('div')
  207. render(h(Provider), root)
  208. expect(serialize(root)).toBe(`<div>1</div>`)
  209. rootState.count++
  210. await nextTick()
  211. expect(serialize(root)).toBe(`<div>2</div>`)
  212. })
  213. it('reactivity with readonly objects', async () => {
  214. const rootState = reactive({ count: 1 })
  215. const Provider = {
  216. setup() {
  217. provide('state', readonly(rootState))
  218. return () => h(Middle)
  219. }
  220. }
  221. const Middle = {
  222. render: () => h(Consumer)
  223. }
  224. const Consumer = {
  225. setup() {
  226. const state = inject<typeof rootState>('state')!
  227. // should not work
  228. state.count++
  229. return () => state.count
  230. }
  231. }
  232. const root = nodeOps.createElement('div')
  233. render(h(Provider), root)
  234. expect(serialize(root)).toBe(`<div>1</div>`)
  235. expect(
  236. `Set operation on key "count" failed: target is readonly`
  237. ).toHaveBeenWarned()
  238. rootState.count++
  239. await nextTick()
  240. expect(serialize(root)).toBe(`<div>2</div>`)
  241. })
  242. it('should warn unfound', () => {
  243. const Provider = {
  244. setup() {
  245. return () => h(Middle)
  246. }
  247. }
  248. const Middle = {
  249. render: () => h(Consumer)
  250. }
  251. const Consumer = {
  252. setup() {
  253. const foo = inject('foo')
  254. expect(foo).toBeUndefined()
  255. return () => foo
  256. }
  257. }
  258. const root = nodeOps.createElement('div')
  259. render(h(Provider), root)
  260. expect(serialize(root)).toBe(`<div><!----></div>`)
  261. expect(`injection "foo" not found.`).toHaveBeenWarned()
  262. })
  263. it('should not warn when default value is undefined', () => {
  264. const Provider = {
  265. setup() {
  266. return () => h(Middle)
  267. }
  268. }
  269. const Middle = {
  270. render: () => h(Consumer)
  271. }
  272. const Consumer = {
  273. setup() {
  274. const foo = inject('foo', undefined)
  275. return () => foo
  276. }
  277. }
  278. const root = nodeOps.createElement('div')
  279. render(h(Provider), root)
  280. expect(`injection "foo" not found.`).not.toHaveBeenWarned()
  281. })
  282. // #2400
  283. it('should not self-inject', () => {
  284. const Comp = {
  285. setup() {
  286. provide('foo', 'foo')
  287. const injection = inject('foo', null)
  288. return () => injection
  289. }
  290. }
  291. const root = nodeOps.createElement('div')
  292. render(h(Comp), root)
  293. expect(serialize(root)).toBe(`<div><!----></div>`)
  294. })
  295. describe('hasInjectionContext', () => {
  296. it('should be false outside of setup', () => {
  297. expect(hasInjectionContext()).toBe(false)
  298. })
  299. it('should be true within setup', () => {
  300. expect.assertions(1)
  301. const Comp = {
  302. setup() {
  303. expect(hasInjectionContext()).toBe(true)
  304. return () => null
  305. }
  306. }
  307. const root = nodeOps.createElement('div')
  308. render(h(Comp), root)
  309. })
  310. it('should be true within app.runWithContext()', () => {
  311. expect.assertions(1)
  312. createApp({}).runWithContext(() => {
  313. expect(hasInjectionContext()).toBe(true)
  314. })
  315. })
  316. })
  317. })