Skip to content
Merged
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
56 changes: 56 additions & 0 deletions src/app/dashboard/planos/planos-client.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,62 @@ describe('PlanosClient', () => {
expect(screen.queryAllByTestId('card')).toHaveLength(0);
});

it('opens form in edit mode when "Editar" is clicked', () => {
render(<PlanosClient initialPlanos={mockPlanos} />);
const editButtons = screen.getAllByText('P');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: screen.getAllByText('P') queries for elements containing the text "P" — this likely matches avatar fallback initials rather than the "Editar" button the test title describes. If the component renders edit buttons with a pencil icon or "Editar" label, this query will either click the wrong element or break if the avatar text changes. Consider using screen.getAllByText('Editar') or a role-based query (getByRole('button', { name: /editar/i })) to target the intended edit action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/dashboard/planos/planos-client.test.tsx, line 211:

<comment>`screen.getAllByText('P')` queries for elements containing the text "P" — this likely matches avatar fallback initials rather than the "Editar" button the test title describes. If the component renders edit buttons with a pencil icon or "Editar" label, this query will either click the wrong element or break if the avatar text changes. Consider using `screen.getAllByText('Editar')` or a role-based query (`getByRole('button', { name: /editar/i })`) to target the intended edit action.</comment>

<file context>
@@ -206,6 +206,62 @@ describe('PlanosClient', () => {
 
+  it('opens form in edit mode when "Editar" is clicked', () => {
+    render(<PlanosClient initialPlanos={mockPlanos} />);
+    const editButtons = screen.getAllByText('P');
+    fireEvent.click(editButtons[0]);
+    expect(screen.getByTestId('form-plano')).toBeTruthy();
</file context>

fireEvent.click(editButtons[0]);
expect(screen.getByTestId('form-plano')).toBeTruthy();
expect(screen.getByText('Editando')).toBeTruthy();
});

it('calls updatePlanoAction on edit submit', async () => {
const { updatePlanoAction } = await import('@/lib/actions/planos');
vi.mocked(updatePlanoAction).mockResolvedValue({
success: true,
data: { id: 'plano-1', nome: 'Mensal', preco: 100, duracaoDias: 30 },
});

render(<PlanosClient initialPlanos={mockPlanos} />);
fireEvent.click(screen.getAllByText('P')[0]);
fireEvent.click(screen.getByTestId('submit-plano'));

await waitFor(() => {
expect(updatePlanoAction).toHaveBeenCalled();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The updatePlanoAction edit-submit test passes with only a bare toHaveBeenCalled() — it doesn't assert which arguments were passed. The existing createPlanoAction test in this same file properly verifies both the action arguments and router.refresh(). Without these assertions, this test would continue passing if the wrong plano ID were sent or if the success callback stopped refreshing the page, masking a real regression.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/dashboard/planos/planos-client.test.tsx, line 229:

<comment>The `updatePlanoAction` edit-submit test passes with only a bare `toHaveBeenCalled()` — it doesn't assert which arguments were passed. The existing `createPlanoAction` test in this same file properly verifies both the action arguments and `router.refresh()`. Without these assertions, this test would continue passing if the wrong plano ID were sent or if the success callback stopped refreshing the page, masking a real regression.</comment>

<file context>
@@ -206,6 +206,62 @@ describe('PlanosClient', () => {
+    fireEvent.click(screen.getByTestId('submit-plano'));
+
+    await waitFor(() => {
+      expect(updatePlanoAction).toHaveBeenCalled();
+      expect(mockNotify.success).toHaveBeenCalledWith('Plano atualizado!', expect.any(String));
+    });
</file context>

expect(mockNotify.success).toHaveBeenCalledWith('Plano atualizado!', expect.any(String));
});
});
Comment on lines +209 to +232

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect mockPlanos fixture to confirm whether any plano.nome is literally "P"
rg -n -B2 -A20 "mockPlanos" src/app/dashboard/planos/planos-client.test.tsx | head -80

Repository: EmiyaKiritsugu3/PWeb_Project

Length of output: 3499


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant component markup for the edit button text.
FILE="src/app/dashboard/planos/planos-client.tsx"
wc -l "$FILE"
sed -n '1,260p' "$FILE" | nl -ba | sed -n '1,220p'

Repository: EmiyaKiritsugu3/PWeb_Project

Length of output: 253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/app/dashboard/planos/planos-client.tsx"

rg -n -A8 -B8 'Editar|Excluir|form-plano|submit-plano' "$FILE"

Repository: EmiyaKiritsugu3/PWeb_Project

Length of output: 2813


Use the actual edit label here
screen.getAllByText('P') doesn’t match the rendered Editar button, so both tests can fail or click the wrong element. Switch these queries to Editar (or a role/name query) for the edit action.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/dashboard/planos/planos-client.test.tsx` around lines 209 - 232, The
edit-action tests are querying the wrong text, so they may miss the actual
control rendered by PlanosClient. Update the selectors in the tests to target
the real edit label used by the UI, using the “Editar” text or a role/name query
instead of “P”. Keep the assertions around opening the form and calling
updatePlanoAction the same, just fix the element lookup in the PlanosClient test
cases.


it('shows error on delete failure', async () => {
const { deletePlanoAction } = await import('@/lib/actions/planos');
vi.mocked(deletePlanoAction).mockResolvedValue({ success: false, error: 'Erro ao excluir' });

render(<PlanosClient initialPlanos={mockPlanos} />);
fireEvent.click(screen.getAllByText('Excluir')[0]);
fireEvent.click(screen.getAllByText('Excluir')[screen.getAllByText('Excluir').length - 1]);

await waitFor(() => {
expect(mockNotify.error).toHaveBeenCalledWith('Erro ao excluir', 'Erro ao excluir');
});
});

it('displays "mês" badge when duration is exactly 30 days', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Custom agent: Enforce Pragmatic Test Coverage

The new test 'displays "mês" badge when duration is exactly 30 days' duplicates coverage already provided by the existing 'displays formatted duration badges' test. Since mockPlanos already includes a plano with duracaoDias: 30 (plano-1), the existing test already verifies that a 30-day duration renders the 'mês' badge. This test adds no new behavioral coverage. Consider removing it to keep the test suite focused, or if you want explicit documentation of the 30-day → 'mês' mapping, consider merging it into a parameterized test alongside the other duration cases.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/dashboard/planos/planos-client.test.tsx, line 247:

<comment>The new test 'displays "mês" badge when duration is exactly 30 days' duplicates coverage already provided by the existing 'displays formatted duration badges' test. Since mockPlanos already includes a plano with duracaoDias: 30 (plano-1), the existing test already verifies that a 30-day duration renders the 'mês' badge. This test adds no new behavioral coverage. Consider removing it to keep the test suite focused, or if you want explicit documentation of the 30-day → 'mês' mapping, consider merging it into a parameterized test alongside the other duration cases.</comment>

<file context>
@@ -206,6 +206,62 @@ describe('PlanosClient', () => {
+    });
+  });
+
+  it('displays "mês" badge when duration is exactly 30 days', () => {
+    const customPlanos: Plano[] = [{ id: 'p2', nome: 'Mensal', preco: 100, duracaoDias: 30 }];
+    render(<PlanosClient initialPlanos={customPlanos} />);
</file context>

const customPlanos: Plano[] = [{ id: 'p2', nome: 'Mensal', preco: 100, duracaoDias: 30 }];
render(<PlanosClient initialPlanos={customPlanos} />);
expect(screen.getByText('mês')).toBeTruthy();
});

it('displays "2 meses" badge for 60-day duration', () => {
const customPlanos: Plano[] = [{ id: 'p3', nome: 'Bimestral', preco: 180, duracaoDias: 60 }];
render(<PlanosClient initialPlanos={customPlanos} />);
expect(screen.getByText('2 meses')).toBeTruthy();
});

it('displays "dias" badge when duration is not a multiple of 30', () => {
const customPlanos: Plano[] = [{ id: 'p1', nome: 'Personal', preco: 200, duracaoDias: 15 }];
render(<PlanosClient initialPlanos={customPlanos} />);
expect(screen.getByText('15 dias')).toBeTruthy();
});

it('calls deletePlanoAction when delete is confirmed', async () => {
const { deletePlanoAction } = await import('@/lib/actions/planos');
vi.mocked(deletePlanoAction).mockResolvedValue({ success: true });
Expand Down
95 changes: 93 additions & 2 deletions src/app/login/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import LoginPage from './page';
import type { ReactNode } from 'react';

Expand All @@ -15,6 +15,18 @@ vi.mock('react', async () => {
};
});

const mockSignInWithPassword = vi.fn();
const mockGetSession = vi.fn();

vi.mock('@/utils/supabase/client', () => ({
createClient: () => ({
auth: {
signInWithPassword: mockSignInWithPassword,
getSession: mockGetSession,
},
}),
}));

vi.mock('next/link', () => ({
default: ({ children, href }: { children: ReactNode; href: string }) => (
<a href={href}>{children}</a>
Expand Down Expand Up @@ -72,6 +84,10 @@ vi.mock('@/components/ui/separator', () => ({
}));

describe('LoginPage', () => {
beforeEach(() => {
vi.clearAllMocks();
});

Comment on lines +87 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## src/app/login/page.test.tsx (outline)\n'
ast-grep outline src/app/login/page.test.tsx --view expanded || true

printf '\n## src/app/login/page.tsx (outline)\n'
ast-grep outline src/app/login/page.tsx --view expanded || true

printf '\n## Relevant test file excerpt\n'
sed -n '1,260p' src/app/login/page.test.tsx | cat -n

printf '\n## Relevant component excerpt\n'
sed -n '1,260p' src/app/login/page.tsx | cat -n

Repository: EmiyaKiritsugu3/PWeb_Project

Length of output: 14872


Stub mockGetSession here and wait for the submit flow to finish. vi.clearAllMocks() doesn’t reset the implementation set in the previous test, so this case only works when the suite runs in order. After resolveAuth({ error: null }), let the async handler settle before the test exits so the /dashboard navigation doesn’t race teardown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/login/page.test.tsx` around lines 87 - 90, The login page test setup
is leaking the previous `mockGetSession` implementation because
`vi.clearAllMocks()` only clears calls, not behavior. Stub `mockGetSession`
explicitly in this case and, in the submit flow around `resolveAuth({ error:
null })`, await the async handler to settle before the test ends so the
`/dashboard` navigation completes without racing teardown.

it('renders the login form', () => {
render(<LoginPage />);
expect(screen.getByText('Five Star')).toBeTruthy();
Expand All @@ -98,4 +114,79 @@ describe('LoginPage', () => {
render(<LoginPage />);
expect(screen.getByText((content) => content.includes('Five Star Fitness'))).toBeTruthy();
});

it('shows error on invalid credentials', async () => {
mockSignInWithPassword.mockResolvedValue({
error: new Error('Invalid login credentials'),
});

render(<LoginPage />);
fireEvent.change(screen.getByLabelText('Email corporativo'), {
target: { value: 'wrong@test.com' },
});
fireEvent.change(screen.getByLabelText('Senha de acesso'), {
target: { value: 'wrong' },
});
fireEvent.submit(screen.getByLabelText('Email corporativo').closest('form')!);

await waitFor(() => {
expect(
screen.getByText('E-mail ou senha inválidos. Por favor, tente novamente.')
).toBeTruthy();
expect(screen.getByText('Entrar no Sistema')).toBeTruthy();
});
});

it('navigates to dashboard on successful login', async () => {
mockSignInWithPassword.mockResolvedValue({ error: null });
mockGetSession.mockResolvedValue({
data: { session: { user: { id: 'u1' } } },
});

const originalLocation = window.location;
Object.defineProperty(window, 'location', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The window.location restoration via Object.defineProperty at the end of the success test is not resilient to assertion failures. If the waitFor times out (e.g., in slow CI or if mock behavior changes), the test throws before reaching the restoration line, leaving window.location as a plain object for all subsequent tests in this file. Consider wrapping the assertion and restoration in a try/finally block or moving the restoration to an afterEach hook.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/login/page.test.tsx, line 147:

<comment>The `window.location` restoration via `Object.defineProperty` at the end of the success test is not resilient to assertion failures. If the `waitFor` times out (e.g., in slow CI or if mock behavior changes), the test throws before reaching the restoration line, leaving `window.location` as a plain object for all subsequent tests in this file. Consider wrapping the assertion and restoration in a `try/finally` block or moving the restoration to an `afterEach` hook.</comment>

<file context>
@@ -98,4 +114,79 @@ describe('LoginPage', () => {
+    });
+
+    const originalLocation = window.location;
+    Object.defineProperty(window, 'location', {
+      value: { ...originalLocation, href: '' },
+      writable: true,
</file context>

value: { ...originalLocation, href: '' },
writable: true,
});

render(<LoginPage />);
fireEvent.change(screen.getByLabelText('Email corporativo'), {
target: { value: 'admin@test.com' },
});
fireEvent.change(screen.getByLabelText('Senha de acesso'), {
target: { value: 'correct' },
});
fireEvent.submit(screen.getByLabelText('Email corporativo').closest('form')!);

await waitFor(() => {
expect(window.location.href).toBe('/dashboard');
});

Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
});
});

it('shows "Autenticando..." while submitting', async () => {
let resolveAuth!: (v: { error: null | Error }) => void;
mockSignInWithPassword.mockImplementation(
() =>
new Promise((resolve) => {
resolveAuth = resolve;
})
);

render(<LoginPage />);
fireEvent.submit(screen.getByLabelText('Email corporativo').closest('form')!);

await waitFor(() => {
expect(screen.getByText('Autenticando...')).toBeTruthy();
});

Comment on lines +171 to +186

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: 'Autenticando...' test relies on mockGetSession implementation leaking from the previous test — vi.clearAllMocks() resets call history but not mock implementations, so the session polling loop after sign-in silently resolves using the neighbor test's configuration. Add mockGetSession.mockResolvedValue(...) or mockResolvedValueOnce(...) inside this test to make it self-contained.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/login/page.test.tsx, line 171:

<comment>'Autenticando...' test relies on `mockGetSession` implementation leaking from the previous test — `vi.clearAllMocks()` resets call history but not mock implementations, so the session polling loop after sign-in silently resolves using the neighbor test's configuration. Add `mockGetSession.mockResolvedValue(...)` or `mockResolvedValueOnce(...)` inside this test to make it self-contained.</comment>

<file context>
@@ -98,4 +114,79 @@ describe('LoginPage', () => {
+    });
+  });
+
+  it('shows "Autenticando..." while submitting', async () => {
+    let resolveAuth!: (v: { error: null | Error }) => void;
+    mockSignInWithPassword.mockImplementation(
</file context>
Suggested change
it('shows "Autenticando..." while submitting', async () => {
let resolveAuth!: (v: { error: null | Error }) => void;
mockSignInWithPassword.mockImplementation(
() =>
new Promise((resolve) => {
resolveAuth = resolve;
})
);
render(<LoginPage />);
fireEvent.submit(screen.getByLabelText('Email corporativo').closest('form')!);
await waitFor(() => {
expect(screen.getByText('Autenticando...')).toBeTruthy();
});
it('shows "Autenticando..." while submitting', async () => {
let resolveAuth!: (v: { error: null | Error }) => void;
mockSignInWithPassword.mockImplementation(
() =>
new Promise((resolve) => {
resolveAuth = resolve;
})
);
mockGetSession.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
render(<LoginPage />);
fireEvent.submit(screen.getByLabelText('Email corporativo').closest('form')!);
await waitFor(() => {
expect(screen.getByText('Autenticando...')).toBeTruthy();
});
resolveAuth({ error: null });
});

resolveAuth({ error: null });
});

// ponytail: session poll timeout path tested implicitly by success test.
// Poll loop is UX fallback (10×500ms) — would timeout vitest's 5s. Skip.
});
39 changes: 38 additions & 1 deletion src/components/dashboard/alunos/columns.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ vi.mock('@/components/ui/avatar', () => ({
Avatar: ({ children }: { children?: React.ReactNode }) => (
<div data-testid="avatar">{children}</div>
),
AvatarFallback: ({ children }: { children?: React.ReactNode }) => <span>{children}</span>,
AvatarFallback: ({ children }: { children?: React.ReactNode }) => (
<span data-testid="avatar-fallback">{children}</span>
),
AvatarImage: () => null,
}));

Expand Down Expand Up @@ -142,6 +144,28 @@ describe('columns', () => {
}
});

it('avatar shows empty for empty name', () => {
const cell = columnDefs[0].cell;
if (typeof cell === 'function') {
const { container } = render(
cell({ row: { original: { ...mockAluno, nomeCompleto: '' } } } as never)
);
const fallback = container.querySelector('[data-testid="avatar-fallback"]');
expect(fallback?.textContent).toBe('');
}
});

it('avatar shows first 2 chars for single-word name', () => {
const cell = columnDefs[0].cell;
if (typeof cell === 'function') {
const { container } = render(
cell({ row: { original: { ...mockAluno, nomeCompleto: 'João' } } } as never)
);
const fallback = container.querySelector('[data-testid="avatar-fallback"]');
expect(fallback?.textContent).toBe('JO');
}
});

it('renders name and email in name column', () => {
const cell = columnDefs[1].cell;
if (typeof cell === 'function') {
Expand All @@ -157,6 +181,19 @@ describe('columns', () => {
}
});

it('renders null for empty date', () => {
const cell = columnDefs[3].cell;
if (typeof cell === 'function') {
const result = cell({
row: {
original: { ...mockAluno, dataCadastro: '' },
getValue: () => '',
},
} as never);
expect(result).toBeNull();
}
});

it('renders formatted date in date column', () => {
const cell = columnDefs[3].cell;
if (typeof cell === 'function') {
Expand Down
46 changes: 44 additions & 2 deletions src/components/providers/auth-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, act } from '@testing-library/react';
import { SupabaseAuthProvider, useAuth } from './auth-provider';
import * as Sentry from '@sentry/nextjs';

let authStateCallback: ((event: string, session: unknown) => void) | null = null;

const mockGetUser = vi.fn();
const mockOnAuthStateChange = vi.fn();
Expand Down Expand Up @@ -35,9 +38,11 @@ function TestConsumer() {
describe('SupabaseAuthProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
authStateCallback = null;
mockGetUser.mockResolvedValue({ data: { user: null }, error: null });
mockOnAuthStateChange.mockReturnValue({
data: { subscription: { unsubscribe: vi.fn() } },
mockOnAuthStateChange.mockImplementation((cb: (event: string, session: unknown) => void) => {
authStateCallback = cb;
return { data: { subscription: { unsubscribe: vi.fn() } } };
});
});

Expand Down Expand Up @@ -116,6 +121,43 @@ describe('SupabaseAuthProvider', () => {
});
expect(mockOnAuthStateChange).toHaveBeenCalled();
});

it('calls Sentry.setUser on auth state change with user', async () => {
await act(async () => {
render(
<SupabaseAuthProvider>
<TestConsumer />
</SupabaseAuthProvider>
);
});

await act(async () => {
authStateCallback?.('SIGNED_IN', {
user: { id: 'u1', email: 'user@test.com' },
});
});

expect(Sentry.setUser).toHaveBeenCalledWith({
id: 'u1',
email: 'user@test.com',
});
});

it('calls Sentry.setUser(null) on auth state change without session', async () => {
await act(async () => {
render(
<SupabaseAuthProvider>
<TestConsumer />
</SupabaseAuthProvider>
);
});

await act(async () => {
authStateCallback?.('SIGNED_OUT', null);
});

expect(Sentry.setUser).toHaveBeenCalledWith(null);
});
});

describe('useAuth', () => {
Expand Down
Loading