Testing Strategy - smartlabsAT/directus-expandable-blocks GitHub Wiki
Testing Strategy
Comprehensive testing guide for the Expandable Blocks extension.
๐งช Testing Overview
Test Structure
test/
โโโ unit/ # Unit tests
โ โโโ composables/ # Composable tests
โ โโโ utils/ # Utility tests
โ โโโ components/ # Component tests
โ โโโ api/ # API service tests
โโโ mocks/ # Mocked dependencies
โโโ setup.ts # Test configuration
Note: E2E tests have been removed from the project. Focus is on comprehensive unit testing.
๐ฆ Unit Testing
Setup
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./test/setup.ts']
}
});
Testing Composables
// useExpandableBlocks.test.ts
import { describe, it, expect, vi } from 'vitest';
import { useExpandableBlocks } from '@/composables/useExpandableBlocks';
describe('useExpandableBlocks', () => {
it('should initialize with empty blocks', () => {
const props = { value: null };
const emit = vi.fn();
const { items } = useExpandableBlocks(props, emit);
expect(items.value).toEqual([]);
});
it('should detect dirty state on content change', () => {
const props = { value: [/* test data */] };
const emit = vi.fn();
const { isDirty, updateBlock } = useExpandableBlocks(props, emit);
expect(isDirty.value).toBe(false);
updateBlock('1', { title: 'New Title' });
expect(isDirty.value).toBe(true);
expect(emit).toHaveBeenCalled();
});
});
Testing Utilities
// m2a-helper.test.ts
describe('M2A Helper', () => {
it('should extract M2A structure correctly', () => {
const relationInfo = {
meta: {
one_collection_field: 'collection',
one_deselect_action: 'nullify'
}
};
const result = extractM2AStructure('pages', 'blocks', relationInfo);
expect(result).toEqual({
junctionCollection: 'pages_blocks',
junctionField: 'pages_id',
collectionField: 'collection',
itemField: 'blocks_id'
});
});
it('should handle missing relations gracefully', () => {
const result = extractM2AStructure('pages', 'blocks', null);
expect(result).toBeNull();
});
});
Component Testing
// NestedBlocks.test.ts
import { mount } from '@vue/test-utils';
import NestedBlocks from '@/components/NestedBlocks.vue';
describe('NestedBlocks', () => {
it('renders blocks correctly', async () => {
const wrapper = mount(NestedBlocks, {
props: {
items: [
{ id: 1, collection: 'text', item: { title: 'Test' } }
]
}
});
expect(wrapper.find('.nested-block').exists()).toBe(true);
expect(wrapper.text()).toContain('Test');
});
it('handles expansion correctly', async () => {
const wrapper = mount(NestedBlocks, {
props: {
items: [/* test data */],
startExpanded: false
}
});
expect(wrapper.find('.block-content').exists()).toBe(false);
await wrapper.find('.expand-button').trigger('click');
expect(wrapper.find('.block-content').exists()).toBe(true);
});
});
๐งช New Component Tests
Testing Item Selector
// ItemSelectorDrawer.test.ts
import { mount } from '@vue/test-utils';
import ItemSelectorDrawer from '@/components/ItemSelectorDrawer.vue';
describe('ItemSelectorDrawer', () => {
it('should search items correctly', async () => {
const wrapper = mount(ItemSelectorDrawer, {
props: {
modelValue: true,
collection: 'content_text',
allowedCollections: ['content_text', 'content_image']
}
});
await wrapper.find('input[type="search"]').setValue('test');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('search')).toBeTruthy();
});
it('should toggle between views', async () => {
const wrapper = mount(ItemSelectorDrawer, {
props: { /* ... */ }
});
await wrapper.find('[data-test="view-table"]').trigger('click');
expect(wrapper.vm.viewMode).toBe('table');
expect(localStorage.getItem('itemSelectorSettings')).toContain('table');
});
});
Testing Search Functionality
// SearchTagInput.test.ts
describe('SearchTagInput', () => {
it('should parse field-specific searches', () => {
const wrapper = mount(SearchTagInput, {
props: {
modelValue: 'title:"Product Launch" status:published'
}
});
expect(wrapper.vm.filters).toEqual([
{ field: 'title', value: 'Product Launch', operator: 'equals' },
{ field: 'status', value: 'published', operator: 'equals' }
]);
});
it('should emit filter changes', async () => {
const wrapper = mount(SearchTagInput);
await wrapper.vm.addFilter('description', 'test');
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['description:"test"']);
});
});
Testing API Services
// ItemLoader.test.ts
import { ItemLoader } from '@/api/services/ItemLoader';
describe('ItemLoader', () => {
it('should load items with relations', async () => {
const mockDatabase = createMockDatabase();
const mockSchema = createMockSchema();
const loader = new ItemLoader(mockDatabase, mockSchema);
const result = await loader.loadItems('content_text', {
limit: 10,
search: 'test'
});
expect(result.items).toHaveLength(10);
expect(result.items[0]).toHaveProperty('title');
});
it('should respect permissions', async () => {
const loader = new ItemLoader(mockDatabase, mockSchema);
loader.setAccountability({ role: 'editor' });
const result = await loader.loadItems('restricted_content');
expect(result.items).toHaveLength(0);
});
});
๐ฏ Key Testing Scenarios
1. State Management
describe('State Management', () => {
test('dirty state detection', () => {
// Test content changes
// Test position changes
// Test combined changes
});
test('save and reset', () => {
// Test save detection
// Test state reset after save
// Test original state updates
});
});
2. Data Integrity
describe('Data Integrity', () => {
test('maintains junction IDs', () => {
// Verify IDs preserved
// Check foreign keys
});
test('handles missing data', () => {
// Test null items
// Test invalid collections
});
});
3. Performance
describe('Performance', () => {
test('handles large datasets', () => {
const largeDataset = generateBlocks(1000);
// Test render time
// Test memory usage
});
test('efficient updates', () => {
// Test selective emitting
// Test minimal re-renders
});
});
๐ Debugging Tests
Using Vitest UI
# Run tests with UI interface
npm run test:ui
# Debug specific test
npm run test -- ItemSelectorDrawer.test.ts
Console Logging in Tests
// Use the logger wrapper in tests
import { logDebug } from '@/utils/logger-wrapper';
describe('Component', () => {
it('should work', () => {
logDebug('Test data', { items });
// Test logic
});
});
Mock Debugging
// Inspect mock calls
const mockFn = vi.fn();
// After test execution
console.log('Mock calls:', mockFn.mock.calls);
console.log('Mock results:', mockFn.mock.results);
๐ Coverage Goals
Target Coverage
- Statements: > 80%
- Branches: > 75%
- Functions: > 80%
- Lines: > 80%
Critical Paths
Must have 100% coverage:
- Dirty state detection
- Save/discard logic
- Permission checks
- Data transformation
Run Coverage
npm run test:coverage
# Generate HTML report
npm run test:coverage -- --reporter=html
๐ Best Practices
1. Test Organization
// Group related tests
describe('Feature: Drag and Drop', () => {
describe('when sorting enabled', () => {
// Tests
});
describe('when sorting disabled', () => {
// Tests
});
});
2. Test Data
// Use factories
function createBlock(overrides = {}) {
return {
id: 1,
collection: 'content_text',
item: { title: 'Test' },
sort: 0,
...overrides
};
}
3. Async Testing
// Always await async operations
test('async operations', async () => {
const result = await loadBlockData('123');
expect(result).toBeDefined();
});
4. Cleanup
// Clean up after tests
afterEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
Related: [Development]] ](/smartlabsAT/directus-expandable-blocks/wiki/[[Contributing)