test(7.6): add tests for comm block components (BlockRenderer + all block types)
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
/**
|
||||
* Tests for Comm Block Components — Phase 7 Task 7.6
|
||||
*
|
||||
* Tests BlockRenderer and all block types:
|
||||
* text, markdown, html, image, audio, video, file, action_card, contact_card, miniapp
|
||||
*/
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import BlockRenderer from '@/components/comm/blocks/BlockRenderer';
|
||||
import MarkdownBlock from '@/components/comm/blocks/MarkdownBlock';
|
||||
import HtmlBlock from '@/components/comm/blocks/HtmlBlock';
|
||||
import ImageBlock from '@/components/comm/blocks/ImageBlock';
|
||||
import AudioBlock from '@/components/comm/blocks/AudioBlock';
|
||||
import VideoBlock from '@/components/comm/blocks/VideoBlock';
|
||||
import FileBlock from '@/components/comm/blocks/FileBlock';
|
||||
import ActionCardBlock from '@/components/comm/blocks/ActionCardBlock';
|
||||
import ContactCardBlock from '@/components/comm/blocks/ContactCardBlock';
|
||||
import MiniAppBlock from '@/components/comm/blocks/MiniAppBlock';
|
||||
import type { MessageBlock } from '@/store/commStore';
|
||||
|
||||
// ─── Mock Data Helpers ───
|
||||
|
||||
function makeBlock(
|
||||
id: string,
|
||||
block_type: string,
|
||||
block_data: Record<string, any>,
|
||||
sort_order: number = 0,
|
||||
): MessageBlock {
|
||||
return { id, block_type, block_data, sort_order };
|
||||
}
|
||||
|
||||
// ─── BlockRenderer Tests ───
|
||||
|
||||
describe('BlockRenderer', () => {
|
||||
it('renders null when blocks array is empty', () => {
|
||||
const { container } = render(<BlockRenderer blocks={[]} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders null when blocks is undefined', () => {
|
||||
const { container } = render(<BlockRenderer blocks={undefined as any} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders a text block with content', () => {
|
||||
const blocks = [makeBlock('b1', 'text', { text: 'Hello World' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders text block using content fallback when text is missing', () => {
|
||||
const blocks = [makeBlock('b1', 'text', { content: 'Fallback content' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('Fallback content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders text block as empty string when no text or content', () => {
|
||||
const blocks = [makeBlock('b1', 'text', {})];
|
||||
const { container } = render(<BlockRenderer blocks={blocks} />);
|
||||
const textDiv = container.querySelector('.text-sm');
|
||||
expect(textDiv).toBeInTheDocument();
|
||||
expect(textDiv?.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('renders multiple blocks in sort_order sequence', () => {
|
||||
const blocks = [
|
||||
makeBlock('b2', 'text', { text: 'Second' }, 2),
|
||||
makeBlock('b1', 'text', { text: 'First' }, 1),
|
||||
makeBlock('b3', 'text', { text: 'Third' }, 3),
|
||||
];
|
||||
const { container } = render(<BlockRenderer blocks={blocks} />);
|
||||
const wrappers = container.querySelectorAll('.block-wrapper');
|
||||
expect(wrappers).toHaveLength(3);
|
||||
expect(wrappers[0].textContent).toBe('First');
|
||||
expect(wrappers[1].textContent).toBe('Second');
|
||||
expect(wrappers[2].textContent).toBe('Third');
|
||||
});
|
||||
|
||||
it('renders unknown block type with fallback message', () => {
|
||||
const blocks = [makeBlock('b1', 'unknown_type', {})];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText(/Unbekannter Block-Typ: unknown_type/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders markdown block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'markdown', { markdown: '# Heading' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('Heading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders html block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'html', { html: '<p>HTML content</p>' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('HTML content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders image block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'image', { url: 'https://example.com/img.png', alt: 'Test Image' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
const img = screen.getByRole('img');
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/img.png');
|
||||
expect(img).toHaveAttribute('alt', 'Test Image');
|
||||
});
|
||||
|
||||
it('renders audio block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'audio', { url: 'https://example.com/audio.mp3' })];
|
||||
const { container } = render(<BlockRenderer blocks={blocks} />);
|
||||
const audio = container.querySelector('audio');
|
||||
expect(audio).toBeInTheDocument();
|
||||
expect(audio).toHaveAttribute('src', 'https://example.com/audio.mp3');
|
||||
});
|
||||
|
||||
it('renders video block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'video', { url: 'https://example.com/video.mp4' })];
|
||||
const { container } = render(<BlockRenderer blocks={blocks} />);
|
||||
const video = container.querySelector('video');
|
||||
expect(video).toBeInTheDocument();
|
||||
expect(video).toHaveAttribute('src', 'https://example.com/video.mp4');
|
||||
});
|
||||
|
||||
it('renders file block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf', name: 'doc.pdf' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('doc.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders action_card block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'action_card', { title: 'Action Title', body: 'Action Body' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('Action Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Action Body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders contact_card block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'contact_card', { contact_id: '123', name: 'John Doe' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders miniapp block via BlockRenderer', () => {
|
||||
const blocks = [makeBlock('b1', 'miniapp', { app_id: 'my-app' })];
|
||||
render(<BlockRenderer blocks={blocks} />);
|
||||
expect(screen.getByText(/Mini-App: my-app/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── MarkdownBlock Tests ───
|
||||
|
||||
describe('MarkdownBlock', () => {
|
||||
it('renders null when markdown content is empty', () => {
|
||||
const block = makeBlock('b1', 'markdown', {});
|
||||
const { container } = render(<MarkdownBlock block={block} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders heading from markdown', () => {
|
||||
const block = makeBlock('b1', 'markdown', { markdown: '# My Heading' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
expect(screen.getByText('My Heading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders paragraph from markdown', () => {
|
||||
const block = makeBlock('b1', 'markdown', { markdown: 'This is a paragraph.' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
expect(screen.getByText('This is a paragraph.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders unordered list from markdown', () => {
|
||||
const block = makeBlock('b1', 'markdown', { markdown: '- Item 1\n- Item 2' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ordered list from markdown', () => {
|
||||
const block = makeBlock('b1', 'markdown', { markdown: '1. First\n2. Second' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
expect(screen.getByText('First')).toBeInTheDocument();
|
||||
expect(screen.getByText('Second')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders link from markdown', () => {
|
||||
const block = makeBlock('b1', 'markdown', { markdown: '[Click here](https://example.com)' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
const link = screen.getByText('Click here');
|
||||
expect(link.closest('a')).toHaveAttribute('href', 'https://example.com');
|
||||
});
|
||||
|
||||
it('renders blockquote from markdown', () => {
|
||||
const block = makeBlock('b1', 'markdown', { markdown: '> This is a quote' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
expect(screen.getByText('This is a quote')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses content fallback when markdown field is missing', () => {
|
||||
const block = makeBlock('b1', 'markdown', { content: 'Fallback markdown' });
|
||||
render(<MarkdownBlock block={block} />);
|
||||
expect(screen.getByText('Fallback markdown')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── HtmlBlock Tests ───
|
||||
|
||||
describe('HtmlBlock', () => {
|
||||
it('renders null when html content is empty', () => {
|
||||
const block = makeBlock('b1', 'html', {});
|
||||
const { container } = render(<HtmlBlock block={block} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders basic HTML content', () => {
|
||||
const block = makeBlock('b1', 'html', { html: '<p>Simple HTML</p>' });
|
||||
render(<HtmlBlock block={block} />);
|
||||
expect(screen.getByText('Simple HTML')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('strips script tags from HTML', () => {
|
||||
const block = makeBlock('b1', 'html', {
|
||||
html: '<p>Safe content</p><script>alert("xss")</script>',
|
||||
});
|
||||
const { container } = render(<HtmlBlock block={block} />);
|
||||
expect(screen.getByText('Safe content')).toBeInTheDocument();
|
||||
expect(container.querySelector('script')).toBeNull();
|
||||
});
|
||||
|
||||
it('strips onclick event handlers', () => {
|
||||
const block = makeBlock('b1', 'html', {
|
||||
html: '<p onclick="alert(1)">Clickable</p>',
|
||||
});
|
||||
const { container } = render(<HtmlBlock block={block} />);
|
||||
const p = container.querySelector('p');
|
||||
expect(p).not.toHaveAttribute('onclick');
|
||||
});
|
||||
|
||||
it('replaces javascript: URLs in href', () => {
|
||||
const block = makeBlock('b1', 'html', {
|
||||
html: '<a href="javascript:alert(1)">Bad Link</a>',
|
||||
});
|
||||
const { container } = render(<HtmlBlock block={block} />);
|
||||
const link = container.querySelector('a');
|
||||
expect(link?.getAttribute('href')).not.toContain('javascript:');
|
||||
});
|
||||
|
||||
it('renders div with dangerouslySetInnerHTML', () => {
|
||||
const block = makeBlock('b1', 'html', { html: '<span data-test="yes">Content</span>' });
|
||||
const { container } = render(<HtmlBlock block={block} />);
|
||||
expect(container.querySelector('[data-test="yes"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── ImageBlock Tests ───
|
||||
|
||||
describe('ImageBlock', () => {
|
||||
it('renders null when url is missing', () => {
|
||||
const block = makeBlock('b1', 'image', {});
|
||||
const { container } = render(<ImageBlock block={block} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders image with url and alt text', () => {
|
||||
const block = makeBlock('b1', 'image', { url: 'https://example.com/img.png', alt: 'Description' });
|
||||
render(<ImageBlock block={block} />);
|
||||
const img = screen.getByRole('img');
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/img.png');
|
||||
expect(img).toHaveAttribute('alt', 'Description');
|
||||
});
|
||||
|
||||
it('renders image with empty alt when not provided', () => {
|
||||
const block = makeBlock('b1', 'image', { url: 'https://example.com/img.png' });
|
||||
const { container } = render(<ImageBlock block={block} />);
|
||||
const img = container.querySelector('img');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute('alt', '');
|
||||
});
|
||||
|
||||
it('renders alt caption below image when alt is provided', () => {
|
||||
const block = makeBlock('b1', 'image', { url: 'https://example.com/img.png', alt: 'Caption text' });
|
||||
render(<ImageBlock block={block} />);
|
||||
expect(screen.getByText('Caption text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render caption when alt is empty', () => {
|
||||
const block = makeBlock('b1', 'image', { url: 'https://example.com/img.png', alt: '' });
|
||||
const { container } = render(<ImageBlock block={block} />);
|
||||
const captions = container.querySelectorAll('.text-xs.text-secondary-500.italic');
|
||||
expect(captions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders image inside a link that opens in new tab', () => {
|
||||
const block = makeBlock('b1', 'image', { url: 'https://example.com/img.png' });
|
||||
const { container } = render(<ImageBlock block={block} />);
|
||||
const link = container.querySelector('a');
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
it('applies width attribute when provided as number', () => {
|
||||
const block = makeBlock('b1', 'image', { url: 'https://example.com/img.png', width: 300 });
|
||||
const { container } = render(<ImageBlock block={block} />);
|
||||
const img = container.querySelector('img');
|
||||
expect(img).toHaveAttribute('width', '300');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── AudioBlock Tests ───
|
||||
|
||||
describe('AudioBlock', () => {
|
||||
it('renders null when url is missing', () => {
|
||||
const block = makeBlock('b1', 'audio', {});
|
||||
const { container } = render(<AudioBlock block={block} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders audio element with url', () => {
|
||||
const block = makeBlock('b1', 'audio', { url: 'https://example.com/audio.mp3' });
|
||||
const { container } = render(<AudioBlock block={block} />);
|
||||
const audio = container.querySelector('audio');
|
||||
expect(audio).toBeInTheDocument();
|
||||
expect(audio).toHaveAttribute('src', 'https://example.com/audio.mp3');
|
||||
expect(audio).toHaveAttribute('controls');
|
||||
});
|
||||
|
||||
it('shows duration label when duration is provided', () => {
|
||||
const block = makeBlock('b1', 'audio', { url: 'https://example.com/audio.mp3', duration: 125 });
|
||||
render(<AudioBlock block={block} />);
|
||||
expect(screen.getByText(/Dauer: 2:05/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show duration label when duration is 0 or invalid', () => {
|
||||
const block = makeBlock('b1', 'audio', { url: 'https://example.com/audio.mp3', duration: 0 });
|
||||
const { container } = render(<AudioBlock block={block} />);
|
||||
expect(container.querySelector('.text-xs.text-secondary-500')).toBeNull();
|
||||
});
|
||||
|
||||
it('formats duration correctly for single-digit seconds', () => {
|
||||
const block = makeBlock('b1', 'audio', { url: 'https://example.com/audio.mp3', duration: 65 });
|
||||
render(<AudioBlock block={block} />);
|
||||
expect(screen.getByText(/Dauer: 1:05/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── VideoBlock Tests ───
|
||||
|
||||
describe('VideoBlock', () => {
|
||||
it('renders null when url is missing', () => {
|
||||
const block = makeBlock('b1', 'video', {});
|
||||
const { container } = render(<VideoBlock block={block} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders video element with url and controls', () => {
|
||||
const block = makeBlock('b1', 'video', { url: 'https://example.com/video.mp4' });
|
||||
const { container } = render(<VideoBlock block={block} />);
|
||||
const video = container.querySelector('video');
|
||||
expect(video).toBeInTheDocument();
|
||||
expect(video).toHaveAttribute('src', 'https://example.com/video.mp4');
|
||||
expect(video).toHaveAttribute('controls');
|
||||
});
|
||||
|
||||
it('sets poster attribute when thumbnail is provided', () => {
|
||||
const block = makeBlock('b1', 'video', { url: 'https://example.com/video.mp4', thumbnail: 'https://example.com/thumb.jpg' });
|
||||
const { container } = render(<VideoBlock block={block} />);
|
||||
const video = container.querySelector('video');
|
||||
expect(video).toHaveAttribute('poster', 'https://example.com/thumb.jpg');
|
||||
});
|
||||
|
||||
it('does not set poster when thumbnail is not a string', () => {
|
||||
const block = makeBlock('b1', 'video', { url: 'https://example.com/video.mp4', thumbnail: 123 });
|
||||
const { container } = render(<VideoBlock block={block} />);
|
||||
const video = container.querySelector('video');
|
||||
expect(video).not.toHaveAttribute('poster');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── FileBlock Tests ───
|
||||
|
||||
describe('FileBlock', () => {
|
||||
it('renders null when both url and name are missing', () => {
|
||||
const block = makeBlock('b1', 'file', {});
|
||||
const { container } = render(<FileBlock block={block} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders file name when provided', () => {
|
||||
const block = makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf', name: 'document.pdf' });
|
||||
render(<FileBlock block={block} />);
|
||||
expect(screen.getByText('document.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses default name "Datei" when name is not provided', () => {
|
||||
const block = makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf' });
|
||||
render(<FileBlock block={block} />);
|
||||
expect(screen.getByText('Datei')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows file size when size is provided', () => {
|
||||
const block = makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf', name: 'doc.pdf', size: 1048576 });
|
||||
render(<FileBlock block={block} />);
|
||||
expect(screen.getByText('1.0 MB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats file size in KB', () => {
|
||||
const block = makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf', name: 'doc.pdf', size: 5120 });
|
||||
render(<FileBlock block={block} />);
|
||||
expect(screen.getByText('5.0 KB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats file size in bytes', () => {
|
||||
const block = makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf', name: 'doc.pdf', size: 512 });
|
||||
render(<FileBlock block={block} />);
|
||||
expect(screen.getByText('512 B')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders download link with aria-label', () => {
|
||||
const block = makeBlock('b1', 'file', { url: 'https://example.com/doc.pdf', name: 'doc.pdf' });
|
||||
render(<FileBlock block={block} />);
|
||||
const downloadLink = screen.getByLabelText('Datei herunterladen: doc.pdf');
|
||||
expect(downloadLink).toBeInTheDocument();
|
||||
expect(downloadLink).toHaveAttribute('href', 'https://example.com/doc.pdf');
|
||||
});
|
||||
|
||||
it('does not render download link when url is missing', () => {
|
||||
const block = makeBlock('b1', 'file', { name: 'doc.pdf' });
|
||||
render(<FileBlock block={block} />);
|
||||
expect(screen.queryByLabelText(/Datei herunterladen/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── ActionCardBlock Tests ───
|
||||
|
||||
describe('ActionCardBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders title and body', () => {
|
||||
const block = makeBlock('b1', 'action_card', { title: 'Card Title', body: 'Card Body Text' });
|
||||
render(<ActionCardBlock block={block} />);
|
||||
expect(screen.getByText('Card Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Card Body Text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without title when not provided', () => {
|
||||
const block = makeBlock('b1', 'action_card', { body: 'Only body' });
|
||||
render(<ActionCardBlock block={block} />);
|
||||
expect(screen.getByText('Only body')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders action buttons', () => {
|
||||
const block = makeBlock('b1', 'action_card', {
|
||||
title: 'Title',
|
||||
body: 'Body',
|
||||
actions: [
|
||||
{ label: 'Confirm', action: 'https://example.com', type: 'primary' },
|
||||
{ label: 'Cancel', action: 'dismiss', type: 'secondary' },
|
||||
],
|
||||
});
|
||||
render(<ActionCardBlock block={block} />);
|
||||
expect(screen.getByText('Confirm')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens URL in new tab when action button is clicked', () => {
|
||||
const mockOpen = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
const block = makeBlock('b1', 'action_card', {
|
||||
title: 'Title',
|
||||
body: 'Body',
|
||||
actions: [{ label: 'Go', action: 'https://example.com', type: 'primary' }],
|
||||
});
|
||||
render(<ActionCardBlock block={block} />);
|
||||
fireEvent.click(screen.getByText('Go'));
|
||||
expect(mockOpen).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
|
||||
it('does not open URL when action is dismiss', () => {
|
||||
const mockOpen = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
const block = makeBlock('b1', 'action_card', {
|
||||
title: 'Title',
|
||||
body: 'Body',
|
||||
actions: [{ label: 'Dismiss', action: 'dismiss', type: 'secondary' }],
|
||||
});
|
||||
render(<ActionCardBlock block={block} />);
|
||||
fireEvent.click(screen.getByText('Dismiss'));
|
||||
expect(mockOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies primary styling to primary action buttons', () => {
|
||||
const block = makeBlock('b1', 'action_card', {
|
||||
title: 'Title',
|
||||
actions: [{ label: 'Primary', action: 'https://example.com', type: 'primary' }],
|
||||
});
|
||||
render(<ActionCardBlock block={block} />);
|
||||
const btn = screen.getByText('Primary');
|
||||
expect(btn.className).toContain('bg-primary-600');
|
||||
});
|
||||
|
||||
it('applies secondary styling to secondary action buttons', () => {
|
||||
const block = makeBlock('b1', 'action_card', {
|
||||
title: 'Title',
|
||||
actions: [{ label: 'Secondary', action: 'dismiss', type: 'secondary' }],
|
||||
});
|
||||
render(<ActionCardBlock block={block} />);
|
||||
const btn = screen.getByText('Secondary');
|
||||
expect(btn.className).toContain('bg-secondary-100');
|
||||
});
|
||||
|
||||
it('uses default label "Aktion" when action label is empty', () => {
|
||||
const block = makeBlock('b1', 'action_card', {
|
||||
title: 'Title',
|
||||
actions: [{ label: '', action: 'dismiss' }],
|
||||
});
|
||||
render(<ActionCardBlock block={block} />);
|
||||
expect(screen.getByText('Aktion')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no action buttons when actions array is empty', () => {
|
||||
const block = makeBlock('b1', 'action_card', { title: 'Title', body: 'Body', actions: [] });
|
||||
render(<ActionCardBlock block={block} />);
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── ContactCardBlock Tests ───
|
||||
|
||||
describe('ContactCardBlock', () => {
|
||||
it('renders contact name', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { contact_id: '123', name: 'Jane Smith' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses default name when name is not provided', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { contact_id: '123' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
expect(screen.getByText('Unbekannter Kontakt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders link to contact page with contact_id', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { contact_id: 'abc-456', name: 'Test User' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
const link = screen.getByText('Test User').closest('a');
|
||||
expect(link).toHaveAttribute('href', '/contacts/abc-456');
|
||||
});
|
||||
|
||||
it('renders link with href="#" when contact_id is missing', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { name: 'No ID User' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
const link = screen.getByText('No ID User').closest('a');
|
||||
expect(link).toHaveAttribute('href', '#');
|
||||
});
|
||||
|
||||
it('generates initials from name', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { contact_id: '1', name: 'John Doe' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
expect(screen.getByText('JD')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('generates initials from single name', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { contact_id: '1', name: 'Max' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
expect(screen.getByText('M')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Kontakt anzeigen" subtitle', () => {
|
||||
const block = makeBlock('b1', 'contact_card', { contact_id: '1', name: 'Test' });
|
||||
render(<ContactCardBlock block={block} />);
|
||||
expect(screen.getByText('Kontakt anzeigen')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── MiniAppBlock Tests ───
|
||||
|
||||
describe('MiniAppBlock', () => {
|
||||
it('renders app_id in label', () => {
|
||||
const block = makeBlock('b1', 'miniapp', { app_id: 'my-mini-app' });
|
||||
render(<MiniAppBlock block={block} />);
|
||||
expect(screen.getByText(/Mini-App: my-mini-app/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses default app_id when not provided', () => {
|
||||
const block = makeBlock('b1', 'miniapp', {});
|
||||
render(<MiniAppBlock block={block} />);
|
||||
expect(screen.getByText(/Mini-App: Unbekannt/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders info message about mini-apps', () => {
|
||||
const block = makeBlock('b1', 'miniapp', { app_id: 'test' });
|
||||
render(<MiniAppBlock block={block} />);
|
||||
expect(screen.getByText(/Mini-Apps werden in Zukunft/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows config details when config is provided', () => {
|
||||
const block = makeBlock('b1', 'miniapp', {
|
||||
app_id: 'test',
|
||||
config: { key: 'value', nested: { data: 123 } },
|
||||
});
|
||||
render(<MiniAppBlock block={block} />);
|
||||
expect(screen.getByText('Konfiguration')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show config details when config is empty', () => {
|
||||
const block = makeBlock('b1', 'miniapp', { app_id: 'test', config: {} });
|
||||
render(<MiniAppBlock block={block} />);
|
||||
expect(screen.queryByText('Konfiguration')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show config details when config is not an object', () => {
|
||||
const block = makeBlock('b1', 'miniapp', { app_id: 'test', config: 'not-an-object' });
|
||||
render(<MiniAppBlock block={block} />);
|
||||
expect(screen.queryByText('Konfiguration')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user