Skip to content

Commit c73c374

Browse files
authored
[LG-5100] feat(code-editor): add test harnesses (#3103)
* refactor(code-editor): reorganize testing utilities and enhance documentation - Moved testing utilities for the CodeEditor component to a new file, improving organization and accessibility. - Updated import paths in test files to reflect the new location of testing utilities. - Enhanced README documentation to clarify the usage of new testing utilities and examples for better guidance on testing interactions with the CodeEditor and Panel components. * refactor(code-editor): reorganize panel testing utilities and update import paths - Moved panel-specific testing utilities to a new file, enhancing organization and maintainability. - Updated import paths in the Panel test file to reflect the new location of the testing utilities. - Improved clarity and accessibility of testing functions for the Panel component. * feat(code-editor): move testing utilities for CodeMirror extensions * feat(code-editor): add test utilities for CodeEditor and Panel components - Introduced `getTestUtils` to provide a comprehensive set of testing utilities for the CodeEditor and Panel components, enhancing the testing framework. - Updated README documentation to include details on the new test utilities, including usage examples and descriptions of available methods. - Refactored CodeEditor and Panel components to utilize `data-lgid` attributes for improved testability and consistency in querying elements during tests. - Enhanced TypeScript definitions to support the new testing utilities and ensure type safety across the testing framework. * feat(code-editor): enhance testing utilities and documentation for CodeEditor - Added comprehensive testing utilities for the CodeEditor and Panel components, improving the framework for unit and integration tests. - Updated README to include detailed sections on testing environment compatibility, recommended usage, and example test cases. - Refactored CodeEditor to include data attributes for better testability in JSDOM environments. - Introduced new test cases to validate editor functionality, content detection, and panel interactions, ensuring robust testing coverage. * feat(code-editor): enhance testing utilities and documentation for async operations - Updated README to clarify usage patterns for testing CodeEditor, including async methods for content retrieval and loading state management. - Introduced `waitForInitialization` and `waitForLoadingToComplete` methods in `getTestUtils` for improved handling of asynchronous operations in tests. - Refactored `getContent` to return a Promise, ensuring compatibility with async testing scenarios. - Enhanced test cases to demonstrate the new async utilities, improving robustness and reliability in testing editor functionality. * refactor(code-editor): remove unused testing utilities for CodeEditor - Deleted `getAllBySelector` and `queryAllBySelector` functions from CodeEditor test utilities to streamline the codebase and improve maintainability. - Updated the export structure to reflect the removal of these functions, ensuring clarity in available testing utilities. * refactor(code-editor): remove compatibility documentation from testing utilities - Deleted compatibility documentation for testing environments from `getTestUtils.ts` to streamline the code and focus on essential utility functions. - This change enhances clarity and reduces clutter in the testing utilities file. * feat(code-editor): enhance testing environment with additional mocks and data attributes - Added mocks for MutationObserver, ResizeObserver, and IntersectionObserver to improve compatibility with CodeMirror in tests. - Implemented document.getSelection and createRange mocks to support CodeMirror functionality. - Updated CodeEditor component to set a default value for copyButtonAppearance, enhancing testability. - Introduced data-lgid attributes in test cases to facilitate element selection and improve test reliability. - Enhanced test cases to include asynchronous waits for component rendering, ensuring accurate assertions. * fix(tests): update data-lgid attributes in CodeEditor tests for consistency - Modified data-lgid attributes in test cases to follow a new naming convention, ensuring consistency across the testing framework. - Updated selectors in assertions to match the revised data-lgid structure, enhancing the reliability of test cases. * refactor(Panel): revert inadvertent panel changes on base merge * test(getTestUtils): mock HTMLDialogElement methods and update test descriptions - Added mocks for HTMLDialogElement methods (`showModal`, `close`, `open`) to facilitate testing of the Modal component. - Updated the test suite description for clarity, focusing on `getTestUtils` instead of the broader API documentation. * feat(CodeEditor): integrate lgIds into context and panel components - Added lgIds to the CodeEditor context, allowing child components to inherit custom data-lgid prefixes. - Updated the Panel component to utilize lgIds from the CodeEditor context, enhancing consistency in data attribute handling. - Removed direct lgIds retrieval from the Panel, streamlining the component's implementation. * refactor(getTestUtils): Simplify exposed utilities and rewrite tests * feat(Panel.testUtils): integrate lgIds into PanelTestContextConfig and renderPanel function - Added lgIds to the PanelTestContextConfig interface to support custom data-lgid attributes. - Updated the renderPanel function to include lgIds, enhancing the testing capabilities for the Panel component. * refactor(getTestUtils): rename utility functions for consistency - Updated utility function names from `getIsLoading` to `isLoading` and `getIsReadOnly` to `isReadOnly` for improved clarity and consistency. - Adjusted corresponding test cases to reflect the new function names, enhancing readability and maintainability of the test suite. - Renamed `getEditor` to `getEditorElement` for better semantic alignment with its purpose. * refactor(README): streamline CodeEditor testing utilities documentation - Consolidated and clarified the documentation for testing utilities in the CodeEditor README. - Renamed utility functions for consistency, including `getEditor` to `getEditorElement` and updated related examples. - Removed outdated sections on feature detection and async initialization utilities to focus on essential testing patterns. - Enhanced example usage for better clarity and relevance to current utility functions. * refactor(getTestUtils): code review changes * refactor(getTestUtils): code editor not progress bar =) * fix(getTestUtils): fix build
1 parent bc61028 commit c73c374

32 files changed

+1007
-874
lines changed

packages/code-editor/README.md

Lines changed: 55 additions & 510 deletions
Large diffs are not rendered by default.

packages/code-editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@leafygreen-ui/tooltip": "workspace:^",
6262
"@leafygreen-ui/typography": "^22.1.0",
6363
"@lezer/highlight": "^1.2.1",
64+
"@lg-tools/test-harnesses": "^0.3.4",
6465
"@replit/codemirror-lang-csharp": "^6.2.0",
6566
"@uiw/codemirror-extensions-hyper-link": "^4.23.12",
6667
"@wasm-fmt/clang-format": "^20.1.7",

packages/code-editor/src/CodeEditor/CodeEditor.spec.tsx

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,72 @@ import { EditorState } from '@codemirror/state';
33
import { act, waitFor } from '@testing-library/react';
44
import userEvent from '@testing-library/user-event';
55

6-
import { renderCodeEditor } from '../testing/testUtils';
6+
import { getTestUtils } from '../testing';
77

88
import { LanguageName } from './hooks/extensions/useLanguageExtension';
9+
import { renderCodeEditor } from './CodeEditor.testUtils';
910
import { CopyButtonAppearance } from './CodeEditor.types';
1011
import { CodeEditorSelectors } from '.';
1112

13+
// Enhanced MutationObserver mock for CodeMirror compatibility
1214
global.MutationObserver = jest.fn().mockImplementation(() => ({
1315
observe: jest.fn(),
1416
unobserve: jest.fn(),
1517
disconnect: jest.fn(),
1618
takeRecords: jest.fn().mockReturnValue([]),
1719
}));
1820

21+
// Mock ResizeObserver which is used by CodeMirror
22+
global.ResizeObserver = jest.fn().mockImplementation(() => ({
23+
observe: jest.fn(),
24+
unobserve: jest.fn(),
25+
disconnect: jest.fn(),
26+
}));
27+
28+
// Mock IntersectionObserver which may be used by CodeMirror
29+
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
30+
observe: jest.fn(),
31+
unobserve: jest.fn(),
32+
disconnect: jest.fn(),
33+
root: null,
34+
rootMargin: '',
35+
thresholds: [],
36+
}));
37+
38+
// Mock document.getSelection for CodeMirror
39+
if (!global.document.getSelection) {
40+
global.document.getSelection = jest.fn().mockReturnValue({
41+
rangeCount: 0,
42+
getRangeAt: jest.fn(),
43+
removeAllRanges: jest.fn(),
44+
addRange: jest.fn(),
45+
toString: jest.fn().mockReturnValue(''),
46+
});
47+
}
48+
49+
// Mock createRange for CodeMirror
50+
if (!global.document.createRange) {
51+
global.document.createRange = jest.fn().mockReturnValue({
52+
setStart: jest.fn(),
53+
setEnd: jest.fn(),
54+
collapse: jest.fn(),
55+
selectNodeContents: jest.fn(),
56+
insertNode: jest.fn(),
57+
surroundContents: jest.fn(),
58+
cloneRange: jest.fn(),
59+
detach: jest.fn(),
60+
getClientRects: jest.fn().mockReturnValue([]),
61+
getBoundingClientRect: jest.fn().mockReturnValue({
62+
top: 0,
63+
left: 0,
64+
bottom: 0,
65+
right: 0,
66+
width: 0,
67+
height: 0,
68+
}),
69+
});
70+
}
71+
1972
// Mock console methods to suppress expected warnings
2073
const originalConsoleWarn = console.warn;
2174
const originalConsoleError = console.error;
@@ -318,27 +371,25 @@ describe('packages/code-editor', () => {
318371
});
319372

320373
test('renders copy button when copyButtonAppearance is "hover"', async () => {
321-
const { container, editor } = renderCodeEditor({
374+
const lgId = 'lg-test-copy-hover';
375+
const { editor } = renderCodeEditor({
322376
copyButtonAppearance: CopyButtonAppearance.Hover,
377+
'data-lgid': lgId,
323378
});
324-
325379
await editor.waitForEditorView();
326-
327-
expect(
328-
container.querySelector(CodeEditorSelectors.CopyButton),
329-
).toBeInTheDocument();
380+
const utils = getTestUtils(lgId);
381+
expect(utils.getCopyButton()).toBeInTheDocument();
330382
});
331383

332384
test('renders copy button when copyButtonAppearance is "persist"', async () => {
333-
const { container, editor } = renderCodeEditor({
385+
const lgId = 'lg-test-copy-persist';
386+
const { editor } = renderCodeEditor({
334387
copyButtonAppearance: CopyButtonAppearance.Persist,
388+
'data-lgid': lgId,
335389
});
336-
337390
await editor.waitForEditorView();
338-
339-
expect(
340-
container.querySelector(CodeEditorSelectors.CopyButton),
341-
).toBeInTheDocument();
391+
const utils = getTestUtils(lgId);
392+
expect(utils.getCopyButton()).toBeInTheDocument();
342393
});
343394

344395
test('does not render copy button when copyButtonAppearance is "none"', async () => {

packages/code-editor/src/testing/testUtils.tsx renamed to packages/code-editor/src/CodeEditor/CodeEditor.testUtils.tsx

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
CodeEditorProps,
99
CodeEditorSelectors,
1010
CodeMirrorView,
11-
} from '..';
11+
} from '.';
1212

1313
let editorViewInstance: CodeMirrorView | null = null;
1414
let getEditorViewFn: (() => CodeMirrorView | null) | null = null;
@@ -96,43 +96,6 @@ function getBySelector(
9696
return elements[0];
9797
}
9898

99-
/**
100-
* Returns all elements matching a specific CodeEditor selector
101-
* @param selector - The CSS selector to look for in the editor
102-
* @param options - Optional filtering options
103-
* @param options.text - Optional text content filter
104-
* @returns All DOM elements matching the selector and optional text filter
105-
* @throws Error if no elements are found
106-
*/
107-
function getAllBySelector(
108-
selector: CodeEditorSelectors,
109-
options?: { text?: string },
110-
): Array<Element> {
111-
const view = ensureEditorView();
112-
const elements = view.dom.querySelectorAll(selector);
113-
114-
if (!elements || elements.length === 0) {
115-
throw new Error(`No elements with selector "${selector}" found`);
116-
}
117-
118-
// If text filter is provided, return only elements containing the text
119-
if (options?.text) {
120-
const matchingElements = Array.from(elements).filter(element =>
121-
element.textContent?.includes(options.text as string),
122-
);
123-
124-
if (!matchingElements || matchingElements.length === 0) {
125-
throw new Error(
126-
`No elements with selector "${selector}" and text "${options.text}" found`,
127-
);
128-
}
129-
130-
return matchingElements;
131-
}
132-
133-
return Array.from(elements);
134-
}
135-
13699
/**
137100
* Returns the first element matching a specific CodeEditor selector or null if not found
138101
* @param selector - The CSS selector to look for in the editor
@@ -174,40 +137,6 @@ function queryBySelector(
174137
return elements[0];
175138
}
176139

177-
/**
178-
* Returns all elements matching a specific CodeEditor selector or null if none are found
179-
* @param selector - The CSS selector to look for in the editor
180-
* @param options - Optional filtering options
181-
* @param options.text - Optional text content filter
182-
* @returns All DOM elements matching the selector and optional text filter, or null if none found
183-
*/
184-
function queryAllBySelector(
185-
selector: CodeEditorSelectors,
186-
options?: { text?: string },
187-
): Array<Element> | null {
188-
const view = ensureEditorView();
189-
const elements = view.dom.querySelectorAll(selector);
190-
191-
if (!elements || elements.length === 0) {
192-
return null;
193-
}
194-
195-
// If text filter is provided, return only elements containing the text
196-
if (options?.text) {
197-
const matchingElements = Array.from(elements).filter(element =>
198-
element.textContent?.includes(options.text as string),
199-
);
200-
201-
if (!matchingElements || matchingElements.length === 0) {
202-
return null;
203-
}
204-
205-
return matchingElements;
206-
}
207-
208-
return Array.from(elements);
209-
}
210-
211140
/**
212141
* Checks if the editor is in read-only mode
213142
* @returns Boolean indicating whether the editor is in read-only mode
@@ -317,9 +246,7 @@ function redo(): boolean {
317246

318247
export const editor = {
319248
getBySelector,
320-
getAllBySelector,
321249
queryBySelector,
322-
queryAllBySelector,
323250
isLineWrappingEnabled,
324251
isReadOnly,
325252
getIndentUnit,

packages/code-editor/src/CodeEditor/CodeEditor.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Body } from '@leafygreen-ui/typography';
1313

1414
import { CodeEditorCopyButton } from '../CodeEditorCopyButton';
1515
import { CopyButtonVariant } from '../CodeEditorCopyButton/CodeEditorCopyButton.types';
16+
import { getLgIds } from '../utils';
1617

1718
import { useFormattingModuleLoaders } from './hooks/formatting/useFormattingModuleLoaders';
1819
import {
@@ -25,7 +26,6 @@ import {
2526
CodeEditorHandle,
2627
type CodeEditorProps,
2728
CopyButtonAppearance,
28-
CopyButtonLgId,
2929
type HTMLElementWithCodeMirror,
3030
} from './CodeEditor.types';
3131
import { CodeEditorProvider } from './CodeEditorContext';
@@ -42,7 +42,8 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
4242
const {
4343
baseFontSize: baseFontSizeProp,
4444
className,
45-
copyButtonAppearance,
45+
copyButtonAppearance = CopyButtonAppearance.Hover,
46+
'data-lgid': dataLgId,
4647
darkMode: darkModeProp,
4748
defaultValue,
4849
enableClickableUrls,
@@ -70,6 +71,8 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
7071
...rest
7172
} = props;
7273

74+
const lgIds = getLgIds(dataLgId);
75+
7376
const { theme } = useDarkMode(darkModeProp);
7477
const [controlledValue, setControlledValue] = useState(value || '');
7578
const isControlled = value !== undefined;
@@ -343,6 +346,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
343346
undo: handleUndo,
344347
redo: handleRedo,
345348
downloadContent: handleDownloadContent,
349+
lgIds,
346350
};
347351

348352
return (
@@ -358,6 +362,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
358362
className,
359363
copyButtonAppearance,
360364
})}
365+
data-lgid={lgIds.root}
361366
{...rest}
362367
>
363368
{panel && (
@@ -371,7 +376,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
371376
className={getCopyButtonStyles(copyButtonAppearance)}
372377
variant={CopyButtonVariant.Button}
373378
disabled={isLoadingProp || isLoadingCoreModules}
374-
data-lgid={CopyButtonLgId}
379+
data-lgid={lgIds.copyButton}
375380
/>
376381
)}
377382
{(isLoadingProp ||
@@ -387,6 +392,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
387392
minHeight,
388393
maxHeight,
389394
})}
395+
data-lgid={lgIds.loader}
390396
>
391397
<Body className={getLoadingTextStyles(theme)}>
392398
Loading code editor...

0 commit comments

Comments
 (0)