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.
ghost/apps/admin-x-framework/test/unit/hooks/useActiveVisitors.test.ts

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);
});
});