diff --git a/CONTEXT.md b/CONTEXT.md index a90fe07..31218d9 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -338,9 +338,9 @@ const encrypted = await user.signer.nip44.encrypt(user.pubkey, "hello world"); const decrypted = await user.signer.nip44.decrypt(user.pubkey, encrypted) // "hello world" ``` -### Rendering Kind 1 Text +### Rendering Rich Text Content -If you need to render kind 1 text, use the `NoteContent` component: +Nostr text notes (kind 1, 11, and 1111) have a plaintext `content` field that may contain URLs, hashtags, and Nostr URIs. These events should render their content using the `NoteContent` component: ```tsx import { NoteContent } from "@/components/NoteContent"; @@ -367,19 +367,53 @@ export function Post(/* ...props */) { - Component-based architecture with React hooks - Default connection to multiple Nostr relays for network redundancy -## Build & Deployment +## Writing Tests -- Build for production: `npm run build` -- Development build: `npm run build:dev` +This project uses Vitest for testing React components. The `TestApp` component provides all necessary providers for components that use Nostr functionality, React Router, and TanStack Query. + +Test files should be placed next to the module they test, using a `.test.tsx` or `.test.ts` extension: +- `src/components/MyComponent.tsx` → `src/components/MyComponent.test.tsx` +- `src/hooks/useCustomHook.ts` → `src/hooks/useCustomHook.test.ts` + +### Test Setup + +Wrap components with the `TestApp` component to provide required context providers: + +```tsx +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TestApp } from '@/test/TestApp'; +import { MyComponent } from './MyComponent'; + +describe('MyComponent', () => { + it('renders correctly', () => { + render( + + + + ); + + expect(screen.getByText('Expected text')).toBeInTheDocument(); + }); +}); +``` + +### Mocking Hooks + +Mock custom hooks using Vitest's `vi.mock()`: + +```tsx +import { vi } from 'vitest'; + +vi.mock('@/hooks/useCurrentUser', () => ({ + useCurrentUser: () => ({ + user: null, // or provide mock user data + }), +})); +``` ## Testing Your Changes -Whenever you modify code, you should test your changes after you're finished by running: +Whenever you modify code, you must run the **test** script using the **run_script** tool. -```bash -npm run test -``` - -This command will typecheck the code and attempt to build it. - -Your task is not considered finished until this test passes without errors. \ No newline at end of file +**Your task is not considered finished until this test passes without errors.** \ No newline at end of file diff --git a/src/components/NoteContent.test.tsx b/src/components/NoteContent.test.tsx new file mode 100644 index 0000000..4da125d --- /dev/null +++ b/src/components/NoteContent.test.tsx @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TestApp } from '@/test/TestApp'; +import { NoteContent } from './NoteContent'; +import type { NostrEvent } from '@nostrify/nostrify'; + +describe('NoteContent', () => { + it('linkifies URLs in kind 1 events', () => { + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: 'Check out this link: https://example.com', + sig: 'test-sig', + }; + + render( + + + + ); + + const link = screen.getByRole('link', { name: 'https://example.com' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('target', '_blank'); + }); + + it('linkifies URLs in kind 1111 events (comments)', () => { + const event: NostrEvent = { + id: 'test-comment-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1111, + tags: [ + ['a', '30040:pubkey:identifier'], + ['k', '30040'], + ['p', 'pubkey'], + ], + content: 'I think the log events should be different kind numbers instead of having a `log-type` tag. That way you can use normal Nostr filters to filter the log types. Also, the `note` type should just b a kind 1111: https://nostrbook.dev/kinds/1111', + sig: 'test-sig', + }; + + render( + + + + ); + + const link = screen.getByRole('link', { name: 'https://nostrbook.dev/kinds/1111' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://nostrbook.dev/kinds/1111'); + expect(link).toHaveAttribute('target', '_blank'); + }); + + it('handles text without URLs correctly', () => { + const event: NostrEvent = { + id: 'test-id', + pubkey: 'test-pubkey', + created_at: Math.floor(Date.now() / 1000), + kind: 1111, + tags: [], + content: 'This is just plain text without any links.', + sig: 'test-sig', + }; + + render( + + + + ); + + expect(screen.getByText('This is just plain text without any links.')).toBeInTheDocument(); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/src/components/NoteContent.tsx b/src/components/NoteContent.tsx index c56cc2f..0372f54 100644 --- a/src/components/NoteContent.tsx +++ b/src/components/NoteContent.tsx @@ -10,7 +10,8 @@ interface NoteContentProps { className?: string; } -export function NoteContent({ +/** Parses content of text note events so that URLs and hashtags are linkified. */ +export function NoteContent({ event, className, }: NoteContentProps) { @@ -18,8 +19,6 @@ export function NoteContent({ // Process the content to render mentions, links, etc. useEffect(() => { - if (!event || event.kind !== 1) return; - const processContent = async () => { const text = event.content; diff --git a/src/test/TestApp.tsx b/src/test/TestApp.tsx new file mode 100644 index 0000000..c2953cc --- /dev/null +++ b/src/test/TestApp.tsx @@ -0,0 +1,31 @@ +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { NostrLoginProvider } from '@nostrify/react/login'; +import NostrProvider from '@/components/NostrProvider'; + +interface TestAppProps { + children: React.ReactNode; +} + +export function TestApp({ children }: TestAppProps) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return ( + + + + + {children} + + + + + ); +} + +export default TestApp; \ No newline at end of file