diff --git a/src/components/button-group/button-group.spec.js b/src/components/button-group/button-group.spec.js index 11d92493954..ea67a4864bc 100644 --- a/src/components/button-group/button-group.spec.js +++ b/src/components/button-group/button-group.spec.js @@ -1,30 +1,80 @@ -import { loadFixture, testVM } from '../../../tests/utils' +import ButtonGroup from './button-group' +import { mount } from '@vue/test-utils' describe('button-group', () => { - beforeEach(loadFixture(__dirname, 'button-group')) - testVM() - - it('basic should contain base class', async () => { - const { - app: { $refs } - } = window + it('has expected default structure', async () => { + const wrapper = mount(ButtonGroup) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('btn-group') + expect(wrapper.classes().length).toBe(1) + expect(wrapper.attributes('role')).toBeDefined() + expect(wrapper.attributes('role')).toBe('group') + expect(wrapper.text()).toBe('') + }) - expect($refs.basic).toHaveClass('btn-group') + it('should render default slot', async () => { + const wrapper = mount(ButtonGroup, { + slots: { + default: 'foobar' + } + }) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('btn-group') + expect(wrapper.classes().length).toBe(1) + expect(wrapper.attributes('role')).toBeDefined() + expect(wrapper.attributes('role')).toBe('group') + expect(wrapper.find('span').exists()).toBe(true) + expect(wrapper.text()).toBe('foobar') }) it('should apply vertical class', async () => { - const { - app: { $refs } - } = window - - expect($refs.vertical).toHaveClass('btn-group-vertical') + const wrapper = mount(ButtonGroup, { + propsData: { + vertical: true + } + }) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('btn-group-vertical') + expect(wrapper.classes()).not.toContain('btn-group') + expect(wrapper.classes().length).toBe(1) }) it('should apply size class', async () => { - const { - app: { $refs } - } = window + const wrapper = mount(ButtonGroup, { + propsData: { + size: 'sm' + } + }) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('btn-group') + expect(wrapper.classes()).toContain('btn-group-sm') + expect(wrapper.classes().length).toBe(2) + }) + + it('should apply size class when vertical', async () => { + const wrapper = mount(ButtonGroup, { + propsData: { + size: 'sm', + vertical: true + } + }) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('btn-group-sm') + expect(wrapper.classes()).toContain('btn-group-vertical') + expect(wrapper.classes()).not.toContain('btn-group') + expect(wrapper.classes().length).toBe(2) + }) - expect($refs.size).toHaveClass('btn-group-sm') + it('has custom role when aria-role prop set', async () => { + const wrapper = mount(ButtonGroup, { + propsData: { + ariaRole: 'foobar' + } + }) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('btn-group') + expect(wrapper.classes().length).toBe(1) + expect(wrapper.attributes('role')).toBeDefined() + expect(wrapper.attributes('role')).toBe('foobar') }) }) diff --git a/src/components/button-group/fixtures/button-group.html b/src/components/button-group/fixtures/button-group.html deleted file mode 100644 index 87e5159c641..00000000000 --- a/src/components/button-group/fixtures/button-group.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
- -
- - Top - Middle - Bottom - -
- -
- - Left - Middle - Right - -
-
- - Left - Middle - Right - -
-
-
diff --git a/src/components/button-group/fixtures/button-group.js b/src/components/button-group/fixtures/button-group.js deleted file mode 100644 index 0bae7b95202..00000000000 --- a/src/components/button-group/fixtures/button-group.js +++ /dev/null @@ -1,3 +0,0 @@ -window.app = new Vue({ - el: '#app' -}) diff --git a/src/components/button/button.spec.js b/src/components/button/button.spec.js index c9525e0d00e..7047262b12c 100644 --- a/src/components/button/button.spec.js +++ b/src/components/button/button.spec.js @@ -1,191 +1,270 @@ -import { loadFixture, testVM, nextTick, setData } from '../../../tests/utils' - -/** - * Button functionality to test: - * - Style variants: [ 'primary','secondary','success','outline-success','warning','danger','link' ] - * - Sizes: [ 'sm','','lg' ] - * - Props: [ disabled, block ] - * - elements: [ , ] - */ -const colorVariants = ['primary', 'secondary', 'success', 'warning', 'danger'] -const outlineVariants = colorVariants.map(v => `outline-${v}`) -const variants = colorVariants.concat(outlineVariants, 'link') -const sizes = ['sm', '', 'lg'] - -const btnRefs = variants.reduce( - (memo, variant) => - memo.concat( - sizes.map(size => { - return { - variant, - size, - ref: `btn${size ? `_${size}` : ''}_${variant.replace(/-/g, '_')}` - } - }) - ), - [] -) +import Button from './button' +import { mount } from '@vue/test-utils' describe('button', () => { - beforeEach(loadFixture(__dirname, 'button')) - testVM() - - it('should contain class names', async () => { - const { - app: { $refs } - } = window + it('has default structure and classes', async () => { + const wrapper = mount(Button) + + expect(wrapper.is('button')).toBe(true) + expect(wrapper.attributes('type')).toBeDefined() + expect(wrapper.attributes('type')).toBe('button') + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes().length).toBe(2) + expect(wrapper.attributes('href')).not.toBeDefined() + expect(wrapper.attributes('role')).not.toBeDefined() + expect(wrapper.attributes('disabled')).not.toBeDefined() + expect(wrapper.attributes('aria-disabled')).not.toBeDefined() + expect(wrapper.attributes('aria-pressed')).not.toBeDefined() + expect(wrapper.attributes('autocomplete')).not.toBeDefined() + expect(wrapper.attributes('tabindex')).not.toBeDefined() + }) - btnRefs.forEach(({ ref, variant, size }) => { - // ref will contain an array of children because of v-for - const vm = $refs[ref][0] + it('renders a link when href provided', async () => { + const wrapper = mount(Button, { + propsData: { + href: '/foo/bar' + } + }) - let classList = ['btn', `btn-${variant}`] - if (size) classList.push(`btn-${size}`) + expect(wrapper.is('a')).toBe(true) + expect(wrapper.attributes('href')).toBeDefined() + expect(wrapper.attributes('href')).toBe('/foo/bar') + expect(wrapper.attributes('type')).not.toBeDefined() + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes().length).toBe(2) + expect(wrapper.attributes('role')).not.toBeDefined() + expect(wrapper.attributes('disabled')).not.toBeDefined() + expect(wrapper.attributes('aria-disabled')).not.toBeDefined() + expect(wrapper.attributes('aria-pressed')).not.toBeDefined() + expect(wrapper.attributes('autocomplete')).not.toBeDefined() + expect(wrapper.attributes('tabindex')).not.toBeDefined() + }) - expect(vm).toHaveAllClasses(classList) + it('renders default slot content', async () => { + const wrapper = mount(Button, { + slots: { + default: 'foobar' + } }) - const vmBlockDisabled = $refs.btn_block_disabled - expect(vmBlockDisabled).toHaveAllClasses(['btn', 'btn-block', 'disabled']) + expect(wrapper.is('button')).toBe(true) + expect(wrapper.attributes('type')).toBeDefined() + expect(wrapper.attributes('type')).toBe('button') + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes().length).toBe(2) + expect(wrapper.find('span').exists()).toBe(true) + expect(wrapper.text()).toBe('foobar') }) - it('should use when given href', async () => { - const { - app: { $refs } - } = window - const btnRootNode = $refs.btn_href + it('applies variant class', async () => { + const wrapper = mount(Button, { + propsData: { + variant: 'danger' + } + }) - expect(btnRootNode).toBeElement('a') - expect(btnRootNode.href).toBe('https://github.com/bootstrap-vue/bootstrap-vue') + expect(wrapper.is('button')).toBe(true) + expect(wrapper.attributes('type')).toBeDefined() + expect(wrapper.attributes('type')).toBe('button') + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-danger') + expect(wrapper.classes().length).toBe(2) }) - it('should use the given tag', async () => { - const { - app: { $refs } - } = window - const btnRootNode = $refs.btn_div + it('applies block class', async () => { + const wrapper = mount(Button, { + propsData: { + block: true + } + }) - expect(btnRootNode).toBeElement('div') + expect(wrapper.is('button')).toBe(true) + expect(wrapper.attributes('type')).toBeDefined() + expect(wrapper.attributes('type')).toBe('button') + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes()).toContain('btn-block') + expect(wrapper.classes().length).toBe(3) }) - it('should use button when no tag is given', async () => { - const { - app: { $refs } - } = window - const btnRootNode = $refs.btn_no_tag + it('renders custom root element', async () => { + const wrapper = mount(Button, { + propsData: { + tag: 'div' + } + }) - expect(btnRootNode).toBeElement('button') + expect(wrapper.is('div')).toBe(true) + expect(wrapper.attributes('type')).not.toBeDefined() + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes().length).toBe(2) + expect(wrapper.attributes('role')).toBeDefined() + expect(wrapper.attributes('role')).toBe('button') + expect(wrapper.attributes('aria-disabled')).toBeDefined() + expect(wrapper.attributes('aria-disabled')).toBe('false') + expect(wrapper.attributes('tabindex')).toBeDefined() + expect(wrapper.attributes('tabindex')).toBe('0') + expect(wrapper.attributes('disabled')).not.toBeDefined() + expect(wrapper.attributes('aria-pressed')).not.toBeDefined() + expect(wrapper.attributes('autocomplete')).not.toBeDefined() }) - it('should emit "click" event when clicked', async () => { - const { - app: { $refs } - } = window - const btn = $refs.btn_click - const spy = jest.fn() - - btn.addEventListener('click', spy) - btn.click() + it('button has attribute disabled when disabled set', async () => { + const wrapper = mount(Button, { + propsData: { + disabled: true + } + }) - expect(spy).toHaveBeenCalled() + expect(wrapper.is('button')).toBe(true) + expect(wrapper.attributes('type')).toBe('button') + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes()).toContain('disabled') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.attributes('aria-disabled')).not.toBeDefined() }) - it('should "click" event should emit with native event object', async () => { - const { - app: { $refs } - } = window - const btn = $refs.btn_click - let event = null - - btn.addEventListener('click', e => (event = e)) - btn.click() + it('link has attribute aria-disabled when disabled set', async () => { + const wrapper = mount(Button, { + propsData: { + href: '/foo/bar', + disabled: true + } + }) - expect(event).toBeInstanceOf(MouseEvent) + expect(wrapper.is('a')).toBe(true) + expect(wrapper.classes()).toContain('btn') + expect(wrapper.classes()).toContain('btn-secondary') + expect(wrapper.classes()).toContain('disabled') + // Both b-button and b-link add hte class 'disabled' + // vue-functional-data-merge or Vue doesn't appear to de-dup classes + // expect(wrapper.classes().length).toBe(3) + // actually returns 4, as disabled is there twice + expect(wrapper.attributes('aria-disabled')).toBeDefined() + expect(wrapper.attributes('aria-disabled')).toBe('true') }) - it('should be disabled and not emit click event with `disabled` prop true', async () => { - const { - app: { $refs } - } = window - const btn = $refs.btn_block_disabled - const spy = jest.fn() + it('should emit click event when clicked', async () => { + let called = 0 + let evt = null + const wrapper = mount(Button, { + listeners: { + click: e => { + evt = e + called++ + } + } + }) - btn.addEventListener('click', spy) - btn.click() + expect(wrapper.is('button')).toBe(true) + expect(called).toBe(0) + expect(evt).toEqual(null) + wrapper.find('button').trigger('click') + expect(called).toBe(1) + expect(evt).toBeInstanceOf(MouseEvent) + }) - expect(btn.disabled).toBe(true) - expect(spy).not.toHaveBeenCalled() + it('should not emit click event when clicked and disabled', async () => { + let called = 0 + const wrapper = mount(Button, { + propsData: { + disabled: true + }, + listeners: { + click: e => { + called++ + } + } + }) + + expect(wrapper.is('button')).toBe(true) + expect(called).toBe(0) + wrapper.find('button').trigger('click') + expect(called).toBe(0) }) it('should not have `.active` class and `aria-pressed` when pressed is null', async () => { - const { app } = window - const vm = app.$refs.btn_pressed - - await setData(app, 'btnToggle', null) - await nextTick() + const wrapper = mount(Button, { + propsData: { + pressed: null + } + }) - expect(vm).not.toHaveClass('active') - expect(vm.getAttribute('aria-pressed')).toBeNull() - vm.click() - expect(app.btnToggle).toBeNull() + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.attributes('aria-pressed')).not.toBeDefined() + wrapper.find('button').trigger('click') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.attributes('aria-pressed')).not.toBeDefined() + expect(wrapper.attributes('autocomplete')).not.toBeDefined() }) it('should not have `.active` class and have `aria-pressed="false"` when pressed is false', async () => { - const { app } = window - const vm = app.$refs.btn_pressed - - await setData(app, 'btnToggle', false) - await nextTick() + const wrapper = mount(Button, { + propsData: { + pressed: false + } + }) - expect(vm).not.toHaveClass('active') - expect(vm.getAttribute('aria-pressed')).toBe('false') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.attributes('aria-pressed')).toBeDefined() + expect(wrapper.attributes('aria-pressed')).toBe('false') + expect(wrapper.attributes('autocomplete')).toBeDefined() + expect(wrapper.attributes('autocomplete')).toBe('off') }) it('should have `.active` class and have `aria-pressed="true"` when pressed is true', async () => { - const { app } = window - const vm = app.$refs.btn_pressed - - await setData(app, 'btnToggle', true) - await nextTick() - - vm.click() + const wrapper = mount(Button, { + propsData: { + pressed: true + } + }) - expect(vm).toHaveClass('active') - expect(vm.getAttribute('aria-pressed')).toBe('true') + expect(wrapper.classes()).toContain('active') + expect(wrapper.attributes('aria-pressed')).toBeDefined() + expect(wrapper.attributes('aria-pressed')).toBe('true') + expect(wrapper.attributes('autocomplete')).toBeDefined() + expect(wrapper.attributes('autocomplete')).toBe('off') }) it('pressed should have `.focus` class when focused', async () => { - const { app } = window - const btn = app.$refs.btn_pressed - - await setData(app, 'btnToggle', false) - await nextTick() - - const focusinEvt = new FocusEvent('focusin') - btn.dispatchEvent(focusinEvt) - await nextTick() - expect(btn).toHaveClass('focus') + const wrapper = mount(Button, { + propsData: { + pressed: false + } + }) - const focusoutEvt = new FocusEvent('focusout') - btn.dispatchEvent(focusoutEvt) - await nextTick() - expect(btn).not.toHaveClass('focus') + expect(wrapper.classes()).not.toContain('focus') + wrapper.trigger('focusin') + expect(wrapper.classes()).toContain('focus') + wrapper.trigger('focusout') + expect(wrapper.classes()).not.toContain('focus') }) it('should update the parent sync value on click and when pressed is not null', async () => { - const { app } = window - const vm = app.$refs.btn_pressed - - await setData(app, 'btnToggle', false) - await nextTick() - - expect(app.btnToggle).toBe(false) - expect(vm).not.toHaveClass('active') - expect(vm.getAttribute('aria-pressed')).toBe('false') - vm.click() - await nextTick() - expect(app.btnToggle).toBe(true) - expect(vm).toHaveClass('active') - expect(vm.getAttribute('aria-pressed')).toBe('true') + let called = 0 + const values = [] + const wrapper = mount(Button, { + propsData: { + pressed: false + }, + listeners: { + 'update:pressed': val => { + values.push(val) + called++ + } + } + }) + + expect(called).toBe(0) + + wrapper.find('button').trigger('click') + + expect(called).toBe(1) + expect(values[0]).toBe(true) }) }) diff --git a/src/components/button/fixtures/button-close.html b/src/components/button/fixtures/button-close.html deleted file mode 100644 index f1192872cab..00000000000 --- a/src/components/button/fixtures/button-close.html +++ /dev/null @@ -1,8 +0,0 @@ -
- - close - - - - -
diff --git a/src/components/button/fixtures/button-close.js b/src/components/button/fixtures/button-close.js deleted file mode 100644 index 94343157bdb..00000000000 --- a/src/components/button/fixtures/button-close.js +++ /dev/null @@ -1,11 +0,0 @@ -window.app = new Vue({ - el: '#app', - data: { - spies: [] - }, - methods: { - handleClick() { - this.spies.forEach(spy => spy.apply(undefined, arguments)) - } - } -}) diff --git a/src/components/button/fixtures/button.html b/src/components/button/fixtures/button.html deleted file mode 100644 index 971755303ea..00000000000 --- a/src/components/button/fixtures/button.html +++ /dev/null @@ -1,63 +0,0 @@ -
-
- -
- - - Link button! - github - -
-
- - I am a div - -
-
- - I am a button - -
-
- - Click me! - -
-
- - Can't touch this - -
-
- - Toggle Me - -
-
-
diff --git a/src/components/button/fixtures/button.js b/src/components/button/fixtures/button.js deleted file mode 100644 index 8e1172350b1..00000000000 --- a/src/components/button/fixtures/button.js +++ /dev/null @@ -1,25 +0,0 @@ -window.app = new Vue({ - el: '#app', - data: { - variants: [ - 'primary', - 'secondary', - 'success', - 'warning', - 'danger', - 'outline-primary', - 'outline-secondary', - 'outline-success', - 'outline-warning', - 'outline-danger', - 'link' - ], - sizes: ['sm', '', 'lg'], - btnToggle: null - }, - methods: { - handleClick(event) { - // STUB! - } - } -})