fix(web): scope Suspense to page body; guard formatConfig against null config

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 16:12:21 +07:00
parent 8c35aed8f2
commit 7256adf637
3 changed files with 22 additions and 16 deletions
+3 -1
View File
@@ -24,7 +24,9 @@ test("renders navigation and redirects to domains", async () => {
) )
// Sidebar nav also renders a "Domains" link label, so scope the assertion // Sidebar nav also renders a "Domains" link label, so scope the assertion
// to the routed page content to unambiguously confirm the redirect + page. // to the routed page content to unambiguously confirm the redirect + page.
// Suspense is now scoped inside <main>, so <main> mounts with the loading
// fallback first — await the lazy chunk resolving to the actual page text.
const main = await screen.findByRole("main") const main = await screen.findByRole("main")
expect(within(main).getByText("Domains")).toBeInTheDocument() expect(await within(main).findByText("Domains")).toBeInTheDocument()
expect(screen.getByRole("link", { name: /domains/i })).toBeInTheDocument() expect(screen.getByRole("link", { name: /domains/i })).toBeInTheDocument()
}) })
+7 -3
View File
@@ -17,17 +17,22 @@ const ChannelsPage = lazy(() => import("@/pages/ChannelsPage").then((m) => ({ de
// Every non-auth route shares the same guard + chrome; wrapping here keeps // Every non-auth route shares the same guard + chrome; wrapping here keeps
// each <Route> below a one-liner instead of repeating both on every page. // each <Route> below a one-liner instead of repeating both on every page.
// Suspense is scoped to just the page body (not Layout) so lazy-loading a
// route doesn't collapse the sidebar/header chrome to the fallback on nav.
function Protected({ children }: { children: ReactNode }) { function Protected({ children }: { children: ReactNode }) {
return ( return (
<ProtectedRoute> <ProtectedRoute>
<Layout>{children}</Layout> <Layout>
<Suspense fallback={<div className="p-6 text-muted-foreground">Загрузка</div>}>
{children}
</Suspense>
</Layout>
</ProtectedRoute> </ProtectedRoute>
) )
} }
export function App() { export function App() {
return ( return (
<Suspense fallback={<div className="p-6 text-muted-foreground">Загрузка</div>}>
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} /> <Route path="/register" element={<RegisterPage />} />
@@ -39,6 +44,5 @@ export function App() {
<Route path="/schedule" element={<Protected><SchedulePage /></Protected>} /> <Route path="/schedule" element={<Protected><SchedulePage /></Protected>} />
<Route path="/channels" element={<Protected><ChannelsPage /></Protected>} /> <Route path="/channels" element={<Protected><ChannelsPage /></Protected>} />
</Routes> </Routes>
</Suspense>
) )
} }
+1 -1
View File
@@ -74,7 +74,7 @@ const EMPTY_FORM: ChannelForm = { type: "telegram", chatId: "", botToken: "", ur
// (Object.entries по всему config печатал бы любое поле, включая случайно // (Object.entries по всему config печатал бы любое поле, включая случайно
// сохранённый секрет). // сохранённый секрет).
function formatConfig(type: string, config: object): string { function formatConfig(type: string, config: object): string {
const c = config as Record<string, unknown> const c = (config ?? {}) as Record<string, unknown>
if (type === "telegram") return c.chat_id ? `chat_id: ${String(c.chat_id)}` : "" if (type === "telegram") return c.chat_id ? `chat_id: ${String(c.chat_id)}` : ""
if (type === "webhook") return c.url ? `url: ${String(c.url)}` : "" if (type === "webhook") return c.url ? `url: ${String(c.url)}` : ""
return "" return ""