Some checks failed
Security Scanning / Trivy Filesystem Scan (push) Failing after 31s
Security Scanning / Security Gate (push) Failing after 2s
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 13s
Deploy / Build API Image (push) Failing after 36s
Deploy / Build Web Image (push) Failing after 12s
Deploy / Build AI Services Image (push) Failing after 12s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m5s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m24s
E2E Tests / Playwright E2E (push) Failing after 20s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Deploy / Deploy to Production (push) Has been cancelled
Deploy / Deploy to Staging (push) Has been cancelled
Deploy / Smoke Test Staging (push) Has been cancelled
Deploy / Rollback Staging (push) Has been cancelled
Deploy / Smoke Test Production (push) Has been cancelled
Deploy / Rollback Production (push) Has been cancelled
Security Scanning / Trivy Scan — AI Services Image (push) Has been cancelled
Security Scanning / Trivy Scan — Web Image (push) Has been cancelled
Mapbox theming
--------------
- New hook `lib/mapbox-style.ts` returning streets-v12 (light) or
dark-v11 (dark) from the app's useTheme().
- Six map components now initialise with the themed style and
`map.setStyle(...)` on theme change: project-map, park-map,
listing-map, district-heatmap (plus re-adding its heatmap source
after style.load), neighborhood-poi-map, valuation/comparables-map.
- Marker / popup DOM styles swapped from hard-coded white/#666/#green
to shadcn CSS tokens (--card, --card-foreground, --muted-foreground,
--primary, --border). Global Mapbox popup + control + attribution
skins added in app/globals.css.
- POI filter pills on neighborhood-poi-map were hard-coded `bg-white`
which rendered same-colour text on white in dark mode — switched to
`bg-card`/`bg-card/60` for proper contrast.
- Extend the MockMap in comparables-map.spec.tsx with setStyle/on
so the new theme-sync effect doesn't blow up in tests.
Detail client normaliser (du-an-server)
---------------------------------------
- Project media from the backend is a `string[]` (raw URLs) or richer
`{url,...}` objects. Handle both shapes and drop entries without
a URL so we never feed "" to <Image src>.
- Amenities are `string[]` in the DB but the frontend type expects
`{id,name,icon,category}`; normalise strings into objects so the
AmenitiesTab has stable keys and a displayable name.
Resolves three classes of runtime warnings on /du-an/<slug>:
"Image is missing required 'src' property", "ReactDOM.preload ...
empty href", and "Each child in a list should have a unique 'key'
prop" (AmenitiesTab).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
3.7 KiB
TypeScript
152 lines
3.7 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;
|
|
setStyle = () => undefined;
|
|
on = () => undefined;
|
|
}
|
|
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);
|
|
});
|
|
});
|