fix: QA fixes for property decisions feature

- Replace deprecated datetime.utcnow() with datetime.now(UTC) in model
  and repository
- Add listing_type validation to decision_service (RENT/BUY only)
- Fix decision filtering tests failing due to rate limiting by patching
  _match_endpoint
- Add SwipeCard component test suite (11 tests covering rendering,
  interactions, and POI distances)
- Add test for invalid listing_type validation
This commit is contained in:
Viktor Barzin 2026-02-21 14:04:34 +00:00
parent 8452f65d25
commit 49280d9679
No known key found for this signature in database
GPG key ID: 0EB088298288D958
6 changed files with 149 additions and 7 deletions

View file

@ -0,0 +1,127 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SwipeCard } from '@/components/SwipeCard';
import { createMockFeature } from '@/__tests__/helpers';
// Mock react-spring to render plain divs instead of animated ones
vi.mock('@react-spring/web', () => ({
animated: {
div: ({ children, style: _style, ...rest }: React.HTMLAttributes<HTMLDivElement>) => (
<div {...rest}>{children}</div>
),
},
useSpring: () => [
{
x: { to: (_fn: (v: number) => number) => 0 },
y: { to: (_fn: (v: number) => number) => 0 },
rotate: 0,
scale: 1,
opacity: 1,
},
{ start: vi.fn() },
],
}));
// Mock use-gesture to return a no-op bind function
vi.mock('@use-gesture/react', () => ({
useDrag: () => () => ({}),
}));
const defaultFeature = createMockFeature({
url: 'https://www.rightmove.co.uk/properties/12345',
total_price: 2500,
rooms: 2,
qm: 65,
qmprice: 38,
photo_thumbnail: 'https://example.com/photo.jpg',
listing_type: 'RENT',
});
describe('SwipeCard', () => {
const defaultProps = {
feature: defaultFeature,
onSwipe: vi.fn(),
isTop: true,
stackIndex: 0,
};
it('renders property price', () => {
render(<SwipeCard {...defaultProps} />);
expect(screen.getByText(/2,500/)).toBeInTheDocument();
});
it('renders /mo suffix for rent listings', () => {
render(<SwipeCard {...defaultProps} />);
expect(screen.getByText('/mo')).toBeInTheDocument();
});
it('does not render /mo for buy listings', () => {
const buyFeature = createMockFeature({
listing_type: 'BUY',
total_price: 500000,
});
render(<SwipeCard {...defaultProps} feature={buyFeature} />);
expect(screen.queryByText('/mo')).not.toBeInTheDocument();
});
it('renders bedroom count', () => {
render(<SwipeCard {...defaultProps} />);
expect(screen.getByText(/2 bed/)).toBeInTheDocument();
});
it('renders square meters', () => {
render(<SwipeCard {...defaultProps} />);
expect(screen.getByText(/65 m²/)).toBeInTheDocument();
});
it('renders price per sqm', () => {
render(<SwipeCard {...defaultProps} />);
expect(screen.getByText(/£38\/m²/)).toBeInTheDocument();
});
it('renders photo thumbnail', () => {
render(<SwipeCard {...defaultProps} />);
const img = screen.getByRole('img');
expect(img).toHaveAttribute('src', 'https://example.com/photo.jpg');
});
it('renders external link button', () => {
render(<SwipeCard {...defaultProps} />);
// The button contains an ExternalLink icon
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBeGreaterThan(0);
});
it('opens listing URL when external link clicked', async () => {
const user = userEvent.setup();
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
render(<SwipeCard {...defaultProps} />);
const button = screen.getByRole('button');
await user.click(button);
expect(openSpy).toHaveBeenCalledWith(
'https://www.rightmove.co.uk/properties/12345',
'_blank',
'noopener,noreferrer',
);
openSpy.mockRestore();
});
it('renders POI distances when present', () => {
const featureWithPOIs = createMockFeature({
poi_distances: [
{ poi_id: 1, poi_name: 'Office', travel_mode: 'TRANSIT', duration_seconds: 1800, distance_meters: 5000 },
],
});
render(<SwipeCard {...defaultProps} feature={featureWithPOIs} />);
expect(screen.getByText(/Office: 30m/)).toBeInTheDocument();
});
it('does not show POI section when no distances', () => {
const featureNoPOIs = createMockFeature({ poi_distances: undefined });
const { container } = render(<SwipeCard {...defaultProps} feature={featureNoPOIs} />);
// No POI badges should be rendered
expect(container.querySelectorAll('.bg-muted.px-2').length).toBe(0);
});
});