wrongmove/frontend/src/constants/__tests__/index.test.ts
Viktor Barzin 9a5ad7878c wrongmove: preserve heatmap color on multi-property popup + arrow nav on card carousel
When the user clicks a heatmap hex that contains multiple properties,
each property card in the resulting popup now renders a left-edge
color stripe matching the heatmap gradient for that property's
individual value of the active metric (Price/m², Total Price, Size,
Bedrooms, …). The "color code" carries from the map into the popup
instead of dying at the hex boundary.

Plumbing:
- `colorSchemes.ts` gains `interpolateMetricColor(value, min, max, stops)`
  that walks the color-stop ramp and returns `rgb(R, G, B)`.
- `Map.tsx` stashes the latest `{min, max}` from `computeColorScale` in
  a ref so `getListingDialog` can compute per-property colors without
  re-running the worker.
- `PropertyCard` accepts an optional `metricColor` prop and applies it
  as a 4px `border-left`. Compact variant unchanged (no stripe).

Also resolves the Round-3 Fix-4 follow-up: `CardCarousel` (inside
PropertyCard.tsx) now has clickable prev/next chevron buttons in
addition to drag + keyboard navigation. Buttons fade in on hover
(group-hover) and are always focus-visible for keyboard users; clicks
stop propagation so the parent card click handler doesn't fire.

Tests: 9 new (4 covering interpolateMetricColor edge cases —
null/NaN/clamp — and 4 covering metricColor stripe + carousel
buttons present/absent). Full suite 210/210.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 23:10:20 +00:00

71 lines
3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { MAP_CONFIG } from '@/constants';
import {
LOW_IS_GOOD_COLOR_STOPS,
HIGH_IS_GOOD_COLOR_STOPS,
interpolateMetricColor,
} from '@/constants/colorSchemes';
describe('MAP_CONFIG', () => {
describe('B18 — default map center is London', () => {
it('DEFAULT_CENTER points at London, not Czech Republic', () => {
// London ≈ [-0.1276, 51.5074]. The old default ([13.38032, 49.994210])
// landed in Czech Republic which is jarring for a UK-only app.
const [lng, lat] = MAP_CONFIG.DEFAULT_CENTER;
expect(lng).toBeGreaterThan(-1);
expect(lng).toBeLessThan(1);
expect(lat).toBeGreaterThan(51);
expect(lat).toBeLessThan(52);
});
it('DEFAULT_ZOOM gives a city-level view (not continent-level)', () => {
// Anything around 10 is a city / inner-borough view in Mapbox terms.
expect(MAP_CONFIG.DEFAULT_ZOOM).toBeGreaterThanOrEqual(9);
expect(MAP_CONFIG.DEFAULT_ZOOM).toBeLessThanOrEqual(13);
});
});
describe('B19 / B29 — Mapbox token sourced from env only', () => {
it('reads from VITE_MAPBOX_TOKEN (the test setup sets it to test-token)', () => {
// The test harness (src/__tests__/setup.ts) sets VITE_MAPBOX_TOKEN
// to "test-token". The constant module reads import.meta.env at import time.
expect(MAP_CONFIG.MAPBOX_TOKEN).toBe('test-token');
});
it('does not contain a hard-coded Mapbox public key as a fallback', () => {
// The previous code shipped a real public key (`pk.eyJ1...`). This regression
// test ensures we never leak a token into the bundle again.
expect(MAP_CONFIG.MAPBOX_TOKEN).not.toMatch(/^pk\.eyJ/);
});
});
});
describe('interpolateMetricColor', () => {
it('returns null for invalid inputs (NaN value, max <= min)', () => {
expect(interpolateMetricColor(NaN, 0, 100, LOW_IS_GOOD_COLOR_STOPS)).toBeNull();
expect(interpolateMetricColor(50, 100, 100, LOW_IS_GOOD_COLOR_STOPS)).toBeNull();
expect(interpolateMetricColor(50, 50, 0, LOW_IS_GOOD_COLOR_STOPS)).toBeNull();
});
it('returns the LOW_IS_GOOD start (green) for value at min', () => {
const c = interpolateMetricColor(0, 0, 100, LOW_IS_GOOD_COLOR_STOPS);
expect(c).toBe('rgb(34, 197, 94)');
});
it('returns the LOW_IS_GOOD end (red) for value at max', () => {
const c = interpolateMetricColor(100, 0, 100, LOW_IS_GOOD_COLOR_STOPS);
expect(c).toBe('rgb(239, 68, 68)');
});
it('inverts for HIGH_IS_GOOD (small → red, large → green)', () => {
expect(interpolateMetricColor(0, 0, 100, HIGH_IS_GOOD_COLOR_STOPS)).toBe('rgb(239, 68, 68)');
expect(interpolateMetricColor(100, 0, 100, HIGH_IS_GOOD_COLOR_STOPS)).toBe('rgb(34, 197, 94)');
});
it('clamps out-of-range values to the gradient endpoints', () => {
const below = interpolateMetricColor(-9999, 0, 100, LOW_IS_GOOD_COLOR_STOPS);
const above = interpolateMetricColor(9999, 0, 100, LOW_IS_GOOD_COLOR_STOPS);
expect(below).toBe('rgb(34, 197, 94)');
expect(above).toBe('rgb(239, 68, 68)');
});
});