apiInject.spec.ts 8.5 KB

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