Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions __tests__/loading-skeleton-mobile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Issue #279 – Handle mobile viewports navigation styling in loading_spinner_skeleton
*
* Verifies the skeleton is height-constrained and internally scrollable on
* small screens (rather than pushing surrounding controls off-screen), and
* that it never traps pointer events so other elements stay clickable on
* mobile viewports.
*/
import { render, screen, fireEvent } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import LoadingSkeleton from "@/app/components/LoadingSkeleton";
import Dashboard from "@/app/dashboard/page";

const mockUseWallet = vi.fn();

vi.mock("@/app/context/WalletContext", () => ({
useWallet: () => mockUseWallet(),
}));

vi.mock("@/app/components/Navbar", () => ({
default: () => <div data-testid="navbar" />,
}));

describe("LoadingSkeleton – mobile viewport constraints (issue #279)", () => {
it("caps height on mobile with max-h-[70vh] and scrolls internally", () => {
render(<LoadingSkeleton />);
const wrapper = screen.getByTestId("loading-skeleton-mobile-wrapper");
expect(wrapper).toHaveClass("max-h-[70vh]");
expect(wrapper).toHaveClass("overflow-y-auto");
});

it("removes the height cap on larger viewports (sm:max-h-none)", () => {
render(<LoadingSkeleton />);
expect(screen.getByTestId("loading-skeleton-mobile-wrapper")).toHaveClass(
"sm:max-h-none"
);
});

it("contains overscroll within the wrapper instead of the page (overscroll-contain)", () => {
render(<LoadingSkeleton />);
expect(screen.getByTestId("loading-skeleton-mobile-wrapper")).toHaveClass(
"overscroll-contain"
);
});

it("does not use fixed/absolute positioning that would trap pointer events", () => {
render(<LoadingSkeleton />);
const root = screen.getByTestId("loading-skeleton");
expect(root).not.toHaveClass("fixed");
expect(root).not.toHaveClass("absolute");
});
});

describe("Dashboard – surrounding controls stay clickable while the skeleton is visible (issue #279)", () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseWallet.mockReturnValue({ address: "GCLIENT", signTransaction: vi.fn() });
vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {})));
});

it("keeps role-filter buttons enabled and clickable while the loading skeleton is displayed", () => {
render(<Dashboard />);
expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument();

const clientFilter = screen.getByRole("button", { name: "As Client" });
expect(clientFilter).not.toBeDisabled();

fireEvent.click(clientFilter);
expect(clientFilter).toHaveAttribute("aria-pressed", "true");
});
});
61 changes: 33 additions & 28 deletions app/components/LoadingSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
export default function LoadingSkeleton() {
return (
<div className="animate-pulse" role="status" aria-live="polite">
<div className="animate-pulse w-full" role="status" aria-live="polite" data-testid="loading-skeleton">
<span className="sr-only">Loading job data…</span>
<div className="border border-gray-800 rounded-xl bg-gray-900 p-6 space-y-6" aria-hidden="true">
<div className="flex items-center justify-between mb-6">
<div>
<div className="h-6 w-32 bg-gray-800 rounded mb-2"></div>
<div className="h-4 w-24 bg-gray-800 rounded"></div>
<div
className="max-h-[70vh] sm:max-h-none overflow-y-auto overscroll-contain"
data-testid="loading-skeleton-mobile-wrapper"
>
<div className="border border-gray-800 rounded-xl bg-gray-900 p-6 space-y-6" aria-hidden="true">
<div className="flex items-center justify-between mb-6">
<div>
<div className="h-6 w-32 bg-gray-800 rounded mb-2"></div>
<div className="h-4 w-24 bg-gray-800 rounded"></div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gray-800 rounded-lg p-3">
<div className="h-4 w-12 bg-gray-700 rounded mb-2"></div>
<div className="h-4 w-28 bg-gray-700 rounded"></div>
</div>
<div className="bg-gray-800 rounded-lg p-3">
<div className="h-4 w-12 bg-gray-700 rounded mb-2"></div>
<div className="h-4 w-28 bg-gray-700 rounded"></div>
</div>
<div className="bg-gray-800 rounded-lg p-3">
<div className="h-4 w-12 bg-gray-700 rounded mb-2"></div>
<div className="h-4 w-28 bg-gray-700 rounded"></div>
</div>
</div>
<div className="space-y-4">
<div className="border border-gray-800 rounded-lg p-4 bg-gray-900">
<div className="h-4 w-24 bg-gray-800 rounded mb-2"></div>
<div className="h-4 w-32 bg-gray-800 rounded"></div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gray-800 rounded-lg p-3">
<div className="h-4 w-12 bg-gray-700 rounded mb-2"></div>
<div className="h-4 w-28 bg-gray-700 rounded"></div>
</div>
<div className="bg-gray-800 rounded-lg p-3">
<div className="h-4 w-12 bg-gray-700 rounded mb-2"></div>
<div className="h-4 w-28 bg-gray-700 rounded"></div>
</div>
<div className="bg-gray-800 rounded-lg p-3">
<div className="h-4 w-12 bg-gray-700 rounded mb-2"></div>
<div className="h-4 w-28 bg-gray-700 rounded"></div>
</div>
</div>
<div className="border border-gray-800 rounded-lg p-4 bg-gray-900">
<div className="h-4 w-24 bg-gray-800 rounded mb-2"></div>
<div className="h-4 w-32 bg-gray-800 rounded"></div>
<div className="space-y-4">
<div className="border border-gray-800 rounded-lg p-4 bg-gray-900">
<div className="h-4 w-24 bg-gray-800 rounded mb-2"></div>
<div className="h-4 w-32 bg-gray-800 rounded"></div>
</div>
<div className="border border-gray-800 rounded-lg p-4 bg-gray-900">
<div className="h-4 w-24 bg-gray-800 rounded mb-2"></div>
<div className="h-4 w-32 bg-gray-800 rounded"></div>
</div>
</div>
</div>
</div>
Expand Down