fix(search): default to half a mile, and offer a quarter mile #165
No files matched your search
@@ -270,6 +270,9 @@ test('searching by name returns school results', async ({ page }) => {
|
|||||||
test('searching by postcode returns nearby schools', async ({ page }) => {
|
test('searching by postcode returns nearby schools', async ({ page }) => {
|
||||||
await searchByName(page, 'B1 1BB');
|
await searchByName(page, 'B1 1BB');
|
||||||
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
||||||
|
// A new postcode search starts at half a mile.
|
||||||
|
await expect(page).toHaveURL(/[?&]radius=0\.5(&|$)/);
|
||||||
|
await expect(page.getByRole('combobox', { name: 'Distance' })).toHaveValue('0.5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the phase filter switches straight from secondary to primary', async ({ page }) => {
|
test('the phase filter switches straight from secondary to primary', async ({ page }) => {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { FilterBar } from '@/components/FilterBar';
|
||||||
|
|
||||||
|
const push = jest.fn();
|
||||||
|
let searchParams = new URLSearchParams();
|
||||||
|
jest.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({ push, replace: jest.fn(), prefetch: jest.fn() }),
|
||||||
|
usePathname: () => '/',
|
||||||
|
useSearchParams: () => searchParams,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
local_authorities: [], school_types: [], years: [], phases: [],
|
||||||
|
genders: [], admissions_policies: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
push.mockClear();
|
||||||
|
searchParams = new URLSearchParams();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FilterBar default distance', () => {
|
||||||
|
it('searches a new postcode within half a mile', async () => {
|
||||||
|
render(<FilterBar filters={FILTERS} />);
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/School name or postcode/i), 'SW19 6AR{Enter}');
|
||||||
|
await waitFor(() => expect(push).toHaveBeenCalledWith(expect.stringContaining('radius=0.5')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows half a mile when the URL carries a postcode but no radius', () => {
|
||||||
|
searchParams = new URLSearchParams('postcode=SW196AR');
|
||||||
|
render(<FilterBar filters={FILTERS} />);
|
||||||
|
expect(screen.getByRole('combobox', { name: 'Distance' })).toHaveValue('0.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a distance the user already chose', async () => {
|
||||||
|
searchParams = new URLSearchParams('postcode=SW196AR&radius=3');
|
||||||
|
render(<FilterBar filters={FILTERS} />);
|
||||||
|
expect(screen.getByRole('combobox', { name: 'Distance' })).toHaveValue('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FilterBar distance options', () => {
|
||||||
|
it('offers a quarter mile without making it the default', () => {
|
||||||
|
searchParams = new URLSearchParams('postcode=SW196AR');
|
||||||
|
render(<FilterBar filters={FILTERS} />);
|
||||||
|
const distance = screen.getByRole('combobox', { name: 'Distance' });
|
||||||
|
expect([...(distance as HTMLSelectElement).options].map(o => o.value)).toEqual(['0.25', '0.5', '1', '3', '5']);
|
||||||
|
expect(distance).toHaveValue('0.5');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -180,5 +180,16 @@ it('lets a keyboard pick a pin from the list, with a real button', async () => {
|
|||||||
it('keeps the postcode in the heading in list view, where there is room', async () => {
|
it('keeps the postcode in the heading in list view, where there is room', async () => {
|
||||||
await renderMap();
|
await renderMap();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
||||||
expect(screen.getByRole('heading', { name: '3 schools within 1.0 miles of SW196AR' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '3 schools within 1 mile of SW196AR' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('draws and names a quarter-mile search as 0.25, not rounded to 0.3', async () => {
|
||||||
|
params = new URLSearchParams('postcode=SW196AR&radius=0.25');
|
||||||
|
const quarter = { ...results(), location_info: { postcode: 'SW196AR', radius: 0.25 * 1.60934, coordinates: [51.42, -0.21] } } as SchoolsResponse;
|
||||||
|
render(<HomeView initialSchools={quarter} filters={filters} />);
|
||||||
|
await act(async () => {});
|
||||||
|
expect(screen.getByTestId('map')).toHaveAttribute('data-radius', '0.25');
|
||||||
|
expect(screen.getByRole('heading', { name: '3 schools within 0.25 miles' })).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'List' }));
|
||||||
|
expect(screen.getByRole('heading', { name: '3 schools within 0.25 miles of SW196AR' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
import { absoluteUrl } from '@/lib/site';
|
import { absoluteUrl } from '@/lib/site';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { fetchSchools, fetchFilters, fetchDataInfo } from '@/lib/api';
|
import { fetchSchools, fetchFilters, fetchDataInfo } from '@/lib/api';
|
||||||
import { formatAcademicYear } from '@/lib/utils';
|
import { DEFAULT_RADIUS_MILES, formatAcademicYear } from '@/lib/utils';
|
||||||
import { HomeView } from '@/components/HomeView';
|
import { HomeView } from '@/components/HomeView';
|
||||||
import { getFlags } from '@/lib/flags';
|
import { getFlags } from '@/lib/flags';
|
||||||
import { HowItWorksSection } from '@/components/HowItWorksSection';
|
import { HowItWorksSection } from '@/components/HowItWorksSection';
|
||||||
@@ -71,7 +71,9 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
|
|
||||||
// Parse search params
|
// Parse search params
|
||||||
const page = parseInt(params.page || '1');
|
const page = parseInt(params.page || '1');
|
||||||
const radius = params.radius ? parseFloat(params.radius) : undefined;
|
// Without this the API applies its own 5-mile default while the Distance
|
||||||
|
// control shows DEFAULT_RADIUS_MILES.
|
||||||
|
const radius = params.radius ? parseFloat(params.radius) : DEFAULT_RADIUS_MILES;
|
||||||
|
|
||||||
// Check if user has performed a search
|
// Check if user has performed a search
|
||||||
const hasSearchParams = !!(
|
const hasSearchParams = !!(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useCallback, useTransition, useRef, useEffect } from "react";
|
import { useState, useCallback, useTransition, useRef, useEffect } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||||
import { isValidPostcode, schoolUrl } from "@/lib/utils";
|
import { DEFAULT_RADIUS_MILES, isValidPostcode, schoolUrl } from "@/lib/utils";
|
||||||
import { track } from "@/lib/analytics";
|
import { track } from "@/lib/analytics";
|
||||||
import { useSchoolSuggest } from "@/hooks/useSchoolSuggest";
|
import { useSchoolSuggest } from "@/hooks/useSchoolSuggest";
|
||||||
import { SuggestList, suggestOptionId } from "./SuggestList";
|
import { SuggestList, suggestOptionId } from "./SuggestList";
|
||||||
@@ -70,7 +70,7 @@ export function FilterBar({
|
|||||||
|
|
||||||
const currentSearch = searchParams.get("search") || "";
|
const currentSearch = searchParams.get("search") || "";
|
||||||
const currentPostcode = searchParams.get("postcode") || "";
|
const currentPostcode = searchParams.get("postcode") || "";
|
||||||
const currentRadius = searchParams.get("radius") || "1";
|
const currentRadius = searchParams.get("radius") || String(DEFAULT_RADIUS_MILES);
|
||||||
const initialOmniValue = currentPostcode || currentSearch;
|
const initialOmniValue = currentPostcode || currentSearch;
|
||||||
|
|
||||||
const [omniValue, setOmniValue] = useState(initialOmniValue);
|
const [omniValue, setOmniValue] = useState(initialOmniValue);
|
||||||
@@ -269,7 +269,7 @@ export function FilterBar({
|
|||||||
if (isPostcode) {
|
if (isPostcode) {
|
||||||
updateURL({
|
updateURL({
|
||||||
postcode: cleaned.toUpperCase(),
|
postcode: cleaned.toUpperCase(),
|
||||||
radius: currentRadius || "1",
|
radius: currentRadius,
|
||||||
search: "",
|
search: "",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -481,6 +481,7 @@ export function FilterBar({
|
|||||||
aria-label="Distance"
|
aria-label="Distance"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
|
<option value="0.25">Within 0.25 miles</option>
|
||||||
<option value="0.5">Within 0.5 miles</option>
|
<option value="0.5">Within 0.5 miles</option>
|
||||||
<option value="1">Within 1 mile</option>
|
<option value="1">Within 1 mile</option>
|
||||||
<option value="3">Within 3 miles</option>
|
<option value="3">Within 3 miles</option>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useComparisonContext } from '@/context/ComparisonContext';
|
|||||||
import { fetchSchools, fetchLAaverages, fetchNationalAverages } from '@/lib/api';
|
import { fetchSchools, fetchLAaverages, fetchNationalAverages } from '@/lib/api';
|
||||||
import type { SchoolsResponse, Filters, School } from '@/lib/types';
|
import type { SchoolsResponse, Filters, School } from '@/lib/types';
|
||||||
import { track } from '@/lib/analytics';
|
import { track } from '@/lib/analytics';
|
||||||
|
import { DEFAULT_RADIUS_MILES } from '@/lib/utils';
|
||||||
import styles from './HomeView.module.css';
|
import styles from './HomeView.module.css';
|
||||||
|
|
||||||
interface HomeViewProps {
|
interface HomeViewProps {
|
||||||
@@ -340,6 +341,7 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
|||||||
setIsLoadingMap(true);
|
setIsLoadingMap(true);
|
||||||
const params: Record<string, any> = {};
|
const params: Record<string, any> = {};
|
||||||
searchParams.forEach((value, key) => { params[key] = value; });
|
searchParams.forEach((value, key) => { params[key] = value; });
|
||||||
|
params.radius ??= DEFAULT_RADIUS_MILES;
|
||||||
params.page = 1;
|
params.page = 1;
|
||||||
params.page_size = 500;
|
params.page_size = 500;
|
||||||
fetchSchools(params, { cache: 'no-store', signal: controller.signal })
|
fetchSchools(params, { cache: 'no-store', signal: controller.signal })
|
||||||
@@ -419,7 +421,7 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
|||||||
setGeoState('idle');
|
setGeoState('idle');
|
||||||
track('near_me_used', { outcome: 'granted' });
|
track('near_me_used', { outcome: 'granted' });
|
||||||
track('search_submitted', { query: postcode, via: 'near_me', has_postcode: true, filters_active: '', filters_count: 0 });
|
track('search_submitted', { query: postcode, via: 'near_me', has_postcode: true, filters_active: '', filters_count: 0 });
|
||||||
router.push(`/?postcode=${encodeURIComponent(postcode)}&radius=1`);
|
router.push(`/?postcode=${encodeURIComponent(postcode)}&radius=${DEFAULT_RADIUS_MILES}`);
|
||||||
} else {
|
} else {
|
||||||
track('near_me_used', { outcome: 'no_postcode' });
|
track('near_me_used', { outcome: 'no_postcode' });
|
||||||
setGeoState('error');
|
setGeoState('error');
|
||||||
@@ -459,8 +461,9 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
|||||||
// open card); only the list beside it is sorted.
|
// open card); only the list beside it is sorted.
|
||||||
const mapResultSchools = mapSchools.length > 0 ? mapSchools : initialSchools.schools;
|
const mapResultSchools = mapSchools.length > 0 ? mapSchools : initialSchools.schools;
|
||||||
const mapListSchools = sortSchools(mapResultSchools);
|
const mapListSchools = sortSchools(mapResultSchools);
|
||||||
|
// Two places, so a 0.25-mile search is not drawn or labelled as 0.3.
|
||||||
const radiusMiles = initialSchools.location_info
|
const radiusMiles = initialSchools.location_info
|
||||||
? Number((initialSchools.location_info.radius / 1.60934).toFixed(1))
|
? Number((initialSchools.location_info.radius / 1.60934).toFixed(2))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Empty-results sentinel: track when a search returns nothing.
|
// Empty-results sentinel: track when a search returns nothing.
|
||||||
@@ -594,8 +597,8 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
|
|||||||
drops the postcode, which the search box just above already shows. */}
|
drops the postcode, which the search box just above already shows. */}
|
||||||
{isMapView && radiusMiles != null
|
{isMapView && radiusMiles != null
|
||||||
? `${schoolCount} within ${radiusMiles} mile${radiusMiles === 1 ? '' : 's'}`
|
? `${schoolCount} within ${radiusMiles} mile${radiusMiles === 1 ? '' : 's'}`
|
||||||
: isLocationSearch && initialSchools.location_info
|
: isLocationSearch && radiusMiles != null && initialSchools.location_info
|
||||||
? `${schoolCount} within ${(initialSchools.location_info.radius / 1.60934).toFixed(1)} miles of ${initialSchools.location_info.postcode}`
|
? `${schoolCount} within ${radiusMiles} mile${radiusMiles === 1 ? '' : 's'} of ${initialSchools.location_info.postcode}`
|
||||||
: `${schoolCount} found`
|
: `${schoolCount} found`
|
||||||
}
|
}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ export function getVariabilityLabel(stdDev: number | null): string {
|
|||||||
// Validation
|
// Validation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Radius a postcode search uses until the user picks another. */
|
||||||
|
export const DEFAULT_RADIUS_MILES = 0.5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate UK postcode format
|
* Validate UK postcode format
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in new issue
Block a user