vdomInterop.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. import {
  2. KeepAlive,
  3. type ShallowRef,
  4. createVNode,
  5. defineComponent,
  6. h,
  7. inject,
  8. nextTick,
  9. onActivated,
  10. onBeforeMount,
  11. onDeactivated,
  12. onMounted,
  13. onUnmounted,
  14. provide,
  15. ref,
  16. renderSlot,
  17. resolveDynamicComponent,
  18. shallowRef,
  19. toDisplayString,
  20. useModel,
  21. useTemplateRef,
  22. } from '@vue/runtime-dom'
  23. import { makeInteropRender } from './_utils'
  24. import {
  25. applyTextModel,
  26. applyVShow,
  27. child,
  28. createComponent,
  29. defineVaporAsyncComponent,
  30. defineVaporComponent,
  31. renderEffect,
  32. setText,
  33. template,
  34. } from '../src'
  35. const define = makeInteropRender()
  36. describe('vdomInterop', () => {
  37. describe('props', () => {
  38. test('should work if props are not provided', () => {
  39. const VaporChild = defineVaporComponent({
  40. props: {
  41. msg: String,
  42. },
  43. setup(_, { attrs }) {
  44. return [document.createTextNode(attrs.class || 'foo')]
  45. },
  46. })
  47. const { html } = define({
  48. setup() {
  49. return () => h(VaporChild as any)
  50. },
  51. }).render()
  52. expect(html()).toBe('foo')
  53. })
  54. test('should handle class prop when vapor renders vdom component', () => {
  55. const VDomChild = defineComponent({
  56. setup() {
  57. return () => h('div', { class: 'foo' })
  58. },
  59. })
  60. const VaporChild = defineVaporComponent({
  61. setup() {
  62. return createComponent(VDomChild as any, { class: () => 'bar' })
  63. },
  64. })
  65. const { html } = define({
  66. setup() {
  67. return () => h(VaporChild as any)
  68. },
  69. }).render()
  70. expect(html()).toBe('<div class="foo bar"></div>')
  71. })
  72. })
  73. describe('v-model', () => {
  74. test('basic work', async () => {
  75. const VaporChild = defineVaporComponent({
  76. props: {
  77. modelValue: {},
  78. modelModifiers: {},
  79. },
  80. emits: ['update:modelValue'],
  81. setup(__props) {
  82. const modelValue = useModel(__props, 'modelValue')
  83. const n0 = template('<h1> </h1>')() as any
  84. const n1 = template('<input>')() as any
  85. const x0 = child(n0) as any
  86. applyTextModel(
  87. n1,
  88. () => modelValue.value,
  89. _value => (modelValue.value = _value),
  90. )
  91. renderEffect(() => setText(x0, toDisplayString(modelValue.value)))
  92. return [n0, n1]
  93. },
  94. })
  95. const { html, host } = define({
  96. setup() {
  97. const msg = ref('foo')
  98. return () =>
  99. h(VaporChild as any, {
  100. modelValue: msg.value,
  101. 'onUpdate:modelValue': (value: string) => {
  102. msg.value = value
  103. },
  104. })
  105. },
  106. }).render()
  107. expect(html()).toBe('<h1>foo</h1><input>')
  108. const inputEl = host.querySelector('input')!
  109. inputEl.value = 'bar'
  110. inputEl.dispatchEvent(new Event('input'))
  111. await nextTick()
  112. expect(html()).toBe('<h1>bar</h1><input>')
  113. })
  114. })
  115. describe('emit', () => {
  116. test('emit from vapor child to vdom parent', () => {
  117. const VaporChild = defineVaporComponent({
  118. emits: ['click'],
  119. setup(_, { emit }) {
  120. emit('click')
  121. return []
  122. },
  123. })
  124. const fn = vi.fn()
  125. define({
  126. setup() {
  127. return () => h(VaporChild as any, { onClick: fn })
  128. },
  129. }).render()
  130. // fn should be called once
  131. expect(fn).toHaveBeenCalledTimes(1)
  132. })
  133. })
  134. describe('v-show', () => {
  135. test('apply v-show to vdom child', async () => {
  136. const VDomChild = {
  137. setup() {
  138. return () => h('div')
  139. },
  140. }
  141. const show = ref(false)
  142. const VaporChild = defineVaporComponent({
  143. setup() {
  144. const n1 = createComponent(VDomChild as any)
  145. applyVShow(n1, () => show.value)
  146. return n1
  147. },
  148. })
  149. const { html } = define({
  150. setup() {
  151. return () => h(VaporChild as any)
  152. },
  153. }).render()
  154. expect(html()).toBe('<div style="display: none;"></div>')
  155. show.value = true
  156. await nextTick()
  157. expect(html()).toBe('<div style=""></div>')
  158. })
  159. })
  160. describe('slots', () => {
  161. test('basic', () => {
  162. const VDomChild = defineComponent({
  163. setup(_, { slots }) {
  164. return () => renderSlot(slots, 'default')
  165. },
  166. })
  167. const VaporChild = defineVaporComponent({
  168. setup() {
  169. return createComponent(
  170. VDomChild as any,
  171. null,
  172. {
  173. default: () => document.createTextNode('default slot'),
  174. },
  175. true,
  176. )
  177. },
  178. })
  179. const { html } = define({
  180. setup() {
  181. return () => h(VaporChild as any)
  182. },
  183. }).render()
  184. expect(html()).toBe('default slot')
  185. })
  186. test('functional slot', () => {
  187. const VDomChild = defineComponent({
  188. setup(_, { slots }) {
  189. return () => createVNode(slots.default!)
  190. },
  191. })
  192. const VaporChild = defineVaporComponent({
  193. setup() {
  194. return createComponent(
  195. VDomChild as any,
  196. null,
  197. {
  198. default: () => document.createTextNode('default slot'),
  199. },
  200. true,
  201. )
  202. },
  203. })
  204. const { html } = define({
  205. setup() {
  206. return () => h(VaporChild as any)
  207. },
  208. }).render()
  209. expect(html()).toBe('default slot')
  210. })
  211. })
  212. describe('provide / inject', () => {
  213. it('should inject value from vdom parent', async () => {
  214. const VaporChild = defineVaporComponent({
  215. setup() {
  216. const foo = inject('foo')
  217. const n0 = template(' ')() as any
  218. renderEffect(() => setText(n0, toDisplayString(foo)))
  219. return n0
  220. },
  221. })
  222. const value = ref('foo')
  223. const { html } = define({
  224. setup() {
  225. provide('foo', value)
  226. return () => h(VaporChild as any)
  227. },
  228. }).render()
  229. expect(html()).toBe('foo')
  230. value.value = 'bar'
  231. await nextTick()
  232. expect(html()).toBe('bar')
  233. })
  234. })
  235. describe('template ref', () => {
  236. it('useTemplateRef with vapor child', async () => {
  237. const VaporChild = defineVaporComponent({
  238. setup(_, { expose }) {
  239. const foo = ref('foo')
  240. expose({ foo })
  241. const n0 = template(' ')() as any
  242. renderEffect(() => setText(n0, toDisplayString(foo)))
  243. return n0
  244. },
  245. })
  246. let elRef: ShallowRef
  247. const { html } = define({
  248. setup() {
  249. elRef = useTemplateRef('el')
  250. return () => h(VaporChild as any, { ref: 'el' })
  251. },
  252. }).render()
  253. expect(html()).toBe('foo')
  254. elRef!.value.foo = 'bar'
  255. await nextTick()
  256. expect(html()).toBe('bar')
  257. })
  258. it('static ref with vapor child', async () => {
  259. const VaporChild = defineVaporComponent({
  260. setup(_, { expose }) {
  261. const foo = ref('foo')
  262. expose({ foo })
  263. const n0 = template(' ')() as any
  264. renderEffect(() => setText(n0, toDisplayString(foo)))
  265. return n0
  266. },
  267. })
  268. let elRef: ShallowRef
  269. const { html } = define({
  270. setup() {
  271. elRef = shallowRef()
  272. return { elRef }
  273. },
  274. render() {
  275. return h(VaporChild as any, { ref: 'elRef' })
  276. },
  277. }).render()
  278. expect(html()).toBe('foo')
  279. elRef!.value.foo = 'bar'
  280. await nextTick()
  281. expect(html()).toBe('bar')
  282. })
  283. })
  284. describe('dynamic component', () => {
  285. it('dynamic component with vapor child', async () => {
  286. const VaporChild = defineVaporComponent({
  287. setup() {
  288. return template('<div>vapor child</div>')() as any
  289. },
  290. })
  291. const VdomChild = defineComponent({
  292. setup() {
  293. return () => h('div', 'vdom child')
  294. },
  295. })
  296. const view = shallowRef<any>(VaporChild)
  297. const { html } = define({
  298. setup() {
  299. return () => h(resolveDynamicComponent(view.value) as any)
  300. },
  301. }).render()
  302. expect(html()).toBe('<div>vapor child</div>')
  303. view.value = VdomChild
  304. await nextTick()
  305. expect(html()).toBe('<div>vdom child</div>')
  306. view.value = VaporChild
  307. await nextTick()
  308. expect(html()).toBe('<div>vapor child</div>')
  309. })
  310. })
  311. describe('attribute fallthrough', () => {
  312. it('should fallthrough attrs to vdom child', () => {
  313. const VDomChild = defineComponent({
  314. setup() {
  315. return () => h('div')
  316. },
  317. })
  318. const VaporChild = defineVaporComponent({
  319. setup() {
  320. return createComponent(
  321. VDomChild as any,
  322. { foo: () => 'vapor foo' },
  323. null,
  324. true,
  325. )
  326. },
  327. })
  328. const { html } = define({
  329. setup() {
  330. return () => h(VaporChild as any, { foo: 'foo', bar: 'bar' })
  331. },
  332. }).render()
  333. expect(html()).toBe('<div foo="foo" bar="bar"></div>')
  334. })
  335. it('should not fallthrough emit handlers to vdom child', () => {
  336. const VDomChild = defineComponent({
  337. emits: ['click'],
  338. setup(_, { emit }) {
  339. return () => h('button', { onClick: () => emit('click') }, 'click me')
  340. },
  341. })
  342. const fn = vi.fn()
  343. const VaporChild = defineVaporComponent({
  344. emits: ['click'],
  345. setup() {
  346. return createComponent(
  347. VDomChild as any,
  348. { onClick: () => fn },
  349. null,
  350. true,
  351. )
  352. },
  353. })
  354. const { host, html } = define({
  355. setup() {
  356. return () => h(VaporChild as any)
  357. },
  358. }).render()
  359. expect(html()).toBe('<button>click me</button>')
  360. const button = host.querySelector('button')!
  361. button.dispatchEvent(new Event('click'))
  362. // fn should be called once
  363. expect(fn).toHaveBeenCalledTimes(1)
  364. })
  365. })
  366. describe('async component', () => {
  367. const duration = 5
  368. test('render vapor async component', async () => {
  369. const VdomChild = {
  370. setup() {
  371. return () => h('div', 'foo')
  372. },
  373. }
  374. const VaporAsyncChild = defineVaporAsyncComponent({
  375. loader: () => {
  376. return new Promise(r => {
  377. setTimeout(() => {
  378. r(VdomChild as any)
  379. }, duration)
  380. })
  381. },
  382. loadingComponent: () => h('span', 'loading...'),
  383. })
  384. const { html } = define({
  385. setup() {
  386. return () => h(VaporAsyncChild as any)
  387. },
  388. }).render()
  389. expect(html()).toBe('<span>loading...</span><!--async component-->')
  390. await new Promise(r => setTimeout(r, duration))
  391. await nextTick()
  392. expect(html()).toBe('<div>foo</div><!--async component-->')
  393. })
  394. })
  395. describe('keepalive', () => {
  396. function assertHookCalls(
  397. hooks: {
  398. beforeMount: any
  399. mounted: any
  400. activated: any
  401. deactivated: any
  402. unmounted: any
  403. },
  404. callCounts: number[],
  405. ) {
  406. expect([
  407. hooks.beforeMount.mock.calls.length,
  408. hooks.mounted.mock.calls.length,
  409. hooks.activated.mock.calls.length,
  410. hooks.deactivated.mock.calls.length,
  411. hooks.unmounted.mock.calls.length,
  412. ]).toEqual(callCounts)
  413. }
  414. let hooks: any
  415. beforeEach(() => {
  416. hooks = {
  417. beforeMount: vi.fn(),
  418. mounted: vi.fn(),
  419. activated: vi.fn(),
  420. deactivated: vi.fn(),
  421. unmounted: vi.fn(),
  422. }
  423. })
  424. test('render vapor component', async () => {
  425. const VaporChild = defineVaporComponent({
  426. setup() {
  427. const msg = ref('vapor')
  428. onBeforeMount(() => hooks.beforeMount())
  429. onMounted(() => hooks.mounted())
  430. onActivated(() => hooks.activated())
  431. onDeactivated(() => hooks.deactivated())
  432. onUnmounted(() => hooks.unmounted())
  433. const n0 = template('<input type="text">', true)() as any
  434. applyTextModel(
  435. n0,
  436. () => msg.value,
  437. _value => (msg.value = _value),
  438. )
  439. return n0
  440. },
  441. })
  442. const show = ref(true)
  443. const toggle = ref(true)
  444. const { html, host } = define({
  445. setup() {
  446. return () =>
  447. show.value
  448. ? h(KeepAlive, null, {
  449. default: () => (toggle.value ? h(VaporChild as any) : null),
  450. })
  451. : null
  452. },
  453. }).render()
  454. expect(html()).toBe('<input type="text">')
  455. let inputEl = host.firstChild as HTMLInputElement
  456. expect(inputEl.value).toBe('vapor')
  457. assertHookCalls(hooks, [1, 1, 1, 0, 0])
  458. // change input value
  459. inputEl.value = 'changed'
  460. inputEl.dispatchEvent(new Event('input'))
  461. await nextTick()
  462. // deactivate
  463. toggle.value = false
  464. await nextTick()
  465. expect(html()).toBe('<!---->')
  466. assertHookCalls(hooks, [1, 1, 1, 1, 0])
  467. // activate
  468. toggle.value = true
  469. await nextTick()
  470. expect(html()).toBe('<input type="text">')
  471. inputEl = host.firstChild as HTMLInputElement
  472. expect(inputEl.value).toBe('changed')
  473. assertHookCalls(hooks, [1, 1, 2, 1, 0])
  474. // unmount keepalive
  475. show.value = false
  476. await nextTick()
  477. expect(html()).toBe('<!---->')
  478. assertHookCalls(hooks, [1, 1, 2, 2, 1])
  479. // mount keepalive
  480. show.value = true
  481. await nextTick()
  482. inputEl = host.firstChild as HTMLInputElement
  483. expect(inputEl.value).toBe('vapor')
  484. assertHookCalls(hooks, [2, 2, 3, 2, 1])
  485. })
  486. })
  487. })