Skip to content

Commit 08e2881

Browse files
authored
Merge pull request #56 from vuejs/feature/stubs
feat: stubs mounting option
2 parents 62aca6d + da5d3cb commit 08e2881

File tree

8 files changed

+391
-7
lines changed

8 files changed

+391
-7
lines changed

.github/workflows/ci.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ jobs:
2424
uses: actions/setup-node@v1
2525
with:
2626
node-version: ${{ matrix.node-version }}
27-
- run: npm install
28-
- run: npm run lint
29-
- run: npm run build
30-
- run: npm test
27+
- run: yarn install
28+
- run: yarn lint
29+
- run: yarn test
30+
- run: yarn build
3131
env:
3232
CI: true

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@
1111
"dist",
1212
"README.md"
1313
],
14+
"dependencies": {
15+
"lodash": "^4.17.15"
16+
},
1417
"devDependencies": {
1518
"@babel/core": "^7.9.0",
1619
"@babel/preset-env": "^7.8.4",
1720
"@babel/types": "^7.8.3",
21+
"@rollup/plugin-node-resolve": "^7.1.3",
1822
"@types/estree": "^0.0.42",
1923
"@types/jest": "^24.9.1",
24+
"@types/lodash": "^4.14.149",
2025
"@vue/compiler-sfc": "^3.0.0-alpha.12",
2126
"babel-jest": "^25.2.3",
2227
"babel-preset-jest": "^25.2.1",

rollup.config.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ts from 'rollup-plugin-typescript2'
2+
import resolve from '@rollup/plugin-node-resolve'
23

34
import pkg from './package.json'
45

@@ -19,8 +20,15 @@ function createEntry(options) {
1920

2021
const config = {
2122
input,
22-
external: ['vue'],
23-
plugins: [],
23+
external: [
24+
'vue',
25+
'lodash/mergeWith',
26+
'lodash/camelCase',
27+
'lodash/upperFirst',
28+
'lodash/kebabCase',
29+
'lodash/flow'
30+
],
31+
plugins: [resolve()],
2432
output: {
2533
banner,
2634
file: 'dist/vue-test-utils.other.js',

src/mount.ts

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
defineComponent,
66
VNodeNormalizedChildren,
77
ComponentOptions,
8+
transformVNodeArgs,
89
Plugin,
910
Directive,
1011
Component,
@@ -15,6 +16,7 @@ import { createWrapper } from './vue-wrapper'
1516
import { createEmitMixin } from './emitMixin'
1617
import { createDataMixin } from './dataMixin'
1718
import { MOUNT_ELEMENT_ID } from './constants'
19+
import { stubComponents } from './stubs'
1820

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

@@ -29,6 +31,7 @@ interface MountingOptions {
2931
plugins?: Plugin[]
3032
mixins?: ComponentOptions[]
3133
mocks?: Record<string, any>
34+
stubs?: Record<any, any>
3235
provide?: Record<any, any>
3336
// TODO how to type `defineComponent`? Using `any` for now.
3437
components?: Record<string, Component | object>
@@ -72,6 +75,7 @@ export function mount(originalComponent: any, options?: MountingOptions) {
7275

7376
// create the wrapper component
7477
const Parent = defineComponent({
78+
name: 'VTU_COMPONENT',
7579
render() {
7680
return h(component, props, slots)
7781
}
@@ -133,6 +137,13 @@ export function mount(originalComponent: any, options?: MountingOptions) {
133137
const { emitMixin, events } = createEmitMixin()
134138
vm.mixin(emitMixin)
135139

140+
// stubs
141+
if (options?.global?.stubs) {
142+
stubComponents(options.global.stubs)
143+
} else {
144+
transformVNodeArgs()
145+
}
146+
136147
// mount the app!
137148
const app = vm.mount(el)
138149

src/stubs.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { transformVNodeArgs, h } from 'vue'
2+
3+
import { pascalCase, kebabCase } from './utils'
4+
5+
interface IStubOptions {
6+
name?: string
7+
}
8+
9+
// TODO: figure out how to type this
10+
type VNodeArgs = any[]
11+
12+
export const createStub = (options: IStubOptions) => {
13+
const tag = options.name ? `${options.name}-stub` : 'anonymous-stub'
14+
const render = () => h(tag)
15+
16+
return { name: tag, render }
17+
}
18+
19+
const resolveComponentStubByName = (
20+
componentName: string,
21+
stubs: Record<any, any>
22+
) => {
23+
const componentPascalName = pascalCase(componentName)
24+
const componentKebabName = kebabCase(componentName)
25+
26+
for (const [stubKey, value] of Object.entries(stubs)) {
27+
if (
28+
stubKey === componentPascalName ||
29+
stubKey === componentKebabName ||
30+
stubKey === componentName
31+
) {
32+
return value
33+
}
34+
}
35+
}
36+
37+
const isHTMLElement = (args: VNodeArgs) =>
38+
Array.isArray(args) && typeof args[0] === 'string'
39+
40+
const isCommentOrFragment = (args: VNodeArgs) => typeof args[0] === 'symbol'
41+
42+
const isParent = (args: VNodeArgs) =>
43+
typeof args[0] === 'object' && args[0]['name'] === 'VTU_COMPONENT'
44+
45+
const isComponent = (args: VNodeArgs) => typeof args[0] === 'object'
46+
47+
export function stubComponents(stubs: Record<any, any>) {
48+
transformVNodeArgs((args) => {
49+
// args[0] can either be:
50+
// 1. a HTML tag (div, span...)
51+
// 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} }
52+
// Depending what it is, we do different things.
53+
if (isHTMLElement(args) || isCommentOrFragment(args) || isParent(args)) {
54+
return args
55+
}
56+
57+
if (isComponent(args)) {
58+
const name = args[0]['name']
59+
if (!name) {
60+
return args
61+
}
62+
63+
const stub = resolveComponentStubByName(name, stubs)
64+
65+
// we return a stub by matching Vue's `h` function
66+
// where the signature is h(Component, props)
67+
// case 1: default stub
68+
if (stub === true) {
69+
return [createStub({ name }), {}]
70+
}
71+
72+
// case 2: custom implementation
73+
if (typeof stub === 'object') {
74+
return [stubs[name], {}]
75+
}
76+
}
77+
78+
return args
79+
})
80+
}

src/utils.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import camelCase from 'lodash/camelCase'
2+
import upperFirst from 'lodash/upperFirst'
3+
import kebabCase from 'lodash/kebabCase'
4+
import flow from 'lodash/flow'
5+
6+
const pascalCase = flow(camelCase, upperFirst)
7+
8+
export { kebabCase, pascalCase }

0 commit comments

Comments
 (0)