document.test.tsx6.3 KBView on GitHub
import React, { createRef } from 'react';
import { act, render, waitFor } from '@testing-library/react';

import { Document, type DocumentHandle } from '@/modules/documents/document';

const mockGetDoc = jest.fn();
const mockUseDocEvents = jest.fn();
const mockAcquireProvider = jest.fn();
const mockReleaseProvider = jest.fn();
const mockGetProvider = jest.fn();
const mockBase64ToUint8Array = jest.fn((...args: unknown[]) => new Uint8Array([String(args[0]).length]));

jest.mock('@/providers/query-provider', () => ({
  trpcClient: {
    documents: {
      getDoc: {
        query: (...args: unknown[]) => mockGetDoc(...args),
      },
    },
  },
  useTRPCClient: () => ({
    documents: {
      getDoc: {
        query: (...args: unknown[]) => mockGetDoc(...args),
      },
    },
  }),
}));

jest.mock('@/modules/documents/yjs', () => ({
  EnsureNodeIds: {},
  createTrpcApplyUpdateClient: jest.fn(() => ({ applyUpdate: jest.fn() })),
  acquireProvider: (...args: unknown[]) => mockAcquireProvider(...args),
  releaseProvider: (...args: unknown[]) => mockReleaseProvider(...args),
  getProvider: (...args: unknown[]) => mockGetProvider(...args),
  base64ToUint8Array: (...args: unknown[]) => mockBase64ToUint8Array(...args),
  useDocEvents: (...args: unknown[]) => mockUseDocEvents(...args),
}));

jest.mock('@/components/markdown-editor', () => {
  const ReactModule = require('react');
  const MarkdownEditor = ReactModule.forwardRef(function MockMarkdownEditor(
    props: {
      onEditorCreated?: (editor: unknown) => void;
      children?: React.ReactNode;
    },
    ref: React.Ref<unknown>,
  ) {
    ReactModule.useImperativeHandle(ref, () => ({
      editor: { id: 'editor-1' },
      getMarkdown: () => 'mock-markdown',
    }));
    ReactModule.useEffect(() => {
      props.onEditorCreated?.({ id: 'editor-1' });
    }, [props.onEditorCreated]);
    return <div data-testid="mock-markdown-editor">{props.children}</div>;
  });

  return {
    MarkdownEditor,
  };
});

type MockProvider = {
  idbReady: Promise<void>;
  ydoc: { getXmlFragment: jest.Mock };
  applyInitialState: jest.Mock;
  forceFlush: jest.Mock<Promise<void>, []>;
};

function makeProvider(fragmentLength: number): MockProvider {
  return {
    idbReady: Promise.resolve(),
    ydoc: {
      getXmlFragment: jest.fn(() => ({ length: fragmentLength })),
    },
    applyInitialState: jest.fn(),
    forceFlush: jest.fn(async () => {}),
  };
}

describe('<Document /> lifecycle', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('always seeds from server, even when IDB is warm', async () => {
    // A warm IDB only proves the client has every edit it has ever
    // observed — agents/other tabs may have written while we weren't
    // watching, so we must merge in the server state on every mount.
    const provider = makeProvider(1);
    mockAcquireProvider.mockReturnValue(provider);
    mockGetProvider.mockReturnValue(provider);
    mockGetDoc.mockResolvedValue({ contentYjs: 'warm-seed' });

    render(<Document documentId="doc-warm" />);

    await waitFor(() => expect(mockGetDoc).toHaveBeenCalledWith({ documentId: 'doc-warm' }));
    await waitFor(() => expect(mockBase64ToUint8Array).toHaveBeenCalledWith('warm-seed'));
    expect(provider.applyInitialState).toHaveBeenCalledWith(new Uint8Array([9]));
    expect(mockUseDocEvents).toHaveBeenCalledWith('doc-warm');
  });

  it('fetches and seeds content on cold open', async () => {
    const provider = makeProvider(0);
    mockAcquireProvider.mockReturnValue(provider);
    mockGetProvider.mockReturnValue(provider);
    mockGetDoc.mockResolvedValue({ contentYjs: 'seed-bytes' });

    render(<Document documentId="doc-cold" />);

    await waitFor(() => expect(mockGetDoc).toHaveBeenCalledWith({ documentId: 'doc-cold' }));
    await waitFor(() => expect(mockBase64ToUint8Array).toHaveBeenCalledWith('seed-bytes'));
    expect(provider.applyInitialState).toHaveBeenCalledWith(new Uint8Array([10]));
  });

  it('forwards targetUserId to getDoc so an admin viewing a teammate seeds their copy', async () => {
    // Regression: AgentInstructionsSection passed targetUserId to itself but never
    // down into Document, so an org admin viewing a teammate's agent got a getDoc
    // call scoped to the admin's OWN session — the server rejected it as a
    // cross-user read and the editor rendered permanently blank.
    const provider = makeProvider(1);
    mockAcquireProvider.mockReturnValue(provider);
    mockGetProvider.mockReturnValue(provider);
    mockGetDoc.mockResolvedValue({ contentYjs: 'teammate-seed' });

    render(<Document documentId="doc-teammate" targetUserId="teammate-1" />);

    await waitFor(() =>
      expect(mockGetDoc).toHaveBeenCalledWith({
        documentId: 'doc-teammate',
        targetUserId: 'teammate-1',
      }),
    );
  });

  it('refreshFromServer force-flushes and re-seeds the provider', async () => {
    const provider = makeProvider(1);
    mockAcquireProvider.mockReturnValue(provider);
    mockGetProvider.mockReturnValue(provider);
    mockGetDoc.mockResolvedValue({ contentYjs: 'fresh-seed' });

    const ref = createRef<DocumentHandle>();
    render(<Document ref={ref} documentId="doc-refresh" />);

    await waitFor(() => expect(ref.current?.documentId).toBe('doc-refresh'));

    await act(async () => {
      await ref.current?.refreshFromServer();
    });

    expect(provider.forceFlush).toHaveBeenCalledTimes(1);
    expect(mockGetDoc).toHaveBeenCalledWith({ documentId: 'doc-refresh' });
    expect(mockBase64ToUint8Array).toHaveBeenCalledWith('fresh-seed');
    expect(provider.applyInitialState).toHaveBeenCalledWith(new Uint8Array([10]));
  });

  it('refreshFromServer also forwards targetUserId', async () => {
    const provider = makeProvider(1);
    mockAcquireProvider.mockReturnValue(provider);
    mockGetProvider.mockReturnValue(provider);
    mockGetDoc.mockResolvedValue({ contentYjs: 'fresh-teammate-seed' });

    const ref = createRef<DocumentHandle>();
    render(<Document ref={ref} documentId="doc-refresh-teammate" targetUserId="teammate-1" />);

    await waitFor(() => expect(ref.current?.documentId).toBe('doc-refresh-teammate'));

    await act(async () => {
      await ref.current?.refreshFromServer();
    });

    expect(mockGetDoc).toHaveBeenCalledWith({
      documentId: 'doc-refresh-teammate',
      targetUserId: 'teammate-1',
    });
  });
});