From df37ffe79a1000f446d1efd77188f22eb0eb6b5f Mon Sep 17 00:00:00 2001 From: Godfr3y Date: Thu, 30 Jul 2026 03:08:37 +0100 Subject: [PATCH 1/2] test: add unit tests for bonding curve chart data point rendering Adds BondingCurveChart.tsx, a Recharts line chart plotting priceXLM against supply, with an x-axis domain capped at currentSupply, a "No data" empty state, and the active price point marked with current-price-highlight classes via custom/reference dots. Adds BondingCurveChart.test.tsx covering the empty state, exact data point count for a given supply, x-axis maximum matching current supply, the highlight class on the current price point, and mocked Recharts calls verifying the { supply, priceXLM } data shape. Closes #721 --- .husky/pre-commit | 3 + src/components/common/BondingCurveChart.tsx | 148 ++++++++++++++ .../__tests__/BondingCurveChart.test.tsx | 190 ++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 src/components/common/BondingCurveChart.tsx create mode 100644 src/components/common/__tests__/BondingCurveChart.test.tsx diff --git a/.husky/pre-commit b/.husky/pre-commit index e65f5e7b..60e30912 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,6 @@ #!/usr/bin/env sh +export PATH="$PATH:/c/Program Files/Git/bin:/c/Program Files/Git/usr/bin:C:/Program Files/Git/bin:C:/Program Files/Git/usr/bin" + sh ./scripts/check-no-package-lock.sh npx lint-staged + diff --git a/src/components/common/BondingCurveChart.tsx b/src/components/common/BondingCurveChart.tsx new file mode 100644 index 00000000..2b88d15d --- /dev/null +++ b/src/components/common/BondingCurveChart.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { + ResponsiveContainer, + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + ReferenceDot, +} from 'recharts'; +import { cn } from '@/lib/utils'; + +export interface BondingCurveDataPoint { + supply: number; + priceXLM: number; + isCurrent?: boolean; +} + +export interface BondingCurveChartProps { + data?: BondingCurveDataPoint[]; + currentSupply?: number; + className?: string; + width?: number | string; + height?: number | string; +} + +export function BondingCurveChart({ + data = [], + currentSupply, + className, + width = '100%', + height = 300, +}: BondingCurveChartProps) { + if (!data || data.length === 0) { + return ( +
+ No data +
+ ); + } + + const maxSupplyInData = Math.max(...data.map((d) => d.supply)); + const xAxisMax = currentSupply ?? maxSupplyInData; + + const currentPoint = data.find( + (d) => d.isCurrent || (currentSupply !== undefined && d.supply === currentSupply) + ); + +interface CustomDotProps { + cx?: number; + cy?: number; + payload?: BondingCurveDataPoint; +} + + const CustomDot = (props: CustomDotProps) => { + const { cx, cy, payload } = props; + if (!cx || !cy || !payload) return null; + + const isHighlighted = + payload.isCurrent || (currentSupply !== undefined && payload.supply === currentSupply); + + return ( + + ); + }; + + return ( +
+ + + + + + [`${value} XLM`, 'Price']} + labelFormatter={(label: number) => `Key Supply: ${label}`} + /> + } + activeDot={{ r: 8, className: 'highlight-active-dot' }} + /> + {currentPoint && ( + + )} + + +
+ ); +} diff --git a/src/components/common/__tests__/BondingCurveChart.test.tsx b/src/components/common/__tests__/BondingCurveChart.test.tsx new file mode 100644 index 00000000..4d5ca2c8 --- /dev/null +++ b/src/components/common/__tests__/BondingCurveChart.test.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { BondingCurveChart, type BondingCurveDataPoint } from '../BondingCurveChart'; +import * as recharts from 'recharts'; + +// Mock Recharts library to inspect parameters and test data shapes +vi.mock('recharts', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + ResponsiveContainer: vi.fn( + ({ + children, + width, + height, + }: { + children?: React.ReactNode; + width?: string | number; + height?: string | number; + }) => ( +
+ {children} +
+ ) + ), + LineChart: vi.fn( + ({ + children, + data, + 'data-testid': testId, + }: { + children?: React.ReactNode; + data?: unknown; + 'data-testid'?: string; + }) => ( +
+ {children} +
+ ) + ), + XAxis: vi.fn( + ({ + dataKey, + domain, + 'data-testid': testId, + }: { + dataKey?: string; + domain?: unknown; + 'data-testid'?: string; + }) => ( +
+ ) + ), + YAxis: vi.fn( + ({ dataKey, 'data-testid': testId }: { dataKey?: string; 'data-testid'?: string }) => ( +
+ ) + ), + Tooltip: vi.fn(() =>
), + CartesianGrid: vi.fn(() =>
), + Line: vi.fn(({ dataKey }: { dataKey?: string }) => ( +
+ )), + ReferenceDot: vi.fn( + ({ + x, + y, + className, + 'data-testid': testId, + }: { + x?: number; + y?: number; + className?: string; + 'data-testid'?: string; + }) => ( +
+ ) + ), + }; +}); + +describe('BondingCurveChart', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders a message "No data" when given an empty data array', () => { + render(); + + expect(screen.getByText('No data')).toBeInTheDocument(); + expect(screen.getByTestId('no-data-message')).toBeInTheDocument(); + }); + + it('renders a message "No data" when data prop is undefined', () => { + render(); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); + + it('renders the chart container with exactly N data points when given N data points for supply N', () => { + const sampleData: BondingCurveDataPoint[] = [ + { supply: 1, priceXLM: 1.01 }, + { supply: 2, priceXLM: 1.02 }, + { supply: 3, priceXLM: 1.03 }, + { supply: 4, priceXLM: 1.04 }, + { supply: 5, priceXLM: 1.05 }, + ]; + + render(); + + const chartElement = screen.getByTestId('bonding-curve-chart'); + expect(chartElement).toBeInTheDocument(); + expect(chartElement).toHaveAttribute('data-datapoints-count', '5'); + + // Assert LineChart mock received all 5 data points + const lineChartMock = vi.mocked(recharts.LineChart); + expect(lineChartMock).toHaveBeenCalled(); + const lineChartProps = lineChartMock.mock.calls[0][0]; + expect(lineChartProps.data).toHaveLength(5); + expect(lineChartProps.data).toEqual(sampleData); + }); + + it('sets the x-axis maximum to equal the current supply value', () => { + const sampleData: BondingCurveDataPoint[] = [ + { supply: 1, priceXLM: 1.1 }, + { supply: 2, priceXLM: 1.2 }, + { supply: 3, priceXLM: 1.3 }, + ]; + const currentSupply = 25; + + render(); + + const chartElement = screen.getByTestId('bonding-curve-chart'); + expect(chartElement).toHaveAttribute('data-xaxis-max', '25'); + + const xAxisMock = vi.mocked(recharts.XAxis); + expect(xAxisMock).toHaveBeenCalled(); + const xAxisProps = xAxisMock.mock.calls[0][0]; + expect(xAxisProps.domain).toEqual([0, 25]); + }); + + it('marks the current price point with a distinct highlight class', () => { + const sampleData: BondingCurveDataPoint[] = [ + { supply: 1, priceXLM: 1.0 }, + { supply: 2, priceXLM: 1.1, isCurrent: true }, + { supply: 3, priceXLM: 1.2 }, + ]; + + render(); + + // Check reference dot rendered for current point has highlight class + const referenceDot = screen.getByTestId('reference-current-dot'); + expect(referenceDot).toBeInTheDocument(); + expect(referenceDot).toHaveClass('current-price-highlight'); + expect(referenceDot).toHaveClass('highlight'); + expect(referenceDot).toHaveAttribute('data-x', '2'); + expect(referenceDot).toHaveAttribute('data-y', '1.1'); + }); + + it('mocks the chart library and asserts it is called with the correct data shape', () => { + const sampleData: BondingCurveDataPoint[] = [ + { supply: 10, priceXLM: 1.5 }, + { supply: 20, priceXLM: 2.0, isCurrent: true }, + ]; + + render(); + + // Verify ResponsiveContainer was invoked + expect(recharts.ResponsiveContainer).toHaveBeenCalled(); + + // Verify LineChart was invoked with correct data shape { supply: number, priceXLM: number } + const lineChartMock = vi.mocked(recharts.LineChart); + expect(lineChartMock).toHaveBeenCalled(); + const firstCallProps = lineChartMock.mock.calls[0][0]; + expect(firstCallProps.data).toEqual([ + { supply: 10, priceXLM: 1.5 }, + { supply: 20, priceXLM: 2.0, isCurrent: true }, + ]); + }); +}); From 5fdfca403c7d4399d798d15f61639bf049bf502c Mon Sep 17 00:00:00 2001 From: Godfr3y Date: Thu, 30 Jul 2026 03:17:50 +0100 Subject: [PATCH 2/2] fixed issues --- src/components/common/BondingCurveChart.tsx | 1 - src/components/common/__tests__/BondingCurveChart.test.tsx | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/common/BondingCurveChart.tsx b/src/components/common/BondingCurveChart.tsx index 2b88d15d..4bd1e7b0 100644 --- a/src/components/common/BondingCurveChart.tsx +++ b/src/components/common/BondingCurveChart.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { ResponsiveContainer, LineChart, diff --git a/src/components/common/__tests__/BondingCurveChart.test.tsx b/src/components/common/__tests__/BondingCurveChart.test.tsx index 4d5ca2c8..7e052bbe 100644 --- a/src/components/common/__tests__/BondingCurveChart.test.tsx +++ b/src/components/common/__tests__/BondingCurveChart.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import type { ReactNode } from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import { BondingCurveChart, type BondingCurveDataPoint } from '../BondingCurveChart'; @@ -15,7 +15,7 @@ vi.mock('recharts', async (importOriginal) => { width, height, }: { - children?: React.ReactNode; + children?: ReactNode; width?: string | number; height?: string | number; }) => ( @@ -30,7 +30,7 @@ vi.mock('recharts', async (importOriginal) => { data, 'data-testid': testId, }: { - children?: React.ReactNode; + children?: ReactNode; data?: unknown; 'data-testid'?: string; }) => (