diff --git a/src/features/popular-requests/api.test.ts b/src/features/popular-requests/api.test.ts new file mode 100644 index 00000000..cc251bca --- /dev/null +++ b/src/features/popular-requests/api.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, vi } from "vitest"; +import { ApiClient } from "@/shared/api/client"; +import { getPopularRequests } from "./api"; +import type { PopularRequest } from "./types"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function createMockClient(responseBody: unknown, status = 200) { + const mockFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(responseBody), { + status, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new ApiClient({ + baseUrl: "https://api.test.com", + locale: "ru", + fetchImpl: mockFetch, + retry: { maxRetries: 0 }, + }); + + return { client, mockFetch }; +} + +function extractUrl(mockFetch: ReturnType): URL { + const call = mockFetch.mock.calls[0] as [string, ...unknown[]]; + return new URL(call[0]); +} + +const POPULAR_RESPONSE: PopularRequest[] = [ + { + mode: "FlightNumber", + carrier: "SU", + flightNumber: "9027", + type: "Onlineboard", + }, + { + mode: "FlightNumber", + carrier: "SU", + flightNumber: "9006", + type: "Onlineboard", + }, + { + mode: "Route", + departure: "KUF", + arrival: "ABA", + type: "Onlineboard", + }, + { + mode: "Route", + departure: "RTW", + arrival: "ABA", + type: "Onlineboard", + }, +]; + +// --------------------------------------------------------------------------- +// getPopularRequests +// --------------------------------------------------------------------------- + +describe("getPopularRequests", () => { + it("calls the correct API path", async () => { + const { client, mockFetch } = createMockClient(POPULAR_RESPONSE); + + await getPopularRequests(client); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe("/Requests/1/getpopular"); + }); + + it("returns the deserialized PopularRequest[]", async () => { + const { client } = createMockClient(POPULAR_RESPONSE); + + const result = await getPopularRequests(client); + + expect(result).toEqual(POPULAR_RESPONSE); + }); + + it("throws ApiHttpError on 404", async () => { + const { client } = createMockClient({ error: "not found" }, 404); + + await expect(getPopularRequests(client)).rejects.toThrow("HTTP 404"); + }); + + it("throws ApiHttpError on 500", async () => { + const { client } = createMockClient({ error: "internal" }, 500); + + await expect(getPopularRequests(client)).rejects.toThrow("HTTP 500"); + }); +}); diff --git a/src/features/popular-requests/api.ts b/src/features/popular-requests/api.ts new file mode 100644 index 00000000..7a9646d5 --- /dev/null +++ b/src/features/popular-requests/api.ts @@ -0,0 +1,24 @@ +/** + * Popular Requests API functions. + * + * Pure functions — each takes an `ApiClient` as a parameter (dependency + * injection). No React hooks, no context, no side effects. + * + * @module + */ + +import type { ApiClient } from "@/shared/api/client.js"; +import type { PopularRequest } from "./types.js"; + +/** + * Fetch popular requests from the API. + * Maps to: `GET /Requests/1/getpopular` + * + * Angular equivalent: `PopularRequestsApiService.getPopularRequests()` + * which called `endpointService.buildCommonURL('getpopular', '1', 'Requests')` + */ +export async function getPopularRequests( + client: ApiClient, +): Promise { + return client.get("Requests/1/getpopular"); +} diff --git a/src/features/popular-requests/hooks/usePopularRequests.ts b/src/features/popular-requests/hooks/usePopularRequests.ts new file mode 100644 index 00000000..245c2e99 --- /dev/null +++ b/src/features/popular-requests/hooks/usePopularRequests.ts @@ -0,0 +1,55 @@ +/** + * React hook for fetching popular requests. + * + * Calls `getPopularRequests` on mount, manages loading/error/data state. + * + * @module + */ + +import { useState, useEffect } from "react"; +import { useApiClient } from "@/shared/api/provider.js"; +import { getPopularRequests } from "../api.js"; +import type { PopularRequest } from "../types.js"; +import type { ApiError } from "@/shared/api/errors.js"; + +export interface UsePopularRequestsResult { + requests: PopularRequest[]; + loading: boolean; + error: ApiError | null; +} + +/** + * Hook that fetches popular requests on mount. Returns loading/error/data. + */ +export function usePopularRequests(): UsePopularRequestsResult { + const client = useApiClient(); + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + getPopularRequests(client) + .then((data) => { + if (!cancelled) { + setRequests(data); + setLoading(false); + } + }) + .catch((err: ApiError) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [client]); + + return { requests, loading, error }; +} diff --git a/src/features/popular-requests/index.ts b/src/features/popular-requests/index.ts index e56605f8..72b03681 100644 --- a/src/features/popular-requests/index.ts +++ b/src/features/popular-requests/index.ts @@ -1,2 +1,27 @@ // Public barrel for the popular-requests feature. See frozen-barrels.md. -export {}; + +// 5A — Types +export type { + RequestMode, + PopularRequestType, + PopularRequest, + PopularRouteRequest, + PopularArrivalRequest, + PopularDepartureRequest, + PopularFlightNumberRequest, +} from "./types.js"; + +// 5A — API functions +export { getPopularRequests } from "./api.js"; + +// 5A — React hooks +export { usePopularRequests } from "./hooks/usePopularRequests.js"; +export type { UsePopularRequestsResult } from "./hooks/usePopularRequests.js"; + +// 5B — Components +export { PopularRequestsPanel } from "./components/PopularRequestsPanel.js"; +export type { PopularRequestsPanelProps } from "./components/PopularRequestsPanel.js"; +export { PopularRequestItem } from "./components/PopularRequestItem.js"; +export type { PopularRequestItemProps } from "./components/PopularRequestItem.js"; +export { RequestInfo } from "./components/RequestInfo.js"; +export type { RequestInfoProps } from "./components/RequestInfo.js"; diff --git a/src/features/popular-requests/types.ts b/src/features/popular-requests/types.ts new file mode 100644 index 00000000..2a456922 --- /dev/null +++ b/src/features/popular-requests/types.ts @@ -0,0 +1,63 @@ +/** + * Data model types for the Popular Requests feature. + * + * Ported from Angular typings (ClientApp/src/typings/popular-request.ts, + * ClientApp/src/typings/enums.ts) — flattened into minimal, UI-oriented + * types with no Angular dependencies. + */ + +// --------------------------------------------------------------------------- +// Enums & literals +// --------------------------------------------------------------------------- + +/** Request mode — discriminator for the popular request union */ +export type RequestMode = + | "FlightNumber" + | "Route" + | "RouteWithBack" + | "Departure" + | "Arrival"; + +/** Which feature area the request navigates to */ +export type PopularRequestType = "Schedule" | "Onlineboard"; + +// --------------------------------------------------------------------------- +// Request sub-types (discriminated by `mode`) +// --------------------------------------------------------------------------- + +export interface PopularRouteRequest { + mode: "Route" | "RouteWithBack"; + departure: string; + arrival: string; + type: PopularRequestType; +} + +export interface PopularArrivalRequest { + mode: "Arrival"; + arrival: string; + type: "Onlineboard"; +} + +export interface PopularDepartureRequest { + mode: "Departure"; + departure: string; + type: "Onlineboard"; +} + +export interface PopularFlightNumberRequest { + mode: "FlightNumber"; + carrier: string; + flightNumber: string; + type: "Onlineboard"; +} + +// --------------------------------------------------------------------------- +// Union type +// --------------------------------------------------------------------------- + +/** Any popular request returned by the API */ +export type PopularRequest = + | PopularRouteRequest + | PopularArrivalRequest + | PopularDepartureRequest + | PopularFlightNumberRequest;