diff --git a/src/App.tsx b/src/App.tsx
index efead44..ed93e73 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from "react";
-import { Routes, Route, NavLink, useNavigate, useLocation } from "react-router-dom";
+import { Routes, Route, NavLink, Navigate, useNavigate, useLocation } from "react-router-dom";
import { ThemeToggle } from "./ThemeToggle";
import ServerError from "./components/ServerError";
import useDocumentTitle from "./hooks/useDocumentTitle";
@@ -24,6 +24,7 @@ import { useSessionExpiry } from "./hooks/useSessionExpiry";
import SessionExpiryBanner from "./components/SessionExpiryBanner";
import WalletStatus from "./components/WalletStatus";
import { useWalletStore } from "./state/walletStore";
+import { useToast } from "./components/Toast";
// Route splitting: dynamic imports for all heavy page routes
const MarketplacePage = lazy(() => import("./pages/MarketplacePage"));
@@ -216,7 +217,8 @@ const APP_ROUTES = {
themePlayground: "/theme-playground",
designSystem: "/design-system/docs",
serverError: "/500",
- rateLimitCard: "/rate-limit",
+ rateLimitCard: "/marketplace/:apiId/rate-limits",
+ legacyRateLimit: "/rate-limit",
slaCard: "/marketplace/:apiId/sla",
webhookDeliveries: "/webhooks/deliveries",
onboarding: "/onboarding",
@@ -235,7 +237,11 @@ const PRIMARY_NAV: Array<{ to: string; label: string }> = [
/** `/rate-limit` no longer exists; limits are per-API. Send users to the Marketplace. */
function LegacyRateLimitRedirect() {
const { showToast } = useToast();
+ const announced = useRef(false);
useEffect(() => {
+ // Guard against StrictMode's double effect invocation in development.
+ if (announced.current) return;
+ announced.current = true;
showToast("Rate limits are now shown per API. Pick an API to see its limits.");
}, [showToast]);
return ;
diff --git a/src/RouteSplitting.test.tsx b/src/RouteSplitting.test.tsx
index 7026380..ffb4c2e 100644
--- a/src/RouteSplitting.test.tsx
+++ b/src/RouteSplitting.test.tsx
@@ -70,7 +70,7 @@ describe('Route Splitting and Responsiveness Suite (Quality-2)', () => {
});
it('dynamically loads Rate Limit Card route inside Suspense boundary', async () => {
- renderApp('/rate-limit');
+ renderApp('/marketplace/weather-001/rate-limits');
await waitFor(() => {
expect(screen.getByRole('heading', { name: /Rate Limit Configuration/i })).toBeTruthy();
}, { timeout: 4000 });
diff --git a/src/pages/ApiDetailPage.tsx b/src/pages/ApiDetailPage.tsx
index 3b87a63..5f9c155 100644
--- a/src/pages/ApiDetailPage.tsx
+++ b/src/pages/ApiDetailPage.tsx
@@ -1325,6 +1325,15 @@ export default function ApiDetailPage({ onBack }: Props) {
View SLA
+
+ Rate limits
+
+ View rate limits
+
+
diff --git a/src/pages/RateLimitCard.test.tsx b/src/pages/RateLimitCard.test.tsx
index a987481..a42bb81 100644
--- a/src/pages/RateLimitCard.test.tsx
+++ b/src/pages/RateLimitCard.test.tsx
@@ -1,30 +1,48 @@
// @vitest-environment jsdom
-import { cleanup, render, screen } from "@testing-library/react";
-import { afterEach, describe, expect, it, vi } from "vitest";
-import { MemoryRouter } from "react-router-dom";
-import RateLimitCard, { type RateLimitCardProps } from "./RateLimitCard";
+import { cleanup, render, screen, within } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { MemoryRouter, Route, Routes } from "react-router-dom";
+import RateLimitCard, { PLAN_PATTERNS, type RateLimitCardProps } from "./RateLimitCard";
+import type { APIItem } from "../data/mockApis";
+import { ROUTE_CATALOGUE } from "../config/routeCatalogue";
/**
- * RateLimitCard tests – issue #567 (GrantFox FWC26 "Stellar Wave")
- * and mobile responsive stacked plan cards.
+ * RateLimitCard tests – per-API rate limits at `/marketplace/:apiId/rate-limits`.
*
- * Covers:
- * 1. Mobile layout: four plan cards, no table
- * 2. Desktop layout: table with four body rows
- * 3. currentPlan prop: badge and aria-current on card or row
- * 4. Numbers: non-breaking tabular-nums span
- * 5. Pattern text: exposed to assistive tech
- * 6. Style injection: no inline