Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow stub out async component without name and without need of async/await #518

Merged
merged 1 commit into from
Apr 5, 2021
Merged

Conversation

freakzlike
Copy link
Collaborator

@freakzlike freakzlike commented Apr 3, 2021

I have dealt some more with stubbing out an async component.
With this PR I want to allow to stub out the async component without the need to provide a name and use async/await.

The async component can now be stubbed out the same way as a sync component:

// AsyncComponent.js
export default defineComponent({
  name: 'AsyncComponent', // Not required any more
  template: '<span>AsyncComponent</span>'
})

// App.js
const App = defineComponent({
  components: {
    MyComponent: defineAsyncComponent(() => import('./AsyncComponent'))
  },
  template: '<MyComponent/>'
})

// App.spec.js
test('stubs async component without resolving', () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        MyComponent: true
      }
    }
  })
  
  // No need to use await flushPromises()
  expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
})

It can also be stubbed out like before using the name rather than the key.

// AsyncComponent.js
export default defineComponent({
  name: 'AsyncComponent',
  template: '<span>AsyncComponent</span>'
})

// App.js
const App = defineComponent({
  components: {
    MyComponent: defineAsyncComponent(() => import('./AsyncComponent'))
  },
  template: '<MyComponent/>'
})

// App.spec.js
test('stubs async component with resolving', async () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        AsyncComponent: true
      }
    }
  })

  // flushPromises now required
  await flushPromises()

  expect(wrapper.html()).toBe('<async-component-stub></async-component-stub>')
})

I'm not pretty sure about the detection of the AsyncComponentWrapper. Maybe someone got a better way.

const getAsyncComponentName = (
  type: VNodeTypes,
  props: ({ [key: string]: unknown } & VNodeProps) | null | undefined
): string | null => {
  // Async components will be mounted as AsyncComponentWrapper
  if (!isMountedComponent(type, props)) return null

  // AsyncComponentWrapper may only contain one component
  const components = (type as ComponentOptions).components as Record<
    string,
    ComponentOptions
  >
  if (!components || Object.keys(components).length !== 1) return null

  const asyncComponentName = Object.keys(components)[0]
  const asyncComponent = components[asyncComponentName]

  // Is AsyncComponentWrapper?
  if (
    !asyncComponent.__asyncLoader ||
    asyncComponent.name !== 'AsyncComponentWrapper'
  )
    return null

  return asyncComponentName
}

Copy link
Member

@lmiller1990 lmiller1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, looking good!! Great contribution.

I think we need to be more thorough in getting the name for un-named components, we should be able to match it using some similar techniques. Assuming the component is the first one in components and grabbing the name from the key doesn't seem very reliable.

If you are able to try to make this change, please do! If you are stuck I can give it a try. I think you are really close, though.

src/stubs.ts Outdated
string,
ComponentOptions
>
if (!components || Object.keys(components).length !== 1) return null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it will be a problem. For example

     it('stubs async component without name', () => {
      const TestComponent = defineComponent({
        components: {
          Foo: {
            template: '<div />'
          },
          MyComponent: AsyncComponentWithoutName,
        },
        template: '<MyComponent/>'
      })

      const wrapper = mount(TestComponent, {
        global: {
          stubs: {
            MyComponent: true
          }
        }
      })

      expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
    })

Fails, because of

components: {
          Foo: {
            template: '<div />'
          },
          MyComponent: AsyncComponentWithoutName,

We can't rely on the Object.keys and getting the first element to capture the name of the component.

What we can do, I think, is use the parent component registry. See here: https://github.com/vuejs/vue-test-utils-next/blob/3e85d6ef3da57e5104f2b2ea1e4ca4b2f6ef75fc/src/stubs.ts#L145

I think we can match up using object identity. Something like above. Maybe...

      if (instance && instance.parent) {
        // try to infer the name based on local resolution
        const registry = (instance.type as any).components
        
        for (const [key, comp] of Object.entries(registry)) {
           if (args[0] === comp) {
             name = key
           }
         }
      }

That's untested, but there should be a way to find the component and get the name from the key.

@freakzlike
Copy link
Collaborator Author

I have make it much cleaner now. The original issue was that it has already found the name of the AsyncComponentWrapper and uses this, instead of the name in the registered components.

I was wondering that it is not possible to stub out a component by using the name in the registered components rather than the name defined in the component.

it('stubs component with different names', () => {
  const MyComponent = defineComponent({
    name: 'MyComponent',
    template: '<span>MyComponent</span>'
  })

  const TestComponent = defineComponent({
    components: {
      MyComponentAlias: MyComponent
    },
    template: '<MyComponentAlias/>'
  })

  const wrapper = mount(TestComponent, {
    global: {
      stubs: {
        MyComponentAlias: true, // does not work
        MyComponent: true // works
      }
    }
  })

  expect(wrapper.html()).toBe(
    '<my-component-alias-stub></my-component-alias-stub>'
  )
})

What do you think?
My idea was to give a list of names (name of component + name in registered components) to resolveComponentStubByName and return the first matching stub.

@freakzlike freakzlike requested a review from lmiller1990 April 4, 2021 07:15
@lmiller1990
Copy link
Member

lmiller1990 commented Apr 5, 2021

Ah, I see. I suppose we should use the key the component is registered under, since that is how you would use the component in your template. The only real usage of name is for better stack traces.

It looks like you accomplished it? https://github.com/vuejs/vue-test-utils-next/pull/518/files#diff-81bd0aa44ca1f02347a71cafda7db0708dcb4c2fe47e2c402db1925e2ffe49afR511

Can you clarify: are you asking for help on something, or are you asking what I think of your proposal?

I think this makes sense. The only edge case I see is someone registered a component using an alias that is used in another component as the name, but this seems fairly unlikely.

Are you making any more changes? If not I can re-review (tonight) and get this merged. Then we can do a release, it's been a while.

Copy link
Member

@cexbrayat cexbrayat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for your investigation, and the thorough tests you added 👍

When you're done addressing the feedback, and have rebased and squashed all the commits, we'll be good to merge.

src/stubs.ts Outdated
const isAsyncComponentWrapper = (type: VNodeTypes) =>
isComponent(type) &&
type.__asyncLoader &&
type.name === 'AsyncComponentWrapper'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment here indicating that AsyncComponentWrapper is the name given by vue-next to defineAsyncComponent.

@freakzlike
Copy link
Collaborator Author

I changed the stubs resolving to use the key in components (prio 1) and the name in the component (prio 2) to search for an entry in global.stubs. So there is also no need to use any special code for async components or for AsyncComponentWrapper

Copy link
Member

@cexbrayat cexbrayat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea.

Regarding the code, I'm not of fan of using an array for the names. I think it would be easier to follow by using different variables, like registeredName and componentName.
Then instead of calling resolveComponentStubByName with an array, we could call it once with registeredName and if nothing is found, another time with componentName..
IIUC what you're doing, that should not change the result, but would be slightly easier to grasp while reading the code.

@freakzlike
Copy link
Collaborator Author

@cexbrayat I changed the code according to your suggestion

@freakzlike freakzlike requested a review from cexbrayat April 5, 2021 08:43
Copy link
Member

@cexbrayat cexbrayat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome job, thanks.

@cexbrayat cexbrayat merged commit ecc1031 into vuejs:master Apr 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants