This guide covers how to test applications that use the Asterisms JS SDK, including unit testing, integration testing, and end-to-end testing.
Testing SDK-integrated applications involves several considerations:
// tests/setup.ts
import { vi } from 'vitest';
// Mock the SDK
vi.mock('@asterisms/sdk', () => ({
createAsterismsSDK: vi.fn(() => ({
auth: {
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: vi.fn(),
getCurrentUser: vi.fn(),
onAuthStateChanged: vi.fn(),
registerProvider: vi.fn()
},
storage: {
upload: vi.fn(),
uploadWithProgress: vi.fn(),
list: vi.fn(),
delete: vi.fn()
},
drive: {
createFolder: vi.fn(),
listFolder: vi.fn(),
share: vi.fn(),
move: vi.fn()
},
notification: {
getNotifications: vi.fn(),
onNotificationReceived: vi.fn(),
markAsRead: vi.fn()
}
}))
}));// tests/auth.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createAsterismsSDK } from '@asterisms/sdk';
import { AuthService } from '../src/services/AuthService';
// Mock the SDK
const mockSDK = {
auth: {
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: vi.fn(),
getCurrentUser: vi.fn(),
onAuthStateChanged: vi.fn()
}
};
vi.mocked(createAsterismsSDK).mockReturnValue(mockSDK as any);
describe('AuthService', () => {
let authService: AuthService;
beforeEach(() => {
vi.clearAllMocks();
authService = new AuthService();
});
it('should login successfully', async () => {
const credentials = { username: 'test@example.com', password: 'password' };
const expectedUser = { id: '1', email: 'test@example.com' };
mockSDK.auth.login.mockResolvedValue({ user: expectedUser });
const result = await authService.login(credentials);
expect(mockSDK.auth.login).toHaveBeenCalledWith({
provider: 'srp6',
credentials
});
expect(result.user).toEqual(expectedUser);
});
it('should handle login errors', async () => {
const credentials = { username: 'test@example.com', password: 'wrong' };
const error = new Error('Invalid credentials');
mockSDK.auth.login.mockRejectedValue(error);
await expect(authService.login(credentials)).rejects.toThrow('Invalid credentials');
});
it('should check authentication status', () => {
mockSDK.auth.isAuthenticated.mockReturnValue(true);
const isAuthenticated = authService.isAuthenticated();
expect(mockSDK.auth.isAuthenticated).toHaveBeenCalled();
expect(isAuthenticated).toBe(true);
});
it('should handle auth state changes', () => {
const callback = vi.fn();
const unsubscribe = vi.fn();
mockSDK.auth.onAuthStateChanged.mockReturnValue(unsubscribe);
const result = authService.onAuthStateChanged(callback);
expect(mockSDK.auth.onAuthStateChanged).toHaveBeenCalledWith(callback);
expect(result).toBe(unsubscribe);
});
});// tests/upload.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UploadService } from '../src/services/UploadService';
describe('UploadService', () => {
let uploadService: UploadService;
let mockSDK: any;
beforeEach(() => {
mockSDK = {
storage: {
upload: vi.fn(),
uploadWithProgress: vi.fn()
}
};
uploadService = new UploadService(mockSDK);
});
it('should upload file successfully', async () => {
const file = new File(['content'], 'test.txt', { type: 'text/plain' });
const expectedResult = { fileId: '123', url: 'https://example.com/file.txt' };
mockSDK.storage.upload.mockResolvedValue(expectedResult);
const result = await uploadService.uploadFile(file);
expect(mockSDK.storage.upload).toHaveBeenCalledWith(file, {
folder: '/uploads',
metadata: expect.any(Object)
});
expect(result).toEqual(expectedResult);
});
it('should handle upload progress', async () => {
const file = new File(['content'], 'test.txt');
const progressCallback = vi.fn();
const mockUpload = {
onProgress: vi.fn(),
promise: Promise.resolve({ fileId: '123' })
};
mockSDK.storage.uploadWithProgress.mockReturnValue(mockUpload);
await uploadService.uploadFileWithProgress(file, progressCallback);
expect(mockUpload.onProgress).toHaveBeenCalledWith(progressCallback);
});
it('should validate file size', () => {
const oversizedFile = new File(['x'.repeat(101 * 1024 * 1024)], 'large.txt');
expect(() => uploadService.validateFile(oversizedFile)).toThrow(
'File size exceeds maximum allowed size'
);
});
it('should validate file type', () => {
const executableFile = new File(['content'], 'virus.exe', {
type: 'application/x-executable'
});
expect(() => uploadService.validateFile(executableFile)).toThrow('File type not allowed');
});
});// tests/components/LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from '../src/components/LoginForm';
import { AuthProvider } from '../src/components/AuthProvider';
// Mock auth hook
const mockAuth = {
isAuthenticated: false,
user: null,
loading: false
};
vi.mock('../src/components/AuthProvider', () => ({
useAuth: () => mockAuth
}));
// Mock SDK
const mockLogin = vi.fn();
vi.mock('../src/lib/sdk', () => ({
sdk: {
auth: {
login: mockLogin
}
}
}));
describe('LoginForm', () => {
it('should render login form', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
it('should handle form submission', async () => {
mockLogin.mockResolvedValue({ user: { id: '1' } });
render(<LoginForm />);
fireEvent.change(screen.getByLabelText(/username/i), {
target: { value: 'test@example.com' }
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'password' }
});
fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
provider: 'srp6',
credentials: {
username: 'test@example.com',
password: 'password'
}
});
});
});
it('should display error message on login failure', async () => {
mockLogin.mockRejectedValue(new Error('Invalid credentials'));
render(<LoginForm />);
fireEvent.change(screen.getByLabelText(/username/i), {
target: { value: 'test@example.com' }
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'wrong' }
});
fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
});
});
it('should show loading state during login', async () => {
let resolveLogin: (value: any) => void;
const loginPromise = new Promise(resolve => {
resolveLogin = resolve;
});
mockLogin.mockReturnValue(loginPromise);
render(<LoginForm />);
fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText(/logging in/i)).toBeInTheDocument();
expect(screen.getByRole('button')).toBeDisabled();
resolveLogin!({ user: { id: '1' } });
});
});// tests/integration/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createAsterismsSDK } from '@asterisms/sdk';
import { SRP6AuthProvider } from '@asterisms/auth-provider-srp6';
describe('Authentication Integration', () => {
let sdk: any;
beforeAll(async () => {
// Use test environment configuration
sdk = createAsterismsSDK({
bundleId: 'com.test.app',
rootDomain: 'test.example.com',
navigationAdapter: {
navigate: () => {},
getCurrentPath: () => '/'
},
environment: 'test'
});
// Register test auth provider
sdk.auth.registerProvider(
'srp6',
new SRP6AuthProvider({
serviceUrl: 'https://auth.test.example.com'
})
);
});
afterAll(async () => {
// Clean up
if (sdk.auth.isAuthenticated()) {
await sdk.auth.logout();
}
});
it('should authenticate with test credentials', async () => {
const result = await sdk.auth.login({
provider: 'srp6',
credentials: {
username: 'test@example.com',
password: 'testpassword'
}
});
expect(result.user).toBeDefined();
expect(result.user.email).toBe('test@example.com');
expect(sdk.auth.isAuthenticated()).toBe(true);
});
it('should handle token refresh', async () => {
// Assuming we're authenticated from previous test
const initialToken = sdk.auth.getToken();
// Force token refresh
await sdk.auth.refreshToken();
const newToken = sdk.auth.getToken();
expect(newToken).not.toBe(initialToken);
expect(sdk.auth.isAuthenticated()).toBe(true);
});
it('should logout successfully', async () => {
await sdk.auth.logout();
expect(sdk.auth.isAuthenticated()).toBe(false);
expect(sdk.auth.getCurrentUser()).toBeNull();
});
});// tests/integration/storage.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestFile } from '../helpers/test-utils';
describe('Storage Integration', () => {
let sdk: any;
beforeAll(async () => {
// Initialize SDK and authenticate
sdk = createTestSDK();
await authenticateTestUser(sdk);
});
afterAll(async () => {
await sdk.auth.logout();
});
it('should upload and retrieve file', async () => {
const testFile = createTestFile('test.txt', 'Hello, World!');
// Upload file
const uploadResult = await sdk.storage.upload(testFile, {
folder: '/test-uploads'
});
expect(uploadResult.fileId).toBeDefined();
expect(uploadResult.url).toBeDefined();
// Verify file exists
const files = await sdk.storage.list({ folder: '/test-uploads' });
const uploadedFile = files.find((f) => f.id === uploadResult.fileId);
expect(uploadedFile).toBeDefined();
expect(uploadedFile.name).toBe('test.txt');
// Clean up
await sdk.storage.delete([uploadResult.fileId]);
});
it('should handle upload progress', async () => {
const testFile = createTestFile('large-test.txt', 'x'.repeat(1024 * 1024));
const progressEvents: any[] = [];
const upload = sdk.storage.uploadWithProgress(testFile);
upload.onProgress((progress: any) => {
progressEvents.push(progress);
});
const result = await upload.promise;
expect(progressEvents.length).toBeGreaterThan(0);
expect(progressEvents[progressEvents.length - 1].percentage).toBe(100);
expect(result.fileId).toBeDefined();
// Clean up
await sdk.storage.delete([result.fileId]);
});
});// tests/events.test.ts
import { describe, it, expect, vi } from 'vitest';
import { EventEmitter } from 'events';
// Create mock event emitter for testing
class MockSDKEventEmitter extends EventEmitter {
onAuthStateChanged(callback: Function) {
this.on('authStateChanged', callback);
return () => this.off('authStateChanged', callback);
}
onUploadProgress(callback: Function) {
this.on('uploadProgress', callback);
return () => this.off('uploadProgress', callback);
}
simulateAuthStateChange(state: any) {
this.emit('authStateChanged', state);
}
simulateUploadProgress(progress: any) {
this.emit('uploadProgress', progress);
}
}
describe('SDK Events', () => {
it('should handle auth state changes', () => {
const mockSDK = new MockSDKEventEmitter();
const callback = vi.fn();
const unsubscribe = mockSDK.onAuthStateChanged(callback);
// Simulate auth state change
mockSDK.simulateAuthStateChange({
isAuthenticated: true,
user: { id: '1', email: 'test@example.com' }
});
expect(callback).toHaveBeenCalledWith({
isAuthenticated: true,
user: { id: '1', email: 'test@example.com' }
});
// Test unsubscribe
unsubscribe();
mockSDK.simulateAuthStateChange({ isAuthenticated: false });
expect(callback).toHaveBeenCalledTimes(1);
});
it('should handle upload progress events', () => {
const mockSDK = new MockSDKEventEmitter();
const progressCallback = vi.fn();
mockSDK.onUploadProgress(progressCallback);
// Simulate progress events
mockSDK.simulateUploadProgress({ fileId: '123', percentage: 50 });
mockSDK.simulateUploadProgress({ fileId: '123', percentage: 100 });
expect(progressCallback).toHaveBeenCalledTimes(2);
expect(progressCallback).toHaveBeenCalledWith({ fileId: '123', percentage: 50 });
expect(progressCallback).toHaveBeenCalledWith({ fileId: '123', percentage: 100 });
});
});// tests/e2e/auth-flow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test('should login and logout successfully', async ({ page }) => {
await page.goto('/login');
// Fill login form
await page.fill('[data-testid="username"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'testpassword');
// Submit form
await page.click('[data-testid="login-button"]');
// Wait for redirect to dashboard
await page.waitForURL('/dashboard');
// Verify user is logged in
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
await expect(page.locator('[data-testid="user-name"]')).toContainText('test@example.com');
// Logout
await page.click('[data-testid="user-menu"]');
await page.click('[data-testid="logout-button"]');
// Verify redirect to login
await page.waitForURL('/login');
await expect(page.locator('[data-testid="login-form"]')).toBeVisible();
});
test('should handle login errors', async ({ page }) => {
await page.goto('/login');
// Fill with invalid credentials
await page.fill('[data-testid="username"]', 'invalid@example.com');
await page.fill('[data-testid="password"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
// Verify error message
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid credentials'
);
});
});// tests/e2e/file-upload.spec.ts
import { test, expect } from '@playwright/test';
import path from 'path';
test.describe('File Upload', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login');
await page.fill('[data-testid="username"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'testpassword');
await page.click('[data-testid="login-button"]');
await page.waitForURL('/dashboard');
});
test('should upload file successfully', async ({ page }) => {
await page.goto('/upload');
// Create test file
const testFilePath = path.join(__dirname, 'fixtures', 'test-file.txt');
// Upload file
await page.setInputFiles('[data-testid="file-input"]', testFilePath);
await page.click('[data-testid="upload-button"]');
// Wait for upload to complete
await expect(page.locator('[data-testid="upload-progress"]')).toContainText('100%');
await expect(page.locator('[data-testid="upload-status"]')).toContainText('Upload completed');
// Verify file appears in file list
await page.goto('/files');
await expect(page.locator('[data-testid="file-list"]')).toContainText('test-file.txt');
});
test('should show upload progress', async ({ page }) => {
await page.goto('/upload');
// Create larger test file for visible progress
const largeFilePath = path.join(__dirname, 'fixtures', 'large-file.zip');
await page.setInputFiles('[data-testid="file-input"]', largeFilePath);
await page.click('[data-testid="upload-button"]');
// Verify progress is shown
await expect(page.locator('[data-testid="upload-progress"]')).toBeVisible();
// Wait for progress to change
await page.waitForFunction(() => {
const progressElement = document.querySelector('[data-testid="upload-progress"]');
return progressElement && parseInt(progressElement.textContent || '0') > 0;
});
// Wait for completion
await expect(page.locator('[data-testid="upload-progress"]')).toContainText('100%');
});
});// tests/helpers/sdk-factory.ts
import { createAsterismsSDK } from '@asterisms/sdk';
import { SRP6AuthProvider } from '@asterisms/auth-provider-srp6';
export function createTestSDK(overrides = {}) {
return createAsterismsSDK({
bundleId: 'com.test.app',
rootDomain: 'test.example.com',
navigationAdapter: {
navigate: vi.fn(),
getCurrentPath: () => '/'
},
environment: 'test',
...overrides
});
}
export async function authenticateTestUser(sdk: any) {
const authProvider = new SRP6AuthProvider({
serviceUrl: 'https://auth.test.example.com'
});
sdk.auth.registerProvider('srp6', authProvider);
return await sdk.auth.login({
provider: 'srp6',
credentials: {
username: 'test@example.com',
password: 'testpassword'
}
});
}// tests/helpers/file-utils.ts
export function createTestFile(name: string, content: string, type = 'text/plain'): File {
const blob = new Blob([content], { type });
return new File([blob], name, { type });
}
export function createTestFiles(count: number): File[] {
return Array.from({ length: count }, (_, i) =>
createTestFile(`test-file-${i}.txt`, `Content ${i}`)
);
}
export function createLargeTestFile(sizeInMB: number): File {
const content = 'x'.repeat(sizeInMB * 1024 * 1024);
return createTestFile('large-file.txt', content);
}// tests/helpers/event-utils.ts
export function createEventTester() {
const events: any[] = [];
const subscriptions: Function[] = [];
const on = (eventName: string, callback: Function) => {
subscriptions.push(callback);
return () => {
const index = subscriptions.indexOf(callback);
if (index > -1) {
subscriptions.splice(index, 1);
}
};
};
const emit = (eventName: string, data: any) => {
events.push({ eventName, data, timestamp: new Date() });
subscriptions.forEach((callback) => callback(data));
};
const getEvents = () => events;
const clearEvents = () => (events.length = 0);
return { on, emit, getEvents, clearEvents };
}// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
globals: true,
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'tests/', 'dist/', '*.config.*']
}
},
resolve: {
alias: {
'@': './src'
}
}
});{
"jest": {
"testEnvironment": "jsdom",
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
"moduleNameMapping": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"coverageDirectory": "coverage",
"collectCoverageFrom": ["src/**/*.{ts,tsx}", "!src/**/*.d.ts", "!src/**/*.test.{ts,tsx}"]
}
}