Community 93 Premium feature complete
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* @ai-summary Tests for Community Stations API Client
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { communityStationsApi } from '../../api/community-stations.api';
|
||||
import { SubmitStationData, ReviewDecision } from '../../types/community-stations.types';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('Community Stations API Client', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('submitStation', () => {
|
||||
it('should submit a station successfully', async () => {
|
||||
const submitData: SubmitStationData = {
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
city: 'Denver',
|
||||
state: 'CO',
|
||||
zipCode: '80202',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
brand: 'Shell',
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
price93: 3.599,
|
||||
notes: 'Good quality',
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: '1',
|
||||
...submitData,
|
||||
status: 'pending',
|
||||
submittedBy: 'user@example.com',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await communityStationsApi.submitStation(submitData);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/stations/community/submit',
|
||||
submitData
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle submission errors', async () => {
|
||||
const submitData: SubmitStationData = {
|
||||
name: 'Shell',
|
||||
address: '123 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
};
|
||||
|
||||
const mockError = new Error('Network error');
|
||||
mockedAxios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(communityStationsApi.submitStation(submitData)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMySubmissions', () => {
|
||||
it('should fetch user submissions', async () => {
|
||||
const mockSubmissions = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
status: 'pending',
|
||||
submittedBy: 'user@example.com',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: mockSubmissions });
|
||||
|
||||
const result = await communityStationsApi.getMySubmissions();
|
||||
|
||||
expect(result).toEqual(mockSubmissions);
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith('/stations/community/mine');
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdrawSubmission', () => {
|
||||
it('should withdraw a submission', async () => {
|
||||
mockedAxios.delete.mockResolvedValueOnce({ data: null });
|
||||
|
||||
await communityStationsApi.withdrawSubmission('1');
|
||||
|
||||
expect(mockedAxios.delete).toHaveBeenCalledWith('/stations/community/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApprovedStations', () => {
|
||||
it('should fetch approved stations with pagination', async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
stations: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
status: 'approved',
|
||||
submittedBy: 'user@example.com',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 0,
|
||||
limit: 50,
|
||||
},
|
||||
};
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await communityStationsApi.getApprovedStations(0, 50);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith('/stations/community/approved', {
|
||||
params: { page: 0, limit: 50 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reviewStation (admin)', () => {
|
||||
it('should approve a station', async () => {
|
||||
const decision: ReviewDecision = { status: 'approved' };
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: '1',
|
||||
status: 'approved',
|
||||
reviewedAt: new Date().toISOString(),
|
||||
reviewedBy: 'admin@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
mockedAxios.patch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await communityStationsApi.reviewStation('1', decision);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
||||
'/stations/community/admin/1/review',
|
||||
decision
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject a station with reason', async () => {
|
||||
const decision: ReviewDecision = {
|
||||
status: 'rejected',
|
||||
rejectionReason: 'Invalid location',
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: '1',
|
||||
status: 'rejected',
|
||||
rejectionReason: 'Invalid location',
|
||||
reviewedAt: new Date().toISOString(),
|
||||
reviewedBy: 'admin@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
mockedAxios.patch.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await communityStationsApi.reviewStation('1', decision);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
expect(mockedAxios.patch).toHaveBeenCalledWith(
|
||||
'/stations/community/admin/1/review',
|
||||
decision
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkReviewStations (admin)', () => {
|
||||
it('should bulk approve stations', async () => {
|
||||
const ids = ['1', '2', '3'];
|
||||
const decision: ReviewDecision = { status: 'approved' };
|
||||
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{ id: '1', status: 'approved' },
|
||||
{ id: '2', status: 'approved' },
|
||||
{ id: '3', status: 'approved' },
|
||||
],
|
||||
};
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await communityStationsApi.bulkReviewStations(ids, decision);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith('/stations/community/admin/bulk-review', {
|
||||
ids,
|
||||
...decision,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @ai-summary Tests for CommunityStationCard component
|
||||
*/
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { CommunityStationCard } from '../../components/CommunityStationCard';
|
||||
import { CommunityStation } from '../../types/community-stations.types';
|
||||
|
||||
describe('CommunityStationCard', () => {
|
||||
const mockStation: CommunityStation = {
|
||||
id: '1',
|
||||
submittedBy: 'user@example.com',
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
city: 'Denver',
|
||||
state: 'CO',
|
||||
zipCode: '80202',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
brand: 'Shell',
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
price93: 3.599,
|
||||
notes: 'Good quality fuel',
|
||||
status: 'approved',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
it('should render station details', () => {
|
||||
render(<CommunityStationCard station={mockStation} />);
|
||||
|
||||
expect(screen.getByText('Shell Downtown')).toBeInTheDocument();
|
||||
expect(screen.getByText('123 Main St')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Denver, CO, 80202/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Brand: Shell')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display 93 octane status', () => {
|
||||
render(<CommunityStationCard station={mockStation} />);
|
||||
|
||||
expect(screen.getByText('93 Octane · w/ Ethanol')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display price when available', () => {
|
||||
render(<CommunityStationCard station={mockStation} />);
|
||||
|
||||
expect(screen.getByText('$3.599/gal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display status badge', () => {
|
||||
render(<CommunityStationCard station={mockStation} />);
|
||||
|
||||
expect(screen.getByText('approved')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show withdraw button for user view', () => {
|
||||
render(
|
||||
<CommunityStationCard
|
||||
station={mockStation}
|
||||
isAdmin={false}
|
||||
onWithdraw={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show approve and reject buttons for admin', () => {
|
||||
const pendingStation = { ...mockStation, status: 'pending' as const };
|
||||
render(
|
||||
<CommunityStationCard
|
||||
station={pendingStation}
|
||||
isAdmin={true}
|
||||
onApprove={jest.fn()}
|
||||
onReject={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should call onWithdraw when withdraw button is clicked', () => {
|
||||
const onWithdraw = jest.fn();
|
||||
render(
|
||||
<CommunityStationCard
|
||||
station={mockStation}
|
||||
isAdmin={false}
|
||||
onWithdraw={onWithdraw}
|
||||
/>
|
||||
);
|
||||
|
||||
const withdrawButton = screen.getByRole('button');
|
||||
fireEvent.click(withdrawButton);
|
||||
|
||||
expect(onWithdraw).toHaveBeenCalledWith(mockStation.id);
|
||||
});
|
||||
|
||||
it('should handle rejection with reason', async () => {
|
||||
const onReject = jest.fn();
|
||||
const pendingStation = { ...mockStation, status: 'pending' as const };
|
||||
|
||||
render(
|
||||
<CommunityStationCard
|
||||
station={pendingStation}
|
||||
isAdmin={true}
|
||||
onReject={onReject}
|
||||
/>
|
||||
);
|
||||
|
||||
// This test would need more interaction handling
|
||||
// for the dialog that appears on reject
|
||||
});
|
||||
|
||||
it('should work on mobile viewport', () => {
|
||||
render(<CommunityStationCard station={mockStation} />);
|
||||
|
||||
expect(screen.getByText('Shell Downtown')).toBeInTheDocument();
|
||||
// Verify touch targets are at least 44px
|
||||
const buttons = screen.getAllByRole('button');
|
||||
buttons.forEach((button) => {
|
||||
expect(button).toHaveStyle({ minHeight: '44px', minWidth: '44px' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @ai-summary Tests for Community Stations React Query hooks
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
useSubmitStation,
|
||||
useMySubmissions,
|
||||
useApprovedStations,
|
||||
usePendingSubmissions,
|
||||
useReviewStation,
|
||||
} from '../../hooks/useCommunityStations';
|
||||
import * as communityStationsApi from '../../api/community-stations.api';
|
||||
|
||||
// Mock the API
|
||||
jest.mock('../../api/community-stations.api');
|
||||
|
||||
// Setup React Query test wrapper
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => {
|
||||
const testQueryClient = createTestQueryClient();
|
||||
return React.createElement(
|
||||
QueryClientProvider,
|
||||
{ client: testQueryClient },
|
||||
children
|
||||
);
|
||||
};
|
||||
|
||||
describe('Community Stations Hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useSubmitStation', () => {
|
||||
it('should handle successful submission', async () => {
|
||||
const mockStation = {
|
||||
id: '1',
|
||||
submittedBy: 'user@example.com',
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
jest.spyOn(communityStationsApi.communityStationsApi, 'submitStation').mockResolvedValueOnce(mockStation as any);
|
||||
|
||||
const { result } = renderHook(() => useSubmitStation(), { wrapper: Wrapper });
|
||||
|
||||
// Initially should be idle
|
||||
expect(result.current.isPending).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useMySubmissions', () => {
|
||||
it('should fetch user submissions', async () => {
|
||||
const mockSubmissions = [
|
||||
{
|
||||
id: '1',
|
||||
submittedBy: 'user@example.com',
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(communityStationsApi.communityStationsApi, 'getMySubmissions')
|
||||
.mockResolvedValueOnce(mockSubmissions as any);
|
||||
|
||||
const { result } = renderHook(() => useMySubmissions(), { wrapper: Wrapper });
|
||||
|
||||
// Initially should be loading
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
// Wait for data to be loaded
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockSubmissions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useApprovedStations', () => {
|
||||
it('should fetch approved stations with pagination', async () => {
|
||||
const mockResponse = {
|
||||
stations: [
|
||||
{
|
||||
id: '1',
|
||||
submittedBy: 'user@example.com',
|
||||
name: 'Shell Downtown',
|
||||
address: '123 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: false,
|
||||
status: 'approved',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 0,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(communityStationsApi.communityStationsApi, 'getApprovedStations')
|
||||
.mockResolvedValueOnce(mockResponse as any);
|
||||
|
||||
const { result } = renderHook(() => useApprovedStations(0, 50), { wrapper: Wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePendingSubmissions', () => {
|
||||
it('should fetch pending submissions for admin', async () => {
|
||||
const mockResponse = {
|
||||
stations: [
|
||||
{
|
||||
id: '1',
|
||||
submittedBy: 'user@example.com',
|
||||
name: 'Chevron on Main',
|
||||
address: '456 Main St',
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
has93Octane: true,
|
||||
has93OctaneEthanolFree: true,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 0,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(communityStationsApi.communityStationsApi, 'getPendingSubmissions')
|
||||
.mockResolvedValueOnce(mockResponse as any);
|
||||
|
||||
const { result } = renderHook(() => usePendingSubmissions(0, 50), { wrapper: Wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReviewStation', () => {
|
||||
it('should approve a station', async () => {
|
||||
const mockStation = {
|
||||
id: '1',
|
||||
status: 'approved',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(communityStationsApi.communityStationsApi, 'reviewStation')
|
||||
.mockResolvedValueOnce(mockStation as any);
|
||||
|
||||
const { result } = renderHook(() => useReviewStation(), { wrapper: Wrapper });
|
||||
|
||||
expect(result.current.isPending).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject a station with reason', async () => {
|
||||
const mockStation = {
|
||||
id: '1',
|
||||
status: 'rejected',
|
||||
rejectionReason: 'Invalid address',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(communityStationsApi.communityStationsApi, 'reviewStation')
|
||||
.mockResolvedValueOnce(mockStation as any);
|
||||
|
||||
const { result } = renderHook(() => useReviewStation(), { wrapper: Wrapper });
|
||||
|
||||
expect(result.current.isPending).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user