devsquad 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/devsquad.js +37 -11
- package/package.json +2 -2
- package/src/init.js +70 -26
- package/src/update.js +95 -0
- package/templates/.claude/skills/cicd/SKILL.md +46 -0
- package/templates/.claude/skills/cicd/backend-ci.md +114 -0
- package/templates/.claude/skills/cicd/frontend-ci.md +97 -0
- package/templates/.claude/skills/clickup/SKILL.md +63 -14
- package/templates/.claude/skills/docs/templates.md +203 -39
- package/templates/.claude/skills/figma/SKILL.md +125 -26
- package/templates/.claude/skills/nestjs/api-contracts.md +221 -0
- package/templates/.claude/skills/react/setup.md +61 -14
- package/templates/.claude/skills/testing/SKILL.md +78 -0
- package/templates/.claude/skills/testing/backend.md +161 -0
- package/templates/.claude/skills/testing/e2e.md +156 -0
- package/templates/.claude/skills/testing/frontend.md +138 -0
- package/templates/CLAUDE.md +7 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Testing — E2E com Playwright
|
|
2
|
+
|
|
3
|
+
## Configuração
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// playwright.config.ts
|
|
7
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
testDir: './e2e',
|
|
11
|
+
fullyParallel: true,
|
|
12
|
+
forbidOnly: !!process.env.CI,
|
|
13
|
+
retries: process.env.CI ? 2 : 0,
|
|
14
|
+
reporter: process.env.CI ? 'github' : 'html',
|
|
15
|
+
use: {
|
|
16
|
+
baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:5173',
|
|
17
|
+
trace: 'on-first-retry',
|
|
18
|
+
screenshot: 'only-on-failure',
|
|
19
|
+
},
|
|
20
|
+
projects: [
|
|
21
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
22
|
+
],
|
|
23
|
+
webServer: {
|
|
24
|
+
command: 'npm run dev',
|
|
25
|
+
url: 'http://localhost:5173',
|
|
26
|
+
reuseExistingServer: !process.env.CI,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Fluxo de login
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// e2e/auth.spec.ts
|
|
35
|
+
import { test, expect } from '@playwright/test';
|
|
36
|
+
|
|
37
|
+
test.describe('Autenticação', () => {
|
|
38
|
+
test('login com credenciais válidas redireciona para dashboard', async ({ page }) => {
|
|
39
|
+
await page.goto('/login');
|
|
40
|
+
|
|
41
|
+
await page.getByLabel('E-mail').fill('admin@empresa.com');
|
|
42
|
+
await page.getByLabel('Senha').fill('Senha@123');
|
|
43
|
+
await page.getByRole('button', { name: /entrar/i }).click();
|
|
44
|
+
|
|
45
|
+
await expect(page).toHaveURL('/dashboard');
|
|
46
|
+
await expect(page.getByText('Bem-vindo')).toBeVisible();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('exibe mensagem de erro com credenciais inválidas', async ({ page }) => {
|
|
50
|
+
await page.goto('/login');
|
|
51
|
+
|
|
52
|
+
await page.getByLabel('E-mail').fill('wrong@test.com');
|
|
53
|
+
await page.getByLabel('Senha').fill('errado');
|
|
54
|
+
await page.getByRole('button', { name: /entrar/i }).click();
|
|
55
|
+
|
|
56
|
+
await expect(page.getByText(/credenciais inválidas/i)).toBeVisible();
|
|
57
|
+
await expect(page).toHaveURL('/login');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('logout limpa sessão e redireciona para login', async ({ page }) => {
|
|
61
|
+
// Faz login primeiro
|
|
62
|
+
await page.goto('/login');
|
|
63
|
+
await page.getByLabel('E-mail').fill('admin@empresa.com');
|
|
64
|
+
await page.getByLabel('Senha').fill('Senha@123');
|
|
65
|
+
await page.getByRole('button', { name: /entrar/i }).click();
|
|
66
|
+
await expect(page).toHaveURL('/dashboard');
|
|
67
|
+
|
|
68
|
+
// Faz logout
|
|
69
|
+
await page.getByRole('button', { name: /sair/i }).click();
|
|
70
|
+
|
|
71
|
+
await expect(page).toHaveURL('/login');
|
|
72
|
+
// Tenta acessar rota protegida
|
|
73
|
+
await page.goto('/dashboard');
|
|
74
|
+
await expect(page).toHaveURL('/login');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Page Objects — organização para testes complexos
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// e2e/pages/LoginPage.ts
|
|
83
|
+
import { Page, Locator } from '@playwright/test';
|
|
84
|
+
|
|
85
|
+
export class LoginPage {
|
|
86
|
+
readonly emailInput: Locator;
|
|
87
|
+
readonly passwordInput: Locator;
|
|
88
|
+
readonly submitButton: Locator;
|
|
89
|
+
readonly errorMessage: Locator;
|
|
90
|
+
|
|
91
|
+
constructor(private page: Page) {
|
|
92
|
+
this.emailInput = page.getByLabel('E-mail');
|
|
93
|
+
this.passwordInput = page.getByLabel('Senha');
|
|
94
|
+
this.submitButton = page.getByRole('button', { name: /entrar/i });
|
|
95
|
+
this.errorMessage = page.getByRole('alert');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async goto() {
|
|
99
|
+
await this.page.goto('/login');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async login(email: string, password: string) {
|
|
103
|
+
await this.emailInput.fill(email);
|
|
104
|
+
await this.passwordInput.fill(password);
|
|
105
|
+
await this.submitButton.click();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Autenticação global (evitar login em cada teste)
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// e2e/fixtures/auth.ts
|
|
114
|
+
import { test as base, expect } from '@playwright/test';
|
|
115
|
+
|
|
116
|
+
export const test = base.extend({
|
|
117
|
+
authenticatedPage: async ({ page }, use) => {
|
|
118
|
+
await page.goto('/login');
|
|
119
|
+
await page.getByLabel('E-mail').fill(process.env.E2E_USER_EMAIL!);
|
|
120
|
+
await page.getByLabel('Senha').fill(process.env.E2E_USER_PASSWORD!);
|
|
121
|
+
await page.getByRole('button', { name: /entrar/i }).click();
|
|
122
|
+
await expect(page).toHaveURL('/dashboard');
|
|
123
|
+
await use(page);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Scripts
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"scripts": {
|
|
133
|
+
"e2e": "playwright test",
|
|
134
|
+
"e2e:ui": "playwright test --ui",
|
|
135
|
+
"e2e:report": "playwright show-report",
|
|
136
|
+
"e2e:codegen": "playwright codegen http://localhost:5173"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## .env.e2e
|
|
142
|
+
|
|
143
|
+
```env
|
|
144
|
+
E2E_BASE_URL=http://localhost:5173
|
|
145
|
+
E2E_USER_EMAIL=admin@empresa.com
|
|
146
|
+
E2E_USER_PASSWORD=Senha@123
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Checklist e2e
|
|
150
|
+
|
|
151
|
+
- [ ] Apenas fluxos críticos de negócio (login, cadastro, checkout, ação principal)
|
|
152
|
+
- [ ] Page Objects para telas com muitos elementos
|
|
153
|
+
- [ ] Autenticação global reutilizada entre testes relacionados
|
|
154
|
+
- [ ] Screenshots e traces habilitados em CI
|
|
155
|
+
- [ ] Variáveis de ambiente em `.env.e2e` (nunca hardcoded)
|
|
156
|
+
- [ ] Dados de teste isolados — nunca usar dados de produção
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Testing — Frontend (React + Vitest + Testing Library)
|
|
2
|
+
|
|
3
|
+
## Teste de componente
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// src/components/ui/Button.test.tsx
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { Button } from './Button';
|
|
10
|
+
|
|
11
|
+
describe('Button', () => {
|
|
12
|
+
it('renderiza o texto corretamente', () => {
|
|
13
|
+
render(<Button>Salvar</Button>);
|
|
14
|
+
expect(screen.getByRole('button', { name: /salvar/i })).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('chama onClick ao clicar', async () => {
|
|
18
|
+
const user = userEvent.setup();
|
|
19
|
+
const onClick = vi.fn();
|
|
20
|
+
render(<Button onClick={onClick}>Clique</Button>);
|
|
21
|
+
|
|
22
|
+
await user.click(screen.getByRole('button'));
|
|
23
|
+
|
|
24
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('fica desabilitado quando loading=true', () => {
|
|
28
|
+
render(<Button loading>Enviando</Button>);
|
|
29
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Teste de formulário com validação
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// src/pages/login/LoginPage.test.tsx
|
|
38
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
39
|
+
import userEvent from '@testing-library/user-event';
|
|
40
|
+
import { LoginPage } from './LoginPage';
|
|
41
|
+
import { AuthProvider } from '../../contexts/AuthContext';
|
|
42
|
+
import { vi } from 'vitest';
|
|
43
|
+
|
|
44
|
+
// Mock do serviço de API
|
|
45
|
+
vi.mock('../../services/api', () => ({
|
|
46
|
+
default: {
|
|
47
|
+
post: vi.fn(),
|
|
48
|
+
interceptors: {
|
|
49
|
+
request: { use: vi.fn() },
|
|
50
|
+
response: { use: vi.fn() },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const renderWithAuth = (ui: React.ReactElement) =>
|
|
56
|
+
render(<AuthProvider>{ui}</AuthProvider>);
|
|
57
|
+
|
|
58
|
+
describe('LoginPage', () => {
|
|
59
|
+
it('exibe erro quando email está vazio', async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
renderWithAuth(<LoginPage />);
|
|
62
|
+
|
|
63
|
+
await user.click(screen.getByRole('button', { name: /entrar/i }));
|
|
64
|
+
|
|
65
|
+
expect(await screen.findByText(/email é obrigatório/i)).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('exibe erro da API ao falhar login', async () => {
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
const { default: api } = await import('../../services/api');
|
|
71
|
+
(api.post as ReturnType<typeof vi.fn>).mockRejectedValue({
|
|
72
|
+
response: { data: { message: 'Credenciais inválidas' } },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
renderWithAuth(<LoginPage />);
|
|
76
|
+
await user.type(screen.getByLabelText(/email/i), 'user@test.com');
|
|
77
|
+
await user.type(screen.getByLabelText(/senha/i), 'errado');
|
|
78
|
+
await user.click(screen.getByRole('button', { name: /entrar/i }));
|
|
79
|
+
|
|
80
|
+
expect(await screen.findByText(/credenciais inválidas/i)).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Teste de hook customizado
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// src/hooks/useUsers.test.tsx
|
|
89
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
90
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
91
|
+
import { useUsers } from './useUsers';
|
|
92
|
+
import api from '../services/api';
|
|
93
|
+
import { vi } from 'vitest';
|
|
94
|
+
|
|
95
|
+
vi.mock('../services/api');
|
|
96
|
+
|
|
97
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
|
98
|
+
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
99
|
+
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
describe('useUsers', () => {
|
|
103
|
+
it('retorna lista de usuários', async () => {
|
|
104
|
+
(api.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
105
|
+
data: { data: [{ id: '1', name: 'João' }] },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { result } = renderHook(() => useUsers(), { wrapper });
|
|
109
|
+
|
|
110
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
111
|
+
|
|
112
|
+
expect(result.current.data).toHaveLength(1);
|
|
113
|
+
expect(result.current.data![0].name).toBe('João');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Scripts recomendados
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"scripts": {
|
|
123
|
+
"test": "vitest",
|
|
124
|
+
"test:ui": "vitest --ui",
|
|
125
|
+
"test:cov": "vitest --coverage",
|
|
126
|
+
"test:run": "vitest run"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Checklist de testes frontend
|
|
132
|
+
|
|
133
|
+
- [ ] Componentes críticos têm testes de render e interação
|
|
134
|
+
- [ ] Formulários testados com casos de validação e erro da API
|
|
135
|
+
- [ ] API mockada nos testes de componente (nunca chamar API real)
|
|
136
|
+
- [ ] Hooks com TanStack Query testados com `renderHook` + wrapper
|
|
137
|
+
- [ ] Nenhum `act()` manual — usar `waitFor` e `findBy*` queries
|
|
138
|
+
- [ ] Acessibilidade: queries por `role`, `label`, `name` — não por classe CSS
|
package/templates/CLAUDE.md
CHANGED
|
@@ -6,17 +6,19 @@ Você é o **DevSquad**, um agente especializado em inicializar e arquitetar sis
|
|
|
6
6
|
- Backend: NestJS + Prisma + PostgreSQL
|
|
7
7
|
- Frontend: React + TypeScript + TailwindCSS
|
|
8
8
|
- Mobile: React Native + Expo + NativeWind
|
|
9
|
-
- Auth: JWT (access 15min + refresh 7d)
|
|
9
|
+
- Auth: JWT (access 15min em memória + refresh 7d em cookie httpOnly)
|
|
10
10
|
- Segurança: OWASP Top 10
|
|
11
11
|
- Versionamento: Git Flow + Conventional Commits
|
|
12
12
|
|
|
13
13
|
## Regras universais (sempre aplicar)
|
|
14
14
|
|
|
15
15
|
**Código**
|
|
16
|
-
- Nunca retorne senha em respostas de API
|
|
17
|
-
- IDs sempre em UUID (não sequencial)
|
|
16
|
+
- Nunca retorne senha ou hashedRefreshToken em respostas de API
|
|
17
|
+
- IDs sempre em UUID (não sequencial) — evita enumeração (OWASP A01)
|
|
18
18
|
- Variáveis sensíveis sempre em `.env`, nunca hardcoded
|
|
19
19
|
- DTOs com class-validator em todos os inputs
|
|
20
|
+
- Tokens JWT: access em memória (nunca localStorage), refresh em cookie httpOnly
|
|
21
|
+
- Respostas de API no formato `{ data, meta }` via interceptor global
|
|
20
22
|
|
|
21
23
|
**Commits**
|
|
22
24
|
- Padrão: `tipo(escopo): descrição` — ex: `feat(auth): adicionar JWT`
|
|
@@ -59,6 +61,8 @@ Carregue a skill correspondente conforme a necessidade do desenvolvedor:
|
|
|
59
61
|
- `/devsquad git` → `.claude/skills/git/`
|
|
60
62
|
- `/devsquad security` → `.claude/skills/security/`
|
|
61
63
|
- `/devsquad database` → `.claude/skills/database/`
|
|
64
|
+
- `/devsquad testing` → `.claude/skills/testing/`
|
|
65
|
+
- `/devsquad cicd` → `.claude/skills/cicd/`
|
|
62
66
|
- `/devsquad postman` → `.claude/skills/postman/`
|
|
63
67
|
- `/devsquad docs` → `.claude/skills/docs/`
|
|
64
68
|
- `/devsquad clickup` → `.claude/skills/clickup/`
|