Skip to content

Commit

Permalink
test: has_many
Browse files Browse the repository at this point in the history
test: has_many -> recomputePosition

chore: change web api

test: bindDestroyEvent

test: bindRemoveEvent

test: bindAddEvent

chore: refactored DOM manipulation, deleted useless code

test: bindRemoveEventCallBack

chore: apply new DOM node to tests

test: bindAddEventCallBack

refactor: event call back function

chore: change mock to spy

chore: change mock to spy

chore: lint

test: initSortable new assert

test: bindAddEventCallBack more assertions
  • Loading branch information
dulerong committed Oct 19, 2021
1 parent 407147b commit 9bfc4c8
Show file tree
Hide file tree
Showing 2 changed files with 302 additions and 38 deletions.
261 changes: 261 additions & 0 deletions app/javascript/adminterface/lib/__tests__/has_many.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/* eslint-env jest */
/* global Event */
import Sortable from 'sortablejs'
import HasManyClass from '../has_many'

jest.mock('sortablejs')

describe('HasMany', () => {
const newNode = document.createElement('div')
const element = document.createElement('form')
newNode.setAttribute('data-sortable', 'address')
element.className = 'has-many-list'

const fieldSetElement = document.createElement('fieldset')
fieldSetElement.className = 'has_many_fields'

const destroyInputElement = document.createElement('input')
destroyInputElement.name = '[_destroy]'
destroyInputElement.type = 'checkbox'
destroyInputElement.checked = true

const addressInputElement = document.createElement('input')
addressInputElement.name = '[address]'
addressInputElement.value = 'Tokyo'

const anchorButtonRemoveElement = document.createElement('a')
anchorButtonRemoveElement.classList.add('button', 'has_many_remove')

const anchorButtonAddElement = document.createElement('a')
anchorButtonAddElement.classList.add('button', 'has_many_add')
const datasetHTML = document.createElement('div')
anchorButtonAddElement.setAttribute('data-html', datasetHTML.outerHTML)
anchorButtonAddElement.setAttribute('data-placeholder', 'script')

const events = {
removeBefore: new Event('has_many_remove:before'),
removeAfter: new Event('has_many_remove:after'),
addBefore: new Event('has_many_add:before'),
addAfter: new Event('has_many_add:after')
}
const options = {
list: '.has-many-list',
item: 'fieldset.has_many_fields',
addLink: 'a.button.has_many_add',
removeLink: 'a.button.has_many_remove',
destroyInput: 'input[name$="[_destroy]"]'
}

beforeEach(() => {
newNode.appendChild(element)
element.appendChild(fieldSetElement)
fieldSetElement.appendChild(destroyInputElement)
fieldSetElement.appendChild(addressInputElement)
fieldSetElement.appendChild(anchorButtonRemoveElement)
fieldSetElement.appendChild(anchorButtonAddElement)
document.body.appendChild(newNode)
global.adminterface = {
addObserver: jest.fn()
}
})

afterEach(() => {
jest.restoreAllMocks()
document.body.innerHTML = ''
})

test('constructor initiates correctly', () => {
const spyOnBind = jest.spyOn(HasManyClass.prototype, '_bind')
spyOnBind.mockImplementation(() => {})

const HasMany = new HasManyClass(newNode)
expect(HasMany.element).toStrictEqual(newNode)

expect(HasMany.events).toStrictEqual(events)
expect(HasMany.options).toStrictEqual(options)

expect(spyOnBind).toHaveBeenCalledTimes(1)
})

test('bind calls bindEvents, initSortable and admininterface', () => {
const spyOnBindEvents = jest.spyOn(HasManyClass.prototype, '_bindEvents')
spyOnBindEvents.mockImplementation(() => {})

const spyOnInitSortable = jest.spyOn(HasManyClass.prototype, '_initSortable')
spyOnInitSortable.mockImplementation(() => {})

const HasMany = new HasManyClass(newNode)

expect(spyOnBindEvents).toHaveBeenCalledWith(newNode)
expect(spyOnInitSortable).toHaveBeenCalledTimes(1)
expect(global.adminterface.addObserver).toHaveBeenCalledTimes(1)
expect(global.adminterface.addObserver).toHaveBeenCalledWith(
newNode,
HasMany,
HasMany.constructor.name,
[]
)
})

test('initSortable assigns new instance of Sortable to sortable property', () => {
const spyOnBindEvents = jest.spyOn(HasManyClass.prototype, '_bindEvents')
spyOnBindEvents.mockImplementation(() => {})

const HasMany = new HasManyClass(newNode)

const MockSortableArgument = {
handle: '.handle',
animation: 150,
ghostClass: 'sortable-placeholder',
dragClass: 'sortable-drag',
onUpdate: expect.any(Function)
}

expect(Sortable).toHaveBeenCalledTimes(1)
expect(Sortable).toHaveBeenCalledWith(
document.querySelector(HasMany.options.list),
MockSortableArgument
)
const sortable = new Sortable(
document.querySelector(HasMany.options.list),
MockSortableArgument
)
expect(HasMany.sortable.toString()).toStrictEqual(sortable.toString())
})

test('recomputePosition changes sortableInput value', () => {
const query = 'input[name$="[address]"]'
const sortableInput = document.body.querySelector(query)

expect(sortableInput.value.length).not.toEqual(0)

const HasMany = new HasManyClass(newNode)
HasMany._recomputePosition()

expect(sortableInput.value.length).toEqual(0)
})

test('bindDestroyEvent called with element and change event added', () => {
const spyOnBindDestroyEvent = jest.spyOn(
HasManyClass.prototype,
'_bindDestroyEvent'
)
const destroyInput = document.querySelector(options.destroyInput)
destroyInput.addEventListener = jest.fn()

new HasManyClass(newNode) // eslint-disable-line no-new

expect(spyOnBindDestroyEvent).toHaveBeenCalledTimes(1)
expect(spyOnBindDestroyEvent).toHaveBeenCalledWith(destroyInput)

expect(destroyInput.addEventListener).toHaveBeenCalledTimes(1)
expect(destroyInput.addEventListener).toHaveBeenCalledWith(
'change',
expect.any(Function)
)
})

test('bindRemoveEvent called with element and click event added', () => {
const spyOnBindRemoveEvent = jest.spyOn(
HasManyClass.prototype,
'_bindRemoveEvent'
)
const removeLinks = document.querySelector(options.removeLink)
removeLinks.addEventListener = jest.fn()

new HasManyClass(newNode) // eslint-disable-line no-new

expect(spyOnBindRemoveEvent).toHaveBeenCalledTimes(1)
expect(spyOnBindRemoveEvent).toHaveBeenCalledWith(removeLinks)

expect(removeLinks.addEventListener).toHaveBeenCalledTimes(1)
expect(removeLinks.addEventListener).toHaveBeenCalledWith(
'click',
expect.any(Function)
)
})

test('bindRemoveEventCallBack', () => {
const mockEvent = {
preventDefault: () => {},
target: document.body.querySelector('fieldset')
}
const spyOnRecomputePosition = jest.spyOn(
HasManyClass.prototype,
'_recomputePosition'
)
const HasMany = new HasManyClass(newNode)
const spyOnDispatch = jest.spyOn(HasMany.element, 'dispatchEvent')

const fieldSet = mockEvent.target.closest(HasMany.options.item)
fieldSet.remove = jest.fn()

const parent = HasMany.element
const removeBeforeEvent = HasMany.events.removeBefore
removeBeforeEvent.detail = { fieldSet, parent }
const removeAfterEvent = HasMany.events.removeAfter
removeAfterEvent.detail = { fieldSet, parent }

const output = HasMany._bindRemoveEventCallBack(mockEvent)

expect(spyOnRecomputePosition).toHaveBeenCalledTimes(1)
expect(spyOnDispatch).toHaveBeenCalledTimes(2)
expect(spyOnDispatch.mock.calls).toMatchObject([
[removeBeforeEvent],
[removeAfterEvent]
])
expect(fieldSet.remove).toHaveBeenCalledTimes(1)
expect(output).toBeTruthy()
})

test('bindAddEvent called with correctly element and click event added', () => {
const spyOnBindAddEvent = jest.spyOn(
HasManyClass.prototype,
'_bindAddEvent'
)

const addLinks = document.querySelector(options.addLink)
addLinks.addEventListener = jest.fn()

new HasManyClass(element) // eslint-disable-line no-new

expect(spyOnBindAddEvent).toHaveBeenCalledTimes(1)
expect(spyOnBindAddEvent).toHaveBeenCalledWith(addLinks)

expect(addLinks.addEventListener).toHaveBeenCalledTimes(1)
expect(addLinks.addEventListener).toHaveBeenCalledWith(
'click',
expect.any(Function)
)
})

test('bindAddEventCallBack', () => {
const mockEvent = {
preventDefault: () => {},
target: document.body.querySelector(options.addLink)
}
const HasMany = new HasManyClass(newNode)
const spyOnDispatchEvent = jest.spyOn(HasMany.element, 'dispatchEvent')
const $list = HasMany.element.querySelector(HasMany.options.list)
const spyOnListAppendChild = jest.spyOn($list, 'appendChild')
const spyOnBindEvents = jest.spyOn(HasMany, '_bindEvents')
const spyOnRecomputePosition = jest.spyOn(HasMany, '_recomputePosition')
const beforeAdd = HasMany.events.addBefore
const parent = HasMany.element

const output = HasMany._bindAddEventCallBack(mockEvent)

expect(spyOnDispatchEvent).toHaveBeenCalledTimes(2)
expect(spyOnDispatchEvent.mock.calls).toMatchObject([
[beforeAdd, [parent]],
[HasMany.events.addAfter]
])

expect(spyOnListAppendChild).toHaveBeenCalledTimes(1)
expect(spyOnListAppendChild).toHaveBeenCalledWith(datasetHTML)
expect(spyOnBindEvents).toHaveBeenCalledTimes(1)
expect(spyOnBindEvents).toHaveBeenCalledWith(datasetHTML)
expect(spyOnRecomputePosition).toHaveBeenCalledTimes(1)
expect(output).toBeTruthy()
})
})
79 changes: 41 additions & 38 deletions app/javascript/adminterface/lib/has_many.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,54 +51,57 @@ class HasMany {
}

_bindAddEvent (el) {
el.addEventListener('click', (e) => {
let beforeAdd
const el = e.target
const parent = this.element
el.addEventListener('click', (e) => this._bindAddEventCallBack(e))
}

e.preventDefault()
parent.dispatchEvent(beforeAdd = this.events.addBefore, [parent])
_bindAddEventCallBack (e) {
let beforeAdd
const el = e.target
const parent = this.element
e.preventDefault()
parent.dispatchEvent(beforeAdd = this.events.addBefore, [parent])

if (!beforeAdd.defaultPrevented) {
let index = parent.dataset.hasManyIndex || parent.querySelectorAll(this.options.item).length - 1
parent.setAttribute('data-has-many-index', ++index)
if (!beforeAdd.defaultPrevented) {
let index = parent.dataset.hasManyIndex || parent.querySelectorAll(this.options.item).length - 1
parent.setAttribute('data-has-many-index', ++index)

const regex = new RegExp(el.dataset.placeholder, 'g')
const html = el.dataset.html.replaceAll(regex, index)
const newNode = document.createElement('div')
newNode.innerHTML = html
const regex = new RegExp(el.dataset.placeholder, 'g')
const html = el.dataset.html.replace(regex, index)
const newNode = document.createElement('div')
newNode.innerHTML = html

const fieldset = newNode.firstElementChild
const $list = this.element.querySelector(this.options.list)
const fieldset = newNode.firstElementChild
const $list = this.element.querySelector(this.options.list)

$list.appendChild(fieldset)
this._bindEvents(fieldset)
this._recomputePosition()
$list.appendChild(fieldset)
this._bindEvents(fieldset)
this._recomputePosition()

const addAfterEvent = this.events.addAfter
addAfterEvent.detail = { fieldset, parent }
return parent.dispatchEvent(this.events.addAfter)
}
})
const addAfterEvent = this.events.addAfter
addAfterEvent.detail = { fieldset, parent }
return parent.dispatchEvent(this.events.addAfter)
}
}

_bindRemoveEvent (el) {
el.addEventListener('click', (e) => {
const el = e.target
const parent = this.element
const fieldset = el.closest(this.options.item)
const removeBeforeEvent = this.events.removeBefore
const removeAfterEvent = this.events.removeAfter

e.preventDefault()
this._recomputePosition()
el.addEventListener('click', (e) => this._bindRemoveEventCallBack(e))
}

removeBeforeEvent.detail = { fieldset, parent }
removeAfterEvent.detail = { fieldset, parent }
parent.dispatchEvent(removeBeforeEvent)
fieldset.remove()
return parent.dispatchEvent(removeAfterEvent)
})
_bindRemoveEventCallBack (e) {
const el = e.target
const parent = this.element
const fieldset = el.closest(this.options.item)
const removeBeforeEvent = this.events.removeBefore
const removeAfterEvent = this.events.removeAfter

e.preventDefault()
this._recomputePosition()

removeBeforeEvent.detail = { fieldset, parent }
removeAfterEvent.detail = { fieldset, parent }
parent.dispatchEvent(removeBeforeEvent)
fieldset.remove()
return parent.dispatchEvent(removeAfterEvent)
}

_bindEvents (el) {
Expand Down

0 comments on commit 9bfc4c8

Please sign in to comment.