How to Test dcupl Applications
Write reliable tests for applications that use the dcupl SDK, covering unit tests for queries, component tests with mocks, and integration tests.
Prerequisites
- Working dcupl application
- Test framework installed (Vitest or Jest)
- Basic understanding of lists and queries
Unit Testing Queries
Test query logic in isolation using real dcupl instances with inline test data.
Basic Query Test
product-queries.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';
describe('Product Queries', () => {
let dcupl: Dcupl;
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'category', type: 'string', filter: true },
{ key: 'inStock', type: 'boolean' },
],
data: [
{ key: 'p1', name: 'Laptop', price: 999, category: 'Electronics', inStock: true },
{ key: 'p2', name: 'Mouse', price: 29, category: 'Electronics', inStock: true },
{ key: 'p3', name: 'Desk', price: 299, category: 'Furniture', inStock: false },
{ key: 'p4', name: 'Chair', price: 199, category: 'Furniture', inStock: true },
],
};
beforeEach(async () => {
dcupl = new Dcupl();
dcupl.models.set(productModel);
await dcupl.init();
});
it('should return all products', () => {
const list = dcupl.lists.create({ modelKey: 'Product' });
const items = list.catalog.query.items();
expect(items).toHaveLength(4);
});
it('should filter by category', () => {
const list = dcupl.lists.create({ modelKey: 'Product' });
list.catalog.query.apply({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});
const items = list.catalog.query.items();
expect(items).toHaveLength(2);
expect(items.every((item) => item.category === 'Electronics')).toBe(true);
});
it('should filter by price range', () => {
const list = dcupl.lists.create({ modelKey: 'Product' });
list.catalog.query.apply({
groupKey: 'priceRange',
groupType: 'and',
queries: [
{ attribute: 'price', operator: 'gte', value: 100 },
{ attribute: 'price', operator: 'lte', value: 500 },
],
});
const items = list.catalog.query.items();
expect(items).toHaveLength(2);
expect(items.map((i) => i.name)).toContain('Desk');
expect(items.map((i) => i.name)).toContain('Chair');
});
});Testing Filter Combinations
filter-combinations.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
describe('Filter Combinations', () => {
let dcupl: Dcupl;
let list: ReturnType<typeof dcupl.lists.create>;
beforeEach(async () => {
dcupl = new Dcupl();
dcupl.models.set({
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'category', type: 'string', filter: true },
{ key: 'brand', type: 'string', filter: true },
{ key: 'price', type: 'float' },
],
data: [
{ key: 'p1', name: 'iPhone', category: 'Phones', brand: 'Apple', price: 999 },
{ key: 'p2', name: 'Galaxy', category: 'Phones', brand: 'Samsung', price: 899 },
{ key: 'p3', name: 'MacBook', category: 'Laptops', brand: 'Apple', price: 1299 },
{ key: 'p4', name: 'ThinkPad', category: 'Laptops', brand: 'Lenovo', price: 999 },
],
});
await dcupl.init();
list = dcupl.lists.create({ modelKey: 'Product' });
});
it('should combine category and brand filters (AND)', () => {
// Filter: Apple products in Phones category
list.catalog.query.apply({
groupKey: 'categoryFilter',
attribute: 'category',
operator: 'eq',
value: 'Phones',
});
list.catalog.query.apply({
groupKey: 'brandFilter',
attribute: 'brand',
operator: 'eq',
value: 'Apple',
});
const items = list.catalog.query.items();
expect(items).toHaveLength(1);
expect(items[0].name).toBe('iPhone');
});
it('should support OR within a filter group', () => {
// Filter: Apple OR Samsung products
list.catalog.query.apply({
groupKey: 'brandFilter',
groupType: 'or',
queries: [
{ attribute: 'brand', operator: 'eq', value: 'Apple' },
{ attribute: 'brand', operator: 'eq', value: 'Samsung' },
],
});
const items = list.catalog.query.items();
expect(items).toHaveLength(3);
});
it('should clear filters correctly', () => {
list.catalog.query.apply({
groupKey: 'categoryFilter',
attribute: 'category',
operator: 'eq',
value: 'Phones',
});
expect(list.catalog.query.items()).toHaveLength(2);
list.catalog.query.clear();
expect(list.catalog.query.items()).toHaveLength(4);
});
it('should remove specific filter group', () => {
list.catalog.query.apply({
groupKey: 'categoryFilter',
attribute: 'category',
operator: 'eq',
value: 'Phones',
});
list.catalog.query.apply({
groupKey: 'brandFilter',
attribute: 'brand',
operator: 'eq',
value: 'Apple',
});
expect(list.catalog.query.items()).toHaveLength(1);
// Remove only category filter
list.catalog.query.remove({ groupKey: 'categoryFilter' });
// Should now show all Apple products
const items = list.catalog.query.items();
expect(items).toHaveLength(2);
expect(items.every((i) => i.brand === 'Apple')).toBe(true);
});
});Testing Sorting and Pagination
sorting-pagination.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
describe('Sorting and Pagination', () => {
let dcupl: Dcupl;
let list: ReturnType<typeof dcupl.lists.create>;
beforeEach(async () => {
dcupl = new Dcupl();
dcupl.models.set({
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'rating', type: 'float' },
],
data: [
{ key: 'p1', name: 'Product A', price: 100, rating: 4.5 },
{ key: 'p2', name: 'Product B', price: 50, rating: 4.8 },
{ key: 'p3', name: 'Product C', price: 150, rating: 4.2 },
{ key: 'p4', name: 'Product D', price: 75, rating: 4.6 },
],
});
await dcupl.init();
list = dcupl.lists.create({ modelKey: 'Product' });
});
it('should sort by price ascending', () => {
const items = list.catalog.query.items({
sort: { attributes: ['price'], order: ['asc'] },
});
expect(items[0].price).toBe(50);
expect(items[3].price).toBe(150);
});
it('should sort by price descending', () => {
const items = list.catalog.query.items({
sort: { attributes: ['price'], order: ['desc'] },
});
expect(items[0].price).toBe(150);
expect(items[3].price).toBe(50);
});
it('should paginate results', () => {
const page1 = list.catalog.query.items({ start: 0, count: 2 });
const page2 = list.catalog.query.items({ start: 2, count: 2 });
expect(page1).toHaveLength(2);
expect(page2).toHaveLength(2);
expect(page1[0].key).not.toBe(page2[0].key);
});
it('should combine sorting and pagination', () => {
const items = list.catalog.query.items({
sort: { attributes: ['price'], order: ['asc'] },
start: 1,
count: 2,
});
expect(items).toHaveLength(2);
expect(items[0].price).toBe(75); // Second cheapest
expect(items[1].price).toBe(100); // Third cheapest
});
});Mocking dcupl for Component Tests
When testing UI components, mock dcupl to avoid initializing the full SDK.
Creating a Mock dcupl Instance
test-helpers/mock-dcupl.ts
import { vi } from 'vitest';
export interface MockDcuplList {
catalog: {
query: {
items: ReturnType<typeof vi.fn>;
count: ReturnType<typeof vi.fn>;
apply: ReturnType<typeof vi.fn>;
clear: ReturnType<typeof vi.fn>;
remove: ReturnType<typeof vi.fn>;
};
fn: {
facets: ReturnType<typeof vi.fn>;
aggregate: ReturnType<typeof vi.fn>;
metadata: ReturnType<typeof vi.fn>;
};
filters: {
get: ReturnType<typeof vi.fn>;
getAll: ReturnType<typeof vi.fn>;
};
};
destroy: ReturnType<typeof vi.fn>;
}
export interface MockDcupl {
models: {
set: ReturnType<typeof vi.fn>;
get: ReturnType<typeof vi.fn>;
};
data: {
set: ReturnType<typeof vi.fn>;
};
lists: {
create: ReturnType<typeof vi.fn>;
get: ReturnType<typeof vi.fn>;
getAll: ReturnType<typeof vi.fn>;
};
init: ReturnType<typeof vi.fn>;
destroy: ReturnType<typeof vi.fn>;
}
export function createMockList<T>(items: T[] = []): MockDcuplList {
return {
catalog: {
query: {
items: vi.fn().mockReturnValue(items),
count: vi.fn().mockReturnValue(items.length),
apply: vi.fn(),
clear: vi.fn(),
remove: vi.fn(),
},
fn: {
facets: vi.fn().mockReturnValue([]),
aggregate: vi.fn().mockReturnValue(0),
metadata: vi.fn().mockReturnValue({
key: 'TestModel',
currentSize: items.length,
initialSize: items.length,
appliedQuery: { queries: [] },
}),
},
filters: {
get: vi.fn().mockReturnValue(null),
getAll: vi.fn().mockReturnValue([]),
},
},
destroy: vi.fn(),
};
}
export function createMockDcupl(lists: Map<string, MockDcuplList> = new Map()): MockDcupl {
return {
models: {
set: vi.fn(),
get: vi.fn(),
},
data: {
set: vi.fn(),
},
lists: {
create: vi.fn().mockImplementation(({ modelKey }) => {
return lists.get(modelKey) || createMockList();
}),
get: vi.fn().mockImplementation((key) => lists.get(key)),
getAll: vi.fn().mockReturnValue(Array.from(lists.values())),
},
init: vi.fn().mockResolvedValue(undefined),
destroy: vi.fn(),
};
}Testing a React Component with Mock
ProductList.spec.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ProductList } from './ProductList';
import { createMockDcupl, createMockList } from '../test-helpers/mock-dcupl';
// Mock the dcupl module
vi.mock('@dcupl/core', () => ({
Dcupl: vi.fn(),
}));
describe('ProductList Component', () => {
const mockProducts = [
{ key: 'p1', name: 'Laptop', price: 999, category: 'Electronics' },
{ key: 'p2', name: 'Mouse', price: 29, category: 'Electronics' },
{ key: 'p3', name: 'Desk', price: 299, category: 'Furniture' },
];
let mockList: ReturnType<typeof createMockList>;
let mockDcupl: ReturnType<typeof createMockDcupl>;
beforeEach(() => {
mockList = createMockList(mockProducts);
mockDcupl = createMockDcupl(new Map([['Product', mockList]]));
});
it('should render product list', () => {
render(<ProductList dcupl={mockDcupl as MockDcupl} />);
expect(screen.getByText('Laptop')).toBeInTheDocument();
expect(screen.getByText('Mouse')).toBeInTheDocument();
expect(screen.getByText('Desk')).toBeInTheDocument();
});
it('should call query.apply when filtering', () => {
render(<ProductList dcupl={mockDcupl as MockDcupl} />);
const filterButton = screen.getByRole('button', { name: /electronics/i });
fireEvent.click(filterButton);
expect(mockList.catalog.query.apply).toHaveBeenCalledWith(
expect.objectContaining({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
})
);
});
it('should call query.clear when clearing filters', () => {
render(<ProductList dcupl={mockDcupl as MockDcupl} />);
const clearButton = screen.getByRole('button', { name: /clear/i });
fireEvent.click(clearButton);
expect(mockList.catalog.query.clear).toHaveBeenCalled();
});
});Testing an Angular Component with Mock
product-list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductListComponent } from './product-list.component';
import { DcuplService } from '../services/dcupl.service';
import { createMockDcupl, createMockList } from '../test-helpers/mock-dcupl';
describe('ProductListComponent', () => {
let component: ProductListComponent;
let fixture: ComponentFixture<ProductListComponent>;
let mockDcuplService: jasmine.SpyObj<DcuplService>;
const mockProducts = [
{ key: 'p1', name: 'Laptop', price: 999 },
{ key: 'p2', name: 'Mouse', price: 29 },
];
beforeEach(async () => {
const mockList = createMockList(mockProducts);
const mockDcupl = createMockDcupl(new Map([['Product', mockList]]));
mockDcuplService = jasmine.createSpyObj('DcuplService', ['getDcupl', 'isReady']);
mockDcuplService.getDcupl.and.returnValue(mockDcupl as MockDcupl);
mockDcuplService.isReady.and.returnValue(true);
await TestBed.configureTestingModule({
imports: [ProductListComponent],
providers: [{ provide: DcuplService, useValue: mockDcuplService }],
}).compileComponents();
fixture = TestBed.createComponent(ProductListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display products', () => {
const productElements = fixture.nativeElement.querySelectorAll('.product-item');
expect(productElements.length).toBe(2);
});
it('should filter products when category selected', () => {
const mockList = mockDcuplService.getDcupl().lists.create({ modelKey: 'Product' });
component.filterByCategory('Electronics');
expect(mockList.catalog.query.apply).toHaveBeenCalled();
});
});Integration Testing
Test complete workflows with real dcupl instances.
Full Workflow Test
product-catalog.integration.spec.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
import type { ModelDefinition } from '@dcupl/common';
describe('Product Catalog Integration', () => {
let dcupl: Dcupl;
const categoryModel: ModelDefinition = {
key: 'Category',
properties: [{ key: 'name', type: 'string' }],
data: [
{ key: 'cat1', name: 'Electronics' },
{ key: 'cat2', name: 'Furniture' },
{ key: 'cat3', name: 'Clothing' },
],
};
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'inStock', type: 'boolean' },
],
references: [{ key: 'category', type: 'singleValued', model: 'Category', filter: true }],
data: [
{ key: 'p1', name: 'Laptop', price: 999, category: 'cat1', inStock: true },
{ key: 'p2', name: 'Mouse', price: 29, category: 'cat1', inStock: true },
{ key: 'p3', name: 'Desk', price: 299, category: 'cat2', inStock: false },
{ key: 'p4', name: 'Chair', price: 199, category: 'cat2', inStock: true },
{ key: 'p5', name: 'T-Shirt', price: 25, category: 'cat3', inStock: true },
],
};
beforeEach(async () => {
dcupl = new Dcupl();
dcupl.models.set(categoryModel);
dcupl.models.set(productModel);
await dcupl.init();
});
afterEach(() => {
dcupl.destroy();
});
it('should complete a full filter workflow', () => {
const list = dcupl.lists.create({ modelKey: 'Product' });
// Step 1: Get initial facets
const categoryFacets = list.catalog.fn.facets({ attribute: 'category' });
expect(categoryFacets).toHaveLength(3);
// Step 2: Apply category filter
list.catalog.query.apply({
groupKey: 'categoryFilter',
attribute: 'category.key',
operator: 'eq',
value: 'cat1',
});
const filteredItems = list.catalog.query.items();
expect(filteredItems).toHaveLength(2);
// Step 3: Verify facets update
const updatedFacets = list.catalog.fn.facets({ attribute: 'category' });
const electronicsFacet = updatedFacets.find((f) => f.value?.key === 'cat1');
expect(electronicsFacet?.count).toBe(2);
// Step 4: Add price filter
list.catalog.query.apply({
groupKey: 'priceFilter',
attribute: 'price',
operator: 'lte',
value: 100,
});
const priceFilteredItems = list.catalog.query.items();
expect(priceFilteredItems).toHaveLength(1);
expect(priceFilteredItems[0].name).toBe('Mouse');
// Step 5: Clear all filters
list.catalog.query.clear();
expect(list.catalog.query.count()).toBe(5);
});
it('should handle data updates correctly', async () => {
const list = dcupl.lists.create({ modelKey: 'Product' });
// Initial count
expect(list.catalog.query.count()).toBe(5);
// Add new product
dcupl.data.set([{ key: 'p6', name: 'Monitor', price: 399, category: 'cat1', inStock: true }], {
model: 'Product',
mode: 'add',
});
await dcupl.update();
list.update();
expect(list.catalog.query.count()).toBe(6);
// Verify the new product is queryable
list.catalog.query.apply({
attribute: 'name',
operator: 'find',
value: 'Monitor',
});
const items = list.catalog.query.items();
expect(items).toHaveLength(1);
expect(items[0].price).toBe(399);
});
it('should create filtered lists', () => {
// Create a list pre-filtered to only electronics
const query = dcupl.query.generate('Product', {
attribute: 'category.key',
operator: 'eq',
value: 'cat1',
});
const electronicsList = dcupl.lists.create({ modelKey: 'Product', query });
const meta = electronicsList.catalog.fn.metadata();
expect(meta.initialSize).toBe(2);
expect(meta.currentSize).toBe(2);
// Additional filters work on the pre-filtered set
electronicsList.catalog.query.apply({
attribute: 'price',
operator: 'lt',
value: 100,
});
expect(electronicsList.catalog.query.count()).toBe(1);
});
});Testing with Aggregations
aggregations.integration.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
describe('Aggregations', () => {
let dcupl: Dcupl;
beforeEach(async () => {
dcupl = new Dcupl();
dcupl.models.set({
key: 'Order',
properties: [
{ key: 'product', type: 'string' },
{ key: 'quantity', type: 'int' },
{ key: 'price', type: 'float' },
{ key: 'status', type: 'string', filter: true },
],
data: [
{ key: 'o1', product: 'Laptop', quantity: 2, price: 999, status: 'completed' },
{ key: 'o2', product: 'Mouse', quantity: 5, price: 29, status: 'completed' },
{ key: 'o3', product: 'Keyboard', quantity: 3, price: 79, status: 'pending' },
{ key: 'o4', product: 'Monitor', quantity: 1, price: 399, status: 'completed' },
],
});
await dcupl.init();
});
it('should calculate sum', () => {
const list = dcupl.lists.create({ modelKey: 'Order' });
const totalQuantity = list.catalog.fn.aggregate({
attribute: 'quantity',
aggregation: 'sum',
});
expect(totalQuantity).toBe(11);
});
it('should calculate average', () => {
const list = dcupl.lists.create({ modelKey: 'Order' });
const avgPrice = list.catalog.fn.aggregate({
attribute: 'price',
aggregation: 'avg',
});
expect(avgPrice).toBe(376.5);
});
it('should calculate min and max', () => {
const list = dcupl.lists.create({ modelKey: 'Order' });
const minPrice = list.catalog.fn.aggregate({
attribute: 'price',
aggregation: 'min',
});
const maxPrice = list.catalog.fn.aggregate({
attribute: 'price',
aggregation: 'max',
});
expect(minPrice).toBe(29);
expect(maxPrice).toBe(999);
});
it('should aggregate filtered results', () => {
const list = dcupl.lists.create({ modelKey: 'Order' });
list.catalog.query.apply({
attribute: 'status',
operator: 'eq',
value: 'completed',
});
const completedTotal = list.catalog.fn.aggregate({
attribute: 'quantity',
aggregation: 'sum',
});
expect(completedTotal).toBe(8); // 2 + 5 + 1
});
});Test Data Strategies
Using Fixtures
test-fixtures/products.ts
import type { ModelDefinition } from '@dcupl/common';
export const PRODUCT_MODEL: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'category', type: 'string', filter: true },
{ key: 'brand', type: 'string', filter: true },
{ key: 'inStock', type: 'boolean' },
{ key: 'rating', type: 'float' },
],
};
export const SAMPLE_PRODUCTS = [
{
key: 'p1',
name: 'Laptop Pro',
price: 1299,
category: 'Electronics',
brand: 'TechCo',
inStock: true,
rating: 4.5,
},
{
key: 'p2',
name: 'Wireless Mouse',
price: 49,
category: 'Electronics',
brand: 'TechCo',
inStock: true,
rating: 4.2,
},
{
key: 'p3',
name: 'Office Desk',
price: 399,
category: 'Furniture',
brand: 'HomeStyle',
inStock: false,
rating: 4.0,
},
{
key: 'p4',
name: 'Ergonomic Chair',
price: 299,
category: 'Furniture',
brand: 'HomeStyle',
inStock: true,
rating: 4.8,
},
{
key: 'p5',
name: 'USB-C Hub',
price: 79,
category: 'Electronics',
brand: 'ConnectPro',
inStock: true,
rating: 4.3,
},
];
export function createProductModel(data = SAMPLE_PRODUCTS): ModelDefinition {
return {
...PRODUCT_MODEL,
data,
};
}Factory Functions for Test Data
test-helpers/factories.ts
let idCounter = 0;
export function createProduct(overrides: Partial<Product> = {}): Product {
idCounter++;
return {
key: `product-${idCounter}`,
name: `Test Product ${idCounter}`,
price: 100 + idCounter * 10,
category: 'General',
brand: 'TestBrand',
inStock: true,
rating: 4.0,
...overrides,
};
}
export function createProducts(count: number, overrides: Partial<Product> = {}): Product[] {
return Array.from({ length: count }, () => createProduct(overrides));
}
export function createProductsInCategory(category: string, count: number): Product[] {
return createProducts(count, { category });
}
// Usage in tests
describe('Product Tests', () => {
it('should handle many products', async () => {
const dcupl = new Dcupl();
dcupl.models.set({
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'category', type: 'string', filter: true },
],
data: createProducts(1000),
});
await dcupl.init();
const list = dcupl.lists.create({ modelKey: 'Product' });
expect(list.catalog.query.count()).toBe(1000);
});
});Resetting State Between Tests
test-setup.ts
import { beforeEach, afterEach } from 'vitest';
import { Dcupl } from '@dcupl/core';
let dcuplInstance: Dcupl | null = null;
export function getTestDcupl(): Dcupl {
if (!dcuplInstance) {
throw new Error('Dcupl not initialized. Call setupTestDcupl first.');
}
return dcuplInstance;
}
export async function setupTestDcupl(models: ModelDefinition[]): Promise<Dcupl> {
dcuplInstance = new Dcupl();
models.forEach((model) => dcuplInstance!.models.set(model));
await dcuplInstance.init();
return dcuplInstance;
}
export function cleanupTestDcupl(): void {
if (dcuplInstance) {
dcuplInstance.destroy();
dcuplInstance = null;
}
}
// Use in test files
describe('My Tests', () => {
beforeEach(async () => {
await setupTestDcupl([productModel, categoryModel]);
});
afterEach(() => {
cleanupTestDcupl();
});
it('should work', () => {
const dcupl = getTestDcupl();
// ... test code
});
});Best Practices
Arrange-Act-Assert pattern:
it('should filter by category', () => {
// Arrange
const list = dcupl.lists.create({ modelKey: 'Product' });
// Act
list.catalog.query.apply({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});
const items = list.catalog.query.items();
// Assert
expect(items).toHaveLength(2);
expect(items.every((i) => i.category === 'Electronics')).toBe(true);
});Each test should be independent:
describe('Filter Tests', () => {
let dcupl: Dcupl;
let list: DcuplList;
beforeEach(async () => {
// Fresh instance for each test
dcupl = new Dcupl();
dcupl.models.set(productModel);
await dcupl.init();
list = dcupl.lists.create({ modelKey: 'Product' });
});
afterEach(() => {
// Clean up
list.destroy();
dcupl.destroy();
});
// Tests are now isolated
});Be specific in assertions:
// Bad - too vague
expect(items.length).toBeGreaterThan(0);
// Good - specific expectation
expect(items).toHaveLength(3);
// Good - verify actual content
expect(items.map((i) => i.key)).toEqual(['p1', 'p2', 'p3']);
// Good - verify properties
expect(items[0]).toMatchObject({
name: 'Laptop',
category: 'Electronics',
});Related
- Debugging with Console - Inspect queries at runtime
- TypeScript Guide - Type-safe testing patterns
- Angular Integration - Angular-specific testing
- React Integration - React-specific testing