apiInject.spec.ts 12 KB

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