diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 3b93a26..386914c 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -46,6 +46,7 @@ "tailwindcss": "^4.0.0", "typescript": "^5.7.2", "vite": "^6.0.0", - "vitest": "^4.0.16" + "vitest": "^4.0.16", + "recharts": "2.15.4" } } diff --git a/apps/storybook/tests/Chart.test.tsx b/apps/storybook/tests/Chart.test.tsx new file mode 100644 index 0000000..4f0dd39 --- /dev/null +++ b/apps/storybook/tests/Chart.test.tsx @@ -0,0 +1,92 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ChartContainer } from '@trycompai/design-system'; +import { Area, AreaChart } from 'recharts'; + +// jsdom has no ResizeObserver, which recharts' ResponsiveContainer needs. +vi.stubGlobal( + 'ResizeObserver', + class { + observe() {} + unobserve() {} + disconnect() {} + }, +); + +describe('ChartContainer', () => { + it('renders a style block for a safe config', () => { + const { container } = render( + + + + + , + ); + const style = container.querySelector('style'); + expect(style?.innerHTML).toContain('--color-revenue: #22c55e;'); + }); + + it('drops config keys that could break out of the style element', () => { + const { container } = render( + ': { + label: 'Evil', + color: 'red', + }, + revenue: { label: 'Revenue', color: '#22c55e' }, + }} + > + + + + , + ); + const style = container.querySelector('style'); + expect(style?.innerHTML).not.toContain(''); + expect(style?.innerHTML).not.toContain('onerror'); + expect(style?.innerHTML).toContain('--color-revenue: #22c55e;'); + expect(container.querySelector('img')).toBeNull(); + }); + + it('drops color values that could break out of the style element', () => { + const { container } = render( + ', + }, + }} + > + + + + , + ); + const style = container.querySelector('style'); + expect(style?.innerHTML).not.toContain(''); + expect(style?.innerHTML).not.toContain('--color-revenue'); + expect(container.querySelector('script')).toBeNull(); + }); + + it('does not render a style block for an unsafe id', () => { + const { container } = render( + + + + + , + ); + expect(container.querySelector('style')).toBeNull(); + expect(container.querySelector('script')).toBeNull(); + }); +}); diff --git a/packages/design-system/package.json b/packages/design-system/package.json index e01a6ce..55d56d7 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -23,7 +23,6 @@ "default": "./lib/utils.ts" }, "./globals.css": "./src/styles/globals.css", - "./fonts/*": "./src/fonts/*", "./tailwind.config": { "types": "./tailwind.config.ts", "import": "./tailwind.config.ts", diff --git a/packages/design-system/src/components/organisms/chart.tsx b/packages/design-system/src/components/organisms/chart.tsx index cfaa47b..ecde28c 100644 --- a/packages/design-system/src/components/organisms/chart.tsx +++ b/packages/design-system/src/components/organisms/chart.tsx @@ -61,10 +61,20 @@ function ChartContainer({ ); } +const CHART_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; +const CHART_CONFIG_KEY_PATTERN = /^[a-zA-Z0-9_-]+$/; + +// Colors interpolated into the