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: stubs mounting option #56

Merged
merged 15 commits into from
Apr 14, 2020
44 changes: 44 additions & 0 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
defineComponent,
VNodeNormalizedChildren,
ComponentOptions,
transformVNodeArgs,
Plugin,
Directive,
Component,
Expand All @@ -15,6 +16,7 @@ import { createWrapper } from './vue-wrapper'
import { createEmitMixin } from './emitMixin'
import { createDataMixin } from './dataMixin'
import { MOUNT_ELEMENT_ID } from './constants'
import { createStub } from './stub'

type Slot = VNode | string | { render: Function }

Expand All @@ -29,6 +31,7 @@ interface MountingOptions {
plugins?: Plugin[]
mixins?: ComponentOptions[]
mocks?: Record<string, any>
Copy link
Member Author

Choose a reason for hiding this comment

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

Typing Vue components is very difficult, will need to investigate the Vue codebase to figure out how best to do this

Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed this too. Am interested to hear about what you find.

stubs?: Record<any, any>
provide?: Record<any, any>
// TODO how to type `defineComponent`? Using `any` for now.
components?: Record<string, Component | object>
Expand Down Expand Up @@ -72,6 +75,7 @@ export function mount(originalComponent: any, options?: MountingOptions) {

// create the wrapper component
const Parent = defineComponent({
name: 'VTU_COMPONENT',
render() {
return h(component, props, slots)
}
Expand Down Expand Up @@ -133,6 +137,46 @@ export function mount(originalComponent: any, options?: MountingOptions) {
const { emitMixin, events } = createEmitMixin()
vm.mixin(emitMixin)

// stubs
if (options?.global?.stubs) {
transformVNodeArgs((args) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can destructure like this, so its easier to understand context later down?

Suggested change
transformVNodeArgs((args) => {
transformVNodeArgs(([componentOptions]) => {

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, but I am not really a fan of this refactor. This variable can be a string (eg 'div'), or an object of options, so componentOptions is a bit misleading. I guess the ideal name is htmlTagOrComponentOptions. I will just put a comment here for now.

// regular HTML Element. Do not stubs these
if (Array.isArray(args) && typeof args[0] === 'string') {
afontcu marked this conversation as resolved.
Show resolved Hide resolved
return args
}

// don't care about comments/fragments
if (typeof args[0] === 'symbol') {
return args
}

// do not stub the VTU Parent component
if (typeof args[0] === 'object' && args[0]['name'] === 'VTU_COMPONENT') {
return args
}

if (
typeof args[0] === 'object' &&
args[0]['name'] in options?.global?.stubs
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably we should do that magic with the Name transformation here? PascalCase, snake-case, that sort of stuff.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah this is probably a good idea, make it easier to find the stubs, I will implement this

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I also did that for the findComponent by name Branch. Need to sync these up later, so we don't duplicate :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, sounds good!

) {
const name = args[0]['name']
// default stub
if (options?.global?.stubs[name] === true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You are already in the conditional check, its there.

Suggested change
if (options?.global?.stubs[name] === true) {
if (options.global.stubs[name] === true) {

Copy link
Member Author

Choose a reason for hiding this comment

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

heh, copy pasted... thanks, I'll clean this up

return [createStub({ name: args[0]['name'] })]
}
Copy link
Member Author

@lmiller1990 lmiller1990 Apr 13, 2020

Choose a reason for hiding this comment

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

@dobromir-hristov we one of us gets to working on findComponent we will probably need to pass props as the second arg here, so we can do findComponent(Foo).props). So it will be like

return [createStub({ name: args[0]['name'] }), args[1]] or something. I didn't do it here because I am not exactly sure how this will work yet, but I don't expect it to be a problem.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, we will check it out.


// custom stub implementation
if (typeof options?.global?.stubs[name] === 'object') {
return [options?.global?.stubs[name]]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return [options?.global?.stubs[name]]
return [options.global.stubs[name]]

Copy link
Member Author

Choose a reason for hiding this comment

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

very good

}
}

return args
})
} else {
transformVNodeArgs()
}

// mount the app!
const app = vm.mount(el)

Expand Down
12 changes: 12 additions & 0 deletions src/stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { h } from 'vue'

interface IStubOptions {
name?: string
}

export const createStub = (options: IStubOptions) => {
const tag = options.name ? `${options.name}-stub` : 'anonymous-stub'
const render = () => h(tag)

return { name: tag, render }
}
163 changes: 163 additions & 0 deletions tests/mountingOptions/stubs.global.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { h, ComponentOptions } from 'vue'

import { mount } from '../../src'
import Hello from '../components/Hello.vue'

describe('mounting options: stubs', () => {
it('stubs in a fragment', () => {
const Foo = {
name: 'Foo',
render() {
return h('p')
}
}
const Component: ComponentOptions = {
render() {
return h(() => [h('div'), h(Foo)])
}
}

const wrapper = mount(Component, {
global: {
stubs: {
Foo: true
}
}
})

expect(wrapper.html()).toBe('<div></div><foo-stub></foo-stub>')
})

it('prevents lifecycle hooks triggering in a stub', () => {
const onBeforeMount = jest.fn()
const beforeCreate = jest.fn()
const Foo = {
name: 'Foo',
setup() {
onBeforeMount(onBeforeMount)
return () => h('div')
},
beforeCreate
}
const Comp = {
render() {
return h(Foo)
}
}

const wrapper = mount(Comp, {
global: {
stubs: {
Foo: true
}
}
})

expect(wrapper.html()).toBe('<foo-stub></foo-stub>')
expect(onBeforeMount).not.toHaveBeenCalled()
expect(beforeCreate).not.toHaveBeenCalled()
})

it('uses a custom stub implementation', () => {
const onBeforeMount = jest.fn()
const FooStub = {
name: 'FooStub',
setup() {
onBeforeMount(onBeforeMount)
return () => h('div', 'foo stub')
}
}
const Foo = {
name: 'Foo',
render() {
return h('div', 'real foo')
}
}

const Comp = {
render() {
return h(Foo)
}
}

const wrapper = mount(Comp, {
global: {
stubs: {
Foo: FooStub
}
}
})

expect(onBeforeMount).toHaveBeenCalled()
expect(wrapper.html()).toBe('<div>foo stub</div>')
})

it('uses an sfc as a custom stub', () => {
const created = jest.fn()
const HelloComp = {
name: 'Hello',
created() {
created()
},
render() {
return h('span', 'real implementation')
}
}

const Comp = {
render() {
return h(HelloComp)
}
}

const wrapper = mount(Comp, {
global: {
stubs: {
Hello: Hello
}
}
})

expect(created).not.toHaveBeenCalled()
expect(wrapper.html()).toBe(
'<div id="root"><div id="msg">Hello world</div></div>'
)
})

it('stubs using inline components', () => {
const Foo = {
name: 'Foo',
render() {
return h('p')
}
}
const Bar = {
name: 'Bar',
render() {
return h('p')
}
}
const Component: ComponentOptions = {
render() {
return h(() => [h(Foo), h(Bar)])
}
}

const wrapper = mount(Component, {
global: {
stubs: {
Foo: {
template: '<span />'
},
Bar: {
render() {
return h('div')
}
}
}
}
})

expect(wrapper.html()).toBe('<span></span><div></div>')
})
})