Files
goodgo-platform/apps/web/components/seo/__tests__/json-ld.spec.tsx
Ho Ngoc Hai 1668c800fe fix(web): resolve all 22 TypeScript typecheck errors in apps/web (TEC-3208)
- Fix TS4111: use bracket notation for index signature access in metadata.spec.ts,
  neighborhood-poi-map.tsx, and neighborhood-poi-map.spec.tsx
- Fix TS2740: add missing property fields (usableAreaM2, floor, totalFloors,
  nearbyPOIs, etc.) to test mock objects in 5 spec files
- Fix TS2339: add missing estimate() and create() methods to transferApi
- Fix TS4114: add override modifier to render() in page.tsx error boundary
- Fix TS2532: add optional chaining for possibly undefined features in
  neighborhood-poi-map.tsx

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-22 15:49:38 +07:00

270 lines
10 KiB
TypeScript

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(<JsonLd data={data} />);
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(<JsonLd data={data} />);
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(<JsonLd data={data} />);
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<string, unknown>;
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<Record<string, unknown>>;
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',
valuationEstimate: null,
agentQualityScore: null,
similarCount: 0,
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 }],
usableAreaM2: null,
floor: null,
totalFloors: null,
nearbyPOIs: null,
metroDistanceM: null,
furnishing: null,
propertyCondition: null,
balconyDirection: null,
maintenanceFeeVND: null,
parkingSlots: null,
viewType: [],
petFriendly: null,
suitableFor: [],
whyThisLocation: 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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
const geo = location['geo'] as Record<string, unknown>;
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<Record<string, unknown>>;
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<Record<string, unknown>>;
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<Record<string, unknown>>;
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<string, unknown>;
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<Record<string, unknown>>;
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<string, unknown>;
expect(worksFor['name']).toBe('GoodGo Agency');
});
});