| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- import {
- type EffectScope,
- ReactiveEffect,
- type Ref,
- inject,
- nextTick,
- onMounted,
- onUpdated,
- provide,
- ref,
- useAttrs,
- watch,
- watchEffect,
- } from '@vue/runtime-dom'
- import {
- createComponent,
- createIf,
- createTextNode,
- defineVaporComponent,
- renderEffect,
- setInsertionState,
- template,
- } from '../src'
- import { makeRender } from './_utils'
- import type { VaporComponentInstance } from '../src/component'
- import { setElementText, setText } from '../src/dom/prop'
- const define = makeRender()
- describe('component', () => {
- it('should update parent(hoc) component host el when child component self update', async () => {
- const value = ref(true)
- let childNode1: Node | null = null
- let childNode2: Node | null = null
- const { component: Child } = define({
- setup() {
- return createIf(
- () => value.value,
- () => (childNode1 = template('<div></div>')()),
- () => (childNode2 = template('<span></span>')()),
- )
- },
- })
- const { host } = define({
- setup() {
- return createComponent(Child)
- },
- }).render()
- expect(host.innerHTML).toBe('<div></div><!--if-->')
- expect(host.children[0]).toBe(childNode1)
- value.value = false
- await nextTick()
- expect(host.innerHTML).toBe('<span></span><!--if-->')
- expect(host.children[0]).toBe(childNode2)
- })
- it('should create a component with props', () => {
- const { component: Comp } = define({
- setup() {
- return template('<div>', true)()
- },
- })
- const { host } = define({
- setup() {
- return createComponent(Comp, { id: () => 'foo', class: () => 'bar' })
- },
- }).render()
- expect(host.innerHTML).toBe('<div id="foo" class="bar"></div>')
- })
- it('should not update Component if only changed props are declared emit listeners', async () => {
- const updatedSyp = vi.fn()
- const { component: Comp } = define({
- emits: ['foo'],
- setup() {
- onUpdated(updatedSyp)
- return template('<div>', true)()
- },
- })
- const toggle = ref(true)
- const fn1 = () => {}
- const fn2 = () => {}
- define({
- setup() {
- const _on_foo = () => (toggle.value ? fn1() : fn2())
- return createComponent(Comp, { onFoo: () => _on_foo })
- },
- }).render()
- expect(updatedSyp).toHaveBeenCalledTimes(0)
- toggle.value = false
- await nextTick()
- expect(updatedSyp).toHaveBeenCalledTimes(0)
- })
- it('component child synchronously updating parent state should trigger parent re-render', async () => {
- const { component: Child } = define({
- setup() {
- const n = inject<Ref<number>>('foo')!
- n.value++
- const n0 = template('<div></div>')()
- renderEffect(() => setElementText(n0, n.value))
- return n0
- },
- })
- const { host } = define({
- setup() {
- const n = ref(0)
- provide('foo', n)
- const n0 = template('<div></div>')()
- renderEffect(() => setElementText(n0, n.value))
- return [n0, createComponent(Child)]
- },
- }).render()
- expect(host.innerHTML).toBe('<div>0</div><div>1</div>')
- await nextTick()
- expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
- })
- it('component child updating parent state in pre-flush should trigger parent re-render', async () => {
- const { component: Child } = define({
- props: ['value'],
- setup(props: any, { emit }) {
- watch(
- () => props.value,
- val => emit('update', val),
- )
- const n0 = template('<div></div>')()
- renderEffect(() => setElementText(n0, props.value))
- return n0
- },
- })
- const outer = ref(0)
- const { host } = define({
- setup() {
- const inner = ref(0)
- const n0 = template('<div></div>')()
- renderEffect(() => setElementText(n0, inner.value))
- const n1 = createComponent(Child, {
- value: () => outer.value,
- onUpdate: () => (val: number) => (inner.value = val),
- })
- return [n0, n1]
- },
- }).render()
- expect(host.innerHTML).toBe('<div>0</div><div>0</div>')
- outer.value++
- await nextTick()
- expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
- })
- it('child only updates once when triggered in multiple ways', async () => {
- const a = ref(0)
- const calls: string[] = []
- const { component: Child } = define({
- props: ['count'],
- setup(props: any) {
- onUpdated(() => calls.push('update child'))
- const n = createTextNode()
- renderEffect(() => {
- setText(n, `${props.count} - ${a.value}`)
- })
- return n
- },
- })
- const { host } = define({
- setup() {
- return createComponent(Child, { count: () => a.value })
- },
- }).render()
- expect(host.innerHTML).toBe('0 - 0')
- expect(calls).toEqual([])
- // This will trigger child rendering directly, as well as via a prop change
- a.value++
- await nextTick()
- expect(host.innerHTML).toBe('1 - 1')
- expect(calls).toEqual(['update child'])
- })
- it(`an earlier update doesn't lead to excessive subsequent updates`, async () => {
- const globalCount = ref(0)
- const parentCount = ref(0)
- const calls: string[] = []
- const { component: Child } = define({
- props: ['count'],
- setup(props: any) {
- watch(
- () => props.count,
- () => {
- calls.push('child watcher')
- globalCount.value = props.count
- },
- )
- onUpdated(() => calls.push('update child'))
- return []
- },
- })
- const { component: Parent } = define({
- props: ['count'],
- setup(props: any) {
- onUpdated(() => calls.push('update parent'))
- const n1 = createTextNode()
- const n2 = createComponent(Child, { count: () => parentCount.value })
- renderEffect(() => {
- setText(n1, `${globalCount.value} - ${props.count}`)
- })
- return [n1, n2]
- },
- })
- const { host } = define({
- setup() {
- onUpdated(() => calls.push('update root'))
- return createComponent(Parent, { count: () => globalCount.value })
- },
- }).render()
- expect(host.innerHTML).toBe(`0 - 0`)
- expect(calls).toEqual([])
- parentCount.value++
- await nextTick()
- expect(host.innerHTML).toBe(`1 - 1`)
- expect(calls).toEqual(['child watcher', 'update parent'])
- })
- it('child component props update should not lead to double update', async () => {
- const text = ref(0)
- const spy = vi.fn()
- const { component: Comp } = define({
- props: ['text'],
- setup(props: any) {
- const n1 = template('<h1></h1>')()
- renderEffect(() => {
- spy()
- setElementText(n1, props.text)
- })
- return n1
- },
- })
- const { host } = define({
- setup() {
- return createComponent(Comp, { text: () => text.value })
- },
- }).render()
- expect(host.innerHTML).toBe('<h1>0</h1>')
- expect(spy).toHaveBeenCalledTimes(1)
- text.value++
- await nextTick()
- expect(host.innerHTML).toBe('<h1>1</h1>')
- expect(spy).toHaveBeenCalledTimes(2)
- })
- it('properly mount child component when using setInsertionState', async () => {
- const spy = vi.fn()
- const { component: Comp } = define({
- setup() {
- onMounted(spy)
- return template('<h1>hi</h1>')()
- },
- })
- const { host } = define({
- setup() {
- const n2 = template('<div></div>', true)()
- setInsertionState(n2 as any)
- createComponent(Comp)
- return n2
- },
- }).render()
- expect(host.innerHTML).toBe('<div><h1>hi</h1></div>')
- expect(spy).toHaveBeenCalledTimes(1)
- })
- it('unmount component', async () => {
- const { host, app, instance } = define(() => {
- const count = ref(0)
- const t0 = template('<div></div>')
- const n0 = t0()
- watchEffect(() => {
- setElementText(n0, count.value)
- })
- renderEffect(() => {})
- return n0
- }).render()
- const i = instance as VaporComponentInstance
- // watchEffect + renderEffect + props validation effect
- expect(getEffectsCount(i.scope)).toBe(3)
- expect(host.innerHTML).toBe('<div>0</div>')
- app.unmount()
- expect(host.innerHTML).toBe('')
- expect(getEffectsCount(i.scope)).toBe(0)
- })
- it('work with v-once + props', () => {
- const Child = defineVaporComponent({
- props: {
- count: Number,
- },
- setup(props) {
- const n0 = template(' ')() as any
- renderEffect(() => setText(n0, String(props.count)))
- return n0
- },
- })
- const count = ref(0)
- const { html } = define({
- setup() {
- return createComponent(
- Child,
- { count: () => count.value },
- null,
- true,
- true, // v-once
- )
- },
- }).render()
- expect(html()).toBe('0')
- count.value++
- expect(html()).toBe('0')
- })
- it('work with v-once + attrs', () => {
- const Child = defineVaporComponent({
- setup() {
- const attrs = useAttrs()
- const n0 = template(' ')() as any
- renderEffect(() => setText(n0, attrs.count as string))
- return n0
- },
- })
- const count = ref(0)
- const { html } = define({
- setup() {
- return createComponent(
- Child,
- { count: () => count.value },
- null,
- true,
- true, // v-once
- )
- },
- }).render()
- expect(html()).toBe('0')
- count.value++
- expect(html()).toBe('0')
- })
- it('v-once props should be frozen and not update when parent changes', async () => {
- const localCount = ref(0)
- const Child = defineVaporComponent({
- props: {
- count: Number,
- },
- setup(props) {
- const n0 = template('<div></div>')() as any
- renderEffect(() =>
- setElementText(n0, `${localCount.value} - ${props.count}`),
- )
- return n0
- },
- })
- const parentCount = ref(0)
- const { html } = define({
- setup() {
- return createComponent(
- Child,
- { count: () => parentCount.value },
- null,
- true,
- true, // v-once
- )
- },
- }).render()
- expect(html()).toBe('<div>0 - 0</div>')
- parentCount.value++
- await nextTick()
- expect(html()).toBe('<div>0 - 0</div>')
- localCount.value++
- await nextTick()
- expect(html()).toBe('<div>1 - 0</div>')
- })
- it('v-once attrs should be frozen and not update when parent changes', async () => {
- const localCount = ref(0)
- const Child = defineVaporComponent({
- inheritAttrs: false,
- setup() {
- const attrs = useAttrs()
- const n0 = template('<div></div>')() as any
- renderEffect(() =>
- setElementText(n0, `${localCount.value} - ${attrs.count}`),
- )
- return n0
- },
- })
- const parentCount = ref(0)
- const { html } = define({
- setup() {
- return createComponent(
- Child,
- { count: () => parentCount.value },
- null,
- true,
- true, // v-once
- )
- },
- }).render()
- expect(html()).toBe('<div>0 - 0</div>')
- parentCount.value++
- await nextTick()
- expect(html()).toBe('<div>0 - 0</div>')
- localCount.value++
- await nextTick()
- expect(html()).toBe('<div>1 - 0</div>')
- })
- test('should mount component only with template in production mode', () => {
- __DEV__ = false
- const { component: Child } = define({
- render() {
- return template('<div> HI </div>', true)()
- },
- })
- const { host } = define({
- setup() {
- return createComponent(Child, null, null, true)
- },
- }).render()
- expect(host.innerHTML).toBe('<div> HI </div>')
- __DEV__ = true
- })
- it('warn if functional vapor component not return a block', () => {
- // @ts-expect-error
- define(() => {
- return () => {}
- }).render()
- expect(
- 'Functional vapor component must return a block directly',
- ).toHaveBeenWarned()
- })
- it('warn if setup return a function and no render function', () => {
- define({
- setup() {
- return () => []
- },
- }).render()
- expect(
- 'Vapor component setup() returned non-block value, and has no render function',
- ).toHaveBeenWarned()
- })
- it('warn non-existent property access', () => {
- define({
- setup() {
- return {}
- },
- render(ctx: any) {
- ctx.foo
- return []
- },
- }).render()
- expect(
- 'Property "foo" was accessed during render but is not defined on instance.',
- ).toHaveBeenWarned()
- })
- })
- function getEffectsCount(scope: EffectScope): number {
- let n = 0
- for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
- if (dep.dep instanceof ReactiveEffect) {
- n++
- }
- }
- return n
- }
|