/** * @license * Copyright 2025 AionUi (aionui.com) * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import MarkdownView from '@/renderer/components/Markdown'; const copyTextMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); vi.mock('@/renderer/components/Markdown/ShadowView', () => ({ __esModule: true, default: ({ children }: { children?: React.ReactNode }) =>
{children}
, })); vi.mock('@/renderer/components/Markdown/CodeBlock', () => ({ __esModule: true, default: ({ children }: { children?: React.ReactNode }) => {children}, })); vi.mock('@/renderer/components/media/LocalImageView', () => ({ __esModule: true, default: ({ src, alt }: { src: string; alt: string }) => {alt}, })); vi.mock('@/renderer/utils/chat/latexDelimiters', () => ({ convertLatexDelimiters: (text: string) => text, })); vi.mock('@/renderer/utils/platform', () => ({ openExternalUrl: vi.fn(), })); vi.mock('@/renderer/utils/ui/clipboard', () => ({ copyText: copyTextMock, })); vi.mock('@arco-design/web-react', () => ({ Button: ({ children, icon, ...props }: React.ButtonHTMLAttributes & { icon?: React.ReactNode }) => ( ), Message: { error: vi.fn(), }, Tooltip: ({ children }: { children?: React.ReactNode }) => <>{children}, })); vi.mock('@icon-park/react', () => ({ Copy: () => , })); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: { defaultValue?: string }) => options?.defaultValue ?? key, }), })); describe('MarkdownView local file links', () => { beforeEach(() => { copyTextMock.mockClear(); }); it('renders local file links as app controls instead of browser anchors', () => { const onLocalFileLink = vi.fn(); render( {'[report.xlsx](/C:/Users/Administrator/AppData/Roaming/AionUi/report.xlsx)'} ); expect(screen.queryByRole('link')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'report.xlsx' })); expect(onLocalFileLink).toHaveBeenCalledWith( 'C:/Users/Administrator/AppData/Roaming/AionUi/report.xlsx', expect.objectContaining({ filePath: 'C:/Users/Administrator/AppData/Roaming/AionUi/report.xlsx', }) ); fireEvent.click(screen.getByRole('button', { name: 'Copy' })); expect(copyTextMock).toHaveBeenCalledWith('C:/Users/Administrator/AppData/Roaming/AionUi/report.xlsx'); }); it('renders line references as file chips and copies the full reference', () => { const onLocalFileLink = vi.fn(); render( {'[2026-06-19.log](C:/Users/Administrator/AppData/Roaming/AionUi/logs/2026-06-19.log:1421)'} ); const fileButton = screen.getByRole('button', { name: /2026-06-19\.log\s+L1421/ }); fireEvent.click(fileButton); expect(onLocalFileLink).toHaveBeenCalledWith( 'C:/Users/Administrator/AppData/Roaming/AionUi/logs/2026-06-19.log', expect.objectContaining({ filePath: 'C:/Users/Administrator/AppData/Roaming/AionUi/logs/2026-06-19.log', rawReference: 'C:/Users/Administrator/AppData/Roaming/AionUi/logs/2026-06-19.log:1421', line: 1421, }) ); fireEvent.click(screen.getByRole('button', { name: 'Copy' })); expect(copyTextMock).toHaveBeenCalledWith('C:/Users/Administrator/AppData/Roaming/AionUi/logs/2026-06-19.log:1421'); }); it('renders line and column references as file chips and copies the full reference', () => { const onLocalFileLink = vi.fn(); render( {'[app.log](C:/Users/Administrator/AppData/Roaming/AionUi/logs/app.log:1421:7)'} ); const fileButton = screen.getByRole('button', { name: /app\.log\s+L1421:7/ }); fireEvent.click(fileButton); expect(onLocalFileLink).toHaveBeenCalledWith( 'C:/Users/Administrator/AppData/Roaming/AionUi/logs/app.log', expect.objectContaining({ filePath: 'C:/Users/Administrator/AppData/Roaming/AionUi/logs/app.log', rawReference: 'C:/Users/Administrator/AppData/Roaming/AionUi/logs/app.log:1421:7', line: 1421, column: 7, }) ); fireEvent.click(screen.getByRole('button', { name: 'Copy' })); expect(copyTextMock).toHaveBeenCalledWith('C:/Users/Administrator/AppData/Roaming/AionUi/logs/app.log:1421:7'); }); it('renders hash range references as file chips and copies normalized local references', () => { const onLocalFileLink = vi.fn(); render( {'[user.js 1-260行](/Users/demo/project/user.js#L1-L260)'} ); expect(screen.queryByRole('link', { name: /user\.js/ })).not.toBeInTheDocument(); const fileButton = screen.getByRole('button', { name: /user\.js 1-260行\s+L1-L260/ }); fireEvent.click(fileButton); expect(onLocalFileLink).toHaveBeenCalledWith( '/Users/demo/project/user.js', expect.objectContaining({ filePath: '/Users/demo/project/user.js', rawReference: '/Users/demo/project/user.js#L1-L260', line: 1, endLine: 260, }) ); fireEvent.click(screen.getByRole('button', { name: 'Copy' })); expect(copyTextMock).toHaveBeenCalledWith('/Users/demo/project/user.js#L1-L260'); }); it('does not render a no-op open button when no local file handler is provided', () => { render({'[report.xlsx](/C:/Users/Administrator/AppData/Roaming/AionUi/report.xlsx)'}); expect(screen.queryByRole('link')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'report.xlsx' })).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Copy' })); expect(copyTextMock).toHaveBeenCalledWith('C:/Users/Administrator/AppData/Roaming/AionUi/report.xlsx'); }); it('keeps ordinary http links as browser anchors', () => { render({'[docs](https://aionui.com/docs)'}); const link = screen.getByRole('link', { name: 'docs' }); expect(link).toHaveAttribute('href', 'https://aionui.com/docs'); }); it('keeps http hash links as browser anchors', () => { render({'[docs](https://aionui.com/docs#L10)'}); const link = screen.getByRole('link', { name: 'docs' }); expect(link).toHaveAttribute('href', 'https://aionui.com/docs#L10'); }); it('adds empty alt text to external raw HTML images without alt text', () => { const { container } = render( {''} ); const image = container.querySelector('img'); expect(image).toHaveAttribute('src', 'https://example.com/generated.png'); expect(image).toHaveAttribute('alt', ''); }); });