Add sharing
This commit is contained in:
parent
240ce28692
commit
8a11275849
16 changed files with 1996 additions and 10 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useQuizLibrary } from '../../hooks/useQuizLibrary';
|
||||
import type { Quiz } from '../../types';
|
||||
import type { Quiz, QuizListItem } from '../../types';
|
||||
|
||||
const mockAuthFetch = vi.fn();
|
||||
|
||||
|
|
@ -1167,4 +1167,319 @@ it('creates blob with correct mime type', async () => {
|
|||
expect(result.current.importing).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shareQuiz', () => {
|
||||
it('shares a quiz and returns the share token', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ shareToken: 'abc123token' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
let token;
|
||||
await act(async () => {
|
||||
token = await result.current.shareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
expect(token).toBe('abc123token');
|
||||
expect(mockAuthFetch).toHaveBeenCalledWith('/api/quizzes/quiz-123/share', {
|
||||
method: 'POST',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets and clears sharingQuizId during share operation', async () => {
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockAuthFetch.mockReturnValueOnce(pendingPromise);
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
act(() => {
|
||||
result.current.shareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sharingQuizId).toBe('quiz-123');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
resolvePromise!({ ok: true, json: () => Promise.resolve({ shareToken: 'token' }) });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sharingQuizId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates quiz in local state with shareToken and isShared', async () => {
|
||||
const initialQuizzes: QuizListItem[] = [
|
||||
{ id: 'quiz-123', title: 'Test Quiz', source: 'manual', questionCount: 5, isShared: false, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
mockAuthFetch
|
||||
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(initialQuizzes) })
|
||||
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ shareToken: 'newtoken' }) });
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.shareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
expect(result.current.quizzes[0].shareToken).toBe('newtoken');
|
||||
expect(result.current.quizzes[0].isShared).toBe(true);
|
||||
});
|
||||
|
||||
it('handles 404 not found when sharing non-existent quiz', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.shareQuiz('non-existent');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz not found.');
|
||||
}
|
||||
|
||||
expect(result.current.sharingQuizId).toBeNull();
|
||||
});
|
||||
|
||||
it('handles generic server error when sharing', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.shareQuiz('quiz-123');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to share quiz.');
|
||||
}
|
||||
});
|
||||
|
||||
it('handles network error when sharing', async () => {
|
||||
mockAuthFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.shareQuiz('quiz-123');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Network error');
|
||||
}
|
||||
|
||||
expect(result.current.sharingQuizId).toBeNull();
|
||||
});
|
||||
|
||||
it('returns existing token if quiz is already shared', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ shareToken: 'existing-token' }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
let token;
|
||||
await act(async () => {
|
||||
token = await result.current.shareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
expect(token).toBe('existing-token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unshareQuiz', () => {
|
||||
it('unshares a quiz successfully', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.unshareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
expect(mockAuthFetch).toHaveBeenCalledWith('/api/quizzes/quiz-123/share', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets and clears sharingQuizId during unshare operation', async () => {
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockAuthFetch.mockReturnValueOnce(pendingPromise);
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
act(() => {
|
||||
result.current.unshareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sharingQuizId).toBe('quiz-123');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
resolvePromise!({ ok: true, json: () => Promise.resolve({ success: true }) });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sharingQuizId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates quiz in local state to remove shareToken and set isShared false', async () => {
|
||||
const initialQuizzes: QuizListItem[] = [
|
||||
{ id: 'quiz-123', title: 'Test Quiz', source: 'manual', questionCount: 5, shareToken: 'oldtoken', isShared: true, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
];
|
||||
|
||||
mockAuthFetch
|
||||
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(initialQuizzes) })
|
||||
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ success: true }) });
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
expect(result.current.quizzes[0].isShared).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.unshareQuiz('quiz-123');
|
||||
});
|
||||
|
||||
expect(result.current.quizzes[0].shareToken).toBeUndefined();
|
||||
expect(result.current.quizzes[0].isShared).toBe(false);
|
||||
});
|
||||
|
||||
it('handles 404 not found when unsharing non-existent quiz', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.unshareQuiz('non-existent');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Quiz not found.');
|
||||
}
|
||||
|
||||
expect(result.current.sharingQuizId).toBeNull();
|
||||
});
|
||||
|
||||
it('handles generic server error when unsharing', async () => {
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.unshareQuiz('quiz-123');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Failed to stop sharing quiz.');
|
||||
}
|
||||
});
|
||||
|
||||
it('handles network error when unsharing', async () => {
|
||||
mockAuthFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.unshareQuiz('quiz-123');
|
||||
});
|
||||
expect.fail('Should have thrown');
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toBe('Network error');
|
||||
}
|
||||
|
||||
expect(result.current.sharingQuizId).toBeNull();
|
||||
});
|
||||
|
||||
it('does not affect other quizzes in the list when unsharing', async () => {
|
||||
const initialQuizzes: QuizListItem[] = [
|
||||
{ id: 'quiz-1', title: 'Quiz 1', source: 'manual', questionCount: 5, shareToken: 'token1', isShared: true, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
{ id: 'quiz-2', title: 'Quiz 2', source: 'manual', questionCount: 3, shareToken: 'token2', isShared: true, createdAt: '2024-01-02', updatedAt: '2024-01-02' },
|
||||
];
|
||||
|
||||
mockAuthFetch
|
||||
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(initialQuizzes) })
|
||||
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ success: true }) });
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.unshareQuiz('quiz-1');
|
||||
});
|
||||
|
||||
expect(result.current.quizzes[0].isShared).toBe(false);
|
||||
expect(result.current.quizzes[1].isShared).toBe(true);
|
||||
expect(result.current.quizzes[1].shareToken).toBe('token2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchQuizzes with sharing info', () => {
|
||||
it('fetches quizzes with isShared and shareToken fields', async () => {
|
||||
const mockQuizzes: QuizListItem[] = [
|
||||
{ id: '1', title: 'Shared Quiz', source: 'manual', questionCount: 5, shareToken: 'token123', isShared: true, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
{ id: '2', title: 'Private Quiz', source: 'ai_generated', questionCount: 10, isShared: false, createdAt: '2024-01-02', updatedAt: '2024-01-02' },
|
||||
];
|
||||
mockAuthFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockQuizzes),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useQuizLibrary());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchQuizzes();
|
||||
});
|
||||
|
||||
expect(result.current.quizzes[0].isShared).toBe(true);
|
||||
expect(result.current.quizzes[0].shareToken).toBe('token123');
|
||||
expect(result.current.quizzes[1].isShared).toBe(false);
|
||||
expect(result.current.quizzes[1].shareToken).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue