Skip to content

Commit 89ef266

Browse files
corrige smoke test: botao cancelar no passo 0 do formulario
No passo 0 o botão exibe 'Cancelar', não 'Voltar'. 'Voltar' só aparece no passo 1.
1 parent 01406e9 commit 89ef266

8 files changed

Lines changed: 43 additions & 79 deletions

File tree

README.md

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,59 @@
11
# Flugo Staff Manager
22

3-
Gerenciador de colaboradores desenvolvido com foco em performance, UX resiliente e suporte offline.
3+
Gerenciador de colaboradores feito com React + Firebase. Formulário multi-etapa com rascunho automático e sincronização com o Firestore.
44

5-
A aplicação utiliza um formulário multi-etapa com persistência de rascunho e sincronização automática com o Firebase Firestore.
6-
7-
## 🚀 Demo
8-
Produção na Vercel: [https://flugo-employees-theta.vercel.app](https://flugo-employees-theta.vercel.app)
5+
Demo: [https://flugo-employees-theta.vercel.app](https://flugo-employees-theta.vercel.app)
96

107
---
118

12-
## 🛠 Tech Stack
13-
- **Framework:** React 19 + TypeScript + Vite
14-
- **UI:** Material UI v7 + Emotion
15-
- **State & Data:** TanStack Query (React Query) v5
16-
- **Forms:** React Hook Form + Zod
17-
- **Backend:** Firebase Firestore
18-
- **Roteamento:** React Router v7
9+
## Stack
1910

20-
## ✨ Diferenciais Técnicos
21-
- **Optimistic Updates:** Feedback instantâneo na UI ao cadastrar, sem esperar resposta do servidor.
22-
- **Offline-First:** Persistência nativa do Firestore (IndexedDB) + Fallback em LocalStorage para garantir que dados nunca se percam.
23-
- **Draft Persistence:** Rascunho automático do formulário no LocalStorage (evita perda de dados ao atualizar a página).
24-
- **Skeleton Loading:** Transições fluidas e sem saltos de layout durante o carregamento inicial.
11+
- React 19 + TypeScript + Vite
12+
- Material UI v7 + Emotion
13+
- TanStack Query v5
14+
- React Hook Form + Zod
15+
- Firebase Firestore
16+
- React Router v7
2517

2618
---
2719

28-
## 📦 Como Rodar
20+
## Como rodar
21+
22+
**Localmente**
23+
24+
```bash
25+
npm install
26+
# configure o .env com base no .env.example
27+
npm run dev
28+
# http://localhost:5173
29+
```
2930

30-
### Localmente
31-
1. Instale as dependências: `npm install`
32-
2. Configure o `.env` (baseie-se no `.env.example`)
33-
3. Inicie o dev: `npm run dev` (disponível em `http://localhost:5173`)
31+
**Com Docker**
3432

35-
### Com Docker
3633
```bash
3734
docker compose up --build
35+
# http://localhost:3000
3836
```
39-
Disponível em `http://localhost:3000`.
4037

4138
---
4239

43-
## 🧪 Testes
44-
O projeto conta com uma suíte de testes robusta:
45-
- **Unitários (Vitest):** Lógica de negócio, validações e hooks.
46-
- `npm run test`
47-
- **E2E (Playwright):** Fluxo real de usuário e integração.
48-
- `npm run test:e2e`
40+
## Testes
41+
42+
```bash
43+
npm run test # unitários (Vitest)
44+
npm run test:e2e # E2E (Playwright)
45+
```
4946

5047
---
5148

52-
## ⚙️ Configuração do Firebase
53-
1. Crie um projeto no Firebase Console.
54-
2. Ative o **Firestore Database** em modo de teste.
55-
3. Obtenha as chaves web e configure no seu `.env`.
49+
## Firebase
50+
51+
1. Crie um projeto no Firebase Console
52+
2. Ative o Firestore em modo de teste
53+
3. Copie as chaves web para o `.env`
54+
55+
As regras de segurança estão em `firestore.rules`. Para publicar:
5656

57-
### Regras do Firestore
58-
As regras estão no arquivo `firestore.rules`. Para deploy:
5957
```bash
6058
npx firebase-tools deploy --only firestore:rules --project SEU_ID
6159
```

src/components/staff-list.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ const columns: { id: keyof Staff; label: string; align?: TableCellProps['align']
3737
{ id: 'status', label: 'Status', align: 'center' },
3838
]
3939

40-
/**
41-
* Componente de Listagem de Colaboradores.
42-
* Focado em legibilidade, performance e feedback de sincronização.
43-
*/
4440
export function StaffList() {
4541
const theme = useTheme()
4642
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
@@ -50,7 +46,6 @@ export function StaffList() {
5046
const { order, orderBy, createSortHandler } = useSortTable('name', setPage)
5147
const { pendingCount, sync } = useSyncPending()
5248

53-
// Sincronização automática ao carregar a página se houver itens offline
5449
useEffect(() => {
5550
if (pendingCount > 0) {
5651
sync().catch(console.error)
@@ -62,7 +57,6 @@ export function StaffList() {
6257

6358
return (
6459
<Box sx={{ p: isMobile ? 1 : 2 }}>
65-
{/* Cabeçalho da Página */}
6660
<Stack
6761
direction={{ xs: 'column', sm: 'row' }}
6862
alignItems={{ xs: 'stretch', sm: 'center' }}
@@ -97,7 +91,6 @@ export function StaffList() {
9791
</Button>
9892
</Stack>
9993

100-
{/* Tabela de Dados */}
10194
<TableContainer
10295
component={Paper}
10396
elevation={0}
@@ -145,7 +138,6 @@ export function StaffList() {
145138
</TableRow>
146139
</TableHead>
147140
<TableBody>
148-
{/* Estado de Carregamento (Skeleton) */}
149141
{isLoading &&
150142
Array.from(new Array(5)).map((_, index) => (
151143
<TableRow key={index}>
@@ -167,7 +159,6 @@ export function StaffList() {
167159
</TableRow>
168160
))}
169161

170-
{/* Estado de Erro */}
171162
{isError && (
172163
<TableRow>
173164
<TableCell colSpan={4} align="center" sx={{ py: 8 }}>
@@ -181,7 +172,6 @@ export function StaffList() {
181172
</TableRow>
182173
)}
183174

184-
{/* Lista Vazia */}
185175
{!isLoading && !isError && !sorted.length && (
186176
<TableRow>
187177
<TableCell colSpan={4} align="center" sx={{ py: 10 }}>
@@ -195,7 +185,6 @@ export function StaffList() {
195185
</TableRow>
196186
)}
197187

198-
{/* Dados da Tabela */}
199188
{paginated.map((row) => (
200189
<TableRow key={row.id} hover sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
201190
<TableCell>
@@ -237,7 +226,7 @@ export function StaffList() {
237226
</TableCell>
238227
<TableCell align="center">
239228
{row._pendingSync ? (
240-
<Tooltip title="Salvo localmente, aguardando conexão para enviar ao servidor">
229+
<Tooltip title="Salvo localmente, aguardando conexão">
241230
<Chip
242231
icon={<SyncIcon sx={{ fontSize: 14 }} />}
243232
label="Sincronizando"
@@ -267,7 +256,6 @@ export function StaffList() {
267256
</TableBody>
268257
</Table>
269258

270-
{/* Paginação */}
271259
<TablePagination
272260
component="div"
273261
count={sorted.length}

src/features/staff/use-staff-form.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('useStaffForm', () => {
6060
})
6161
})
6262

63-
describe('handleNext — validação de e-mail duplicado', () => {
63+
describe('handleNext', () => {
6464
it('bloqueia avanço e seta erro quando e-mail já existe', async () => {
6565
vi.mocked(staffHooks.useStaffs).mockReturnValue({
6666
data: [{ id: '1', email: 'ana@empresa.com', name: 'Ana', department: 'TI', status: 'ACTIVE' }],

src/features/staff/use-staff-form.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ const STEP_FIELDS: Array<Array<keyof StaffSchema>> = [
1111
['department'],
1212
]
1313

14-
/**
15-
* Hook customizado para gerenciar o formulário de cadastro de funcionários.
16-
* Segue boas práticas de persistência de rascunho e validação multi-etapas.
17-
*/
1814
export function useStaffForm() {
1915
const navigate = useNavigate()
2016
const [activeStep, setActiveStep] = useState(0)
@@ -26,7 +22,6 @@ export function useStaffForm() {
2622

2723
const draftKey = 'staff_form_draft'
2824

29-
// Inicialização de valores padrão com suporte a rascunho (LocalStorage)
3025
const defaultValues = useMemo(() => {
3126
try {
3227
const savedDraft = localStorage.getItem(draftKey)
@@ -49,7 +44,6 @@ export function useStaffForm() {
4944
defaultValues,
5045
})
5146

52-
// Sincronização automática do rascunho enquanto o usuário digita
5347
const formValues = form.watch()
5448
useEffect(() => {
5549
if (!submittedRef.current) {
@@ -61,21 +55,18 @@ export function useStaffForm() {
6155
try {
6256
const result = await createStaff(data)
6357

64-
// Limpeza imediata do rascunho após sucesso ou salvamento local (offline)
6558
submittedRef.current = true
6659
localStorage.removeItem(draftKey)
6760

6861
if (result.synced) {
6962
setToast({ message: 'Colaborador cadastrado com sucesso!', severity: 'success' })
7063
} else {
71-
// Feedback visual amigável para o modo offline
7264
setToast({
7365
message: `Salvo no dispositivo. Será enviado quando houver internet.`,
7466
severity: 'success'
7567
})
7668
}
7769

78-
// Pequeno atraso para o usuário ver o feedback de sucesso antes de mudar de página
7970
setTimeout(() => navigate('/staffs'), 1500)
8071
} catch (err: unknown) {
8172
setToast({
@@ -88,7 +79,6 @@ export function useStaffForm() {
8879
const handleNext = async () => {
8980
const fields = STEP_FIELDS[activeStep]
9081

91-
// Validação de e-mail duplicado (UX Preventiva)
9282
if (activeStep === 0) {
9383
const email = form.getValues('email').trim().toLowerCase()
9484
const isDuplicate = staffs?.some(s => s.email.trim().toLowerCase() === email)
@@ -105,7 +95,6 @@ export function useStaffForm() {
10595
const isValid = await form.trigger(fields)
10696
if (!isValid) return false
10797

108-
// Se for o último passo, submete. Senão, avança.
10998
if (activeStep === STEPS.length - 1) {
11099
await form.handleSubmit(onSubmit)()
111100
} else {
@@ -131,7 +120,6 @@ export function useStaffForm() {
131120
setToast,
132121
handleNext,
133122
handleBack,
134-
// Cálculo do progresso visual
135123
currentProgress: (activeStep / STEPS.length) * 100,
136124
}
137125
}

src/libs/firebase.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { initializeApp, getApps, getApp, type FirebaseApp } from 'firebase/app'
22
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager, type Firestore } from 'firebase/firestore'
33
import { z } from 'zod'
44

5-
// 1. Schema de Validação de Ambiente
65
const envSchema = z.object({
76
VITE_FIREBASE_API_KEY: z.string().min(1),
87
VITE_FIREBASE_PROJECT_ID: z.string().min(1),
@@ -31,10 +30,8 @@ const parseEnv = () => {
3130

3231
const env = parseEnv()
3332

34-
// 2. Exportação do Status de Configuração
3533
export const isFirebaseConfigured = !!env
3634

37-
// 3. Inicialização Segura (Fallback para Mocks se não configurado)
3835
let app: FirebaseApp
3936
let db: Firestore
4037

src/services/staffs.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import { FirebaseStorage, LocalOnlyStorage, type StaffStorage } from './storage-
33
import type { Staff } from '@/features/staff/types'
44
import type { StaffSchema } from '@/features/staff/validation'
55

6-
// 4. Seleção Dinâmica do Provider
76
const storage: StaffStorage = isFirebaseConfigured ? FirebaseStorage : LocalOnlyStorage
87

9-
// 5. API Pública do Serviço (Mantendo compatibilidade)
108
export async function listStaffs(): Promise<Staff[]> {
119
return storage.list()
1210
}

src/services/storage-provider.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { addPendingStaff, getPendingStaffs, removePendingByEmail } from '@/servi
44
import type { Staff } from '@/features/staff/types'
55
import type { StaffSchema } from '@/features/staff/validation'
66

7-
// 1. Definição da Interface do Storage
87
export interface StaffStorage {
98
list(): Promise<Staff[]>;
109
create(data: StaffSchema): Promise<{ synced: boolean; error?: string }>;
@@ -13,15 +12,13 @@ export interface StaffStorage {
1312
delete(id: string): Promise<void>;
1413
}
1514

16-
// Utilitário de Timeout
1715
function withTimeout<T>(promise: Promise<T>, ms = 30000): Promise<T> {
1816
const timeout = new Promise<never>((_, reject) =>
19-
setTimeout(() => reject(new Error('Timeout: Banco de dados não respondeu.')), ms)
17+
setTimeout(() => reject(new Error('Timeout de conexão com o banco.')), ms)
2018
)
2119
return Promise.race([promise, timeout])
2220
}
2321

24-
// Utilitário de Log Remoto
2522
async function logRemoteError(context: string, error: unknown) {
2623
if (!isFirebaseConfigured) return
2724
try {
@@ -38,7 +35,6 @@ async function logRemoteError(context: string, error: unknown) {
3835
}
3936
}
4037

41-
// 2. Implementação Firebase (Online)
4238
export const FirebaseStorage: StaffStorage = {
4339
async list(): Promise<Staff[]> {
4440
const pending = getPendingStaffs()
@@ -66,7 +62,7 @@ export const FirebaseStorage: StaffStorage = {
6662
async create(data: StaffSchema): Promise<{ synced: boolean; error?: string }> {
6763
if (!isFirebaseConfigured) {
6864
addPendingStaff(data)
69-
return { synced: false, error: 'Firebase não configurado' }
65+
return { synced: false, error: 'Firebase offline' }
7066
}
7167

7268
try {
@@ -120,22 +116,21 @@ export const FirebaseStorage: StaffStorage = {
120116
}
121117
}
122118

123-
// 3. Implementação Local (Offline Fallback Permanente)
124119
export const LocalOnlyStorage: StaffStorage = {
125120
async list(): Promise<Staff[]> {
126121
return getPendingStaffs()
127122
},
128123
async create(data: StaffSchema): Promise<{ synced: boolean; error?: string }> {
129124
addPendingStaff(data)
130-
return { synced: false, error: 'Modo Offline' }
125+
return { synced: false, error: 'Offline' }
131126
},
132127
async sync(): Promise<boolean> {
133128
return false
134129
},
135130
async update(): Promise<void> {
136-
console.warn('Update not supported in LocalOnly mode')
131+
console.warn('Update local not supported')
137132
},
138133
async delete(): Promise<void> {
139-
console.warn('Delete not supported in LocalOnly mode')
134+
console.warn('Delete local not supported')
140135
}
141136
}

tests/e2e/smoke.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ test.describe('Smoke', () => {
2020
await expect(page.getByLabel('E-mail')).toBeVisible()
2121
})
2222

23-
test('botão Voltar no formulário retorna para a lista', async ({ page }) => {
23+
test('botão Cancelar no formulário retorna para a lista', async ({ page }) => {
2424
await page.goto('/staffs/new', { waitUntil: 'domcontentloaded' })
2525
await expect(page.getByText('Informações Básicas')).toBeVisible({ timeout: 20000 })
26-
await page.getByRole('button', { name: /Voltar/i }).click()
26+
await page.getByRole('button', { name: /Cancelar/i }).click()
2727
await expect(page).toHaveURL(/\/staffs/, { timeout: 10000 })
2828
})
2929

0 commit comments

Comments
 (0)