Files
fotospiel-app/resources/js/guest-v2/__tests__/AiMagicEditSheet.test.tsx
Codex Agent 36bed12ff9
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
feat: implement AI styling foundation and billing scope rework
2026-02-06 20:01:58 +01:00

230 lines
6.6 KiB
TypeScript

import React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
const fetchGuestAiStylesMock = vi.fn();
const createGuestAiEditMock = vi.fn();
const fetchGuestAiEditStatusMock = vi.fn();
const translate = (key: string, options?: unknown, fallback?: string) => {
if (typeof fallback === 'string') {
return fallback;
}
if (typeof options === 'string') {
return options;
}
return key;
};
vi.mock('../services/aiEditsApi', () => ({
fetchGuestAiStyles: (...args: unknown[]) => fetchGuestAiStylesMock(...args),
createGuestAiEdit: (...args: unknown[]) => createGuestAiEditMock(...args),
fetchGuestAiEditStatus: (...args: unknown[]) => fetchGuestAiEditStatusMock(...args),
}));
vi.mock('@/shared/guest/i18n/useTranslation', () => ({
useTranslation: () => ({
t: translate,
}),
}));
vi.mock('../lib/toast', () => ({
pushGuestToast: vi.fn(),
}));
vi.mock('../lib/guestTheme', () => ({
useGuestThemeVariant: () => ({ isDark: false }),
}));
vi.mock('lucide-react', () => ({
Copy: () => <span>copy</span>,
Download: () => <span>download</span>,
Loader2: () => <span>loader</span>,
MessageSquare: () => <span>message</span>,
RefreshCcw: () => <span>refresh</span>,
Share2: () => <span>share</span>,
Sparkles: () => <span>sparkles</span>,
Wand2: () => <span>wand</span>,
X: () => <span>x</span>,
}));
vi.mock('@tamagui/sheet', () => {
const Sheet = ({ open, children }: { open?: boolean; children: React.ReactNode }) => (open ? <div>{children}</div> : null);
Sheet.Overlay = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
Sheet.Frame = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
Sheet.Handle = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
return { Sheet };
});
vi.mock('@tamagui/stacks', () => ({
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('@tamagui/button', () => ({
Button: ({ children, onPress, ...rest }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress} {...rest}>
{children}
</button>
),
}));
import AiMagicEditSheet from '../components/AiMagicEditSheet';
describe('AiMagicEditSheet', () => {
const originalOnLine = navigator.onLine;
beforeEach(() => {
fetchGuestAiStylesMock.mockReset();
createGuestAiEditMock.mockReset();
fetchGuestAiEditStatusMock.mockReset();
Object.defineProperty(window.navigator, 'onLine', {
configurable: true,
value: true,
});
});
afterEach(() => {
vi.useRealTimers();
Object.defineProperty(window.navigator, 'onLine', {
configurable: true,
value: originalOnLine,
});
});
it('loads styles and creates an ai edit request', async () => {
fetchGuestAiStylesMock.mockResolvedValue({
data: [
{
id: 1,
key: 'ghibli-soft',
name: 'Ghibli Soft',
description: 'Soft shading style',
},
],
meta: {},
});
createGuestAiEditMock.mockResolvedValue({
duplicate: false,
data: {
id: 15,
event_id: 2,
photo_id: 7,
status: 'succeeded',
style: { id: 1, key: 'ghibli-soft', name: 'Ghibli Soft' },
outputs: [{ id: 99, provider_url: 'https://example.com/ai.jpg', is_primary: true }],
},
});
render(
<AiMagicEditSheet
open
onOpenChange={vi.fn()}
eventToken="event-token"
photoId={7}
originalImageUrl="/storage/original.jpg"
/>
);
expect(await screen.findByText('Ghibli Soft')).toBeInTheDocument();
fireEvent.click(screen.getByText('Generate AI edit'));
await waitFor(() => expect(createGuestAiEditMock).toHaveBeenCalledTimes(1));
await waitFor(() => expect(screen.getByText('AI result')).toBeInTheDocument());
expect(screen.getByText('Copy link')).toBeInTheDocument();
});
it('shows an error when style loading fails', async () => {
fetchGuestAiStylesMock.mockRejectedValue(new Error('Styles not reachable'));
render(
<AiMagicEditSheet
open
onOpenChange={vi.fn()}
eventToken="event-token"
photoId={7}
originalImageUrl="/storage/original.jpg"
/>
);
expect(await screen.findByText('Styles not reachable')).toBeInTheDocument();
});
it('pauses polling while offline and resumes after reconnect', async () => {
Object.defineProperty(window.navigator, 'onLine', {
configurable: true,
value: false,
});
fetchGuestAiStylesMock.mockResolvedValue({
data: [{ id: 1, key: 'ghibli-soft', name: 'Ghibli Soft' }],
meta: {},
});
createGuestAiEditMock.mockResolvedValue({
duplicate: false,
data: {
id: 22,
event_id: 2,
photo_id: 7,
status: 'processing',
style: { id: 1, key: 'ghibli-soft', name: 'Ghibli Soft' },
outputs: [],
},
});
fetchGuestAiEditStatusMock.mockResolvedValue({
data: {
id: 22,
event_id: 2,
photo_id: 7,
status: 'succeeded',
style: { id: 1, key: 'ghibli-soft', name: 'Ghibli Soft' },
outputs: [{ id: 7, provider_url: 'https://example.com/generated.jpg', is_primary: true }],
},
});
render(
<AiMagicEditSheet
open
onOpenChange={vi.fn()}
eventToken="event-token"
photoId={7}
originalImageUrl="/storage/original.jpg"
/>
);
expect(await screen.findByText('Ghibli Soft')).toBeInTheDocument();
fireEvent.click(screen.getByText('Generate AI edit'));
await waitFor(() => expect(createGuestAiEditMock).toHaveBeenCalledTimes(1));
expect(await screen.findByText('You are offline. Status updates resume automatically when connection is back.')).toBeInTheDocument();
vi.useFakeTimers();
await vi.advanceTimersByTimeAsync(7000);
expect(fetchGuestAiEditStatusMock).toHaveBeenCalledTimes(0);
Object.defineProperty(window.navigator, 'onLine', {
configurable: true,
value: true,
});
await act(async () => {
window.dispatchEvent(new Event('online'));
});
await act(async () => {
await vi.advanceTimersByTimeAsync(3000);
});
await Promise.resolve();
expect(fetchGuestAiEditStatusMock).toHaveBeenCalledTimes(1);
});
});