Skip to content

Commit

Permalink
feat: add classes, attributes, and props methods (#141)
Browse files Browse the repository at this point in the history
* Add classes wrapper method

* Add support for css modules

* Add attributes wrapper method

* Added props wrapper method
  • Loading branch information
tim-hutchinson authored and eddyerburgh committed Nov 20, 2017
1 parent ac4f82f commit 12de854
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/wrappers/error-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export default class ErrorWrapper implements BaseWrapper {
throwError(`find did not return ${this.selector}, cannot call at() on empty Wrapper`)
}

attributes (): void {
throwError(`find did not return ${this.selector}, cannot call attributes() on empty Wrapper`)
}

classes (): void {
throwError(`find did not return ${this.selector}, cannot call classes() on empty Wrapper`)
}

contains (): void {
throwError(`find did not return ${this.selector}, cannot call contains() on empty Wrapper`)
}
Expand Down Expand Up @@ -72,6 +80,10 @@ export default class ErrorWrapper implements BaseWrapper {
throwError(`find did not return ${this.selector}, cannot call name() on empty Wrapper`)
}

props (): void {
throwError(`find did not return ${this.selector}, cannot call props() on empty Wrapper`)
}

text (): void {
throwError(`find did not return ${this.selector}, cannot call text() on empty Wrapper`)
}
Expand Down
18 changes: 18 additions & 0 deletions src/wrappers/wrapper-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ export default class WrapperArray implements BaseWrapper {
return this.wrappers[index]
}

attributes (): void {
this.throwErrorIfWrappersIsEmpty('attributes')

throwError('attributes must be called on a single wrapper, use at(i) to access a wrapper')
}

classes (): void {
this.throwErrorIfWrappersIsEmpty('classes')

throwError('classes must be called on a single wrapper, use at(i) to access a wrapper')
}

contains (selector: Selector): boolean {
this.throwErrorIfWrappersIsEmpty('contains')

Expand Down Expand Up @@ -107,6 +119,12 @@ export default class WrapperArray implements BaseWrapper {
throwError('name must be called on a single wrapper, use at(i) to access a wrapper')
}

props (): void {
this.throwErrorIfWrappersIsEmpty('props')

throwError('props must be called on a single wrapper, use at(i) to access a wrapper')
}

text (): void {
this.throwErrorIfWrappersIsEmpty('text')

Expand Down
49 changes: 49 additions & 0 deletions src/wrappers/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ export default class Wrapper implements BaseWrapper {
throwError('at() must be called on a WrapperArray')
}

/**
* Returns an Object containing all the attribute/value pairs on the element.
*/
attributes (): { [name: string]: string } {
const attributes = [...this.element.attributes] // NameNodeMap is not iterable
const attributeMap = {}
attributes.forEach((att) => {
attributeMap[att.localName] = att.value
})
return attributeMap
}

/**
* Returns an Array containing all the classes on the element
*/
classes (): Array<string> {
let classes = [...this.element.classList]
// Handle converting cssmodules identifiers back to the original class name
if (this.vm && this.vm.$style) {
const cssModuleIdentifiers = {}
let moduleIdent
Object.keys(this.vm.$style).forEach((key) => {
moduleIdent = this.vm.$style[key]
// CSS Modules may be multi-class if they extend others. Extended classes should be already present in $style.
moduleIdent = moduleIdent.split(' ')[0]
cssModuleIdentifiers[moduleIdent] = key
})
classes = classes.map(className => cssModuleIdentifiers[className] || className)
}
return classes
}

/**
* Checks if wrapper contains provided selector.
*/
Expand Down Expand Up @@ -309,6 +341,23 @@ export default class Wrapper implements BaseWrapper {
return this.vnode.tag
}

/**
* Returns an Object containing the prop name/value pairs on the element
*/
props (): { [name: string]: any } {
if (!this.isVueComponent) {
throwError('wrapper.props() must be called on a Vue instance')
}
// $props object does not exist in Vue 2.1.x, so use $options.propsData instead
let _props
if (this.vm && this.vm.$options && this.vm.$options.propsData) {
_props = this.vm.$options.propsData
} else {
_props = this.vm.$props
}
return _props || {} // Return an empty object if no props exist
}

/**
* Sets vm data
*/
Expand Down
7 changes: 6 additions & 1 deletion test/resources/components/component-with-css-modules.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<template>
<div :class="$style['color-red']"></div>
<div :class="$style['extension']"></div>
</template>

<style module>
.color-red {
color: red;
}
.extension {
composes: color-red;
background: blue;
}
</style>

<script>
Expand Down
25 changes: 25 additions & 0 deletions test/unit/specs/mount/Wrapper/attributes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { compileToFunctions } from 'vue-template-compiler'
import mount from '~src/mount'

describe('attributes', () => {
it('returns true if wrapper contains attribute matching value', () => {
const attribute = 'attribute'
const value = 'value'
const compiled = compileToFunctions(`<div ${attribute}=${value}></div>`)
const wrapper = mount(compiled)
expect(wrapper.attributes()).to.eql({ attribute: value })
})

it('returns empty object if wrapper does not contain any attributes', () => {
const compiled = compileToFunctions('<div />')
const wrapper = mount(compiled)
expect(wrapper.attributes()).to.eql({})
})

it('returns empoty object if wrapper element is null', () => {
const compiled = compileToFunctions('<div />')
const wrapper = mount(compiled)
wrapper.element = null
expect(wrapper.attributes()).to.eql({})
})
})
24 changes: 24 additions & 0 deletions test/unit/specs/mount/Wrapper/classes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

import { compileToFunctions } from 'vue-template-compiler'
import mount from '~src/mount'
import ComponentWithCssModules from '~resources/components/component-with-css-modules.vue'

describe('classes', () => {
it('returns array of class names if wrapper has class names', () => {
const compiled = compileToFunctions('<div class="a-class b-class" />')
const wrapper = mount(compiled)
expect(wrapper.classes()).to.eql(['a-class', 'b-class'])
})

it('returns empty array if wrapper has no classes', () => {
const compiled = compileToFunctions('<div />')
const wrapper = mount(compiled)
expect(wrapper.classes()).to.eql([])
})

it('returns original class names when element mapped in css modules', () => {
const wrapper = mount(ComponentWithCssModules)

expect(wrapper.classes()).to.eql(['extension', 'color-red'])
})
})
28 changes: 28 additions & 0 deletions test/unit/specs/mount/Wrapper/props.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { compileToFunctions } from 'vue-template-compiler'
import mount from '~src/mount'
import ComponentWithProps from '~resources/components/component-with-props.vue'

describe('props', () => {
it('returns true if wrapper has prop', () => {
const prop1 = {}
const prop2 = 'string val'
const wrapper = mount(ComponentWithProps, {
propsData: { prop1, prop2 }
})
expect(wrapper.props()).to.eql({ prop1: {}, prop2: 'string val' })
})

it('returns an empty object if wrapper does not have props', () => {
const compiled = compileToFunctions('<div />')
const wrapper = mount(compiled)
expect(wrapper.props()).to.eql({})
})

it('throws an error if called on a non vm wrapper', () => {
const compiled = compileToFunctions('<div><p /></div>')
const p = mount(compiled).findAll('p').at(0)
const message = '[vue-test-utils]: wrapper.props() must be called on a Vue instance'
const fn = () => p.props()
expect(fn).to.throw().with.property('message', message)
})
})
18 changes: 18 additions & 0 deletions test/unit/specs/mount/WrapperArray/attributes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { compileToFunctions } from 'vue-template-compiler'
import mount from '~src/mount'

describe('attributes', () => {
it('throws error if wrapper array contains no items', () => {
const compiled = compileToFunctions('<div />')
const message = '[vue-test-utils]: attributes cannot be called on 0 items'
expect(() => mount(compiled).findAll('p').attributes('p')).to.throw().with.property('message', message)
})

it('throws error when called on a WrapperArray', () => {
const compiled = compileToFunctions('<div><div /></div>')
const wrapper = mount(compiled)
const message = '[vue-test-utils]: attributes must be called on a single wrapper, use at(i) to access a wrapper'
const fn = () => wrapper.findAll('div').attributes()
expect(fn).to.throw().with.property('message', message)
})
})
18 changes: 18 additions & 0 deletions test/unit/specs/mount/WrapperArray/classes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { compileToFunctions } from 'vue-template-compiler'
import mount from '~src/mount'

describe('classes', () => {
it('throws error if wrapper array contains no items', () => {
const compiled = compileToFunctions('<div />')
const message = '[vue-test-utils]: classes cannot be called on 0 items'
expect(() => mount(compiled).findAll('p').classes('p')).to.throw().with.property('message', message)
})

it('throws error when called on a WrapperArray', () => {
const compiled = compileToFunctions('<div><div /></div>')
const wrapper = mount(compiled)
const message = '[vue-test-utils]: classes must be called on a single wrapper, use at(i) to access a wrapper'
const fn = () => wrapper.findAll('div').classes()
expect(fn).to.throw().with.property('message', message)
})
})
18 changes: 18 additions & 0 deletions test/unit/specs/mount/WrapperArray/props.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { compileToFunctions } from 'vue-template-compiler'
import mount from '~src/mount'

describe('props', () => {
it('throws error if wrapper array contains no items', () => {
const compiled = compileToFunctions('<div />')
const message = '[vue-test-utils]: props cannot be called on 0 items'
expect(() => mount(compiled).findAll('p').props('p')).to.throw().with.property('message', message)
})

it('throws error when called on a WrapperArray', () => {
const compiled = compileToFunctions('<div><div /></div>')
const wrapper = mount(compiled)
const message = '[vue-test-utils]: props must be called on a single wrapper, use at(i) to access a wrapper'
const fn = () => wrapper.findAll('div').props()
expect(fn).to.throw().with.property('message', message)
})
})
21 changes: 21 additions & 0 deletions test/unit/specs/wrappers/error-wrapper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ describe('ErrorWrapper', () => {
expect(() => error.at()).to.throw().with.property('message', message)
})

it('attributes throws error when called', () => {
const selector = 'div'
const message = `[vue-test-utils]: find did not return ${selector}, cannot call attributes() on empty Wrapper`
const error = new ErrorWrapper(selector)
expect(() => error.attributes()).to.throw().with.property('message', message)
})

it('classes throws error when called', () => {
const selector = 'div'
const message = `[vue-test-utils]: find did not return ${selector}, cannot call classes() on empty Wrapper`
const error = new ErrorWrapper(selector)
expect(() => error.classes()).to.throw().with.property('message', message)
})

it('contains throws error when called', () => {
const selector = 'div'
const message = `[vue-test-utils]: find did not return ${selector}, cannot call contains() on empty Wrapper`
Expand Down Expand Up @@ -106,6 +120,13 @@ describe('ErrorWrapper', () => {
expect(() => error.name()).to.throw().with.property('message', message)
})

it('props throws error when called', () => {
const selector = 'div'
const message = `[vue-test-utils]: find did not return ${selector}, cannot call props() on empty Wrapper`
const error = new ErrorWrapper(selector)
expect(() => error.props()).to.throw().with.property('message', message)
})

it('text throws error when called', () => {
const selector = 'div'
const message = `[vue-test-utils]: find did not return ${selector}, cannot call text() on empty Wrapper`
Expand Down
36 changes: 36 additions & 0 deletions test/unit/specs/wrappers/wrapper-array.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ describe('WrapperArray', () => {
expect(() => wrapperArray.findAll()).to.throw().with.property('message', message)
})

it('attributes throws error if called when there are 0 items in wrapper array', () => {
const wrapperArray = new WrapperArray()
const message = '[vue-test-utils]: attributes cannot be called on 0 items'
expect(() => wrapperArray.attributes()).to.throw().with.property('message', message)
})

it('attributes throws error if called when there are items in wrapper array', () => {
const wrapperArray = new WrapperArray([1])
const message = '[vue-test-utils]: attributes must be called on a single wrapper, use at(i) to access a wrapper'
expect(() => wrapperArray.attributes()).to.throw().with.property('message', message)
})

it('classes throws error if called when there are 0 items in wrapper array', () => {
const wrapperArray = new WrapperArray()
const message = '[vue-test-utils]: classes cannot be called on 0 items'
expect(() => wrapperArray.classes()).to.throw().with.property('message', message)
})

it('classes throws error if called when there are items in wrapper array', () => {
const wrapperArray = new WrapperArray([1])
const message = '[vue-test-utils]: classes must be called on a single wrapper, use at(i) to access a wrapper'
expect(() => wrapperArray.classes()).to.throw().with.property('message', message)
})

it('contains returns true if every wrapper.contains() returns true', () => {
const selector = 'selector'
const contains = sinon.stub()
Expand Down Expand Up @@ -194,6 +218,18 @@ describe('WrapperArray', () => {
expect(() => wrapperArray.name()).to.throw().with.property('message', message)
})

it('props throws error if called when there are 0 items in wrapper array', () => {
const wrapperArray = new WrapperArray()
const message = '[vue-test-utils]: props cannot be called on 0 items'
expect(() => wrapperArray.props()).to.throw().with.property('message', message)
})

it('props throws error if called when there are items in wrapper array', () => {
const wrapperArray = new WrapperArray([1])
const message = '[vue-test-utils]: props must be called on a single wrapper, use at(i) to access a wrapper'
expect(() => wrapperArray.props()).to.throw().with.property('message', message)
})

it('text throws error if called when there are 0 items in wrapper array', () => {
const wrapperArray = new WrapperArray()
const message = '[vue-test-utils]: text cannot be called on 0 items'
Expand Down

0 comments on commit 12de854

Please sign in to comment.