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');
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -34,6 +35,7 @@
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^6.0.2"
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
13
apps/web/vitest.config.ts
Normal file
13
apps/web/vitest.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['**/__tests__/**/*.spec.ts', '**/__tests__/**/*.test.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
});
|
||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -314,6 +314,9 @@ importers:
|
||||
typescript:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
vitest:
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.3(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(vite@7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
libs/mcp-servers:
|
||||
dependencies:
|
||||
@@ -8660,7 +8663,7 @@ snapshots:
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.5
|
||||
semver: 7.7.4
|
||||
tinyglobby: 0.2.15
|
||||
tinyglobby: 0.2.16
|
||||
ts-api-utils: 2.5.0(typescript@6.0.2)
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
@@ -8750,6 +8753,14 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.3(vite@7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.3
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@vitest/mocker@4.1.3(vite@7.3.2(@types/node@25.5.2)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.3
|
||||
@@ -11872,6 +11883,22 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
vite@7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.7
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.9
|
||||
rollup: 4.60.1
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 25.5.2
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
terser: 5.46.1
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
vite@7.3.2(@types/node@25.5.2)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.7
|
||||
@@ -11888,6 +11915,34 @@ snapshots:
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
vitest@4.1.3(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(vite@7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.3
|
||||
'@vitest/mocker': 4.1.3(vite@7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 4.1.3
|
||||
'@vitest/runner': 4.1.3
|
||||
'@vitest/snapshot': 4.1.3
|
||||
'@vitest/spy': 4.1.3
|
||||
'@vitest/utils': 4.1.3
|
||||
es-module-lexer: 2.0.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 4.0.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.1.1
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 7.3.2(@types/node@25.5.2)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@types/node': 25.5.2
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.3(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(vite@7.3.2(@types/node@25.5.2)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.3
|
||||
|
||||
Reference in New Issue
Block a user