test(web): add Vitest setup and unit tests for validations and utils
- Add vitest config and test script to web app - Auth validation tests: phone format, password rules, registration flow - Listing validation tests: all schema steps, constants, merged schema - Utils tests: cn() class merging with Tailwind conflict resolution - 36 tests across 3 test files Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
86
apps/web/lib/__tests__/auth-validations.spec.ts
Normal file
86
apps/web/lib/__tests__/auth-validations.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { loginSchema, registerSchema } from '../validations/auth';
|
||||
|
||||
describe('loginSchema', () => {
|
||||
it('should accept valid Vietnamese phone and password', () => {
|
||||
const result = loginSchema.safeParse({ phone: '0912345678', password: 'abc123' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept +84 format phone', () => {
|
||||
const result = loginSchema.safeParse({ phone: '+84912345678', password: 'abc123' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject empty phone', () => {
|
||||
const result = loginSchema.safeParse({ phone: '', password: 'abc123' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject invalid phone format', () => {
|
||||
const result = loginSchema.safeParse({ phone: '12345', password: 'abc123' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject empty password', () => {
|
||||
const result = loginSchema.safeParse({ phone: '0912345678', password: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept various valid Vietnamese prefixes', () => {
|
||||
const prefixes = ['032', '056', '070', '081', '090'];
|
||||
for (const prefix of prefixes) {
|
||||
const result = loginSchema.safeParse({ phone: `${prefix}1234567`, password: 'pass' });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerSchema', () => {
|
||||
const validData = {
|
||||
fullName: 'Nguyen Van A',
|
||||
phone: '0912345678',
|
||||
password: '12345678',
|
||||
confirmPassword: '12345678',
|
||||
};
|
||||
|
||||
it('should accept valid registration data', () => {
|
||||
const result = registerSchema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept with optional email', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, email: 'test@example.com' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept empty string email', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, email: '' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid email', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, email: 'not-email' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject short password', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, password: '123', confirmPassword: '123' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject mismatched passwords', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, confirmPassword: 'different' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject short full name', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, fullName: 'A' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject empty full name', () => {
|
||||
const result = registerSchema.safeParse({ ...validData, fullName: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
165
apps/web/lib/__tests__/listing-validations.spec.ts
Normal file
165
apps/web/lib/__tests__/listing-validations.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
listingBasicSchema,
|
||||
listingLocationSchema,
|
||||
listingDetailsSchema,
|
||||
listingPricingSchema,
|
||||
createListingSchema,
|
||||
TRANSACTION_TYPES,
|
||||
PROPERTY_TYPES,
|
||||
DIRECTIONS,
|
||||
LISTING_STATUSES,
|
||||
} from '../validations/listings';
|
||||
|
||||
describe('listingBasicSchema', () => {
|
||||
it('should accept valid basic info', () => {
|
||||
const result = listingBasicSchema.safeParse({
|
||||
transactionType: 'SALE',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp',
|
||||
description: 'Mô tả chi tiết về căn hộ',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid transaction type', () => {
|
||||
const result = listingBasicSchema.safeParse({
|
||||
transactionType: 'INVALID',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Valid title',
|
||||
description: 'Valid description',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject short title', () => {
|
||||
const result = listingBasicSchema.safeParse({
|
||||
transactionType: 'SALE',
|
||||
propertyType: 'HOUSE',
|
||||
title: 'Ab',
|
||||
description: 'Valid description',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject short description', () => {
|
||||
const result = listingBasicSchema.safeParse({
|
||||
transactionType: 'RENT',
|
||||
propertyType: 'VILLA',
|
||||
title: 'Valid title',
|
||||
description: 'Short',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept all valid property types', () => {
|
||||
for (const pt of PROPERTY_TYPES) {
|
||||
const result = listingBasicSchema.safeParse({
|
||||
transactionType: 'SALE',
|
||||
propertyType: pt.value,
|
||||
title: 'Valid title here',
|
||||
description: 'Valid description here',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('listingLocationSchema', () => {
|
||||
it('should accept valid location', () => {
|
||||
const result = listingLocationSchema.safeParse({
|
||||
address: '123 Đường ABC',
|
||||
ward: 'Phường 1',
|
||||
district: 'Quận 1',
|
||||
city: 'TP. Hồ Chí Minh',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject empty address', () => {
|
||||
const result = listingLocationSchema.safeParse({
|
||||
address: '',
|
||||
ward: 'Phường 1',
|
||||
district: 'Quận 1',
|
||||
city: 'TP. HCM',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept optional lat/lng', () => {
|
||||
const result = listingLocationSchema.safeParse({
|
||||
address: '123 Đường ABC',
|
||||
ward: 'Phường 1',
|
||||
district: 'Quận 1',
|
||||
city: 'Hà Nội',
|
||||
latitude: '21.0285',
|
||||
longitude: '105.8542',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listingPricingSchema', () => {
|
||||
it('should accept valid price', () => {
|
||||
const result = listingPricingSchema.safeParse({ priceVND: '5000000000' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject empty price', () => {
|
||||
const result = listingPricingSchema.safeParse({ priceVND: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept optional rent and commission', () => {
|
||||
const result = listingPricingSchema.safeParse({
|
||||
priceVND: '15000000',
|
||||
rentPriceMonthly: '15000000',
|
||||
commissionPct: '3',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createListingSchema (merged)', () => {
|
||||
const validListing = {
|
||||
transactionType: 'SALE' as const,
|
||||
propertyType: 'APARTMENT' as const,
|
||||
title: 'Căn hộ 2PN Quận 7',
|
||||
description: 'Căn hộ view sông, full nội thất',
|
||||
address: '123 Nguyễn Hữu Thọ',
|
||||
ward: 'Phường Tân Hưng',
|
||||
district: 'Quận 7',
|
||||
city: 'TP. Hồ Chí Minh',
|
||||
areaM2: '75',
|
||||
priceVND: '3500000000',
|
||||
};
|
||||
|
||||
it('should accept a complete valid listing', () => {
|
||||
const result = createListingSchema.safeParse(validListing);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject if basic fields missing', () => {
|
||||
const { title: _, ...noTitle } = validListing;
|
||||
const result = createListingSchema.safeParse(noTitle);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constants', () => {
|
||||
it('should have 2 transaction types', () => {
|
||||
expect(TRANSACTION_TYPES).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should have 6 property types', () => {
|
||||
expect(PROPERTY_TYPES).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('should have 8 directions', () => {
|
||||
expect(DIRECTIONS).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('should have 8 listing statuses', () => {
|
||||
expect(Object.keys(LISTING_STATUSES)).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
26
apps/web/lib/__tests__/utils.spec.ts
Normal file
26
apps/web/lib/__tests__/utils.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { cn } from '../utils';
|
||||
|
||||
describe('cn', () => {
|
||||
it('should merge class names', () => {
|
||||
expect(cn('foo', 'bar')).toBe('foo bar');
|
||||
});
|
||||
|
||||
it('should handle conditional classes', () => {
|
||||
expect(cn('base', false && 'hidden', 'visible')).toBe('base visible');
|
||||
});
|
||||
|
||||
it('should merge conflicting Tailwind classes', () => {
|
||||
expect(cn('px-4', 'px-6')).toBe('px-6');
|
||||
expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500');
|
||||
});
|
||||
|
||||
it('should handle empty inputs', () => {
|
||||
expect(cn()).toBe('');
|
||||
expect(cn('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
expect(cn(['foo', 'bar'])).toBe('foo bar');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user