test-utils icon indicating copy to clipboard operation
test-utils copied to clipboard

feature: allow stubbing components by definition, not by name

Open xanf opened this issue 3 years ago • 5 comments

stubs has an important limitation for years - you can't stub anonymous component. Let me show that in code:

const AnonymousComponent = { template: '<div>anonymous</div>' }
const Component = {
  computed: {
    cmp() {
      return AnonymousComponent;
    }
  },
  template: '<div><component :is="cmp" /></div>',
}

const wrapper = mount(Component, {
  global: {
    stubs: {
      // How do I stub AnonymousComponent?
    }
  }
})

If you feel that this is rare use-case, it will come quite frequent when all fancy setup() / <script setup> things will come in play:

const AnonymousComponent = { template: '<div>anonymous</div>' }
const Comp = {
  render() {
    return h(AnonymousComponent)
  }
}

const wrapper = mount(Comp, {
  global: {
    stubs: {
       // How do I stub AnonymousComponent?
    }
  }
})

In real world, AnonymousComponent is usually coming from third-party library so "just add name" is not a solution. Obviously there is an escape hatch - one can use jest.mock to mock AnonymousComponent import and provide stub in that way, but it looks ugly, complex, and (most important) - creates two different ways of solving same task (stubbing components)

The core reason of the problem is obvious - we're using name as an identifier for components to stub. I also see certain caveats here - for example registeredName which is used for stubbing definitely feels like implementation detail for me - I want to treat my components as black box as much as possible and I don't care how components: { ... } is defined

So, basically an idea is to allow passing component definition to stubs. Luckily, we've supported array syntax in stubs for long time, so the only change will be to allow that arrays to contain a new type of "stub definition", something like:

{ component: AnonymousComponent, stub: MySuperStub }

So, the usage will be:

const wrapper = mount(Comp, {
  global: {
    stubs: [
       { component: AnonymousComponent, stub: MySuperStub }, // using concrete stub      
       { component: SomeOtherComponent, stub: true },  // let VTU generate the stub,
       'HelloCmp', // mixing old array-style stubs, ugly, but works,
       { component: 'StubByName', stub: true } // Old good stub-by-name
    ]
  }
})

This change, while being completely additive and non-breaking will simplify life for every person who is stubbing here :) WDYT?

xanf avatar Jun 30 '21 06:06 xanf

I personally have no idea how useful this would be for the majority of users, but since it's not a breaking change, I don't have anything against it. I agree "just add a name" is not a very good solution.

The only real consideration is here is the complexity- do you see 1) significant difficultly in implementing or 2) unsolvable edge cases?

lmiller1990 avatar Jul 06 '21 09:07 lmiller1990

I personally have no idea how useful this would be for the majority of users, but since it's not a breaking change, I don't have anything against it. I agree "just add a name" is not a very good solution.

The only real consideration is here is the complexity- do you see 1) significant difficultly in implementing or 2) unsolvable edge cases?

No, actually I have a working prototype and it does not bring any complexity while really solving important pain points for shallow users. I'll open PR within couple of days

xanf avatar Jul 06 '21 09:07 xanf

Sounds good!

Also your many fixes are now out in rc.10 - thanks a lot for that. https://github.com/vuejs/vue-test-utils-next/releases/tag/v2.0.0-rc.10

lmiller1990 avatar Jul 06 '21 10:07 lmiller1990

I currently have a util to stub a component but render the slots and also with slot data. Here I have a quick example:

<template>
  <BigLayout>
    <template #header="{ toolbar }">
      {{ toolbar ? 'Header in toolbar' : 'Header not in toolbar' }}
    </template>
    
    <span>Some Content</span>
  </BigLayout>
</template>

I want to stub the BigLayout but still render the header slot with some data.

With the new stub definition we could implement this and be able to add more enhancement to the stubs:

const wrapper = mount(Comp, {
  global: {
    stubs: [
      {
        stub: true,
        component: BigLayout,
        scopedSlots: {
          header: { toolbar: true }
        }
      }
    ]
  }
})

freakzlike avatar Dec 23 '21 14:12 freakzlike

I currently have a util to stub a component but render the slots and also with slot data. Here I have a quick example:

<template>
  <BigLayout>
    <template #header="{ toolbar }">
      {{ toolbar ? 'Header in toolbar' : 'Header not in toolbar' }}
    </template>
    
    <span>Some Content</span>
  </BigLayout>
</template>

I want to stub the BigLayout but still render the header slot with some data.

With the new stub definition we could implement this and be able to add more enhancement to the stubs:

const wrapper = mount(Comp, {
  global: {
    stubs: [
      {
        stub: true,
        component: BigLayout,
        scopedSlots: {
          header: { toolbar: true }
        }
      }
    ]
  }
})

I like this. I also have utils to handling scoped slots and rendering those.

I know this is kind of old, but this is a issue still happening. I will draft an Issue to bring the rendering of slots / dynamic / named slots back

ironicnet avatar Mar 07 '24 10:03 ironicnet