Skip to content
Merged
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
100 changes: 100 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"@tanstack/react-query": "5.80.7",
"@types/canvas-confetti": "1.6.4",
"canvas-confetti": "1.9.3",
"immer": "^11.1.8",
"html2canvas": "^1.4.1",
"immer": "^11.1.8",
"next": "14.2.3",
"qrcode.react": "4.2.0",
"react": "^18.3.0",
Expand All @@ -33,6 +33,7 @@
"devDependencies": {
"@next/bundle-analyzer": "^14.2.3",
"@playwright/test": "^1.40.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.5.0",
Expand Down
101 changes: 101 additions & 0 deletions src/__tests__/SubscriptionCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
import SubscriptionCard from "@/components/SubscriptionCard";
import type { Subscription } from "@/types/subscription";

vi.mock("@stellar-split/sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@stellar-split/sdk")>();
return {
...actual,
formatAmount: (n: bigint) => `${n / 1000000n}`,
truncateAddress: (s: string) => `${s.slice(0, 4)}...${s.slice(-4)}`,
};
});

vi.mock("next/link", () => ({
default: ({ children, href, ...props }: React.ComponentProps<"a"> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));

const mockSubscription: Subscription = {
id: "sub-1",
templateName: "Monthly Hosting",
creator: "GCREADER1234567890ABCDEF",
recipients: [
{ address: "GDEST1234567890ABCDEF", amount: 50000000n },
],
frequency: "monthly",
intervalDays: 30,
status: "active",
createdAt: Math.floor(Date.now() / 1000) - 86400 * 30,
nextRunDate: Math.floor(Date.now() / 1000) + 86400 * 5,
lastRunDate: Math.floor(Date.now() / 1000) - 86400 * 25,
token: "USDC",
totalInvoicesGenerated: 3,
totalUsdcCollected: 150000000n,
invoiceHistory: [
{
invoiceId: "101",
generatedAt: Math.floor(Date.now() / 1000) - 86400 * 30,
deadline: Math.floor(Date.now() / 1000) - 86400 * 25,
amount: 50000000n,
status: "Released",
},
],
};

describe("SubscriptionCard", () => {
it("renders template name", () => {
render(<SubscriptionCard subscription={mockSubscription} />);
expect(screen.getByText("Monthly Hosting")).toBeInTheDocument();
});

it("renders status", () => {
render(<SubscriptionCard subscription={mockSubscription} />);
expect(screen.getByText("Active")).toBeInTheDocument();
});

it("renders frequency", () => {
render(<SubscriptionCard subscription={mockSubscription} />);
expect(screen.getByText("Monthly")).toBeInTheDocument();
});

it("renders total invoices", () => {
render(<SubscriptionCard subscription={mockSubscription} />);
expect(screen.getByText("3")).toBeInTheDocument();
});

it("renders USDC collected", () => {
render(<SubscriptionCard subscription={mockSubscription} />);
expect(screen.getByText("150")).toBeInTheDocument();
});

it("renders paused status", () => {
const paused = { ...mockSubscription, status: "paused" as const };
render(<SubscriptionCard subscription={paused} />);
expect(screen.getByText("Paused")).toBeInTheDocument();
});

it("renders cancelled status", () => {
const cancelled = { ...mockSubscription, status: "cancelled" as const };
render(<SubscriptionCard subscription={cancelled} />);
expect(screen.getByText("Cancelled")).toBeInTheDocument();
});

it("links to detail page", () => {
render(<SubscriptionCard subscription={mockSubscription} />);
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", "/subscriptions/sub-1");
});

it("shows dash for next run when paused", () => {
const paused = { ...mockSubscription, status: "paused" as const };
render(<SubscriptionCard subscription={paused} />);
// The next run date should show a dash for paused subscriptions
const nextRunSection = screen.getByText("Next Run");
expect(nextRunSection.parentElement?.textContent).toContain("—");
});
});
135 changes: 135 additions & 0 deletions src/__tests__/SubscriptionDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
import SubscriptionDetailClient from "@/components/SubscriptionDetailClient";
import type { Subscription } from "@/types/subscription";

vi.mock("@stellar-split/sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@stellar-split/sdk")>();
return {
...actual,
formatAmount: (n: bigint) => `${n / 1000000n}`,
truncateAddress: (s: string) => `${s.slice(0, 4)}...${s.slice(-4)}`,
};
});

vi.mock("next/link", () => ({
default: ({ children, href, ...props }: React.ComponentProps<"a"> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));

vi.mock("next/navigation", () => ({
useParams: () => ({ id: "sub-1" }),
useRouter: () => ({ replace: vi.fn() }),
}));

const mockSubscription: Subscription = {
id: "sub-1",
templateName: "Weekly Design Retainer",
creator: "GCREADER1234567890ABCDEF",
recipients: [
{ address: "GDEST1234567890ABCDEF", amount: 100000000n },
],
frequency: "weekly",
intervalDays: 7,
status: "active",
createdAt: Math.floor(Date.now() / 1000) - 86400 * 14,
nextRunDate: Math.floor(Date.now() / 1000) + 86400 * 3,
lastRunDate: Math.floor(Date.now() / 1000) - 86400 * 4,
token: "USDC",
totalInvoicesGenerated: 2,
totalUsdcCollected: 200000000n,
invoiceHistory: [
{
invoiceId: "201",
generatedAt: Math.floor(Date.now() / 1000) - 86400 * 14,
deadline: Math.floor(Date.now() / 1000) - 86400 * 10,
amount: 100000000n,
status: "Released",
},
{
invoiceId: "202",
generatedAt: Math.floor(Date.now() / 1000) - 86400 * 7,
deadline: Math.floor(Date.now() / 1000) - 86400 * 3,
amount: 100000000n,
status: "Pending",
},
],
};

describe("SubscriptionDetailClient", () => {
beforeEach(() => {
localStorage.clear();
localStorage.setItem(
"stellar_split_subscriptions",
JSON.stringify([
{
...mockSubscription,
recipients: mockSubscription.recipients.map((r) => ({
...r,
amount: r.amount.toString(),
})),
totalUsdcCollected: mockSubscription.totalUsdcCollected.toString(),
invoiceHistory: mockSubscription.invoiceHistory.map((inv) => ({
...inv,
amount: inv.amount.toString(),
})),
},
])
);
});

it("renders subscription template name", async () => {
render(<SubscriptionDetailClient />);
const els = await screen.findAllByText("Weekly Design Retainer");
expect(els.length).toBeGreaterThanOrEqual(1);
});

it("renders subscription status", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Active")).toBeInTheDocument();
});

it("renders frequency", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Weekly")).toBeInTheDocument();
});

it("renders pause button for active subscription", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Pause Subscription")).toBeInTheDocument();
});

it("renders cancel button", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Cancel Subscription")).toBeInTheDocument();
});

it("renders invoice history table", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText(/Invoice History/)).toBeInTheDocument();
});

it("renders recipients section", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Recipients")).toBeInTheDocument();
});

it("renders calendar preview for active subscription", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Upcoming Invoice Dates")).toBeInTheDocument();
});

it("renders total invoices count", async () => {
render(<SubscriptionDetailClient />);
expect(await screen.findByText("Total Invoices")).toBeInTheDocument();
});

it("renders back link", async () => {
render(<SubscriptionDetailClient />);
const backLink = await screen.findByText("← Subscriptions");
expect(backLink).toHaveAttribute("href", "/subscriptions");
});
});
Loading
Loading