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