chore: remove GitHub Actions workflows, use Woodpecker CI exclusively
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
import { useEffect } from 'react';
|
||||
import { GA_MEASUREMENT_ID } from '@/lib/analytics';
|
||||
|
||||
export function GoogleAnalytics() {
|
||||
useEffect(() => {
|
||||
if (GA_MEASUREMENT_ID) {
|
||||
console.log('Google Analytics initialized:', GA_MEASUREMENT_ID);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!GA_MEASUREMENT_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
window.gtag = gtag;
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${GA_MEASUREMENT_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
send_page_view: false
|
||||
});
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { trackContactForm } from '@/lib/analytics';
|
||||
|
||||
export function ContactFormAnalyticsExample() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
company: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// 追踪表单提交
|
||||
trackContactForm(formData);
|
||||
|
||||
// 提交表单逻辑...
|
||||
console.log('Form submitted:', formData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-1">
|
||||
姓名
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
||||
邮箱
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium mb-1">
|
||||
电话
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="company" className="block text-sm font-medium mb-1">
|
||||
公司
|
||||
</label>
|
||||
<input
|
||||
id="company"
|
||||
type="text"
|
||||
value={formData.company}
|
||||
onChange={(e) => setFormData({ ...formData, company: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium mb-1">
|
||||
留言内容
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg h-32"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
提交
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('next/image', () => {
|
||||
return ({ src, alt, width, height, className, ...props }: any) => (
|
||||
<img src={src} alt={alt} width={width} height={height} className={className} {...props} />
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
Mail: () => <span data-testid="mail-icon" />,
|
||||
Phone: () => <span data-testid="phone-icon" />,
|
||||
MapPin: () => <span data-testid="map-pin-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
description: '以智慧连接数字趋势,以伙伴身份陪您成长',
|
||||
email: 'contact@novalon.cn',
|
||||
phone: '028-88888888',
|
||||
address: '中国四川省成都市龙泉驿区幸福路12号',
|
||||
icp: '蜀ICP备XXXXXXXX号',
|
||||
police: '川公网安备XXXXXXXXXXX号',
|
||||
},
|
||||
NAVIGATION: [
|
||||
{ id: 'home', label: '首页', href: '/' },
|
||||
{ id: 'services', label: '服务', href: '/#services' },
|
||||
{ id: 'products', label: '产品', href: '/#products' },
|
||||
{ id: 'cases', label: '案例', href: '/#cases' },
|
||||
{ id: 'about', label: '关于', href: '/#about' },
|
||||
{ id: 'contact', label: '联系', href: '/contact' },
|
||||
],
|
||||
}));
|
||||
|
||||
import { Footer } from './footer';
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render footer component', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render logo', () => {
|
||||
render(<Footer />);
|
||||
const logo = screen.getByAltText('四川睿新致远科技有限公司');
|
||||
expect(logo).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company description', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('以智慧连接数字趋势,以伙伴身份陪您成长')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render quick links section', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('快速链接')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render service items section', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('服务项目')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render contact information section', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('联系方式')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render navigation links', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
expect(screen.getByText('服务')).toBeInTheDocument();
|
||||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||||
expect(screen.getByText('案例')).toBeInTheDocument();
|
||||
expect(screen.getByText('关于')).toBeInTheDocument();
|
||||
expect(screen.getByText('联系')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render service links', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('软件开发')).toBeInTheDocument();
|
||||
expect(screen.getByText('云服务')).toBeInTheDocument();
|
||||
expect(screen.getByText('数据分析')).toBeInTheDocument();
|
||||
expect(screen.getByText('信息安全')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render contact details', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('contact@novalon.cn')).toBeInTheDocument();
|
||||
expect(screen.getByText('028-88888888')).toBeInTheDocument();
|
||||
expect(screen.getByText('中国四川省成都市龙泉驿区幸福路12号')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Icons', () => {
|
||||
it('should render contact icons', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByTestId('mail-icon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('phone-icon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('map-pin-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legal Links', () => {
|
||||
it('should render privacy policy link', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('隐私政策')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render terms of service link', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('服务条款')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ICP filing info', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('蜀ICP备XXXXXXXX号')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render police filing info', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('川公网安备XXXXXXXXXXX号')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Copyright', () => {
|
||||
it('should render copyright with current year', () => {
|
||||
render(<Footer />);
|
||||
const currentYear = new Date().getFullYear();
|
||||
expect(screen.getByText(new RegExp(`${currentYear}`))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company name in copyright', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText(/四川睿新致远科技有限公司/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper role attribute', () => {
|
||||
render(<Footer />);
|
||||
const footer = screen.getByTestId('footer');
|
||||
expect(footer).toHaveAttribute('role', 'contentinfo');
|
||||
});
|
||||
|
||||
it('should have proper link hrefs', () => {
|
||||
render(<Footer />);
|
||||
const privacyLink = screen.getByText('隐私政策').closest('a');
|
||||
const termsLink = screen.getByText('服务条款').closest('a');
|
||||
|
||||
expect(privacyLink).toHaveAttribute('href', '/privacy');
|
||||
expect(termsLink).toHaveAttribute('href', '/terms');
|
||||
});
|
||||
});
|
||||
|
||||
describe('QR Code Section', () => {
|
||||
it('should render QR code image', () => {
|
||||
render(<Footer />);
|
||||
const qrCode = screen.getByAltText('微信公众号二维码');
|
||||
expect(qrCode).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render QR code description', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('扫码关注获取最新资讯')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
const mockReplace = jest.fn();
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
usePathname: jest.fn(() => '/'),
|
||||
useSearchParams: jest.fn(() => new URLSearchParams()),
|
||||
useRouter: jest.fn(() => ({
|
||||
push: mockPush,
|
||||
replace: mockReplace,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, onClick, ...props }: any) => (
|
||||
<a href={href} onClick={onClick} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('next/image', () => {
|
||||
return ({ src, alt, width, height, className, ...props }: any) => (
|
||||
<img src={src} alt={alt} width={width} height={height} className={className} {...props} />
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
Menu: () => <span data-testid="menu-icon" />,
|
||||
X: () => <span data-testid="x-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, asChild, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
shortName: '睿新致遠',
|
||||
},
|
||||
NAVIGATION: [
|
||||
{ id: 'home', label: '首页', href: '/' },
|
||||
{ id: 'services', label: '服务', href: '/#services' },
|
||||
{ id: 'products', label: '产品', href: '/#products' },
|
||||
{ id: 'cases', label: '案例', href: '/#cases' },
|
||||
{ id: 'about', label: '关于', href: '/#about' },
|
||||
{ id: 'contact', label: '联系', href: '/contact' },
|
||||
],
|
||||
}));
|
||||
|
||||
jest.mock('@/hooks/use-focus-trap', () => ({
|
||||
useFocusTrap: () => ({ current: null }),
|
||||
}));
|
||||
|
||||
import { Header } from './header';
|
||||
|
||||
describe('Header', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render header component', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByRole('banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render logo', () => {
|
||||
render(<Header />);
|
||||
const logo = screen.getByAltText('四川睿新致远科技有限公司');
|
||||
expect(logo).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render desktop navigation', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByTestId('desktop-navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render navigation items', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
expect(screen.getByText('服务')).toBeInTheDocument();
|
||||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||||
expect(screen.getByText('案例')).toBeInTheDocument();
|
||||
expect(screen.getByText('关于')).toBeInTheDocument();
|
||||
expect(screen.getByText('联系')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render consult button', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByTestId('consult-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render mobile menu button', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByTestId('mobile-menu-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mobile Menu', () => {
|
||||
it('should toggle mobile menu on button click', async () => {
|
||||
render(<Header />);
|
||||
const menuButton = screen.getByTestId('mobile-menu-button');
|
||||
|
||||
expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('mobile-navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('mobile-navigation')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show X icon when menu is open', async () => {
|
||||
render(<Header />);
|
||||
const menuButton = screen.getByTestId('mobile-menu-button');
|
||||
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('x-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show Menu icon when menu is closed', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByTestId('menu-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper ARIA attributes on menu button', () => {
|
||||
render(<Header />);
|
||||
const menuButton = screen.getByTestId('mobile-menu-button');
|
||||
|
||||
expect(menuButton).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(menuButton).toHaveAttribute('aria-controls', 'mobile-menu');
|
||||
expect(menuButton).toHaveAttribute('aria-label', '打开菜单');
|
||||
});
|
||||
|
||||
it('should update aria-expanded when menu is toggled', async () => {
|
||||
render(<Header />);
|
||||
const menuButton = screen.getByTestId('mobile-menu-button');
|
||||
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(menuButton).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(menuButton).toHaveAttribute('aria-label', '关闭菜单');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have proper navigation role', () => {
|
||||
render(<Header />);
|
||||
const nav = screen.getByTestId('desktop-navigation');
|
||||
expect(nav).toHaveAttribute('role', 'navigation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should have correct href for navigation items', () => {
|
||||
render(<Header />);
|
||||
const homeLink = screen.getByText('首页').closest('a');
|
||||
const contactLink = screen.getByText('联系').closest('a');
|
||||
|
||||
expect(homeLink).toHaveAttribute('href', '/');
|
||||
expect(contactLink).toHaveAttribute('href', '/contact');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MobileMenu } from './mobile-menu';
|
||||
|
||||
jest.mock('@/hooks/use-focus-trap', () => ({
|
||||
useFocusTrap: () => ({ current: null }),
|
||||
}));
|
||||
|
||||
describe('MobileMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render menu button', () => {
|
||||
render(<MobileMenu />);
|
||||
expect(screen.getByRole('button', { name: '打开菜单' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render menu icon when closed', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render menu panel when closed', () => {
|
||||
render(<MobileMenu />);
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Opening Menu', () => {
|
||||
it('should open menu when button clicked', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should change button label when open', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.getByRole('button', { name: '关闭菜单' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render navigation items', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
expect(screen.getByText('核心业务')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Closing Menu', () => {
|
||||
it('should close menu when button clicked again', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: '关闭菜单' });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close menu when overlay clicked', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
const overlay = document.querySelector('.fixed.inset-0');
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay);
|
||||
}
|
||||
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard Navigation', () => {
|
||||
it('should open menu with Enter key', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.keyDown(button, { key: 'Enter' });
|
||||
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open menu with Space key', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.keyDown(button, { key: ' ' });
|
||||
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close menu with Escape key', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
fireEvent.keyDown(button, { key: 'Escape' });
|
||||
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have aria-expanded attribute', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
it('should update aria-expanded when open', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('should have aria-controls attribute', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveAttribute('aria-controls', 'mobile-menu-panel');
|
||||
});
|
||||
|
||||
it('should have navigation role', () => {
|
||||
render(<MobileMenu />);
|
||||
const button = screen.getByRole('button', { name: '打开菜单' });
|
||||
fireEvent.click(button);
|
||||
|
||||
const nav = screen.getByRole('navigation');
|
||||
expect(nav).toHaveAttribute('aria-label', '移动端导航');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have responsive visibility', () => {
|
||||
const { container } = render(<MobileMenu />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass('lg:hidden');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<MobileMenu className="custom-class" />);
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MobileTabBar } from './mobile-tab-bar';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
usePathname: () => '/',
|
||||
}));
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
describe('MobileTabBar', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render tab bar', () => {
|
||||
render(<MobileTabBar />);
|
||||
const nav = screen.getByRole('navigation');
|
||||
expect(nav).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all tabs', () => {
|
||||
render(<MobileTabBar />);
|
||||
expect(screen.getByText('首页')).toBeInTheDocument();
|
||||
expect(screen.getByText('服务')).toBeInTheDocument();
|
||||
expect(screen.getByText('产品')).toBeInTheDocument();
|
||||
expect(screen.getByText('新闻')).toBeInTheDocument();
|
||||
expect(screen.getByText('联系')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render tab icons', () => {
|
||||
render(<MobileTabBar />);
|
||||
const icons = document.querySelectorAll('svg');
|
||||
expect(icons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Active State', () => {
|
||||
it('should highlight active tab', () => {
|
||||
render(<MobileTabBar />);
|
||||
const homeTab = screen.getByText('首页').closest('a');
|
||||
expect(homeTab).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show active indicator', () => {
|
||||
render(<MobileTabBar />);
|
||||
const activeIndicator = document.querySelector('.bg-\\[\\#C41E3A\\]');
|
||||
expect(activeIndicator).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation Links', () => {
|
||||
it('should have correct href for home', () => {
|
||||
render(<MobileTabBar />);
|
||||
const homeLink = screen.getByText('首页').closest('a');
|
||||
expect(homeLink).toHaveAttribute('href', '/');
|
||||
});
|
||||
|
||||
it('should have correct href for services', () => {
|
||||
render(<MobileTabBar />);
|
||||
const servicesLink = screen.getByText('服务').closest('a');
|
||||
expect(servicesLink).toHaveAttribute('href', '/#services');
|
||||
});
|
||||
|
||||
it('should have correct href for products', () => {
|
||||
render(<MobileTabBar />);
|
||||
const productsLink = screen.getByText('产品').closest('a');
|
||||
expect(productsLink).toHaveAttribute('href', '/#products');
|
||||
});
|
||||
|
||||
it('should have correct href for news', () => {
|
||||
render(<MobileTabBar />);
|
||||
const newsLink = screen.getByText('新闻').closest('a');
|
||||
expect(newsLink).toHaveAttribute('href', '/#news');
|
||||
});
|
||||
|
||||
it('should have correct href for contact', () => {
|
||||
render(<MobileTabBar />);
|
||||
const contactLink = screen.getByText('联系').closest('a');
|
||||
expect(contactLink).toHaveAttribute('href', '/#contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have navigation role', () => {
|
||||
render(<MobileTabBar />);
|
||||
const nav = screen.getByRole('navigation');
|
||||
expect(nav).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have accessible tab labels', () => {
|
||||
render(<MobileTabBar />);
|
||||
const tabs = screen.getAllByRole('link');
|
||||
expect(tabs.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have fixed positioning', () => {
|
||||
render(<MobileTabBar />);
|
||||
const nav = screen.getByRole('navigation');
|
||||
expect(nav).toHaveClass('fixed');
|
||||
expect(nav).toHaveClass('bottom-0');
|
||||
});
|
||||
|
||||
it('should have responsive visibility', () => {
|
||||
render(<MobileTabBar />);
|
||||
const nav = screen.getByRole('navigation');
|
||||
expect(nav).toHaveClass('md:hidden');
|
||||
});
|
||||
|
||||
it('should have backdrop blur', () => {
|
||||
render(<MobileTabBar />);
|
||||
const nav = screen.getByRole('navigation');
|
||||
expect(nav).toHaveClass('backdrop-blur-xl');
|
||||
});
|
||||
|
||||
it('should have proper height', () => {
|
||||
render(<MobileTabBar />);
|
||||
const nav = screen.getByRole('navigation');
|
||||
const heightDiv = nav.querySelector('.h-16');
|
||||
expect(heightDiv).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { AboutSection } from './about-section';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
describe('AboutSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render about section', () => {
|
||||
render(<AboutSection />);
|
||||
const section = document.querySelector('section#about');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<AboutSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company slogan', () => {
|
||||
render(<AboutSection />);
|
||||
expect(screen.getByText(/企业需要的/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company mission', () => {
|
||||
render(<AboutSection />);
|
||||
expect(screen.getByText(/我们只做一件事/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Statistics', () => {
|
||||
it('should render statistics cards', () => {
|
||||
render(<AboutSection />);
|
||||
const cards = document.querySelectorAll('.text-3xl');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display statistics in grid layout', () => {
|
||||
const { container } = render(<AboutSection />);
|
||||
const grid = container.querySelector('.grid-cols-2');
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Call to Action', () => {
|
||||
it('should render learn more button', () => {
|
||||
render(<AboutSection />);
|
||||
expect(screen.getByRole('link', { name: /了解更多关于我们/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should link to about page', () => {
|
||||
render(<AboutSection />);
|
||||
const link = screen.getByRole('link', { name: /了解更多关于我们/ });
|
||||
expect(link).toHaveAttribute('href', '/about');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have region role', () => {
|
||||
render(<AboutSection />);
|
||||
const section = screen.getByRole('region');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-labelledby attribute', () => {
|
||||
render(<AboutSection />);
|
||||
const section = document.querySelector('section#about');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'about-heading');
|
||||
});
|
||||
|
||||
it('should have accessible heading', () => {
|
||||
render(<AboutSection />);
|
||||
const heading = screen.getByRole('heading', { level: 2 });
|
||||
expect(heading).toHaveAttribute('id', 'about-heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have background color', () => {
|
||||
render(<AboutSection />);
|
||||
const section = document.querySelector('section#about');
|
||||
expect(section).toHaveClass('bg-[#FAFAFA]');
|
||||
});
|
||||
|
||||
it('should have proper padding', () => {
|
||||
render(<AboutSection />);
|
||||
const section = document.querySelector('section#about');
|
||||
expect(section).toHaveClass('py-24');
|
||||
});
|
||||
|
||||
it('should have decorative background pattern', () => {
|
||||
const { container } = render(<AboutSection />);
|
||||
const pattern = container.querySelector('.absolute.inset-0');
|
||||
expect(pattern).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { CasesSection } from './cases-section';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
CASES: [
|
||||
{
|
||||
id: 'case-1',
|
||||
client: '测试客户',
|
||||
title: '测试案例',
|
||||
description: '测试描述',
|
||||
industry: '制造业',
|
||||
results: [{ value: '40%', label: '效率提升' }],
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
client: '测试客户2',
|
||||
title: '测试案例2',
|
||||
description: '测试描述2',
|
||||
industry: '零售业',
|
||||
results: [{ value: '50%', label: '成本降低' }],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('CasesSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render cases section', () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section#cases');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText(/我们与优秀的企业同行/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render case cards', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('测试案例')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render client names', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('测试客户')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render industry badges', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('制造业')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render results', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('40%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render view more button', () => {
|
||||
render(<CasesSection />);
|
||||
expect(screen.getByText('查看更多案例')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have section id', () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section#cases');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have region role', () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section[role="region"]');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-labelledby', () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section[aria-labelledby="cases-heading"]');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct background', () => {
|
||||
render(<CasesSection />);
|
||||
const section = document.querySelector('section.bg-white');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have container', () => {
|
||||
render(<CasesSection />);
|
||||
const container = document.querySelector('.container-wide');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { describe, it, expect, jest, beforeAll, afterEach } from '@jest/globals';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
Mail: () => <span data-testid="mail-icon" />,
|
||||
Phone: () => <span data-testid="phone-icon" />,
|
||||
MapPin: () => <span data-testid="map-pin-icon" />,
|
||||
Send: () => <span data-testid="send-icon" />,
|
||||
Loader2: () => <span data-testid="loader-icon" />,
|
||||
Clock: () => <span data-testid="clock-icon" />,
|
||||
HeadphonesIcon: () => <span data-testid="headphones-icon" />,
|
||||
CheckCircle2: () => <span data-testid="check-circle-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/sanitize', () => ({
|
||||
sanitizeInput: (value: string) => value,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/csrf', () => ({
|
||||
generateCSRFToken: jest.fn(() => 'test-csrf-token'),
|
||||
setCSRFTokenToStorage: jest.fn(),
|
||||
getCSRFTokenFromStorage: jest.fn(() => 'test-csrf-token'),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
email: 'contact@novalon.cn',
|
||||
phone: '028-88888888',
|
||||
address: '中国四川省成都市龙泉驿区幸福路12号',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, className, disabled, ...props }: any) => (
|
||||
<button className={className} disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/input', () => ({
|
||||
Input: ({ label, id, placeholder, required, value, onChange, onBlur, error, ...props }: any) => (
|
||||
<div>
|
||||
<label htmlFor={id}>{label}{required && '*'}</label>
|
||||
<input
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
data-testid={`${id}-input`}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span data-testid={`${id}-error`}>{error}</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/textarea', () => ({
|
||||
Textarea: ({ label, id, placeholder, rows, required, value, onChange, onBlur, error, ...props }: any) => (
|
||||
<div>
|
||||
<label htmlFor={id}>{label}{required && '*'}</label>
|
||||
<textarea
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
data-testid={`${id}-input`}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span data-testid={`${id}-error`}>{error}</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/toast', () => ({
|
||||
Toast: ({ message, type, onClose, ...props }: any) => (
|
||||
<div data-testid="toast-notification" data-type={type} {...props}>
|
||||
{message}
|
||||
<button onClick={onClose}>关闭</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
import { ContactSection } from './contact-section';
|
||||
|
||||
describe('ContactSection', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render contact section', () => {
|
||||
render(<ContactSection />);
|
||||
const section = document.querySelector('section#contact');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render contact form', () => {
|
||||
render(<ContactSection />);
|
||||
expect(screen.getByTestId('name-input')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('phone-input')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('email-input')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('message-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render submit button', () => {
|
||||
render(<ContactSection />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company contact information', () => {
|
||||
render(<ContactSection />);
|
||||
expect(screen.getByText('contact@novalon.cn')).toBeInTheDocument();
|
||||
expect(screen.getByText('028-88888888')).toBeInTheDocument();
|
||||
expect(screen.getByText('中国四川省成都市龙泉驿区幸福路12号')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render work hours card', () => {
|
||||
render(<ContactSection />);
|
||||
expect(screen.getByTestId('work-hours-card')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('should show error for invalid name', async () => {
|
||||
render(<ContactSection />);
|
||||
const nameInput = screen.getByTestId('name-input');
|
||||
|
||||
await userEvent.type(nameInput, '张');
|
||||
fireEvent.blur(nameInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('name-error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error for invalid phone', async () => {
|
||||
render(<ContactSection />);
|
||||
const phoneInput = screen.getByTestId('phone-input');
|
||||
|
||||
await userEvent.type(phoneInput, '1234567890');
|
||||
fireEvent.blur(phoneInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('phone-error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error for invalid email', async () => {
|
||||
render(<ContactSection />);
|
||||
const emailInput = screen.getByTestId('email-input');
|
||||
|
||||
await userEvent.type(emailInput, 'invalid-email');
|
||||
fireEvent.blur(emailInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('email-error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error for short message', async () => {
|
||||
render(<ContactSection />);
|
||||
const messageInput = screen.getByTestId('message-input');
|
||||
|
||||
await userEvent.type(messageInput, '短留言');
|
||||
fireEvent.blur(messageInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('message-error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper form labels', () => {
|
||||
render(<ContactSection />);
|
||||
|
||||
expect(screen.getByLabelText(/姓名/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/电话/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/邮箱/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/留言/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper ARIA attributes', () => {
|
||||
render(<ContactSection />);
|
||||
const section = document.querySelector('section#contact');
|
||||
expect(section).toHaveAttribute('role', 'region');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'contact-heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF Protection', () => {
|
||||
it('should generate CSRF token on mount', () => {
|
||||
const { generateCSRFToken, setCSRFTokenToStorage } = require('@/lib/csrf');
|
||||
render(<ContactSection />);
|
||||
|
||||
expect(generateCSRFToken).toHaveBeenCalled();
|
||||
expect(setCSRFTokenToStorage).toHaveBeenCalledWith('test-csrf-token');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, it, expect, jest, beforeAll } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, className, ...props }: any) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
section: ({ children, className, ...props }: any) => (
|
||||
<section className={className} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
span: ({ children, className, ...props }: any) => (
|
||||
<span className={className} {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
h1: ({ children, className, ...props }: any) => (
|
||||
<h1 className={className} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
},
|
||||
AnimatePresence: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, ...props }: any) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('lucide-react', () => ({
|
||||
ArrowRight: () => <span data-testid="arrow-right" />,
|
||||
Shield: () => <span data-testid="shield-icon" />,
|
||||
Zap: () => <span data-testid="zap-icon" />,
|
||||
Award: () => <span data-testid="award-icon" />,
|
||||
}));
|
||||
|
||||
jest.mock('next/dynamic', () => {
|
||||
const React = require('react');
|
||||
return {
|
||||
__esModule: true,
|
||||
default: (importFn: any, options: any) => {
|
||||
return React.forwardRef((props: any, ref: any) => {
|
||||
return null;
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@/components/ui/ripple-button', () => ({
|
||||
RippleButton: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SealButton: ({ children, className, ...props }: any) => (
|
||||
<button className={className} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/animations', () => ({
|
||||
GradientText: ({ children, className }: any) => (
|
||||
<span className={className}>{children}</span>
|
||||
),
|
||||
MagneticButton: ({ children, className }: any) => (
|
||||
<button className={className}>{children}</button>
|
||||
),
|
||||
BlurReveal: ({ children, className }: any) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
CounterWithEffect: ({ end, suffix, className }: any) => (
|
||||
<span className={className}>{end}{suffix || ''}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/constants', () => ({
|
||||
COMPANY_INFO: {
|
||||
name: '四川睿新致远科技有限公司',
|
||||
shortName: '睿新致遠',
|
||||
description: '以智慧连接数字趋势,以伙伴身份陪您成长',
|
||||
},
|
||||
STATS: [
|
||||
{ value: '10+', label: '企业客户' },
|
||||
{ value: '20+', label: '成功案例' },
|
||||
{ value: '30+', label: '项目交付' },
|
||||
{ value: '12+', label: '年行业经验' },
|
||||
],
|
||||
}));
|
||||
|
||||
import { HeroSection } from './hero-section';
|
||||
|
||||
describe('HeroSection', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render hero section', () => {
|
||||
render(<HeroSection />);
|
||||
const section = document.querySelector('section#home');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render company name', () => {
|
||||
render(<HeroSection />);
|
||||
expect(screen.getByText('睿新致遠')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render features', () => {
|
||||
render(<HeroSection />);
|
||||
expect(screen.getByText('安全可靠')).toBeInTheDocument();
|
||||
expect(screen.getByText('高效便捷')).toBeInTheDocument();
|
||||
expect(screen.getByText('专业服务')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Statistics', () => {
|
||||
it('should render statistics section', () => {
|
||||
render(<HeroSection />);
|
||||
expect(screen.getByText('企业客户')).toBeInTheDocument();
|
||||
expect(screen.getByText('成功案例')).toBeInTheDocument();
|
||||
expect(screen.getByText('项目交付')).toBeInTheDocument();
|
||||
expect(screen.getByText('年行业经验')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper ARIA labels', () => {
|
||||
render(<HeroSection />);
|
||||
const section = document.querySelector('section#home');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'hero-heading');
|
||||
});
|
||||
|
||||
it('should have accessible buttons', () => {
|
||||
render(<HeroSection />);
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { InsightsSection } from './insights-section';
|
||||
|
||||
jest.mock('@/components/ui/insight-card', () => ({
|
||||
InsightCard: ({ title, category }: any) => (
|
||||
<div data-testid="insight-card">
|
||||
<div>{title}</div>
|
||||
<div>{category}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('InsightsSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render insights section', () => {
|
||||
render(<InsightsSection />);
|
||||
const section = document.querySelector('section#insights');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<InsightsSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
render(<InsightsSection />);
|
||||
expect(screen.getByText(/分享前沿技术趋势/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render insight cards', () => {
|
||||
render(<InsightsSection />);
|
||||
const cards = screen.getAllByTestId('insight-card');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render insight titles', () => {
|
||||
render(<InsightsSection />);
|
||||
expect(screen.getByText('2025年技术趋势:AI驱动的数字化转型')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render insight categories', () => {
|
||||
render(<InsightsSection />);
|
||||
expect(screen.getByText('技术趋势')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render view all button', () => {
|
||||
render(<InsightsSection />);
|
||||
expect(screen.getByText('查看全部洞察')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have section id', () => {
|
||||
render(<InsightsSection />);
|
||||
const section = document.querySelector('section#insights');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct background', () => {
|
||||
render(<InsightsSection />);
|
||||
const section = document.querySelector('section.bg-\\[\\#FAFAFA\\]');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have container', () => {
|
||||
render(<InsightsSection />);
|
||||
const container = document.querySelector('.container-wide');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have grid layout', () => {
|
||||
render(<InsightsSection />);
|
||||
const grid = document.querySelector('.grid');
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { NewsSection } from './news-section';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
describe('NewsSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render news section', () => {
|
||||
render(<NewsSection />);
|
||||
const section = document.querySelector('section#news');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<NewsSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
render(<NewsSection />);
|
||||
expect(screen.getByText(/了解公司最新动态/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('News Cards', () => {
|
||||
it('should render news cards', () => {
|
||||
render(<NewsSection />);
|
||||
const cards = document.querySelectorAll('[class*="flex-col"]');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display news in grid layout', () => {
|
||||
const { container } = render(<NewsSection />);
|
||||
const grid = container.querySelector('.grid-cols-1');
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render news categories', () => {
|
||||
render(<NewsSection />);
|
||||
const categories = document.querySelectorAll('[class*="rounded-full"]');
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render news dates', () => {
|
||||
render(<NewsSection />);
|
||||
const dates = document.querySelectorAll('[class*="text-sm"]');
|
||||
expect(dates.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Call to Action', () => {
|
||||
it('should render view all news link', () => {
|
||||
render(<NewsSection />);
|
||||
expect(screen.getByRole('link', { name: /查看全部新闻/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should link to news page', () => {
|
||||
render(<NewsSection />);
|
||||
const link = screen.getByRole('link', { name: /查看全部新闻/ });
|
||||
expect(link).toHaveAttribute('href', '/news');
|
||||
});
|
||||
|
||||
it('should render read more links', () => {
|
||||
render(<NewsSection />);
|
||||
const readMoreLinks = screen.getAllByText(/阅读更多/);
|
||||
expect(readMoreLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have region role', () => {
|
||||
render(<NewsSection />);
|
||||
const section = screen.getByRole('region');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-labelledby attribute', () => {
|
||||
render(<NewsSection />);
|
||||
const section = document.querySelector('section#news');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'news-heading');
|
||||
});
|
||||
|
||||
it('should have accessible heading', () => {
|
||||
render(<NewsSection />);
|
||||
const heading = screen.getByRole('heading', { level: 2 });
|
||||
expect(heading).toHaveAttribute('id', 'news-heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have background color', () => {
|
||||
render(<NewsSection />);
|
||||
const section = document.querySelector('section#news');
|
||||
expect(section).toHaveClass('bg-[#F5F5F5]');
|
||||
});
|
||||
|
||||
it('should have proper padding', () => {
|
||||
render(<NewsSection />);
|
||||
const section = document.querySelector('section#news');
|
||||
expect(section).toHaveClass('py-24');
|
||||
});
|
||||
|
||||
it('should have container styling', () => {
|
||||
const { container } = render(<NewsSection />);
|
||||
const containerDiv = container.querySelector('.container-custom');
|
||||
expect(containerDiv).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ProductsSection } from './products-section';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
describe('ProductsSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render products section', () => {
|
||||
render(<ProductsSection />);
|
||||
const section = document.querySelector('section#products');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<ProductsSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
render(<ProductsSection />);
|
||||
expect(screen.getByText(/自主研发的企业级产品/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Product Cards', () => {
|
||||
it('should render product cards', () => {
|
||||
render(<ProductsSection />);
|
||||
const cards = document.querySelectorAll('[class*="flex-col"]');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display products in grid layout', () => {
|
||||
const { container } = render(<ProductsSection />);
|
||||
const grid = container.querySelector('.grid-cols-1');
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render product categories', () => {
|
||||
render(<ProductsSection />);
|
||||
const badges = document.querySelectorAll('[class*="rounded-full"]');
|
||||
expect(badges.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render product features', () => {
|
||||
render(<ProductsSection />);
|
||||
const features = document.querySelectorAll('[class*="inline-flex"]');
|
||||
expect(features.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Solution Section', () => {
|
||||
it('should render custom solution section', () => {
|
||||
render(<ProductsSection />);
|
||||
expect(screen.getByText(/需要定制化解决方案/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render custom solution description', () => {
|
||||
render(<ProductsSection />);
|
||||
expect(screen.getByText(/我们的专业团队/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render contact button', () => {
|
||||
render(<ProductsSection />);
|
||||
expect(screen.getByRole('link', { name: /联系我们/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should link to contact page', () => {
|
||||
render(<ProductsSection />);
|
||||
const link = screen.getByRole('link', { name: /联系我们/ });
|
||||
expect(link).toHaveAttribute('href', '/contact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have region role', () => {
|
||||
render(<ProductsSection />);
|
||||
const section = screen.getByRole('region');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-labelledby attribute', () => {
|
||||
render(<ProductsSection />);
|
||||
const section = document.querySelector('section#products');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'products-heading');
|
||||
});
|
||||
|
||||
it('should have accessible heading', () => {
|
||||
render(<ProductsSection />);
|
||||
const heading = screen.getByRole('heading', { level: 2 });
|
||||
expect(heading).toHaveAttribute('id', 'products-heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have background color', () => {
|
||||
render(<ProductsSection />);
|
||||
const section = document.querySelector('section#products');
|
||||
expect(section).toHaveClass('bg-[#F5F7FA]');
|
||||
});
|
||||
|
||||
it('should have proper padding', () => {
|
||||
render(<ProductsSection />);
|
||||
const section = document.querySelector('section#products');
|
||||
expect(section).toHaveClass('py-24');
|
||||
});
|
||||
|
||||
it('should have decorative background elements', () => {
|
||||
const { container } = render(<ProductsSection />);
|
||||
const decorativeElements = container.querySelectorAll('.blur-3xl');
|
||||
expect(decorativeElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ServicesSection } from './services-section';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: any) => <a href={href}>{children}</a>;
|
||||
});
|
||||
|
||||
describe('ServicesSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render services section', () => {
|
||||
render(<ServicesSection />);
|
||||
const section = document.querySelector('section#services');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<ServicesSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
render(<ServicesSection />);
|
||||
expect(screen.getByText(/专业技术团队/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Service Cards', () => {
|
||||
it('should render service cards', () => {
|
||||
render(<ServicesSection />);
|
||||
const cards = document.querySelectorAll('.p-6');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display services in grid layout', () => {
|
||||
const { container } = render(<ServicesSection />);
|
||||
const grid = container.querySelector('.grid-cols-1');
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render service icons', () => {
|
||||
render(<ServicesSection />);
|
||||
const icons = document.querySelectorAll('svg');
|
||||
expect(icons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Call to Action', () => {
|
||||
it('should render view all services button', () => {
|
||||
render(<ServicesSection />);
|
||||
expect(screen.getByRole('link', { name: /查看全部服务/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should link to services page', () => {
|
||||
render(<ServicesSection />);
|
||||
const link = screen.getByRole('link', { name: /查看全部服务/ });
|
||||
expect(link).toHaveAttribute('href', '/services');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have section with id', () => {
|
||||
render(<ServicesSection />);
|
||||
const section = document.querySelector('section#services');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-labelledby attribute', () => {
|
||||
render(<ServicesSection />);
|
||||
const section = document.querySelector('section#services');
|
||||
expect(section).toHaveAttribute('aria-labelledby', 'services-heading');
|
||||
});
|
||||
|
||||
it('should have accessible heading', () => {
|
||||
render(<ServicesSection />);
|
||||
const heading = screen.getByRole('heading', { level: 2 });
|
||||
expect(heading).toHaveAttribute('id', 'services-heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have white background', () => {
|
||||
render(<ServicesSection />);
|
||||
const section = document.querySelector('section#services');
|
||||
expect(section).toHaveClass('bg-white');
|
||||
});
|
||||
|
||||
it('should have proper padding', () => {
|
||||
render(<ServicesSection />);
|
||||
const section = document.querySelector('section#services');
|
||||
expect(section).toHaveClass('py-24');
|
||||
});
|
||||
|
||||
it('should have decorative background elements', () => {
|
||||
const { container } = render(<ServicesSection />);
|
||||
const decorativeElements = container.querySelectorAll('.blur-3xl');
|
||||
expect(decorativeElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { TestimonialsSection } from './testimonials-section';
|
||||
|
||||
jest.mock('@/components/ui/testimonial-card', () => ({
|
||||
TestimonialCard: ({ author, quote }: any) => (
|
||||
<div data-testid="testimonial-card">
|
||||
<div>{author}</div>
|
||||
<div>{quote}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('TestimonialsSection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render testimonials section', () => {
|
||||
render(<TestimonialsSection />);
|
||||
const section = document.querySelector('section#testimonials');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section heading', () => {
|
||||
render(<TestimonialsSection />);
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render section description', () => {
|
||||
render(<TestimonialsSection />);
|
||||
expect(screen.getByText(/听听我们的客户怎么说/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render testimonial cards', () => {
|
||||
render(<TestimonialsSection />);
|
||||
const cards = screen.getAllByTestId('testimonial-card');
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render testimonial authors', () => {
|
||||
render(<TestimonialsSection />);
|
||||
expect(screen.getByText('张总')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render testimonial quotes', () => {
|
||||
render(<TestimonialsSection />);
|
||||
expect(screen.getByText(/睿新致远的团队非常专业/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have section id', () => {
|
||||
render(<TestimonialsSection />);
|
||||
const section = document.querySelector('section#testimonials');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct background', () => {
|
||||
render(<TestimonialsSection />);
|
||||
const section = document.querySelector('section.bg-white');
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have container', () => {
|
||||
render(<TestimonialsSection />);
|
||||
const container = document.querySelector('.container-wide');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have grid layout', () => {
|
||||
render(<TestimonialsSection />);
|
||||
const grid = document.querySelector('.grid');
|
||||
expect(grid).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { InkCard, GeometricCard, FlipCard, TiltCard, GlowCard, ExpandCard } from './animated-card';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, onHoverStart, onHoverEnd, onClick, ...props }: any) => (
|
||||
<div
|
||||
{...props}
|
||||
onMouseEnter={onHoverStart}
|
||||
onMouseLeave={onHoverEnd}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Animated Cards', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('InkCard', () => {
|
||||
it('should render ink card', () => {
|
||||
render(<InkCard>Test Content</InkCard>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<InkCard className="custom-class">Test</InkCard>);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should handle mouse move', () => {
|
||||
const { container } = render(<InkCard>Test</InkCard>);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.mouseMove(card, {
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle hover events', () => {
|
||||
const { container } = render(<InkCard>Test</InkCard>);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.mouseEnter(card);
|
||||
fireEvent.mouseLeave(card);
|
||||
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GeometricCard', () => {
|
||||
it('should render geometric card', () => {
|
||||
render(<GeometricCard>Test Content</GeometricCard>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<GeometricCard className="custom-class">Test</GeometricCard>);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should have corner decorations', () => {
|
||||
const { container } = render(<GeometricCard>Test</GeometricCard>);
|
||||
const corners = container.querySelectorAll('.absolute');
|
||||
expect(corners.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FlipCard', () => {
|
||||
it('should render flip card', () => {
|
||||
render(
|
||||
<FlipCard front="Front" back="Back" />
|
||||
);
|
||||
expect(screen.getByText('Front')).toBeInTheDocument();
|
||||
expect(screen.getByText('Back')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should flip on click', () => {
|
||||
const { container } = render(
|
||||
<FlipCard front="Front" back="Back" />
|
||||
);
|
||||
|
||||
const card = container.firstChild as HTMLElement;
|
||||
fireEvent.click(card);
|
||||
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<FlipCard front="Front" back="Back" className="custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TiltCard', () => {
|
||||
it('should render tilt card', () => {
|
||||
render(<TiltCard>Test Content</TiltCard>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<TiltCard className="custom-class">Test</TiltCard>);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should handle mouse move', () => {
|
||||
const { container } = render(<TiltCard>Test</TiltCard>);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.mouseMove(card, {
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle mouse leave', () => {
|
||||
const { container } = render(<TiltCard>Test</TiltCard>);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.mouseLeave(card);
|
||||
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GlowCard', () => {
|
||||
it('should render glow card', () => {
|
||||
render(<GlowCard>Test Content</GlowCard>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<GlowCard className="custom-class">Test</GlowCard>);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should handle mouse move', () => {
|
||||
const { container } = render(<GlowCard>Test</GlowCard>);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.mouseMove(card, {
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
});
|
||||
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExpandCard', () => {
|
||||
it('should render expand card', () => {
|
||||
render(<ExpandCard>Test Content</ExpandCard>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<ExpandCard className="custom-class">Test</ExpandCard>);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should expand on click', () => {
|
||||
const { container } = render(
|
||||
<ExpandCard expandedContent={<div>Expanded</div>}>Test</ExpandCard>
|
||||
);
|
||||
|
||||
const card = container.firstChild as HTMLElement;
|
||||
fireEvent.click(card);
|
||||
|
||||
expect(screen.getByText('Expanded')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { AnimatedNumber, StatCard } from './animated-number';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
span: ({ children, ...props }: any) => <span {...props}>{children}</span>,
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
describe('AnimatedNumber', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render number', () => {
|
||||
render(<AnimatedNumber value={100} />);
|
||||
expect(screen.getByText('0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with prefix', () => {
|
||||
render(<AnimatedNumber value={100} prefix="$" />);
|
||||
expect(screen.getByText(/\$0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with suffix', () => {
|
||||
render(<AnimatedNumber value={100} suffix="+" />);
|
||||
expect(screen.getByText(/0\+/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with prefix and suffix', () => {
|
||||
render(<AnimatedNumber value={100} prefix="$" suffix="+" />);
|
||||
expect(screen.getByText(/\$0\+/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with custom className', () => {
|
||||
const { container } = render(<AnimatedNumber value={100} className="custom-class" />);
|
||||
const element = container.querySelector('.custom-class');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animation', () => {
|
||||
it('should accept duration prop', () => {
|
||||
render(<AnimatedNumber value={100} duration={3000} />);
|
||||
const element = screen.getByText('0');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept delay prop', () => {
|
||||
render(<AnimatedNumber value={100} delay={500} />);
|
||||
const element = screen.getByText('0');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should start from 0', () => {
|
||||
render(<AnimatedNumber value={100} />);
|
||||
const element = screen.getByText('0');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle zero value', () => {
|
||||
render(<AnimatedNumber value={0} />);
|
||||
const element = screen.getByText('0');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle large numbers', () => {
|
||||
render(<AnimatedNumber value={1000000} />);
|
||||
const element = screen.getByText('0');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle decimal numbers', () => {
|
||||
render(<AnimatedNumber value={99} />);
|
||||
const element = screen.getByText('0');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('StatCard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render stat card', () => {
|
||||
render(<StatCard value={100} label="Users" />);
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with prefix', () => {
|
||||
render(<StatCard value={100} label="Revenue" prefix="$" />);
|
||||
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with suffix', () => {
|
||||
render(<StatCard value={100} label="Growth" suffix="%" />);
|
||||
expect(screen.getByText('Growth')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with prefix and suffix', () => {
|
||||
render(<StatCard value={100} label="Score" prefix="+" suffix="pts" />);
|
||||
expect(screen.getByText('Score')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with index', () => {
|
||||
render(<StatCard value={100} label="Users" index={2} />);
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have text-center class', () => {
|
||||
const { container } = render(<StatCard value={100} label="Users" />);
|
||||
const card = container.querySelector('.text-center');
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have group class', () => {
|
||||
const { container } = render(<StatCard value={100} label="Users" />);
|
||||
const card = container.querySelector('.group');
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { BackButton } from './back-button';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
back: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('BackButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render back button', () => {
|
||||
render(<BackButton />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render button text', () => {
|
||||
render(<BackButton />);
|
||||
expect(screen.getByText('返回')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render arrow icon', () => {
|
||||
const { container } = render(<BackButton />);
|
||||
const svg = container.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('should call router.back() when clicked', () => {
|
||||
const mockBack = jest.fn();
|
||||
jest.spyOn(require('next/navigation'), 'useRouter').mockReturnValue({
|
||||
back: mockBack,
|
||||
});
|
||||
|
||||
render(<BackButton />);
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
|
||||
expect(mockBack).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have ghost variant', () => {
|
||||
const { container } = render(<BackButton />);
|
||||
const button = container.querySelector('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have small size', () => {
|
||||
const { container } = render(<BackButton />);
|
||||
const button = container.querySelector('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Badge } from './badge';
|
||||
|
||||
describe('Badge', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render badge with text', () => {
|
||||
render(<Badge>Test Badge</Badge>);
|
||||
expect(screen.getByText('Test Badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render as span by default', () => {
|
||||
const { container } = render(<Badge>Badge</Badge>);
|
||||
const badge = container.querySelector('span');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have data-slot attribute', () => {
|
||||
const { container } = render(<Badge>Badge</Badge>);
|
||||
const badge = container.querySelector('[data-slot="badge"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variants', () => {
|
||||
it('should render default variant', () => {
|
||||
const { container } = render(<Badge>Default</Badge>);
|
||||
const badge = container.querySelector('[data-variant="default"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render secondary variant', () => {
|
||||
const { container } = render(<Badge variant="secondary">Secondary</Badge>);
|
||||
const badge = container.querySelector('[data-variant="secondary"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render destructive variant', () => {
|
||||
const { container } = render(<Badge variant="destructive">Destructive</Badge>);
|
||||
const badge = container.querySelector('[data-variant="destructive"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render outline variant', () => {
|
||||
const { container } = render(<Badge variant="outline">Outline</Badge>);
|
||||
const badge = container.querySelector('[data-variant="outline"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ghost variant', () => {
|
||||
const { container } = render(<Badge variant="ghost">Ghost</Badge>);
|
||||
const badge = container.querySelector('[data-variant="ghost"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render success variant', () => {
|
||||
const { container } = render(<Badge variant="success">Success</Badge>);
|
||||
const badge = container.querySelector('[data-variant="success"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render warning variant', () => {
|
||||
const { container } = render(<Badge variant="warning">Warning</Badge>);
|
||||
const badge = container.querySelector('[data-variant="warning"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render info variant', () => {
|
||||
const { container } = render(<Badge variant="info">Info</Badge>);
|
||||
const badge = container.querySelector('[data-variant="info"]');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<Badge className="custom-class">Badge</Badge>);
|
||||
const badge = container.querySelector('.custom-class');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have rounded-full class', () => {
|
||||
const { container } = render(<Badge>Badge</Badge>);
|
||||
const badge = container.querySelector('.rounded-full');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have inline-flex class', () => {
|
||||
const { container } = render(<Badge>Badge</Badge>);
|
||||
const badge = container.querySelector('.inline-flex');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AsChild', () => {
|
||||
it('should render as child component when asChild is true', () => {
|
||||
const { container } = render(
|
||||
<Badge asChild>
|
||||
<button>Button Badge</button>
|
||||
</Badge>
|
||||
);
|
||||
const button = container.querySelector('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent('Button Badge');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Button } from './button';
|
||||
|
||||
describe('Button Component', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render button with text', () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render as a button element by default', () => {
|
||||
render(<Button>Test</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button.tagName).toBe('BUTTON');
|
||||
});
|
||||
|
||||
it('should apply default variant styles', () => {
|
||||
render(<Button>Default</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('bg-[#C41E3A]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variants', () => {
|
||||
it('should apply secondary variant styles', () => {
|
||||
render(<Button variant="secondary">Secondary</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('bg-[#1C1C1C]');
|
||||
});
|
||||
|
||||
it('should apply outline variant styles', () => {
|
||||
render(<Button variant="outline">Outline</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('border-2');
|
||||
expect(button).toHaveClass('border-[#1C1C1C]');
|
||||
});
|
||||
|
||||
it('should apply ghost variant styles', () => {
|
||||
render(<Button variant="ghost">Ghost</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('text-[#3D3D3D]');
|
||||
});
|
||||
|
||||
it('should apply link variant styles', () => {
|
||||
render(<Button variant="link">Link</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('underline-offset-4');
|
||||
});
|
||||
|
||||
it('should apply destructive variant styles', () => {
|
||||
render(<Button variant="destructive">Destructive</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('bg-[#C41E3A]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sizes', () => {
|
||||
it('should apply default size styles', () => {
|
||||
render(<Button size="default">Default Size</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-11');
|
||||
});
|
||||
|
||||
it('should apply small size styles', () => {
|
||||
render(<Button size="sm">Small</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-9');
|
||||
});
|
||||
|
||||
it('should apply large size styles', () => {
|
||||
render(<Button size="lg">Large</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-12');
|
||||
});
|
||||
|
||||
it('should apply icon size styles', () => {
|
||||
render(<Button size="icon">Icon</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-11');
|
||||
expect(button).toHaveClass('w-11');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should be focusable', () => {
|
||||
render(<Button>Focusable</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).not.toHaveAttribute('tabindex', '-1');
|
||||
});
|
||||
|
||||
it('should have proper disabled state', () => {
|
||||
render(<Button disabled>Disabled</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveClass('disabled:opacity-50');
|
||||
});
|
||||
|
||||
it('should have focus visible styles', () => {
|
||||
render(<Button>Focus Visible</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('focus-visible:ring-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
render(<Button className="custom-class">Custom</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should pass through additional props', () => {
|
||||
render(<Button data-testid="custom-button" type="submit">Submit</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveAttribute('data-testid', 'custom-button');
|
||||
expect(button).toHaveAttribute('type', 'submit');
|
||||
});
|
||||
|
||||
it('should handle click events', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<Button onClick={handleClick}>Click</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
button.click();
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Touch Support', () => {
|
||||
it('should have touch manipulation', () => {
|
||||
render(<Button>Touch</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('touch-manipulation');
|
||||
});
|
||||
|
||||
it('should have minimum touch target size', () => {
|
||||
render(<Button>Touch Target</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('min-h-[44px]');
|
||||
expect(button).toHaveClass('min-w-[44px]');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from './card';
|
||||
|
||||
describe('Card Components', () => {
|
||||
describe('Card', () => {
|
||||
it('should render card with children', () => {
|
||||
render(
|
||||
<Card>
|
||||
<div>Card Content</div>
|
||||
</Card>
|
||||
);
|
||||
expect(screen.getByText('Card Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<Card data-testid="card">Test</Card>);
|
||||
const card = screen.getByTestId('card');
|
||||
expect(card).toHaveAttribute('data-slot', 'card');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<Card data-testid="card">Test</Card>);
|
||||
const card = screen.getByTestId('card');
|
||||
expect(card).toHaveClass('bg-[#FAFAFA]');
|
||||
expect(card).toHaveClass('rounded-xl');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<Card className="custom-card">Test</Card>);
|
||||
const card = screen.getByText('Test');
|
||||
expect(card).toHaveClass('custom-card');
|
||||
});
|
||||
|
||||
it('should have hover effects', () => {
|
||||
render(<Card data-testid="card">Test</Card>);
|
||||
const card = screen.getByTestId('card');
|
||||
expect(card).toHaveClass('hover:border-[#1C1C1C]');
|
||||
expect(card).toHaveClass('hover:shadow-[0_8px_24px_rgba(0,0,0,0.06)]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardHeader', () => {
|
||||
it('should render header with children', () => {
|
||||
render(
|
||||
<CardHeader>
|
||||
<div>Header Content</div>
|
||||
</CardHeader>
|
||||
);
|
||||
expect(screen.getByText('Header Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<CardHeader data-testid="header">Test</CardHeader>);
|
||||
const header = screen.getByTestId('header');
|
||||
expect(header).toHaveAttribute('data-slot', 'card-header');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<CardHeader data-testid="header">Test</CardHeader>);
|
||||
const header = screen.getByTestId('header');
|
||||
expect(header).toHaveClass('px-6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardTitle', () => {
|
||||
it('should render title text', () => {
|
||||
render(<CardTitle>Card Title</CardTitle>);
|
||||
expect(screen.getByText('Card Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<CardTitle data-testid="title">Test</CardTitle>);
|
||||
const title = screen.getByTestId('title');
|
||||
expect(title).toHaveAttribute('data-slot', 'card-title');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<CardTitle data-testid="title">Test</CardTitle>);
|
||||
const title = screen.getByTestId('title');
|
||||
expect(title).toHaveClass('font-semibold');
|
||||
expect(title).toHaveClass('text-[#1C1C1C]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardDescription', () => {
|
||||
it('should render description text', () => {
|
||||
render(<CardDescription>Card Description</CardDescription>);
|
||||
expect(screen.getByText('Card Description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<CardDescription data-testid="desc">Test</CardDescription>);
|
||||
const desc = screen.getByTestId('desc');
|
||||
expect(desc).toHaveAttribute('data-slot', 'card-description');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<CardDescription data-testid="desc">Test</CardDescription>);
|
||||
const desc = screen.getByTestId('desc');
|
||||
expect(desc).toHaveClass('text-[#5C5C5C]');
|
||||
expect(desc).toHaveClass('text-sm');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardAction', () => {
|
||||
it('should render action content', () => {
|
||||
render(<CardAction>Action Button</CardAction>);
|
||||
expect(screen.getByText('Action Button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<CardAction data-testid="action">Test</CardAction>);
|
||||
const action = screen.getByTestId('action');
|
||||
expect(action).toHaveAttribute('data-slot', 'card-action');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<CardAction data-testid="action">Test</CardAction>);
|
||||
const action = screen.getByTestId('action');
|
||||
expect(action).toHaveClass('col-start-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardContent', () => {
|
||||
it('should render content', () => {
|
||||
render(<CardContent>Content Body</CardContent>);
|
||||
expect(screen.getByText('Content Body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<CardContent data-testid="content">Test</CardContent>);
|
||||
const content = screen.getByTestId('content');
|
||||
expect(content).toHaveAttribute('data-slot', 'card-content');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<CardContent data-testid="content">Test</CardContent>);
|
||||
const content = screen.getByTestId('content');
|
||||
expect(content).toHaveClass('px-6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardFooter', () => {
|
||||
it('should render footer content', () => {
|
||||
render(<CardFooter>Footer Content</CardFooter>);
|
||||
expect(screen.getByText('Footer Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper data-slot attribute', () => {
|
||||
render(<CardFooter data-testid="footer">Test</CardFooter>);
|
||||
const footer = screen.getByTestId('footer');
|
||||
expect(footer).toHaveAttribute('data-slot', 'card-footer');
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
render(<CardFooter data-testid="footer">Test</CardFooter>);
|
||||
const footer = screen.getByTestId('footer');
|
||||
expect(footer).toHaveClass('px-6');
|
||||
expect(footer).toHaveClass('border-t');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Card Composition', () => {
|
||||
it('should render complete card structure', () => {
|
||||
render(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Test Title</CardTitle>
|
||||
<CardDescription>Test Description</CardDescription>
|
||||
<CardAction>Action</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>Content</CardContent>
|
||||
<CardFooter>Footer</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Action')).toBeInTheDocument();
|
||||
expect(screen.getByText('Content')).toBeInTheDocument();
|
||||
expect(screen.getByText('Footer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow nested components', () => {
|
||||
render(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<span>Nested Title</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<p>Nested Content</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Nested Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Nested Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should be accessible as a div element', () => {
|
||||
render(<Card>Accessible Card</Card>);
|
||||
const card = screen.getByText('Accessible Card');
|
||||
expect(card.tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
it('should support custom ARIA attributes', () => {
|
||||
render(
|
||||
<Card role="region" aria-label="Test Card">
|
||||
Test
|
||||
</Card>
|
||||
);
|
||||
const card = screen.getByRole('region', { name: 'Test Card' });
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from './dialog';
|
||||
|
||||
jest.mock('@radix-ui/react-dialog', () => ({
|
||||
Root: ({ children, open }: any) => <div data-open={open}>{children}</div>,
|
||||
Trigger: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
||||
Portal: ({ children }: any) => <div>{children}</div>,
|
||||
Close: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
||||
Overlay: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
Content: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
Title: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
|
||||
Description: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
||||
}));
|
||||
|
||||
describe('Dialog Components', () => {
|
||||
describe('Dialog', () => {
|
||||
it('should render dialog root', () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<div>Dialog Content</div>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Dialog Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogTrigger', () => {
|
||||
it('should render trigger button', () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger>Open Dialog</DialogTrigger>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Open Dialog' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogContent', () => {
|
||||
it('should render content with children', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent>
|
||||
<p>Dialog Body</p>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Dialog Body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent className="custom-class">
|
||||
<p>Test</p>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
const content = screen.getByText('Test').parentElement;
|
||||
expect(content).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogHeader', () => {
|
||||
it('should render header with children', () => {
|
||||
render(<DialogHeader>Header Content</DialogHeader>);
|
||||
expect(screen.getByText('Header Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<DialogHeader className="custom-header">Test</DialogHeader>);
|
||||
const header = screen.getByText('Test');
|
||||
expect(header).toHaveClass('custom-header');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogFooter', () => {
|
||||
it('should render footer with children', () => {
|
||||
render(<DialogFooter>Footer Content</DialogFooter>);
|
||||
expect(screen.getByText('Footer Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<DialogFooter className="custom-footer">Test</DialogFooter>);
|
||||
const footer = screen.getByText('Test');
|
||||
expect(footer).toHaveClass('custom-footer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogTitle', () => {
|
||||
it('should render title text', () => {
|
||||
render(<DialogTitle>Dialog Title</DialogTitle>);
|
||||
expect(screen.getByText('Dialog Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render as h2 element', () => {
|
||||
render(<DialogTitle>Test Title</DialogTitle>);
|
||||
const title = screen.getByRole('heading', { level: 2 });
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<DialogTitle className="custom-title">Test</DialogTitle>);
|
||||
const title = screen.getByText('Test');
|
||||
expect(title).toHaveClass('custom-title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogDescription', () => {
|
||||
it('should render description text', () => {
|
||||
render(<DialogDescription>Dialog Description</DialogDescription>);
|
||||
expect(screen.getByText('Dialog Description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<DialogDescription className="custom-desc">Test</DialogDescription>);
|
||||
const desc = screen.getByText('Test');
|
||||
expect(desc).toHaveClass('custom-desc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dialog Composition', () => {
|
||||
it('should render complete dialog structure', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Dialog</DialogTitle>
|
||||
<DialogDescription>This is a test dialog</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>Dialog Body</div>
|
||||
<DialogFooter>
|
||||
<button>Close Dialog</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a test dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dialog Body')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Close Dialog' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible title', () => {
|
||||
render(<DialogTitle>Accessible Title</DialogTitle>);
|
||||
const title = screen.getByRole('heading', { name: 'Accessible Title' });
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support custom ARIA attributes', () => {
|
||||
render(
|
||||
<DialogContent aria-label="Test Dialog">
|
||||
<p>Content</p>
|
||||
</DialogContent>
|
||||
);
|
||||
const content = screen.getByText('Content').parentElement;
|
||||
expect(content).toHaveAttribute('aria-label', 'Test Dialog');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
} from './dropdown-menu';
|
||||
|
||||
describe('DropdownMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render dropdown menu trigger', () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Open Menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render menu items', () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render menu label', () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Menu Label</DropdownMenuLabel>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Menu Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render separator', () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
const separator = document.querySelector('[role="separator"]');
|
||||
expect(separator).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('should open menu on trigger click', () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
const trigger = screen.getByText('Open Menu');
|
||||
fireEvent.click(trigger);
|
||||
|
||||
expect(trigger).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close menu on item click', () => {
|
||||
const onSelect = jest.fn();
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onSelect={onSelect}>Item 1</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
const item = screen.getByText('Item 1');
|
||||
fireEvent.click(item);
|
||||
|
||||
expect(onSelect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownMenuShortcut', () => {
|
||||
it('should render shortcut text', () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
Save
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||
expect(screen.getByText('⌘S')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply custom className to trigger', () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="custom-trigger">Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Open Menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className to content', () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="custom-content">
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className to item', () => {
|
||||
render(
|
||||
<DropdownMenu defaultOpen>
|
||||
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="custom-item">Item 1</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { ErrorBoundary } from './error-boundary';
|
||||
|
||||
const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
|
||||
if (shouldThrow) {
|
||||
throw new Error('Test error');
|
||||
}
|
||||
return <div>No error</div>;
|
||||
};
|
||||
|
||||
describe('ErrorBoundary', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Normal Rendering', () => {
|
||||
it('should render children when no error', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<div>Test Content</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show error UI when no error', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<div>Normal Content</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('出错了')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should catch errors and display fallback UI', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText('出错了')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display default error message', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/抱歉,页面出现了问题/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display custom fallback if provided', () => {
|
||||
render(
|
||||
<ErrorBoundary fallback={<div>Custom Error UI</div>}>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Custom Error UI')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should log error to console', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Recovery', () => {
|
||||
it('should have retry button', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '重试' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible error icon', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const icon = document.querySelector('svg');
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have accessible retry button', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: '重试' });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent('重试');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have centered layout', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const container = screen.getByText('出错了').closest('div');
|
||||
expect(container).toHaveClass('text-center');
|
||||
});
|
||||
|
||||
it('should have error icon with red background', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const iconContainer = document.querySelector('.bg-red-100');
|
||||
expect(iconContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have styled retry button', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: '重试' });
|
||||
expect(button).toHaveClass('bg-[#C41E3A]');
|
||||
expect(button).toHaveClass('text-white');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { GlassCard } from './glass-card';
|
||||
|
||||
describe('GlassCard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render glass card', () => {
|
||||
const { container } = render(<GlassCard>Test Content</GlassCard>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
render(<GlassCard>Test Content</GlassCard>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<GlassCard className="custom-class">Test</GlassCard>);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variants', () => {
|
||||
it('should render default variant', () => {
|
||||
const { container } = render(<GlassCard>Test</GlassCard>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render elevated variant', () => {
|
||||
const { container } = render(<GlassCard variant="elevated">Test</GlassCard>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render outline variant', () => {
|
||||
const { container } = render(<GlassCard variant="outline">Test</GlassCard>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render glow variant', () => {
|
||||
const { container } = render(<GlassCard variant="glow">Test</GlassCard>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have rounded class', () => {
|
||||
const { container } = render(<GlassCard>Test</GlassCard>);
|
||||
expect(container.firstChild).toHaveClass('rounded-2xl');
|
||||
});
|
||||
|
||||
it('should have border class', () => {
|
||||
const { container } = render(<GlassCard>Test</GlassCard>);
|
||||
expect(container.firstChild).toHaveClass('border');
|
||||
});
|
||||
|
||||
it('should have backdrop-blur class', () => {
|
||||
const { container } = render(<GlassCard>Test</GlassCard>);
|
||||
expect(container.firstChild).toHaveClass('backdrop-blur-xl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Forward Ref', () => {
|
||||
it('should forward ref', () => {
|
||||
const ref = { current: null };
|
||||
render(<GlassCard ref={ref}>Test</GlassCard>);
|
||||
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import * as React from 'react';
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Input } from './input';
|
||||
|
||||
jest.mock('@/lib/utils', () => ({
|
||||
cn: (...args: any[]) => args.filter(Boolean).join(' '),
|
||||
}));
|
||||
|
||||
describe('Input', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render input element', () => {
|
||||
render(<Input />);
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with label', () => {
|
||||
render(<Input label="用户名" />);
|
||||
expect(screen.getByLabelText('用户名')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render required indicator when required', () => {
|
||||
render(<Input label="用户名" required />);
|
||||
expect(screen.getByText('*')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message', () => {
|
||||
render(<Input label="用户名" error="请输入用户名" />);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('请输入用户名');
|
||||
});
|
||||
|
||||
it('should render with placeholder', () => {
|
||||
render(<Input placeholder="请输入用户名" />);
|
||||
expect(screen.getByPlaceholderText('请输入用户名')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with custom data-testid', () => {
|
||||
render(<Input data-testid="custom-input" />);
|
||||
expect(screen.getByTestId('custom-input')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Types', () => {
|
||||
it('should render text input by default', () => {
|
||||
render(<Input />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input.tagName.toLowerCase()).toBe('input');
|
||||
});
|
||||
|
||||
it('should render email input', () => {
|
||||
render(<Input type="email" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveAttribute('type', 'email');
|
||||
});
|
||||
|
||||
it('should render password input', () => {
|
||||
render(<Input type="password" />);
|
||||
const input = screen.getByDisplayValue('');
|
||||
expect(input).toHaveAttribute('type', 'password');
|
||||
});
|
||||
|
||||
it('should render tel input', () => {
|
||||
render(<Input type="tel" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveAttribute('type', 'tel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interaction', () => {
|
||||
it('should handle user input', async () => {
|
||||
render(<Input />);
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.type(input, 'test value');
|
||||
expect(input).toHaveValue('test value');
|
||||
});
|
||||
|
||||
it('should handle onChange event', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<Input onChange={handleChange} />);
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.type(input, 'a');
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onBlur event', async () => {
|
||||
const handleBlur = jest.fn();
|
||||
render(<Input onBlur={handleBlur} />);
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.click(input);
|
||||
await userEvent.tab();
|
||||
expect(handleBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onFocus event', async () => {
|
||||
const handleFocus = jest.fn();
|
||||
render(<Input onFocus={handleFocus} />);
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.click(input);
|
||||
expect(handleFocus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled State', () => {
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
render(<Input disabled />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should not accept input when disabled', async () => {
|
||||
render(<Input disabled />);
|
||||
const input = screen.getByRole('textbox');
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have aria-required when required', () => {
|
||||
render(<Input required />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveAttribute('aria-required', 'true');
|
||||
});
|
||||
|
||||
it('should have aria-invalid when error exists', () => {
|
||||
render(<Input error="错误信息" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveAttribute('aria-invalid', 'true');
|
||||
});
|
||||
|
||||
it('should have aria-describedby when error exists', () => {
|
||||
render(<Input error="错误信息" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveAttribute('aria-describedby');
|
||||
});
|
||||
|
||||
it('should have proper label association', () => {
|
||||
render(<Input label="用户名" id="username" />);
|
||||
const input = screen.getByLabelText('用户名');
|
||||
expect(input).toHaveAttribute('id', 'username');
|
||||
});
|
||||
|
||||
it('should have role="alert" on error message', () => {
|
||||
render(<Input error="错误信息" />);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Styling', () => {
|
||||
it('should apply custom className', () => {
|
||||
render(<Input className="custom-class" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should have error styling when error exists', () => {
|
||||
render(<Input error="错误信息" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input.className).toMatch(/border-\[#C41E3A\]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ref Forwarding', () => {
|
||||
it('should forward ref to input element', () => {
|
||||
const ref = React.createRef<HTMLInputElement>();
|
||||
render(<Input ref={ref} />);
|
||||
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { InsightCard } from './insight-card';
|
||||
|
||||
jest.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: ({ src, alt, fill, className }: any) => (
|
||||
<img src={src} alt={alt} className={className} data-fill={fill} />
|
||||
),
|
||||
}));
|
||||
|
||||
describe('InsightCard', () => {
|
||||
const defaultProps = {
|
||||
title: 'Test Title',
|
||||
excerpt: 'Test excerpt',
|
||||
category: 'Technology',
|
||||
readTime: '5 min',
|
||||
publishedAt: '2026-01-01',
|
||||
href: '/test-article',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render insight card', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render excerpt', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByText('Test excerpt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render category badge', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByText('Technology')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render read time', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByText('5 min')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render published date', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByText('2026-01-01')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render read more link', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
expect(screen.getByText('阅读更多')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image', () => {
|
||||
it('should render image when imageUrl is provided', () => {
|
||||
render(<InsightCard {...defaultProps} imageUrl="/test.jpg" />);
|
||||
const image = screen.getByAltText('Test Title');
|
||||
expect(image).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render image when imageUrl is not provided', () => {
|
||||
render(<InsightCard {...defaultProps} />);
|
||||
const image = screen.queryByRole('img');
|
||||
expect(image).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Featured', () => {
|
||||
it('should not be featured by default', () => {
|
||||
const { container } = render(<InsightCard {...defaultProps} />);
|
||||
const article = container.querySelector('article');
|
||||
expect(article).not.toHaveClass('md:col-span-2');
|
||||
});
|
||||
|
||||
it('should be featured when featured prop is true', () => {
|
||||
const { container } = render(<InsightCard {...defaultProps} featured />);
|
||||
const article = container.querySelector('article');
|
||||
expect(article).toHaveClass('md:col-span-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have article element', () => {
|
||||
const { container } = render(<InsightCard {...defaultProps} />);
|
||||
const article = container.querySelector('article');
|
||||
expect(article).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have border class', () => {
|
||||
const { container } = render(<InsightCard {...defaultProps} />);
|
||||
const article = container.querySelector('article');
|
||||
expect(article).toHaveClass('border');
|
||||
});
|
||||
|
||||
it('should have rounded class', () => {
|
||||
const { container } = render(<InsightCard {...defaultProps} />);
|
||||
const article = container.querySelector('article');
|
||||
expect(article).toHaveClass('rounded-lg');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Skeleton,
|
||||
CardSkeleton,
|
||||
ServiceCardSkeleton,
|
||||
CaseCardSkeleton,
|
||||
ProductCardSkeleton,
|
||||
NewsCardSkeleton,
|
||||
SectionSkeleton,
|
||||
} from './loading-skeleton';
|
||||
|
||||
describe('Loading Skeleton Components', () => {
|
||||
describe('Skeleton', () => {
|
||||
it('should render skeleton element', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply default styles', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
const skeleton = container.firstChild as HTMLElement;
|
||||
expect(skeleton).toHaveClass('animate-pulse');
|
||||
expect(skeleton).toHaveClass('bg-[#F5F5F5]');
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<Skeleton className="custom-class" />);
|
||||
const skeleton = container.firstChild as HTMLElement;
|
||||
expect(skeleton).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CardSkeleton', () => {
|
||||
it('should render card skeleton', () => {
|
||||
const { container } = render(<CardSkeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have correct structure', () => {
|
||||
const { container } = render(<CardSkeleton />);
|
||||
const skeletonElements = container.querySelectorAll('.animate-pulse');
|
||||
expect(skeletonElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceCardSkeleton', () => {
|
||||
it('should render service card skeleton', () => {
|
||||
const { container } = render(<ServiceCardSkeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have full height', () => {
|
||||
const { container } = render(<ServiceCardSkeleton />);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
expect(card).toHaveClass('h-full');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CaseCardSkeleton', () => {
|
||||
it('should render case card skeleton', () => {
|
||||
const { container } = render(<CaseCardSkeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have image placeholder', () => {
|
||||
const { container } = render(<CaseCardSkeleton />);
|
||||
const imageSkeleton = container.querySelector('.h-48');
|
||||
expect(imageSkeleton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProductCardSkeleton', () => {
|
||||
it('should render product card skeleton', () => {
|
||||
const { container } = render(<ProductCardSkeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have flex column layout', () => {
|
||||
const { container } = render(<ProductCardSkeleton />);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
expect(card).toHaveClass('flex');
|
||||
expect(card).toHaveClass('flex-col');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NewsCardSkeleton', () => {
|
||||
it('should render news card skeleton', () => {
|
||||
const { container } = render(<NewsCardSkeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have image placeholder', () => {
|
||||
const { container } = render(<NewsCardSkeleton />);
|
||||
const imageSkeleton = container.querySelector('.h-48');
|
||||
expect(imageSkeleton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SectionSkeleton', () => {
|
||||
it('should render section skeleton', () => {
|
||||
const { container } = render(<SectionSkeleton />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render multiple service card skeletons', () => {
|
||||
const { container } = render(<SectionSkeleton />);
|
||||
const cards = container.querySelectorAll('.bg-white');
|
||||
expect(cards.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should not have any accessible content', () => {
|
||||
const { container } = render(<CardSkeleton />);
|
||||
const textContent = container.textContent;
|
||||
expect(textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should be purely decorative', () => {
|
||||
const { container } = render(<SectionSkeleton />);
|
||||
const elements = container.querySelectorAll('*');
|
||||
elements.forEach(element => {
|
||||
expect(element).not.toHaveAttribute('aria-label');
|
||||
expect(element).not.toHaveAttribute('aria-labelledby');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animation', () => {
|
||||
it('should have pulse animation', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
const skeleton = container.firstChild as HTMLElement;
|
||||
expect(skeleton).toHaveClass('animate-pulse');
|
||||
});
|
||||
|
||||
it('should apply animation to all skeleton elements', () => {
|
||||
const { container } = render(<CardSkeleton />);
|
||||
const skeletons = container.querySelectorAll('.animate-pulse');
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { OptimizedImage } from './optimized-image';
|
||||
|
||||
jest.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: ({ src, alt, onLoad, onError, className, ...props }: any) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={className}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
data-testid="optimized-image"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('OptimizedImage', () => {
|
||||
const defaultProps = {
|
||||
src: '/test.jpg',
|
||||
alt: 'Test Image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render optimized image', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
expect(screen.getByTestId('optimized-image')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with alt text', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
expect(screen.getByAltText('Test Image')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<OptimizedImage {...defaultProps} className="custom-class" />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should apply container className', () => {
|
||||
const { container } = render(
|
||||
<OptimizedImage {...defaultProps} containerClassName="container-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('container-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
it('should handle onLoad event', () => {
|
||||
const onLoad = jest.fn();
|
||||
render(<OptimizedImage {...defaultProps} onLoad={onLoad} />);
|
||||
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
fireEvent.load(image);
|
||||
|
||||
expect(onLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onError event', () => {
|
||||
const onError = jest.fn();
|
||||
render(<OptimizedImage {...defaultProps} onError={onError} />);
|
||||
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
fireEvent.error(image);
|
||||
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error state on error', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
fireEvent.error(image);
|
||||
|
||||
const errorIcon = document.querySelector('svg');
|
||||
expect(errorIcon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Object Fit', () => {
|
||||
it('should apply cover object fit by default', () => {
|
||||
render(<OptimizedImage {...defaultProps} />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toHaveClass('object-cover');
|
||||
});
|
||||
|
||||
it('should apply contain object fit', () => {
|
||||
render(<OptimizedImage {...defaultProps} objectFit="contain" />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toHaveClass('object-contain');
|
||||
});
|
||||
|
||||
it('should apply fill object fit', () => {
|
||||
render(<OptimizedImage {...defaultProps} objectFit="fill" />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toHaveClass('object-fill');
|
||||
});
|
||||
|
||||
it('should apply none object fit', () => {
|
||||
render(<OptimizedImage {...defaultProps} objectFit="none" />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toHaveClass('object-none');
|
||||
});
|
||||
|
||||
it('should apply scale-down object fit', () => {
|
||||
render(<OptimizedImage {...defaultProps} objectFit="scale-down" />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toHaveClass('object-scale-down');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fill Mode', () => {
|
||||
it('should render in fill mode', () => {
|
||||
const { container } = render(<OptimizedImage {...defaultProps} fill />);
|
||||
expect(container.firstChild).toHaveClass('relative');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Priority', () => {
|
||||
it('should handle priority prop', () => {
|
||||
render(<OptimizedImage {...defaultProps} priority />);
|
||||
const image = screen.getByTestId('optimized-image');
|
||||
expect(image).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { PageHeader } from './page-header';
|
||||
|
||||
jest.mock('framer-motion', () => ({
|
||||
motion: {
|
||||
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
h1: ({ children, ...props }: any) => <h1 {...props}>{children}</h1>,
|
||||
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
||||
},
|
||||
useInView: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/ink-decoration', () => ({
|
||||
InkBackground: () => <div data-testid="ink-background" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/effects/data-particle-flow', () => ({
|
||||
DataParticleFlow: () => <div data-testid="data-particle-flow" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/effects/subtle-dots', () => ({
|
||||
SubtleDots: () => <div data-testid="subtle-dots" />,
|
||||
}));
|
||||
|
||||
describe('PageHeader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render page header', () => {
|
||||
render(<PageHeader title="Test Title" />);
|
||||
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
render(<PageHeader title="Test Title" />);
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render description when provided', () => {
|
||||
render(<PageHeader title="Test" description="Test description" />);
|
||||
expect(screen.getByText('Test description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render description when not provided', () => {
|
||||
render(<PageHeader title="Test" />);
|
||||
expect(screen.queryByText('Test description')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render badge when provided', () => {
|
||||
render(<PageHeader title="Test" badge="Test Badge" />);
|
||||
expect(screen.getByText('Test Badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render badge when not provided', () => {
|
||||
render(<PageHeader title="Test" />);
|
||||
expect(screen.queryByText('Test Badge')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Effects', () => {
|
||||
it('should render ink background', () => {
|
||||
render(<PageHeader title="Test" />);
|
||||
expect(screen.getByTestId('ink-background')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render data particle flow', () => {
|
||||
render(<PageHeader title="Test" />);
|
||||
expect(screen.getByTestId('data-particle-flow')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render subtle dots', () => {
|
||||
render(<PageHeader title="Test" />);
|
||||
expect(screen.getByTestId('subtle-dots')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have container class', () => {
|
||||
const { container } = render(<PageHeader title="Test" />);
|
||||
const containerDiv = container.querySelector('.container-wide');
|
||||
expect(containerDiv).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<PageHeader title="Test" className="custom-class" />);
|
||||
const customDiv = container.querySelector('.custom-class');
|
||||
expect(customDiv).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetClose,
|
||||
} from './sheet';
|
||||
|
||||
describe('Sheet', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render sheet trigger', () => {
|
||||
render(
|
||||
<Sheet>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Open Sheet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render sheet content when open', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Sheet Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render sheet title', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByRole('heading', { name: 'Sheet Title' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render sheet description', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
<SheetDescription>Sheet Description</SheetDescription>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Sheet Description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render sheet footer', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetFooter>
|
||||
<button>Footer Button</button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Footer Button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('should open sheet on trigger click', () => {
|
||||
render(
|
||||
<Sheet>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
const trigger = screen.getByText('Open Sheet');
|
||||
fireEvent.click(trigger);
|
||||
|
||||
expect(screen.getByText('Sheet Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close sheet on close button click', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: /close/i });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
expect(screen.queryByText('Sheet Title')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sides', () => {
|
||||
it('should render right side by default', () => {
|
||||
const { container } = render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render left side', () => {
|
||||
const { container } = render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent side="left">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render top side', () => {
|
||||
const { container } = render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent side="top">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render bottom side', () => {
|
||||
const { container } = render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent side="bottom">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply custom className to content', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent className="custom-content">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Sheet Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className to header', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader className="custom-header">
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Sheet Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className to footer', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetFooter className="custom-footer">
|
||||
<button>Footer</button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Close Button', () => {
|
||||
it('should show close button by default', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide close button when showCloseButton is false', () => {
|
||||
render(
|
||||
<Sheet defaultOpen>
|
||||
<SheetTrigger>Open Sheet</SheetTrigger>
|
||||
<SheetContent showCloseButton={false}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: /close/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { TestimonialCard } from './testimonial-card';
|
||||
|
||||
jest.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: ({ src, alt, width, height, className }: any) => (
|
||||
<img src={src} alt={alt} width={width} height={height} className={className} />
|
||||
),
|
||||
}));
|
||||
|
||||
describe('TestimonialCard', () => {
|
||||
const defaultProps = {
|
||||
quote: 'Test quote',
|
||||
author: 'Test Author',
|
||||
position: 'Manager',
|
||||
company: 'Test Company',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render testimonial card', () => {
|
||||
render(<TestimonialCard {...defaultProps} />);
|
||||
const blockquote = screen.getByText((content, element) => {
|
||||
return element?.tagName === 'BLOCKQUOTE' && content.includes('Test quote');
|
||||
});
|
||||
expect(blockquote).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render author name', () => {
|
||||
render(<TestimonialCard {...defaultProps} />);
|
||||
expect(screen.getByText('Test Author')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render position and company', () => {
|
||||
render(<TestimonialCard {...defaultProps} />);
|
||||
expect(screen.getByText(/Manager · Test Company/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render quote with quotes', () => {
|
||||
render(<TestimonialCard {...defaultProps} />);
|
||||
const blockquote = screen.getByText((content, element) => {
|
||||
return element?.tagName === 'BLOCKQUOTE' && content.includes('Test quote');
|
||||
});
|
||||
expect(blockquote).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rating', () => {
|
||||
it('should render 5 stars by default', () => {
|
||||
render(<TestimonialCard {...defaultProps} />);
|
||||
const stars = document.querySelectorAll('svg.w-4.h-4');
|
||||
expect(stars).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should render custom rating', () => {
|
||||
render(<TestimonialCard {...defaultProps} rating={3} />);
|
||||
const stars = document.querySelectorAll('svg.w-4.h-4');
|
||||
expect(stars).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should not render stars when rating is 0', () => {
|
||||
render(<TestimonialCard {...defaultProps} rating={0} />);
|
||||
const stars = document.querySelectorAll('svg.w-4.h-4');
|
||||
expect(stars).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Avatar', () => {
|
||||
it('should render avatar when avatarUrl is provided', () => {
|
||||
render(<TestimonialCard {...defaultProps} avatarUrl="/avatar.jpg" />);
|
||||
const avatar = screen.getByAltText('Test Author');
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render avatar when avatarUrl is not provided', () => {
|
||||
render(<TestimonialCard {...defaultProps} />);
|
||||
const avatar = screen.queryByAltText('Test Author');
|
||||
expect(avatar).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct card classes', () => {
|
||||
const { container } = render(<TestimonialCard {...defaultProps} />);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
expect(card).toHaveClass('relative');
|
||||
expect(card).toHaveClass('p-8');
|
||||
expect(card).toHaveClass('rounded-lg');
|
||||
});
|
||||
|
||||
it('should have border class', () => {
|
||||
const { container } = render(<TestimonialCard {...defaultProps} />);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
expect(card).toHaveClass('border');
|
||||
});
|
||||
|
||||
it('should have background class', () => {
|
||||
const { container } = render(<TestimonialCard {...defaultProps} />);
|
||||
const card = container.firstChild as HTMLElement;
|
||||
expect(card.className).toContain('bg-white');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import * as React from 'react';
|
||||
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Textarea } from './textarea';
|
||||
|
||||
jest.mock('@/lib/utils', () => ({
|
||||
cn: (...args: any[]) => args.filter(Boolean).join(' '),
|
||||
}));
|
||||
|
||||
describe('Textarea', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render textarea element', () => {
|
||||
render(<Textarea />);
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with label', () => {
|
||||
render(<Textarea label="留言内容" />);
|
||||
expect(screen.getByLabelText('留言内容')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render required indicator when required', () => {
|
||||
render(<Textarea label="留言内容" required />);
|
||||
expect(screen.getByText('*')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message', () => {
|
||||
render(<Textarea label="留言内容" error="请输入留言内容" />);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('请输入留言内容');
|
||||
});
|
||||
|
||||
it('should render with placeholder', () => {
|
||||
render(<Textarea placeholder="请输入留言内容" />);
|
||||
expect(screen.getByPlaceholderText('请输入留言内容')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with custom data-testid', () => {
|
||||
render(<Textarea data-testid="custom-textarea" />);
|
||||
expect(screen.getByTestId('custom-textarea')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with default rows', () => {
|
||||
render(<Textarea rows={5} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveAttribute('rows', '5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interaction', () => {
|
||||
it('should handle user input', async () => {
|
||||
render(<Textarea />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.type(textarea, '这是一段测试留言');
|
||||
expect(textarea).toHaveValue('这是一段测试留言');
|
||||
});
|
||||
|
||||
it('should handle onChange event', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<Textarea onChange={handleChange} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.type(textarea, 'a');
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onBlur event', async () => {
|
||||
const handleBlur = jest.fn();
|
||||
render(<Textarea onBlur={handleBlur} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.click(textarea);
|
||||
await userEvent.tab();
|
||||
expect(handleBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onFocus event', async () => {
|
||||
const handleFocus = jest.fn();
|
||||
render(<Textarea onFocus={handleFocus} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.click(textarea);
|
||||
expect(handleFocus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle multiline input', async () => {
|
||||
render(<Textarea />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
|
||||
await userEvent.type(textarea, '第一行{enter}第二行');
|
||||
expect(textarea).toHaveValue('第一行\n第二行');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled State', () => {
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
render(<Textarea disabled />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should not accept input when disabled', async () => {
|
||||
render(<Textarea disabled />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
|
||||
expect(textarea).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have aria-required when required', () => {
|
||||
render(<Textarea required />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveAttribute('aria-required', 'true');
|
||||
});
|
||||
|
||||
it('should have aria-invalid when error exists', () => {
|
||||
render(<Textarea error="错误信息" />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveAttribute('aria-invalid', 'true');
|
||||
});
|
||||
|
||||
it('should have aria-describedby when error exists', () => {
|
||||
render(<Textarea error="错误信息" />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveAttribute('aria-describedby');
|
||||
});
|
||||
|
||||
it('should have proper label association', () => {
|
||||
render(<Textarea label="留言内容" id="message" />);
|
||||
const textarea = screen.getByLabelText('留言内容');
|
||||
expect(textarea).toHaveAttribute('id', 'message');
|
||||
});
|
||||
|
||||
it('should have role="alert" on error message', () => {
|
||||
render(<Textarea error="错误信息" />);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Styling', () => {
|
||||
it('should apply custom className', () => {
|
||||
render(<Textarea className="custom-class" />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should have error styling when error exists', () => {
|
||||
render(<Textarea error="错误信息" />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea.className).toMatch(/border-\[#C41E3A\]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ref Forwarding', () => {
|
||||
it('should forward ref to textarea element', () => {
|
||||
const ref = React.createRef<HTMLTextAreaElement>();
|
||||
render(<Textarea ref={ref} />);
|
||||
expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Value Handling', () => {
|
||||
it('should display controlled value', () => {
|
||||
render(<Textarea value="预设内容" onChange={() => {}} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveValue('预设内容');
|
||||
});
|
||||
|
||||
it('should display defaultValue', () => {
|
||||
render(<Textarea defaultValue="默认内容" />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveValue('默认内容');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MaxLength', () => {
|
||||
it('should respect maxLength attribute', () => {
|
||||
render(<Textarea maxLength={100} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
expect(textarea).toHaveAttribute('maxlength', '100');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { Toast } from './toast';
|
||||
|
||||
describe('Toast Component', () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render toast with message', () => {
|
||||
render(<Toast message="Test message" onClose={mockOnClose} />);
|
||||
expect(screen.getByText('Test message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with success type by default', () => {
|
||||
render(<Toast message="Success" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast).toHaveAttribute('data-type', 'success');
|
||||
});
|
||||
|
||||
it('should render with error type', () => {
|
||||
render(<Toast message="Error" type="error" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast).toHaveAttribute('data-type', 'error');
|
||||
});
|
||||
|
||||
it('should render with info type', () => {
|
||||
render(<Toast message="Info" type="info" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast).toHaveAttribute('data-type', 'info');
|
||||
});
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
expect(screen.getByRole('button', { name: '关闭提示' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auto-close', () => {
|
||||
it('should auto-close after default duration (3000ms)', async () => {
|
||||
render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should auto-close after custom duration', async () => {
|
||||
render(<Toast message="Test" duration={5000} onClose={mockOnClose} />);
|
||||
|
||||
jest.advanceTimersByTime(5000);
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cleanup timer on unmount', () => {
|
||||
const { unmount } = render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
unmount();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manual Close', () => {
|
||||
it('should close when close button clicked', async () => {
|
||||
render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: '关闭提示' });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
jest.advanceTimersByTime(300);
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have alert role', () => {
|
||||
render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-live attribute', () => {
|
||||
render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast).toHaveAttribute('aria-live', 'polite');
|
||||
});
|
||||
|
||||
it('should have accessible close button', () => {
|
||||
render(<Toast message="Test" onClose={mockOnClose} />);
|
||||
const closeButton = screen.getByRole('button', { name: '关闭提示' });
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Attributes', () => {
|
||||
it('should support custom data-testid', () => {
|
||||
render(
|
||||
<Toast
|
||||
message="Test"
|
||||
onClose={mockOnClose}
|
||||
data-testid="custom-toast"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('custom-toast')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Icons', () => {
|
||||
it('should render success icon for success type', () => {
|
||||
render(<Toast message="Success" type="success" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast.querySelector('svg')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error icon for error type', () => {
|
||||
render(<Toast message="Error" type="error" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast.querySelector('svg')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render info icon for info type', () => {
|
||||
render(<Toast message="Info" type="info" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast.querySelector('svg')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply success background color', () => {
|
||||
render(<Toast message="Success" type="success" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast.className).toContain('bg-green-50');
|
||||
});
|
||||
|
||||
it('should apply error background color', () => {
|
||||
render(<Toast message="Error" type="error" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast.className).toContain('bg-red-50');
|
||||
});
|
||||
|
||||
it('should apply info background color', () => {
|
||||
render(<Toast message="Info" type="info" onClose={mockOnClose} />);
|
||||
const toast = screen.getByRole('alert');
|
||||
expect(toast.className).toContain('bg-blue-50');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { TouchButton } from './touch-button';
|
||||
|
||||
describe('TouchButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render button with text', () => {
|
||||
render(<TouchButton>Click Me</TouchButton>);
|
||||
expect(screen.getByText('Click Me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render button element', () => {
|
||||
render(<TouchButton>Button</TouchButton>);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<TouchButton className="custom-class">Button</TouchButton>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variants', () => {
|
||||
it('should render primary variant by default', () => {
|
||||
const { container } = render(<TouchButton>Primary</TouchButton>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render secondary variant', () => {
|
||||
const { container } = render(<TouchButton variant="secondary">Secondary</TouchButton>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ghost variant', () => {
|
||||
const { container } = render(<TouchButton variant="ghost">Ghost</TouchButton>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sizes', () => {
|
||||
it('should render small size', () => {
|
||||
const { container } = render(<TouchButton size="sm">Small</TouchButton>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render medium size by default', () => {
|
||||
const { container } = render(<TouchButton>Medium</TouchButton>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render large size', () => {
|
||||
const { container } = render(<TouchButton size="lg">Large</TouchButton>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full Width', () => {
|
||||
it('should not be full width by default', () => {
|
||||
const { container } = render(<TouchButton>Button</TouchButton>);
|
||||
expect(container.firstChild).not.toHaveClass('w-full');
|
||||
});
|
||||
|
||||
it('should be full width when fullWidth is true', () => {
|
||||
const { container } = render(<TouchButton fullWidth>Button</TouchButton>);
|
||||
expect(container.firstChild).toHaveClass('w-full');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled State', () => {
|
||||
it('should not be disabled by default', () => {
|
||||
render(<TouchButton>Button</TouchButton>);
|
||||
expect(screen.getByRole('button')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
render(<TouchButton disabled>Button</TouchButton>);
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Touch Events', () => {
|
||||
it('should handle touch start', () => {
|
||||
const { container } = render(<TouchButton>Button</TouchButton>);
|
||||
const button = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.touchStart(button);
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle touch end', () => {
|
||||
const { container } = render(<TouchButton>Button</TouchButton>);
|
||||
const button = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.touchStart(button);
|
||||
fireEvent.touchEnd(button);
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle touch cancel', () => {
|
||||
const { container } = render(<TouchButton>Button</TouchButton>);
|
||||
const button = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.touchStart(button);
|
||||
fireEvent.touchCancel(button);
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Click Events', () => {
|
||||
it('should handle click events', () => {
|
||||
const onClick = jest.fn();
|
||||
render(<TouchButton onClick={onClick}>Button</TouchButton>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { TouchSwipe } from './touch-swipe';
|
||||
|
||||
describe('TouchSwipe', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render children', () => {
|
||||
render(
|
||||
<TouchSwipe>
|
||||
<div>Test Content</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<TouchSwipe className="custom-class">
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Touch Events', () => {
|
||||
it('should handle touch start', () => {
|
||||
const { container } = render(
|
||||
<TouchSwipe>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 100, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(div).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle touch end', () => {
|
||||
const { container } = render(
|
||||
<TouchSwipe>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 100, clientY: 100 }],
|
||||
});
|
||||
fireEvent.touchEnd(div, {
|
||||
changedTouches: [{ clientX: 150, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(div).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSwipeLeft when swiping left', () => {
|
||||
const onSwipeLeft = jest.fn();
|
||||
const { container } = render(
|
||||
<TouchSwipe onSwipeLeft={onSwipeLeft}>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 150, clientY: 100 }],
|
||||
});
|
||||
|
||||
fireEvent.touchEnd(div, {
|
||||
changedTouches: [{ clientX: 50, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(onSwipeLeft).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onSwipeRight when swiping right', () => {
|
||||
const onSwipeRight = jest.fn();
|
||||
const { container } = render(
|
||||
<TouchSwipe onSwipeRight={onSwipeRight}>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 50, clientY: 100 }],
|
||||
});
|
||||
|
||||
fireEvent.touchEnd(div, {
|
||||
changedTouches: [{ clientX: 150, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(onSwipeRight).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger swipe when below threshold', () => {
|
||||
const onSwipeLeft = jest.fn();
|
||||
const { container } = render(
|
||||
<TouchSwipe onSwipeLeft={onSwipeLeft} threshold={100}>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 100, clientY: 100 }],
|
||||
});
|
||||
fireEvent.touchEnd(div, {
|
||||
changedTouches: [{ clientX: 80, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(onSwipeLeft).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Threshold', () => {
|
||||
it('should use default threshold of 50', () => {
|
||||
const onSwipeLeft = jest.fn();
|
||||
const { container } = render(
|
||||
<TouchSwipe onSwipeLeft={onSwipeLeft}>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 100, clientY: 100 }],
|
||||
});
|
||||
fireEvent.touchEnd(div, {
|
||||
changedTouches: [{ clientX: 40, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(onSwipeLeft).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept custom threshold', () => {
|
||||
const onSwipeLeft = jest.fn();
|
||||
const { container } = render(
|
||||
<TouchSwipe onSwipeLeft={onSwipeLeft} threshold={200}>
|
||||
<div>Test</div>
|
||||
</TouchSwipe>
|
||||
);
|
||||
|
||||
const div = container.firstChild as HTMLElement;
|
||||
|
||||
fireEvent.touchStart(div, {
|
||||
touches: [{ clientX: 300, clientY: 100 }],
|
||||
});
|
||||
|
||||
fireEvent.touchEnd(div, {
|
||||
changedTouches: [{ clientX: 50, clientY: 100 }],
|
||||
});
|
||||
|
||||
expect(onSwipeLeft).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user