Frontend Development#
Developer or User?
This section is part of the instrutions for programmers interested in REEV. If you want to use REEV, the best place is to start at Quickstart.
This section describes the best practices to use for frontend development.
Import Order#
Import order should be (and is also enforced by prettier):
external packages
project includes with
@/
prefixrelative includes with
./
prefix
Overall, restrict relative include order for tests to include code to be tested with ../
.
Types#
For the Vue reactive stores, explicitely specify the type in the creating function, e.g., ref<string> or ref<string | undefined>.
In many places in the UI, using undefined is more appropriate than null as this is the value before the data is loaded from the backend.
Test Structure#
Consider the following structure, an example is given below.
imports
- define fixture data
put larger fixture data into
.json
files within the__tests__
folders (will go to LFS by our.gitattributes
configuration)
define the tests
- use assemble, act, assert structure e.g., as described here
guard assertions are OK
use
describe.concurrent
to describe the tests, usually one block per.spec.ts
file- use
it
to define the tests use
async () => { ... }
only when necessary, e.g., forawait nextTick()
- use
use the
setupMountedComponents()
helper from@/components/__tests__/utils
to mount components, setup store, and setup router with mocks
import { setupMountedComponents } from '@bihealth/reev-frontend-lib/lib/testUtils'
import { describe, expect, it } from 'vitest'
import { h, nextTick } from 'vue'
import { type UserData, useUserStore } from '@/stores/user'
import UserProfileButton from './UserProfileButton.vue'
/** Example User data */
const adminUser: UserData = {
id: '2c0a153e-5e8c-11ee-8c99-0242ac120002',
email: 'admin@example.com',
is_active: true,
is_superuser: true,
is_verified: true,
oauth_accounts: [
{
id: '2c0a153e-5e8c-11ee-8c99-0242ac120002',
oauth_name: 'google',
account_id: '1234567890',
account_email: 'admin@example.com'
}
]
}
/** Dummy routes for testing. */
const dummyRoutes = [
{
path: '/',
name: 'home',
component: h('div', { innerHTML: 'for testing' })
},
{
path: '/login',
name: 'login',
component: h('div', { innerHTML: 'for testing' })
},
{
path: '/profile',
name: 'profile',
component: h('div', { innerHTML: 'for testing' })
}
]
describe.concurrent('UserProfileButton', () => {
it('displays Login button without any user', async () => {
// arrange:
const { wrapper } = await setupMountedComponents(
{ component: UserProfileButton },
{
initialStoreState: {
user: {
currentUser: null
}
},
routes: dummyRoutes
}
)
// act: nothing, only test rendering
// assert:
const loginButton = wrapper.findComponent('#login')
expect(loginButton.exists()).toBe(true)
const logoutButton = wrapper.findComponent('#profile')
expect(logoutButton.exists()).toBe(false)
})
it('displays Profile button with a user', async () => {
// arrange:
const { wrapper } = await setupMountedComponents(
{ component: UserProfileButton },
{
initialStoreState: {
user: {
currentUser: adminUser
}
},
routes: dummyRoutes
}
)
// act: nothing, only test rendering
// assert:
const loginButton = wrapper.findComponent('#login')
expect(loginButton.exists()).toBe(false)
const logoutButton = wrapper.findComponent('#profile')
expect(logoutButton.exists()).toBe(true)
})
it('switches from Login to Profile button when store changes', async () => {
// arrange:
// Note that we use an `async` test here as we need `await nextTick()` for the DOM
// update to bubble through when updating the state property.
const { wrapper } = await setupMountedComponents(
{ component: UserProfileButton },
{
initialStoreState: {
user: {
currentUser: null
}
},
routes: dummyRoutes
}
)
// act:
let loginButton = wrapper.findComponent('#login')
expect(loginButton.exists()).toBe(true)
let logoutButton = wrapper.findComponent('#profile')
expect(logoutButton.exists()).toBe(false)
const userStore = useUserStore()
userStore.currentUser = adminUser
await nextTick()
// assert:
loginButton = wrapper.findComponent('#login')
expect(loginButton.exists()).toBe(false)
logoutButton = wrapper.findComponent('#profile')
expect(logoutButton.exists()).toBe(true)
})
})
Note that there is a separation between views and components (cf. https://stackoverflow.com/a/50866150/84349) that is also reflected in the tests.
The main purpose of views is to handle the routing.
The actual work happens in components.
In tests, we thus use stubbed out of nested components in views but not components. This keeps the load time acceptable while testing the components in isolation.