You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
911 lines
24 KiB
911 lines
24 KiB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
|
import { get, writable } from 'svelte/store'; |
|
import type { UserState } from '../../src/lib/stores/userStore'; |
|
import { NDKEvent } from '@nostr-dev-kit/ndk'; |
|
|
|
// Mock userStore |
|
const createMockUserStore = (signedIn: boolean = false) => { |
|
const store = writable<UserState>({ |
|
pubkey: signedIn ? 'a'.repeat(64) : null, |
|
npub: signedIn ? 'npub1test' : null, |
|
profile: signedIn ? { |
|
name: 'Test User', |
|
displayName: 'Test User', |
|
picture: 'https://example.com/avatar.jpg', |
|
} : null, |
|
relays: { inbox: [], outbox: [] }, |
|
loginMethod: signedIn ? 'extension' : null, |
|
ndkUser: null, |
|
signer: signedIn ? { sign: vi.fn() } as any : null, |
|
signedIn, |
|
}); |
|
return store; |
|
}; |
|
|
|
// Mock activeOutboxRelays |
|
const mockActiveOutboxRelays = writable<string[]>(['wss://relay.example.com']); |
|
|
|
// Mock NDK |
|
const createMockNDK = () => ({ |
|
fetchEvent: vi.fn(), |
|
publish: vi.fn(), |
|
}); |
|
|
|
describe('CommentButton - Address Parsing', () => { |
|
it('parses valid event address correctly', () => { |
|
const address = '30041:abc123def456:my-article'; |
|
const parts = address.split(':'); |
|
|
|
expect(parts).toHaveLength(3); |
|
|
|
const [kindStr, pubkey, dTag] = parts; |
|
const kind = parseInt(kindStr); |
|
|
|
expect(kind).toBe(30041); |
|
expect(pubkey).toBe('abc123def456'); |
|
expect(dTag).toBe('my-article'); |
|
expect(isNaN(kind)).toBe(false); |
|
}); |
|
|
|
it('handles dTag with colons correctly', () => { |
|
const address = '30041:abc123:article:with:colons'; |
|
const parts = address.split(':'); |
|
|
|
expect(parts.length).toBeGreaterThanOrEqual(3); |
|
|
|
const [kindStr, pubkey, ...dTagParts] = parts; |
|
const dTag = dTagParts.join(':'); |
|
|
|
expect(parseInt(kindStr)).toBe(30041); |
|
expect(pubkey).toBe('abc123'); |
|
expect(dTag).toBe('article:with:colons'); |
|
}); |
|
|
|
it('returns null for invalid address format (too few parts)', () => { |
|
const address = '30041:abc123'; |
|
const parts = address.split(':'); |
|
|
|
if (parts.length !== 3) { |
|
expect(parts.length).toBeLessThan(3); |
|
} |
|
}); |
|
|
|
it('returns null for invalid address format (invalid kind)', () => { |
|
const address = 'invalid:abc123:dtag'; |
|
const parts = address.split(':'); |
|
const kind = parseInt(parts[0]); |
|
|
|
expect(isNaN(kind)).toBe(true); |
|
}); |
|
|
|
it('parses different publication kinds correctly', () => { |
|
const addresses = [ |
|
'30040:pubkey:section-id', // Zettel section |
|
'30041:pubkey:article-id', // Long-form article |
|
'30818:pubkey:wiki-id', // Wiki article |
|
'30023:pubkey:blog-id', // Blog post |
|
]; |
|
|
|
addresses.forEach(address => { |
|
const parts = address.split(':'); |
|
const kind = parseInt(parts[0]); |
|
|
|
expect(isNaN(kind)).toBe(false); |
|
expect(kind).toBeGreaterThan(0); |
|
}); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - NIP-22 Event Creation', () => { |
|
let mockNDK: any; |
|
let mockUserStore: any; |
|
let mockActiveOutboxRelays: any; |
|
|
|
beforeEach(() => { |
|
mockNDK = createMockNDK(); |
|
mockUserStore = createMockUserStore(true); |
|
mockActiveOutboxRelays = writable(['wss://relay.example.com']); |
|
}); |
|
|
|
afterEach(() => { |
|
vi.clearAllMocks(); |
|
}); |
|
|
|
it('creates kind 1111 comment event', async () => { |
|
const address = '30041:' + 'a'.repeat(64) + ':my-article'; |
|
const content = 'This is my comment'; |
|
|
|
// Mock event creation |
|
const commentEvent = new NDKEvent(mockNDK); |
|
commentEvent.kind = 1111; |
|
commentEvent.content = content; |
|
|
|
expect(commentEvent.kind).toBe(1111); |
|
expect(commentEvent.content).toBe(content); |
|
}); |
|
|
|
it('includes correct uppercase tags (A, K, P) for root', () => { |
|
const address = '30041:' + 'b'.repeat(64) + ':article-id'; |
|
const authorPubkey = 'b'.repeat(64); |
|
const kind = 30041; |
|
const relayHint = 'wss://relay.example.com'; |
|
|
|
const tags = [ |
|
['A', address, relayHint, authorPubkey], |
|
['K', kind.toString()], |
|
['P', authorPubkey, relayHint], |
|
]; |
|
|
|
// Verify uppercase root tags |
|
expect(tags[0][0]).toBe('A'); |
|
expect(tags[0][1]).toBe(address); |
|
expect(tags[0][2]).toBe(relayHint); |
|
expect(tags[0][3]).toBe(authorPubkey); |
|
|
|
expect(tags[1][0]).toBe('K'); |
|
expect(tags[1][1]).toBe(kind.toString()); |
|
|
|
expect(tags[2][0]).toBe('P'); |
|
expect(tags[2][1]).toBe(authorPubkey); |
|
expect(tags[2][2]).toBe(relayHint); |
|
}); |
|
|
|
it('includes correct lowercase tags (a, k, p) for parent', () => { |
|
const address = '30041:' + 'c'.repeat(64) + ':article-id'; |
|
const authorPubkey = 'c'.repeat(64); |
|
const kind = 30041; |
|
const relayHint = 'wss://relay.example.com'; |
|
|
|
const tags = [ |
|
['a', address, relayHint], |
|
['k', kind.toString()], |
|
['p', authorPubkey, relayHint], |
|
]; |
|
|
|
// Verify lowercase parent tags |
|
expect(tags[0][0]).toBe('a'); |
|
expect(tags[0][1]).toBe(address); |
|
expect(tags[0][2]).toBe(relayHint); |
|
|
|
expect(tags[1][0]).toBe('k'); |
|
expect(tags[1][1]).toBe(kind.toString()); |
|
|
|
expect(tags[2][0]).toBe('p'); |
|
expect(tags[2][1]).toBe(authorPubkey); |
|
expect(tags[2][2]).toBe(relayHint); |
|
}); |
|
|
|
it('includes e tag with event ID when available', () => { |
|
const eventId = 'd'.repeat(64); |
|
const relayHint = 'wss://relay.example.com'; |
|
|
|
const eTag = ['e', eventId, relayHint]; |
|
|
|
expect(eTag[0]).toBe('e'); |
|
expect(eTag[1]).toBe(eventId); |
|
expect(eTag[2]).toBe(relayHint); |
|
expect(eTag[1]).toHaveLength(64); |
|
}); |
|
|
|
it('creates complete NIP-22 tag structure', () => { |
|
const address = '30041:' + 'e'.repeat(64) + ':test-article'; |
|
const authorPubkey = 'e'.repeat(64); |
|
const kind = 30041; |
|
const eventId = 'f'.repeat(64); |
|
const relayHint = 'wss://relay.example.com'; |
|
|
|
const tags = [ |
|
// Root scope - uppercase tags |
|
['A', address, relayHint, authorPubkey], |
|
['K', kind.toString()], |
|
['P', authorPubkey, relayHint], |
|
|
|
// Parent scope - lowercase tags |
|
['a', address, relayHint], |
|
['k', kind.toString()], |
|
['p', authorPubkey, relayHint], |
|
|
|
// Event ID |
|
['e', eventId, relayHint], |
|
]; |
|
|
|
// Verify all tags are present |
|
expect(tags).toHaveLength(7); |
|
|
|
// Verify root tags |
|
expect(tags.filter(t => t[0] === 'A')).toHaveLength(1); |
|
expect(tags.filter(t => t[0] === 'K')).toHaveLength(1); |
|
expect(tags.filter(t => t[0] === 'P')).toHaveLength(1); |
|
|
|
// Verify parent tags |
|
expect(tags.filter(t => t[0] === 'a')).toHaveLength(1); |
|
expect(tags.filter(t => t[0] === 'k')).toHaveLength(1); |
|
expect(tags.filter(t => t[0] === 'p')).toHaveLength(1); |
|
|
|
// Verify event tag |
|
expect(tags.filter(t => t[0] === 'e')).toHaveLength(1); |
|
}); |
|
|
|
it('uses correct relay hints from activeOutboxRelays', () => { |
|
const relays = get(mockActiveOutboxRelays); |
|
const relayHint = relays[0]; |
|
|
|
expect(relayHint).toBe('wss://relay.example.com'); |
|
expect(relays).toHaveLength(1); |
|
}); |
|
|
|
it('handles multiple outbox relays correctly', () => { |
|
const multipleRelays = writable([ |
|
'wss://relay1.example.com', |
|
'wss://relay2.example.com', |
|
'wss://relay3.example.com', |
|
]); |
|
|
|
const relays = get(multipleRelays); |
|
const relayHint = relays[0]; |
|
|
|
expect(relayHint).toBe('wss://relay1.example.com'); |
|
expect(relays).toHaveLength(3); |
|
}); |
|
|
|
it('handles empty relay list gracefully', () => { |
|
const emptyRelays = writable<string[]>([]); |
|
const relays = get(emptyRelays); |
|
const relayHint = relays[0] || ''; |
|
|
|
expect(relayHint).toBe(''); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - Event Signing and Publishing', () => { |
|
let mockNDK: any; |
|
let mockSigner: any; |
|
|
|
beforeEach(() => { |
|
mockNDK = createMockNDK(); |
|
mockSigner = { |
|
sign: vi.fn().mockResolvedValue(undefined), |
|
}; |
|
}); |
|
|
|
afterEach(() => { |
|
vi.clearAllMocks(); |
|
}); |
|
|
|
it('signs event with user signer', async () => { |
|
const commentEvent = new NDKEvent(mockNDK); |
|
commentEvent.kind = 1111; |
|
commentEvent.content = 'Test comment'; |
|
|
|
await mockSigner.sign(commentEvent); |
|
|
|
expect(mockSigner.sign).toHaveBeenCalledWith(commentEvent); |
|
expect(mockSigner.sign).toHaveBeenCalledTimes(1); |
|
}); |
|
|
|
it('publishes to outbox relays', async () => { |
|
const publishMock = vi.fn().mockResolvedValue(new Set(['wss://relay.example.com'])); |
|
|
|
const commentEvent = new NDKEvent(mockNDK); |
|
commentEvent.publish = publishMock; |
|
|
|
const publishedRelays = await commentEvent.publish(); |
|
|
|
expect(publishMock).toHaveBeenCalled(); |
|
expect(publishedRelays.size).toBeGreaterThan(0); |
|
}); |
|
|
|
it('handles publishing errors gracefully', async () => { |
|
const publishMock = vi.fn().mockResolvedValue(new Set()); |
|
|
|
const commentEvent = new NDKEvent(mockNDK); |
|
commentEvent.publish = publishMock; |
|
|
|
const publishedRelays = await commentEvent.publish(); |
|
|
|
expect(publishedRelays.size).toBe(0); |
|
}); |
|
|
|
it('throws error when publishing fails', async () => { |
|
const publishMock = vi.fn().mockRejectedValue(new Error('Network error')); |
|
|
|
const commentEvent = new NDKEvent(mockNDK); |
|
commentEvent.publish = publishMock; |
|
|
|
await expect(commentEvent.publish()).rejects.toThrow('Network error'); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - User Authentication', () => { |
|
it('requires user to be signed in', () => { |
|
const signedOutStore = createMockUserStore(false); |
|
const user = get(signedOutStore); |
|
|
|
expect(user.signedIn).toBe(false); |
|
expect(user.signer).toBeNull(); |
|
}); |
|
|
|
it('shows error when user is not signed in', () => { |
|
const signedOutStore = createMockUserStore(false); |
|
const user = get(signedOutStore); |
|
|
|
if (!user.signedIn || !user.signer) { |
|
const error = 'You must be signed in to comment'; |
|
expect(error).toBe('You must be signed in to comment'); |
|
} |
|
}); |
|
|
|
it('allows commenting when user is signed in', () => { |
|
const signedInStore = createMockUserStore(true); |
|
const user = get(signedInStore); |
|
|
|
expect(user.signedIn).toBe(true); |
|
expect(user.signer).not.toBeNull(); |
|
}); |
|
|
|
it('displays user profile information when signed in', () => { |
|
const signedInStore = createMockUserStore(true); |
|
const user = get(signedInStore); |
|
|
|
expect(user.profile).not.toBeNull(); |
|
expect(user.profile?.displayName).toBe('Test User'); |
|
expect(user.profile?.picture).toBe('https://example.com/avatar.jpg'); |
|
}); |
|
|
|
it('handles missing user profile gracefully', () => { |
|
const storeWithoutProfile = writable<UserState>({ |
|
pubkey: 'a'.repeat(64), |
|
npub: 'npub1test', |
|
profile: null, |
|
relays: { inbox: [], outbox: [] }, |
|
loginMethod: 'extension', |
|
ndkUser: null, |
|
signer: { sign: vi.fn() } as any, |
|
signedIn: true, |
|
}); |
|
|
|
const user = get(storeWithoutProfile); |
|
const displayName = user.profile?.displayName || user.profile?.name || 'Anonymous'; |
|
|
|
expect(displayName).toBe('Anonymous'); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - User Interactions', () => { |
|
it('prevents submission of empty comment', () => { |
|
const commentContent = ''; |
|
const isEmpty = !commentContent.trim(); |
|
|
|
expect(isEmpty).toBe(true); |
|
}); |
|
|
|
it('allows submission of non-empty comment', () => { |
|
const commentContent = 'This is a valid comment'; |
|
const isEmpty = !commentContent.trim(); |
|
|
|
expect(isEmpty).toBe(false); |
|
}); |
|
|
|
it('handles whitespace-only comments as empty', () => { |
|
const commentContent = ' \n\t '; |
|
const isEmpty = !commentContent.trim(); |
|
|
|
expect(isEmpty).toBe(true); |
|
}); |
|
|
|
it('clears input after successful comment', () => { |
|
let commentContent = 'This is my comment'; |
|
|
|
// Simulate successful submission |
|
commentContent = ''; |
|
|
|
expect(commentContent).toBe(''); |
|
}); |
|
|
|
it('closes comment UI after successful posting', () => { |
|
let showCommentUI = true; |
|
|
|
// Simulate successful post with delay |
|
setTimeout(() => { |
|
showCommentUI = false; |
|
}, 0); |
|
|
|
// Initially still open |
|
expect(showCommentUI).toBe(true); |
|
}); |
|
|
|
it('calls onCommentPosted callback when provided', () => { |
|
const onCommentPosted = vi.fn(); |
|
|
|
// Simulate successful comment post |
|
onCommentPosted(); |
|
|
|
expect(onCommentPosted).toHaveBeenCalled(); |
|
}); |
|
|
|
it('does not error when onCommentPosted is not provided', () => { |
|
const onCommentPosted = undefined; |
|
|
|
expect(() => { |
|
if (onCommentPosted) { |
|
onCommentPosted(); |
|
} |
|
}).not.toThrow(); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - UI State Management', () => { |
|
it('button is hidden by default', () => { |
|
const sectionHovered = false; |
|
const showCommentUI = false; |
|
const visible = sectionHovered || showCommentUI; |
|
|
|
expect(visible).toBe(false); |
|
}); |
|
|
|
it('button appears on section hover', () => { |
|
const sectionHovered = true; |
|
const showCommentUI = false; |
|
const visible = sectionHovered || showCommentUI; |
|
|
|
expect(visible).toBe(true); |
|
}); |
|
|
|
it('button remains visible when comment UI is shown', () => { |
|
const sectionHovered = false; |
|
const showCommentUI = true; |
|
const visible = sectionHovered || showCommentUI; |
|
|
|
expect(visible).toBe(true); |
|
}); |
|
|
|
it('toggles comment UI when button is clicked', () => { |
|
let showCommentUI = false; |
|
|
|
// Simulate button click |
|
showCommentUI = !showCommentUI; |
|
expect(showCommentUI).toBe(true); |
|
|
|
// Click again |
|
showCommentUI = !showCommentUI; |
|
expect(showCommentUI).toBe(false); |
|
}); |
|
|
|
it('resets error state when toggling UI', () => { |
|
let error: string | null = 'Previous error'; |
|
let success = true; |
|
|
|
// Simulate UI toggle |
|
error = null; |
|
success = false; |
|
|
|
expect(error).toBeNull(); |
|
expect(success).toBe(false); |
|
}); |
|
|
|
it('shows error message when present', () => { |
|
const error = 'Failed to post comment'; |
|
|
|
expect(error).toBeDefined(); |
|
expect(error.length).toBeGreaterThan(0); |
|
}); |
|
|
|
it('shows success message after posting', () => { |
|
const success = true; |
|
const successMessage = 'Comment posted successfully!'; |
|
|
|
if (success) { |
|
expect(successMessage).toBe('Comment posted successfully!'); |
|
} |
|
}); |
|
|
|
it('disables submit button when submitting', () => { |
|
const isSubmitting = true; |
|
const disabled = isSubmitting; |
|
|
|
expect(disabled).toBe(true); |
|
}); |
|
|
|
it('disables submit button when comment is empty', () => { |
|
const commentContent = ''; |
|
const isSubmitting = false; |
|
const disabled = isSubmitting || !commentContent.trim(); |
|
|
|
expect(disabled).toBe(true); |
|
}); |
|
|
|
it('enables submit button when comment is valid', () => { |
|
const commentContent = 'Valid comment'; |
|
const isSubmitting = false; |
|
const disabled = isSubmitting || !commentContent.trim(); |
|
|
|
expect(disabled).toBe(false); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - Edge Cases', () => { |
|
it('handles invalid address format gracefully', () => { |
|
const invalidAddresses = [ |
|
'', |
|
'invalid', |
|
'30041:', |
|
':pubkey:dtag', |
|
'30041:pubkey', |
|
'not-a-number:pubkey:dtag', |
|
]; |
|
|
|
invalidAddresses.forEach(address => { |
|
const parts = address.split(':'); |
|
const isValid = parts.length === 3 && !isNaN(parseInt(parts[0])); |
|
|
|
expect(isValid).toBe(false); |
|
}); |
|
}); |
|
|
|
it('handles network errors during event fetch', async () => { |
|
const mockNDK = { |
|
fetchEvent: vi.fn().mockRejectedValue(new Error('Network error')), |
|
}; |
|
|
|
let eventId = ''; |
|
try { |
|
await mockNDK.fetchEvent({}); |
|
} catch (err) { |
|
// Handle gracefully, continue without event ID |
|
eventId = ''; |
|
} |
|
|
|
expect(eventId).toBe(''); |
|
}); |
|
|
|
it('handles missing relay information', () => { |
|
const emptyRelays: string[] = []; |
|
const relayHint = emptyRelays[0] || ''; |
|
|
|
expect(relayHint).toBe(''); |
|
}); |
|
|
|
it('handles very long comment text without truncation', () => { |
|
const longComment = 'a'.repeat(10000); |
|
const content = longComment; |
|
|
|
expect(content.length).toBe(10000); |
|
expect(content).toBe(longComment); |
|
}); |
|
|
|
it('handles special characters in comments', () => { |
|
const specialComments = [ |
|
'Comment with "quotes"', |
|
'Comment with emoji 😊', |
|
'Comment with\nnewlines', |
|
'Comment with\ttabs', |
|
'Comment with <html> tags', |
|
'Comment with & ampersands', |
|
]; |
|
|
|
specialComments.forEach(comment => { |
|
expect(comment.length).toBeGreaterThan(0); |
|
expect(typeof comment).toBe('string'); |
|
}); |
|
}); |
|
|
|
it('handles event creation failure', async () => { |
|
const address = 'invalid:address'; |
|
const parts = address.split(':'); |
|
|
|
if (parts.length !== 3) { |
|
const error = 'Invalid event address'; |
|
expect(error).toBe('Invalid event address'); |
|
} |
|
}); |
|
|
|
it('handles signing errors', async () => { |
|
const mockSigner = { |
|
sign: vi.fn().mockRejectedValue(new Error('Signing failed')), |
|
}; |
|
|
|
const event = { kind: 1111, content: 'test' }; |
|
|
|
await expect(mockSigner.sign(event)).rejects.toThrow('Signing failed'); |
|
}); |
|
|
|
it('handles publish failure when no relays accept event', async () => { |
|
const publishMock = vi.fn().mockResolvedValue(new Set()); |
|
|
|
const relaySet = await publishMock(); |
|
|
|
if (relaySet.size === 0) { |
|
const error = 'Failed to publish to any relays'; |
|
expect(error).toBe('Failed to publish to any relays'); |
|
} |
|
}); |
|
}); |
|
|
|
describe('CommentButton - Cancel Functionality', () => { |
|
it('clears comment content when canceling', () => { |
|
let commentContent = 'This comment will be canceled'; |
|
|
|
// Simulate cancel |
|
commentContent = ''; |
|
|
|
expect(commentContent).toBe(''); |
|
}); |
|
|
|
it('closes comment UI when canceling', () => { |
|
let showCommentUI = true; |
|
|
|
// Simulate cancel |
|
showCommentUI = false; |
|
|
|
expect(showCommentUI).toBe(false); |
|
}); |
|
|
|
it('clears error state when canceling', () => { |
|
let error: string | null = 'Some error'; |
|
|
|
// Simulate cancel |
|
error = null; |
|
|
|
expect(error).toBeNull(); |
|
}); |
|
|
|
it('clears success state when canceling', () => { |
|
let success = true; |
|
|
|
// Simulate cancel |
|
success = false; |
|
|
|
expect(success).toBe(false); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - Event Fetching', () => { |
|
let mockNDK: any; |
|
|
|
beforeEach(() => { |
|
mockNDK = createMockNDK(); |
|
}); |
|
|
|
afterEach(() => { |
|
vi.clearAllMocks(); |
|
}); |
|
|
|
it('fetches target event to get event ID', async () => { |
|
const address = '30041:' + 'a'.repeat(64) + ':article'; |
|
const parts = address.split(':'); |
|
const [kindStr, authorPubkey, dTag] = parts; |
|
const kind = parseInt(kindStr); |
|
|
|
const mockEvent = { |
|
id: 'b'.repeat(64), |
|
kind, |
|
pubkey: authorPubkey, |
|
tags: [['d', dTag]], |
|
}; |
|
|
|
mockNDK.fetchEvent.mockResolvedValue(mockEvent); |
|
|
|
const targetEvent = await mockNDK.fetchEvent({ |
|
kinds: [kind], |
|
authors: [authorPubkey], |
|
'#d': [dTag], |
|
}); |
|
|
|
expect(mockNDK.fetchEvent).toHaveBeenCalled(); |
|
expect(targetEvent?.id).toBe('b'.repeat(64)); |
|
}); |
|
|
|
it('continues without event ID when fetch fails', async () => { |
|
mockNDK.fetchEvent.mockRejectedValue(new Error('Fetch failed')); |
|
|
|
let eventId = ''; |
|
try { |
|
const targetEvent = await mockNDK.fetchEvent({}); |
|
if (targetEvent) { |
|
eventId = targetEvent.id; |
|
} |
|
} catch (err) { |
|
// Continue without event ID |
|
eventId = ''; |
|
} |
|
|
|
expect(eventId).toBe(''); |
|
}); |
|
|
|
it('handles null event from fetch', async () => { |
|
mockNDK.fetchEvent.mockResolvedValue(null); |
|
|
|
const targetEvent = await mockNDK.fetchEvent({}); |
|
let eventId = ''; |
|
|
|
if (targetEvent) { |
|
eventId = targetEvent.id; |
|
} |
|
|
|
expect(eventId).toBe(''); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - CSS Classes and Styling', () => { |
|
it('applies visible class when section is hovered', () => { |
|
const sectionHovered = true; |
|
const showCommentUI = false; |
|
const hasVisibleClass = sectionHovered || showCommentUI; |
|
|
|
expect(hasVisibleClass).toBe(true); |
|
}); |
|
|
|
it('removes visible class when not hovered and UI closed', () => { |
|
const sectionHovered = false; |
|
const showCommentUI = false; |
|
const hasVisibleClass = sectionHovered || showCommentUI; |
|
|
|
expect(hasVisibleClass).toBe(false); |
|
}); |
|
|
|
it('button has correct aria-label', () => { |
|
const ariaLabel = 'Add comment'; |
|
|
|
expect(ariaLabel).toBe('Add comment'); |
|
}); |
|
|
|
it('button has correct title attribute', () => { |
|
const title = 'Add comment'; |
|
|
|
expect(title).toBe('Add comment'); |
|
}); |
|
|
|
it('submit button shows loading state when submitting', () => { |
|
const isSubmitting = true; |
|
const buttonText = isSubmitting ? 'Posting...' : 'Post Comment'; |
|
|
|
expect(buttonText).toBe('Posting...'); |
|
}); |
|
|
|
it('submit button shows normal state when not submitting', () => { |
|
const isSubmitting = false; |
|
const buttonText = isSubmitting ? 'Posting...' : 'Post Comment'; |
|
|
|
expect(buttonText).toBe('Post Comment'); |
|
}); |
|
}); |
|
|
|
describe('CommentButton - NIP-22 Compliance', () => { |
|
it('uses kind 1111 for comment events', () => { |
|
const kind = 1111; |
|
|
|
expect(kind).toBe(1111); |
|
}); |
|
|
|
it('includes all required NIP-22 tags for addressable events', () => { |
|
const requiredRootTags = ['A', 'K', 'P']; |
|
const requiredParentTags = ['a', 'k', 'p']; |
|
|
|
const tags = [ |
|
['A', 'address', 'relay', 'pubkey'], |
|
['K', 'kind'], |
|
['P', 'pubkey', 'relay'], |
|
['a', 'address', 'relay'], |
|
['k', 'kind'], |
|
['p', 'pubkey', 'relay'], |
|
]; |
|
|
|
requiredRootTags.forEach(tag => { |
|
expect(tags.some(t => t[0] === tag)).toBe(true); |
|
}); |
|
|
|
requiredParentTags.forEach(tag => { |
|
expect(tags.some(t => t[0] === tag)).toBe(true); |
|
}); |
|
}); |
|
|
|
it('A tag includes relay hint and author pubkey', () => { |
|
const aTag = ['A', '30041:pubkey:dtag', 'wss://relay.com', 'pubkey']; |
|
|
|
expect(aTag).toHaveLength(4); |
|
expect(aTag[0]).toBe('A'); |
|
expect(aTag[2]).toMatch(/^wss:\/\//); |
|
expect(aTag[3]).toBeTruthy(); |
|
}); |
|
|
|
it('P tag includes relay hint', () => { |
|
const pTag = ['P', 'pubkey', 'wss://relay.com']; |
|
|
|
expect(pTag).toHaveLength(3); |
|
expect(pTag[0]).toBe('P'); |
|
expect(pTag[2]).toMatch(/^wss:\/\//); |
|
}); |
|
|
|
it('lowercase tags for parent scope match root tags', () => { |
|
const address = '30041:pubkey:dtag'; |
|
const kind = '30041'; |
|
const pubkey = 'pubkey'; |
|
const relay = 'wss://relay.com'; |
|
|
|
const rootTags = [ |
|
['A', address, relay, pubkey], |
|
['K', kind], |
|
['P', pubkey, relay], |
|
]; |
|
|
|
const parentTags = [ |
|
['a', address, relay], |
|
['k', kind], |
|
['p', pubkey, relay], |
|
]; |
|
|
|
// Verify parent tags match root tags (lowercase) |
|
expect(parentTags[0][1]).toBe(rootTags[0][1]); // address |
|
expect(parentTags[1][1]).toBe(rootTags[1][1]); // kind |
|
expect(parentTags[2][1]).toBe(rootTags[2][1]); // pubkey |
|
}); |
|
}); |
|
|
|
describe('CommentButton - Integration Scenarios', () => { |
|
it('complete comment flow for signed-in user', () => { |
|
const userStore = createMockUserStore(true); |
|
const user = get(userStore); |
|
|
|
// User is signed in |
|
expect(user.signedIn).toBe(true); |
|
|
|
// Comment content is valid |
|
const content = 'Great article!'; |
|
expect(content.trim().length).toBeGreaterThan(0); |
|
|
|
// Address is valid |
|
const address = '30041:' + 'a'.repeat(64) + ':article'; |
|
const parts = address.split(':'); |
|
expect(parts.length).toBe(3); |
|
|
|
// Event would be created with kind 1111 |
|
const kind = 1111; |
|
expect(kind).toBe(1111); |
|
}); |
|
|
|
it('prevents comment flow for signed-out user', () => { |
|
const userStore = createMockUserStore(false); |
|
const user = get(userStore); |
|
|
|
expect(user.signedIn).toBe(false); |
|
|
|
if (!user.signedIn) { |
|
const error = 'You must be signed in to comment'; |
|
expect(error).toBeTruthy(); |
|
} |
|
}); |
|
|
|
it('handles comment with event ID lookup', async () => { |
|
const mockNDK = createMockNDK(); |
|
const eventId = 'c'.repeat(64); |
|
|
|
mockNDK.fetchEvent.mockResolvedValue({ id: eventId }); |
|
|
|
const targetEvent = await mockNDK.fetchEvent({}); |
|
|
|
const tags = [ |
|
['e', targetEvent.id, 'wss://relay.com'], |
|
]; |
|
|
|
expect(tags[0][1]).toBe(eventId); |
|
}); |
|
|
|
it('handles comment without event ID lookup', () => { |
|
const eventId = ''; |
|
|
|
const tags = [ |
|
['A', 'address', 'relay', 'pubkey'], |
|
['K', 'kind'], |
|
['P', 'pubkey', 'relay'], |
|
['a', 'address', 'relay'], |
|
['k', 'kind'], |
|
['p', 'pubkey', 'relay'], |
|
]; |
|
|
|
// No e tag should be included |
|
expect(tags.filter(t => t[0] === 'e')).toHaveLength(0); |
|
|
|
// But all other required tags should be present |
|
expect(tags.length).toBe(6); |
|
}); |
|
});
|
|
|