Files
goodgo-platform/apps/web/components/valuation/__tests__/comparables-map.spec.tsx
Ho Ngoc Hai 5d4ecdeb2f feat(web): AVM v2 upgraded valuation dashboard (TEC-2763)
R5.4 ships the upgraded AVM UI behind the `avm_v2` A/B flag. When the
flag is on, the dashboard exposes:

- Tab switch between single valuation and multi-property compare
- Waterfall drivers chart (ValueDriversChart) alongside the existing
  horizontal bar breakdown
- Mapbox comparables map with similarity-coloured markers and an
  optional highlighted subject pin
- Confidence interval + range bar and PDF export remain available
- Valuation history chart surface unchanged (still lazy-loaded)

Flag plumbing (useAvmV2Flag):
- NEXT_PUBLIC_FEATURE_AVM_V2=1 enables by default
- `?avm_v2=1|0` URL param forces + persists to localStorage
- safe localStorage handling (no throw when storage is blocked)

Tests: comparables-map, value-drivers-chart, use-avm-v2-flag specs
added. Pre-existing "Yếu tố chính" assertion in valuation-results.spec
updated to match the current copy ("Yếu tố ảnh hưởng giá") so the
valuation suite is green (7 files, 52 tests).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-18 15:05:46 +07:00

150 lines
3.6 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import type { ValuationComparable } from '@/lib/valuation-api';
import { ComparablesMap } from '../comparables-map';
// Mapbox GL does not run cleanly in jsdom — mock with a minimal stand-in
// that records addTo calls so we can assert marker count.
const markerAddTo = vi.fn();
const mapAddControl = vi.fn();
const mapFitBounds = vi.fn();
const mapFlyTo = vi.fn();
const mapRemove = vi.fn();
vi.mock('mapbox-gl', () => {
class MockMap {
addControl = mapAddControl;
fitBounds = mapFitBounds;
flyTo = mapFlyTo;
remove = mapRemove;
}
class MockNavigationControl {}
class MockAttributionControl {}
class MockMarker {
setLngLat() {
return this;
}
setPopup() {
return this;
}
addTo() {
markerAddTo();
return this;
}
remove() {
// noop
}
}
class MockPopup {
setHTML() {
return this;
}
}
class MockLngLatBounds {
extend() {
return this;
}
isEmpty() {
return false;
}
}
return {
default: {
accessToken: '',
Map: MockMap,
NavigationControl: MockNavigationControl,
AttributionControl: MockAttributionControl,
Marker: MockMarker,
Popup: MockPopup,
LngLatBounds: MockLngLatBounds,
},
};
});
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
const sampleComparables: ValuationComparable[] = [
{
id: 'comp-1',
title: 'Căn hộ tương tự A',
address: '456 Nguyễn Hữu Thọ',
district: 'Quận 7',
priceVND: '4800000000',
areaM2: 78,
pricePerM2: 61_500_000,
similarity: 0.92,
latitude: 10.73,
longitude: 106.72,
},
{
id: 'comp-2',
title: 'Căn hộ tương tự B',
address: '789 Phạm Viết Chánh',
district: 'Bình Thạnh',
priceVND: '5200000000',
areaM2: 82,
pricePerM2: 63_400_000,
similarity: 0.7,
latitude: 10.8,
longitude: 106.7,
},
];
describe('ComparablesMap', () => {
beforeEach(() => {
markerAddTo.mockClear();
mapAddControl.mockClear();
mapFitBounds.mockClear();
mapFlyTo.mockClear();
mapRemove.mockClear();
process.env['NEXT_PUBLIC_MAPBOX_TOKEN'] = 'pk.test';
});
afterEach(() => {
delete (process.env as Record<string, string | undefined>)[
'NEXT_PUBLIC_MAPBOX_TOKEN'
];
});
it('renders header and descriptor', () => {
render(<ComparablesMap comparables={sampleComparables} />);
expect(screen.getByText('Bản đồ so sánh')).toBeInTheDocument();
expect(screen.getByText(/2 BĐS so sánh/)).toBeInTheDocument();
});
it('renders prompt when mapbox token is missing', () => {
delete (process.env as Record<string, string | undefined>)[
'NEXT_PUBLIC_MAPBOX_TOKEN'
];
render(<ComparablesMap comparables={sampleComparables} />);
expect(
screen.getByText(/Thiết lập NEXT_PUBLIC_MAPBOX_TOKEN/),
).toBeInTheDocument();
});
it('shows empty state when no comparables have coordinates', () => {
const withoutCoords = sampleComparables.map(
({ latitude: _lat, longitude: _lng, ...rest }) => rest,
);
render(<ComparablesMap comparables={withoutCoords} />);
expect(
screen.getByText(/Không có toạ độ cho các BĐS so sánh/),
).toBeInTheDocument();
});
it('adds a marker for each geolocated comparable plus subject pin', () => {
render(
<ComparablesMap
comparables={sampleComparables}
subjectLatitude={10.77}
subjectLongitude={106.7}
/>,
);
expect(markerAddTo).toHaveBeenCalledTimes(3);
});
});