import { render } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import type { AgentPublicProfile } from '@/lib/agents-api'; import type { ListingDetail } from '@/lib/listings-api'; import { JsonLd, generateListingJsonLd, generateBreadcrumbJsonLd, generateWebsiteJsonLd, generateAgentJsonLd } from '../json-ld'; // ─── JsonLd component ──────────────────────────────────────── describe('JsonLd', () => { it('renders a script tag with type application/ld+json', () => { const data = { '@context': 'https://schema.org', '@type': 'WebSite' }; const { container } = render(); const script = container.querySelector('script[type="application/ld+json"]'); expect(script).toBeInTheDocument(); }); it('serializes data as JSON inside the script tag', () => { const data = { '@context': 'https://schema.org', '@type': 'WebSite', name: 'GoodGo' }; const { container } = render(); const script = container.querySelector('script')!; expect(JSON.parse(script.innerHTML)).toEqual(data); }); it('handles array data', () => { const data = [ { '@context': 'https://schema.org', '@type': 'WebSite' }, { '@context': 'https://schema.org', '@type': 'Organization' }, ]; const { container } = render(); const script = container.querySelector('script')!; expect(JSON.parse(script.innerHTML)).toHaveLength(2); }); }); // ─── generateWebsiteJsonLd ────────────────────────────────── describe('generateWebsiteJsonLd', () => { it('generates WebSite schema', () => { const result = generateWebsiteJsonLd('https://goodgo.vn'); expect(result['@context']).toBe('https://schema.org'); expect(result['@type']).toBe('WebSite'); expect(result.name).toBe('GoodGo'); expect(result.url).toBe('https://goodgo.vn'); }); it('includes SearchAction', () => { const result = generateWebsiteJsonLd('https://goodgo.vn'); expect(result.potentialAction).toBeDefined(); const action = result.potentialAction as Record; expect(action['@type']).toBe('SearchAction'); }); it('includes bilingual language', () => { const result = generateWebsiteJsonLd('https://goodgo.vn'); expect(result.inLanguage).toEqual(['vi', 'en']); }); }); // ─── generateBreadcrumbJsonLd ─────────────────────────────── describe('generateBreadcrumbJsonLd', () => { it('generates BreadcrumbList schema', () => { const items = [ { name: 'Trang chủ', url: 'https://goodgo.vn' }, { name: 'Tìm kiếm', url: 'https://goodgo.vn/search' }, ]; const result = generateBreadcrumbJsonLd(items); expect(result['@type']).toBe('BreadcrumbList'); expect(result.itemListElement).toHaveLength(2); }); it('sets correct positions starting from 1', () => { const items = [ { name: 'Home', url: 'https://goodgo.vn' }, { name: 'Search', url: 'https://goodgo.vn/search' }, ]; const result = generateBreadcrumbJsonLd(items); const elements = result.itemListElement as Array>; expect(elements[0]?.['position']).toBe(1); expect(elements[1]?.['position']).toBe(2); }); it('returns empty list for no items', () => { const result = generateBreadcrumbJsonLd([]); expect(result.itemListElement).toEqual([]); }); }); // ─── generateListingJsonLd ────────────────────────────────── describe('generateListingJsonLd', () => { const mockListing: ListingDetail = { id: 'listing-1', status: 'ACTIVE', transactionType: 'SALE', priceVND: '3500000000', pricePerM2: 40_000_000, rentPriceMonthly: null, commissionPct: null, viewCount: 10, saveCount: 5, inquiryCount: 3, publishedAt: '2026-01-01T00:00:00Z', createdAt: '2025-12-01T00:00:00Z', property: { id: 'prop-1', propertyType: 'APARTMENT', title: 'Căn hộ 2PN', description: 'Mô tả căn hộ đẹp', address: '123 Nguyễn Hữu Thọ', ward: 'Phường Tân Hưng', district: 'Quận 7', city: 'Hồ Chí Minh', areaM2: 75, bedrooms: 2, bathrooms: 2, floors: null, direction: null, yearBuilt: null, legalStatus: null, amenities: [], projectName: null, latitude: 10.73, longitude: 106.72, media: [{ id: 'media-1', type: 'image', url: 'https://example.com/img1.jpg', order: 0, caption: null }], }, seller: { id: 'seller-1', fullName: 'Seller', phone: '0912345678' }, agent: null, }; it('generates RealEstateListing schema type', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); expect(result['@type']).toBe('RealEstateListing'); }); it('sets correct URL', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); expect(result['url']).toBe('https://goodgo.vn/listings/listing-1'); }); it('includes offer with VND price', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); const offers = result['offers'] as Record; expect(offers['price']).toBe(3_500_000_000); expect(offers['priceCurrency']).toBe('VND'); }); it('sets InStock availability for ACTIVE listing', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); const offers = result['offers'] as Record; expect(offers['availability']).toBe('https://schema.org/InStock'); }); it('sets SoldOut availability for non-ACTIVE listing', () => { const soldListing = { ...mockListing, status: 'SOLD' as const }; const result = generateListingJsonLd(soldListing, 'https://goodgo.vn'); const offers = result['offers'] as Record; expect(offers['availability']).toBe('https://schema.org/SoldOut'); }); it('includes geo coordinates when available', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); const location = result['contentLocation'] as Record; const geo = location['geo'] as Record; expect(geo['latitude']).toBe(10.73); expect(geo['longitude']).toBe(106.72); }); it('includes property area in additionalProperty', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); const additionalProps = result['additionalProperty'] as Array>; const areaEntry = additionalProps.find((p) => p['name'] === 'Dien tich'); expect(areaEntry?.['value']).toBe('75 m\u00B2'); }); it('includes bedrooms when present', () => { const result = generateListingJsonLd(mockListing, 'https://goodgo.vn'); const additionalProps = result['additionalProperty'] as Array>; const bedEntry = additionalProps.find((p) => p['name'] === 'Phong ngu'); expect(bedEntry?.['value']).toBe(2); }); it('excludes bedrooms when null', () => { const noBedListing = { ...mockListing, property: { ...mockListing.property, bedrooms: null }, }; const result = generateListingJsonLd(noBedListing, 'https://goodgo.vn'); const additionalProps = result['additionalProperty'] as Array>; const bedEntry = additionalProps.find((p) => p['name'] === 'Phong ngu'); expect(bedEntry).toBeUndefined(); }); }); // ─── generateAgentJsonLd ──────────────────────────────────── describe('generateAgentJsonLd', () => { const mockAgent: AgentPublicProfile = { id: 'agent-1', fullName: 'Nguyen Van C', phone: '0912345678', email: 'agent@goodgo.vn', avatarUrl: 'https://example.com/avatar.jpg', bio: 'Agent chuyên nghiệp', agency: 'GoodGo Agency', licenseNumber: null, serviceAreas: ['Quận 1', 'Quận 7'], activeListings: [], totalReviews: 20, avgReviewRating: 4.5, qualityScore: 95, totalDeals: 50, isVerified: true, memberSince: '2024-01-01T00:00:00Z', }; it('generates RealEstateAgent schema type', () => { const result = generateAgentJsonLd(mockAgent, 'https://goodgo.vn'); expect(result['@type']).toBe('RealEstateAgent'); }); it('includes agent name', () => { const result = generateAgentJsonLd(mockAgent, 'https://goodgo.vn'); expect(result['name']).toBe('Nguyen Van C'); }); it('includes aggregate rating when reviews exist', () => { const result = generateAgentJsonLd(mockAgent, 'https://goodgo.vn'); const rating = result['aggregateRating'] as Record; expect(rating['ratingValue']).toBe(4.5); expect(rating['reviewCount']).toBe(20); }); it('excludes aggregate rating when no reviews', () => { const noReviewAgent = { ...mockAgent, totalReviews: 0 }; const result = generateAgentJsonLd(noReviewAgent, 'https://goodgo.vn'); expect(result['aggregateRating']).toBeUndefined(); }); it('includes service areas', () => { const result = generateAgentJsonLd(mockAgent, 'https://goodgo.vn'); const areas = result['areaServed'] as Array>; expect(areas).toHaveLength(2); expect(areas[0]?.['name']).toBe('Quận 1'); }); it('includes agency as worksFor', () => { const result = generateAgentJsonLd(mockAgent, 'https://goodgo.vn'); const worksFor = result['worksFor'] as Record; expect(worksFor['name']).toBe('GoodGo Agency'); }); });