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/posts/test/unit/hooks/usePostSuccessModal.test.tsx

493 lines
15 KiB

/* eslint-disable @typescript-eslint/no-explicit-any */
import {HttpResponse, http} from 'msw';
import {act, renderHook, waitFor} from '@testing-library/react';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {createTestWrapper, mockData, mockServer} from '../../utils/msw-helpers';
import {usePostSuccessModal} from '@src/hooks/usePostSuccessModal';
// Mock React context (not HTTP)
vi.mock('@src/providers/PostAnalyticsContext');
const mockUseGlobalData = vi.mocked(await import('@src/providers/PostAnalyticsContext')).useGlobalData;
// Mock localStorage
const mockLocalStorage = {
getItem: vi.fn(),
removeItem: vi.fn(),
setItem: vi.fn()
};
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage
});
describe('usePostSuccessModal', () => {
beforeEach(() => {
vi.clearAllMocks();
// Default mocks
mockUseGlobalData.mockReturnValue({
site: {
title: 'Test Site',
icon: 'https://example.com/icon.png'
}
} as any);
mockLocalStorage.getItem.mockReturnValue(null);
// Default MSW setup - no posts data by default
mockServer.setup({
posts: []
});
});
it('initializes with modal closed and no data', () => {
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
expect(result.current.isModalOpen).toBe(false);
expect(result.current.post).toBeUndefined();
expect(result.current.postCount).toBe(null);
expect(result.current.showPostCount).toBe(false);
expect(result.current.modalProps).toBe(null);
});
it('does not open modal when localStorage is empty', () => {
mockLocalStorage.getItem.mockReturnValue(null);
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
expect(result.current.isModalOpen).toBe(false);
});
it('handles invalid JSON in localStorage gracefully', () => {
mockLocalStorage.getItem.mockReturnValue('invalid json');
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
expect(result.current.isModalOpen).toBe(false);
});
it('ignores localStorage errors gracefully', () => {
mockLocalStorage.getItem.mockImplementation(() => {
throw new Error('LocalStorage error');
});
expect(() => {
renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
}).not.toThrow();
});
it('creates modal props when post data is available', async () => {
const testPost = mockData.post({
id: 'post-123',
title: 'Test Post',
url: 'https://example.com/test-post',
feature_image: 'https://example.com/image.jpg',
published_at: '2023-12-01T12:00:00Z',
authors: [{name: 'John Doe'}],
email: {email_count: 100, opened_count: 30},
newsletter: {name: 'Weekly Newsletter'}
} as any);
// Set up MSW to return the post data
mockServer.setup({
posts: [testPost]
});
// Simulate localStorage containing published post data
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.post).toEqual(testPost);
expect(result.current.isModalOpen).toBe(true);
});
});
it('opens modal when localStorage contains valid post data', async () => {
const testPost = mockData.post({
id: 'post-123',
title: 'Published Post'
});
mockServer.setup({
posts: [testPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.isModalOpen).toBe(true);
});
});
it('cleans up localStorage when modal opens', async () => {
const testPost = mockData.post({
id: 'post-123',
title: 'Test Post'
});
mockServer.setup({
posts: [testPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
// Wait for the modal to open (localStorage data consumed)
await waitFor(() => {
expect(result.current.isModalOpen).toBe(true);
});
// Behavior test: localStorage data should be consumed and not trigger again
// Clear the localStorage mock and verify subsequent renders don't trigger
mockLocalStorage.getItem.mockReturnValue(null);
// Close modal - should close properly
act(() => {
result.current.closeModal();
});
expect(result.current.isModalOpen).toBe(false);
});
it('handles post count response', async () => {
// Setup MSW with custom handlers for count endpoint
mockServer.setup({
customHandlers: [
http.get('/ghost/api/admin/posts/*', ({request}) => {
const url = new URL(request.url);
const fields = url.searchParams.get('fields');
if (fields === 'id') {
// Post count endpoint
return HttpResponse.json({
meta: {
pagination: {
total: 42
}
}
});
}
// Regular post data endpoint
return HttpResponse.json({posts: []});
})
]
});
// Simulate localStorage containing published post data
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.postCount).toBe(42);
expect(result.current.showPostCount).toBe(true);
});
});
it('closes modal correctly', () => {
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
result.current.closeModal();
expect(result.current.isModalOpen).toBe(false);
expect(result.current.postCount).toBe(null);
});
it('handles email-only posts', async () => {
const testPost = mockData.post({
id: 'post-123',
title: 'Email Only Post',
email_only: true,
email: {email_count: 50, opened_count: 15}
} as any);
mockServer.setup({
posts: [testPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.post?.email_only).toBe(true);
});
});
it('handles multiple authors', async () => {
const testPost = mockData.post({
id: 'post-123',
title: 'Test Post',
authors: [
{name: 'John Doe'},
{name: 'Jane Smith'},
{name: 'Bob Johnson'}
]
} as any);
mockServer.setup({
posts: [testPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.post?.authors).toHaveLength(3);
});
});
it('handles posts without authors', async () => {
const testPost = mockData.post({
id: 'post-123',
title: 'Test Post'
});
mockServer.setup({
posts: [testPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.post?.authors).toBeUndefined();
});
});
it('creates modal props with correct email data for different subscriber counts', async () => {
// Test single subscriber - behavior: modal props should be created
const singleSubscriberPost = mockData.post({
id: 'post-123',
title: 'Single Subscriber Post',
email: {email_count: 1, opened_count: 0},
newsletter: {name: 'Test Newsletter'}
} as any);
mockServer.setup({
posts: [singleSubscriberPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result: singleResult} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(singleResult.current.modalProps).toBeTruthy();
expect(singleResult.current.modalProps?.emailOnly).toBeFalsy();
expect(singleResult.current.modalProps?.description).toBeTruthy();
});
// Test multiple subscribers - behavior: modal props should be created
const multipleSubscribersPost = mockData.post({
id: 'post-456',
title: 'Multiple Subscribers Post',
email: {email_count: 100, opened_count: 30},
newsletter: {name: 'Test Newsletter'}
} as any);
mockServer.setup({
posts: [multipleSubscribersPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-456',
type: 'post'
}));
const {result: multipleResult} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(multipleResult.current.modalProps).toBeTruthy();
expect(multipleResult.current.modalProps?.emailOnly).toBeFalsy();
expect(multipleResult.current.modalProps?.description).toBeTruthy();
});
});
it('creates appropriate modal props for different post types', async () => {
// Test email-only post - behavior: should set emailOnly flag
const emailOnlyPost = mockData.post({
id: 'email-post',
title: 'Email Only Post',
email_only: true,
email: {email_count: 50, opened_count: 15}
} as any);
mockServer.setup({
posts: [emailOnlyPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'email-post',
type: 'post'
}));
const {result: emailResult} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(emailResult.current.modalProps?.emailOnly).toBe(true);
expect(emailResult.current.modalProps?.description).toBeTruthy();
});
// Test published post with email - behavior: should not be emailOnly
const publishedPost = mockData.post({
id: 'published-post',
title: 'Published Post',
email: {email_count: 100, opened_count: 30}
} as any);
mockServer.setup({
posts: [publishedPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'published-post',
type: 'post'
}));
const {result: publishedResult} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(publishedResult.current.modalProps?.emailOnly).toBeFalsy();
expect(publishedResult.current.modalProps?.description).toBeTruthy();
});
// Test published post without email - behavior: should not be emailOnly
const publishedOnlyPost = mockData.post({
id: 'published-only',
title: 'Published Only Post'
});
mockServer.setup({
posts: [publishedOnlyPost]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'published-only',
type: 'post'
}));
const {result: publishedOnlyResult} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(publishedOnlyResult.current.modalProps?.emailOnly).toBeFalsy();
expect(publishedOnlyResult.current.modalProps?.description).toBeTruthy();
});
});
it('handles loading state', () => {
// Without localStorage data, no API calls are made
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
expect(result.current.post).toBeUndefined();
});
it('handles error state', () => {
// Test when MSW server returns an error
mockServer.setup({
customHandlers: [
http.get('/ghost/api/admin/posts/*', () => {
return HttpResponse.json({error: 'API Error'}, {status: 500});
})
]
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
expect(result.current.post).toBeUndefined();
});
it('handles empty posts response', async () => {
mockServer.setup({
posts: [] // Empty posts array
});
mockLocalStorage.getItem.mockReturnValue(JSON.stringify({
id: 'post-123',
type: 'post'
}));
const {result} = renderHook(() => usePostSuccessModal(), {
wrapper: createTestWrapper()
});
await waitFor(() => {
expect(result.current.post).toBeUndefined();
});
});
});