aboutsummaryrefslogtreecommitdiff
path: root/js/src/button
diff options
context:
space:
mode:
authorJohann-S <[email protected]>2019-03-25 11:32:02 +0100
committerJohann-S <[email protected]>2019-07-23 14:23:50 +0200
commit891a187059918d850981f585dc61ac42f456662b (patch)
tree6bf2f978d88206cfa5319a75c136e16375430c8a /js/src/button
parentc834895fa0e7d215ee8cb17b3efa8d0ce57a718c (diff)
downloadbootstrap-891a187059918d850981f585dc61ac42f456662b.tar.xz
bootstrap-891a187059918d850981f585dc61ac42f456662b.zip
rewrite button unit tests
Diffstat (limited to 'js/src/button')
-rw-r--r--js/src/button/button.js200
-rw-r--r--js/src/button/button.spec.js292
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)
+ })
+ })
+})