diff --git a/.npmignore b/.npmignore index 62d1cd3..aadad2d 100644 --- a/.npmignore +++ b/.npmignore @@ -8,4 +8,5 @@ tsconfig.tsbuildinfo *.test.* *.spec.* vitest.config.ts -setupTests.ts \ No newline at end of file +setupTests.ts +coverage \ No newline at end of file diff --git a/README.md b/README.md index b689220..b5a51d2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Building blocks for UI applications at RISC Zero. > > When making code changes, please have the [Biome VSCode extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) installed. -### Tests +### 🧪 Tests Coverage -All test files named with the pattern `*.spec.*` are ran using `bun test` while all files named `*.test.*` are ran through `vitest`. \ No newline at end of file +```md +| Statements | Branches | Functions | Lines | +| --------------------------- | ----------------------- | ------------------------- | ----------------- | +| ![Statements](https://img.shields.io/badge/statements-19.05%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-58.18%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-33.33%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-19.05%25-red.svg?style=flat) | +``` diff --git a/avatar.test.tsx b/avatar.test.tsx new file mode 100644 index 0000000..6b04a6b --- /dev/null +++ b/avatar.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { createRef } from 'react'; +import { Avatar, AvatarFallback } from './avatar'; + +describe('Avatar', () => { + test('renders without crashing', () => { + render(); + const avatarElement = screen.getByTestId('avatar'); + expect(avatarElement).toBeInTheDocument(); + }); + + test('forwards ref correctly', () => { + const ref = createRef(); + render(); + expect(ref.current).not.toBeNull(); + }); +}); + +describe('AvatarFallback', () => { + test('renders without crashing', () => { + render(); + const avatarFallbackElement = screen.getByTestId('avatar-fallback'); + expect(avatarFallbackElement).toBeInTheDocument(); + }); + + test('forwards ref correctly', () => { + const ref = createRef(); + render(); + expect(ref.current).not.toBeNull(); + }); + + test('displays the initials inside', () => { + render(CC); + const avatarFallbackText = screen.getByText('CC'); + expect(avatarFallbackText).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/avatar.tsx b/avatar.tsx index b410aa0..18bccb2 100644 --- a/avatar.tsx +++ b/avatar.tsx @@ -10,6 +10,7 @@ const Avatar = forwardRef< >(({ className, ...rest }, ref) => ( @@ -28,6 +29,7 @@ const AvatarFallback = forwardRef< >(({ className, ...rest }, ref) => ( diff --git a/badge.test.tsx b/badge.test.tsx new file mode 100644 index 0000000..eddcec1 --- /dev/null +++ b/badge.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import { Badge } from './badge'; + +describe('Badge', () => { + test('renders without crashing', () => { + render(); + const badgeElement = screen.getByTestId('badge'); + expect(badgeElement).toBeInTheDocument(); + }); + + test('applies correct classes based on variant prop', () => { + render(); + const badgeElement = screen.getByTestId('badge'); + expect(badgeElement).toHaveClass('border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80'); + }); + + test('forwards additional props to rendered div element', () => { + render(); + const badgeElement = screen.getByTestId('badge'); + expect(badgeElement).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/badge.tsx b/badge.tsx index 572d422..3a896f2 100644 --- a/badge.tsx +++ b/badge.tsx @@ -24,7 +24,7 @@ export interface BadgeProps extends HTMLAttributes, VariantProps } function Badge({ className, variant, ...rest }: BadgeProps) { - return
; + return
; } export { Badge, badgeVariants }; diff --git a/checkbox.test.tsx b/checkbox.test.tsx new file mode 100644 index 0000000..fd0252d --- /dev/null +++ b/checkbox.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; +import { Checkbox } from './checkbox'; +import { createRef } from 'react'; + +describe('Checkbox', () => { + test('renders without crashing', () => { + render(); + const checkboxElement = screen.getByRole('checkbox'); + expect(checkboxElement).toBeInTheDocument(); + }); + + test('forwards ref correctly', () => { + const ref = createRef(); + render(); + expect(ref.current).not.toBeNull(); + }); + + test('applies correct class names', () => { + render(); + const checkboxElement = screen.getByRole('checkbox'); + expect(checkboxElement).toHaveClass('test-class'); + }); + + test('renders CheckIcon when checked', () => { + render(); + const checkIconElement = screen.getByTestId('check-icon'); + expect(checkIconElement).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/checkbox.tsx b/checkbox.tsx index e725a3c..60bf56a 100644 --- a/checkbox.tsx +++ b/checkbox.tsx @@ -18,7 +18,7 @@ const Checkbox = forwardRef< {...rest} > - + )); diff --git a/constants.test.ts b/constants.test.ts new file mode 100644 index 0000000..1f87ad2 --- /dev/null +++ b/constants.test.ts @@ -0,0 +1,13 @@ +import { RISC0_PRIVACY_POLICY_URL, RISC0_TERMS_OF_SERVICE_URL } from './constants'; + +describe('Constants', () => { + it('should have correct RISC0_PRIVACY_POLICY_URL', () => { + expect(typeof RISC0_PRIVACY_POLICY_URL).toBe('string'); + expect(RISC0_PRIVACY_POLICY_URL).toBe('https://www.risczero.com/policy'); + }); + + it('should have correct RISC0_TERMS_OF_SERVICE_URL', () => { + expect(typeof RISC0_TERMS_OF_SERVICE_URL).toBe('string'); + expect(RISC0_TERMS_OF_SERVICE_URL).toBe('https://www.risczero.com/terms-of-service'); + }); +}); \ No newline at end of file diff --git a/hooks/use-event-listener.test.ts b/hooks/use-event-listener.test.ts index b641975..74ea2b2 100644 --- a/hooks/use-event-listener.test.ts +++ b/hooks/use-event-listener.test.ts @@ -1,5 +1,5 @@ -import { renderHook } from '@testing-library/react-hooks'; -import type { RefObject } from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { useRef, type RefObject } from 'react'; import { useEventListener } from './use-event-listener'; import { vi } from 'vitest'; @@ -29,4 +29,72 @@ describe('useEventListener', () => { expect(handler).not.toHaveBeenCalled(); }); + + it('adds event listener to window when no element is provided', () => { + const handler = vi.fn(); + const { unmount } = renderHook(() => useEventListener('click', handler)); + + act(() => { + window.dispatchEvent(new Event('click')); + }); + + expect(handler).toHaveBeenCalled(); + + unmount(); + }); + + it('adds event listener to provided element', () => { + const handler = vi.fn(); + const { result, unmount } = renderHook(() => { + const ref = useRef(document.createElement('div')); + useEventListener('click', handler, ref); + return ref; + }); + + act(() => { + result.current?.current?.dispatchEvent(new Event('click')); + }); + + expect(handler).toHaveBeenCalled(); + + unmount(); + }); + + it('removes event listener when component unmounts', () => { + const handler = vi.fn(); + const { unmount } = renderHook(() => useEventListener('click', handler)); + + unmount(); + + act(() => { + window.dispatchEvent(new Event('click')); + }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it('updates event listener when handler changes', () => { + const handler1 = vi.fn(); + const handler2 = vi.fn(); + const { rerender, unmount } = renderHook( + ({ handler }) => useEventListener('click', handler), + { initialProps: { handler: handler1 } } + ); + + act(() => { + window.dispatchEvent(new Event('click')); + }); + + expect(handler1).toHaveBeenCalled(); + + rerender({ handler: handler2 }); + + act(() => { + window.dispatchEvent(new Event('click')); + }); + + expect(handler2).toHaveBeenCalled(); + + unmount(); + }); }); \ No newline at end of file diff --git a/hooks/use-local-storage.test.ts b/hooks/use-local-storage.test.ts deleted file mode 100644 index e643dd0..0000000 --- a/hooks/use-local-storage.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { renderHook, act } from '@testing-library/react-hooks'; -import { useRef } from 'react'; -import { useEventListener } from './use-event-listener'; -import { vi } from 'vitest'; - -describe('useEventListener', () => { - it('adds event listener to window when no element is provided', () => { - const handler = vi.fn(); - const { unmount } = renderHook(() => useEventListener('click', handler)); - - act(() => { - window.dispatchEvent(new Event('click')); - }); - - expect(handler).toHaveBeenCalled(); - - unmount(); - }); - - it('adds event listener to provided element', () => { - const handler = vi.fn(); - const { result, unmount } = renderHook(() => { - const ref = useRef(document.createElement('div')); - useEventListener('click', handler, ref); - return ref; - }); - - act(() => { - result.current?.current?.dispatchEvent(new Event('click')); - }); - - expect(handler).toHaveBeenCalled(); - - unmount(); - }); - - it('removes event listener when component unmounts', () => { - const handler = vi.fn(); - const { unmount } = renderHook(() => useEventListener('click', handler)); - - unmount(); - - act(() => { - window.dispatchEvent(new Event('click')); - }); - - expect(handler).not.toHaveBeenCalled(); - }); - - it('updates event listener when handler changes', () => { - const handler1 = vi.fn(); - const handler2 = vi.fn(); - const { rerender, unmount } = renderHook( - ({ handler }) => useEventListener('click', handler), - { initialProps: { handler: handler1 } } - ); - - act(() => { - window.dispatchEvent(new Event('click')); - }); - - expect(handler1).toHaveBeenCalled(); - - rerender({ handler: handler2 }); - - act(() => { - window.dispatchEvent(new Event('click')); - }); - - expect(handler2).toHaveBeenCalled(); - - unmount(); - }); -}); \ No newline at end of file diff --git a/input.test.tsx b/input.test.tsx new file mode 100644 index 0000000..20f9c2f --- /dev/null +++ b/input.test.tsx @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react'; +import { Input } from './input'; +import { createRef } from 'react'; + +describe('Input', () => { + test('renders without crashing', () => { + render(); + const inputElement = screen.getByRole('textbox'); + expect(inputElement).toBeInTheDocument(); + }); + + test('forwards ref correctly', () => { + const ref = createRef(); + render(); + expect(ref.current).not.toBeNull(); + }); + + test('renders startIcon correctly', () => { + const StartIcon = () =>
; + render(} />); + const startIconElement = screen.getByTestId('start-icon'); + expect(startIconElement).toBeInTheDocument(); + }); + + test('renders endIcon correctly', () => { + const EndIcon = () =>
; + render(} />); + const endIconElement = screen.getByTestId('end-icon'); + expect(endIconElement).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/package.json b/package.json index 7df4eff..ad661de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@risc0/ui", - "version": "0.0.67", + "version": "0.0.68", "sideEffects": false, "type": "module", "scripts": { @@ -8,7 +8,7 @@ "check": "tsc && bunx @biomejs/biome check . --apply-unsafe", "prepare": "npx husky", "sort-package": "bunx sort-package-json 'package.json'", - "test": "bun test spec && vitest run --silent" + "test": "vitest run --silent --coverage && istanbul-badges-readme" }, "dependencies": { "@radix-ui/react-avatar": "1.0.4", @@ -50,7 +50,9 @@ "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", "@vitejs/plugin-react-swc": "3.6.0", + "@vitest/coverage-v8": "1.6.0", "happy-dom": "14.10.1", + "istanbul-badges-readme": "1.9.0", "vitest": "1.6.0" }, "peerDependencies": { diff --git a/textarea.test.tsx b/textarea.test.tsx new file mode 100644 index 0000000..4ac2248 --- /dev/null +++ b/textarea.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from '@testing-library/react'; +import { Textarea } from './textarea'; +import { createRef } from 'react'; + +describe('Textarea', () => { + test('renders without crashing', () => { + render(