Yuki Takei 2 дней назад
Родитель
Сommit
0e395a59d4

+ 133 - 0
apps/app/src/features/mermaid/components/MermaidViewer.spec.tsx

@@ -0,0 +1,133 @@
+import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
+import { act, render } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+// Mock mermaid to control rendering behavior in tests
+vi.mock('mermaid', () => ({
+  default: {
+    initialize: vi.fn(),
+    render: vi.fn(),
+  },
+}));
+
+// Mock useNextThemes to provide a stable isDarkMode value
+vi.mock('~/stores-universal/use-next-themes', () => ({
+  useNextThemes: vi.fn(() => ({ isDarkMode: false })),
+}));
+
+// uuid mock: return predictable IDs
+vi.mock('uuid', () => ({
+  v7: vi.fn(() => 'test-uuid'),
+}));
+
+import mermaid from 'mermaid';
+
+import { MermaidViewer } from './MermaidViewer';
+
+const mockRender = vi.mocked(mermaid.render);
+
+describe('MermaidViewer', () => {
+  beforeEach(() => {
+    vi.useFakeTimers();
+  });
+
+  afterEach(() => {
+    vi.useRealTimers();
+    vi.clearAllMocks();
+    document.body.innerHTML = '';
+  });
+
+  it('should render container with rendering-status attribute set to "true" initially', () => {
+    mockRender.mockReturnValue(new Promise(() => {})); // pending — never resolves
+
+    const { container } = render(<MermaidViewer value="graph TD; A-->B" />);
+    const div = container.firstElementChild as HTMLElement;
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+  });
+
+  it('should set rendering-status attribute to "false" via requestAnimationFrame after successful render', async () => {
+    mockRender.mockResolvedValue({
+      svg: '<svg>test</svg>',
+      bindFunctions: undefined as never,
+      diagramType: 'flowchart',
+    });
+
+    const { container } = render(<MermaidViewer value="graph TD; A-->B" />);
+    const div = container.firstElementChild as HTMLElement;
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+
+    // Flush the async mermaid.render() call
+    await act(async () => {
+      await Promise.resolve();
+    });
+
+    // Attribute should still be "true" — waiting for rAF
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+
+    // Flush the requestAnimationFrame
+    await act(() => {
+      vi.runAllTimers();
+    });
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('false');
+  });
+
+  it('should set rendering-status attribute to "false" immediately on error (no rAF delay)', async () => {
+    mockRender.mockRejectedValue(new Error('Mermaid render failed'));
+
+    const { container } = render(
+      <MermaidViewer value="invalid mermaid syntax ###" />,
+    );
+    const div = container.firstElementChild as HTMLElement;
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+
+    // Flush the rejected promise
+    await act(async () => {
+      await Promise.resolve();
+    });
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('false');
+  });
+
+  it('should cancel pending requestAnimationFrame when component unmounts during render', async () => {
+    let resolveRender!: (value: {
+      svg: string;
+      bindFunctions: never;
+      diagramType: string;
+    }) => void;
+    mockRender.mockReturnValue(
+      new Promise<{ svg: string; bindFunctions: never; diagramType: string }>(
+        (resolve) => {
+          resolveRender = resolve;
+        },
+      ),
+    );
+
+    const cancelAnimationFrameSpy = vi.spyOn(window, 'cancelAnimationFrame');
+
+    const { unmount } = render(<MermaidViewer value="graph TD; A-->B" />);
+
+    // Resolve render but don't flush rAF yet
+    await act(async () => {
+      resolveRender({
+        svg: '<svg>test</svg>',
+        bindFunctions: undefined as never,
+        diagramType: 'flowchart',
+      });
+      await Promise.resolve();
+    });
+
+    // Unmount before rAF fires
+    unmount();
+
+    // Verify cancelAnimationFrame was called
+    expect(cancelAnimationFrameSpy).toHaveBeenCalled();
+
+    // Advancing timers should not cause errors (no DOM update on unmounted component)
+    await act(() => {
+      vi.runAllTimers();
+    });
+  });
+});

+ 53 - 0
apps/app/src/features/plantuml/components/PlantUmlViewer.spec.tsx

@@ -0,0 +1,53 @@
+import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts';
+import { fireEvent, render } from '@testing-library/react';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { PlantUmlViewer } from './PlantUmlViewer';
+
+describe('PlantUmlViewer', () => {
+  afterEach(() => {
+    document.body.innerHTML = '';
+  });
+
+  it('should render with rendering-status attribute set to "true" initially', () => {
+    const { container } = render(
+      <PlantUmlViewer src="http://example.com/plantuml.png" />,
+    );
+    const div = container.firstElementChild as HTMLElement;
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+  });
+
+  it('should set rendering-status attribute to "false" when image loads', () => {
+    const { container } = render(
+      <PlantUmlViewer src="http://example.com/plantuml.png" />,
+    );
+    const div = container.firstElementChild as HTMLElement;
+    const img = div.querySelector('img') as HTMLImageElement;
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+
+    fireEvent.load(img);
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('false');
+  });
+
+  it('should set rendering-status attribute to "false" when image fails to load', () => {
+    const { container } = render(
+      <PlantUmlViewer src="http://example.com/nonexistent.png" />,
+    );
+    const div = container.firstElementChild as HTMLElement;
+    const img = div.querySelector('img') as HTMLImageElement;
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('true');
+
+    fireEvent.error(img);
+
+    expect(div.getAttribute(GROWI_IS_CONTENT_RENDERING_ATTR)).toBe('false');
+  });
+
+  it('should render an img element with the provided src', () => {
+    render(<PlantUmlViewer src="http://example.com/diagram.png" />);
+    const img = document.querySelector('img') as HTMLImageElement;
+    expect(img.src).toBe('http://example.com/diagram.png');
+  });
+});