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
88 changes: 88 additions & 0 deletions apps/web/__tests__/unit/cap-pagination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import React from "react";
import { describe, expect, it } from "vitest";
import { CapPagination } from "@/app/(org)/dashboard/caps/components/CapPagination";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@cap/ui";

describe("CapPagination and Client Navigation (Issue #2305)", () => {
it("verifies CapPagination imports Link from next/link and passes asChild", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test Missing From CI

The new pagination test is absent from the explicit web-test lists executed by CI, so these assertions can regress without a required check failing. Add this file to the relevant CI test invocation or run the complete web unit-test suite.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/__tests__/unit/cap-pagination.test.ts
Line: 16

Comment:
**Test Missing From CI**

The new pagination test is absent from the explicit web-test lists executed by CI, so these assertions can regress without a required check failing. Add this file to the relevant CI test invocation or run the complete web unit-test suite.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

const filePath = join(
process.cwd(),
"app/(org)/dashboard/caps/components/CapPagination.tsx",
);
const content = readFileSync(filePath, "utf-8");

expect(content).toContain('import Link from "next/link";');
expect(content).toContain("asChild");
expect(content).toContain("scroll={false}");
});

it("verifies @cap/ui Pagination.tsx supports asChild via Radix Slot", () => {
const filePath = join(
process.cwd(),
"../../packages/ui/src/components/Pagination.tsx",
);
const content = readFileSync(filePath, "utf-8");

expect(content).toContain('import { Slot } from "@radix-ui/react-slot";');
expect(content).toContain("asChild?: boolean;");
expect(content).toContain('const Comp = asChild ? Slot : "a";');
});

it("renders client Link elements inside PaginationPrevious, PaginationLink, and PaginationNext", () => {
const element = CapPagination({
currentPage: 2,
totalPages: 5,
}) as React.ReactElement<{ children: React.ReactElement<{ children: React.ReactNode[] }> }>;

expect(element).toBeDefined();
expect(element.type).toBe(Pagination);

const content = element.props.children;
expect(content.type).toBe(PaginationContent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Comments Narrate Assertions

The comments here and at lines 59 and 78 merely narrate the immediately following assertions. The repository requires defaulting to no code comments and explicitly prohibits comments that only describe what the code does. Remove these comments or replace them with genuinely non-obvious context; this repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/__tests__/unit/cap-pagination.test.ts
Line: 50

Comment:
**Comments Narrate Assertions**

The comments here and at lines 59 and 78 merely narrate the immediately following assertions. The repository requires defaulting to no code comments and explicitly prohibits comments that only describe what the code does. Remove these comments or replace them with genuinely non-obvious context; this repository requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


const items = React.Children.toArray(content.props.children) as React.ReactElement[];
expect(items.length).toBeGreaterThan(0);

// First item should be PaginationPrevious with asChild
const prevItem = items[0].props.children as React.ReactElement<{ asChild?: boolean; children: React.ReactElement<{ href: string; scroll: boolean }> }>;
expect(prevItem.type).toBe(PaginationPrevious);
expect(prevItem.props.asChild).toBe(true);
expect(prevItem.props.children.props.href).toBe("/dashboard/caps?page=1");
expect(prevItem.props.children.props.scroll).toBe(false);

// Last item should be PaginationNext with asChild
const nextItem = items[items.length - 1].props.children as React.ReactElement<{ asChild?: boolean; children: React.ReactElement<{ href: string; scroll: boolean }> }>;
expect(nextItem.type).toBe(PaginationNext);
expect(nextItem.props.asChild).toBe(true);
expect(nextItem.props.children.props.href).toBe("/dashboard/caps?page=3");
expect(nextItem.props.children.props.scroll).toBe(false);
});
Comment on lines +40 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Links Are Never Rendered

Calling CapPagination directly only inspects unevaluated React element props; it never executes PaginationPrevious, PaginationNext, or Radix Slot. The test can therefore remain green if the final anchors lose their href, labels, icons, or delegated props. Render the component in a DOM environment and assert the resulting links.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/__tests__/unit/cap-pagination.test.ts
Line: 40-68

Comment:
**Links Are Never Rendered**

Calling `CapPagination` directly only inspects unevaluated React element props; it never executes `PaginationPrevious`, `PaginationNext`, or Radix `Slot`. The test can therefore remain green if the final anchors lose their href, labels, icons, or delegated props. Render the component in a DOM environment and assert the resulting links.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


it("respects custom hrefForPage generator with client navigation", () => {
const customHref = (page: number) => `/custom/route?p=${page}`;
const element = CapPagination({
currentPage: 1,
totalPages: 3,
hrefForPage: customHref,
}) as React.ReactElement<{ children: React.ReactElement<{ children: React.ReactNode[] }> }>;

const content = element.props.children;
const items = React.Children.toArray(content.props.children) as React.ReactElement[];

// On page 1, first link is page 1 (no Previous)
const firstLink = items[0].props.children as React.ReactElement<{ asChild?: boolean; children: React.ReactElement<{ href: string; scroll: boolean }> }>;
expect(firstLink.type).toBe(PaginationLink);
expect(firstLink.props.asChild).toBe(true);
expect(firstLink.props.children.props.href).toBe("/custom/route?p=1");
expect(firstLink.props.children.props.scroll).toBe(false);
});
});
38 changes: 26 additions & 12 deletions apps/web/app/(org)/dashboard/caps/components/CapPagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PaginationNext,
PaginationPrevious,
} from "@cap/ui";
import Link from "next/link";

interface CapPaginationProps {
currentPage: number;
Expand All @@ -25,50 +26,63 @@ export const CapPagination: React.FC<CapPaginationProps> = ({
{currentPage > 1 && (
<PaginationItem>
<PaginationPrevious
asChild
className="h-10 bg-transparent hover:bg-gray-4"
href={hrefForPage(currentPage - 1)}
/>
>
<Link href={hrefForPage(currentPage - 1)} scroll={false} />
</PaginationPrevious>
</PaginationItem>
)}
<PaginationItem>
<PaginationLink
asChild
className="h-10 min-w-10"
href={hrefForPage(1)}
isActive={currentPage === 1}
>
1
<Link href={hrefForPage(1)} scroll={false}>
1
</Link>
</PaginationLink>
</PaginationItem>
{currentPage !== 1 && (
<PaginationItem>
<PaginationLink
asChild
className="h-10 min-w-10"
href={hrefForPage(currentPage)}
isActive={true}
>
{currentPage}
<Link href={hrefForPage(currentPage)} scroll={false}>
{currentPage}
</Link>
</PaginationLink>
</PaginationItem>
)}
{totalPages > currentPage + 1 && (
<PaginationItem>
<PaginationLink
asChild
className="h-10 min-w-10 hover:bg-gray-3"
href={hrefForPage(currentPage + 1)}
isActive={false}
>
{currentPage + 1}
<Link href={hrefForPage(currentPage + 1)} scroll={false}>
{currentPage + 1}
</Link>
</PaginationLink>
</PaginationItem>
)}
{currentPage > 2 && <PaginationEllipsis />}
<PaginationItem>
<PaginationNext
asChild
className="h-10 bg-transparent hover:bg-gray-4"
href={hrefForPage(
currentPage === totalPages ? currentPage : currentPage + 1,
)}
/>
>
<Link
href={hrefForPage(
currentPage === totalPages ? currentPage : currentPage + 1,
)}
scroll={false}
/>
</PaginationNext>
</PaginationItem>
</PaginationContent>
</Pagination>
Expand Down
120 changes: 85 additions & 35 deletions packages/ui/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { classNames } from "@cap/utils";
import { Slot } from "@radix-ui/react-slot";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { type ButtonProps, buttonVariants } from "./Button";
Expand Down Expand Up @@ -34,59 +35,108 @@ PaginationItem.displayName = "PaginationItem";

type PaginationLinkProps = {
isActive?: boolean;
asChild?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;

const PaginationLink = ({
className,
isActive,
size = "md",
asChild = false,
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={classNames(
buttonVariants({
variant: isActive ? "dark" : "white",
size,
}),
className,
)}
{...props}
/>
);
}: PaginationLinkProps) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
aria-current={isActive ? "page" : undefined}
className={classNames(
buttonVariants({
variant: isActive ? "dark" : "white",
size,
}),
className,
)}
{...props}
/>
);
};
PaginationLink.displayName = "PaginationLink";

const PaginationPrevious = ({
className,
children,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="md"
className={classNames("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="size-4" />
<p className="text-sm text-gray-12">Previous</p>
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
let content = children;
if (props.asChild && React.isValidElement(children)) {
const child = children as React.ReactElement<{ children?: React.ReactNode }>;
if (!child.props.children) {
content = React.cloneElement(child, {}, (
<>
<ChevronLeft className="size-4" />
<p className="text-sm text-gray-12">Previous</p>
</>
));
}
} else if (!children) {
content = (
<>
<ChevronLeft className="size-4" />
<p className="text-sm text-gray-12">Previous</p>
</>
);
}

return (
<PaginationLink
aria-label="Go to previous page"
size="md"
className={classNames("gap-1 pl-2.5", className)}
{...props}
>
{content}
</PaginationLink>
);
};
PaginationPrevious.displayName = "PaginationPrevious";

const PaginationNext = ({
className,
children,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="md"
className={classNames("gap-1 pr-2.5", className)}
{...props}
>
<p className="text-sm text-gray-12">Next</p>
<ChevronRight className="size-4" />
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
let content = children;
if (props.asChild && React.isValidElement(children)) {
const child = children as React.ReactElement<{ children?: React.ReactNode }>;
if (!child.props.children) {
content = React.cloneElement(child, {}, (
<>
<p className="text-sm text-gray-12">Next</p>
<ChevronRight className="size-4" />
</>
));
}
} else if (!children) {
content = (
<>
<p className="text-sm text-gray-12">Next</p>
<ChevronRight className="size-4" />
</>
);
}

return (
<PaginationLink
aria-label="Go to next page"
size="md"
className={classNames("gap-1 pr-2.5", className)}
{...props}
>
{content}
</PaginationLink>
);
};
PaginationNext.displayName = "PaginationNext";

const PaginationEllipsis = ({
Expand Down