Enzyme to RTL codemod
This package automates the conversion of Jest tests from Enzyme to React Testing Library (RTL). It is designed to be used with your own large language model (LLM) and your implementation for programmatically making API requests to retrieve LLM responses based on the prompts generated by this tool.
Note
In response to numerous requests from external developers, we are open-sourcing a version of our Slack-built tool for converting Enzyme tests to React Testing Library (RTL). While this tool is not a complete solution for all use cases, it serves as a starting point for automating the migration process. With over 1.5 million Enzyme downloads from npm (as of September 2024), our goal is to ease this transition, save time, and demonstrate a practical application of LLM integration in developer workflows.
We hope this tool proves useful. We encourage contributions to this repository or forking it to make necessary adjustments. We will provide limited support for reviewing critical bug fixes.
Requirements
- Jest: This package relies on your host project's Jest binary and configuration. Refer to the API/Usage section for more details.
- Enzyme: The package depends on the version of Enzyme used in your host project.
- Jscodeshift: Included as part of this package.
- LLM Support:
- You need to integrate an LLM to process the generated prompts.
- The LLM implementation is your responsibility, using the model available to you.
- LLM is instructed to return converted code in certain xml tags in the prompt, which should make it compatible with any LLM model
How to install (in progress)
npm install @slack/enzyme-to-rtl-codemod
or
yarn add @slack/enzyme-to-rtl-codemod
API/Usage
There are three ways to use this package:
- Using a single workflow function
convertTestFiles({...})
- Using many individual functions with more control over the flow
- CLI (currently not implemented)
1. Running the conversion flow for one or more files with one method using the convertTestFiles()
function:
import {
convertTestFiles,
LLMCallFunction,
} from '@slack/enzyme-to-rtl-codemod';
const callLLMFunctionExample: LLMCallFunction = async (
prompt: string,
): Promise<string> => {
const config = {
};
const LLLresponse = await callLLMapi(config, prompt);
return LLLresponse;
};
const convertFiles = async (filePaths: string[]) => {
const results = await convertTestFiles({
filePaths: filePaths,
jestBinaryPath: 'npx jest',
outputResultsPath: 'ai-conversion-testing/temp',
testId: 'data-test',
llmCallFunction: callLLMFunctionExample,
enableFeedbackStep: true,
});
console.log('Results:', results);
};
const enzymeFilePaths = [
'path/to/your/enzymeFile1.jest.tsx',
'path/to/your/enzymeFile2.jest.tsx',
];
convertFiles(enzymeFilePaths);
2. Running the conversion flow with individual methods for one file:
This approach gives you more control over the flow and allows you to inspect the output of each method. You may also want to extract only the AST-converted code.
Important: The methods must be called in the correct order, as the flow depends on it.
import {
initializeConfig,
convertWithAST,
getReactCompDom,
generateInitialPrompt,
extractCodeContentToFile,
runTestAndAnalyze,
generateFeedbackPrompt,
} from '@slack/enzyme-to-rtl-codemod';
import { callLLM } from './llm-helper';
const convertTestFile = async (filePath: string): Promise<void> => {
const config = initializeConfig({
filePath,
jestBinaryPath: 'npx jest',
outputResultsPath: 'ai-conversion-testing/temp',
testId: 'data-test',
});
const astConvertedCode = convertWithAST({
filePath,
testId: config.testId,
astTransformedFilePath: config.astTranformedFilePath,
});
const reactCompDom = await getReactCompDom({
filePath,
enzymeImportsPresent: config.enzymeImportsPresent,
filePathWithEnzymeAdapter: config.filePathWithEnzymeAdapter,
collectedDomTreeFilePath: config.collectedDomTreeFilePath,
enzymeMountAdapterFilePath: config.enzymeMountAdapterFilePath,
jestBinaryPath: config.jestBinaryPath,
reactVersion: config.reactVersion,
});
const initialPrompt = generateInitialPrompt({
filePath,
getByTestIdAttribute: config.testId,
astCodemodOutput: astConvertedCode,
renderedCompCode: reactCompDom,
originalTestCaseNum: config.originalTestCaseNum,
});
const LLMresponse = await callLLM(initialPrompt);
const convertedFilePath = extractCodeContentToFile({
LLMresponse,
rtlConvertedFilePath: config.rtlConvertedFilePathAttmp1,
});
const attempt1Result = await runTestAndAnalyze({
filePath: convertedFilePath,
jestBinaryPath: config.jestBinaryPath,
jestRunLogsPath: config.jestRunLogsFilePathAttmp1,
rtlConvertedFilePath: config.rtlConvertedFilePathAttmp1,
outputResultsPath: config.outputResultsPath,
originalTestCaseNum: config.originalTestCaseNum,
summaryFile: config.jsonSummaryPath,
attempt: 'attempt1',
});
let finalResult = attempt1Result;
if (!attempt1Result.attempt1.testPass) {
const feedbackPrompt = generateFeedbackPrompt({
rtlConvertedFilePathAttmpt1: config.rtlConvertedFilePathAttmp1,
getByTestIdAttribute: config.testId,
jestRunLogsFilePathAttmp1: config.jestRunLogsFilePathAttmp1,
renderedCompCode: reactCompDom,
originalTestCaseNum: config.originalTestCaseNum,
});
const LLMresponseAttmp2 = await callLLM(feedbackPrompt);
const convertedFeedbackFilePath = extractCodeContentToFile({
LLMresponse: LLMresponseAttmp2,
rtlConvertedFilePath: config.rtlConvertedFilePathAttmp2,
});
const attempt2Result = await runTestAndAnalyze({
filePath: convertedFeedbackFilePath,
jestBinaryPath: config.jestBinaryPath,
jestRunLogsPath: config.jestRunLogsFilePathAttmp2,
rtlConvertedFilePath: config.rtlConvertedFilePathAttmp2,
outputResultsPath: config.outputResultsPath,
originalTestCaseNum: config.originalTestCaseNum,
summaryFile: config.jsonSummaryPath,
attempt: 'attempt2',
});
finalResult = attempt2Result;
}
console.log('final result:', finalResult);
};
convertTestFile('<testPath1>');
3. Run conversion flow with cli and config for one file or more files:
TODO
Output results
Results will be written to the outputResultsPath/<timeStampFolder>/<filePath>/*
folder.
Example:
└── 2024-09-05_16-15-41
├── <file-path>
| ├── ast-transformed-<file_title>.test.tsx - AST converted/annotated file
| ├── attmp-1-jest-run-logs-<file_title>.md - Jest run logs for RTL file attempt 1
| ├── attmp-1-rtl-converted-<file_title>.test.tsx - Converted Enzyme to RTL file attempt 1
| ├── attmp-2-jest-run-logs-<file_title>.md - Jest run logs for RTL file attempt 2
| ├── attmp-2-rtl-converted-<file_title>.test.tsx - Converted Enzyme to RTL file attempt 2
| ├── dom-tree-<file_title>.csv - Collected DOM for each test case in Enzyme file
| ├── enzyme-mount-adapter.js - Enzyme rendering methods with DOM logs collection logic
| └── enzyme-mount-overwritten-<file_title>.test.tsx - Enzyme file with new methods that emit DOM
NOTE:
- This package will only work if your test files use Enzyme
mount
and shallow
imported directly from the Enzyme package. If your project uses helper methods to wrap Enzyme’s mount or shallow, this package may not work as expected.
import { mount } from 'enzyme';
- This package works only with jest, no other test runners have been tested
Debugging
- By default log level is
info
- Set the log level to
verbose
in convertFiles
or initializeConfig
Exported methods
This package exports the following:
convertTestFiles
- run the entire conversion flow in one function. Easy and fast way to start convertingLLMCallFunction
- llm call function typeinitializeConfig
- Initialize configuration settings required for the conversion process. This method prepares paths and settings, such as Jest binary, output paths, and test identifiers.convertWithAST
- Run jscodeshift and make AST conversions/annotations.getReactCompDom
- Get React component DOM for test cases.generateInitialPrompt
- Generate a prompt for an LLM to assist in converting Enzyme test cases to RTL.generateFeedbackPrompt
- Generate a feedback prompt for an LLM to assist in fixing React unit tests using RTL.extractCodeContentToFile
- Extract code content from an LLM response and write it to a file.runTestAndAnalyze
- Run an RTL test file with Jest and analyze the results.