diff --git a/docs/api/wrapper/overview.md b/docs/api/wrapper/overview.md new file mode 100644 index 000000000..2130b6e46 --- /dev/null +++ b/docs/api/wrapper/overview.md @@ -0,0 +1,43 @@ +## overview + +Prints a simple overview of the `Wrapper`. + +- **Example:** + +```js +import { mount } from '@vue/test-utils' +import Component from './Component.vue' + +const wrapper = mount(Component) +wrapper.overview() + +// Console output +/* +Wrapper (Visible): + +Html: +
+

My name is Tess Ting

+
+ +Data: { + firstName: Tess, + lastName: Ting +} + +Computed: { + fullName: Tess Ting' +} + +Emitted: {', + foo: [', + 0: [ hello, world ], + 1: [ bye, world ]' + ], + bar: [ + 0: [ hey ]' + ] +} + +*/ +``` diff --git a/flow/wrapper.flow.js b/flow/wrapper.flow.js index 8bc2d2bf2..e37c3d558 100644 --- a/flow/wrapper.flow.js +++ b/flow/wrapper.flow.js @@ -26,6 +26,7 @@ declare interface BaseWrapper { isVisible(): boolean | void; isVueInstance(): boolean | void; name(): string | void; + overview(): void; props(key?: string): { [name: string]: any } | any | void; text(): string | void; selector: Selector | void; diff --git a/packages/test-utils/src/error-wrapper.js b/packages/test-utils/src/error-wrapper.js index 3365e1017..5937c1c25 100644 --- a/packages/test-utils/src/error-wrapper.js +++ b/packages/test-utils/src/error-wrapper.js @@ -187,6 +187,14 @@ export default class ErrorWrapper implements BaseWrapper { ) } + overview(): void { + throwError( + `find did not return ${buildSelectorString( + this.selector + )}, cannot call overview() on empty Wrapper` + ) + } + props(): void { throwError( `find did not return ${buildSelectorString( diff --git a/packages/test-utils/src/wrapper-array.js b/packages/test-utils/src/wrapper-array.js index 6a15bcb6d..12ca36844 100644 --- a/packages/test-utils/src/wrapper-array.js +++ b/packages/test-utils/src/wrapper-array.js @@ -143,6 +143,15 @@ export default class WrapperArray implements BaseWrapper { ) } + overview(): void { + this.throwErrorIfWrappersIsEmpty('overview()') + + throwError( + `overview() must be called on a single wrapper, use at(i) ` + + `to access a wrapper` + ) + } + props(): void { this.throwErrorIfWrappersIsEmpty('props') diff --git a/packages/test-utils/src/wrapper.js b/packages/test-utils/src/wrapper.js index ea31cf192..89e8d707d 100644 --- a/packages/test-utils/src/wrapper.js +++ b/packages/test-utils/src/wrapper.js @@ -324,6 +324,77 @@ export default class Wrapper implements BaseWrapper { return this.vnode.tag } + /** + * Prints a simple overview of the wrapper current state + * with useful information for debugging + */ + overview(): void { + if (!this.isVueInstance()) { + throwError(`wrapper.overview() can only be called on a Vue instance`) + } + + const identation = 4 + const formatJSON = (json: any, replacer: Function | null = null) => + JSON.stringify(json, replacer, identation).replace(/"/g, '') + + const visibility = this.isVisible() ? 'Visible' : 'Not visible' + + const html = this.html() + ? this.html().replace(/^(?!\s*$)/gm, ' '.repeat(identation)) + '\n' + : '' + + // $FlowIgnore + const data = formatJSON(this.vm.$data) + + /* eslint-disable operator-linebreak */ + // $FlowIgnore + const computed = this.vm._computedWatchers + ? formatJSON( + // $FlowIgnore + ...Object.keys(this.vm._computedWatchers).map(computedKey => ({ + // $FlowIgnore + [computedKey]: this.vm[computedKey] + })) + ) + : // $FlowIgnore + this.vm.$options.computed + ? formatJSON( + // $FlowIgnore + ...Object.entries(this.vm.$options.computed).map(([key, value]) => ({ + // $FlowIgnore + [key]: value() + })) + ) + : '{}' + /* eslint-enable operator-linebreak */ + + const emittedJSONReplacer = (key, value) => + value instanceof Array + ? value.map((calledWith, index) => { + const callParams = calledWith.map(param => + typeof param === 'object' + ? JSON.stringify(param) + .replace(/"/g, '') + .replace(/,/g, ', ') + : param + ) + + return `${index}: [ ${callParams.join(', ')} ]` + }) + : value + + const emitted = formatJSON(this.emitted(), emittedJSONReplacer) + + console.log( + '\n' + + `Wrapper (${visibility}):\n\n` + + `Html:\n${html}\n` + + `Data: ${data}\n\n` + + `Computed: ${computed}\n\n` + + `Emitted: ${emitted}\n` + ) + } + /** * Returns an Object containing the prop name/value pairs on the element */ diff --git a/test/specs/error-wrapper.spec.js b/test/specs/error-wrapper.spec.js index 0eb610696..3357194c9 100644 --- a/test/specs/error-wrapper.spec.js +++ b/test/specs/error-wrapper.spec.js @@ -22,6 +22,7 @@ describeWithShallowAndMount('ErrorWrapper', mountingMethod => { 'isVisible', 'isVueInstance', 'name', + 'overview', 'props', 'setComputed', 'setMethods', diff --git a/test/specs/wrapper-array/overview.spec.js b/test/specs/wrapper-array/overview.spec.js new file mode 100644 index 000000000..5bdb37a8e --- /dev/null +++ b/test/specs/wrapper-array/overview.spec.js @@ -0,0 +1,24 @@ +import { describeWithShallowAndMount } from '~resources/utils' +import { compileToFunctions } from 'vue-template-compiler' +import '@vue/test-utils' + +describeWithShallowAndMount('overview', mountingMethod => { + it('throws error if wrapper array contains no items', () => { + const wrapper = mountingMethod(compileToFunctions('
')) + const message = '[vue-test-utils]: overview() cannot be called on 0 items' + + expect(() => wrapper.findAll('p').overview()) + .to.throw() + .with.property('message', message) + }) + + it('throws error when called on a WrapperArray', () => { + const wrapper = mountingMethod(compileToFunctions('
')) + const message = + '[vue-test-utils]: overview() must be called on a single wrapper, use at(i) to access a wrapper' + + expect(() => wrapper.findAll('div').overview()) + .to.throw() + .with.property('message', message) + }) +}) diff --git a/test/specs/wrapper/overview.spec.js b/test/specs/wrapper/overview.spec.js new file mode 100644 index 000000000..16c2d7965 --- /dev/null +++ b/test/specs/wrapper/overview.spec.js @@ -0,0 +1,408 @@ +import { describeWithShallowAndMount, vueVersion } from '~resources/utils' + +describeWithShallowAndMount('overview', mountingMethod => { + const originalLog = console.log + let consoleOutput = [] + const consoleLogMock = (...output) => + consoleOutput.push(...output.join(' ').split('\n')) + + beforeEach(() => { + consoleOutput = [] + console.log = consoleLogMock + }) + afterEach(() => (console.log = originalLog)) + + it('throws error when called on non VueWrapper', () => { + const wrapper = mountingMethod({ template: '

' }) + const nonVueWrapper = wrapper.find('p') + const message = + '[vue-test-utils]: wrapper.overview() can only be called on a Vue instance' + + expect(() => nonVueWrapper.overview()) + .to.throw() + .with.property('message', message) + }) + + if (vueVersion > 2) { + it('prints a simple overview of the Wrapper', () => { + const wrapper = mountingMethod({ + template: + '

My name is {{ firstName }} {{ lastName }}

', + data() { + return { + firstName: 'Tess', + lastName: 'Ting' + } + }, + computed: { + onePlusOne: () => 1 + 1 + } + }) + wrapper.vm.$emit('foo', 'hello', 'world') + wrapper.vm.$emit('foo', 'bye', 'world') + wrapper.vm.$emit('bar', 'hey') + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '

My name is Tess Ting

', + '
', + '', + 'Data: {', + ' firstName: Tess,', + ' lastName: Ting', + '}', + '', + 'Computed: {', + ' onePlusOne: 2', + '}', + '', + 'Emitted: {', + ' foo: [', + ' 0: [ hello, world ],', + ' 1: [ bye, world ]', + ' ],', + ' bar: [', + ' 0: [ hey ]', + ' ]', + '}', + '' + ] + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + + describe('vibility', () => { + it('prints "Visible" when the wrapper is visible', () => { + const wrapper = mountingMethod({ template: '
' }) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {}', + '' + ] + + wrapper.isVisible = () => true + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + + it('prints "Not Visible" when the wrapper is not visible', () => { + const wrapper = mountingMethod({ template: '
' }) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Not visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {}', + '' + ] + + wrapper.isVisible = () => false + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + }) + + describe('html', () => { + it('prints Html as empty when html is not defined', () => { + const wrapper = mountingMethod({ template: '' }) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {}', + '' + ] + + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + }) + + describe('data', () => { + it('prints Data as {} when data is empty', () => { + const wrapper = mountingMethod({ + template: '
', + computed: { + onePlusOne: () => 1 + 1 + } + }) + wrapper.vm.$emit('foo', 'hello', 'world') + wrapper.vm.$emit('foo', 'bye', 'world') + wrapper.vm.$emit('bar', 'hey') + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {', + ' onePlusOne: 2', + '}', + '', + 'Emitted: {', + ' foo: [', + ' 0: [ hello, world ],', + ' 1: [ bye, world ]', + ' ],', + ' bar: [', + ' 0: [ hey ]', + ' ]', + '}', + '' + ] + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + }) + + describe('computed', () => { + it('prints Computed as {} when data is empty', () => { + const wrapper = mountingMethod({ + template: '
' + }) + wrapper.vm.$emit('foo', 'hello', 'world') + wrapper.vm.$emit('foo', 'bye', 'world') + wrapper.vm.$emit('bar', 'hey') + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {', + ' foo: [', + ' 0: [ hello, world ],', + ' 1: [ bye, world ]', + ' ],', + ' bar: [', + ' 0: [ hey ]', + ' ]', + '}', + '' + ] + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + }) + + describe('emitted events', () => { + it('prints Emitted as {} when no events have been emitted', () => { + const wrapper = mountingMethod({ + template: '
' + }) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {}', + '' + ] + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + + it('prints an empty array in Emitted arrays of calls when emit was empty', () => { + const wrapper = mountingMethod({ + template: '
' + }) + + wrapper.vm.$emit('foo') + wrapper.vm.$emit('foo') + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {', + ' foo: [', + ' 0: [ ],', + ' 1: [ ]', + ' ]', + '}', + '' + ] + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + + it('prints inline formated object in Emitted arrays of calls when an object has been emitted', () => { + const wrapper = mountingMethod({ + template: '
' + }) + + wrapper.vm.$emit('foo', { + title: 'How to test', + author: 'Tester', + price: 10 + }) + wrapper.vm.$emit( + 'foo', + { title: 'How to test 2', author: 'Tester Jr', price: 12 }, + 'New' + ) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {', + ' foo: [', + ' 0: [ {title:How to test, author:Tester, price:10} ],', + ' 1: [ {title:How to test 2, author:Tester Jr, price:12}, New ]', + ' ]', + '}', + '' + ] + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + }) + + describe('child components', () => { + it('prints children compenents HTML', () => { + const wrapper = mountingMethod({ + template: `
1
`, + components: { + tester: { + template: `
test
` + } + } + }) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + mountingMethod.name === 'shallowMount' + ? '
1' + : '
1
test
', + '
', + '', + 'Data: {}', + '', + 'Computed: {}', + '', + 'Emitted: {}', + '' + ] + + wrapper.isVisible = () => true + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + + it('does not print child component data or computed', () => { + const wrapper = mountingMethod({ + template: `
1
`, + data() { + return { + fathersMessage: 'I am your father' + } + }, + computed: { + onePlusOne: () => 1 + 1 + }, + components: { + tester: { + template: `
test
`, + data() { + return { + something: 'hiden' + } + }, + computed: { + twoPlusTwo: () => 2 + 2 + } + } + } + }) + + const expectedConsoleOutput = [ + '', + 'Wrapper (Visible):', + '', + 'Html:', + mountingMethod.name === 'shallowMount' + ? '
1' + : '
1
test
', + '
', + '', + 'Data: {', + ' fathersMessage: I am your father', + '}', + '', + 'Computed: {', + ' onePlusOne: 2', + '}', + '', + 'Emitted: {}', + '' + ] + + wrapper.overview() + expect(consoleOutput).to.have.ordered.members(expectedConsoleOutput) + }) + }) + } +})