apiInject.spec.ts 9.1 KB

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