apiInject.spec.ts 12 KB

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