diff --git a/src/components/compare/SupplierComparisonModal.tsx b/src/components/compare/SupplierComparisonModal.tsx index 34e8d61b5..b8131d2ac 100644 --- a/src/components/compare/SupplierComparisonModal.tsx +++ b/src/components/compare/SupplierComparisonModal.tsx @@ -1,5 +1,4 @@ -import { useMemo, useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useMemo, useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -26,27 +25,17 @@ import { TrendingDown, TrendingUp, Package, - Crown, Minus, - ShieldCheck, - Clock, - Palette, Sparkles, LayoutGrid, List, - Info, AlertCircle, ExternalLink, ChevronRight, Filter, - ArrowRightLeft, } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { - useSupplierComparison, - type Product, - type SupplierComparisonSort, -} from '@/hooks/products'; +import { useSupplierComparison, type Product, type SupplierComparisonSort } from '@/hooks/products'; import { Skeleton } from '@/components/ui/skeleton'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { PriceSparkline } from './PriceSparkline'; @@ -92,7 +81,6 @@ export function SupplierComparisonModal({ open, onOpenChange, }: SupplierComparisonModalProps) { - const navigate = useNavigate(); const [onlyVerified, setOnlyVerified] = useState(false); const [sortBy, setSortBy] = useState('score'); const [viewMode, setViewMode] = useState<'table' | 'grid'>('table'); @@ -136,33 +124,36 @@ export function SupplierComparisonModal({ const filteredProducts = useMemo(() => { let filtered = allProducts; if (onlyVerified) { - filtered = filtered.filter(p => p.isVerified); + filtered = filtered.filter((p) => p.isVerified); } if (activeFilters.includes('moq10')) { - filtered = filtered.filter(p => (p.product.minQuantity ?? 1) <= 10); + filtered = filtered.filter((p) => (p.product.minQuantity ?? 1) <= 10); } if (activeFilters.includes('instock')) { - filtered = filtered.filter(p => p.product.stock > 0); + filtered = filtered.filter((p) => p.product.stock > 0); } return filtered; }, [allProducts, onlyVerified, activeFilters]); const winner = useMemo(() => { if (allProducts.length === 0) return null; - return allProducts.reduce((prev, current) => (prev.score > current.score) ? prev : current, allProducts[0]); + return allProducts.reduce( + (prev, current) => (prev.score > current.score ? prev : current), + allProducts[0], + ); }, [allProducts]); const winnerReason = useMemo(() => { if (!winner) return ''; const { scoreBreakdown } = winner; - const topFactor = Object.entries(scoreBreakdown).reduce((a, b) => a[1] > b[1] ? a : b); + const topFactor = Object.entries(scoreBreakdown).reduce((a, b) => (a[1] > b[1] ? a : b)); const factorLabels: Record = { price: 'melhor preço', stock: 'maior estoque', colors: 'maior variedade de cores', moq: 'baixo pedido mínimo', lead: 'entrega mais rápida', - verified: 'fornecedor ativo e confiável' + verified: 'fornecedor ativo e confiável', }; return `O ${winner.product.supplier.name} é o vencedor principalmente pelo ${factorLabels[topFactor[0]] || 'equilíbrio de fatores'}.`; }, [winner]); @@ -198,8 +189,12 @@ export function SupplierComparisonModal({

Nenhuma alternativa encontrada

-

Tente buscar por outra categoria ou produto.

- +

+ Tente buscar por outra categoria ou produto. +

+
@@ -210,20 +205,20 @@ export function SupplierComparisonModal({ return ( - -
-
+ +
+
-
+
- Comparador de Fornecedores -
-

- {displayProduct.name} -

- + + Comparador de Fornecedores + +
+

{displayProduct.name}

+ {allProducts.length} fornecedores
@@ -232,10 +227,16 @@ export function SupplierComparisonModal({
setViewMode(v as 'table' | 'grid')}> - + Tabela - + Cards @@ -264,26 +265,38 @@ export function SupplierComparisonModal({ {Object.entries(SORT_LABEL).map(([val, label]) => ( - {label} + + {label} + ))}
- setActiveFilters(prev => prev.includes('moq10') ? prev.filter(f => f !== 'moq10') : [...prev, 'moq10'])} + + setActiveFilters((prev) => + prev.includes('moq10') ? prev.filter((f) => f !== 'moq10') : [...prev, 'moq10'], + ) + } /> - setActiveFilters(prev => prev.includes('instock') ? prev.filter(f => f !== 'instock') : [...prev, 'instock'])} + + setActiveFilters((prev) => + prev.includes('instock') + ? prev.filter((f) => f !== 'instock') + : [...prev, 'instock'], + ) + } /> -
+
-
@@ -302,81 +318,152 @@ export function SupplierComparisonModal({
-
+
- + - {maxEconomiaPorMOQ > 0 && } - {fastestLeadTimeDays != null && fastestLeadTimeDays > 0 && } + {maxEconomiaPorMOQ > 0 && ( + + )} + {fastestLeadTimeDays !== null && fastestLeadTimeDays > 0 && ( + + )}
-
-
+
+
-

Por que este vencedor?

-

- {winnerReason} -

+

Por que este vencedor?

+

{winnerReason}

{viewMode === 'table' ? ( -
+
- Prod - Fornecedor - Preço / Ticket - Histórico & Δ - Estoque - Lead - Score + + Prod + + + Fornecedor + + + Preço / Ticket + + + Histórico & Δ + + + Estoque + + + Lead + + + Score + {filteredProducts.map((row) => ( - + ))}
) : ( -
+
{filteredProducts.map((row) => ( - + ))}
)} {/* Sticky Decision Footer */} -
+
{filteredProducts.slice(0, 3).map((p) => ( -
- +
+
))}
-

Resumo da Decisão

-

+

Resumo da Decisão

+

{filteredProducts.length} fornecedores filtrados de {allProducts.length} totais

-
- Produto Selecionado +
+ + Produto Selecionado + {displayProduct.supplier.name}
- - +
@@ -386,15 +473,26 @@ export function SupplierComparisonModal({ ); } -function ToggleFilter({ label, active, onToggle }: { id: string, label: string, active: boolean, onToggle: () => void }) { +function ToggleFilter({ + label, + active, + onToggle, +}: { + id: string; + label: string; + active: boolean; + onToggle: () => void; +}) { return ( -
@@ -582,53 +802,68 @@ function ComparisonCard({ row, formatCurrency, formatPercent }: any) { ); } -function ScoreBreakdown({ score, breakdown, label }: { score: number; breakdown: Record; label?: string }) { +function ScoreBreakdown({ + score, + breakdown, + label, +}: { + score: number; + breakdown: Record; + label?: string; +}) { return ( - - +
-
+
-
+
-

Análise IA

+

Análise IA

- {score} + + {score} +
{Object.entries(breakdown).map(([key, val]) => (
{BREAKDOWN_LABELS[key] || key} - {val.toFixed(1)} / {WEIGHT_LIMITS[key]} + + {val.toFixed(1)} / {WEIGHT_LIMITS[key]} +
-
+
))}
-
-

- "Esta pontuação é dinâmica e reflete o equilíbrio entre custo operacional, disponibilidade imediata e confiabilidade do fornecedor." +

+

+ "Esta pontuação é dinâmica e reflete o equilíbrio entre custo operacional, + disponibilidade imediata e confiabilidade do fornecedor."

diff --git a/src/components/products/PriceFreshnessBadge.tsx b/src/components/products/PriceFreshnessBadge.tsx index 32185504d..d06e1e4e2 100644 --- a/src/components/products/PriceFreshnessBadge.tsx +++ b/src/components/products/PriceFreshnessBadge.tsx @@ -119,7 +119,7 @@ function FreshnessTooltipBody({ freshness, priceUpdatedAt }: FreshnessTooltipPro // Padrão único pt-BR: "em DD/MM/AAAA". A hora local e a forma por extenso // ficam como detalhamento auxiliar, sem repetir a data curta. const shortDate = isValidDate ? formatPriceDateShort(dateValue) : null; - const longDate = isValidDate ? formatPriceDateLong(dateValue) : null; + const _longDate = isValidDate ? formatPriceDateLong(dateValue) : null; const _exactDateTime = isValidDate ? formatExactDateTime(dateValue) : null; const statusLabel = STATUS_LABELS[freshness.status]; const rule = buildClassificationRule(freshness.thresholdDays); @@ -128,12 +128,15 @@ function FreshnessTooltipBody({ freshness, priceUpdatedAt }: FreshnessTooltipPro
{statusLabel} - {freshness.status !== 'unknown' && (() => { - const stripped = freshness.label.match(/\(([^)]+)\)/)?.[1] || freshness.label.replace(/^(Atualizado|Próximo do limite|Possivelmente defasado)\s+/i, '').trim(); - return ( - ({stripped}) - ); - })()} + {freshness.status !== 'unknown' && + (() => { + const stripped = + freshness.label.match(/\(([^)]+)\)/)?.[1] || + freshness.label + .replace(/^(Atualizado|Próximo do limite|Possivelmente defasado)\s+/i, '') + .trim(); + return ({stripped}); + })()}
{shortDate && (
diff --git a/src/components/products/SmartRecommendationsMock.tsx b/src/components/products/SmartRecommendationsMock.tsx index 04dd5d252..fe6166bda 100644 --- a/src/components/products/SmartRecommendationsMock.tsx +++ b/src/components/products/SmartRecommendationsMock.tsx @@ -6,7 +6,6 @@ import { useRef } from 'react'; import { Sparkles, ChevronLeft, ChevronRight, Trophy, TrendingUp, Star } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { QuickAddToQuote } from './QuickAddToQuote'; @@ -91,7 +90,15 @@ const MOCK_RECS: MockRec[] = [ }, ]; -function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestChoice?: boolean; badgeLabel?: string }) { +function MockMiniCard({ + rec, + isBestChoice, + badgeLabel, +}: { + rec: MockRec; + isBestChoice?: boolean; + badgeLabel?: string; +}) { const scorePct = Math.round(rec.score * 100); const isHighMatch = scorePct >= 95; @@ -99,10 +106,12 @@ function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestC
@@ -113,11 +122,11 @@ function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestC loading="lazy" /> {(isHighMatch || isBestChoice) && ( -
)}
@@ -131,17 +140,23 @@ function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestC
) : badgeLabel ? (
- {badgeLabel === 'Mais Pedido' ? : } + {badgeLabel === 'Mais Pedido' ? ( + + ) : ( + + )} {badgeLabel.toUpperCase()}
- ) :
} + ) : ( +
+ )} {scorePct}% @@ -149,7 +164,7 @@ function MockMiniCard({ rec, isBestChoice, badgeLabel }: { rec: MockRec; isBestC
-

+

{rec.name}

@@ -176,7 +191,10 @@ export function SmartRecommendationsMock() { scrollerRef.current?.scrollBy({ left: delta, behavior: 'smooth' }); return ( -

+
@@ -187,7 +205,8 @@ export function SmartRecommendationsMock() { Recomendações inteligentes para este produto

- Sugestões geradas por IA com base em similaridade, margem e perfil do cliente · preview com dados mockados + Sugestões geradas por IA com base em similaridade, margem e perfil do cliente ·{' '} + preview com dados mockados

@@ -222,10 +241,12 @@ export function SmartRecommendationsMock() { > {MOCK_RECS.map((rec, i) => (
- 0.9 ? 'Mais Pedido' : rec.score > 0.7 ? 'Tendência' : undefined} + 0.9 ? 'Mais Pedido' : rec.score > 0.7 ? 'Tendência' : undefined + } />
))} diff --git a/src/components/products/gallery/PromoFlixPlayer.test.tsx b/src/components/products/gallery/PromoFlixPlayer.test.tsx index 74d99908b..250133caa 100644 --- a/src/components/products/gallery/PromoFlixPlayer.test.tsx +++ b/src/components/products/gallery/PromoFlixPlayer.test.tsx @@ -2,12 +2,23 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, fireEvent, act, waitFor } from '@testing-library/react'; import { PromoFlixPlayer } from './PromoFlixPlayer'; -let lastHlsInstance: any = null; +type MockHlsInstance = { + loadSource: ReturnType; + attachMedia: ReturnType; + on: ReturnType; + destroy: ReturnType; + startLoad: ReturnType; + recoverMediaError: ReturnType; + currentLevel: number; + autoLevelEnabled: boolean; +}; + +let lastHlsInstance: MockHlsInstance | null = null; // Mock hls.js for dynamic import vi.mock('hls.js', () => { const mockHls = vi.fn().mockImplementation(() => { - const instance = { + const instance: MockHlsInstance = { loadSource: vi.fn(), attachMedia: vi.fn(), on: vi.fn(), @@ -20,16 +31,21 @@ vi.mock('hls.js', () => { lastHlsInstance = instance; return instance; }); - - (mockHls as any).isSupported = vi.fn().mockReturnValue(true); - (mockHls as any).Events = { + + const mockHlsStatic = mockHls as unknown as { + isSupported: ReturnType; + Events: Record; + ErrorTypes: Record; + }; + mockHlsStatic.isSupported = vi.fn().mockReturnValue(true); + mockHlsStatic.Events = { MANIFEST_PARSED: 'hlsManifestParsed', ERROR: 'hlsError', LEVEL_SWITCHED: 'hlsLevelSwitched', LEVEL_SWITCHING: 'hlsLevelSwitching', FRAG_LOADED: 'hlsFragLoaded', }; - (mockHls as any).ErrorTypes = { + mockHlsStatic.ErrorTypes = { NETWORK_ERROR: 'networkError', MEDIA_ERROR: 'mediaError', OTHER_ERROR: 'otherError', @@ -77,7 +93,7 @@ describe('PromoFlixPlayer Automated Tests', () => { localStorage.clear(); vi.clearAllMocks(); lastHlsInstance = null; - + // Mock HTMLMediaElement prototype Object.defineProperty(HTMLMediaElement.prototype, 'play', { configurable: true, @@ -112,55 +128,55 @@ describe('PromoFlixPlayer Automated Tests', () => { it('should show manual load button after 10s timeout if video is stuck', async () => { vi.useFakeTimers(); const { getByText } = render(); - + // Initial state: loading expect(getByText(/Carregando/i)).toBeDefined(); - + // Advance 11 seconds to trigger STUCK_LOADING_TIMEOUT (10s) await act(async () => { await vi.advanceTimersByTimeAsync(11000); }); - + // Should show manual load button expect(getByText(/Carregar Manualmente/i)).toBeDefined(); - + vi.useRealTimers(); }); it('should show actionable error message when native video fails with code 4 (SRC_NOT_SUPPORTED)', async () => { const { findByText, getByRole } = render(); - + const video = document.querySelector('video'); if (video) { // Simulate SRC_NOT_SUPPORTED (code 4) on native playback (no HLS.js attached for .mp4) await act(async () => { Object.defineProperty(video, 'error', { value: { code: 4, message: 'SRC_NOT_SUPPORTED' }, - configurable: true + configurable: true, }); fireEvent(video, new Event('error')); }); } - + // Check for actionable message (generalized — previously falsely attributed to CORS) expect(await findByText(/Não foi possível reproduzir este vídeo/i)).toBeDefined(); expect(await findByText(/formato pode não ser suportado/i)).toBeDefined(); - + // Ensure "Tentar Novamente" button exists and works const retryButton = getByRole('button', { name: /Tentar Novamente/i }); expect(retryButton).toBeDefined(); - + await act(async () => { fireEvent.click(retryButton); }); - + // After retry, it should be in loading state again expect(await findByText(/Carregando/i)).toBeDefined(); }); it('should hide loading overlay when progress event has buffer', async () => { const { queryByText } = render(); - + const video = document.querySelector('video'); if (video) { // Mock buffered range @@ -170,14 +186,14 @@ describe('PromoFlixPlayer Automated Tests', () => { start: () => 0, end: () => 10, }, - configurable: true + configurable: true, }); - + await act(async () => { fireEvent(video, new Event('progress')); }); } - + // Overlay should be gone (isLoading = false) await waitFor(() => { expect(queryByText(/Carregando/i)).toBeNull(); @@ -186,14 +202,14 @@ describe('PromoFlixPlayer Automated Tests', () => { it('should hide loading overlay when loadeddata event fires', async () => { const { queryByText } = render(); - + const video = document.querySelector('video'); if (video) { await act(async () => { fireEvent(video, new Event('loadeddata')); }); } - + await waitFor(() => { expect(queryByText(/Carregando/i)).toBeNull(); }); @@ -201,14 +217,14 @@ describe('PromoFlixPlayer Automated Tests', () => { it('should hide loading overlay when canplay event fires', async () => { const { queryByText } = render(); - + const video = document.querySelector('video'); if (video) { await act(async () => { fireEvent(video, new Event('canplay')); }); } - + await waitFor(() => { expect(queryByText(/Carregando/i)).toBeNull(); }); @@ -216,56 +232,65 @@ describe('PromoFlixPlayer Automated Tests', () => { describe('HLS.js specific scenarios', () => { const waitForHlsInstance = async () => { - await waitFor(() => { - expect(lastHlsInstance).not.toBeNull(); - }, { timeout: 2000 }); - return lastHlsInstance; + await waitFor( + () => { + expect(lastHlsInstance).not.toBeNull(); + }, + { timeout: 2000 }, + ); + return lastHlsInstance!; }; it('should handle HLS.js fatal network errors with recovery attempts', async () => { const { findByText } = render(); - + const hlsInstance = await waitForHlsInstance(); - const errorHandler = hlsInstance.on.mock.calls.find((call: any) => call[0] === 'hlsError')[1]; + const errorHandler = hlsInstance.on.mock.calls.find( + (call: unknown[]) => call[0] === 'hlsError', + )![1]; // Simulate 1st fatal network error (should trigger startLoad) await act(async () => { errorHandler('hlsError', { fatal: true, type: 'networkError', - details: 'manifestLoadError' + details: 'manifestLoadError', }); }); expect(hlsInstance.startLoad).toHaveBeenCalledTimes(1); - + // Simulate 4 more fatal network errors (total 5, > 3 threshold) for (let i = 0; i < 4; i++) { await act(async () => { errorHandler('hlsError', { fatal: true, type: 'networkError', - details: 'manifestLoadError' + details: 'manifestLoadError', }); }); } // Should show the final error message - expect(await findByText(/Não foi possível carregar o vídeo. Verifique sua conexão/i)).toBeDefined(); + expect( + await findByText(/Não foi possível carregar o vídeo. Verifique sua conexão/i), + ).toBeDefined(); }); it('should handle HLS.js fatal media errors with recovery', async () => { render(); - + const hlsInstance = await waitForHlsInstance(); - const errorHandler = hlsInstance.on.mock.calls.find((call: any) => call[0] === 'hlsError')[1]; + const errorHandler = hlsInstance.on.mock.calls.find( + (call: unknown[]) => call[0] === 'hlsError', + )![1]; // Simulate fatal media error await act(async () => { errorHandler('hlsError', { fatal: true, type: 'mediaError', - details: 'bufferStalledError' + details: 'bufferStalledError', }); }); @@ -274,9 +299,11 @@ describe('PromoFlixPlayer Automated Tests', () => { it('should hide loading overlay when HLS.js MANIFEST_PARSED event fires', async () => { const { queryByText } = render(); - + const hlsInstance = await waitForHlsInstance(); - const manifestHandler = hlsInstance.on.mock.calls.find((call: any) => call[0] === 'hlsManifestParsed')[1]; + const manifestHandler = hlsInstance.on.mock.calls.find( + (call: unknown[]) => call[0] === 'hlsManifestParsed', + )![1]; await act(async () => { manifestHandler('hlsManifestParsed', { levels: [] }); @@ -289,9 +316,11 @@ describe('PromoFlixPlayer Automated Tests', () => { it('should hide loading overlay when HLS.js FRAG_LOADED event fires', async () => { const { queryByText } = render(); - + const hlsInstance = await waitForHlsInstance(); - const fragLoadedHandler = hlsInstance.on.mock.calls.find((call: any) => call[0] === 'hlsFragLoaded')[1]; + const fragLoadedHandler = hlsInstance.on.mock.calls.find( + (call: unknown[]) => call[0] === 'hlsFragLoaded', + )![1]; await act(async () => { fragLoadedHandler('hlsFragLoaded', {}); @@ -305,18 +334,25 @@ describe('PromoFlixPlayer Automated Tests', () => { describe('Level Changes and Auto-Leveling', () => { const waitForHlsInstance = async () => { - await waitFor(() => { - expect(lastHlsInstance).not.toBeNull(); - }, { timeout: 2000 }); - return lastHlsInstance; + await waitFor( + () => { + expect(lastHlsInstance).not.toBeNull(); + }, + { timeout: 2000 }, + ); + return lastHlsInstance!; }; it('should update quality state during multiple level changes with auto-level enabled', async () => { const { queryByText } = render(); - + const hlsInstance = await waitForHlsInstance(); - const levelSwitchedHandler = hlsInstance.on.mock.calls.find((call: any) => call[0] === 'hlsLevelSwitched')[1]; - const manifestHandler = hlsInstance.on.mock.calls.find((call: any) => call[0] === 'hlsManifestParsed')[1]; + const levelSwitchedHandler = hlsInstance.on.mock.calls.find( + (call: unknown[]) => call[0] === 'hlsLevelSwitched', + )![1]; + const manifestHandler = hlsInstance.on.mock.calls.find( + (call: unknown[]) => call[0] === 'hlsManifestParsed', + )![1]; // Initially auto-level is enabled (-1) expect(hlsInstance.autoLevelEnabled).toBe(true); @@ -347,7 +383,7 @@ describe('PromoFlixPlayer Automated Tests', () => { it('should show "Manual Load" button and re-initialize player on click', async () => { vi.useFakeTimers(); const { getByText, queryByText } = render(); - + // Advance to trigger timeout await act(async () => { await vi.advanceTimersByTimeAsync(11000); diff --git a/src/components/products/gallery/PromoFlixPlayer.tsx b/src/components/products/gallery/PromoFlixPlayer.tsx index 437c8b169..09e28abc8 100644 --- a/src/components/products/gallery/PromoFlixPlayer.tsx +++ b/src/components/products/gallery/PromoFlixPlayer.tsx @@ -46,6 +46,7 @@ import { import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { logger } from '@/lib/logger'; const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2] as const; @@ -171,9 +172,8 @@ export function PromoFlixPlayer({ [], ); - const logTelemetry = useCallback((event: string, details?: any) => { - const timestamp = new Date().toISOString(); - console.log(`[PromoFlix Telemetry] [${timestamp}] ${event}`, details || ''); + const logTelemetry = useCallback((event: string, details?: Record) => { + logger.debug(`[PromoFlix Telemetry] ${event}`, details); }, []); const flash = useCallback((label: string) => { @@ -213,9 +213,10 @@ export function PromoFlixPlayer({ const vv = videoRef.current; // Não sobrescreve erro fatal já apresentado por hls.js ou onError nativo if (vv && vv.readyState < 1) { - setHlsError((prev) => - prev ?? - 'O vídeo está demorando para responder. Pode haver um bloqueio de rede ou CORS.', + setHlsError( + (prev) => + prev ?? + 'O vídeo está demorando para responder. Pode haver um bloqueio de rede ou CORS.', ); logTelemetry('LOADING_ERROR_FINAL', { readyState: vv.readyState }); } @@ -232,7 +233,6 @@ export function PromoFlixPlayer({ const video = videoRef.current; if (!video || !src) return; - // Invalida qualquer init anterior em andamento (Strict Mode / troca de src / Retry) const myToken = ++initTokenRef.current; autoplayFallbackTriedRef.current = false; @@ -465,10 +465,23 @@ export function PromoFlixPlayer({ clearLoadingTimeout(); } }); - }, [src, isHls, volume, isMuted, isPlaying, autoPlay, armLoadingTimeout, clearLoadingTimeout]); + }, [ + src, + isHls, + volume, + isMuted, + isPlaying, + autoPlay, + armLoadingTimeout, + clearLoadingTimeout, + logTelemetry, + ]); useEffect(() => { initPlayer(); + // Captura o nó atual para uso seguro no cleanup (evita ler videoRef.current + // tardiamente, quando o ref já pode ter mudado). + const v = videoRef.current; return () => { // Invalida callbacks de imports HLS ainda pendentes initTokenRef.current += 1; @@ -482,7 +495,6 @@ export function PromoFlixPlayer({ hlsRef.current = null; } // Limpa src do