From 23ce644f4baffa1885d3aaad08cd7fcc179249c5 Mon Sep 17 00:00:00 2001 From: Lynndabel Date: Mon, 29 Jun 2026 06:08:34 +0100 Subject: [PATCH] Add RTL tests for the MyCommitmentCard status, change-percent, and action callbacks --- package-lock.json | 80 +++- .../__tests__/MyCommitmentCard.test.tsx | 419 ++++++++++++++++++ 2 files changed, 483 insertions(+), 16 deletions(-) create mode 100644 src/components/__tests__/MyCommitmentCard.test.tsx diff --git a/package-lock.json b/package-lock.json index f9201e9e..3b316269 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "commitlabs-frontend", "version": "0.1.0", "dependencies": { - "@rolldown/binding-linux-x64-gnu": "^1.1.3", "@stellar/freighter-api": "^1.6.0", "@stellar/stellar-sdk": "^11.2.2", "@tailwindcss/postcss": "^4.1.18", @@ -27,6 +26,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -1500,6 +1500,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1543,21 +1559,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", - "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -7837,6 +7838,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/src/components/__tests__/MyCommitmentCard.test.tsx b/src/components/__tests__/MyCommitmentCard.test.tsx new file mode 100644 index 00000000..779cb970 --- /dev/null +++ b/src/components/__tests__/MyCommitmentCard.test.tsx @@ -0,0 +1,419 @@ +// @vitest-environment happy-dom + +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import MyCommitmentCard from "@/components/MyCommitmentCard"; +import { type Commitment, CommitmentType, CommitmentStatus } from "@/types/commitment"; + +// --------------------------------------------------------------------------- +// Module mocks +// --------------------------------------------------------------------------- + +vi.mock("next/link", () => ({ + default: ({ + href, + children, + className, + }: React.AnchorHTMLAttributes & { href: string }) => + React.createElement("a", { href, className }, children), +})); + +// Mock RelistPriceEditor since it's referenced but not imported in the component +vi.mock("@/components/marketplace/RelistPriceEditor", () => ({ + default: () => React.createElement("div", { "data-testid": "relist-price-editor" }), +})); + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const createCommitment = ( + overrides: Partial = {} +): Commitment => ({ + id: "CMT-001", + type: "Safe", + status: "Active", + asset: "XLM", + amount: "1000", + currentValue: "1050", + changePercent: 5.0, + durationProgress: 50, + daysRemaining: 15, + complianceScore: 85, + maxLoss: "2%", + currentDrawdown: "1%", + createdDate: "2024-01-01", + expiryDate: "2024-02-01", + ...overrides, +}); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function renderCard( + commitment: Commitment, + callbacks: { + onDetails?: (id: string) => void; + onAttestations?: (id: string) => void; + onEarlyExit?: (id: string) => void; + onListForSale?: (id: string) => void; + } = {} +) { + return render(); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("MyCommitmentCard", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ── Type-icon mapping ────────────────────────────────────────────────────── + + describe("type-icon mapping", () => { + it("renders Safe icon for Safe type", () => { + const commitment = createCommitment({ type: "Safe" }); + const { container } = renderCard(commitment); + + expect(screen.getByText("Safe")).toBeInTheDocument(); + // Verify the type badge has Safe-specific styling + const typeBadge = screen.getByText("Safe").closest("div"); + expect(typeBadge?.className).toContain("border-[rgba(16,185,129,0.5)]"); + expect(typeBadge?.className).toContain("text-[#05DF72]"); + }); + + it("renders Balanced icon for Balanced type", () => { + const commitment = createCommitment({ type: "Balanced" }); + const { container } = renderCard(commitment); + + expect(screen.getByText("Balanced")).toBeInTheDocument(); + // Verify the type badge has Balanced-specific styling + const typeBadge = screen.getByText("Balanced").closest("div"); + expect(typeBadge?.className).toContain("border-[rgba(59,130,246,0.5)]"); + expect(typeBadge?.className).toContain("text-[#51a2ff]"); + }); + + it("renders Aggressive icon for Aggressive type", () => { + const commitment = createCommitment({ type: "Aggressive" }); + const { container } = renderCard(commitment); + + expect(screen.getByText("Aggressive")).toBeInTheDocument(); + // Verify the type badge has Aggressive-specific styling + const typeBadge = screen.getByText("Aggressive").closest("div"); + expect(typeBadge?.className).toContain("border-[rgba(245,158,11,0.5)]"); + expect(typeBadge?.className).toContain("text-[#ff8904]"); + }); + }); + + // ── Gain/loss coloring ───────────────────────────────────────────────────── + + describe("gain/loss coloring", () => { + it("applies increase styling for positive changePercent", () => { + const commitment = createCommitment({ changePercent: 10.5 }); + renderCard(commitment); + + // Should show +10.50% with green color + expect(screen.getByText("+10.50%")).toBeInTheDocument(); + const changeElement = screen.getByText("+10.50%"); + expect(changeElement.className).toContain("text-[#05DF72]"); + }); + + it("applies decrease styling for negative changePercent", () => { + const commitment = createCommitment({ changePercent: -5.25 }); + renderCard(commitment); + + // Should show -5.25% with red color + expect(screen.getByText("-5.25%")).toBeInTheDocument(); + const changeElement = screen.getByText("-5.25%"); + expect(changeElement.className).toContain("text-[#EF4444]"); + }); + + it("applies increase styling for zero changePercent", () => { + const commitment = createCommitment({ changePercent: 0 }); + renderCard(commitment); + + // Zero is treated as positive (>= 0) + expect(screen.getByText("+0.00%")).toBeInTheDocument(); + const changeElement = screen.getByText("+0.00%"); + expect(changeElement.className).toContain("text-[#05DF72]"); + }); + + it("formats changePercent to 2 decimal places", () => { + const commitment = createCommitment({ changePercent: 12.3456 }); + renderCard(commitment); + + expect(screen.getByText("+12.35%")).toBeInTheDocument(); + }); + }); + + // ── Action button callbacks ───────────────────────────────────────────────── + + describe("action button callbacks", () => { + it("calls onDetails with commitment id when Details button is clicked", () => { + const onDetails = vi.fn(); + const commitment = createCommitment({ id: "CMT-123" }); + renderCard(commitment, { onDetails }); + + const detailsButton = screen.getByText("Details").closest("button"); + fireEvent.click(detailsButton!); + + expect(onDetails).toHaveBeenCalledTimes(1); + expect(onDetails).toHaveBeenCalledWith("CMT-123"); + }); + + it("calls onAttestations with commitment id when Attestations button is clicked", () => { + const onAttestations = vi.fn(); + const commitment = createCommitment({ id: "CMT-456" }); + renderCard(commitment, { onAttestations }); + + const attestationsButton = screen.getByText("Attestations").closest("button"); + fireEvent.click(attestationsButton!); + + expect(onAttestations).toHaveBeenCalledTimes(1); + expect(onAttestations).toHaveBeenCalledWith("CMT-456"); + }); + + it("calls onEarlyExit with commitment id when Early Exit button is clicked", () => { + const onEarlyExit = vi.fn(); + const commitment = createCommitment({ id: "CMT-789", status: "Active" }); + renderCard(commitment, { onEarlyExit }); + + const earlyExitButton = screen.getByText("Early Exit (Penalty Applies)").closest("button"); + fireEvent.click(earlyExitButton!); + + expect(onEarlyExit).toHaveBeenCalledTimes(1); + expect(onEarlyExit).toHaveBeenCalledWith("CMT-789"); + }); + + it("calls onListForSale with commitment id when List for sale button is clicked", () => { + const onListForSale = vi.fn(); + const commitment = createCommitment({ id: "CMT-999", status: "Active" }); + renderCard(commitment, { onListForSale }); + + const listForSaleButton = screen.getByText("List for sale").closest("button"); + fireEvent.click(listForSaleButton!); + + expect(onListForSale).toHaveBeenCalledTimes(1); + expect(onListForSale).toHaveBeenCalledWith("CMT-999"); + }); + }); + + // ── Optional callbacks ───────────────────────────────────────────────────── + + describe("optional callbacks", () => { + it("does not throw when onDetails is not provided", () => { + const commitment = createCommitment({ id: "CMT-001" }); + const { container } = renderCard(commitment, {}); + + const detailsButton = screen.getByText("Details").closest("button"); + expect(() => fireEvent.click(detailsButton!)).not.toThrow(); + }); + + it("does not throw when onAttestations is not provided", () => { + const commitment = createCommitment({ id: "CMT-002" }); + const { container } = renderCard(commitment, {}); + + const attestationsButton = screen.getByText("Attestations").closest("button"); + expect(() => fireEvent.click(attestationsButton!)).not.toThrow(); + }); + + it("does not throw when onEarlyExit is not provided", () => { + const commitment = createCommitment({ id: "CMT-003", status: "Active" }); + const { container } = renderCard(commitment, {}); + + const earlyExitButton = screen.getByText("Early Exit (Penalty Applies)").closest("button"); + expect(() => fireEvent.click(earlyExitButton!)).not.toThrow(); + }); + + it("does not throw when onListForSale is not provided", () => { + const commitment = createCommitment({ id: "CMT-004", status: "Active" }); + const { container } = renderCard(commitment, {}); + + const listForSaleButton = screen.getByText("List for sale").closest("button"); + expect(() => fireEvent.click(listForSaleButton!)).not.toThrow(); + }); + }); + + // ── Status-based button visibility ────────────────────────────────────────── + + describe("status-based button visibility", () => { + it("shows List for sale and Early Exit buttons when status is Active", () => { + const commitment = createCommitment({ status: "Active" }); + renderCard(commitment); + + expect(screen.getByText("List for sale")).toBeInTheDocument(); + expect(screen.getByText("Early Exit (Penalty Applies)")).toBeInTheDocument(); + }); + + it("does not show List for sale and Early Exit buttons when status is Settled", () => { + const commitment = createCommitment({ status: "Settled" }); + renderCard(commitment); + + expect(screen.queryByText("List for sale")).not.toBeInTheDocument(); + expect(screen.queryByText("Early Exit (Penalty Applies)")).not.toBeInTheDocument(); + }); + + it("does not show List for sale and Early Exit buttons when status is Violated", () => { + const commitment = createCommitment({ status: "Violated" }); + renderCard(commitment); + + expect(screen.queryByText("List for sale")).not.toBeInTheDocument(); + expect(screen.queryByText("Early Exit (Penalty Applies)")).not.toBeInTheDocument(); + }); + + it("does not show List for sale and Early Exit buttons when status is Early Exit", () => { + const commitment = createCommitment({ status: "Early Exit" }); + renderCard(commitment); + + expect(screen.queryByText("List for sale")).not.toBeInTheDocument(); + expect(screen.queryByText("Early Exit (Penalty Applies)")).not.toBeInTheDocument(); + }); + }); + + // ── Status badge styling ─────────────────────────────────────────────────── + + describe("status badge styling", () => { + it("applies Active status styling", () => { + const commitment = createCommitment({ status: "Active" }); + renderCard(commitment); + + const statusBadge = screen.getByText("Active"); + expect(statusBadge.className).toContain("bg-[rgba(16,185,129,0.1)]"); + expect(statusBadge.className).toContain("text-[#05DF72]"); + }); + + it("applies Settled status styling", () => { + const commitment = createCommitment({ status: "Settled" }); + renderCard(commitment); + + const statusBadge = screen.getByText("Settled"); + expect(statusBadge.className).toContain("bg-[rgba(59,130,246,0.1)]"); + expect(statusBadge.className).toContain("text-[#51a2ff]"); + }); + + it("applies Early Exit status styling", () => { + const commitment = createCommitment({ status: "Early Exit" }); + renderCard(commitment); + + const statusBadge = screen.getByText("Early Exit"); + expect(statusBadge.className).toContain("bg-[rgba(245,158,11,0.1)]"); + expect(statusBadge.className).toContain("text-[#ff8904]"); + }); + + it("applies Violated status styling", () => { + const commitment = createCommitment({ status: "Violated" }); + renderCard(commitment); + + const statusBadge = screen.getByText("Violated"); + expect(statusBadge.className).toContain("bg-[rgba(239,68,68,0.1)]"); + expect(statusBadge.className).toContain("text-[#ef4444]"); + }); + }); + + // ── Data fields display ──────────────────────────────────────────────────── + + describe("data fields display", () => { + it("displays commitment id as a link", () => { + const commitment = createCommitment({ id: "CMT-XYZ" }); + renderCard(commitment); + + const idLink = screen.getByText("CMT-XYZ").closest("a"); + expect(idLink).toHaveAttribute("href", "/commitments/CMT-XYZ"); + }); + + it("displays amount and asset", () => { + const commitment = createCommitment({ amount: "5000", asset: "USDC" }); + renderCard(commitment); + + expect(screen.getByText("5000")).toBeInTheDocument(); + expect(screen.getByText("USDC")).toBeInTheDocument(); + }); + + it("displays current value", () => { + const commitment = createCommitment({ currentValue: "5250" }); + renderCard(commitment); + + expect(screen.getByText("5250 XLM")).toBeInTheDocument(); + }); + + it("displays duration progress and days remaining", () => { + const commitment = createCommitment({ durationProgress: 75, daysRemaining: 10 }); + renderCard(commitment); + + expect(screen.getByText("10 days left")).toBeInTheDocument(); + expect(screen.getByText("75%")).toBeInTheDocument(); + }); + + it("displays compliance score", () => { + const commitment = createCommitment({ complianceScore: 92 }); + renderCard(commitment); + + expect(screen.getByText("92%")).toBeInTheDocument(); + }); + + it("displays max loss and current drawdown", () => { + const commitment = createCommitment({ maxLoss: "5%", currentDrawdown: "2.5%" }); + renderCard(commitment); + + expect(screen.getByText("5%")).toBeInTheDocument(); + expect(screen.getByText("2.5%")).toBeInTheDocument(); + }); + + it("displays created and expiry dates", () => { + const commitment = createCommitment({ + createdDate: "2024-01-15", + expiryDate: "2024-02-15", + }); + renderCard(commitment); + + expect(screen.getByText("2024-01-15")).toBeInTheDocument(); + expect(screen.getByText("2024-02-15")).toBeInTheDocument(); + }); + }); + + // ── Edge cases ───────────────────────────────────────────────────────────── + + describe("edge cases", () => { + it("renders without crashing when all optional callbacks are omitted", () => { + const commitment = createCommitment(); + const { container } = renderCard(commitment, {}); + + expect(container.firstChild).toBeInTheDocument(); + }); + + it("handles very small changePercent values", () => { + const commitment = createCommitment({ changePercent: 0.001 }); + renderCard(commitment); + + expect(screen.getByText("+0.00%")).toBeInTheDocument(); + }); + + it("handles very large changePercent values", () => { + const commitment = createCommitment({ changePercent: 999.999 }); + renderCard(commitment); + + expect(screen.getByText("+1000.00%")).toBeInTheDocument(); + }); + + it("handles boundary compliance score of 80", () => { + const commitment = createCommitment({ complianceScore: 80 }); + const { container } = renderCard(commitment); + + // At exactly 80, should use the default gradient (not green) + expect(screen.getByText("80%")).toBeInTheDocument(); + }); + + it("handles compliance score above 80 threshold", () => { + const commitment = createCommitment({ complianceScore: 81 }); + const { container } = renderCard(commitment); + + // Above 80, should use green color + expect(screen.getByText("81%")).toBeInTheDocument(); + }); + }); +});