You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
645 lines
20 KiB
645 lines
20 KiB
import {renderHook, act} from '@testing-library/react';
|
|
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
import {useActiveVisitors} from '../../../src/hooks/useActiveVisitors';
|
|
import React from 'react';
|
|
|
|
// Mock the @tinybirdco/charts module
|
|
vi.mock('@tinybirdco/charts', () => ({
|
|
useQuery: vi.fn()
|
|
}));
|
|
|
|
// Mock the stats-config utils
|
|
vi.mock('../../../src/utils/stats-config', () => ({
|
|
getStatEndpointUrl: vi.fn()
|
|
}));
|
|
|
|
// Mock the useTinybirdToken hook
|
|
vi.mock('../../../src/hooks/useTinybirdToken', () => ({
|
|
useTinybirdToken: vi.fn()
|
|
}));
|
|
|
|
import {useQuery} from '@tinybirdco/charts';
|
|
import {getStatEndpointUrl} from '../../../src/utils/stats-config';
|
|
import {useTinybirdToken} from '../../../src/hooks/useTinybirdToken';
|
|
|
|
const mockUseQuery = vi.mocked(useQuery);
|
|
const mockGetStatEndpointUrl = vi.mocked(getStatEndpointUrl);
|
|
const mockUseTinybirdToken = vi.mocked(useTinybirdToken);
|
|
|
|
describe('useActiveVisitors', () => {
|
|
let queryClient: QueryClient;
|
|
let wrapper: React.FC<{children: React.ReactNode}>;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {retry: false},
|
|
mutations: {retry: false}
|
|
}
|
|
});
|
|
wrapper = ({children}) => React.createElement(QueryClientProvider, {client: queryClient}, children);
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
mockGetStatEndpointUrl.mockImplementation((_config: any, endpoint: any) => `https://api.example.com/${endpoint}`);
|
|
mockUseTinybirdToken.mockReturnValue({
|
|
token: 'mock-token',
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: vi.fn()
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('returns initial state when enabled is true', () => {
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(result.current).toEqual({
|
|
activeVisitors: 0,
|
|
isLoading: false,
|
|
error: null
|
|
});
|
|
});
|
|
|
|
it('returns zero state when enabled is false', () => {
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: false}), {wrapper});
|
|
|
|
expect(result.current).toEqual({
|
|
activeVisitors: 0,
|
|
isLoading: false,
|
|
error: null
|
|
});
|
|
});
|
|
|
|
it('shows loading state only on initial load with no last known count', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: true,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
// Should show loading on initial load when no lastKnownCount exists
|
|
expect(result.current.isLoading).toBe(true);
|
|
expect(result.current.activeVisitors).toBe(0);
|
|
});
|
|
|
|
it('does not show loading when lastKnownCount exists', () => {
|
|
// First render with data to establish lastKnownCount
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 25}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result, rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
expect(result.current.activeVisitors).toBe(25);
|
|
|
|
// Second render with loading but data should not show loading
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: true,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
rerender();
|
|
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.activeVisitors).toBe(25); // Retains last known count
|
|
});
|
|
|
|
it('returns active visitor count from data', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 42}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(result.current.activeVisitors).toBe(42);
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
it('handles error state', () => {
|
|
const mockError = 'Network error';
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: mockError,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(result.current.error).toBe(mockError);
|
|
});
|
|
|
|
it('calls getStatEndpointUrl with correct parameters and uses tinybirdToken', () => {
|
|
const statsConfig = {
|
|
id: 'test-site-id',
|
|
endpoint: 'https://api.test.com',
|
|
token: 'test-token'
|
|
};
|
|
|
|
renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper});
|
|
|
|
expect(mockGetStatEndpointUrl).toHaveBeenCalledWith(statsConfig, 'api_active_visitors');
|
|
expect(mockUseTinybirdToken).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls useTinybirdQuery with undefined endpoint when no statsConfig and uses tinybirdToken', () => {
|
|
renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
endpoint: undefined,
|
|
token: 'mock-token',
|
|
params: expect.objectContaining({
|
|
site_uuid: ''
|
|
})
|
|
})
|
|
);
|
|
expect(mockUseTinybirdToken).toHaveBeenCalled();
|
|
});
|
|
|
|
it('sets up 60-second interval when enabled', () => {
|
|
renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
// Initially refreshKey should be 0
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({
|
|
_refresh: '0'
|
|
})
|
|
})
|
|
);
|
|
|
|
// Fast-forward 60 seconds
|
|
act(() => {
|
|
vi.advanceTimersByTime(60000);
|
|
});
|
|
|
|
// Should increment refreshKey
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({
|
|
_refresh: '1'
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('does not set up interval when disabled', () => {
|
|
renderHook(() => useActiveVisitors({enabled: false}), {wrapper});
|
|
|
|
// Fast-forward 60 seconds
|
|
act(() => {
|
|
vi.advanceTimersByTime(60000);
|
|
});
|
|
|
|
// Should still be at refreshKey 0
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({
|
|
_refresh: '0'
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('includes postUuid in params when provided', () => {
|
|
const postUuid = 'test-post-uuid';
|
|
renderHook(() => useActiveVisitors({postUuid, enabled: true}), {wrapper});
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({
|
|
post_uuid: postUuid
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('does not include postUuid in params when not provided', () => {
|
|
renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.not.objectContaining({
|
|
post_uuid: expect.anything()
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('uses statsConfig for site_uuid', () => {
|
|
const statsConfig = {
|
|
id: 'test-site-id',
|
|
endpoint: 'https://api.test.com',
|
|
token: 'test-token'
|
|
};
|
|
|
|
renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper});
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({
|
|
site_uuid: 'test-site-id'
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('uses empty string for site_uuid when no statsConfig', () => {
|
|
renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
params: expect.objectContaining({
|
|
site_uuid: ''
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('retains last known count after refresh', () => {
|
|
// Initial data
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 25}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result, rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
expect(result.current.activeVisitors).toBe(25);
|
|
|
|
// Simulate refresh with loading state but no new data
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: true,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
rerender();
|
|
|
|
// Should retain last known count and not show loading
|
|
expect(result.current.activeVisitors).toBe(25);
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
it('handles zero active visitors correctly', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 0}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(result.current.activeVisitors).toBe(0);
|
|
});
|
|
|
|
it('handles invalid data format gracefully', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{some_other_field: 42}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
expect(result.current.activeVisitors).toBe(0);
|
|
});
|
|
|
|
it('cleans up interval on unmount', () => {
|
|
// Spy on clearInterval before creating the hook
|
|
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
|
|
|
const {unmount} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
unmount();
|
|
|
|
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('updates lastKnownCount when new valid data is received', () => {
|
|
// Start with no data
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result, rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
expect(result.current.activeVisitors).toBe(0);
|
|
|
|
// Provide valid data
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 15}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
rerender();
|
|
|
|
expect(result.current.activeVisitors).toBe(15);
|
|
|
|
// Now when data becomes null again, should retain the count
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
rerender();
|
|
|
|
expect(result.current.activeVisitors).toBe(15);
|
|
});
|
|
|
|
it('handles statsConfig changes correctly', () => {
|
|
const initialStatsConfig = {
|
|
id: 'initial-site-id',
|
|
endpoint: 'https://initial.api.com',
|
|
token: 'initial-token'
|
|
};
|
|
|
|
const {rerender} = renderHook(
|
|
({statsConfig}) => useActiveVisitors({statsConfig, enabled: true}),
|
|
{initialProps: {statsConfig: initialStatsConfig}, wrapper}
|
|
);
|
|
|
|
expect(mockGetStatEndpointUrl).toHaveBeenCalledWith(initialStatsConfig, 'api_active_visitors');
|
|
expect(mockUseTinybirdToken).toHaveBeenCalled();
|
|
|
|
// Change statsConfig
|
|
const newStatsConfig = {
|
|
id: 'new-site-id',
|
|
endpoint: 'https://new.api.com',
|
|
token: 'new-token'
|
|
};
|
|
|
|
rerender({statsConfig: newStatsConfig});
|
|
|
|
expect(mockGetStatEndpointUrl).toHaveBeenCalledWith(newStatsConfig, 'api_active_visitors');
|
|
expect(mockUseTinybirdToken).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not update lastKnownCount when disabled', () => {
|
|
// Start enabled with data
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 20}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result, rerender} = renderHook(
|
|
({enabled}) => useActiveVisitors({enabled}),
|
|
{initialProps: {enabled: true}, wrapper}
|
|
);
|
|
|
|
expect(result.current.activeVisitors).toBe(20);
|
|
|
|
// Disable and provide new data
|
|
mockUseQuery.mockReturnValue({
|
|
data: [{active_visitors: 30}],
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: 'mock-token',
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
rerender({enabled: false});
|
|
|
|
// Should return 0 when disabled, regardless of new data
|
|
expect(result.current.activeVisitors).toBe(0);
|
|
expect(result.current.error).toBeNull();
|
|
});
|
|
|
|
it('resets interval when enabled state changes', () => {
|
|
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
|
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
|
|
|
// Clear any previous calls
|
|
setIntervalSpy.mockClear();
|
|
clearIntervalSpy.mockClear();
|
|
|
|
const {rerender} = renderHook(
|
|
({enabled}) => useActiveVisitors({enabled}),
|
|
{initialProps: {enabled: true}, wrapper}
|
|
);
|
|
|
|
expect(setIntervalSpy).toHaveBeenCalledTimes(1);
|
|
const firstIntervalId = setIntervalSpy.mock.results[0]?.value;
|
|
|
|
// Disable
|
|
rerender({enabled: false});
|
|
|
|
expect(clearIntervalSpy).toHaveBeenCalledWith(firstIntervalId);
|
|
|
|
// Re-enable
|
|
rerender({enabled: true});
|
|
|
|
// Should create a new interval
|
|
expect(setIntervalSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should call useQuery with undefined endpoint when token is loading (preventing HTTP requests)', () => {
|
|
// Mock useTinybirdToken to return undefined token (still loading)
|
|
mockUseTinybirdToken.mockReturnValue({
|
|
token: undefined,
|
|
isLoading: true,
|
|
error: null,
|
|
refetch: vi.fn()
|
|
});
|
|
|
|
// Clear any previous calls
|
|
mockUseQuery.mockClear();
|
|
|
|
// Render the hook
|
|
renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
// EXPECTED: useQuery should be called with undefined endpoint when token is loading
|
|
// This prevents HTTP requests by disabling the SWR query entirely
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
endpoint: undefined,
|
|
token: undefined
|
|
})
|
|
);
|
|
});
|
|
|
|
it('calls useQuery with token when token is available', () => {
|
|
// Mock useTinybirdToken to return a valid token
|
|
mockUseTinybirdToken.mockReturnValue({
|
|
token: 'valid-token',
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: vi.fn()
|
|
});
|
|
|
|
// Clear any previous calls
|
|
mockUseQuery.mockClear();
|
|
|
|
// Render the hook
|
|
renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
// Should call useQuery with the valid token
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
token: 'valid-token'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('transitions from undefined to valid token correctly', () => {
|
|
// Start with undefined token
|
|
mockUseTinybirdToken.mockReturnValue({
|
|
token: undefined,
|
|
isLoading: true,
|
|
error: null,
|
|
refetch: vi.fn()
|
|
});
|
|
|
|
// Render the hook
|
|
const {rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
// Verify first call with undefined token (due to tokenLoading: true)
|
|
expect(mockUseQuery).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
token: undefined
|
|
})
|
|
);
|
|
|
|
// Clear previous calls
|
|
mockUseQuery.mockClear();
|
|
|
|
// Now provide a valid token
|
|
mockUseTinybirdToken.mockReturnValue({
|
|
token: 'valid-token',
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: vi.fn()
|
|
});
|
|
|
|
// Rerender
|
|
rerender();
|
|
|
|
// Should now call useQuery with the valid token
|
|
expect(mockUseQuery).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
token: 'valid-token'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('shows loading state when token is loading', () => {
|
|
// Mock token as loading
|
|
mockUseTinybirdToken.mockReturnValue({
|
|
token: undefined,
|
|
isLoading: true,
|
|
error: null,
|
|
refetch: vi.fn()
|
|
});
|
|
|
|
// Mock useQuery to return no loading (since token loading should be considered)
|
|
mockUseQuery.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: null,
|
|
meta: null,
|
|
statistics: null,
|
|
endpoint: 'https://api.example.com/api_active_visitors',
|
|
token: undefined,
|
|
refresh: vi.fn()
|
|
});
|
|
|
|
const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper});
|
|
|
|
// Should show loading because token is loading and no lastKnownCount
|
|
expect(result.current.isLoading).toBe(true);
|
|
expect(result.current.activeVisitors).toBe(0);
|
|
});
|
|
}); |