import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { CustomerPreview } from '../CustomerPreview';
import { Service, Business } from '../../../types';
// Mock dependencies
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
// Mock Lucide icons
vi.mock('lucide-react', () => ({
Clock: () => ,
DollarSign: () => ,
Image: () => ,
CheckCircle2: () => ,
AlertCircle: () => ,
}));
// Mock Badge component
vi.mock('../../ui/Badge', () => ({
default: ({ children, variant, size }: any) =>
{children},
}));
describe('CustomerPreview', () => {
const mockBusiness: Business = {
id: 'business-1',
name: 'Test Business',
primaryColor: '#2563eb',
secondaryColor: '#0ea5e9',
} as Business;
const mockService: Service = {
id: '1',
name: 'Haircut',
description: 'Professional haircut service',
price: 50,
durationMinutes: 60,
photos: [],
category: { id: 'cat1', name: 'Hair Services' },
variable_pricing: false,
} as Service;
const mockServiceWithPhoto: Service = {
...mockService,
photos: ['https://example.com/photo.jpg'],
};
const mockServiceWithDeposit: Service = {
...mockService,
deposit_amount: 10,
};
const mockServiceVariablePricing: Service = {
...mockService,
variable_pricing: true,
deposit_amount: 25,
};
const defaultProps = {
service: mockService,
business: mockBusiness,
};
beforeEach(() => {
vi.clearAllMocks();
});
it('renders customer preview heading', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByText('Customer Preview')).toBeInTheDocument();
});
it('shows live preview badge', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByTestId('badge-info')).toBeInTheDocument();
expect(screen.getByText('Live Preview')).toBeInTheDocument();
});
it('displays service name', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByText('Haircut')).toBeInTheDocument();
});
it('displays service description', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByText('Professional haircut service')).toBeInTheDocument();
});
it('displays service category', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByText('Hair Services')).toBeInTheDocument();
});
it('displays duration', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByText('60 mins')).toBeInTheDocument();
});
it('shows clock icon for duration', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByTestId('icon-clock')).toBeInTheDocument();
});
it('displays price', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByText('50')).toBeInTheDocument();
});
it('shows dollar sign icon for price', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByTestId('icon-dollar-sign')).toBeInTheDocument();
});
it('shows image icon when no photos', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByTestId('icon-image')).toBeInTheDocument();
});
it('displays photo when available', () => {
const props = { ...defaultProps, service: mockServiceWithPhoto };
render(React.createElement(CustomerPreview, props));
const img = document.querySelector('img');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', 'https://example.com/photo.jpg');
expect(img).toHaveAttribute('alt', 'Haircut');
});
it('displays deposit requirement when set', () => {
const props = { ...defaultProps, service: mockServiceWithDeposit };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText(/Deposit required:/)).toBeInTheDocument();
expect(screen.getByText((content, element) => {
return element?.textContent === 'Deposit required: $10';
})).toBeInTheDocument();
});
it('does not show deposit when not required', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.queryByText(/Deposit required:/)).not.toBeInTheDocument();
});
it('shows variable pricing badge', () => {
const props = { ...defaultProps, service: mockServiceVariablePricing };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('Variable')).toBeInTheDocument();
});
it('shows "Price varies" text for variable pricing', () => {
const props = { ...defaultProps, service: mockServiceVariablePricing };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('Price varies')).toBeInTheDocument();
});
it('shows deposit for variable pricing services', () => {
const props = { ...defaultProps, service: mockServiceVariablePricing };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText(/Deposit required:/)).toBeInTheDocument();
});
it('displays info alert about preview', () => {
render(React.createElement(CustomerPreview, defaultProps));
expect(screen.getByTestId('icon-alert-circle')).toBeInTheDocument();
expect(screen.getByText(/This is how your service will appear to customers/)).toBeInTheDocument();
});
it('handles null service gracefully', () => {
const props = { ...defaultProps, service: null };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('New Service')).toBeInTheDocument();
expect(screen.getByText('Service description will appear here...')).toBeInTheDocument();
});
it('displays default category when not set', () => {
const serviceWithoutCategory = { ...mockService, category: undefined };
const props = { ...defaultProps, service: serviceWithoutCategory };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('General')).toBeInTheDocument();
});
it('uses preview data when provided', () => {
const previewData = {
name: 'Custom Name',
description: 'Custom Description',
price: 75,
durationMinutes: 90,
};
const props = { ...defaultProps, previewData };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('Custom Name')).toBeInTheDocument();
expect(screen.getByText('Custom Description')).toBeInTheDocument();
expect(screen.getByText('75')).toBeInTheDocument();
expect(screen.getByText('90 mins')).toBeInTheDocument();
});
it('preview data overrides service data', () => {
const previewData = { name: 'Preview Name' };
const props = { ...defaultProps, previewData };
render(React.createElement(CustomerPreview, props));
// Should show preview data instead of service data
expect(screen.getByText('Preview Name')).toBeInTheDocument();
expect(screen.queryByText('Haircut')).not.toBeInTheDocument();
});
it('formats price correctly', () => {
const props = { ...defaultProps, service: { ...mockService, price: 123 } };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('123')).toBeInTheDocument();
});
it('handles zero price', () => {
const props = { ...defaultProps, service: { ...mockService, price: 0 } };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('0')).toBeInTheDocument();
});
it('applies business colors to gradient', () => {
render(React.createElement(CustomerPreview, defaultProps));
// The gradient uses business colors in the style attribute
const gradientDiv = document.querySelector('[style*="gradient"]');
expect(gradientDiv).toBeInTheDocument();
});
it('displays deposit with correct formatting', () => {
const props = { ...defaultProps, service: { ...mockService, deposit_amount: 15 } };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText((content, element) => {
return element?.textContent === 'Deposit required: $15';
})).toBeInTheDocument();
});
it('shows default duration when not set', () => {
const serviceWithoutDuration = { ...mockService, durationMinutes: undefined };
const props = { ...defaultProps, service: serviceWithoutDuration };
render(React.createElement(CustomerPreview, props));
expect(screen.getByText('30 mins')).toBeInTheDocument();
});
it('displays border styling to indicate selected preview', () => {
render(React.createElement(CustomerPreview, defaultProps));
const card = document.querySelector('.border-brand-600');
expect(card).toBeInTheDocument();
});
it('displays ring styling', () => {
render(React.createElement(CustomerPreview, defaultProps));
const card = document.querySelector('.ring-brand-600');
expect(card).toBeInTheDocument();
});
it('handles preview data with partial updates', () => {
const previewData = { price: 99 };
const props = { ...defaultProps, previewData };
render(React.createElement(CustomerPreview, props));
// Name should still be from service
expect(screen.getByText('Haircut')).toBeInTheDocument();
// Price should be from previewData
expect(screen.getByText('99')).toBeInTheDocument();
});
it('merges photos from preview data', () => {
const previewData = { photos: ['https://preview.com/new.jpg'] };
const props = { ...defaultProps, previewData };
render(React.createElement(CustomerPreview, props));
const img = document.querySelector('img');
expect(img).toHaveAttribute('src', 'https://preview.com/new.jpg');
});
it('handles empty photos array', () => {
const props = { ...defaultProps, service: { ...mockService, photos: [] } };
render(React.createElement(CustomerPreview, props));
expect(screen.getByTestId('icon-image')).toBeInTheDocument();
});
it('uses first photo only for cover', () => {
const serviceWithMultiplePhotos = {
...mockService,
photos: ['https://first.com/1.jpg', 'https://second.com/2.jpg'],
};
const props = { ...defaultProps, service: serviceWithMultiplePhotos };
render(React.createElement(CustomerPreview, props));
const img = document.querySelector('img');
expect(img).toHaveAttribute('src', 'https://first.com/1.jpg');
});
it('displays horizontal card layout', () => {
render(React.createElement(CustomerPreview, defaultProps));
const cardContainer = document.querySelector('.flex.h-full');
expect(cardContainer).toBeInTheDocument();
});
});