apiInject.spec.ts 8.3 KB

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