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(types): add typescript declarations #15

Merged
merged 6 commits into from
Jul 17, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"version": "0.1.0",
"description": "Utilities for testing Vue components.",
"main": "dist/vue-test-utils.js",
"types": "types/index.d.ts",
"files": [
"src",
"dist/*.js"
"dist/*.js",
"types/index.d.ts"
],
"scripts": {
"build": "node build/build.js",
Expand All @@ -16,10 +18,11 @@
"lint:docs": "eslint --ext js,vue,md docs --ignore-path .gitignore",
"lint:fix": "npm run lint -- --fix",
"postinstall": "node build/install-hooks.js",
"test": "npm run lint && npm run lint:docs && npm run flow && npm run test:unit && npm run test:integration && npm run test:integration:karma",
"test": "npm run lint && npm run lint:docs && npm run flow && npm run test:types && npm run test:unit && npm run test:integration && npm run test:integration:karma",
"test:integration": "cross-env BABEL_ENV=test mocha-webpack --webpack-config build/webpack.test.config.js test/integration/specs --recursive --require test/integration/setup/mocha.setup.js",
"test:integration:karma": "cross-env BABEL_ENV=test TARGET=browser karma start test/integration/setup/karma.conf.js --single-run",
"test:unit": "cross-env BABEL_ENV=test mocha-webpack --webpack-config build/webpack.test.config.js test/unit/specs --recursive --require test/unit/setup/mocha.setup.js",
"test:types": "tsc -p types",
"release": "bash build/release.sh",
"release:note": "node build/gen-release-note.js"
},
Expand Down Expand Up @@ -72,6 +75,7 @@
"shelljs": "^0.7.8",
"sinon": "^2.3.2",
"sinon-chai": "^2.10.0",
"typescript": "^2.4.1",
"vue": "^2.3.3",
"vue-loader": "^12.2.1",
"vue-router": "^2.6.0",
Expand Down
94 changes: 94 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Vue, { VNodeData, Component, ComponentOptions, FunctionalComponentOptions } from 'vue'

/**
* Utility type to declare an extended Vue constructor
*/
type VueClass<V extends Vue> = (new (...args: any[]) => V) & typeof Vue

/**
* Utility type for a selector
*/
type Selector = string | Component

/**
* Utility type for slots
*/
type Slots = {
[key: string]: (Component | string)[] | Component | string
}

/**
* Utility type for stubs which can be a string of template as a shorthand
* If it is an array of string, the specified children are replaced by blank components
*/
type Stubs = {
[key: string]: Component | string | boolean

Choose a reason for hiding this comment

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

} | string[]

/**
* Base class of Wrapper and WrapperArray
* It has common methods on both Wrapper and WrapperArray
*/
interface BaseWrapper {
contains (selector: Selector): boolean
exists (): boolean

hasAttribute (attribute: string, value: string): boolean
hasClass (className: string): boolean
hasProp (prop: string, value: any): boolean
hasStyle (style: string, value: string): boolean

is (selector: Selector): boolean
isEmpty (): boolean
isVueInstance (): boolean

update (): void
setData (data: object): void
setProps (props: object): void
trigger (eventName: string, options?: object): void
}

interface Wrapper<V extends Vue> extends BaseWrapper {
readonly vm: V
readonly element: HTMLElement
readonly options: WrapperOptions

find<R extends Vue> (selector: Selector): Wrapper<R>

Choose a reason for hiding this comment

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

We can separate find to more overloadings. Thus for VClass and CompoentOption there is no need to manually cat.

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 pointing this out.
By the way, I realized a type parameter could not be inferred from other type parameter so we have to manually set a component type if we use component constructors. Does this impossible until microsoft/TypeScript#14400 is solved, right?

// find<R extends Vue, Ctor extends VueClass<R> = VueClass<R>> (selector: Ctor): Wrapper<R>

// `find`'s type parameters will be `<Vue, typeof ClassComponent>`
const found = wrapper.find(ClassComponent)

Choose a reason for hiding this comment

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

Exactly. VueJS really needs it.

findAll<R extends Vue> (selector: Selector): WrapperArray<R>

html (): string
text (): string
name (): string
}

interface WrapperArray<V extends Vue> extends BaseWrapper {
readonly length: number

at (index: number): Wrapper<V>
}

interface WrapperOptions {
attachedToDocument: boolean
}

interface MountOptions<V extends Vue> extends ComponentOptions<V> {

Choose a reason for hiding this comment

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

I wonder the status of vuejs/vue#5887.

That PR will break this declaration. But 5887 does not seem to have activities...

@ktsn @yyx990803 What's your opinion?

I can change 5887 to have a compatible version by changing ComponentOptions, though. @DanielRosenwasser

Copy link
Member Author

Choose a reason for hiding this comment

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

I think vuejs/vue#5887 will not be merged soon since we need to have some migration time for the new declaration. So we can use the current declaration for now.

attachToDocument?: boolean
clone?: boolean
context?: VNodeData

Choose a reason for hiding this comment

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

This is only used by FunctionalComponent, can we make two separate options for different usage?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about that but I ended up to choose type declaration simplicity. Also I have no idea to declare if a constructor of functional component is passed to mount/shallow 🤔

localVue?: typeof Vue
intercept?: object
Copy link
Member

@HerringtonDarkholme HerringtonDarkholme Jul 17, 2017

Choose a reason for hiding this comment

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

@eddyerburgh I wonder why intercept is by default global.

https://github.com/vuejs/vue-test-utils/blob/master/docs/en/api/mount.md

if (options.intercept) {

I know this option is used to be global. But since it is changed its, it seems better to modify localVue by default. Since one mount returns one wrapper, it feels more natural that intercept only modify the wrapper.

If users want to modify Vue's prototype globally, we can, if strongly desired, provide another global API like mount. But fairly users can just Vue.install by themselves.

Copy link
Member

Choose a reason for hiding this comment

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

It doesn't add them globally, it's just a poorly named function. I'll refactor to make that clear

Copy link
Member

Choose a reason for hiding this comment

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

I've refactored in 086f8ae

slots?: Slots
stub?: Stubs

Choose a reason for hiding this comment

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

I wonder why this API is called stub rather than stubs, since it can stub multiple components? @eddyerburgh

Copy link
Member

Choose a reason for hiding this comment

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

I think you're right, stubs is more appropriate 👍

Copy link
Member

Choose a reason for hiding this comment

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

@ktsn I've renamed stub to stubs in 0ff0f63

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated 👍

}

type ShallowOptions<V extends Vue> = MountOptions<V>

export declare function createLocalVue (): typeof Vue

export declare function mount<V extends Vue, Ctor extends VueClass<V> = VueClass<V>> (component: Ctor, options?: MountOptions<V>): Wrapper<V>
export declare function mount<V extends Vue> (component: ComponentOptions<V>, options?: MountOptions<V>): Wrapper<V>
export declare function mount (component: FunctionalComponentOptions, options?: MountOptions<Vue>): Wrapper<Vue>

export declare function shallow<V extends Vue, Ctor extends VueClass<V> = VueClass<V>> (component: Ctor, options?: ShallowOptions<V>): Wrapper<V>
export declare function shallow<V extends Vue> (component: ComponentOptions<V>, options?: ShallowOptions<V>): Wrapper<V>
export declare function shallow (component: FunctionalComponentOptions, options?: ShallowOptions<Vue>): Wrapper<Vue>
62 changes: 62 additions & 0 deletions types/test/mount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Vuex from 'vuex'
import { mount, createLocalVue } from '../'
import { normalOptions, functionalOptions, Normal, ClassComponent } from './resources'

/**
* Should create wrapper vm based on (function) component options or constructors
* The users can specify component type via the type parameter
*/
const normalWrapper = mount<Normal>(normalOptions)
const normalFoo: string = normalWrapper.vm.foo

const classWrapper = mount<ClassComponent>(ClassComponent)
const classFoo: string = classWrapper.vm.bar

const functinalWrapper = mount(functionalOptions)

/**
* Test for mount options
*/
const localVue = createLocalVue()
localVue.use(Vuex)

const store = new Vuex.Store({})

mount<ClassComponent>(ClassComponent, {
attachToDocument: true,
clone: true,
localVue,
intercept: {
$store: store
},
slots: {
default: `<div>Foo</div>`,
foo: [normalOptions, functionalOptions],
bar: ClassComponent
},
stub: {
foo: normalOptions,
bar: functionalOptions,
baz: ClassComponent,
qux: `<div>Test</div>`
}
})

mount(functionalOptions, {
context: {
props: { foo: 'test' }
},
stub: ['child']
})

/**
* MountOptions should receive Vue's component options
*/
mount<ClassComponent>(ClassComponent, {
propsData: {
test: 'test'
},
created () {
this.bar
}
})
33 changes: 33 additions & 0 deletions types/test/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Vue, { ComponentOptions, FunctionalComponentOptions } from 'vue'

/**
* Normal component options
*/
export interface Normal extends Vue {
foo: string
}
export const normalOptions: ComponentOptions<Normal> = {
name: 'normal',
data () {
return {
foo: 'bar'
}
}
}

/**
* Functional component options
*/
export const functionalOptions: FunctionalComponentOptions = {
functional: true,
render (h) {
return h('div')
}
}

/**
* Component constructor declared with vue-class-component etc.
*/
export class ClassComponent extends Vue {
bar = 'bar'
}
62 changes: 62 additions & 0 deletions types/test/shallow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Vuex from 'vuex'
import { shallow, createLocalVue } from '../'
import { normalOptions, functionalOptions, Normal, ClassComponent } from './resources'

/**
* Should create wrapper vm based on (function) component options or constructors
* The users can specify component type via the type parameter
*/
const normalWrapper = shallow<Normal>(normalOptions)
const normalFoo: string = normalWrapper.vm.foo

const classWrapper = shallow<ClassComponent>(ClassComponent)
const classFoo: string = classWrapper.vm.bar

const functinalWrapper = shallow(functionalOptions)

/**
* Test for shallow options
*/
const localVue = createLocalVue()
localVue.use(Vuex)

const store = new Vuex.Store({})

shallow<ClassComponent>(ClassComponent, {
attachToDocument: true,
clone: true,
localVue,
intercept: {
$store: store
},
slots: {
default: `<div>Foo</div>`,
foo: [normalOptions, functionalOptions],
baz: ClassComponent
},
stub: {
foo: normalOptions,
bar: functionalOptions,
baz: ClassComponent,
qux: `<div>Test</div>`
}
})

shallow(functionalOptions, {
context: {
props: { foo: 'test' }
},
stub: ['child']
})

/**
* ShallowOptions should receive Vue's component options
*/
shallow<ClassComponent>(ClassComponent, {
propsData: {
test: 'test'
},
created () {
this.bar
}
})
52 changes: 52 additions & 0 deletions types/test/wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { mount } from '../'
import { normalOptions, Normal, ClassComponent } from './resources'

/**
* Tests for BaseWrapper API
*/
let wrapper = mount<Normal>(normalOptions)

let bool: boolean = wrapper.contains('.foo')
bool = wrapper.contains(normalOptions)
bool = wrapper.contains(ClassComponent)

bool = wrapper.exists()

bool = wrapper.hasAttribute('foo', 'bar')
bool = wrapper.hasClass('foo-class')
bool = wrapper.hasProp('checked', true)
bool = wrapper.hasStyle('color', 'red')

bool = wrapper.is(normalOptions)
bool = wrapper.isEmpty()
bool = wrapper.isVueInstance()

wrapper.update()
wrapper.setData({ foo: 'bar' })
wrapper.setProps({ checked: true })
wrapper.trigger('mousedown.enter', {
preventDefault: true
})

/**
* Tests for Wrapper API
*/
wrapper.vm.foo
wrapper.vm.$emit('event', 'arg')

let el: HTMLElement = wrapper.element

bool = wrapper.options.attachedToDocument

wrapper = wrapper.find('.foo')
let array = wrapper.findAll<Normal>(normalOptions)

let str: string = wrapper.html()
str = wrapper.text()
str = wrapper.name()

/**
* Tests for WrapperArray API
*/
let num: number = array.length
wrapper = array.at(1)
12 changes: 12 additions & 0 deletions types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "es2015",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": [
"**/*.ts"
]
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6342,6 +6342,10 @@ typedarray@^0.0.6, typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"

typescript@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc"

uglify-js@^2.6, uglify-js@^2.8.27:
version "2.8.27"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.27.tgz#47787f912b0f242e5b984343be8e35e95f694c9c"
Expand Down