apiInject.spec.ts 8.3 KB

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