diff options
| author | Johann-S <[email protected]> | 2019-03-25 11:32:02 +0100 |
|---|---|---|
| committer | Johann-S <[email protected]> | 2019-07-23 14:23:50 +0200 |
| commit | 891a187059918d850981f585dc61ac42f456662b (patch) | |
| tree | 6bf2f978d88206cfa5319a75c136e16375430c8a /js/src/button | |
| parent | c834895fa0e7d215ee8cb17b3efa8d0ce57a718c (diff) | |
| download | bootstrap-891a187059918d850981f585dc61ac42f456662b.tar.xz bootstrap-891a187059918d850981f585dc61ac42f456662b.zip | |
rewrite button unit tests
Diffstat (limited to 'js/src/button')
| -rw-r--r-- | js/src/button/button.js | 200 | ||||
| -rw-r--r-- | js/src/button/button.spec.js | 292 |
2 files changed, 492 insertions, 0 deletions
diff --git a/js/src/button/button.js b/js/src/button/button.js new file mode 100644 index 000000000..2e6033b64 --- /dev/null +++ b/js/src/button/button.js @@ -0,0 +1,200 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { jQuery as $ } from '../util/index' +import Data from '../dom/data' +import EventHandler from '../dom/event-handler' +import SelectorEngine from '../dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'button' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.button' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const ClassName = { + ACTIVE: 'active', + BUTTON: 'btn', + FOCUS: 'focus' +} + +const Selector = { + DATA_TOGGLE_CARROT: '[data-toggle^="button"]', + DATA_TOGGLE: '[data-toggle="buttons"]', + INPUT: 'input:not([type="hidden"])', + ACTIVE: '.active', + BUTTON: '.btn' +} + +const Event = { + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`, + FOCUS_DATA_API: `focus${EVENT_KEY}${DATA_API_KEY}`, + BLUR_DATA_API: `blur${EVENT_KEY}${DATA_API_KEY}` +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Button { + constructor(element) { + this._element = element + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + // Public + + toggle() { + let triggerChangeEvent = true + let addAriaPressed = true + + const rootElement = SelectorEngine.closest( + this._element, + Selector.DATA_TOGGLE + ) + + if (rootElement) { + const input = SelectorEngine.findOne(Selector.INPUT, this._element) + + if (input) { + if (input.type === 'radio') { + if (input.checked && + this._element.classList.contains(ClassName.ACTIVE)) { + triggerChangeEvent = false + } else { + const activeElement = SelectorEngine.findOne(Selector.ACTIVE, rootElement) + + if (activeElement) { + activeElement.classList.remove(ClassName.ACTIVE) + } + } + } + + if (triggerChangeEvent) { + if (input.hasAttribute('disabled') || + rootElement.hasAttribute('disabled') || + input.classList.contains('disabled') || + rootElement.classList.contains('disabled')) { + return + } + + input.checked = !this._element.classList.contains(ClassName.ACTIVE) + EventHandler.trigger(input, 'change') + } + + input.focus() + addAriaPressed = false + } + } + + if (addAriaPressed) { + this._element.setAttribute('aria-pressed', + !this._element.classList.contains(ClassName.ACTIVE)) + } + + if (triggerChangeEvent) { + this._element.classList.toggle(ClassName.ACTIVE) + } + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + this._element = null + } + + // Static + + static _jQueryInterface(config) { + return this.each(function () { + let data = Data.getData(this, DATA_KEY) + + if (!data) { + data = new Button(this) + } + + if (config === 'toggle') { + data[config]() + } + }) + } + + static _getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { + event.preventDefault() + + let button = event.target + if (!button.classList.contains(ClassName.BUTTON)) { + button = SelectorEngine.closest(button, Selector.BUTTON) + } + + let data = Data.getData(button, DATA_KEY) + if (!data) { + data = new Button(button) + } + + data.toggle() +}) + +EventHandler.on(document, Event.FOCUS_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { + const button = SelectorEngine.closest(event.target, Selector.BUTTON) + + if (button) { + button.classList.add(ClassName.FOCUS) + } +}) + +EventHandler.on(document, Event.BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { + const button = SelectorEngine.closest(event.target, Selector.BUTTON) + + if (button) { + button.classList.remove(ClassName.FOCUS) + } +}) + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * 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 + $.fn[NAME].Constructor = Button + + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Button._jQueryInterface + } +} + +export default Button diff --git a/js/src/button/button.spec.js b/js/src/button/button.spec.js new file mode 100644 index 000000000..711408896 --- /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) + }) + }) +}) |
