Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions frontend/src/components/form/api/__tests__/ApiMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
function mockPromiseResolving (value) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
clearTimeout(timer)
resolve(value)
}, 100)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warum reichen 100 Millisekunden? Was garantiert uns dass wir das nicht beim nächsten Test auf 200 Millisekunden hochschrauben müssen? Könnte man auch 0 Millisekunden nehmen? In JavaScript ist ein Timeout von 0 Millisekunden ausreichend, um auf die nächste Runde des Event Loops zu warten. Vielleicht brauchts auch gar keinen Timeout?

return new Promise((resolve) => {
  resolve(value)
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ist von ApiWrapper kopiert

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ja, habe ich inzwischen auch gesehen. Das sollte dann wenn möglich auch dort abgelöst werden.

})
}

class MockStubbing {
constructor (fieldName, value) {
this._fieldName = fieldName
this._value = value
}

forFieldName (fieldName) {
this._fieldName = fieldName
return this
}

get fieldName () {
return this._fieldName
}

get value () {
return this._value
}
}

class ApiMockState {
constructor () {
this._get = jest.fn()
this._patch = jest.fn()
}

getMocks () {
return {
get: this._get,
patch: this._patch
}
}

get () {
const apiMock = this
return {
thenReturn (mockStubbing) {
if (!(mockStubbing instanceof MockStubbing)) {
throw new Error('apiMock must be instance of MockStubbing')
}
if (mockStubbing.fieldName === undefined || mockStubbing.value === undefined) {
throw new Error('fieldName and value must be defined')
}
apiMock._get.mockReturnValue({
[mockStubbing.fieldName]: mockStubbing.value,
_meta: {
load: Promise.resolve(mockStubbing.value)
}
})
}
}
}

patch () {
const apiMock = this
return {
thenReturn (mockStubbing) {
if (!(mockStubbing instanceof MockStubbing)) {
throw new Error('apiMock must be instance of MockStubbing')
}
if (mockStubbing.fieldName !== undefined || mockStubbing.value === undefined) {
throw new Error('fieldName must be undefined and value must be defined')
}
apiMock._patch.mockReturnValue(mockPromiseResolving(mockStubbing.value))
}
}
}
}

export class ApiMock {
static create () {
return new ApiMockState()
}

static success (value) {
return new MockStubbing(undefined, value)
}
}
Comment on lines +1 to +87
Copy link
Member

@carlobeltrame carlobeltrame Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that's a lot of code for adding just a single test. This looks like something we could integrate directly in hal-json-vuex, would you agree? I am worried that this will be hard to maintain if it's sitting here.

Is there a special reason why this is necessary here and not in the other ApiFormComponent tests?

Copy link
Contributor Author

@BacLuc BacLuc Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started here and thought i don't want this massive object in the test just to mock the api.
For the next tests, my live will be easier.

It would be nice if hal-json-vuex would offer a way to mock itself in tests.
I would wait a little to look if it really helps to write shorter tests with less boilerplate, and then we can migrate it to hal-json-vuex.
I also did not check if my mocks represent the behaviour of hal-json-vuex correctly, it worked for that test.

With JUnit you normally just write the hierarchy in the test like:
const api = jest.fn()
const get = jest.fn()
api.mockReturnValue('get', get)
get.mockReturnValue

Maybe there is a reason to not write such a mocking helper, but to repeat yourself?

109 changes: 86 additions & 23 deletions frontend/src/components/form/api/__tests__/ApiSelect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,109 @@ import Vuetify from 'vuetify'

import formBaseComponents from '@/plugins/formBaseComponents'

import { mount } from '@vue/test-utils'
import { mount as mountComponent } from '@vue/test-utils'
import ApiSelect from '../ApiSelect.vue'
import flushPromises from 'flush-promises'
import ApiWrapper from '@/components/form/api/ApiWrapper'
import { i18n } from '@/plugins'
import merge from 'lodash/merge'
import { ApiMock } from '@/components/form/api/__tests__/ApiMock'

Vue.use(Vuetify)
Vue.use(formBaseComponents)

describe('ApiTextField.vue', () => {
describe('An ApiSelect', () => {
let vuetify
let wrapper
let apiMock

const fieldName = 'test-field/123'

const FIRST_OPTION = {
value: 1,
text: 'firstOption'
}
const SECOND_OPTION = {
value: '2',
text: 'secondOption'
}

const selectValues = [
FIRST_OPTION,
SECOND_OPTION
]

beforeEach(() => {
vuetify = new Vuetify()
apiMock = ApiMock.create()
})

// keep this the first test --> otherwise element IDs change constantly
test('renders correctly', () => {
const props = {
value: 'Test Value',
afterEach(() => {
jest.restoreAllMocks()
wrapper.destroy()
})

const mount = (options) => {
const app = Vue.component('App', {
components: { ApiSelect },
props: {
fieldName: { type: String, default: fieldName },
selectValues: { type: Array, default: () => selectValues }
},
template: `
<div data-app>
<api-select
:auto-save="false"
:fieldname="fieldName"
uri="test-field/123"
label="Test field"
required="true"
:items="selectValues"
/>
</div>
`
})
apiMock.get().thenReturn(ApiMock.success(FIRST_OPTION.value).forFieldName(fieldName))
const defaultOptions = {
mocks: {
$tc: () => {
},
api: apiMock.getMocks()
}
}
return mountComponent(app, { vuetify, i18n, attachTo: document.body, ...merge(defaultOptions, options) })
}

const waitForDebounce = () => new Promise((resolve) => setTimeout(resolve, 110))

test('renders correctly', async () => {
apiMock.get().thenReturn(ApiMock.success(FIRST_OPTION.value).forFieldName(fieldName))
wrapper = mount()
await waitForDebounce()
await flushPromises()

/* field name and URI for saving back to API */
fieldname: 'test-field',
uri: 'test-field/123',
expect(wrapper).toMatchSnapshot('closed')

/* display label */
label: 'Test Field',
await wrapper.find('.v-input__slot').trigger('click')
await waitForDebounce()
await flushPromises()
expect(wrapper).toMatchSnapshot('open')
})

/* overrideDirty=true will reset the input if 'value' changes, even if the input is dirty. Use with caution. */
overrideDirty: false,
test('triggers api.patch and status update if input changes', async () => {
apiMock.patch().thenReturn(ApiMock.success(SECOND_OPTION.value))
wrapper = mount()

/* enable/disable auto save */
autoSave: true,
await flushPromises()

/* Validation criteria */
required: true
}
await wrapper.find('.v-input__slot').trigger('click')
await wrapper.findAll('[role="option"]').at(1).trigger('click')
await wrapper.find('input').trigger('submit')

const wrapper = mount(ApiSelect, {
vuetify,
propsData: props
})
await waitForDebounce()
await flushPromises()

expect(wrapper.element).toMatchSnapshot()
expect(apiMock.getMocks().patch).toBeCalledTimes(1)
expect(wrapper.findComponent(ApiWrapper).vm.localValue).toBe(SECOND_OPTION.value)
})
})
Loading