component.spec.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import {
  2. type Ref,
  3. inject,
  4. nextTick,
  5. onMounted,
  6. onUpdated,
  7. provide,
  8. ref,
  9. watch,
  10. watchEffect,
  11. } from '@vue/runtime-dom'
  12. import {
  13. createComponent,
  14. createIf,
  15. createTextNode,
  16. renderEffect,
  17. setInsertionState,
  18. template,
  19. } from '../src'
  20. import { makeRender } from './_utils'
  21. import type { VaporComponentInstance } from '../src/component'
  22. import { setElementText, setText } from '../src/dom/prop'
  23. const define = makeRender()
  24. describe('component', () => {
  25. it('should update parent(hoc) component host el when child component self update', async () => {
  26. const value = ref(true)
  27. let childNode1: Node | null = null
  28. let childNode2: Node | null = null
  29. const { component: Child } = define({
  30. setup() {
  31. return createIf(
  32. () => value.value,
  33. () => (childNode1 = template('<div></div>')()),
  34. () => (childNode2 = template('<span></span>')()),
  35. )
  36. },
  37. })
  38. const { host } = define({
  39. setup() {
  40. return createComponent(Child)
  41. },
  42. }).render()
  43. expect(host.innerHTML).toBe('<div></div><!--if-->')
  44. expect(host.children[0]).toBe(childNode1)
  45. value.value = false
  46. await nextTick()
  47. expect(host.innerHTML).toBe('<span></span><!--if-->')
  48. expect(host.children[0]).toBe(childNode2)
  49. })
  50. it('should create a component with props', () => {
  51. const { component: Comp } = define({
  52. setup() {
  53. return template('<div>', true)()
  54. },
  55. })
  56. const { host } = define({
  57. setup() {
  58. return createComponent(Comp, { id: () => 'foo', class: () => 'bar' })
  59. },
  60. }).render()
  61. expect(host.innerHTML).toBe('<div id="foo" class="bar"></div>')
  62. })
  63. it('should not update Component if only changed props are declared emit listeners', async () => {
  64. const updatedSyp = vi.fn()
  65. const { component: Comp } = define({
  66. emits: ['foo'],
  67. setup() {
  68. onUpdated(updatedSyp)
  69. return template('<div>', true)()
  70. },
  71. })
  72. const toggle = ref(true)
  73. const fn1 = () => {}
  74. const fn2 = () => {}
  75. define({
  76. setup() {
  77. const _on_foo = () => (toggle.value ? fn1() : fn2())
  78. return createComponent(Comp, { onFoo: () => _on_foo })
  79. },
  80. }).render()
  81. expect(updatedSyp).toHaveBeenCalledTimes(0)
  82. toggle.value = false
  83. await nextTick()
  84. expect(updatedSyp).toHaveBeenCalledTimes(0)
  85. })
  86. it('component child synchronously updating parent state should trigger parent re-render', async () => {
  87. const { component: Child } = define({
  88. setup() {
  89. const n = inject<Ref<number>>('foo')!
  90. n.value++
  91. const n0 = template('<div></div>')()
  92. renderEffect(() => setElementText(n0, n.value))
  93. return n0
  94. },
  95. })
  96. const { host } = define({
  97. setup() {
  98. const n = ref(0)
  99. provide('foo', n)
  100. const n0 = template('<div></div>')()
  101. renderEffect(() => setElementText(n0, n.value))
  102. return [n0, createComponent(Child)]
  103. },
  104. }).render()
  105. expect(host.innerHTML).toBe('<div>0</div><div>1</div>')
  106. await nextTick()
  107. expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
  108. })
  109. it('component child updating parent state in pre-flush should trigger parent re-render', async () => {
  110. const { component: Child } = define({
  111. props: ['value'],
  112. setup(props: any, { emit }) {
  113. watch(
  114. () => props.value,
  115. val => emit('update', val),
  116. )
  117. const n0 = template('<div></div>')()
  118. renderEffect(() => setElementText(n0, props.value))
  119. return n0
  120. },
  121. })
  122. const outer = ref(0)
  123. const { host } = define({
  124. setup() {
  125. const inner = ref(0)
  126. const n0 = template('<div></div>')()
  127. renderEffect(() => setElementText(n0, inner.value))
  128. const n1 = createComponent(Child, {
  129. value: () => outer.value,
  130. onUpdate: () => (val: number) => (inner.value = val),
  131. })
  132. return [n0, n1]
  133. },
  134. }).render()
  135. expect(host.innerHTML).toBe('<div>0</div><div>0</div>')
  136. outer.value++
  137. await nextTick()
  138. expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
  139. })
  140. it('child only updates once when triggered in multiple ways', async () => {
  141. const a = ref(0)
  142. const calls: string[] = []
  143. const { component: Child } = define({
  144. props: ['count'],
  145. setup(props: any) {
  146. onUpdated(() => calls.push('update child'))
  147. const n = createTextNode()
  148. renderEffect(() => {
  149. setText(n, `${props.count} - ${a.value}`)
  150. })
  151. return n
  152. },
  153. })
  154. const { host } = define({
  155. setup() {
  156. return createComponent(Child, { count: () => a.value })
  157. },
  158. }).render()
  159. expect(host.innerHTML).toBe('0 - 0')
  160. expect(calls).toEqual([])
  161. // This will trigger child rendering directly, as well as via a prop change
  162. a.value++
  163. await nextTick()
  164. expect(host.innerHTML).toBe('1 - 1')
  165. expect(calls).toEqual(['update child'])
  166. })
  167. it(`an earlier update doesn't lead to excessive subsequent updates`, async () => {
  168. const globalCount = ref(0)
  169. const parentCount = ref(0)
  170. const calls: string[] = []
  171. const { component: Child } = define({
  172. props: ['count'],
  173. setup(props: any) {
  174. watch(
  175. () => props.count,
  176. () => {
  177. calls.push('child watcher')
  178. globalCount.value = props.count
  179. },
  180. )
  181. onUpdated(() => calls.push('update child'))
  182. return []
  183. },
  184. })
  185. const { component: Parent } = define({
  186. props: ['count'],
  187. setup(props: any) {
  188. onUpdated(() => calls.push('update parent'))
  189. const n1 = createTextNode()
  190. const n2 = createComponent(Child, { count: () => parentCount.value })
  191. renderEffect(() => {
  192. setText(n1, `${globalCount.value} - ${props.count}`)
  193. })
  194. return [n1, n2]
  195. },
  196. })
  197. const { host } = define({
  198. setup() {
  199. onUpdated(() => calls.push('update root'))
  200. return createComponent(Parent, { count: () => globalCount.value })
  201. },
  202. }).render()
  203. expect(host.innerHTML).toBe(`0 - 0`)
  204. expect(calls).toEqual([])
  205. parentCount.value++
  206. await nextTick()
  207. expect(host.innerHTML).toBe(`1 - 1`)
  208. expect(calls).toEqual(['child watcher', 'update parent'])
  209. })
  210. it('child component props update should not lead to double update', async () => {
  211. const text = ref(0)
  212. const spy = vi.fn()
  213. const { component: Comp } = define({
  214. props: ['text'],
  215. setup(props: any) {
  216. const n1 = template('<h1></h1>')()
  217. renderEffect(() => {
  218. spy()
  219. setElementText(n1, props.text)
  220. })
  221. return n1
  222. },
  223. })
  224. const { host } = define({
  225. setup() {
  226. return createComponent(Comp, { text: () => text.value })
  227. },
  228. }).render()
  229. expect(host.innerHTML).toBe('<h1>0</h1>')
  230. expect(spy).toHaveBeenCalledTimes(1)
  231. text.value++
  232. await nextTick()
  233. expect(host.innerHTML).toBe('<h1>1</h1>')
  234. expect(spy).toHaveBeenCalledTimes(2)
  235. })
  236. it('properly mount child component when using setInsertionState', async () => {
  237. const spy = vi.fn()
  238. const { component: Comp } = define({
  239. setup() {
  240. onMounted(spy)
  241. return template('<h1>hi</h1>')()
  242. },
  243. })
  244. const { host } = define({
  245. setup() {
  246. const n2 = template('<div></div>', true)()
  247. setInsertionState(n2 as any)
  248. createComponent(Comp)
  249. return n2
  250. },
  251. }).render()
  252. expect(host.innerHTML).toBe('<div><h1>hi</h1></div>')
  253. expect(spy).toHaveBeenCalledTimes(1)
  254. })
  255. it('unmount component', async () => {
  256. const { host, app, instance } = define(() => {
  257. const count = ref(0)
  258. const t0 = template('<div></div>')
  259. const n0 = t0()
  260. watchEffect(() => {
  261. setElementText(n0, count.value)
  262. })
  263. renderEffect(() => {})
  264. return n0
  265. }).render()
  266. const i = instance as VaporComponentInstance
  267. // watchEffect + renderEffect + props validation effect
  268. expect(i.scope.effects.length).toBe(3)
  269. expect(host.innerHTML).toBe('<div>0</div>')
  270. app.unmount()
  271. expect(host.innerHTML).toBe('')
  272. expect(i.scope.effects.length).toBe(0)
  273. })
  274. test('should mount component only with template in production mode', () => {
  275. __DEV__ = false
  276. const { component: Child } = define({
  277. render() {
  278. return template('<div> HI </div>', true)()
  279. },
  280. })
  281. const { host } = define({
  282. setup() {
  283. return createComponent(Child, null, null, true)
  284. },
  285. }).render()
  286. expect(host.innerHTML).toBe('<div> HI </div>')
  287. __DEV__ = true
  288. })
  289. it('warn if functional vapor component not return a block', () => {
  290. define(() => {
  291. return () => {}
  292. }).render()
  293. expect(
  294. 'Functional vapor component must return a block directly',
  295. ).toHaveBeenWarned()
  296. })
  297. it('warn if setup return a function and no render function', () => {
  298. define({
  299. setup() {
  300. return () => []
  301. },
  302. }).render()
  303. expect(
  304. 'Vapor component setup() returned non-block value, and has no render function',
  305. ).toHaveBeenWarned()
  306. })
  307. })