apiInject.spec.ts 9.9 KB

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