fix(tests+async): corrige test drift pós-Lovable + setState pós-unmount em páginas admin/auth#154
Conversation
…nt em paginas admin/auth 19 correcoes em 16 arquivos. Producao (7 guardas de setState pos-unmount): - useSecurityData, AdminSegurancaAcessoPage: useRef mountedRef + cleanup (polling 30s) - PermissionsPage, RolePermissionsPage, RolesPage, StorageTestPage: guarda isCancelled - Auth.tsx: guarda cancelled em loadInfo (resolvido vs #137) Setup de testes: - tests/setup.ts: stub global no-op de WebSocket (readyState=CLOSED) elimina o erro do undici/Realtime no jsdom Correcao de tipo: - StorageTestPage: onClick={() => fetchFiles()} (TS2322) Testes corrigidos (drift apos edicoes Lovable): - BridgeStatusBanner, DevInfraGateMatrix, DevOnlyBridgeOverlay, MagicUp, ProductSparkline, simulation-orchestrator, quote-calculations, quote-stepper-ui Gate TS: zero regressoes.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Warning Review limit reached
Your plan currently allows 2 reviews/hour. Refill in 7 minutes and 44 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (16)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
2 issues found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/pages/admin/RolesPage.tsx">
<violation number="1" location="src/pages/admin/RolesPage.tsx:41">
P2: The new unmount guard is optional (`() => false` by default), so `fetchRoles()` calls outside `useEffect` still allow post-unmount state updates.</violation>
</file>
<file name="src/components/security/useSecurityData.ts">
<violation number="1" location="src/components/security/useSecurityData.ts:112">
P2: The mounted-ref guard is re-enabled on every effect run, so older in-flight requests can still update state with stale data after dependencies change.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }, []); | ||
|
|
||
| const fetchRoles = async () => { | ||
| const fetchRoles = async (isCancelled: () => boolean = () => false) => { |
There was a problem hiding this comment.
P2: The new unmount guard is optional (() => false by default), so fetchRoles() calls outside useEffect still allow post-unmount state updates.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pages/admin/RolesPage.tsx, line 41:
<comment>The new unmount guard is optional (`() => false` by default), so `fetchRoles()` calls outside `useEffect` still allow post-unmount state updates.</comment>
<file context>
@@ -29,22 +29,30 @@ export default function RolesPage() {
}, []);
- const fetchRoles = async () => {
+ const fetchRoles = async (isCancelled: () => boolean = () => false) => {
try {
const { data, error } = await supabase
</file context>
|
|
||
| useEffect(() => { if (effectiveUserId) loadSecurityData(); }, [effectiveUserId, loadSecurityData]); | ||
| useEffect(() => { | ||
| mountedRef.current = true; |
There was a problem hiding this comment.
P2: The mounted-ref guard is re-enabled on every effect run, so older in-flight requests can still update state with stale data after dependencies change.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/security/useSecurityData.ts, line 112:
<comment>The mounted-ref guard is re-enabled on every effect run, so older in-flight requests can still update state with stale data after dependencies change.</comment>
<file context>
@@ -95,13 +101,20 @@ export function useSecurityData(effectiveUserId: string | undefined, isManagingO
- useEffect(() => { if (effectiveUserId) loadSecurityData(); }, [effectiveUserId, loadSecurityData]);
+ useEffect(() => {
+ mountedRef.current = true;
+ if (effectiveUserId) loadSecurityData();
+ return () => {
</file context>
There was a problem hiding this comment.
Pull request overview
Este PR faz uma auditoria do escopo test:quality (Vitest) e corrige drift de testes pós-mudanças recentes, além de reduzir vazamentos de setState após unmount em páginas/admin hooks com operações assíncronas e polling.
Changes:
- Adiciona guards de cancelamento/mount para evitar
setStateapós unmount em páginas admin/auth e no hook de segurança. - Ajusta setup global de testes com stub no-op de
WebSocketpara impedir conexão Realtime real em ambiente jsdom. - Atualiza vários testes unitários/integrados para refletir comportamento atual (UI stepper, cálculos com
round2, overlay DevOnly strict, mocks de hooks e spy desupabase.functions).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/quote-stepper-ui.test.tsx | Realinha asserts de UI do stepper (classe ring-4) e ordem/semântica do fluxo testado. |
| tests/unit/quote-calculations.test.ts | Ajusta expectativa de arredondamento monetário (2 casas) em calculateItemTotal. |
| tests/setup.ts | Stub global de WebSocket para evitar Realtime/undici em jsdom e reduzir unhandled errors. |
| tests/integration/simulation-orchestrator.test.ts | Captura supabase.functions (getter lazy) e centraliza spy/mocks para invoke. |
| tests/components/products/ProductSparkline.labels.test.tsx | Corrige path do mock do hook para @/hooks/intelligence. |
| tests/components/pages/MagicUp.test.tsx | Corrige assert para marcador real da página (testid do título), não do layout. |
| tests/components/DevOnlyBridgeOverlay.test.tsx | Ajusta testes para semântica de <DevOnly strict> (visibilidade baseada em isDev). |
| tests/components/DevInfraGateMatrix.test.tsx | Atualiza matriz parametrizada para refletir strict (ignora isAllowed). |
| tests/components/BridgeStatusBanner.test.tsx | Remove assertion órfã de texto que não existe mais no componente. |
| src/pages/auth/Auth.tsx | Adiciona flag cancelled no effect de loadInfo para evitar updates após unmount. |
| src/pages/admin/StorageTestPage.tsx | Introduz token de cancelamento no fetch inicial e corrige handler de onClick do botão atualizar. |
| src/pages/admin/RolesPage.tsx | Adiciona token de cancelamento no fetch inicial para evitar setState após unmount. |
| src/pages/admin/RolePermissionsPage.tsx | Adiciona token de cancelamento no fetch inicial (mas há ajuste pendente no tratamento de erro). |
| src/pages/admin/PermissionsPage.tsx | Adiciona token de cancelamento no fetch inicial para evitar setState após unmount. |
| src/pages/admin/AdminSegurancaAcessoPage.tsx | Usa mountedRef para proteger polling/handlers e evitar updates após unmount. |
| src/components/security/useSecurityData.ts | Usa mountedRef para proteger setters durante awaits, reduzindo vazamentos após unmount. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| setRolePermissions(rolePermRes.data || []); | ||
| } catch (error: unknown) { | ||
| if (isCancelled()) return; | ||
| toast({ title: 'Erro ao carregar dados', description: error.message, variant: 'destructive' }); |
| @@ -74,6 +79,7 @@ export function useSecurityData(effectiveUserId: string | undefined, isManagingO | |||
| .eq('user_id', effectiveUserId).eq('type', 'security') | |||
| .order('created_at', { ascending: false }).limit(10); | |||
|
|
|||
| if (!mountedRef.current) return; | |||
| setNotifications((notifs as SecurityNotification[]) || []); | |||
|
|
|||
| const failedAttempts = attempts?.filter(a => !a.success).length || 0; | |||
| @@ -95,13 +101,20 @@ export function useSecurityData(effectiveUserId: string | undefined, isManagingO | |||
| securityAlerts: unreadAlerts, | |||
| }); | |||
| } catch (error) { | |||
| if (!mountedRef.current) return; | |||
| console.error('Error loading security data:', error); | |||
| } finally { | |||
| setIsLoading(false); | |||
| if (mountedRef.current) setIsLoading(false); | |||
| } | |||
| }, [effectiveUserId, is2FAEnabled, allowedIPs]); | |||
|
|
|||
| useEffect(() => { if (effectiveUserId) loadSecurityData(); }, [effectiveUserId, loadSecurityData]); | |||
| useEffect(() => { | |||
| mountedRef.current = true; | |||
| if (effectiveUserId) loadSecurityData(); | |||
| return () => { | |||
| mountedRef.current = false; | |||
| }; | |||
| }, [effectiveUserId, loadSecurityData]); | |||
| describe('Simulation Orchestrator Integration', () => { | ||
| // We mock the fetch for edge function calls if we are in a pure unit test env, | ||
| // but here we try to validate the invocation structure. | ||
|
|
||
| let fns: typeof supabase.functions; | ||
| let invokeSpy: ReturnType<typeof vi.spyOn>; | ||
|
|
||
| beforeAll(() => { | ||
| fns = supabase.functions; | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| invokeSpy = vi | ||
| .spyOn(fns, 'invoke') | ||
| .mockResolvedValue({ data: { ok: true }, error: null } as never); | ||
| }); | ||
|
|
| // Ordem canônica do fluxo (ver useQuoteBuilderState): | ||
| // client -> conditions -> items -> review. | ||
| const steps: QuoteBuilderStep[] = ['client', 'conditions', 'items', 'review']; | ||
|
|
||
| describe('Visualização de Estados', () => { |
| const fetchRoles = async (isCancelled: () => boolean = () => false) => { | ||
| try { | ||
| const { data, error } = await supabase | ||
| .from('roles') | ||
| .select('*') | ||
| .order('name'); | ||
|
|
||
| if (isCancelled()) return; | ||
| if (error) throw error; | ||
| setRoles(data || []); | ||
| } catch (error: unknown) { | ||
| if (isCancelled()) return; | ||
| toast({ title: 'Erro', description: error instanceof Error ? error.message : String(error), variant: 'destructive' }); | ||
| } finally { | ||
| setIsLoading(false); | ||
| if (!isCancelled()) setIsLoading(false); | ||
| } |
| const fetchPermissions = async (isCancelled: () => boolean = () => false) => { | ||
| try { | ||
| const { data, error } = await supabase | ||
| .from('permissions') | ||
| .select('*') | ||
| .order('category', { ascending: true }); | ||
|
|
||
| if (isCancelled()) return; | ||
| if (error) throw error; | ||
| setPermissions(data || []); | ||
| } catch (error: unknown) { | ||
| if (isCancelled()) return; | ||
| toast({ title: 'Erro', description: error instanceof Error ? error.message : String(error), variant: 'destructive' }); | ||
| } finally { | ||
| setIsLoading(false); | ||
| if (!isCancelled()) setIsLoading(false); | ||
| } |
| @@ -36,19 +37,25 @@ export default function StorageTestPage() { | |||
| } | |||
| setFiles(data || []); | |||
| } catch (error: any) { | |||
| if (isCancelled()) return; | |||
| console.error("Error fetching files:", error); | |||
| toast({ | |||
| title: "Erro ao buscar arquivos", | |||
| description: error.message, | |||
| variant: "destructive", | |||
| }); | |||
| } finally { | |||
| setLoadingFiles(false); | |||
| if (!isCancelled()) setLoadingFiles(false); | |||
| } | |||
Triagem de CI (head
|
- RestrictedRouteNotice: Button size "xs" -> "sm" (variante inexistente; className ja fixa altura) (3x) - SecretsManagerHealthPanel: narrowing de boot.requestId antes de copyToClipboard - QuoteViewPage: remove cast as Record<string,unknown> no update e coalesce quote.id - ConnectionUI.test / ConnectionsOverviewTable.test: mocks alinhados (testing->isTesting; tipos de retorno relaxados) - selectors.ts (#165): hoist dos valores narrowed de criteria.* fora dos closures (7x TS18048) typecheck: 1294/1295, 0 regressoes. Testes afetados: 26 passando.
Resumo
Auditoria exaustiva do escopo
test:quality(vitest) que corrigiu 19 bugs em 16 arquivos (+204/−69): 7 vazamentos reais de produção (setState após unmount), 1 stub de setup, 1 correção de tipo e 10 testes que sofreram drift após edições do Lovable. Todos os grupos do escopo de CI validados localmente em verde; gate de TS sem regressões (1253 vs baseline 1333).Produção — 7 guardas de setState pós-unmount
useSecurityData.tseAdminSegurancaAcessoPage.tsx:useRef(mountedRef)+ cleanup; o polling de 30s deixa de chamar setState após o componente desmontar.PermissionsPage,RolePermissionsPage,RolesPage,StorageTestPage: guardaisCancellednas funções de fetch dentro douseEffect(cleanup seta a flag).Auth.tsx: guardacancelledemloadInfo(geo + ping ao backend). Resolvido contra a versão atual domainpós-fix: redact sensitive auth and bridge logs #137 (que migrou paracatch {).Setup de testes
tests/setup.ts: stub global no-op deWebSocket(readyState = CLOSED, nunca conecta). O Supabase Realtime (.channel()) abria conexão real via undici quando hooks/componentes com realtime eram montados; o undici disparavadispatchEventcom umEventincompatível com o jsdom, lançando "The 'event' argument must be an instance of Event" como uncaught exception (vitest reportava como unhandled error → falso-positivo). Testes não dependem de realtime; quem precisar pode mockar explicitamente.Correção de tipo
StorageTestPage.tsx:onClick={() => fetchFiles()}(TS2322 — o handler passava oMouseEventcomo argumento posicional indevido).Testes corrigidos (drift após edições do Lovable)
BridgeStatusBanner: remove assertion órfã de texto que não existe mais no componente.DevInfraGateMatrix/DevOnlyBridgeOverlay: a matriz foi realinhada à semântica<DevOnly strict>, onde a visibilidade é decidida exclusivamente porisDev(role dev real) e o overrideisAllowed(env/localStorage) é ignorado no modo strict.MagicUp: validafindByTestId("page-title-magic-up")— oMainLayouté aplicado pelo roteador, não pela própria página.ProductSparkline: corrige o mock path de@/hooks/useSparklineSalespara@/hooks/intelligence.simulation-orchestrator:supabase.functionsé um getter lazy (supabase-js v2) que retorna uma nova instância deFunctionsClienta cada acesso; captura-se a instância uma vez embeforeAll(fns) e usa-se a mesma referência para o spy (beforeEach) e para a chamada — antes o spy registrava 0 chamadas.quote-calculations:calculateItemTotalaplicaround2(arredondamento monetário half-up):0.3333 * 10.5555 = 3.51814815 → 3.52.quote-stepper-ui: marcador da etapa ativa éring-4(nãoscale-110, que pertence aoHorizontalStepper); ordem canônica do fluxoclient → conditions → items → review.Notas para o reviewer
tests/setup.ts) — afeta todo o ambiente de teste, intencionalmente.syntax-integrity.test.tsxé env-inconclusivo no ambiente local (não é um bug deste PR; não foi tocado).@typescript-eslint/no-explicit-anyemStorageTestPage.tsx(linhas 27/49/90/113/133/155,catch (error: any)) são pré-existentes nomain— não foram introduzidos por este PR.Validação
test:quality(vitest run --exclude tests/hooks --exclude tests/e2e): todos os grupos afetados em verde.check-tsc-baseline.mjs): zero regressões.Summary by cubic
Evita setState após desmontagem em páginas admin/auth e corrige drift de testes, incluindo stub global de
WebSocket, para estabilizar otest:qualitysem regressões de TS.useSecurityData,AdminSegurancaAcessoPage(polling de 30s com cleanup),PermissionsPage,RolePermissionsPage,RolesPage,StorageTestPageeAuth.tsx.WebSocketemtests/setup.tspara evitar erro doundicino jsdom; quem precisar de realtime deve mockar explicitamente.DevOnlyem modo strict decide só porisDev(ignora overrides);MagicUpvalidapage-title;ProductSparklineusa mock de@/hooks/intelligence;simulation-orchestratorespia uma única instância desupabase.functions(FunctionsClientlazy);quote-calculationsvalida arredondamento monetário (3.52);quote-stepper-uiesperaring-4e ordem canônicaclient → conditions → items → review.onClick={() => fetchFiles()}emStorageTestPage(TS2322).vitestem verde; gate de TS sem regressões.Written for commit f0f069c. Summary will update on new commits. Review in cubic