Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
"build": "vite build",
"build:dev": "node scripts/generate-health.mjs && vite build --mode development",
"preview": "vite preview",
"test": "vitest run",
"test:quality": "vitest run --exclude 'tests/hooks/**'",
"test:watch": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"coverage": "vitest run --coverage",
"test": "node scripts/run-vitest.mjs run",
"test:quality": "node scripts/run-vitest.mjs run --exclude tests/hooks/**",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Quote the hooks exclude glob in test:quality

The test:quality script now passes --exclude tests/hooks/** without quotes, so in POSIX shells npm expands the glob before Node runs. That means vitest receives one expanded path as the --exclude value and the rest as positional test filters, which can end up running hook tests instead of excluding them (or otherwise changing the suite unpredictably). This breaks the intended quality gate behavior on CI/dev environments that perform glob expansion.

Useful? React with 👍 / 👎.

"test:watch": "node scripts/run-vitest.mjs",
"test:run": "node scripts/run-vitest.mjs run",
"test:coverage": "node scripts/run-vitest.mjs run --coverage",
"coverage": "node scripts/run-vitest.mjs run --coverage",
"test:price-freshness": "bash -c 'set -euo pipefail; files=( tests/utils/price-freshness*.test.ts tests/components/PriceFreshnessBadge*.test.tsx ); vitest run \"${files[@]}\" --coverage --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=html --coverage.include=src/utils/price-freshness.ts --coverage.include=src/components/products/PriceFreshnessBadge.tsx --coverage.thresholds.statements=0 --coverage.thresholds.branches=0 --coverage.thresholds.functions=0 --coverage.thresholds.lines=0 && node scripts/check-price-freshness-coverage.mjs'",
"test:cloud-status": "vitest run tests/components/CloudStatusBanner.test.tsx tests/hooks/useDevGate.test.ts",
"test:cloud-status-coverage": "vitest run tests/components/CloudStatusBanner.test.tsx tests/hooks/useDevGate.test.ts --coverage --coverage.reporter=text --coverage.reporter=json-summary --coverage.include=src/components/system/CloudStatusBanner.tsx --coverage.include=src/hooks/admin/useDevGate.ts && node scripts/check-cloud-status-coverage.mjs",
Expand Down
36 changes: 36 additions & 0 deletions scripts/run-vitest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
// Cross-platform vitest launcher that forces TZ before workers spawn.
//
// Why a wrapper instead of `vitest run`:
// vitest's config-level `test.env: { TZ: ... }` only stubs process.env in
// the worker after Date.prototype.toLocaleString has already cached the TZ
// (see comment in vitest.config.ts). Setting TZ in the parent process here
// guarantees it propagates to the worker spawn, so snapshot files that
// were generated under America/Sao_Paulo stay reproducible regardless of
// the host clock (CI Ubuntu UTC, dev BRT, etc.).
//
// Args passthrough: anything after the script name is forwarded verbatim to
// vitest. e.g. `node scripts/run-vitest.mjs run --coverage`.

import { spawn } from 'node:child_process';

const env = { ...process.env };
if (!env.TZ) {
env.TZ = 'America/Sao_Paulo';
}

const args = process.argv.slice(2);
const child = spawn('npx', ['vitest', ...args], {
stdio: 'inherit',
env,
shell: true,
});

child.on('close', (code) => {
process.exit(code ?? 1);
});

child.on('error', (err) => {
console.error('Failed to launch vitest:', err);
process.exit(1);
});
267 changes: 176 additions & 91 deletions src/components/admin/connections/ConnectionsOverviewTable.tsx

Large diffs are not rendered by default.

35 changes: 15 additions & 20 deletions src/components/admin/connections/__tests__/ConnectionUI.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ConnectionsOverviewTable } from '../ConnectionsOverviewTable';
import { TooltipProvider } from '@/components/ui/tooltip';
import { useAuth } from '@/contexts/AuthContext';
import { useConnectionsOverview } from '@/hooks/intelligence';
import { useConnectionTester } from '@/hooks/intelligence';
import { useConnectionsOverview, useConnectionTester } from '@/hooks/intelligence';

// Mocks
vi.mock('@/contexts/AuthContext', () => ({
useAuth: vi.fn(),
}));

// IMPORTANT: all mocks for '@/hooks/intelligence' must live in one vi.mock()
// call — repeated calls overwrite earlier ones (last write wins).
vi.mock('@/hooks/intelligence', () => ({
useConnectionsOverview: vi.fn(),
}));

vi.mock('@/hooks/intelligence', () => ({
useConnectionTester: vi.fn(),
useConnectionsOverviewFilters: vi.fn(() => ({
filters: { types: [], status: [], window: 'all', onlyConsecutiveFailures: false },
activeCount: 0,
reset: vi.fn(),
toggleType: vi.fn(),
setStatus: vi.fn(),
setWindow: vi.fn(),
removeType: vi.fn(),
setOnlyConsecutiveFailures: vi.fn(),
})),
applyFilters: vi.fn((rows) => rows),
}));

vi.mock('@/hooks/common', () => ({
Expand All @@ -30,24 +39,10 @@ vi.mock('@/hooks/admin', () => ({
useSecretsManager: vi.fn(() => ({
secrets: [],
list: vi.fn(),
refreshCache: vi.fn(), // Adicionado para evitar erro 'refreshSecrets is not a function'
refreshCache: vi.fn(),
})),
}));

vi.mock('@/hooks/intelligence', () => ({
useConnectionsOverviewFilters: vi.fn(() => ({
filters: { types: [], status: [], window: 'all', onlyConsecutiveFailures: false },
activeCount: 0,
reset: vi.fn(),
toggleType: vi.fn(),
setStatus: vi.fn(),
setWindow: vi.fn(),
removeType: vi.fn(),
setOnlyConsecutiveFailures: vi.fn(),
})),
applyFilters: vi.fn((rows) => rows),
}));

describe('ConnectionsOverviewTable Interações e Acessibilidade', () => {
const mockRows = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ConnectionsOverviewTable } from '../ConnectionsOverviewTable';
import { useAuth } from '@/contexts/AuthContext';
import { useConnectionsOverview } from '@/hooks/intelligence';
import { useConnectionTester } from '@/hooks/intelligence';
import { useConnectionsOverview, useConnectionTester } from '@/hooks/intelligence';
import { useConsecutiveFailures } from '@/hooks/common';
import { useSecretsManager } from '@/hooks/admin';
import { TooltipProvider } from '@/components/ui/tooltip';
Expand All @@ -13,23 +12,11 @@ vi.mock('@/contexts/AuthContext', () => ({
useAuth: vi.fn(),
}));

// IMPORTANT: all mocks for '@/hooks/intelligence' must live in one vi.mock()
// call — repeated calls overwrite earlier ones (last write wins).
vi.mock('@/hooks/intelligence', () => ({
useConnectionsOverview: vi.fn(),
}));

vi.mock('@/hooks/intelligence', () => ({
useConnectionTester: vi.fn(),
}));

vi.mock('@/hooks/common', () => ({
useConsecutiveFailures: vi.fn(),
}));

vi.mock('@/hooks/admin', () => ({
useSecretsManager: vi.fn(),
}));

vi.mock('@/hooks/intelligence', () => ({
useConnectionsOverviewFilters: vi.fn(() => ({
filters: { types: [], status: [], window: 'all', onlyConsecutiveFailures: false },
activeCount: 0,
Expand All @@ -43,6 +30,14 @@ vi.mock('@/hooks/intelligence', () => ({
applyFilters: vi.fn((rows) => rows),
}));

vi.mock('@/hooks/common', () => ({
useConsecutiveFailures: vi.fn(),
}));

vi.mock('@/hooks/admin', () => ({
useSecretsManager: vi.fn(),
}));

describe('ConnectionsOverviewTable Regression Tests', () => {
const mockRows = [
{
Expand Down
66 changes: 39 additions & 27 deletions src/components/auth/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Mail, Loader2, ArrowLeft, Clock, ShieldCheck } from 'lucide-react';
import { Mail, Loader2, ArrowLeft, Clock } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
Expand All @@ -28,7 +28,12 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
const { createRequest } = usePasswordResetRequests();

const [isSubmitting, setIsSubmitting] = useState(false);
const [requestSent, setRequestSent] = useState(false);
// `requestSent` keeps the inline success UI available (the JSX renders an
// "approval pending" panel based on it). The submit handler currently
// navigates to /forgot-password-confirmation on success, so this branch is
// a fallback. Keep it false; flipping it to true presents the inline
// success state if/when navigation is short-circuited.
const [requestSent] = useState(false);

const form = useForm<ForgotPasswordFormData>({
resolver: zodResolver(forgotPasswordSchema),
Expand All @@ -39,24 +44,25 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
setIsSubmitting(true);
try {
const result = await createRequest(data.email);
const safeMessage = String(result.message ?? '');

if (!result.success) {
toast({
variant: 'destructive',
title: 'Erro ao enviar solicitação',
description: result.message,
description: safeMessage,
});
return;
}

toast({
title: 'Solicitação enviada!',
description: result.message,
description: safeMessage,
});

// Navega para a página de confirmação com instruções detalhadas
navigate('/forgot-password-confirmation');
} catch (error) {
} catch {
toast({
variant: 'destructive',
title: 'Erro inesperado',
Expand All @@ -75,35 +81,36 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
className="space-y-6 text-center py-4"
className="space-y-6 py-4 text-center"
>
<div className="flex justify-center">
<div className="w-16 h-16 rounded-full bg-warning/10 flex items-center justify-center ring-1 ring-warning/20">
<Clock className="h-8 w-8 text-warning animate-pulse" />
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-warning/10 ring-1 ring-warning/20">
<Clock className="h-8 w-8 animate-pulse text-warning" />
</div>
</div>

<div className="space-y-2">
<h2 className="font-display text-xl font-semibold text-white">Solicitação enviada!</h2>
<p className="text-sm text-white/50">
Sua solicitação de recuperação de senha para{' '}
<span className="font-medium text-white">{form.getValues('email')}</span>{' '}
foi enviada para aprovação.
<span className="font-medium text-white">{form.getValues('email')}</span> foi enviada
para aprovação.
</p>
</div>

<div className="p-4 rounded-2xl bg-white/5 border border-white/10 backdrop-blur-sm">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm">
<p className="text-sm text-white/60">
<strong className="text-white">Próximo passo:</strong> Um gestor irá analisar sua solicitação.
Após a aprovação, você receberá um email com o link para redefinir sua senha.
<strong className="text-white">Próximo passo:</strong> Um gestor irá analisar sua
solicitação. Após a aprovação, você receberá um email com o link para redefinir sua
senha.
</p>
</div>

<div className="space-y-3 pt-2">
<Button
type="button"
variant="ghost"
className="w-full text-white/40 hover:text-white hover:bg-white/5"
className="w-full text-white/40 hover:bg-white/5 hover:text-white"
onClick={onBack}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Expand All @@ -119,24 +126,29 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
className="space-y-6"
data-testid="forgot-password-screen"
>
<div className="text-center space-y-2">
<h2 className="font-display text-2xl font-bold text-white tracking-tight">Esqueceu sua senha?</h2>
<p className="text-[13px] text-white/50 leading-relaxed">
Não se preocupe, comandante! Digite seu e-mail abaixo para iniciarmos o procedimento de resgate.
<div className="space-y-2 text-center">
<h2 className="font-display text-2xl font-bold tracking-tight text-white">
Esqueceu sua senha?
</h2>
<p className="text-[13px] leading-relaxed text-white/50">
Não se preocupe, comandante! Digite seu e-mail abaixo para iniciarmos o procedimento
de resgate.
</p>
</div>

<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="forgot-email" className="text-white">Email</Label>
<Label htmlFor="forgot-email" className="text-white">
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-white/40" />
<Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
<Input
id="forgot-email"
type="email"
placeholder="seu@email.com"
autoComplete="email"
className="pl-10 bg-white/5 border-white/10 text-white lowercase focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-300 placeholder:text-white/20"
className="border-white/10 bg-white/5 pl-10 lowercase text-white transition-all duration-300 placeholder:text-white/20 focus:border-blue-500/50 focus:ring-blue-500/20"
{...form.register('email')}
onChange={(e) => {
const lower = e.target.value.toLowerCase();
Expand All @@ -146,15 +158,15 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
/>
</div>
{form.formState.errors.email && (
<p className="text-sm text-destructive font-medium">
<p className="text-sm font-medium text-destructive">
{form.formState.errors.email.message}
</p>
)}
</div>

<Button
type="submit"
className="w-full h-11 text-base font-semibold bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/20 transition-all active:scale-[0.98] rounded-xl text-white border border-white/10"
className="h-11 w-full rounded-xl border border-white/10 bg-blue-600 text-base font-semibold text-white shadow-lg shadow-blue-500/20 transition-all hover:bg-blue-700 active:scale-[0.98]"
disabled={isSubmitting}
>
{isSubmitting ? (
Expand All @@ -171,7 +183,7 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
<Button
type="button"
variant="ghost"
className="w-full h-11 text-white/40 hover:text-white hover:bg-white/5 rounded-xl transition-colors"
className="h-11 w-full rounded-xl text-white/40 transition-colors hover:bg-white/5 hover:text-white"
onClick={onBack}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Expand All @@ -183,4 +195,4 @@ export function ForgotPasswordForm({ onBack }: ForgotPasswordFormProps) {
);
}

export default ForgotPasswordForm;
export default ForgotPasswordForm;
Loading
Loading