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.
324 lines
10 KiB
324 lines
10 KiB
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {expectMemoizationWithoutParams} from '../../utils/hook-testing-utils';
|
|
import {mockApiHook, mockLoading, mockNull, mockSuccess} from '@tryghost/admin-x-framework/test/hook-testing-utils';
|
|
import {renderHook, waitFor} from '@testing-library/react';
|
|
import {useLatestPostStats} from '@src/hooks/useLatestPostStats';
|
|
import type {PostStatsResponseType} from '@tryghost/admin-x-framework/api/stats';
|
|
import type {PostsResponseType} from '@tryghost/admin-x-framework/api/posts';
|
|
|
|
// Mock external dependencies
|
|
vi.mock('@tryghost/admin-x-framework/api/posts', () => ({
|
|
useBrowsePosts: vi.fn()
|
|
}));
|
|
|
|
vi.mock('@tryghost/admin-x-framework/api/stats', () => ({
|
|
usePostStats: vi.fn()
|
|
}));
|
|
|
|
const mockUseBrowsePosts = vi.mocked(await import('@tryghost/admin-x-framework/api/posts')).useBrowsePosts;
|
|
const mockUsePostStats = vi.mocked(await import('@tryghost/admin-x-framework/api/stats')).usePostStats;
|
|
|
|
describe('useLatestPostStats', () => {
|
|
const mockPost = {
|
|
id: 'post-123',
|
|
uuid: 'post-uuid-123',
|
|
title: 'Test Post',
|
|
slug: 'test-post',
|
|
feature_image: 'https://example.com/image.jpg',
|
|
published_at: '2024-01-15T10:00:00.000Z',
|
|
url: 'https://example.com/test-post/',
|
|
excerpt: 'This is a test post excerpt',
|
|
email_only: false,
|
|
status: 'published',
|
|
email: {
|
|
opened_count: 100,
|
|
email_count: 200,
|
|
status: 'sent'
|
|
},
|
|
count: {
|
|
clicks: 50
|
|
},
|
|
authors: [{name: 'Test Author'}]
|
|
};
|
|
|
|
const mockStatsData = {
|
|
stats: [{
|
|
id: 'post-123',
|
|
recipient_count: 200,
|
|
opened_count: 100,
|
|
open_rate: 0.5,
|
|
member_delta: 5,
|
|
free_members: 3,
|
|
paid_members: 2,
|
|
visitors: 150
|
|
}]
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('fetches latest post with correct parameters', () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockSuccess(mockUsePostStats, mockStatsData as PostStatsResponseType);
|
|
|
|
renderHook(() => useLatestPostStats());
|
|
|
|
expect(mockUseBrowsePosts).toHaveBeenCalledWith({
|
|
searchParams: {
|
|
filter: 'status:[published,sent]',
|
|
order: 'published_at DESC',
|
|
limit: '1',
|
|
include: 'authors,email,count.clicks'
|
|
}
|
|
});
|
|
});
|
|
|
|
it('does not fetch stats when no post is available', () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: []} as PostsResponseType);
|
|
|
|
renderHook(() => useLatestPostStats());
|
|
|
|
expect(mockUsePostStats).toHaveBeenCalledWith('', {
|
|
enabled: false
|
|
});
|
|
});
|
|
|
|
it('fetches stats when post is available', () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockSuccess(mockUsePostStats, mockStatsData as PostStatsResponseType);
|
|
|
|
renderHook(() => useLatestPostStats());
|
|
|
|
expect(mockUsePostStats).toHaveBeenCalledWith('post-123', {
|
|
enabled: true
|
|
});
|
|
});
|
|
|
|
it('returns combined post and stats data', async () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockSuccess(mockUsePostStats, mockStatsData as PostStatsResponseType);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data).toEqual({
|
|
// Post data
|
|
id: 'post-123',
|
|
uuid: 'post-uuid-123',
|
|
title: 'Test Post',
|
|
slug: 'test-post',
|
|
feature_image: 'https://example.com/image.jpg',
|
|
published_at: '2024-01-15T10:00:00.000Z',
|
|
url: 'https://example.com/test-post/',
|
|
excerpt: 'This is a test post excerpt',
|
|
email_only: false,
|
|
status: 'published',
|
|
email: {
|
|
opened_count: 100,
|
|
email_count: 200,
|
|
status: 'sent'
|
|
},
|
|
count: {
|
|
clicks: 50
|
|
},
|
|
authors: [{name: 'Test Author'}],
|
|
// Stats data
|
|
recipient_count: 200,
|
|
opened_count: 100,
|
|
open_rate: 0.5,
|
|
member_delta: 5,
|
|
free_members: 3,
|
|
paid_members: 2,
|
|
visitors: 150,
|
|
click_rate: null
|
|
});
|
|
});
|
|
});
|
|
|
|
it('returns post with default stats when stats are not available', async () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockNull(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data).toEqual({
|
|
// Post data
|
|
id: 'post-123',
|
|
uuid: 'post-uuid-123',
|
|
title: 'Test Post',
|
|
slug: 'test-post',
|
|
feature_image: 'https://example.com/image.jpg',
|
|
published_at: '2024-01-15T10:00:00.000Z',
|
|
url: 'https://example.com/test-post/',
|
|
excerpt: 'This is a test post excerpt',
|
|
email_only: false,
|
|
status: 'published',
|
|
email: {
|
|
opened_count: 100,
|
|
email_count: 200,
|
|
status: 'sent'
|
|
},
|
|
count: {
|
|
clicks: 50
|
|
},
|
|
authors: [{name: 'Test Author'}],
|
|
// Default stats
|
|
recipient_count: null,
|
|
opened_count: null,
|
|
open_rate: null,
|
|
member_delta: 0,
|
|
free_members: 0,
|
|
paid_members: 0,
|
|
visitors: 0,
|
|
click_rate: null
|
|
});
|
|
});
|
|
});
|
|
|
|
it('returns post with default stats when stats array is empty', async () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockSuccess(mockUsePostStats, {stats: []} as PostStatsResponseType);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data?.member_delta).toBe(0);
|
|
expect(result.current.data?.free_members).toBe(0);
|
|
expect(result.current.data?.paid_members).toBe(0);
|
|
expect(result.current.data?.visitors).toBe(0);
|
|
});
|
|
});
|
|
|
|
it('returns null when no post is available', () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: []} as PostsResponseType);
|
|
|
|
mockNull(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
expect(result.current.data).toBeNull();
|
|
});
|
|
|
|
it('handles posts data being undefined', () => {
|
|
mockNull(mockUseBrowsePosts);
|
|
|
|
mockNull(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
expect(result.current.data).toBeNull();
|
|
});
|
|
|
|
it('handles post with missing optional fields', async () => {
|
|
const minimalPost = {
|
|
id: 'post-456',
|
|
uuid: 'post-uuid-456',
|
|
published_at: '2024-01-15T10:00:00.000Z',
|
|
title: '',
|
|
slug: '',
|
|
url: ''
|
|
};
|
|
|
|
mockSuccess(mockUseBrowsePosts, {posts: [minimalPost]} as PostsResponseType);
|
|
|
|
mockSuccess(mockUsePostStats, mockStatsData as PostStatsResponseType);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data).toEqual({
|
|
id: 'post-456',
|
|
uuid: 'post-uuid-456',
|
|
title: '',
|
|
slug: '',
|
|
feature_image: null,
|
|
published_at: '2024-01-15T10:00:00.000Z',
|
|
url: '',
|
|
excerpt: '',
|
|
email_only: false,
|
|
status: undefined,
|
|
email: undefined,
|
|
count: undefined,
|
|
authors: [],
|
|
// Stats data from mockStatsData
|
|
recipient_count: 200,
|
|
opened_count: 100,
|
|
open_rate: 0.5,
|
|
member_delta: 5,
|
|
free_members: 3,
|
|
paid_members: 2,
|
|
visitors: 150,
|
|
click_rate: null
|
|
});
|
|
});
|
|
});
|
|
|
|
it('returns correct loading state when posts are loading', () => {
|
|
mockLoading(mockUseBrowsePosts);
|
|
|
|
mockNull(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
expect(result.current.isLoading).toBe(true);
|
|
});
|
|
|
|
it('returns correct loading state when stats are loading', () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockLoading(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
expect(result.current.isLoading).toBe(true);
|
|
});
|
|
|
|
it('returns false loading when posts loaded but no post ID (stats not fetched)', () => {
|
|
mockSuccess(mockUseBrowsePosts, {posts: []} as PostsResponseType);
|
|
|
|
mockNull(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
it('handles stats loading when both posts and stats are loading', () => {
|
|
mockApiHook(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType, true);
|
|
|
|
mockLoading(mockUsePostStats);
|
|
|
|
const {result} = renderHook(() => useLatestPostStats());
|
|
|
|
expect(result.current.isLoading).toBe(true);
|
|
});
|
|
|
|
it('memoizes result correctly', () => {
|
|
// Setup initial state
|
|
mockSuccess(mockUseBrowsePosts, {posts: [mockPost]} as PostsResponseType);
|
|
|
|
mockSuccess(mockUsePostStats, mockStatsData as PostStatsResponseType);
|
|
|
|
expectMemoizationWithoutParams(
|
|
() => useLatestPostStats().data,
|
|
() => {
|
|
// Change the stats data to trigger dependency change
|
|
const newStatsData = {
|
|
stats: [{
|
|
...mockStatsData.stats[0],
|
|
member_delta: 10
|
|
}]
|
|
};
|
|
|
|
mockSuccess(mockUsePostStats, newStatsData as PostStatsResponseType);
|
|
}
|
|
);
|
|
});
|
|
}); |