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.
493 lines
15 KiB
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();
|
|
});
|
|
});
|
|
}); |