apiInject.spec.ts 11 KB

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