Aula 1 - Módulo 6: Implementação de testes robustos para aplicações tRPC
Com a tipagem rigorosa do tRPC, os testes garantem que tanto o runtime quanto o compile-time funcionem perfeitamente. Cada procedure precisa ser testada em cenários reais e edge cases.
Testes permitem refatorar procedures, middleware e transformers com confiança, mantendo a integridade da API e dos contratos estabelecidos.
Desde validators Zod até WebSocket subscriptions, cada camada da aplicação tRPC deve ser testada para garantir robustez em produção.
O tRPC oferece ferramentas específicas para mock de procedures, permitindo testes isolados e determinísticos.
Type Safety Testing:
Testes que validam tanto tipos TypeScript quanto runtime behavior
Procedure Mocking:
Simulação de procedures para testes isolados e determinísticos
Context Testing:
Validação de middleware, autenticação e estado do contexto
Subscription Testing:
Testes específicos para WebSocket e real-time features
// 📁 tests/setup.ts
import { beforeEach, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import { db } from '../src/server/db';
// 🧹 Cleanup automático após cada teste
afterEach(() => {
cleanup();
});
// 🗄️ Reset do banco de dados
beforeEach(async () => {
await db.migrate.rollback();
await db.migrate.latest();
await db.seed.run();
});
// 🎭 Mock de variáveis de ambiente
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'sqlite::memory:';
process.env.JWT_SECRET = 'test-secret-key';
// 📊 Setup de métricas para testes
jest.mock('../src/monitoring/metrics', () => ({
trpcMetrics: {
requestsTotal: { inc: jest.fn() },
requestDuration: { observe: jest.fn() },
activeConnections: { set: jest.fn() },
},
}));
// 📁 tests/utils/trpc-test-utils.ts
import { createTRPCMsw } from 'msw-trpc';
import { setupServer } from 'msw/node';
import { type AppRouter } from '../../src/server/api/root';
import { createInnerTRPCContext } from '../../src/server/api/trpc';
// 🎭 MSW handler para tRPC
export const trpcMsw = createTRPCMsw<AppRouter>();
// 🖥️ Mock server setup
export const server = setupServer();
// 🔧 Utility para criar contexto de teste
export function createTestContext() {
return createInnerTRPCContext({
session: {
user: {
id: 'test-user-id',
email: 'test@example.com',
role: 'USER',
},
expires: '2024-12-31',
},
});
}
// 📞 Helper para chamar procedures em testes
export async function callProcedure<T>(
procedure: string,
input?: any,
context = createTestContext()
) {
const { createCaller } = await import('../../src/server/api/root');
const caller = createCaller(context);
// 🎯 Dynamic procedure call
const procedurePath = procedure.split('.');
let current: any = caller;
for (const path of procedurePath) {
current = current[path];
}
return await current(input);
}
// 🧪 Helper para testes de validação
export function expectZodError(fn: () => Promise<any>, path?: string) {
return expect(fn()).rejects.toThrow(
expect.objectContaining({
code: 'BAD_REQUEST',
cause: expect.objectContaining({
code: 'ZOD_ERROR',
...(path && {
fieldErrors: expect.objectContaining({
[path]: expect.any(Array),
}),
}),
}),
})
);
}
// 📁 vitest.config.ts
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'build/',
'dist/',
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
});
// 📁 tests/unit/procedures/user.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { callProcedure, createTestContext } from '../../utils/trpc-test-utils';
import { db } from '../../../src/server/db';
describe('User Procedures', () => {
beforeEach(async () => {
// 🗄️ Reset database para cada teste
await db.user.deleteMany();
});
describe('user.create', () => {
it('deve criar usuário com dados válidos', async () => {
// 📝 Arrange
const input = {
email: 'test@example.com',
name: 'Test User',
password: 'securepassword123',
};
// 🎬 Act
const result = await callProcedure('user.create', input);
// ✅ Assert
expect(result).toMatchObject({
id: expect.any(String),
email: 'test@example.com',
name: 'Test User',
createdAt: expect.any(Date),
});
expect(result.password).toBeUndefined(); // Não deve retornar senha
});
it('deve falhar com email inválido', async () => {
// 📝 Arrange
const input = {
email: 'invalid-email',
name: 'Test User',
password: 'securepassword123',
};
// 🎬 Act & Assert
await expect(
callProcedure('user.create', input)
).rejects.toThrow('Invalid email');
});
it('deve falhar com email duplicado', async () => {
// 📝 Arrange
const input = {
email: 'test@example.com',
name: 'Test User',
password: 'securepassword123',
};
await callProcedure('user.create', input);
// 🎬 Act & Assert
await expect(
callProcedure('user.create', input)
).rejects.toThrow('Email already exists');
});
});
describe('user.getById', () => {
it('deve retornar usuário existente', async () => {
// 📝 Arrange
const createInput = {
email: 'test@example.com',
name: 'Test User',
password: 'securepassword123',
};
const createdUser = await callProcedure('user.create', createInput);
// 🎬 Act
const result = await callProcedure('user.getById', {
id: createdUser.id
});
// ✅ Assert
expect(result).toMatchObject({
id: createdUser.id,
email: 'test@example.com',
name: 'Test User',
});
});
it('deve falhar com ID inexistente', async () => {
// 🎬 Act & Assert
await expect(
callProcedure('user.getById', { id: 'non-existent-id' })
).rejects.toThrow('User not found');
});
});
});
// 📁 tests/unit/middleware/auth.test.ts
import { describe, it, expect, vi } from 'vitest';
import { TRPCError } from '@trpc/server';
import { enforceUserIsAuthed } from '../../../src/server/api/trpc';
import { createTestContext } from '../../utils/trpc-test-utils';
describe('Auth Middleware', () => {
it('deve permitir acesso com usuário autenticado', async () => {
// 📝 Arrange
const ctx = createTestContext();
const next = vi.fn().mockResolvedValue({ result: 'success' });
// 🎬 Act
const result = await enforceUserIsAuthed({
ctx,
next,
path: 'test',
type: 'query',
getRawInput: () => ({}),
});
// ✅ Assert
expect(next).toHaveBeenCalledWith({
ctx: expect.objectContaining({
session: expect.objectContaining({
user: expect.any(Object),
}),
}),
});
expect(result).toEqual({ result: 'success' });
});
it('deve falhar sem usuário autenticado', async () => {
// 📝 Arrange
const ctx = createTestContext();
ctx.session = null; // Usuário não autenticado
const next = vi.fn();
// 🎬 Act & Assert
await expect(
enforceUserIsAuthed({
ctx,
next,
path: 'test',
type: 'query',
getRawInput: () => ({}),
})
).rejects.toThrow(
expect.objectContaining({
code: 'UNAUTHORIZED',
message: 'Not authenticated',
})
);
expect(next).not.toHaveBeenCalled();
});
it('deve adicionar user ao context quando autenticado', async () => {
// 📝 Arrange
const ctx = createTestContext();
let capturedCtx: any;
const next = vi.fn().mockImplementation(({ ctx }) => {
capturedCtx = ctx;
return Promise.resolve({ result: 'success' });
});
// 🎬 Act
await enforceUserIsAuthed({
ctx,
next,
path: 'test',
type: 'query',
getRawInput: () => ({}),
});
// ✅ Assert
expect(capturedCtx.user).toBeDefined();
expect(capturedCtx.user.id).toBe(ctx.session?.user.id);
});
});
// 📁 tests/unit/validators/user.test.ts
import { describe, it, expect } from 'vitest';
import {
createUserSchema,
updateUserSchema,
getUserByIdSchema
} from '../../../src/schemas/user';
describe('User Validators', () => {
describe('createUserSchema', () => {
it('deve validar dados corretos', () => {
// 📝 Arrange
const validData = {
email: 'test@example.com',
name: 'Test User',
password: 'securepassword123',
};
// 🎬 Act
const result = createUserSchema.safeParse(validData);
// ✅ Assert
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual(validData);
}
});
it('deve falhar com email inválido', () => {
// 📝 Arrange
const invalidData = {
email: 'invalid-email',
name: 'Test User',
password: 'securepassword123',
};
// 🎬 Act
const result = createUserSchema.safeParse(invalidData);
// ✅ Assert
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toContain('email');
expect(result.error.issues[0].message).toContain('Invalid email');
}
});
it('deve falhar com senha muito curta', () => {
// 📝 Arrange
const invalidData = {
email: 'test@example.com',
name: 'Test User',
password: '123', // Muito curta
};
// 🎬 Act
const result = createUserSchema.safeParse(invalidData);
// ✅ Assert
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toContain('password');
expect(result.error.issues[0].message).toContain('minimum');
}
});
it('deve falhar com campos obrigatórios faltando', () => {
// 📝 Arrange
const invalidData = {
email: 'test@example.com',
// name faltando
// password faltando
};
// 🎬 Act
const result = createUserSchema.safeParse(invalidData);
// ✅ Assert
expect(result.success).toBe(false);
if (!result.success) {
const paths = result.error.issues.map(issue => issue.path[0]);
expect(paths).toContain('name');
expect(paths).toContain('password');
}
});
});
describe('getUserByIdSchema', () => {
it('deve validar UUID válido', () => {
// 📝 Arrange
const validData = {
id: '123e4567-e89b-12d3-a456-426614174000',
};
// 🎬 Act
const result = getUserByIdSchema.safeParse(validData);
// ✅ Assert
expect(result.success).toBe(true);
});
it('deve falhar com ID inválido', () => {
// 📝 Arrange
const invalidData = {
id: 'invalid-uuid',
};
// 🎬 Act
const result = getUserByIdSchema.safeParse(invalidData);
// ✅ Assert
expect(result.success).toBe(false);
});
});
});
// 📁 tests/integration/http-handler.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { createServer } from '../../../src/server/app';
import { db } from '../../../src/server/db';
describe('tRPC HTTP Handler Integration', () => {
let app: any;
beforeAll(async () => {
// 🚀 Setup server para testes
app = await createServer();
await db.migrate.latest();
});
afterAll(async () => {
// 🧹 Cleanup
await db.destroy();
});
it('deve processar query via HTTP', async () => {
// 📝 Arrange
const userId = 'test-user-id';
await db.user.create({
data: {
id: userId,
email: 'test@example.com',
name: 'Test User',
password: 'hashedpassword',
},
});
// 🎬 Act
const response = await request(app)
.get('/api/trpc/user.getById')
.query({
input: JSON.stringify({ id: userId }),
})
.set('Cookie', 'session=valid-session-token');
// ✅ Assert
expect(response.status).toBe(200);
expect(response.body.result.data).toMatchObject({
id: userId,
email: 'test@example.com',
name: 'Test User',
});
});
it('deve processar mutation via HTTP POST', async () => {
// 📝 Arrange
const userData = {
email: 'newuser@example.com',
name: 'New User',
password: 'securepassword123',
};
// 🎬 Act
const response = await request(app)
.post('/api/trpc/user.create')
.send(userData)
.set('Content-Type', 'application/json');
// ✅ Assert
expect(response.status).toBe(200);
expect(response.body.result.data).toMatchObject({
id: expect.any(String),
email: userData.email,
name: userData.name,
});
// 🔍 Verificar no banco
const createdUser = await db.user.findUnique({
where: { email: userData.email },
});
expect(createdUser).toBeTruthy();
});
it('deve retornar erro 401 para procedures protegidas', async () => {
// 🎬 Act
const response = await request(app)
.get('/api/trpc/user.getProfile')
.query({
input: JSON.stringify({}),
});
// ✅ Assert
expect(response.status).toBe(401);
expect(response.body.error.code).toBe('UNAUTHORIZED');
});
it('deve processar batch de requests', async () => {
// 📝 Arrange
const batchRequest = [
{
id: 1,
method: 'query',
params: {
path: 'user.getById',
input: { id: 'user-1' },
},
},
{
id: 2,
method: 'query',
params: {
path: 'user.getById',
input: { id: 'user-2' },
},
},
];
// 🎬 Act
const response = await request(app)
.post('/api/trpc')
.send(batchRequest)
.set('Content-Type', 'application/json')
.set('Cookie', 'session=valid-session-token');
// ✅ Assert
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body).toHaveLength(2);
});
});
// 📁 tests/integration/websocket.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import WebSocket from 'ws';
import { createWSClient, wsLink } from '@trpc/client';
import { createTRPCProxyClient } from '@trpc/client';
import { type AppRouter } from '../../../src/server/api/root';
import { startWebSocketServer } from '../../../src/server/websocket';
describe('WebSocket Integration', () => {
let wsServer: any;
let client: any;
const WS_PORT = 3002;
beforeAll(async () => {
// 🚀 Start WebSocket server
wsServer = await startWebSocketServer(WS_PORT);
// 📞 Create client
const wsClient = createWSClient({
url: `ws://localhost:${WS_PORT}`,
});
client = createTRPCProxyClient<AppRouter>({
links: [wsLink({ client: wsClient })],
});
});
afterAll(async () => {
// 🧹 Cleanup
await wsServer.close();
});
it('deve conectar via WebSocket', async () => {
// 🎬 Act & Assert
const result = await client.health.check.query();
expect(result.status).toBe('ok');
});
it('deve receber subscription em tempo real', async () => {
// 📝 Arrange
const receivedMessages: any[] = [];
// 🎧 Setup subscription
const subscription = client.notifications.onUserMessage.subscribe(
{ userId: 'test-user' },
{
onData: (data: any) => {
receivedMessages.push(data);
},
onError: (error: any) => {
console.error('Subscription error:', error);
},
}
);
// ⏰ Wait for connection
await new Promise(resolve => setTimeout(resolve, 100));
// 🎬 Trigger event that should emit subscription
await client.user.sendMessage.mutate({
userId: 'test-user',
message: 'Hello from test!',
});
// ⏰ Wait for message propagation
await new Promise(resolve => setTimeout(resolve, 200));
// ✅ Assert
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toMatchObject({
userId: 'test-user',
message: 'Hello from test!',
timestamp: expect.any(String),
});
// 🧹 Cleanup subscription
subscription.unsubscribe();
});
it('deve lidar com múltiplas conexões simultâneas', async () => {
// 📝 Arrange
const clients = [];
const receivedMessages: any[] = [];
// 🚀 Create multiple clients
for (let i = 0; i < 3; i++) {
const wsClient = createWSClient({
url: `ws://localhost:${WS_PORT}`,
});
const client = createTRPCProxyClient<AppRouter>({
links: [wsLink({ client: wsClient })],
});
clients.push(client);
// 🎧 Setup subscription for each client
client.notifications.onGlobalMessage.subscribe(
{},
{
onData: (data: any) => {
receivedMessages.push({ clientId: i, data });
},
}
);
}
// ⏰ Wait for all connections
await new Promise(resolve => setTimeout(resolve, 200));
// 🎬 Broadcast message
await client.admin.broadcastMessage.mutate({
message: 'Global announcement!',
});
// ⏰ Wait for message propagation
await new Promise(resolve => setTimeout(resolve, 300));
// ✅ Assert
expect(receivedMessages).toHaveLength(3);
receivedMessages.forEach((msg, index) => {
expect(msg.clientId).toBe(index);
expect(msg.data.message).toBe('Global announcement!');
});
});
it('deve reconectar automaticamente após desconexão', async () => {
// 📝 Arrange
let connectionCount = 0;
let reconnectionCount = 0;
const wsClient = createWSClient({
url: `ws://localhost:${WS_PORT}`,
onOpen: () => {
connectionCount++;
},
retryDelayMs: 100,
});
const client = createTRPCProxyClient<AppRouter>({
links: [wsLink({ client: wsClient })],
});
// 🎬 Initial connection
await client.health.check.query();
expect(connectionCount).toBe(1);
// 💥 Force disconnect
(wsClient as any).getConnection()?.close();
// ⏰ Wait for reconnection
await new Promise(resolve => setTimeout(resolve, 500));
// 🔄 Test connection after reconnect
const result = await client.health.check.query();
// ✅ Assert
expect(result.status).toBe('ok');
expect(connectionCount).toBeGreaterThan(1);
});
});
// 📁 tests/integration/database.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { db } from '../../../src/server/db';
import { callProcedure } from '../../utils/trpc-test-utils';
describe('Database Integration', () => {
beforeEach(async () => {
// 🗄️ Clean database before each test
await db.user.deleteMany();
await db.post.deleteMany();
});
afterEach(async () => {
// 🧹 Cleanup after each test
await db.user.deleteMany();
await db.post.deleteMany();
});
it('deve manter transações ACID', async () => {
// 📝 Arrange
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'securepassword123',
};
// 🎬 Act - Criar usuário e post em transação
const result = await callProcedure('user.createWithPost', {
user: userData,
post: {
title: 'First Post',
content: 'This is my first post!',
},
});
// ✅ Assert - Verificar que ambos foram criados
expect(result.user).toMatchObject({
email: userData.email,
name: userData.name,
});
expect(result.post).toMatchObject({
title: 'First Post',
authorId: result.user.id,
});
// 🔍 Verificar no banco
const userInDb = await db.user.findUnique({
where: { id: result.user.id },
include: { posts: true },
});
expect(userInDb?.posts).toHaveLength(1);
expect(userInDb?.posts[0].title).toBe('First Post');
});
it('deve fazer rollback em caso de erro', async () => {
// 📝 Arrange
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'securepassword123',
};
// 🎬 Act & Assert - Tentar criar com post inválido
await expect(
callProcedure('user.createWithPost', {
user: userData,
post: {
title: '', // Título inválido - deve causar erro
content: 'This should fail!',
},
})
).rejects.toThrow();
// ✅ Assert - Verificar que NADA foi criado (rollback)
const userCount = await db.user.count();
const postCount = await db.post.count();
expect(userCount).toBe(0);
expect(postCount).toBe(0);
});
it('deve lidar com queries complexas', async () => {
// 📝 Arrange - Criar dados de teste
const users = await Promise.all([
db.user.create({
data: {
email: 'user1@example.com',
name: 'User 1',
password: 'password',
},
}),
db.user.create({
data: {
email: 'user2@example.com',
name: 'User 2',
password: 'password',
},
}),
]);
// Criar posts para os usuários
await Promise.all([
db.post.create({
data: {
title: 'Post 1',
content: 'Content 1',
authorId: users[0].id,
published: true,
},
}),
db.post.create({
data: {
title: 'Post 2',
content: 'Content 2',
authorId: users[1].id,
published: true,
},
}),
db.post.create({
data: {
title: 'Draft Post',
content: 'Draft Content',
authorId: users[0].id,
published: false,
},
}),
]);
// 🎬 Act - Query complexa com filtros e joins
const result = await callProcedure('post.getPublishedWithAuthors', {
page: 1,
limit: 10,
sortBy: 'createdAt',
sortOrder: 'desc',
});
// ✅ Assert
expect(result.posts).toHaveLength(2); // Apenas posts publicados
expect(result.total).toBe(2);
expect(result.hasMore).toBe(false);
// Verificar se inclui dados do autor
result.posts.forEach(post => {
expect(post.author).toBeDefined();
expect(post.author.email).toBeDefined();
expect(post.published).toBe(true);
});
});
it('deve otimizar queries com N+1 prevention', async () => {
// 📝 Arrange - Criar múltiplos usuários com posts
const users = await Promise.all(
Array.from({ length: 5 }, (_, i) =>
db.user.create({
data: {
email: `user${i}@example.com`,
name: `User ${i}`,
password: 'password',
},
})
)
);
// Criar posts para cada usuário
await Promise.all(
users.flatMap(user =>
Array.from({ length: 3 }, (_, i) =>
db.post.create({
data: {
title: `Post ${i} by ${user.name}`,
content: `Content ${i}`,
authorId: user.id,
published: true,
},
})
)
)
);
// 🎬 Act - Query que poderia causar N+1
const startTime = Date.now();
const result = await callProcedure('user.getAllWithPostCounts', {});
const endTime = Date.now();
// ✅ Assert - Verificar resultado e performance
expect(result).toHaveLength(5);
expect(endTime - startTime).toBeLessThan(100); // Deve ser rápido
result.forEach(user => {
expect(user.postCount).toBe(3);
expect(user.name).toBeDefined();
});
});
});
// 📁 tests/e2e/setup.ts
import { test as base, expect } from '@playwright/test';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { type AppRouter } from '../../src/server/api/root';
// 🎭 Extend base test com tRPC client
export const test = base.extend<{
trpcClient: ReturnType<typeof createTRPCProxyClient<AppRouter>>;
}>({
trpcClient: async ({ page }, use) => {
// 🔗 Setup tRPC client que funciona com cookies do browser
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
fetch: async (url, options) => {
// 🍪 Inject cookies from browser context
const cookies = await page.context().cookies();
const cookieHeader = cookies
.map(cookie => `${cookie.name}=${cookie.value}`)
.join('; ');
return fetch(url, {
...options,
headers: {
...options?.headers,
...(cookieHeader && { Cookie: cookieHeader }),
},
});
},
}),
],
});
await use(client);
},
});
export { expect };
// 📁 tests/e2e/auth.spec.ts
import { test, expect } from './setup';
test.describe('Authentication Flow', () => {
test('deve fazer login completo via interface', async ({ page, trpcClient }) => {
// 🎬 Navegar para página de login
await page.goto('/login');
// 📝 Preencher formulário
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'securepassword123');
// 🔘 Submeter formulário
await page.click('[data-testid="login-button"]');
// ⏰ Aguardar redirecionamento
await page.waitForURL('/dashboard');
// ✅ Verificar UI indica usuário logado
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
await expect(page.locator('text=test@example.com')).toBeVisible();
// 🔍 Verificar via tRPC que sessão está ativa
const profile = await trpcClient.user.getProfile.query();
expect(profile.email).toBe('test@example.com');
});
test('deve mostrar erro para credenciais inválidas', async ({ page }) => {
await page.goto('/login');
// 📝 Credenciais inválidas
await page.fill('[data-testid="email-input"]', 'wrong@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
// ✅ Verificar mensagem de erro
await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid credentials'
);
// 🚫 Não deve redirecionar
await expect(page).toHaveURL('/login');
});
test('deve fazer logout e limpar sessão', async ({ page, trpcClient }) => {
// 🔑 Login primeiro
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'securepassword123');
await page.click('[data-testid="login-button"]');
await page.waitForURL('/dashboard');
// 🚪 Fazer logout
await page.click('[data-testid="user-menu"]');
await page.click('[data-testid="logout-button"]');
// ⏰ Aguardar redirecionamento
await page.waitForURL('/');
// ✅ Verificar UI indica usuário deslogado
await expect(page.locator('[data-testid="login-link"]')).toBeVisible();
// 🔍 Verificar via tRPC que sessão foi removida
await expect(
trpcClient.user.getProfile.query()
).rejects.toThrow('UNAUTHORIZED');
});
test('deve proteger rotas autenticadas', async ({ page }) => {
// 🚫 Tentar acessar rota protegida sem login
await page.goto('/dashboard');
// ✅ Deve ser redirecionado para login
await page.waitForURL('/login');
// Verificar mensagem de redirecionamento
await expect(page.locator('[data-testid="redirect-message"]')).toContainText(
'Please log in to access this page'
);
});
});
// 📁 tests/e2e/user-crud.spec.ts
import { test, expect } from './setup';
test.describe('User CRUD Operations', () => {
test.beforeEach(async ({ page }) => {
// 🔑 Login antes de cada teste
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'admin@example.com');
await page.fill('[data-testid="password-input"]', 'adminpassword');
await page.click('[data-testid="login-button"]');
await page.waitForURL('/dashboard');
});
test('deve criar novo usuário via interface', async ({ page, trpcClient }) => {
// 🎬 Navegar para criação de usuário
await page.goto('/admin/users');
await page.click('[data-testid="create-user-button"]');
// 📝 Preencher formulário
const userData = {
email: 'newuser@example.com',
name: 'New User',
role: 'USER',
};
await page.fill('[data-testid="email-input"]', userData.email);
await page.fill('[data-testid="name-input"]', userData.name);
await page.selectOption('[data-testid="role-select"]', userData.role);
// 💾 Salvar
await page.click('[data-testid="save-button"]');
// ✅ Verificar sucesso na UI
await expect(page.locator('[data-testid="success-message"]')).toContainText(
'User created successfully'
);
// 🔍 Verificar via tRPC
const users = await trpcClient.user.getAll.query();
const createdUser = users.find(u => u.email === userData.email);
expect(createdUser).toBeTruthy();
expect(createdUser?.name).toBe(userData.name);
expect(createdUser?.role).toBe(userData.role);
});
test('deve editar usuário existente', async ({ page, trpcClient }) => {
// 📝 Criar usuário primeiro via tRPC
const user = await trpcClient.user.create.mutate({
email: 'editable@example.com',
name: 'Editable User',
password: 'password123',
});
// 🎬 Navegar para edição
await page.goto(`/admin/users/${user.id}/edit`);
// ✏️ Modificar dados
await page.fill('[data-testid="name-input"]', 'Updated User Name');
await page.selectOption('[data-testid="role-select"]', 'ADMIN');
// 💾 Salvar alterações
await page.click('[data-testid="update-button"]');
// ✅ Verificar sucesso
await expect(page.locator('[data-testid="success-message"]')).toContainText(
'User updated successfully'
);
// 🔍 Verificar mudanças via tRPC
const updatedUser = await trpcClient.user.getById.query({ id: user.id });
expect(updatedUser.name).toBe('Updated User Name');
expect(updatedUser.role).toBe('ADMIN');
});
test('deve deletar usuário com confirmação', async ({ page, trpcClient }) => {
// 📝 Criar usuário para deletar
const user = await trpcClient.user.create.mutate({
email: 'deletable@example.com',
name: 'Deletable User',
password: 'password123',
});
// 🎬 Ir para lista de usuários
await page.goto('/admin/users');
// 🗑️ Clicar em deletar
await page.click(`[data-testid="delete-user-${user.id}"]`);
// ⚠️ Confirmar na modal
await expect(page.locator('[data-testid="confirm-modal"]')).toBeVisible();
await expect(page.locator('[data-testid="confirm-modal"]')).toContainText(
'Are you sure you want to delete this user?'
);
await page.click('[data-testid="confirm-delete"]');
// ✅ Verificar remoção da UI
await expect(page.locator('[data-testid="success-message"]')).toContainText(
'User deleted successfully'
);
// 🔍 Verificar via tRPC que foi deletado
await expect(
trpcClient.user.getById.query({ id: user.id })
).rejects.toThrow('User not found');
});
test('deve filtrar e buscar usuários', async ({ page, trpcClient }) => {
// 📝 Criar múltiplos usuários para teste
const users = await Promise.all([
trpcClient.user.create.mutate({
email: 'john@example.com',
name: 'John Doe',
password: 'password123',
}),
trpcClient.user.create.mutate({
email: 'jane@example.com',
name: 'Jane Smith',
password: 'password123',
}),
trpcClient.user.create.mutate({
email: 'admin@test.com',
name: 'Admin User',
password: 'password123',
}),
]);
// 🎬 Ir para lista
await page.goto('/admin/users');
// 🔍 Buscar por nome
await page.fill('[data-testid="search-input"]', 'Jane');
await page.press('[data-testid="search-input"]', 'Enter');
// ✅ Verificar filtros
await expect(page.locator('[data-testid="user-list"] tr')).toHaveCount(2); // Header + 1 resultado
await expect(page.locator('text=Jane Smith')).toBeVisible();
await expect(page.locator('text=John Doe')).not.toBeVisible();
// 🔄 Limpar busca
await page.fill('[data-testid="search-input"]', '');
await page.press('[data-testid="search-input"]', 'Enter');
// ✅ Todos usuários devem aparecer
await expect(page.locator('[data-testid="user-list"] tr')).toHaveCount(4); // Header + 3 usuários + admin
});
test('deve paginar resultados corretamente', async ({ page, trpcClient }) => {
// 📝 Criar muitos usuários
const userPromises = Array.from({ length: 25 }, (_, i) =>
trpcClient.user.create.mutate({
email: `user${i}@example.com`,
name: `User ${i}`,
password: 'password123',
})
);
await Promise.all(userPromises);
// 🎬 Ir para lista
await page.goto('/admin/users');
// ✅ Verificar primeira página (assumindo 10 per page)
await expect(page.locator('[data-testid="user-list"] tr')).toHaveCount(11); // Header + 10
// ➡️ Ir para próxima página
await page.click('[data-testid="next-page"]');
// ✅ Verificar segunda página
await expect(page.locator('[data-testid="user-list"] tr')).toHaveCount(11); // Header + 10
// ➡️ Ir para terceira página
await page.click('[data-testid="next-page"]');
// ✅ Verificar terceira página (resto)
await expect(page.locator('[data-testid="user-list"] tr')).toHaveCount(7); // Header + 6 restantes
// ⬅️ Voltar para primeira página
await page.click('[data-testid="page-1"]');
// ✅ Verificar volta
await expect(page.locator('[data-testid="user-list"] tr')).toHaveCount(11);
});
});
// 📁 tests/e2e/performance.spec.ts
import { test, expect } from './setup';
test.describe('Performance Tests', () => {
test('deve carregar dashboard em menos de 2 segundos', async ({ page }) => {
// 🔑 Login primeiro
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'securepassword123');
await page.click('[data-testid="login-button"]');
// ⏱️ Medir tempo de carregamento do dashboard
const startTime = Date.now();
await page.goto('/dashboard');
// ⏰ Aguardar elementos principais carregarem
await page.waitForSelector('[data-testid="dashboard-content"]');
await page.waitForSelector('[data-testid="stats-cards"]');
const endTime = Date.now();
const loadTime = endTime - startTime;
// ✅ Verificar performance
expect(loadTime).toBeLessThan(2000); // Menos de 2 segundos
console.log(`Dashboard loaded in ${loadTime}ms`);
});
test('deve lidar com 100 items na lista sem lag', async ({ page, trpcClient }) => {
// 📝 Criar muitos items
const items = await Promise.all(
Array.from({ length: 100 }, (_, i) =>
trpcClient.post.create.mutate({
title: `Post ${i}`,
content: `Content for post ${i}`,
})
)
);
// 🎬 Navegar para lista
const startTime = Date.now();
await page.goto('/posts');
// ⏰ Aguardar carregamento completo
await page.waitForSelector('[data-testid="posts-list"]');
await page.waitForFunction(
() => document.querySelectorAll('[data-testid^="post-item-"]').length >= 20
);
const endTime = Date.now();
const loadTime = endTime - startTime;
// ✅ Verificar performance
expect(loadTime).toBeLessThan(3000);
// 📜 Testar scroll performance
const scrollStartTime = Date.now();
// Scroll até o final
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// Aguardar lazy loading carregar mais items
await page.waitForFunction(
() => document.querySelectorAll('[data-testid^="post-item-"]').length >= 100
);
const scrollEndTime = Date.now();
const scrollTime = scrollEndTime - scrollStartTime;
expect(scrollTime).toBeLessThan(2000);
console.log(`100 items loaded in ${loadTime}ms, scroll completed in ${scrollTime}ms`);
});
test('deve manter responsividade durante operações batch', async ({ page, trpcClient }) => {
// 🔑 Login como admin
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'admin@example.com');
await page.fill('[data-testid="password-input"]', 'adminpassword');
await page.click('[data-testid="login-button"]');
await page.goto('/admin/batch-operations');
// 🎬 Iniciar operação batch (100 items)
await page.click('[data-testid="batch-create-100"]');
// ⏱️ Verificar que UI permanece responsiva
const checkResponsiveness = async () => {
const startTime = Date.now();
await page.click('[data-testid="test-button"]');
const endTime = Date.now();
return endTime - startTime;
};
// Testar responsividade durante a operação
let maxResponseTime = 0;
const testDuration = 10000; // 10 segundos
const testStart = Date.now();
while (Date.now() - testStart < testDuration) {
const responseTime = await checkResponsiveness();
maxResponseTime = Math.max(maxResponseTime, responseTime);
// Aguardar um pouco antes do próximo teste
await page.waitForTimeout(500);
}
// ✅ UI deve permanecer responsiva (< 100ms)
expect(maxResponseTime).toBeLessThan(100);
// ⏰ Aguardar operação batch completar
await page.waitForSelector('[data-testid="batch-complete"]', {
timeout: 30000
});
console.log(`Max response time during batch: ${maxResponseTime}ms`);
});
test('deve otimizar carregamento de imagens', async ({ page }) => {
// 🎬 Navegar para página com muitas imagens
await page.goto('/gallery');
// 📊 Verificar lazy loading
const images = page.locator('img[data-testid^="gallery-image-"]');
const imageCount = await images.count();
// ✅ Apenas primeiras imagens devem estar carregadas
const visibleImages = await page.evaluate(() => {
const imgs = document.querySelectorAll('img[data-testid^="gallery-image-"]');
return Array.from(imgs).filter(img =>
(img as HTMLImageElement).complete && (img as HTMLImageElement).naturalHeight > 0
).length;
});
// Deve carregar apenas imagens acima da dobra inicialmente
expect(visibleImages).toBeLessThan(imageCount / 2);
// 📜 Scroll para triggerar lazy loading
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight / 2);
});
// ⏰ Aguardar mais imagens carregarem
await page.waitForTimeout(2000);
const visibleImagesAfterScroll = await page.evaluate(() => {
const imgs = document.querySelectorAll('img[data-testid^="gallery-image-"]');
return Array.from(imgs).filter(img =>
(img as HTMLImageElement).complete && (img as HTMLImageElement).naturalHeight > 0
).length;
});
// ✅ Mais imagens devem ter carregado
expect(visibleImagesAfterScroll).toBeGreaterThan(visibleImages);
console.log(`Loaded ${visibleImages} initially, ${visibleImagesAfterScroll} after scroll`);
});
});
Na próxima aula, vamos explorar CI/CD e Automação, implementando pipelines que executam automaticamente todos esses testes em cada commit, garantindo qualidade contínua em produção.