Skip to content

Commit 12de854

Browse files
tim-hutchinsoneddyerburgh
authored andcommitted
feat: add classes, attributes, and props methods (#141)
* Add classes wrapper method * Add support for css modules * Add attributes wrapper method * Added props wrapper method
1 parent ac4f82f commit 12de854

12 files changed

+273
-1
lines changed

src/wrappers/error-wrapper.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ export default class ErrorWrapper implements BaseWrapper {
1212
throwError(`find did not return ${this.selector}, cannot call at() on empty Wrapper`)
1313
}
1414

15+
attributes (): void {
16+
throwError(`find did not return ${this.selector}, cannot call attributes() on empty Wrapper`)
17+
}
18+
19+
classes (): void {
20+
throwError(`find did not return ${this.selector}, cannot call classes() on empty Wrapper`)
21+
}
22+
1523
contains (): void {
1624
throwError(`find did not return ${this.selector}, cannot call contains() on empty Wrapper`)
1725
}
@@ -72,6 +80,10 @@ export default class ErrorWrapper implements BaseWrapper {
7280
throwError(`find did not return ${this.selector}, cannot call name() on empty Wrapper`)
7381
}
7482

83+
props (): void {
84+
throwError(`find did not return ${this.selector}, cannot call props() on empty Wrapper`)
85+
}
86+
7587
text (): void {
7688
throwError(`find did not return ${this.selector}, cannot call text() on empty Wrapper`)
7789
}

src/wrappers/wrapper-array.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ export default class WrapperArray implements BaseWrapper {
2020
return this.wrappers[index]
2121
}
2222

23+
attributes (): void {
24+
this.throwErrorIfWrappersIsEmpty('attributes')
25+
26+
throwError('attributes must be called on a single wrapper, use at(i) to access a wrapper')
27+
}
28+
29+
classes (): void {
30+
this.throwErrorIfWrappersIsEmpty('classes')
31+
32+
throwError('classes must be called on a single wrapper, use at(i) to access a wrapper')
33+
}
34+
2335
contains (selector: Selector): boolean {
2436
this.throwErrorIfWrappersIsEmpty('contains')
2537

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

122+
props (): void {
123+
this.throwErrorIfWrappersIsEmpty('props')
124+
125+
throwError('props must be called on a single wrapper, use at(i) to access a wrapper')
126+
}
127+
110128
text (): void {
111129
this.throwErrorIfWrappersIsEmpty('text')
112130

src/wrappers/wrapper.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,38 @@ export default class Wrapper implements BaseWrapper {
3333
throwError('at() must be called on a WrapperArray')
3434
}
3535

36+
/**
37+
* Returns an Object containing all the attribute/value pairs on the element.
38+
*/
39+
attributes (): { [name: string]: string } {
40+
const attributes = [...this.element.attributes] // NameNodeMap is not iterable
41+
const attributeMap = {}
42+
attributes.forEach((att) => {
43+
attributeMap[att.localName] = att.value
44+
})
45+
return attributeMap
46+
}
47+
48+
/**
49+
* Returns an Array containing all the classes on the element
50+
*/
51+
classes (): Array<string> {
52+
let classes = [...this.element.classList]
53+
// Handle converting cssmodules identifiers back to the original class name
54+
if (this.vm && this.vm.$style) {
55+
const cssModuleIdentifiers = {}
56+
let moduleIdent
57+
Object.keys(this.vm.$style).forEach((key) => {
58+
moduleIdent = this.vm.$style[key]
59+
// CSS Modules may be multi-class if they extend others. Extended classes should be already present in $style.
60+
moduleIdent = moduleIdent.split(' ')[0]
61+
cssModuleIdentifiers[moduleIdent] = key
62+
})
63+
classes = classes.map(className => cssModuleIdentifiers[className] || className)
64+
}
65+
return classes
66+
}
67+
3668
/**
3769
* Checks if wrapper contains provided selector.
3870
*/
@@ -309,6 +341,23 @@ export default class Wrapper implements BaseWrapper {
309341
return this.vnode.tag
310342
}
311343

344+
/**
345+
* Returns an Object containing the prop name/value pairs on the element
346+
*/
347+
props (): { [name: string]: any } {
348+
if (!this.isVueComponent) {
349+
throwError('wrapper.props() must be called on a Vue instance')
350+
}
351+
// $props object does not exist in Vue 2.1.x, so use $options.propsData instead
352+
let _props
353+
if (this.vm && this.vm.$options && this.vm.$options.propsData) {
354+
_props = this.vm.$options.propsData
355+
} else {
356+
_props = this.vm.$props
357+
}
358+
return _props || {} // Return an empty object if no props exist
359+
}
360+
312361
/**
313362
* Sets vm data
314363
*/

test/resources/components/component-with-css-modules.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
<template>
2-
<div :class="$style['color-red']"></div>
2+
<div :class="$style['extension']"></div>
33
</template>
44

55
<style module>
66
.color-red {
77
color: red;
88
}
9+
10+
.extension {
11+
composes: color-red;
12+
background: blue;
13+
}
914
</style>
1015

1116
<script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { compileToFunctions } from 'vue-template-compiler'
2+
import mount from '~src/mount'
3+
4+
describe('attributes', () => {
5+
it('returns true if wrapper contains attribute matching value', () => {
6+
const attribute = 'attribute'
7+
const value = 'value'
8+
const compiled = compileToFunctions(`<div ${attribute}=${value}></div>`)
9+
const wrapper = mount(compiled)
10+
expect(wrapper.attributes()).to.eql({ attribute: value })
11+
})
12+
13+
it('returns empty object if wrapper does not contain any attributes', () => {
14+
const compiled = compileToFunctions('<div />')
15+
const wrapper = mount(compiled)
16+
expect(wrapper.attributes()).to.eql({})
17+
})
18+
19+
it('returns empoty object if wrapper element is null', () => {
20+
const compiled = compileToFunctions('<div />')
21+
const wrapper = mount(compiled)
22+
wrapper.element = null
23+
expect(wrapper.attributes()).to.eql({})
24+
})
25+
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
import { compileToFunctions } from 'vue-template-compiler'
3+
import mount from '~src/mount'
4+
import ComponentWithCssModules from '~resources/components/component-with-css-modules.vue'
5+
6+
describe('classes', () => {
7+
it('returns array of class names if wrapper has class names', () => {
8+
const compiled = compileToFunctions('<div class="a-class b-class" />')
9+
const wrapper = mount(compiled)
10+
expect(wrapper.classes()).to.eql(['a-class', 'b-class'])
11+
})
12+
13+
it('returns empty array if wrapper has no classes', () => {
14+
const compiled = compileToFunctions('<div />')
15+
const wrapper = mount(compiled)
16+
expect(wrapper.classes()).to.eql([])
17+
})
18+
19+
it('returns original class names when element mapped in css modules', () => {
20+
const wrapper = mount(ComponentWithCssModules)
21+
22+
expect(wrapper.classes()).to.eql(['extension', 'color-red'])
23+
})
24+
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { compileToFunctions } from 'vue-template-compiler'
2+
import mount from '~src/mount'
3+
import ComponentWithProps from '~resources/components/component-with-props.vue'
4+
5+
describe('props', () => {
6+
it('returns true if wrapper has prop', () => {
7+
const prop1 = {}
8+
const prop2 = 'string val'
9+
const wrapper = mount(ComponentWithProps, {
10+
propsData: { prop1, prop2 }
11+
})
12+
expect(wrapper.props()).to.eql({ prop1: {}, prop2: 'string val' })
13+
})
14+
15+
it('returns an empty object if wrapper does not have props', () => {
16+
const compiled = compileToFunctions('<div />')
17+
const wrapper = mount(compiled)
18+
expect(wrapper.props()).to.eql({})
19+
})
20+
21+
it('throws an error if called on a non vm wrapper', () => {
22+
const compiled = compileToFunctions('<div><p /></div>')
23+
const p = mount(compiled).findAll('p').at(0)
24+
const message = '[vue-test-utils]: wrapper.props() must be called on a Vue instance'
25+
const fn = () => p.props()
26+
expect(fn).to.throw().with.property('message', message)
27+
})
28+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { compileToFunctions } from 'vue-template-compiler'
2+
import mount from '~src/mount'
3+
4+
describe('attributes', () => {
5+
it('throws error if wrapper array contains no items', () => {
6+
const compiled = compileToFunctions('<div />')
7+
const message = '[vue-test-utils]: attributes cannot be called on 0 items'
8+
expect(() => mount(compiled).findAll('p').attributes('p')).to.throw().with.property('message', message)
9+
})
10+
11+
it('throws error when called on a WrapperArray', () => {
12+
const compiled = compileToFunctions('<div><div /></div>')
13+
const wrapper = mount(compiled)
14+
const message = '[vue-test-utils]: attributes must be called on a single wrapper, use at(i) to access a wrapper'
15+
const fn = () => wrapper.findAll('div').attributes()
16+
expect(fn).to.throw().with.property('message', message)
17+
})
18+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { compileToFunctions } from 'vue-template-compiler'
2+
import mount from '~src/mount'
3+
4+
describe('classes', () => {
5+
it('throws error if wrapper array contains no items', () => {
6+
const compiled = compileToFunctions('<div />')
7+
const message = '[vue-test-utils]: classes cannot be called on 0 items'
8+
expect(() => mount(compiled).findAll('p').classes('p')).to.throw().with.property('message', message)
9+
})
10+
11+
it('throws error when called on a WrapperArray', () => {
12+
const compiled = compileToFunctions('<div><div /></div>')
13+
const wrapper = mount(compiled)
14+
const message = '[vue-test-utils]: classes must be called on a single wrapper, use at(i) to access a wrapper'
15+
const fn = () => wrapper.findAll('div').classes()
16+
expect(fn).to.throw().with.property('message', message)
17+
})
18+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { compileToFunctions } from 'vue-template-compiler'
2+
import mount from '~src/mount'
3+
4+
describe('props', () => {
5+
it('throws error if wrapper array contains no items', () => {
6+
const compiled = compileToFunctions('<div />')
7+
const message = '[vue-test-utils]: props cannot be called on 0 items'
8+
expect(() => mount(compiled).findAll('p').props('p')).to.throw().with.property('message', message)
9+
})
10+
11+
it('throws error when called on a WrapperArray', () => {
12+
const compiled = compileToFunctions('<div><div /></div>')
13+
const wrapper = mount(compiled)
14+
const message = '[vue-test-utils]: props must be called on a single wrapper, use at(i) to access a wrapper'
15+
const fn = () => wrapper.findAll('div').props()
16+
expect(fn).to.throw().with.property('message', message)
17+
})
18+
})

test/unit/specs/wrappers/error-wrapper.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ describe('ErrorWrapper', () => {
88
expect(() => error.at()).to.throw().with.property('message', message)
99
})
1010

11+
it('attributes throws error when called', () => {
12+
const selector = 'div'
13+
const message = `[vue-test-utils]: find did not return ${selector}, cannot call attributes() on empty Wrapper`
14+
const error = new ErrorWrapper(selector)
15+
expect(() => error.attributes()).to.throw().with.property('message', message)
16+
})
17+
18+
it('classes throws error when called', () => {
19+
const selector = 'div'
20+
const message = `[vue-test-utils]: find did not return ${selector}, cannot call classes() on empty Wrapper`
21+
const error = new ErrorWrapper(selector)
22+
expect(() => error.classes()).to.throw().with.property('message', message)
23+
})
24+
1125
it('contains throws error when called', () => {
1226
const selector = 'div'
1327
const message = `[vue-test-utils]: find did not return ${selector}, cannot call contains() on empty Wrapper`
@@ -106,6 +120,13 @@ describe('ErrorWrapper', () => {
106120
expect(() => error.name()).to.throw().with.property('message', message)
107121
})
108122

123+
it('props throws error when called', () => {
124+
const selector = 'div'
125+
const message = `[vue-test-utils]: find did not return ${selector}, cannot call props() on empty Wrapper`
126+
const error = new ErrorWrapper(selector)
127+
expect(() => error.props()).to.throw().with.property('message', message)
128+
})
129+
109130
it('text throws error when called', () => {
110131
const selector = 'div'
111132
const message = `[vue-test-utils]: find did not return ${selector}, cannot call text() on empty Wrapper`

test/unit/specs/wrappers/wrapper-array.spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,30 @@ describe('WrapperArray', () => {
5959
expect(() => wrapperArray.findAll()).to.throw().with.property('message', message)
6060
})
6161

62+
it('attributes throws error if called when there are 0 items in wrapper array', () => {
63+
const wrapperArray = new WrapperArray()
64+
const message = '[vue-test-utils]: attributes cannot be called on 0 items'
65+
expect(() => wrapperArray.attributes()).to.throw().with.property('message', message)
66+
})
67+
68+
it('attributes throws error if called when there are items in wrapper array', () => {
69+
const wrapperArray = new WrapperArray([1])
70+
const message = '[vue-test-utils]: attributes must be called on a single wrapper, use at(i) to access a wrapper'
71+
expect(() => wrapperArray.attributes()).to.throw().with.property('message', message)
72+
})
73+
74+
it('classes throws error if called when there are 0 items in wrapper array', () => {
75+
const wrapperArray = new WrapperArray()
76+
const message = '[vue-test-utils]: classes cannot be called on 0 items'
77+
expect(() => wrapperArray.classes()).to.throw().with.property('message', message)
78+
})
79+
80+
it('classes throws error if called when there are items in wrapper array', () => {
81+
const wrapperArray = new WrapperArray([1])
82+
const message = '[vue-test-utils]: classes must be called on a single wrapper, use at(i) to access a wrapper'
83+
expect(() => wrapperArray.classes()).to.throw().with.property('message', message)
84+
})
85+
6286
it('contains returns true if every wrapper.contains() returns true', () => {
6387
const selector = 'selector'
6488
const contains = sinon.stub()
@@ -194,6 +218,18 @@ describe('WrapperArray', () => {
194218
expect(() => wrapperArray.name()).to.throw().with.property('message', message)
195219
})
196220

221+
it('props throws error if called when there are 0 items in wrapper array', () => {
222+
const wrapperArray = new WrapperArray()
223+
const message = '[vue-test-utils]: props cannot be called on 0 items'
224+
expect(() => wrapperArray.props()).to.throw().with.property('message', message)
225+
})
226+
227+
it('props throws error if called when there are items in wrapper array', () => {
228+
const wrapperArray = new WrapperArray([1])
229+
const message = '[vue-test-utils]: props must be called on a single wrapper, use at(i) to access a wrapper'
230+
expect(() => wrapperArray.props()).to.throw().with.property('message', message)
231+
})
232+
197233
it('text throws error if called when there are 0 items in wrapper array', () => {
198234
const wrapperArray = new WrapperArray()
199235
const message = '[vue-test-utils]: text cannot be called on 0 items'

0 commit comments

Comments
 (0)