Skip to content

Testing Guide

Guidelines for writing unit and end-to-end tests in IONFLOW Frontend.

TypeToolLocationCoverage
Unit TestsVitest*.spec.tsComponents, composables, services
E2E TestsPlaywrighte2e/User flows
Terminal window
# Run all unit tests
pnpm test:unit
# Run in watch mode
pnpm test:unit --watch
# Run with coverage
pnpm test:unit --coverage
# Run specific file
pnpm test:unit src/components/common/TruncateText.spec.ts
# Run tests matching pattern
pnpm test:unit --grep "TruncateText"

Test files should be next to the component:

src/components/common/
├── TruncateText.vue
└── TruncateText.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import ComponentName from './ComponentName.vue';
describe('ComponentName', () => {
beforeEach(() => {
// Setup before each test
});
it('should render correctly', () => {
const wrapper = mount(ComponentName);
expect(wrapper.exists()).toBe(true);
});
it('should display props', () => {
const wrapper = mount(ComponentName, {
props: {
title: 'Test Title',
},
});
expect(wrapper.text()).toContain('Test Title');
});
});
it('should accept and display props', () => {
const wrapper = mount(TruncateText, {
props: {
text: 'Hello World',
maxLength: 5,
},
});
expect(wrapper.text()).toContain('Hello...');
});
it('should emit events', async () => {
const wrapper = mount(ComponentName);
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('select')).toBeTruthy();
expect(wrapper.emitted('select')?.[0]).toEqual([expectedPayload]);
});
it('should render slots', () => {
const wrapper = mount(ComponentName, {
slots: {
default: '<p>Slot content</p>',
header: '<h1>Header</h1>',
},
});
expect(wrapper.html()).toContain('Slot content');
expect(wrapper.html()).toContain('Header');
});
import { describe, it, expect } from 'vitest';
import { useDataTableParams } from './data.table.params';
describe('useDataTableParams', () => {
it('should initialize with default values', () => {
const { first, rows } = useDataTableParams();
expect(first.value).toBe(0);
expect(rows.value).toBe(10);
});
it('should update on page change', () => {
const { first, rows, onPage } = useDataTableParams();
onPage({ first: 20, rows: 10 });
expect(first.value).toBe(20);
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import axios from 'axios';
import flowService from './flow.service';
vi.mock('axios');
describe('FlowService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch flows', async () => {
const mockData = { data: [{ id: '1', name: 'Flow 1' }] };
vi.mocked(axios.get).mockResolvedValue({ data: mockData });
const result = await flowService.list();
expect(result.data).toEqual(mockData);
});
});
import { describe, it, expect, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useAuthStore } from './auth';
describe('useAuthStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('should initialize with null user', () => {
const store = useAuthStore();
expect(store.user).toBeNull();
});
it('should set user', () => {
const store = useAuthStore();
const user = { id: 1, name: 'Test User' };
store.setUser(user);
expect(store.user).toEqual(user);
});
});
vi.mock('@/services/tenant/flow.service', () => ({
default: {
list: vi.fn().mockResolvedValue({ data: [] }),
get: vi.fn(),
},
}));
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) => key,
}),
}));
import { mount } from '@vue/test-utils';
import { createRouter, createMemoryHistory } from 'vue-router';
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/', component: {} }],
});
const wrapper = mount(Component, {
global: {
plugins: [router],
},
});
Terminal window
# Run all E2E tests
pnpm test:e2e
# Run with UI
pnpm test:e2e --ui
# Run specific test
pnpm test:e2e e2e/login.spec.ts
# Run in headed mode
pnpm test:e2e --headed
e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('should display login form', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Password')).toBeVisible();
});
test('should login successfully', async ({ page }) => {
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL('/dashboard');
});
});
e2e/pages/login.page.ts
import type { Page } from '@playwright/test';
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
}
// Usage
test('should login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password');
});
Component TypeCoverage
Common componentsRequired
Feature componentsRecommended
ServicesRequired
StoresRequired
ComposablesRequired
Terminal window
# Generate coverage report
pnpm test:unit --coverage
# Open HTML report
open coverage/index.html
  • Test behavior, not implementation
  • Use meaningful test descriptions
  • Keep tests independent
  • Mock external dependencies
  • Test edge cases
  • Don’t test third-party libraries
  • Don’t test implementation details
  • Don’t write flaky tests
  • Don’t skip error scenarios
  • Don’t couple tests