diff --git a/package-lock.json b/package-lock.json index aff49ee..f0366e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12432,8 +12432,7 @@ "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -18762,6 +18761,14 @@ "integrity": "sha512-mxBBMuSaPG9+NkVMbh28r8gvWQJ8UXxqDxVNeLy2KBUZiSNxZsagjYwLL8gjROb4oaaYtwRv3K8gAmw76I/U7Q==", "dev": true }, + "vue-types": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-1.3.4.tgz", + "integrity": "sha512-c8a0wShFBSOkPUWTtv/OKi55cEAScTTQx/kOd2EuahSyMFPB8f+vYQbsn9RvTQylEtR3ERu9G/em6sGb9Nvmbw==", + "requires": { + "lodash": "^4.17.10" + } + }, "vuetify": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.4.2.tgz", diff --git a/package.json b/package.json index 4a91087..4194068 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "vue": "^2.5.21", "vue-router": "^3.0.1", + "vue-types": "^1.3.4", "vuetify": "^1.3.0", "vuex": "^3.0.1" }, diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue index b71f0e4..6a3a3ff 100644 --- a/src/components/HelloWorld.vue +++ b/src/components/HelloWorld.vue @@ -13,18 +13,49 @@ > + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -102,7 +133,9 @@ - - + + + + + diff --git a/src/components/common/VStatsCard.vue b/src/components/common/VStatsCard.vue index 2311438..510965e 100644 --- a/src/components/common/VStatsCard.vue +++ b/src/components/common/VStatsCard.vue @@ -19,27 +19,14 @@ diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js index 014a764..38c0640 100644 --- a/src/plugins/vuetify.js +++ b/src/plugins/vuetify.js @@ -1,5 +1,5 @@ import Vue from 'vue' -import Vuetify, { VApp, VCard, VCardText, VContainer, VFlex, VIcon, VLayout } from 'vuetify/lib' +import Vuetify, { VApp, VBtn, VCard, VCardText, VContainer, VFlex, VHover, VIcon, VLayout, VTooltip } from 'vuetify/lib' import 'vuetify/src/stylus/app.styl' Vue.use(Vuetify, { @@ -15,11 +15,14 @@ Vue.use(Vuetify, { }, components: { VApp, + VBtn, VCard, VCardText, VContainer, VFlex, + VHover, VIcon, - VLayout + VLayout, + VTooltip } }) diff --git a/src/stories/common.stories.js b/src/stories/common.stories.js index 35c900c..3a29f60 100644 --- a/src/stories/common.stories.js +++ b/src/stories/common.stories.js @@ -1,16 +1,63 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import { storiesOf } from '@storybook/vue' -import { wrapComponent } from './helpers/decorators' - -import VStatsCard from '@/components/common/VStatsCard.vue' - -storiesOf('VStatsCard', module) - .addDecorator(story => wrapComponent(story, 'xl', 'xs4')) - .add('default', () => ({ - components: { VStatsCard }, - template: '' - })) - .add('custom color and icon', () => ({ - components: { VStatsCard }, - template: '' - })) +/* eslint-disable import/no-extraneous-dependencies */ +import { storiesOf } from '@storybook/vue' +import { action } from '@storybook/addon-actions' +import { wrapComponent } from './helpers/decorators' + +import VBtnPlus from '@/components/common/VBtnPlus.vue' +import VStatsCard from '@/components/common/VStatsCard.vue' + +export const methods = { + onClicked: action('onClicked') +} + +storiesOf('VBtnPlus', module) + .addDecorator(story => wrapComponent(story, 'xl', 'xs2')) + .add('text with defaults', () => ({ + components: { VBtnPlus }, + template: `
+ + +
`, + methods + })) + .add('icon with defaults', () => ({ + components: { VBtnPlus }, + template: `
+ + +
`, + methods + })) + .add('with tooltip and custom colors and types', () => ({ + components: { VBtnPlus }, + template: `
+ + + + + + + + +
`, + methods + })) + .add('themes', () => ({ + components: { VBtnPlus }, + template: `
+ + +
`, + methods + })) + +storiesOf('VStatsCard', module) + .addDecorator(story => wrapComponent(story, 'xl', 'xs4')) + .add('default', () => ({ + components: { VStatsCard }, + template: '' + })) + .add('custom color and icon', () => ({ + components: { VStatsCard }, + template: '' + })) diff --git a/src/stories/helpers/decorators.js b/src/stories/helpers/decorators.js index b999723..8313fa3 100644 --- a/src/stories/helpers/decorators.js +++ b/src/stories/helpers/decorators.js @@ -1,20 +1,20 @@ -/** - * Wraps a story in a grid list container with wrapped row layout and single flex item. Making it possible to size a component as required. - * The flex item should be specified in the following format: - * - media breakpoint: xs: extra small, sm: small, md: medium, lg: large, xl: extra large - * - size: number between 1-12 - * @param {Object} story - The story object to wrap - * @param {String} gutter - Size of the grid list gutter: xs, sm, md, lg, xl - * @param {String} size - Size of the flex item in the format: , see above - * @returns {Object} The wrapped story object - */ -export function wrapComponent(story, gutter, size) { - const WrappedComponent = story() - return { - components: { WrappedComponent }, - template: ``, - data() { - return { borderStyle: 'medium solid grey' } - } - } -} +/** + * Wraps a story in a grid list container with wrapped row layout and single flex item. Making it possible to size a component as required. + * The flex item should be specified in the following format: + * - media breakpoint: xs: extra small, sm: small, md: medium, lg: large, xl: extra large + * - size: number between 1-12 + * @param {Object} story - The story object to wrap + * @param {String} gutter - Size of the grid list gutter: xs, sm, md, lg, xl + * @param {String} size - Size of the flex item in the format: , see above + * @returns {Object} The wrapped story object + */ +export function wrapComponent(story, gutter, size) { + const WrappedComponent = story() + return { + components: { WrappedComponent }, + template: ``, + data() { + return { borderStyle: 'medium solid grey' } + } + } +} diff --git a/tests/helpers/test-utils.js b/tests/helpers/test-utils.js new file mode 100644 index 0000000..3c8a6ea --- /dev/null +++ b/tests/helpers/test-utils.js @@ -0,0 +1,18 @@ +/* eslint-disable no-undef */ +export function spyConsole() { + let spy = {} + + beforeEach(() => { + spy.console = jest.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + spy.console.mockClear() + }) + + afterAll(() => { + spy.console.mockRestore() + }) + + return spy +} diff --git a/tests/unit/components/common/VBtnPlus.spec.js b/tests/unit/components/common/VBtnPlus.spec.js new file mode 100644 index 0000000..e5805fe --- /dev/null +++ b/tests/unit/components/common/VBtnPlus.spec.js @@ -0,0 +1,63 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { SilenceWarnHack } from '@tst/helpers/SilenceWarnHack' +import { spyConsole } from '@tst/helpers/test-utils' +import Vuetify from 'vuetify' +import VBtnPlus from '@/components/common/VBtnPlus.vue' + +const silenceWarnHack = new SilenceWarnHack() + +describe('VStatsCard.vue', () => { + let localVue = null + + beforeEach(() => { + silenceWarnHack.enable() + localVue = createLocalVue() + localVue.use(Vuetify) + silenceWarnHack.disable() + }) + + it('renders with default settings when only label is specified', () => { + const label = 'Very cool' + const defaultColor = 'primary' + const defaultType = 'raised' + const defaultSize = 'small' + const wrapper = mount(VBtnPlus, { + localVue: localVue, + propsData: { label } + }) + expect(wrapper.text()).toMatch(label) + expect(wrapper.html()).toContain(`${defaultType}="true"`) + expect(wrapper.html()).toContain(`size="${defaultSize}"`) + expect(wrapper.html()).toContain(`class="v-btn theme--light ${defaultColor} text-capitalize"`) + expect(wrapper.html()).not.toContain(`v-icon"`) + }) + + it('renders with default settings when only icon is specified', () => { + const icon = 'mdi-info' + const defaultColor = 'primary' + const defaultType = 'raised' + const defaultSize = 'small' + const wrapper = mount(VBtnPlus, { + localVue: localVue, + propsData: { icon } + }) + expect(wrapper.html()).toContain(`class="v-icon mdi ${icon} theme--light"`) + expect(wrapper.html()).toContain(`${defaultType}="true"`) + expect(wrapper.html()).toContain(`size="${defaultSize}"`) + expect(wrapper.html()).toContain(`class="v-btn theme--light ${defaultColor} text-lowercase"`) + }) + + describe('test prop warnings', () => { + let spy = spyConsole() + + it('displays warning messages when both label and icon are not specified', () => { + mount(VBtnPlus, { + localVue: localVue + }) + expect(console.error).toHaveBeenCalledTimes(1) + expect(spy.console.mock.calls[0][0]).toContain( + '[Vue warn]: Missing required prop, specify at least one of the following: "label" or "icon"' + ) + }) + }) +}) diff --git a/tests/unit/components/common/VStatsCard.spec.js b/tests/unit/components/common/VStatsCard.spec.js index 3d1fb4e..ed04e8e 100644 --- a/tests/unit/components/common/VStatsCard.spec.js +++ b/tests/unit/components/common/VStatsCard.spec.js @@ -1,5 +1,6 @@ import { createLocalVue, shallowMount } from '@vue/test-utils' import { SilenceWarnHack } from '@tst/helpers/SilenceWarnHack' +import { spyConsole } from '@tst/helpers/test-utils' import Vuetify from 'vuetify' import VStatsCard from '@/components/common/VStatsCard.vue' @@ -45,18 +46,16 @@ describe('VStatsCard.vue', () => { expect(wrapper.html()).toContain(`class="text-xs-center py-3 white--text ${color}"`) }) - it('throws an exception if title is not specified', () => { - const title = 'My title' - const subTitle = 'My subTitle' - const color = 'red' - const icon = 'mdi-information' - const wrapper = shallowMount(VStatsCard, { - localVue: localVue, - propsData: { color, icon, title, subTitle } + describe('test prop warnings', () => { + let spy = spyConsole() + + it('displays two warning messages if title and subTitle is not specified', () => { + shallowMount(VStatsCard, { + localVue: localVue + }) + expect(console.error).toHaveBeenCalledTimes(2) + expect(spy.console.mock.calls[0][0]).toContain('[Vue warn]: Missing required prop: "title"') + expect(spy.console.mock.calls[1][0]).toContain('[Vue warn]: Missing required prop: "subTitle"') }) - expect(wrapper.text()).toMatch(title) - expect(wrapper.text()).toMatch(subTitle) - expect(wrapper.html()).toContain(`${icon}`) - expect(wrapper.html()).toContain(`class="text-xs-center py-3 white--text ${color}"`) }) })