Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f8a2913
refactor(code-editor): reorganize testing utilities and enhance docum…
tsck Sep 3, 2025
f5aec34
refactor(code-editor): reorganize panel testing utilities and update …
tsck Sep 3, 2025
cd3b75c
feat(code-editor): move testing utilities for CodeMirror extensions
tsck Sep 3, 2025
bfa15f2
feat(code-editor): add test utilities for CodeEditor and Panel compon…
tsck Sep 3, 2025
4f04106
feat(code-editor): enhance testing utilities and documentation for Co…
tsck Sep 3, 2025
4be195c
feat(code-editor): enhance testing utilities and documentation for as…
tsck Sep 3, 2025
364e64a
refactor(code-editor): remove unused testing utilities for CodeEditor
tsck Sep 3, 2025
1024857
refactor(code-editor): remove compatibility documentation from testin…
tsck Sep 3, 2025
ae34a8c
feat(code-editor): enhance testing environment with additional mocks …
tsck Sep 3, 2025
100170e
fix(tests): update data-lgid attributes in CodeEditor tests for consi…
tsck Sep 3, 2025
80882c5
Merge branch 'integration/code-editor' of github.com:mongodb/leafygre…
tsck Sep 4, 2025
f70773f
refactor(Panel): revert inadvertent panel changes on base merge
tsck Sep 4, 2025
ba92455
test(getTestUtils): mock HTMLDialogElement methods and update test de…
tsck Sep 4, 2025
552bf9d
feat(CodeEditor): integrate lgIds into context and panel components
tsck Sep 10, 2025
2be69ad
refactor(getTestUtils): Simplify exposed utilities and rewrite tests
tsck Sep 10, 2025
be3c7a4
feat(Panel.testUtils): integrate lgIds into PanelTestContextConfig an…
tsck Sep 10, 2025
f43df90
refactor(getTestUtils): rename utility functions for consistency
tsck Sep 10, 2025
39dd8de
refactor(README): streamline CodeEditor testing utilities documentation
tsck Sep 10, 2025
d2ec73a
refactor(getTestUtils): code review changes
tsck Sep 12, 2025
d316ddd
refactor(getTestUtils): code editor not progress bar =)
tsck Sep 12, 2025
6184ef2
fix(getTestUtils): fix build
tsck Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
571 changes: 61 additions & 510 deletions packages/code-editor/README.md

Large diffs are not rendered by default.

69 changes: 65 additions & 4 deletions packages/code-editor/src/CodeEditor/CodeEditor.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,70 @@ import { EditorState } from '@codemirror/state';
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { renderCodeEditor } from '../testing/testUtils';

import { LanguageName } from './hooks/extensions/useLanguageExtension';
import { renderCodeEditor } from './CodeEditor.testUtils';
import { CopyButtonAppearance } from './CodeEditor.types';
import { CodeEditorSelectors } from '.';

// Enhanced MutationObserver mock for CodeMirror compatibility
global.MutationObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
takeRecords: jest.fn().mockReturnValue([]),
}));

// Mock ResizeObserver which is used by CodeMirror
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));

// Mock IntersectionObserver which may be used by CodeMirror
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
root: null,
rootMargin: '',
thresholds: [],
}));

// Mock document.getSelection for CodeMirror
if (!global.document.getSelection) {
global.document.getSelection = jest.fn().mockReturnValue({
rangeCount: 0,
getRangeAt: jest.fn(),
removeAllRanges: jest.fn(),
addRange: jest.fn(),
toString: jest.fn().mockReturnValue(''),
});
}

// Mock createRange for CodeMirror
if (!global.document.createRange) {
global.document.createRange = jest.fn().mockReturnValue({
setStart: jest.fn(),
setEnd: jest.fn(),
collapse: jest.fn(),
selectNodeContents: jest.fn(),
insertNode: jest.fn(),
surroundContents: jest.fn(),
cloneRange: jest.fn(),
detach: jest.fn(),
getClientRects: jest.fn().mockReturnValue([]),
getBoundingClientRect: jest.fn().mockReturnValue({
top: 0,
left: 0,
bottom: 0,
right: 0,
width: 0,
height: 0,
}),
});
}

// Mock console methods to suppress expected warnings
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
Expand Down Expand Up @@ -320,24 +371,34 @@ describe('packages/code-editor', () => {
test('renders copy button when copyButtonAppearance is "hover"', async () => {
const { container, editor } = renderCodeEditor({
copyButtonAppearance: CopyButtonAppearance.Hover,
'data-lgid': 'lg-test-copy-hover',
});

await editor.waitForEditorView();

// Wait a bit for copy button to be rendered
await new Promise(resolve => setTimeout(resolve, 100));
Copy link
Collaborator

@shaneeza shaneeza Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: instead of the timeout, would this work?

await waitFor(() => {
  expect(...).toBeInTheDocument();
});


// The copy button selector looks for the specific lgid structure
expect(
container.querySelector(CodeEditorSelectors.CopyButton),
container.querySelector('[data-lgid="lg-test-copy-hover-copy_button"]'),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could you use the test util here? I see it as another way of testing that our utils work.

).toBeInTheDocument();
});

test('renders copy button when copyButtonAppearance is "persist"', async () => {
const { container, editor } = renderCodeEditor({
copyButtonAppearance: CopyButtonAppearance.Persist,
'data-lgid': 'lg-test-copy-persist',
});

await editor.waitForEditorView();

// Wait a bit for copy button to be rendered
await new Promise(resolve => setTimeout(resolve, 100));

// The copy button selector looks for the specific lgid structure
expect(
container.querySelector(CodeEditorSelectors.CopyButton),
container.querySelector('[data-lgid="lg-test-copy-persist-copy_button"]'),
).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
CodeEditorProps,
CodeEditorSelectors,
CodeMirrorView,
} from '..';
} from '.';

let editorViewInstance: CodeMirrorView | null = null;
let getEditorViewFn: (() => CodeMirrorView | null) | null = null;
Expand Down Expand Up @@ -96,43 +96,6 @@ function getBySelector(
return elements[0];
}

/**
* Returns all elements matching a specific CodeEditor selector
* @param selector - The CSS selector to look for in the editor
* @param options - Optional filtering options
* @param options.text - Optional text content filter
* @returns All DOM elements matching the selector and optional text filter
* @throws Error if no elements are found
*/
function getAllBySelector(
selector: CodeEditorSelectors,
options?: { text?: string },
): Array<Element> {
const view = ensureEditorView();
const elements = view.dom.querySelectorAll(selector);

if (!elements || elements.length === 0) {
throw new Error(`No elements with selector "${selector}" found`);
}

// If text filter is provided, return only elements containing the text
if (options?.text) {
const matchingElements = Array.from(elements).filter(element =>
element.textContent?.includes(options.text as string),
);

if (!matchingElements || matchingElements.length === 0) {
throw new Error(
`No elements with selector "${selector}" and text "${options.text}" found`,
);
}

return matchingElements;
}

return Array.from(elements);
}

/**
* Returns the first element matching a specific CodeEditor selector or null if not found
* @param selector - The CSS selector to look for in the editor
Expand Down Expand Up @@ -174,40 +137,6 @@ function queryBySelector(
return elements[0];
}

/**
* Returns all elements matching a specific CodeEditor selector or null if none are found
* @param selector - The CSS selector to look for in the editor
* @param options - Optional filtering options
* @param options.text - Optional text content filter
* @returns All DOM elements matching the selector and optional text filter, or null if none found
*/
function queryAllBySelector(
selector: CodeEditorSelectors,
options?: { text?: string },
): Array<Element> | null {
const view = ensureEditorView();
const elements = view.dom.querySelectorAll(selector);

if (!elements || elements.length === 0) {
return null;
}

// If text filter is provided, return only elements containing the text
if (options?.text) {
const matchingElements = Array.from(elements).filter(element =>
element.textContent?.includes(options.text as string),
);

if (!matchingElements || matchingElements.length === 0) {
return null;
}

return matchingElements;
}

return Array.from(elements);
}

/**
* Checks if the editor is in read-only mode
* @returns Boolean indicating whether the editor is in read-only mode
Expand Down Expand Up @@ -317,9 +246,7 @@ function redo(): boolean {

export const editor = {
getBySelector,
getAllBySelector,
queryBySelector,
queryAllBySelector,
isLineWrappingEnabled,
isReadOnly,
getIndentUnit,
Expand Down
12 changes: 9 additions & 3 deletions packages/code-editor/src/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Body } from '@leafygreen-ui/typography';

import { CodeEditorCopyButton } from '../CodeEditorCopyButton';
import { CopyButtonVariant } from '../CodeEditorCopyButton/CodeEditorCopyButton.types';
import { getLgIds } from '../utils';

import { useFormattingModuleLoaders } from './hooks/formatting/useFormattingModuleLoaders';
import {
Expand All @@ -25,7 +26,6 @@ import {
CodeEditorHandle,
type CodeEditorProps,
CopyButtonAppearance,
CopyButtonLgId,
type HTMLElementWithCodeMirror,
} from './CodeEditor.types';
import { CodeEditorProvider } from './CodeEditorContext';
Expand All @@ -42,7 +42,8 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
const {
baseFontSize: baseFontSizeProp,
className,
copyButtonAppearance,
copyButtonAppearance = CopyButtonAppearance.Hover,
'data-lgid': dataLgId,
darkMode: darkModeProp,
defaultValue,
enableClickableUrls,
Expand Down Expand Up @@ -70,6 +71,8 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
...rest
} = props;

const lgIds = getLgIds(dataLgId);

const { theme } = useDarkMode(darkModeProp);
const [controlledValue, setControlledValue] = useState(value || '');
const isControlled = value !== undefined;
Expand Down Expand Up @@ -343,6 +346,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
undo: handleUndo,
redo: handleRedo,
downloadContent: handleDownloadContent,
lgIds,
};

return (
Expand All @@ -358,6 +362,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
className,
copyButtonAppearance,
})}
data-lgid={lgIds.root}
{...rest}
>
{panel && (
Expand All @@ -371,7 +376,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
className={getCopyButtonStyles(copyButtonAppearance)}
variant={CopyButtonVariant.Button}
disabled={isLoadingProp || isLoadingCoreModules}
data-lgid={CopyButtonLgId}
data-lgid={lgIds.copyButton}
/>
)}
{(isLoadingProp ||
Expand All @@ -387,6 +392,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
minHeight,
maxHeight,
})}
data-lgid={lgIds.loader}
>
<Body className={getLoadingTextStyles(theme)}>
Loading code editor...
Expand Down
Loading
Loading