feat: enhance testing and admin page functionality
- Mocked `useRouter` from `next/navigation` in tests to improve routing behavior during testing. - Updated admin page tests to reflect changes in title display and removed unnecessary back link check. - Refactored admin page header to improve accessibility and layout, displaying month and year more clearly. - Removed unused imports and components to streamline code and enhance maintainability.
This commit is contained in:
@@ -93,13 +93,12 @@ describe("AdminPage", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows admin title and back link when allowed and data loaded", async () => {
|
it("shows admin title (month/year) when allowed and data loaded", async () => {
|
||||||
mockFetchForAdmin();
|
mockFetchForAdmin();
|
||||||
render(<AdminPage />);
|
render(<AdminPage />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByRole("heading", { name: /admin|админка/i })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { name: /admin|админка/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(screen.getByRole("link", { name: /back to calendar|назад к календарю/i })).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows access denied when fetchAdminMe returns is_admin false", async () => {
|
it("shows access denied when fetchAdminMe returns is_admin false", async () => {
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useAdminPage, AdminDutyList, ReassignSheet } from "@/components/admin";
|
import { useAdminPage, AdminDutyList, ReassignSheet } from "@/components/admin";
|
||||||
import { useTranslation } from "@/i18n/use-translation";
|
import { useTranslation } from "@/i18n/use-translation";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
|
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
|
||||||
import { LoadingState } from "@/components/states/LoadingState";
|
import { LoadingState } from "@/components/states/LoadingState";
|
||||||
import { ErrorState } from "@/components/states/ErrorState";
|
import { ErrorState } from "@/components/states/ErrorState";
|
||||||
@@ -47,24 +45,28 @@ export default function AdminPage() {
|
|||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{admin.adminAccessDeniedDetail ?? t("admin.access_denied")}
|
{admin.adminAccessDeniedDetail ?? t("admin.access_denied")}
|
||||||
</p>
|
</p>
|
||||||
<Button asChild variant="outline">
|
|
||||||
<Link href="/">{t("admin.back_to_calendar")}</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const month = admin.currentMonth.getMonth();
|
||||||
|
const year = admin.currentMonth.getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={PAGE_WRAPPER_CLASS}>
|
<div className={PAGE_WRAPPER_CLASS}>
|
||||||
<header className="sticky top-0 z-10 flex items-center justify-between border-b bg-[var(--header-bg)] py-3">
|
<header className="sticky top-0 z-10 flex flex-col items-center border-b bg-[var(--header-bg)] py-3">
|
||||||
<h1 className="text-lg font-semibold">
|
<h1
|
||||||
{t("admin.title")} — {monthName(admin.currentMonth.getMonth())}{" "}
|
className="m-0 flex flex-col items-center justify-center gap-0 leading-none"
|
||||||
{admin.currentMonth.getFullYear()}
|
aria-label={`${t("admin.title")}, ${monthName(month)} ${year}`}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-normal leading-none text-muted">
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
<span className="text-[1.1rem] font-semibold leading-tight sm:text-[1.25rem]">
|
||||||
|
{monthName(month)}
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Button asChild variant="ghost" size="sm">
|
|
||||||
<Link href="/">{t("admin.back_to_calendar")}</Link>
|
|
||||||
</Button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{admin.successMessage && (
|
{admin.successMessage && (
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { resetAppStore } from "@/test/test-utils";
|
|||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { useTelegramAuth } from "@/hooks/use-telegram-auth";
|
import { useTelegramAuth } from "@/hooks/use-telegram-auth";
|
||||||
|
|
||||||
|
vi.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/hooks/use-telegram-auth", () => ({
|
vi.mock("@/hooks/use-telegram-auth", () => ({
|
||||||
useTelegramAuth: vi.fn(),
|
useTelegramAuth: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useState, useEffect, useCallback } from "react";
|
import { useRef, useState, useEffect, useCallback } from "react";
|
||||||
import Link from "next/link";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { settingsButton } from "@telegram-apps/sdk-react";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useMonthData } from "@/hooks/use-month-data";
|
import { useMonthData } from "@/hooks/use-month-data";
|
||||||
import { useSwipe } from "@/hooks/use-swipe";
|
import { useSwipe } from "@/hooks/use-swipe";
|
||||||
import { useStickyScroll } from "@/hooks/use-sticky-scroll";
|
import { useStickyScroll } from "@/hooks/use-sticky-scroll";
|
||||||
import { useAutoRefresh } from "@/hooks/use-auto-refresh";
|
import { useAutoRefresh } from "@/hooks/use-auto-refresh";
|
||||||
import { useTranslation } from "@/i18n/use-translation";
|
|
||||||
import { CalendarHeader } from "@/components/calendar/CalendarHeader";
|
import { CalendarHeader } from "@/components/calendar/CalendarHeader";
|
||||||
import { CalendarGrid } from "@/components/calendar/CalendarGrid";
|
import { CalendarGrid } from "@/components/calendar/CalendarGrid";
|
||||||
import { DutyList } from "@/components/duty/DutyList";
|
import { DutyList } from "@/components/duty/DutyList";
|
||||||
@@ -80,7 +80,7 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const router = useRouter();
|
||||||
|
|
||||||
const { retry } = useMonthData({
|
const { retry } = useMonthData({
|
||||||
initDataRaw,
|
initDataRaw,
|
||||||
@@ -138,6 +138,35 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
}
|
}
|
||||||
}, [loading, accessDenied, setAppContentReady]);
|
}, [loading, accessDenied, setAppContentReady]);
|
||||||
|
|
||||||
|
// Show native Settings button in Telegram context menu for admins; click navigates to /admin.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdmin) return;
|
||||||
|
let offClick: (() => void) | undefined;
|
||||||
|
try {
|
||||||
|
if (settingsButton.mount.isAvailable()) {
|
||||||
|
settingsButton.mount();
|
||||||
|
}
|
||||||
|
if (settingsButton.show.isAvailable()) {
|
||||||
|
settingsButton.show();
|
||||||
|
}
|
||||||
|
if (settingsButton.onClick.isAvailable()) {
|
||||||
|
offClick = settingsButton.onClick(() => router.push("/admin"));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-Telegram environment; SettingsButton not available.
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
if (typeof offClick === "function") offClick();
|
||||||
|
if (settingsButton.hide.isAvailable()) {
|
||||||
|
settingsButton.hide();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors in non-Telegram environment.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isAdmin, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6">
|
<div className="content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6">
|
||||||
<div
|
<div
|
||||||
@@ -149,16 +178,6 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
disabled={navDisabled}
|
disabled={navDisabled}
|
||||||
onPrevMonth={handlePrevMonth}
|
onPrevMonth={handlePrevMonth}
|
||||||
onNextMonth={handleNextMonth}
|
onNextMonth={handleNextMonth}
|
||||||
trailingContent={
|
|
||||||
isAdmin ? (
|
|
||||||
<Link
|
|
||||||
href="/admin"
|
|
||||||
className="text-sm text-accent hover:underline focus-visible:outline-accent rounded"
|
|
||||||
>
|
|
||||||
{t("admin.link")}
|
|
||||||
</Link>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<CalendarGrid
|
<CalendarGrid
|
||||||
currentMonth={currentMonth}
|
currentMonth={currentMonth}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslation } from "@/i18n/use-translation";
|
import { useTranslation } from "@/i18n/use-translation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -21,8 +20,6 @@ export interface CalendarHeaderProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onPrevMonth: () => void;
|
onPrevMonth: () => void;
|
||||||
onNextMonth: () => void;
|
onNextMonth: () => void;
|
||||||
/** Optional content shown above the nav row (e.g. Admin link). */
|
|
||||||
trailingContent?: ReactNode;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +28,6 @@ export function CalendarHeader({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
onPrevMonth,
|
onPrevMonth,
|
||||||
onNextMonth,
|
onNextMonth,
|
||||||
trailingContent,
|
|
||||||
className,
|
className,
|
||||||
}: CalendarHeaderProps) {
|
}: CalendarHeaderProps) {
|
||||||
const { t, monthName, weekdayLabels } = useTranslation();
|
const { t, monthName, weekdayLabels } = useTranslation();
|
||||||
@@ -41,9 +37,6 @@ export function CalendarHeader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={cn("flex flex-col", className)}>
|
<header className={cn("flex flex-col", className)}>
|
||||||
{trailingContent != null && (
|
|
||||||
<div className="flex justify-end mb-1 min-h-[1.5rem]">{trailingContent}</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user