diff --git a/docs/en/README.md b/docs/en/README.md index cb1468fcd..84919dbc2 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -18,6 +18,7 @@ * [API](api/README.md) * [mount](api/mount.md) * [shallow](api/shallow.md) + * [renderToString](api/renderToString.md) * [Mounting Options](api/options.md) - [context](api/options.md#context) - [slots](api/options.md#slots) diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 0a5379876..f3c276469 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -14,6 +14,7 @@ * [API](api/README.md) * [mount](api/mount.md) * [shallow](api/shallow.md) + * [renderToString](api/renderToString.md) * [Mounting Options](api/options.md) - [context](api/options.md#context) - [slots](api/options.md#slots) diff --git a/docs/en/api/README.md b/docs/en/api/README.md index e0bce900d..d636709b0 100644 --- a/docs/en/api/README.md +++ b/docs/en/api/README.md @@ -2,6 +2,7 @@ * [mount](./mount.md) * [shallow](./shallow.md) +* [renderToString](./renderToString.md) * [Mounting Options](./options.md) - [context](./options.md#context) - [slots](./options.md#slots) diff --git a/docs/en/api/renderToString.md b/docs/en/api/renderToString.md new file mode 100644 index 000000000..4955c616a --- /dev/null +++ b/docs/en/api/renderToString.md @@ -0,0 +1,103 @@ +# `renderToString(component {, options}])` + +- **Arguments:** + + - `{Component} component` + - `{Object} options` + - `{Object} context` + - `{Array|Component} children` + - `{Object} slots` + - `{Array|Component|String} default` + - `{Array|Component|String} named` + - `{Object} mocks` + - `{Object|Array} stubs` + - `{Vue} localVue` + +- **Returns:** `{string}` + +- **Options:** + +See [options](./options.md) + +- **Usage:** + +Renders a component to HTML. + +`renderToString` uses [`vue-server-renderer`](https://ssr.vuejs.org/en/basic.html) under the hood, to render a component to HTML. + +**Without options:** + +```js +import { renderToString } from '@vue/test-utils' +import { expect } from 'chai' +import Foo from './Foo.vue' + +describe('Foo', () => { + it('renders a div', () => { + const renderedString = renderToString(Foo) + expect(renderedString).toContain('
') + }) +}) +``` + +**With Vue options:** + +```js +import { renderToString } from '@vue/test-utils' +import { expect } from 'chai' +import Foo from './Foo.vue' + +describe('Foo', () => { + it('renders a div', () => { + const renderedString = renderToString(Foo, { + propsData: { + color: 'red' + } + }) + expect(renderedString).toContain('red') + }) +}) +``` + +**Default and named slots:** + +```js +import { renderToString } from '@vue/test-utils' +import { expect } from 'chai' +import Foo from './Foo.vue' +import Bar from './Bar.vue' +import FooBar from './FooBar.vue' + +describe('Foo', () => { + it('renders a div', () => { + const renderedString = renderToString(Foo, { + slots: { + default: [Bar, FooBar], + fooBar: FooBar, // Will match , + foo: '
' + } + }) + expect(renderedString).toContain('
') + }) +}) +``` + +**Stubbing global properties:** + +```js +import { renderToString } from '@vue/test-utils' +import { expect } from 'chai' +import Foo from './Foo.vue' + +describe('Foo', () => { + it('renders a div', () => { + const $route = { path: 'http://www.example-path.com' } + const renderedString = renderToString(Foo, { + mocks: { + $route + } + }) + expect(renderedString).toContain($route.path) + }) +}) +``` diff --git a/flow/modules.flow.js b/flow/modules.flow.js index 730828917..eb193125e 100644 --- a/flow/modules.flow.js +++ b/flow/modules.flow.js @@ -15,3 +15,7 @@ declare module 'lodash/cloneDeep' { declare module 'vue-template-compiler' { declare module.exports: any; } + +declare module 'vue-server-renderer' { + declare module.exports: any; +} diff --git a/package.json b/package.json index a6f4c0c4a..3e4c1e1db 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "vue-class-component": "^6.1.2", "vue-loader": "^13.6.2", "vue-router": "^3.0.1", + "vue-server-renderer": "2.5.13", "vue-template-compiler": "^2.5.13", "vuetify": "^0.16.9", "vuex": "^3.0.1", @@ -96,6 +97,7 @@ }, "peerDependencies": { "vue": "2.x", + "vue-server-renderer": "2.x", "vue-template-compiler": "^2.x" }, "dependencies": { diff --git a/scripts/test-compat.sh b/scripts/test-compat.sh index 6f6d94ed4..79e12ee23 100755 --- a/scripts/test-compat.sh +++ b/scripts/test-compat.sh @@ -4,7 +4,7 @@ set -e test_version_number(){ echo "running unit tests with Vue $1" - yarn add vue@$1 vue-template-compiler@$1 + yarn add vue@$1 vue-template-compiler@$1 vue-server-renderer@$1 yarn test:unit yarn test:unit:karma } diff --git a/src/index.js b/src/index.js index 3eb71602b..bae2ffc52 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import shallow from './shallow' import mount from './mount' +import renderToString from './renderToString' import createLocalVue from './create-local-vue' import TransitionStub from './components/TransitionStub' import TransitionGroupStub from './components/TransitionGroupStub' @@ -11,6 +12,7 @@ export default { config, mount, shallow, + renderToString, TransitionStub, TransitionGroupStub, RouterLinkStub diff --git a/src/lib/add-slots.js b/src/lib/add-slots.js index d0583d640..5e1619a99 100644 --- a/src/lib/add-slots.js +++ b/src/lib/add-slots.js @@ -54,6 +54,9 @@ function addSlots (vm: Component, slots: Object): void { if (Array.isArray(slots[key])) { slots[key].forEach((slotValue) => { + if (!isValidSlot(slotValue)) { + throwError('slots[key] must be a Component, string or an array of Components') + } addSlotToVm(vm, key, slotValue) }) } else { diff --git a/src/mount.js b/src/mount.js index 3fc0d350c..2e27b43ae 100644 --- a/src/mount.js +++ b/src/mount.js @@ -11,8 +11,8 @@ import errorHandler from './lib/error-handler' import { findAllVueComponentsFromVm } from './lib/find-vue-components' Vue.config.productionTip = false -Vue.config.errorHandler = errorHandler Vue.config.devtools = false +Vue.config.errorHandler = errorHandler export default function mount (component: Component, options: Options = {}): VueWrapper { // Remove cached constructor diff --git a/src/renderToString.js b/src/renderToString.js new file mode 100644 index 000000000..c040f88f2 --- /dev/null +++ b/src/renderToString.js @@ -0,0 +1,39 @@ +// @flow + +import Vue from 'vue' +import createInstance from './lib/create-instance' +import './lib/polyfills/object-assign-polyfill' +import { throwError } from './lib/util' + +Vue.config.productionTip = false +Vue.config.devtools = false + +export default function renderToString (component: Component, options: Options = {}): string { + let renderer + try { + renderer = require('vue-server-renderer').createRenderer() + } catch (e) {} + + if (!renderer) { + throwError('renderToString must be run in node. It cannot be run in a browser') + } + // Remove cached constructor + delete component._Ctor + + if (options.attachToDocument) { + throwError('you cannot use attachToDocument with renderToString') + } + + const vm = createInstance(component, options) + + let renderedString = '' + + // $FlowIgnore + renderer.renderToString(vm, (err, res) => { + if (err) { + console.log(err) + } + renderedString = res + }) + return renderedString +} diff --git a/test/resources/test-utils.js b/test/resources/test-utils.js index 5350ccb8b..3770243c1 100644 --- a/test/resources/test-utils.js +++ b/test/resources/test-utils.js @@ -1,10 +1,12 @@ /* global describe, it*/ import Vue from 'vue' -import { shallow, mount } from '~vue-test-utils' +import { shallow, mount, renderToString } from '~vue-test-utils' export const vueVersion = Number(`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`) +export const isRunningJSDOM = navigator.userAgent.includes && navigator.userAgent.includes('jsdom') + export function injectSupported () { return vueVersion > 2.2 } @@ -21,20 +23,43 @@ export function functionalSFCsSupported () { return vueVersion >= 2.5 } +const shallowAndMount = [mount, shallow] +const shallowMountAndRender = isRunningJSDOM + ? [mount, shallow, renderToString] + : [mount, shallow] + export function describeWithShallowAndMount (spec, cb) { - ;[mount, shallow].forEach(method => { + shallowAndMount.forEach(method => { describe(`${spec} with ${method.name}`, () => cb(method)) }) } describeWithShallowAndMount.skip = function (spec, cb) { - ;[mount, shallow].forEach(method => { + shallowAndMount.forEach(method => { describe.skip(`${spec} with ${method.name}`, () => cb(method)) }) } describeWithShallowAndMount.only = function (spec, cb) { - ;[mount, shallow].forEach(method => { + shallowAndMount.forEach(method => { + describe.only(`${spec} with ${method.name}`, () => cb(method)) + }) +} + +export function describeWithMountingMethods (spec, cb) { + shallowMountAndRender.forEach(method => { + describe(`${spec} with ${method.name}`, () => cb(method)) + }) +} + +describeWithMountingMethods.skip = function (spec, cb) { + shallowMountAndRender.forEach(method => { + describe.skip(`${spec} with ${method.name}`, () => cb(method)) + }) +} + +describeWithMountingMethods.only = function (spec, cb) { + shallowMountAndRender.forEach(method => { describe.only(`${spec} with ${method.name}`, () => cb(method)) }) } diff --git a/test/setup/karma.conf.js b/test/setup/karma.conf.js index 7777394a2..17a9c0e7f 100644 --- a/test/setup/karma.conf.js +++ b/test/setup/karma.conf.js @@ -7,10 +7,10 @@ module.exports = function (config) { reporters: ['spec'], files: [ '../../node_modules/babel-polyfill/dist/polyfill.js', - '../specs/**/*.+(vue|js)' + 'load-tests.js' ], preprocessors: { - '../specs/**/*.+(vue|js)': ['webpack', 'sourcemap'] + 'load-tests.js': ['webpack', 'sourcemap'] }, client: { mocha: { timeout: 20000 }}, webpack: webpackConfig, diff --git a/test/setup/load-tests.js b/test/setup/load-tests.js new file mode 100644 index 000000000..9563fff0e --- /dev/null +++ b/test/setup/load-tests.js @@ -0,0 +1,3 @@ +const testsContext = require.context('../specs', true, /\.spec\.(js|vue)$/) + +testsContext.keys().forEach(testsContext) diff --git a/test/setup/webpack.test.config.js b/test/setup/webpack.test.config.js index 74f710f71..6660d520c 100644 --- a/test/setup/webpack.test.config.js +++ b/test/setup/webpack.test.config.js @@ -38,5 +38,8 @@ module.exports = { devtoolModuleFilenameTemplate: '[absolute-resource-path]', devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]' }, - devtool: '#inline-cheap-module-source-map' + devtool: '#inline-cheap-module-source-map', + node: { + fs: 'empty' + } } diff --git a/test/specs/components/TransitionGroupStub.spec.js b/test/specs/components/TransitionGroupStub.spec.js index b1f071bae..d94a04741 100644 --- a/test/specs/components/TransitionGroupStub.spec.js +++ b/test/specs/components/TransitionGroupStub.spec.js @@ -22,7 +22,6 @@ describe('TransitionGroupStub', () => { }), watch: { someWatchedData (newData) { - console.log('asd') this.someData = newData } }, diff --git a/test/specs/mounting-options/attachToDocument.spec.js b/test/specs/mounting-options/attachToDocument.spec.js index 82f73bdca..500bd7b3b 100644 --- a/test/specs/mounting-options/attachToDocument.spec.js +++ b/test/specs/mounting-options/attachToDocument.spec.js @@ -1,5 +1,9 @@ import { compileToFunctions } from 'vue-template-compiler' -import { describeWithShallowAndMount } from '~resources/test-utils' +import { + describeWithShallowAndMount, + isRunningJSDOM +} from '~resources/test-utils' +import { renderToString } from '~vue-test-utils' describeWithShallowAndMount('options.attachToDocument', (mountingMethod) => { it('returns VueWrapper with attachedToDocument set to true when passed attachToDocument in options', () => { @@ -8,3 +12,16 @@ describeWithShallowAndMount('options.attachToDocument', (mountingMethod) => { expect(wrapper.options.attachedToDocument).to.equal(true) }) }) + +describe('options.attachToDocument with renderToString', () => { + it('throws error that renderToString does not accept attachToDocument', () => { + // renderToString can only be run in node + if (!isRunningJSDOM) { + return + } + const compiled = compileToFunctions('
') + const fn = () => renderToString(compiled, { attachToDocument: true }) + const message = '[vue-test-utils]: you cannot use attachToDocument with renderToString' + expect(fn).to.throw().with.property('message', message) + }) +}) diff --git a/test/specs/mounting-options/attrs.spec.js b/test/specs/mounting-options/attrs.spec.js index 8eb82b038..7acb7a3fd 100644 --- a/test/specs/mounting-options/attrs.spec.js +++ b/test/specs/mounting-options/attrs.spec.js @@ -1,9 +1,13 @@ import { compileToFunctions } from 'vue-template-compiler' import { attrsSupported } from '~resources/test-utils' -import { describeWithShallowAndMount } from '~resources/test-utils' +import { + describeWithMountingMethods, + itSkipIf + } from '~resources/test-utils' -describeWithShallowAndMount('options.attrs', (mountingMethod) => { - it('handles inherit attrs', () => { +describeWithMountingMethods('options.attrs', (mountingMethod) => { + itSkipIf(mountingMethod.name === 'renderToString', + 'handles inherit attrs', () => { if (!attrsSupported()) return const wrapper = mountingMethod(compileToFunctions('

'), { attrs: { @@ -15,7 +19,8 @@ describeWithShallowAndMount('options.attrs', (mountingMethod) => { expect(wrapper.vm.$attrs.anAttr).to.equal('an attribute') }) - it('defines attrs as empty object even when not passed', () => { + itSkipIf(mountingMethod.name === 'renderToString', + 'defines attrs as empty object even when not passed', () => { const wrapper = mountingMethod(compileToFunctions('

')) expect(wrapper.vm.$attrs).to.deep.equal({}) }) diff --git a/test/specs/mounting-options/context.spec.js b/test/specs/mounting-options/context.spec.js index 63730eb92..3dc1e8051 100644 --- a/test/specs/mounting-options/context.spec.js +++ b/test/specs/mounting-options/context.spec.js @@ -1,8 +1,8 @@ import Vue from 'vue' import { vueVersion } from '~resources/test-utils' -import { describeWithShallowAndMount } from '~resources/test-utils' +import { describeWithMountingMethods } from '~resources/test-utils' -describeWithShallowAndMount('options.context', (mountingMethod) => { +describeWithMountingMethods('options.context', (mountingMethod) => { it('mounts functional component when passed context object', () => { if (vueVersion <= 2.2) { console.log('WARN: no current way to test functional component is component in v2.1.x') @@ -21,8 +21,7 @@ describeWithShallowAndMount('options.context', (mountingMethod) => { props: { show: true } } - const wrapper = mountingMethod(Component, { context }) - expect(wrapper.is(Component)).to.equal(true) + mountingMethod(Component, { context }) }) it('throws error if non functional component is passed with context option', () => { @@ -69,7 +68,10 @@ describeWithShallowAndMount('options.context', (mountingMethod) => { render: (h, { props }) => h('div', props.testProp) } const wrapper = mountingMethod(Component) - expect(wrapper.element.textContent).to.equal(defaultValue) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain(defaultValue) }) it('mounts functional component with a defined context.children text', () => { @@ -84,7 +86,10 @@ describeWithShallowAndMount('options.context', (mountingMethod) => { children: ['render text'] } }) - expect(wrapper.text()).to.equal('render text') + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('render text') }) it('mounts functional component with a defined context.children element', () => { @@ -99,6 +104,9 @@ describeWithShallowAndMount('options.context', (mountingMethod) => { children: [h => h('div', 'render component')] } }) - expect(wrapper.text()).to.equal('render component') + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('render component') }) }) diff --git a/test/specs/mounting-options/localVue.spec.js b/test/specs/mounting-options/localVue.spec.js index 43ed502b5..cc69b8c2f 100644 --- a/test/specs/mounting-options/localVue.spec.js +++ b/test/specs/mounting-options/localVue.spec.js @@ -1,14 +1,27 @@ import Vue from 'vue' -import Component from '~resources/components/component.vue' -import { describeWithShallowAndMount } from '~resources/test-utils' +import { describeWithMountingMethods } from '~resources/test-utils' -describeWithShallowAndMount('options.localVue', (mountingMethod) => { +describeWithMountingMethods('options.localVue', (mountingMethod) => { it('mounts component using passed localVue as base Vue', () => { + const TestComponent = { + template: ` +

{{test}}
+ ` + } const localVue = Vue.extend() localVue.version = '2.3' - const wrapper = mountingMethod(Component, { localVue: localVue, mocks: { test: true }}) - expect(wrapper.vm.test).to.equal(true) - const freshWrapper = mountingMethod(Component) - expect(typeof freshWrapper.vm.test).to.equal('undefined') + const wrapper = mountingMethod(TestComponent, { + localVue: localVue, + mocks: { test: 'some value' } + }) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('some value') + const freshWrapper = mountingMethod(TestComponent) + const freshHTML = mountingMethod.name === 'renderToString' + ? freshWrapper + : freshWrapper.html() + expect(freshHTML).to.not.contain('some value') }) }) diff --git a/test/specs/mounting-options/mocks.spec.js b/test/specs/mounting-options/mocks.spec.js index 4e047f727..e2bb8fac5 100644 --- a/test/specs/mounting-options/mocks.spec.js +++ b/test/specs/mounting-options/mocks.spec.js @@ -2,51 +2,64 @@ import { createLocalVue } from '~vue-test-utils' import Component from '~resources/components/component.vue' import ComponentWithVuex from '~resources/components/component-with-vuex.vue' import { - describeWithShallowAndMount, + describeWithMountingMethods, itDoNotRunIf } from '~resources/test-utils' -describeWithShallowAndMount('options.mocks', (mountingMethod) => { - it('adds variables to vm when passed as mocks object', () => { +describeWithMountingMethods('options.mocks', (mountingMethod) => { + it('adds variables to vm when passed', () => { + const TestComponent = { + template: ` +
+ {{$store.store}} + {{$route.path}} +
+ ` + } const $store = { store: true } const $route = { path: 'http://test.com' } - const wrapper = mountingMethod(Component, { + const wrapper = mountingMethod(TestComponent, { mocks: { $store, $route } }) - expect(wrapper.vm.$store).to.equal($store) - expect(wrapper.vm.$route).to.equal($route) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).contains('true') + expect(HTML).contains('http://test.com') }) - it('adds variables to vm when passed as mocks object', () => { - const stub = sinon.stub() - const $reactiveMock = { value: 'value' } - const wrapper = mountingMethod({ - template: ` + // render returns a string so reactive does not apply + itDoNotRunIf(mountingMethod.name === 'renderToString', + 'adds variables as reactive properties to vm when passed', () => { + const stub = sinon.stub() + const $reactiveMock = { value: 'value' } + const wrapper = mountingMethod({ + template: `
{{value}}
`, - computed: { - value () { - return this.$reactiveMock.value - } - }, - watch: { - value () { - stub() - } - } - }, { - mocks: { $reactiveMock } - }) - expect(wrapper.text()).to.contain('value') - $reactiveMock.value = 'changed value' - wrapper.update() - expect(wrapper.text()).to.contain('changed value') - }) + computed: { + value () { + return this.$reactiveMock.value + } + }, + watch: { + value () { + stub() + } + } + }, { + mocks: { $reactiveMock } + }) + expect(wrapper.text()).to.contain('value') + $reactiveMock.value = 'changed value' + wrapper.update() + expect(wrapper.text()).to.contain('changed value') + }) itDoNotRunIf(mountingMethod.name === 'shallow', 'adds variables available to nested vms', () => { @@ -59,7 +72,10 @@ describeWithShallowAndMount('options.mocks', (mountingMethod) => { }, { mocks: { $store: { state: { count, foo: {}}}} }) - expect(wrapper.text()).contains(count) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).contains(count) }) itDoNotRunIf(mountingMethod.name === 'shallow', @@ -75,10 +91,14 @@ describeWithShallowAndMount('options.mocks', (mountingMethod) => { mocks: { $store: { state: { count, foo: {}}}}, localVue }) - expect(wrapper.text()).contains(count) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).contains(count) }) - it('does not affect global vue class when passed as mocks object', () => { + itDoNotRunIf(mountingMethod.name === 'renderToString', + 'does not affect global vue class when passed as mocks object', () => { const $store = { store: true } const wrapper = mountingMethod(Component, { mocks: { diff --git a/test/specs/mounting-options/provide.spec.js b/test/specs/mounting-options/provide.spec.js index 2ca2fe94a..b08163dd3 100644 --- a/test/specs/mounting-options/provide.spec.js +++ b/test/specs/mounting-options/provide.spec.js @@ -1,22 +1,26 @@ -import { mount } from '~vue-test-utils' import ComponentWithInject from '~resources/components/component-with-inject.vue' import { injectSupported } from '~resources/test-utils' -import { describeWithShallowAndMount } from '~resources/test-utils' - -describeWithShallowAndMount('options.provide', (mountingMethod) => { - it('provides objects which is injected by mounted component', () => { - if (!injectSupported()) return - - const wrapper = mount(ComponentWithInject, { - provide: { fromMount: 'objectValue' } +import { + describeWithMountingMethods, + itDoNotRunIf + } from '~resources/test-utils' + +describeWithMountingMethods('options.provide', (mountingMethod) => { + itDoNotRunIf(!injectSupported(), + 'provides objects which is injected by mounted component', () => { + if (!injectSupported()) return + + const wrapper = mountingMethod(ComponentWithInject, { + provide: { fromMount: 'objectValue' } + }) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('objectValue') }) - expect(wrapper.text()).to.contain('objectValue') - }) - - it('provides function which is injected by mounted component', () => { - if (!injectSupported()) return - + itDoNotRunIf(!injectSupported(), + 'provides function which is injected by mounted component', () => { const wrapper = mountingMethod(ComponentWithInject, { provide () { return { @@ -24,17 +28,20 @@ describeWithShallowAndMount('options.provide', (mountingMethod) => { } } }) - - expect(wrapper.text()).to.contain('functionValue') + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('functionValue') }) - it('supports beforeCreate in component', () => { - if (!injectSupported()) return + itDoNotRunIf(!injectSupported() || mountingMethod.name === 'renderToString', + 'supports beforeCreate in component', () => { + if (!injectSupported()) return - const wrapper = mountingMethod(ComponentWithInject, { - provide: { fromMount: '_' } - }) + const wrapper = mountingMethod(ComponentWithInject, { + provide: { fromMount: '_' } + }) - expect(wrapper.vm.setInBeforeCreate).to.equal('created') - }) + expect(wrapper.vm.setInBeforeCreate).to.equal('created') + }) }) diff --git a/test/specs/mounting-options/slots.spec.js b/test/specs/mounting-options/slots.spec.js index bad0e0c99..1e6dbbf07 100644 --- a/test/specs/mounting-options/slots.spec.js +++ b/test/specs/mounting-options/slots.spec.js @@ -3,11 +3,11 @@ import Component from '~resources/components/component.vue' import ComponentWithSlots from '~resources/components/component-with-slots.vue' import ComponentAsAClass from '~resources/components/component-as-a-class.vue' import { - describeWithShallowAndMount, + describeWithMountingMethods, vueVersion } from '~resources/test-utils' -describeWithShallowAndMount('options.slots', (mountingMethod) => { +describeWithMountingMethods('options.slots', (mountingMethod) => { let _window beforeEach(() => { @@ -22,23 +22,39 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { it('mounts component with default slot if passed component in slot object', () => { const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: Component }}) - expect(wrapper.contains(Component)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.contains(Component)).to.equal(true) + } }) it('mounts component with default slot if passed component in array in slot object', () => { const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: [Component] }}) - expect(wrapper.contains(Component)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.contains(Component)).to.equal(true) + } }) it('mounts component with default slot if passed object with template prop in slot object', () => { const compiled = compileToFunctions('
') const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: [compiled] }}) - expect(wrapper.contains('#div')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('div id="div"') + } else { + expect(wrapper.contains('#div')).to.equal(true) + } }) it('mounts component with default slot if passed string in slot object', () => { const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: '' }}) - expect(wrapper.contains('span')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains(' { @@ -46,7 +62,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { return } const wrapper = mountingMethod(ComponentAsAClass, { slots: { default: '' }}) - expect(wrapper.contains('span')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains(' { @@ -60,6 +80,9 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { }) it('mounts component with default slot if passed string in slot object', () => { + if (mountingMethod.name === 'renderToString') { + return + } const wrapper1 = mountingMethod(ComponentWithSlots, { slots: { default: 'foo123{{ foo }}' }}) expect(wrapper1.find('main').html()).to.equal('
foo123bar
') const wrapper2 = mountingMethod(ComponentWithSlots, { slots: { default: '

1

{{ foo }}2' }}) @@ -96,12 +119,20 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { it('mounts component with default slot if passed string in slot array object', () => { const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: [''] }}) - expect(wrapper.contains('span')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains(' { const wrapper = mountingMethod(ComponentWithSlots, { slots: { default: ['{{ foo }}1', 'bar'] }}) - expect(wrapper.find('main').html()).to.equal('
bar1bar
') + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
bar1bar
') + } else { + expect(wrapper.find('main').html()).to.equal('
bar1bar
') + } }) it('throws error if passed string in default slot array vue-template-compiler is undefined', () => { @@ -127,7 +158,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { footer: [Component] } }) - expect(wrapper.findAll(Component).length).to.equal(2) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.findAll(Component).length).to.equal(2) + } }) it('mounts component with named slot if passed component in slot object', () => { @@ -136,8 +171,12 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { header: Component } }) - expect(wrapper.findAll(Component).length).to.equal(1) - expect(Array.isArray(wrapper.vm.$slots.header)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.findAll(Component).length).to.equal(1) + expect(Array.isArray(wrapper.vm.$slots.header)).to.equal(true) + } }) it('mounts functional component with default slot if passed component in slot object', () => { @@ -147,7 +186,12 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', ctx.data, ctx.slots().default) } const wrapper = mountingMethod(TestComponent, { slots: { default: Component }}) - expect(wrapper.contains(Component)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + const renderedAttribute = vueVersion < 2.3 ? 'server-rendered' : 'data-server-rendered' + expect(wrapper).contains(`
`) + } else { + expect(wrapper.contains(Component)).to.equal(true) + } }) it('mounts component with default slot if passed component in slot object', () => { @@ -157,7 +201,12 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', ctx.data, ctx.slots().default) } const wrapper = mountingMethod(TestComponent, { slots: { default: [Component] }}) - expect(wrapper.contains(Component)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + const renderedAttribute = vueVersion < 2.3 ? 'server-rendered' : 'data-server-rendered' + expect(wrapper).contains(`
`) + } else { + expect(wrapper.contains(Component)).to.equal(true) + } }) it('mounts component with default slot if passed object with template prop in slot object', () => { @@ -168,7 +217,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { } const compiled = compileToFunctions('
') const wrapper = mountingMethod(TestComponent, { slots: { default: [compiled] }}) - expect(wrapper.contains('#div')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.contains('#div')).to.equal(true) + } }) it('mounts component with default slot if passed string in slot object', () => { @@ -178,7 +231,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', ctx.data, ctx.slots().default) } const wrapper = mountingMethod(TestComponent, { slots: { default: '' }}) - expect(wrapper.contains('span')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains(' { @@ -187,7 +244,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', {}, ctx.slots().named) } const wrapper = mountingMethod(TestComponent, { slots: { named: Component }}) - expect(wrapper.contains(Component)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.contains(Component)).to.equal(true) + } }) it('mounts component with named slot if passed string in slot object in array', () => { @@ -196,7 +257,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', {}, ctx.slots().named) } const wrapper = mountingMethod(TestComponent, { slots: { named: [Component] }}) - expect(wrapper.contains(Component)).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('
') + } else { + expect(wrapper.contains(Component)).to.equal(true) + } }) it('mounts component with named slot if passed string in slot object in array', () => { @@ -205,7 +270,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', {}, ctx.slots().named) } const wrapper = mountingMethod(TestComponent, { slots: { named: '' }}) - expect(wrapper.contains('span')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains(' { @@ -214,7 +283,11 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { render: (h, ctx) => h('div', {}, ctx.slots().named) } const wrapper = mountingMethod(TestComponent, { slots: { named: [''] }}) - expect(wrapper.contains('span')).to.equal(true) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains(' { @@ -238,6 +311,7 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { const message = '[vue-test-utils]: slots[key] must be a Component, string or an array of Components' expect(fn).to.throw().with.property('message', message) }) + it('throws error if passed false for named slots', () => { const TestComponent = { name: 'component-with-slots', @@ -259,6 +333,7 @@ describeWithShallowAndMount('options.slots', (mountingMethod) => { const message = '[vue-test-utils]: slots[key] must be a Component, string or an array of Components' expect(fn).to.throw().with.property('message', message) }) + it('throws error if passed string in default slot array when vue-template-compiler is undefined', () => { const TestComponent = { name: 'component-with-slots', diff --git a/test/specs/mounting-options/stubs.spec.js b/test/specs/mounting-options/stubs.spec.js index 26dea2c6a..4dd81f8d6 100644 --- a/test/specs/mounting-options/stubs.spec.js +++ b/test/specs/mounting-options/stubs.spec.js @@ -6,11 +6,11 @@ import ComponentAsAClass from '~resources/components/component-as-a-class.vue' import { createLocalVue } from '~vue-test-utils' import Vue from 'vue' import { - describeWithShallowAndMount, + describeWithMountingMethods, itDoNotRunIf } from '~resources/test-utils' -describeWithShallowAndMount('options.stub', (mountingMethod) => { +describeWithMountingMethods('options.stub', (mountingMethod) => { let info let warn let configStubsSave @@ -48,15 +48,20 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { ChildComponent: '
' } }) - expect(wrapper.findAll('.stub').length).to.equal(1) - expect(wrapper.findAll(Component).length).to.equal(1) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).to.contain('"stub"') + } else { + expect(wrapper.findAll('.stub').length).to.equal(1) + expect(wrapper.findAll(Component).length).to.equal(1) + } }) - it('replaces component with a component', () => { + itDoNotRunIf(mountingMethod.name === 'renderToString', + 'replaces component with a component', () => { const wrapper = mountingMethod(ComponentWithChild, { stubs: { ChildComponent: { - render: h => h('div'), + render: h => h('time'), mounted () { console.info('stubbed') } @@ -75,7 +80,8 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { }) }) - itDoNotRunIf(mountingMethod.name === 'shallow', + itDoNotRunIf(mountingMethod.name === 'shallow' || + mountingMethod.name === 'renderToString', 'does not modify component directly', () => { const wrapper = mountingMethod(ComponentWithNestedChildren, { stubs: { @@ -83,6 +89,7 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { } }) expect(wrapper.findAll(Component).length).to.equal(0) + const mountedWrapper = mountingMethod(ComponentWithNestedChildren) expect(mountedWrapper.findAll(Component).length).to.equal(1) }) @@ -96,30 +103,38 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { 'registered-component': Component } }) - expect(wrapper.findAll(Component).length).to.equal(1) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('
') }) it('stubs components with dummy when passed as an array', () => { const ComponentWithGlobalComponent = { - render: h => h('registered-component') + render: h => h('div', [h('registered-component')]) } - mountingMethod(ComponentWithGlobalComponent, { + const wrapper = mountingMethod(ComponentWithGlobalComponent, { stubs: ['registered-component'] }) - - expect(warn.called).to.equal(false) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('') }) it('stubs components with dummy when passed a boolean', () => { const ComponentWithGlobalComponent = { - render: h => h('registered-component') + render: h => h('div', [h('registered-component')]) } - mountingMethod(ComponentWithGlobalComponent, { + const wrapper = mountingMethod(ComponentWithGlobalComponent, { stubs: { 'registered-component': true } }) - expect(warn.called).to.equal(false) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('') }) it('stubs components with dummy when passed as an array', () => { @@ -162,12 +177,15 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { stubs: { ChildComponent: false }}) - expect(wrapper.find('span').contains('div')).to.equal(true) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('
') }) it('combines with stubs from config', () => { const localVue = createLocalVue() - config.stubs['time-component'] = '

' + config.stubs['time-component'] = '
' const SpanComponent = { render: h => h('span') } @@ -189,7 +207,11 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { }, localVue }) - expect(wrapper.findAll('p').length).to.equal(2) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('
') + expect(HTML).to.contain('

') }) it('prioritize mounting options over config', () => { @@ -211,7 +233,10 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { }, localVue }) - expect(wrapper.contains('span')).to.equal(true) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('') }) it('converts config to array if stubs is an array', () => { @@ -231,9 +256,11 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { stubs: ['a-component'], localVue }) - expect(wrapper.contains('time')).to.equal(false) - expect(wrapper.contains('p')).to.equal(false) - expect(wrapper.html()).to.equal('

') + + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('
') }) it('handles components without a render function', () => { @@ -256,8 +283,10 @@ describeWithShallowAndMount('options.stub', (mountingMethod) => { 'stub-component': StubComponent } }) - - expect(wrapper.text()).contains('No render function') + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).contains('No render function') }) it('throws an error when passed an invalid value as stub', () => { diff --git a/test/specs/renderToString.spec.js b/test/specs/renderToString.spec.js new file mode 100644 index 000000000..af2100a68 --- /dev/null +++ b/test/specs/renderToString.spec.js @@ -0,0 +1,15 @@ +import { renderToString } from '~vue-test-utils' +import Component from '~resources/components/component.vue' +import { + isRunningJSDOM, + itDoNotRunIf +} from '~resources/test-utils' + +describe('renderToString', () => { + itDoNotRunIf(isRunningJSDOM, + 'throws error when not run in node', () => { + const fn = () => renderToString(Component) + const message = '[vue-test-utils]: renderToString must be run in node. It cannot be run in a browser' + expect(fn).to.throw().with.property('message', message) + }) +}) diff --git a/test/specs/wrapper-array/hasStyle.spec.js b/test/specs/wrapper-array/hasStyle.spec.js index 3b76a8700..87d4cc0c1 100644 --- a/test/specs/wrapper-array/hasStyle.spec.js +++ b/test/specs/wrapper-array/hasStyle.spec.js @@ -13,7 +13,6 @@ describe('hasStyle', () => { if (navigator.userAgent.includes && navigator.userAgent.includes('jsdom')) { return } - console.log(navigator.userAgent.includes('jsdom')) const wrapper = mount(ComponentWithStyle) expect(wrapper.findAll('div').hasStyle('color', 'red')).to.equal(true) }) diff --git a/test/specs/wrapper/hasStyle.spec.js b/test/specs/wrapper/hasStyle.spec.js index e74101c03..57a7c4f05 100644 --- a/test/specs/wrapper/hasStyle.spec.js +++ b/test/specs/wrapper/hasStyle.spec.js @@ -13,7 +13,6 @@ describeWithShallowAndMount('hasStyle', (mountingMethod) => { if (navigator.userAgent.includes && navigator.userAgent.includes('jsdom')) { return } - console.log(navigator.userAgent.includes('jsdom')) const wrapper = mountingMethod(ComponentWithStyle) expect(wrapper.find('div').hasStyle('color', 'red')).to.equal(true) }) diff --git a/types/index.d.ts b/types/index.d.ts index 066075b9f..5a169c6d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -138,5 +138,10 @@ export declare function shallow (component: VueClass, options? export declare function shallow (component: ComponentOptions, options?: ThisTypedShallowOptions): Wrapper export declare function shallow (component: FunctionalComponentOptions, options?: ShallowOptions): Wrapper + +export declare function renderToString (component: VueClass, options?: ThisTypedShallowOptions): string +export declare function renderToString (component: ComponentOptions, options?: ThisTypedShallowOptions): string +export declare function renderToString (component: FunctionalComponentOptions, options?: ShallowOptions): string + export declare let TransitionStub: Component | string | true export declare let TransitionGroupStub: Component | string | true diff --git a/types/test/renderToString.ts b/types/test/renderToString.ts new file mode 100644 index 000000000..4e2c47cd5 --- /dev/null +++ b/types/test/renderToString.ts @@ -0,0 +1,47 @@ +import Vuex from 'vuex' +import { mount, renderToString, 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(normalOptions) +const normalFoo: string = normalWrapper.vm.foo + +const classWrapper = mount(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({}) + +renderToString(ClassComponent, { + localVue, + mocks: { + $store: store + }, + slots: { + default: `
Foo
`, + foo: [normalOptions, functionalOptions], + bar: ClassComponent + }, + stubs: { + foo: normalOptions, + bar: functionalOptions, + baz: ClassComponent, + qux: `
Test
` + }, + attrs: { + attribute: 'attr' + }, + listeners: { + listener: () => {} + } +})