🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →

Testes e Qualidade

Aula 1 - Módulo 6: Implementação de testes robustos para aplicações tRPC

🎯 Por que Testes são Fundamentais em tRPC?

🔒 Garantia de Qualidade

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.

🚀 Refatoração Segura

Testes permitem refatorar procedures, middleware e transformers com confiança, mantendo a integridade da API e dos contratos estabelecidos.

📊 Cobertura Completa

Desde validators Zod até WebSocket subscriptions, cada camada da aplicação tRPC deve ser testada para garantir robustez em produção.

🎭 Mocking Inteligente

O tRPC oferece ferramentas específicas para mock de procedures, permitindo testes isolados e determinísticos.

⚠️ Conceitos Importantes para Entender

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

🧪 Fundamentos de Testes em tRPC

🔧 Setup do Ambiente de Testes

tests/setup.ts
// 📁 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() },
  },
}));

🏗️ Test Utilities para tRPC

tests/utils/trpc-test-utils.ts
// 📁 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),
          }),
        }),
      }),
    })
  );
}

⚙️ Configuração Vitest

vitest.config.ts
// 📁 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'),
    },
  },
});

🔬 Testes Unitários

🎯 Testando Procedures

tests/unit/procedures/user.test.ts
// 📁 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');
    });
  });
});

🛡️ Testando Middleware

tests/unit/middleware/auth.test.ts
// 📁 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);
  });
});

📏 Testando Validators (Zod)

tests/unit/validators/user.test.ts
// 📁 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);
    });
  });
});

🔗 Testes de Integração

🌐 Testando HTTP Handlers

tests/integration/http-handler.test.ts
// 📁 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);
  });
});

🔌 Testando WebSocket Subscriptions

tests/integration/websocket.test.ts
// 📁 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);
  });
});

🗄️ Testando Database Integration

tests/integration/database.test.ts
// 📁 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();
    });
  });
});

🎭 Testes End-to-End

🌍 Setup Playwright para tRPC

tests/e2e/setup.ts
// 📁 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 };

🔐 Testes de Autenticação E2E

tests/e2e/auth.spec.ts
// 📁 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'
    );
  });
});

📊 Testes de CRUD Completo

tests/e2e/user-crud.spec.ts
// 📁 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);
  });
});

⚡ Testes de Performance E2E

tests/e2e/performance.spec.ts
// 📁 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`);
  });
});

✅ O que você conquistou nesta aula

Testes Unitários completos para procedures
Testes de Middleware e autenticação
Validação Zod com cobertura completa
Mocking inteligente de dependencies
Testes de Integração HTTP e WebSocket
Database Testing com transações
E2E Testing com Playwright
Performance Testing e otimização

🎯 Próximos Passos

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.

Aula Anterior
Módulo 6
Aula 1 de 5
Testes e Qualidade
Próxima Aula