diff options
-rw-r--r-- | build/build-plugins.js | 2 | ||||
-rw-r--r-- | js/index.esm.js | 2 | ||||
-rw-r--r-- | js/index.umd.js | 2 | ||||
-rw-r--r-- | js/src/button/button.js (renamed from js/src/button.js) | 11 | ||||
-rw-r--r-- | js/src/button/button.spec.js | 292 | ||||
-rw-r--r-- | js/tests/helpers/fixture.js | 18 | ||||
-rw-r--r-- | js/tests/unit/button.js | 225 |
7 files changed, 318 insertions, 234 deletions
diff --git a/build/build-plugins.js b/build/build-plugins.js index 471707035b..5d8c429635 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -33,7 +33,7 @@ const bsPlugins = { Polyfill: path.resolve(__dirname, '../js/src/dom/polyfill.js'), SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'), Alert: path.resolve(__dirname, '../js/src/alert/alert.js'), - Button: path.resolve(__dirname, '../js/src/button.js'), + Button: path.resolve(__dirname, '../js/src/button/button.js'), Carousel: path.resolve(__dirname, '../js/src/carousel.js'), Collapse: path.resolve(__dirname, '../js/src/collapse.js'), Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'), diff --git a/js/index.esm.js b/js/index.esm.js index e3a851537c..ca47d7405e 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -6,7 +6,7 @@ */ import Alert from './src/alert/alert' -import Button from './src/button' +import Button from './src/button/button' import Carousel from './src/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' diff --git a/js/index.umd.js b/js/index.umd.js index 039e6d1bb4..2cb90696da 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -6,7 +6,7 @@ */ import Alert from './src/alert/alert' -import Button from './src/button' +import Button from './src/button/button' import Carousel from './src/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' diff --git a/js/src/button.js b/js/src/button/button.js index c69a8a3901..2e6033b64e 100644 --- a/js/src/button.js +++ b/js/src/button/button.js @@ -5,10 +5,10 @@ * -------------------------------------------------------------------------- */ -import { jQuery as $ } from './util/index' -import Data from './dom/data' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' +import { jQuery as $ } from '../util/index' +import Data from '../dom/data' +import EventHandler from '../dom/event-handler' +import SelectorEngine from '../dom/selector-engine' /** * ------------------------------------------------------------------------ @@ -158,7 +158,6 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, eve let data = Data.getData(button, DATA_KEY) if (!data) { data = new Button(button) - Data.setData(button, DATA_KEY, data) } data.toggle() @@ -186,7 +185,7 @@ EventHandler.on(document, Event.BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, even * ------------------------------------------------------------------------ * add .button to jQuery only if jQuery is present */ - +/* istanbul ignore if */ if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Button._jQueryInterface diff --git a/js/src/button/button.spec.js b/js/src/button/button.spec.js new file mode 100644 index 0000000000..7114088964 --- /dev/null +++ b/js/src/button/button.spec.js @@ -0,0 +1,292 @@ +import Button from './button' +import EventHandler from '../dom/event-handler' + +/** Test helpers */ +import { + getFixture, + clearFixture, + createEvent, + jQueryMock +} from '../../tests/helpers/fixture' + +describe('Button', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Button.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('data-api', () => { + it('should toggle active class on click', () => { + fixtureEl.innerHTML = [ + '<button class="btn" data-toggle="button">btn</button>', + '<button class="btn testParent" data-toggle="button"><div class="test"></div></button>' + ].join('') + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + const btnTestParent = fixtureEl.querySelector('.testParent') + + expect(btn.classList.contains('active')).toEqual(false) + + btn.click() + + expect(btn.classList.contains('active')).toEqual(true) + + btn.click() + + expect(btn.classList.contains('active')).toEqual(false) + + divTest.click() + + expect(btnTestParent.classList.contains('active')).toEqual(true) + }) + + it('should trigger input change event when toggled button has input field', done => { + fixtureEl.innerHTML = [ + '<div class="btn-group" data-toggle="buttons">', + ' <label class="btn btn-primary">', + ' <input type="radio" id="radio" autocomplete="off"> Radio', + ' </label>', + '</div>' + ].join('') + + const input = fixtureEl.querySelector('input') + const label = fixtureEl.querySelector('label') + + input.addEventListener('change', () => { + expect().nothing() + done() + }) + + label.click() + }) + + it('should not trigger input change event when input already checked and button is active', () => { + fixtureEl.innerHTML = [ + '<button type="button" class="btn btn-primary active" data-toggle="buttons">', + ' <input type="radio" id="radio" autocomplete="off" checked> Radio', + '</button>' + ].join('') + + const button = fixtureEl.querySelector('button') + + spyOn(EventHandler, 'trigger') + + button.click() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should remove active when an other radio button is clicked', () => { + fixtureEl.innerHTML = [ + '<div class="btn-group btn-group-toggle" data-toggle="buttons">', + ' <label class="btn btn-secondary active">', + ' <input type="radio" name="options" id="option1" autocomplete="off" checked> Active', + ' </label>', + ' <label class="btn btn-secondary">', + ' <input type="radio" name="options" id="option2" autocomplete="off"> Radio', + ' </label>', + ' <label class="btn btn-secondary">', + ' <input type="radio" name="options" id="option3" autocomplete="off"> Radio', + ' </label>', + '</div>' + ].join('') + + const option1 = fixtureEl.querySelector('#option1') + const option2 = fixtureEl.querySelector('#option2') + + expect(option1.checked).toEqual(true) + expect(option1.parentElement.classList.contains('active')).toEqual(true) + + const clickEvent = createEvent('click') + + option2.dispatchEvent(clickEvent) + + expect(option1.checked).toEqual(false) + expect(option1.parentElement.classList.contains('active')).toEqual(false) + expect(option2.checked).toEqual(true) + expect(option2.parentElement.classList.contains('active')).toEqual(true) + }) + + it('should do nothing if the child is not an input', () => { + fixtureEl.innerHTML = [ + '<div class="btn-group btn-group-toggle" data-toggle="buttons">', + ' <label class="btn btn-secondary active">', + ' <span id="option1">el 1</span>', + ' </label>', + ' <label class="btn btn-secondary">', + ' <span id="option2">el 2</span>', + ' </label>', + ' <label class="btn btn-secondary">', + ' <span>el 3</span>', + ' </label>', + '</div>' + ].join('') + + const option2 = fixtureEl.querySelector('#option2') + const clickEvent = createEvent('click') + + option2.dispatchEvent(clickEvent) + + expect().nothing() + }) + + it('should add focus class on focus event', () => { + fixtureEl.innerHTML = '<button class="btn" data-toggle="button"><input type="text" /></button>' + + const btn = fixtureEl.querySelector('.btn') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('focus') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(true) + }) + + it('should not add focus class', () => { + fixtureEl.innerHTML = '<button data-toggle="button"><input type="text" /></button>' + + const btn = fixtureEl.querySelector('button') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('focus') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(false) + }) + + it('should remove focus class on blur event', () => { + fixtureEl.innerHTML = '<button class="btn focus" data-toggle="button"><input type="text" /></button>' + + const btn = fixtureEl.querySelector('.btn') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('blur') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(false) + }) + + it('should not remove focus class on blur event', () => { + fixtureEl.innerHTML = '<button class="focus" data-toggle="button"><input type="text" /></button>' + + const btn = fixtureEl.querySelector('button') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('blur') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(true) + }) + }) + + describe('toggle', () => { + it('should toggle aria-pressed', () => { + fixtureEl.innerHTML = '<button class="btn" data-toggle="button" aria-pressed="false"></button>' + + const btnEl = fixtureEl.querySelector('.btn') + const button = new Button(btnEl) + + expect(btnEl.getAttribute('aria-pressed')).toEqual('false') + expect(btnEl.classList.contains('active')).toEqual(false) + + button.toggle() + + expect(btnEl.getAttribute('aria-pressed')).toEqual('true') + expect(btnEl.classList.contains('active')).toEqual(true) + }) + + it('should handle disabled attribute on non-button elements', () => { + fixtureEl.innerHTML = [ + '<div class="btn-group disabled" data-toggle="buttons" aria-disabled="true" disabled>', + ' <label class="btn btn-danger disabled" aria-disabled="true" disabled>', + ' <input type="checkbox" aria-disabled="true" autocomplete="off" disabled class="disabled"/>', + ' </label>', + '</div>' + ].join('') + + const btnGroupEl = fixtureEl.querySelector('.btn-group') + const btnDanger = fixtureEl.querySelector('.btn-danger') + const input = fixtureEl.querySelector('input') + + const button = new Button(btnGroupEl) + + button.toggle() + + expect(btnDanger.hasAttribute('disabled')).toEqual(true) + expect(input.checked).toEqual(false) + }) + }) + + describe('dispose', () => { + it('should dispose a button', () => { + fixtureEl.innerHTML = '<button class="btn" data-toggle="button"></button>' + + const btnEl = fixtureEl.querySelector('.btn') + const button = new Button(btnEl) + + expect(Button._getInstance(btnEl)).toBeDefined() + + button.dispose() + + expect(Button._getInstance(btnEl)).toBeNull() + }) + }) + + describe('_jQueryInterface', () => { + it('should handle config passed and toggle existing button', () => { + fixtureEl.innerHTML = '<button class="btn" data-toggle="button"></button>' + + const btnEl = fixtureEl.querySelector('.btn') + const button = new Button(btnEl) + + spyOn(button, 'toggle') + + jQueryMock.fn.button = Button._jQueryInterface + jQueryMock.elements = [btnEl] + + jQueryMock.fn.button.call(jQueryMock, 'toggle') + + expect(button.toggle).toHaveBeenCalled() + }) + + it('should create new button instance and call toggle', () => { + fixtureEl.innerHTML = '<button class="btn" data-toggle="button"></button>' + + const btnEl = fixtureEl.querySelector('.btn') + + jQueryMock.fn.button = Button._jQueryInterface + jQueryMock.elements = [btnEl] + + jQueryMock.fn.button.call(jQueryMock, 'toggle') + + expect(Button._getInstance(btnEl)).toBeDefined() + expect(btnEl.classList.contains('active')).toEqual(true) + }) + + it('should just create a button instance without calling toggle', () => { + fixtureEl.innerHTML = '<button class="btn" data-toggle="button"></button>' + + const btnEl = fixtureEl.querySelector('.btn') + + jQueryMock.fn.button = Button._jQueryInterface + jQueryMock.elements = [btnEl] + + jQueryMock.fn.button.call(jQueryMock) + + expect(Button._getInstance(btnEl)).toBeDefined() + expect(btnEl.classList.contains('active')).toEqual(false) + }) + }) +}) diff --git a/js/tests/helpers/fixture.js b/js/tests/helpers/fixture.js index 524d544448..e7240ee181 100644 --- a/js/tests/helpers/fixture.js +++ b/js/tests/helpers/fixture.js @@ -18,3 +18,21 @@ export const clearFixture = () => { fixtureEl.innerHTML = '' } + +export const createEvent = (eventName, params) => { + params = params || {} + const e = document.createEvent('Event') + + e.initEvent(eventName, Boolean(params.bubbles), Boolean(params.cancelable)) + return e +} + +export const jQueryMock = { + elements: undefined, + fn: {}, + each(fn) { + this.elements.forEach(el => { + fn.call(el) + }) + } +} diff --git a/js/tests/unit/button.js b/js/tests/unit/button.js deleted file mode 100644 index d351cc7705..0000000000 --- a/js/tests/unit/button.js +++ /dev/null @@ -1,225 +0,0 @@ -$(function () { - 'use strict' - - var Button = typeof window.bootstrap === 'undefined' ? window.Button : window.bootstrap.Button - - QUnit.module('button plugin') - - QUnit.test('should be defined on jquery object', function (assert) { - assert.expect(1) - assert.ok($(document.body).button, 'button method is defined') - }) - - QUnit.module('button', { - beforeEach: function () { - // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode - $.fn.bootstrapButton = $.fn.button.noConflict() - }, - afterEach: function () { - $.fn.button = $.fn.bootstrapButton - delete $.fn.bootstrapButton - $('#qunit-fixture').html('') - } - }) - - QUnit.test('should provide no conflict', function (assert) { - assert.expect(1) - assert.strictEqual(typeof $.fn.button, 'undefined', 'button was set back to undefined (org value)') - }) - - QUnit.test('should return jquery collection containing the element', function (assert) { - assert.expect(2) - var $el = $('<div/>') - var $button = $el.bootstrapButton() - assert.ok($button instanceof $, 'returns jquery collection') - assert.strictEqual($button[0], $el[0], 'collection contains element') - }) - - QUnit.test('should toggle active', function (assert) { - assert.expect(2) - var $btn = $('<button class="btn" data-toggle="button">mdo</button>') - assert.ok(!$btn.hasClass('active'), 'btn does not have active class') - $btn.bootstrapButton('toggle') - assert.ok($btn.hasClass('active'), 'btn has class active') - }) - - QUnit.test('should toggle active when btn children are clicked', function (assert) { - assert.expect(2) - var $btn = $('<button class="btn" data-toggle="button">mdo</button>') - var $inner = $('<i/>') - $btn - .append($inner) - .appendTo('#qunit-fixture') - assert.ok(!$btn.hasClass('active'), 'btn does not have active class') - $inner.trigger('click') - assert.ok($btn.hasClass('active'), 'btn has class active') - }) - - QUnit.test('should toggle aria-pressed', function (assert) { - assert.expect(2) - var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">redux</button>') - assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is false') - $btn.bootstrapButton('toggle') - assert.strictEqual($btn.attr('aria-pressed'), 'true', 'btn aria-pressed state is true') - }) - - QUnit.test('should toggle aria-pressed on buttons with container', function (assert) { - assert.expect(1) - var groupHTML = '<div class="btn-group" data-toggle="buttons">' + - '<button id="btn1" class="btn btn-secondary" type="button">One</button>' + - '<button class="btn btn-secondary" type="button">Two</button>' + - '</div>' - $('#qunit-fixture').append(groupHTML) - $('#btn1').bootstrapButton('toggle') - assert.strictEqual($('#btn1').attr('aria-pressed'), 'true') - }) - - QUnit.test('should toggle aria-pressed when btn children are clicked', function (assert) { - assert.expect(2) - var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">redux</button>') - var $inner = $('<i/>') - $btn - .append($inner) - .appendTo('#qunit-fixture') - assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is false') - $inner.trigger('click') - assert.strictEqual($btn.attr('aria-pressed'), 'true', 'btn aria-pressed state is true') - }) - - QUnit.test('should trigger input change event when toggled button has input field', function (assert) { - assert.expect(1) - var done = assert.async() - - var groupHTML = '<div class="btn-group" data-toggle="buttons">' + - '<label class="btn btn-primary">' + - '<input type="radio" id="radio" autocomplete="off">Radio' + - '</label>' + - '</div>' - var $group = $(groupHTML).appendTo('#qunit-fixture') - - $group.find('input').on('change', function (e) { - e.preventDefault() - assert.ok(true, 'change event fired') - done() - }) - - $group.find('label').trigger('click') - }) - - QUnit.test('should check for closest matching toggle', function (assert) { - assert.expect(12) - var groupHTML = - '<div class="btn-group" data-toggle="buttons">' + - ' <label class="btn btn-primary active">' + - ' <input type="radio" name="options" id="option1" checked="true"> Option 1' + - ' </label>' + - ' <label class="btn btn-primary">' + - ' <input type="radio" name="options" id="option2"> Option 2' + - ' </label>' + - ' <label class="btn btn-primary">' + - ' <input type="radio" name="options" id="option3"> Option 3' + - ' </label>' + - '</div>' - - var $group = $(groupHTML).appendTo('#qunit-fixture') - - var $btn1 = $group.children().eq(0) - var $btn2 = $group.children().eq(1) - var inputBtn2 = $btn2.find('input')[0] - - assert.ok($btn1.hasClass('active'), 'btn1 has active class') - assert.ok($btn1.find('input').prop('checked'), 'btn1 is checked') - assert.ok(!$btn2.hasClass('active'), 'btn2 does not have active class') - assert.ok(!inputBtn2.checked, 'btn2 is not checked') - - inputBtn2.dispatchEvent(new Event('click')) - - assert.ok(!$btn1.hasClass('active'), 'btn1 does not have active class') - assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is not checked') - assert.ok($btn2.hasClass('active'), 'btn2 has active class') - assert.ok(inputBtn2.checked, 'btn2 is checked') - - inputBtn2.dispatchEvent(new Event('click')) // clicking an already checked radio should not un-check it - - assert.ok(!$btn1.hasClass('active'), 'btn1 does not have active class') - assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is not checked') - assert.ok($btn2.hasClass('active'), 'btn2 has active class') - assert.ok(inputBtn2.checked, 'btn2 is checked') - }) - - QUnit.test('should only toggle selectable inputs', function (assert) { - assert.expect(6) - var groupHTML = '<div class="btn-group" data-toggle="buttons">' + - '<label class="btn btn-primary active">' + - '<input type="hidden" name="option1" id="option1-default" value="false">' + - '<input type="checkbox" name="option1" id="option1" checked="true"> Option 1' + - '</label>' + - '</div>' - var $group = $(groupHTML).appendTo('#qunit-fixture') - - var $btn = $group.children().eq(0) - var $hidden = $btn.find('input#option1-default') - var $cb = $btn.find('input#option1') - - assert.ok($btn.hasClass('active'), 'btn has active class') - assert.ok($cb.prop('checked'), 'btn is checked') - assert.ok(!$hidden.prop('checked'), 'hidden is not checked') - $btn.trigger('click') - assert.ok(!$btn.hasClass('active'), 'btn does not have active class') - assert.ok(!$cb.prop('checked'), 'btn is not checked') - assert.ok(!$hidden.prop('checked'), 'hidden is not checked') // should not be changed - }) - - QUnit.test('should not add aria-pressed on labels for radio/checkbox inputs in a data-toggle="buttons" group', function (assert) { - assert.expect(2) - var groupHTML = '<div class="btn-group" data-toggle="buttons">' + - '<label class="btn btn-primary"><input type="checkbox" autocomplete="off"> Checkbox</label>' + - '<label class="btn btn-primary"><input type="radio" name="options" autocomplete="off"> Radio</label>' + - '</div>' - var $group = $(groupHTML).appendTo('#qunit-fixture') - - var $btn1 = $group.children().eq(0) - var $btn2 = $group.children().eq(1) - - $btn1.find('input').trigger('click') - assert.ok($btn1.is(':not([aria-pressed])'), 'label for nested checkbox input has not been given an aria-pressed attribute') - - $btn2.find('input').trigger('click') - assert.ok($btn2.is(':not([aria-pressed])'), 'label for nested radio input has not been given an aria-pressed attribute') - }) - - QUnit.test('should handle disabled attribute on non-button elements', function (assert) { - assert.expect(2) - var groupHTML = '<div class="btn-group disabled" data-toggle="buttons" aria-disabled="true" disabled>' + - '<label class="btn btn-danger disabled" aria-disabled="true" disabled>' + - '<input type="checkbox" aria-disabled="true" autocomplete="off" disabled class="disabled"/>' + - '</label>' + - '</div>' - var $group = $(groupHTML).appendTo('#qunit-fixture') - - var $btn = $group.children().eq(0) - var $input = $btn.children().eq(0) - - $btn.trigger('click') - assert.ok($btn.is(':not(.active)'), 'button did not become active') - assert.ok(!$input.is(':checked'), 'checkbox did not get checked') - }) - - QUnit.test('dispose should remove data and the element', function (assert) { - assert.expect(2) - - var $el = $('<div/>') - var $button = $el.bootstrapButton() - - assert.ok(typeof Button._getInstance($button[0]) !== 'undefined') - - Button._getInstance($button[0]).dispose() - - assert.ok(Button._getInstance($button[0]) === null) - }) - - QUnit.test('should return the version', function (assert) { - assert.expect(1) - assert.strictEqual(typeof Button.VERSION, 'string') - }) -}) |