| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- import {
- createApp,
- getCurrentInstance,
- h,
- nextTick,
- nodeOps,
- ref,
- render,
- serializeInner,
- useSlots,
- } from '@vue/runtime-test'
- import { createBlock, normalizeVNode } from '../src/vnode'
- import { createSlots } from '../src/helpers/createSlots'
- describe('component: slots', () => {
- function renderWithSlots(slots: any): any {
- let instance: any
- const Comp = {
- render() {
- instance = getCurrentInstance()
- return h('div')
- },
- }
- render(h(Comp, null, slots), nodeOps.createElement('div'))
- return instance
- }
- test('initSlots: instance.slots should be set correctly', () => {
- let instance: any
- const Comp = {
- render() {
- instance = getCurrentInstance()
- return h('div')
- },
- }
- const slots = { foo: () => {}, _: 1 }
- render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
- expect(instance.slots).toMatchObject(slots)
- })
- test('initSlots: instance.slots should remove compiler marker if parent is using manual render function', () => {
- const { slots } = renderWithSlots({ _: 1 })
- expect(slots).toMatchObject({})
- })
- test('initSlots: ensure compiler marker non-enumerable', () => {
- const Comp = {
- render() {
- const slots = useSlots()
- // Only user-defined slots should be enumerable
- expect(Object.keys(slots)).toEqual(['foo'])
- // Internal compiler markers must still exist but be non-enumerable
- expect(slots).toHaveProperty('_')
- expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
- false,
- )
- return h('div')
- },
- }
- const slots = { foo: () => {}, _: 1 }
- render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
- })
- test('initSlots: should normalize object slots (when value is null, string, array)', () => {
- const { slots } = renderWithSlots({
- _inner: '_inner',
- foo: null,
- header: 'header',
- footer: ['f1', 'f2'],
- })
- expect(
- '[Vue warn]: Non-function value encountered for slot "_inner". Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(
- '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(
- '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(slots).not.toHaveProperty('foo')
- expect(slots._inner()).toMatchObject([normalizeVNode('_inner')])
- expect(slots.header()).toMatchObject([normalizeVNode('header')])
- expect(slots.footer()).toMatchObject([
- normalizeVNode('f1'),
- normalizeVNode('f2'),
- ])
- })
- test('initSlots: should normalize object slots (when value is function)', () => {
- let proxy: any
- const Comp = {
- render() {
- proxy = getCurrentInstance()
- return h('div')
- },
- }
- render(
- h(Comp, null, {
- header: () => 'header',
- }),
- nodeOps.createElement('div'),
- )
- expect(proxy.slots.header()).toMatchObject([normalizeVNode('header')])
- })
- test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => {
- const { slots } = renderWithSlots([h('span')])
- expect(
- '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(slots.default()).toMatchObject([normalizeVNode(h('span'))])
- })
- test('updateSlots: instance.slots should be updated correctly (when slotType is number)', async () => {
- const flag1 = ref(true)
- let instance: any
- const Child = () => {
- instance = getCurrentInstance()
- return 'child'
- }
- const Comp = {
- setup() {
- return () => [
- h(
- Child,
- null,
- createSlots({ _: 2 as any }, [
- flag1.value
- ? {
- name: 'one',
- fn: () => [h('span')],
- }
- : {
- name: 'two',
- fn: () => [h('div')],
- },
- ]),
- ),
- ]
- },
- }
- render(h(Comp), nodeOps.createElement('div'))
- expect(instance.slots).toHaveProperty('one')
- expect(instance.slots).not.toHaveProperty('two')
- flag1.value = false
- await nextTick()
- expect(instance.slots).not.toHaveProperty('one')
- expect(instance.slots).toHaveProperty('two')
- })
- test('updateSlots: instance.slots should be updated correctly (when slotType is null)', async () => {
- const flag1 = ref(true)
- let instance: any
- const Child = () => {
- instance = getCurrentInstance()
- return 'child'
- }
- const oldSlots = {
- header: 'header',
- footer: undefined,
- }
- const newSlots = {
- header: undefined,
- footer: 'footer',
- }
- const Comp = {
- setup() {
- return () => [
- h(Child, { n: flag1.value }, flag1.value ? oldSlots : newSlots),
- ]
- },
- }
- render(h(Comp), nodeOps.createElement('div'))
- expect(instance.slots).toHaveProperty('header')
- expect(instance.slots).not.toHaveProperty('footer')
- flag1.value = false
- await nextTick()
- expect(
- '[Vue warn]: Non-function value encountered for slot "header". Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(
- '[Vue warn]: Non-function value encountered for slot "footer". Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(instance.slots).not.toHaveProperty('header')
- expect(instance.slots.footer()).toMatchObject([normalizeVNode('footer')])
- })
- test('updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', async () => {
- const flag1 = ref(true)
- let instance: any
- const Child = () => {
- instance = getCurrentInstance()
- return 'child'
- }
- const Comp = {
- setup() {
- return () => [
- h(Child, { n: flag1.value }, flag1.value ? ['header'] : ['footer']),
- ]
- },
- }
- render(h(Comp), nodeOps.createElement('div'))
- expect(instance.slots.default()).toMatchObject([normalizeVNode('header')])
- flag1.value = false
- await nextTick()
- expect(
- '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
- ).toHaveBeenWarned()
- expect(instance.slots.default()).toMatchObject([normalizeVNode('footer')])
- })
- test('should respect $stable flag with a value of true', async () => {
- const flag1 = ref(1)
- const flag2 = ref(2)
- const spy = vi.fn()
- const Child = () => {
- spy()
- return 'child'
- }
- const App = {
- setup() {
- return () => [
- flag1.value,
- h(
- Child,
- { n: flag2.value },
- {
- foo: () => 'foo',
- $stable: true,
- },
- ),
- ]
- },
- }
- render(h(App), nodeOps.createElement('div'))
- expect(spy).toHaveBeenCalledTimes(1)
- // parent re-render, props didn't change, slots are stable
- // -> child should not update
- flag1.value++
- await nextTick()
- expect(spy).toHaveBeenCalledTimes(1)
- // parent re-render, props changed
- // -> child should update
- flag2.value++
- await nextTick()
- expect(spy).toHaveBeenCalledTimes(2)
- })
- test('should respect $stable flag with a value of false', async () => {
- const flag1 = ref(1)
- const flag2 = ref(2)
- const spy = vi.fn()
- const Child = () => {
- spy()
- return 'child'
- }
- const App = {
- setup() {
- return () => [
- flag1.value,
- h(
- Child,
- { n: flag2.value },
- {
- foo: () => 'foo',
- $stable: false,
- },
- ),
- ]
- },
- }
- render(h(App), nodeOps.createElement('div'))
- expect(spy).toHaveBeenCalledTimes(1)
- // parent re-render, props didn't change, slots are not stable
- // -> child should update
- flag1.value++
- await nextTick()
- expect(spy).toHaveBeenCalledTimes(2)
- // parent re-render, props changed
- // -> child should update
- flag2.value++
- await nextTick()
- expect(spy).toHaveBeenCalledTimes(3)
- })
- test('should not warn when mounting another app in setup', () => {
- const Comp = {
- setup(_: any, { slots }: any) {
- return () => slots.default?.()
- },
- }
- const mountComp = () => {
- createApp({
- setup() {
- return () => h(Comp, () => 'msg')
- },
- }).mount(nodeOps.createElement('div'))
- }
- const App = {
- setup() {
- mountComp()
- return () => null
- },
- }
- createApp(App).mount(nodeOps.createElement('div'))
- expect(
- 'Slot "default" invoked outside of the render function',
- ).not.toHaveBeenWarned()
- })
- test('basic warn', () => {
- const Comp = {
- setup(_: any, { slots }: any) {
- slots.default && slots.default()
- return () => null
- },
- }
- const App = {
- setup() {
- return () => h(Comp, () => h('div'))
- },
- }
- createApp(App).mount(nodeOps.createElement('div'))
- expect(
- 'Slot "default" invoked outside of the render function',
- ).toHaveBeenWarned()
- })
- test('basic warn when mounting another app in setup', () => {
- const Comp = {
- setup(_: any, { slots }: any) {
- slots.default?.()
- return () => null
- },
- }
- const mountComp = () => {
- createApp({
- setup() {
- return () => h(Comp, () => 'msg')
- },
- }).mount(nodeOps.createElement('div'))
- }
- const App = {
- setup() {
- mountComp()
- return () => null
- },
- }
- createApp(App).mount(nodeOps.createElement('div'))
- expect(
- 'Slot "default" invoked outside of the render function',
- ).toHaveBeenWarned()
- })
- test('should not warn when render in setup', () => {
- const container = {
- setup(_: any, { slots }: any) {
- return () => slots.default && slots.default()
- },
- }
- const comp = h(container, null, () => h('div'))
- const App = {
- setup() {
- render(h(comp), nodeOps.createElement('div'))
- return () => null
- },
- }
- createApp(App).mount(nodeOps.createElement('div'))
- expect(
- 'Slot "default" invoked outside of the render function',
- ).not.toHaveBeenWarned()
- })
- test('basic warn when render in setup', () => {
- const container = {
- setup(_: any, { slots }: any) {
- slots.default && slots.default()
- return () => null
- },
- }
- const comp = h(container, null, () => h('div'))
- const App = {
- setup() {
- render(h(comp), nodeOps.createElement('div'))
- return () => null
- },
- }
- createApp(App).mount(nodeOps.createElement('div'))
- expect(
- 'Slot "default" invoked outside of the render function',
- ).toHaveBeenWarned()
- })
- test('slot name starts with underscore', () => {
- const Comp = {
- setup(_: any, { slots }: any) {
- return () => slots._foo()
- },
- }
- const App = {
- setup() {
- return () => h(Comp, null, { _foo: () => 'foo' })
- },
- }
- const root = nodeOps.createElement('div')
- createApp(App).mount(root)
- expect(serializeInner(root)).toBe('foo')
- })
- })
|