fix: resolve all ESLint errors and TypeScript compilation errors
- Auto-fixed 712 import ordering errors via `pnpm lint --fix` - Manually fixed 13 remaining ESLint errors: - Prefixed unused vars with _ (mockAdminUser, params) - Removed unused imports (UnauthorizedException, vi, screen) - Moved imports above vi.mock() calls to fix import group ordering - Removed eslint-disable for non-existent rules - Fixed empty object pattern in Playwright fixture - Fixed ~40 TypeScript TS4111 index signature errors in test files: - Used bracket notation for Record<string, unknown> property access - Added missing PropertyMedia fields (id, order, caption) to test data - Fixed pre-existing test failures in rate-limit guard specs: - Added NODE_ENV override to bypass test-mode skip in guard Both `pnpm lint` and `pnpm typecheck` now exit 0 cleanly. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
252
apps/web/components/seo/__tests__/json-ld.spec.tsx
Normal file
252
apps/web/components/seo/__tests__/json-ld.spec.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
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',
|
||||
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<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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user