The Migration Nobody Wanted to Hand-Roll
When React 18 landed without any viable Enzyme adapter, Slack faced an uncomfortable reality: more than 15,000 frontend tests needed conversion to React Testing Library (RTL). At roughly 10,000 engineering hours, this wasn't a migration team members could quietly knock out between feature work. It demanded automation.
The problem wasn't just volume, though. Enzyme and RTL operate on fundamentally different philosophies. Enzyme inspects component internals; RTL queries the rendered DOM the way a user would interact with it. That conceptual gap made simple find-and-replace conversions impossible. After evaluating existing tools and finding nothing suitable, the team set out to build their own solution.
Why AST Transformations Hit a Ceiling
The initial approach used Abstract Syntax Tree (AST) transformations, representing code as tree structures with nodes that could be queried and converted. For instance, wrapper.find('selector'); could be parsed and rewritten through targeted rules.

The strategy was to prioritize the most common patterns first. Rendering methods like mount and shallow were obvious starting points, along with the ten most frequently used Enzyme methods in the codebase:
[
{ method: 'find', count: 13244 },
{ method: 'prop', count: 3050 },
{ method: 'simulate', count: 2755 },
{ method: 'text', count: 2181 },
{ method: 'update', count: 2147 },
{ method: 'instance', count: 1549 },
{ method: 'props', count: 1522 },
{ method: 'hostNodes', count: 1477 },
{ method: 'exists', count: 1174 },
{ method: 'first', count: 684 },
... and 55 more methods
]
The core requirement was 100% correctness. Any wrong output would mean broken tests. That bar proved punishing: the team identified 65 distinct Enzyme methods across their code, each carrying its own quirks. Consider find, which accepts selector strings, component types, constructors, object properties, and supports nested filters like first or filter. Encoding all of that as transformation rules meant a rapidly expanding, fragile rule set.
A deeper limitation emerged: AST transformations only see the file being converted. They have no knowledge of the rendered component DOM or the React component source. That matters enormously in RTL, where the correct query method—getByRole versus getByTestId—depends on what's actually in the rendered output. AST simply cannot incorporate that context.
After establishing patterns for 10 Enzyme methods and common Jest matcher cases, the team concluded AST alone couldn't handle the scale. They shipped a codemod achieving 45% automatic conversion on evaluation files, adding comments with documentation links wherever manual intervention was needed. Engineering teams were told to run the codemod first, then finish the rest by hand.
The Inconsistency Problem with Standalone LLMs

Working with Slack's DevXP AI team, the team integrated Anthropic's Claude 2.1 through an internal API endpoint. Early results were volatile. Conversion success fluctuated between 40-60%, swinging from remarkably good Enzyme-to-RTL translation to disappointing output depending on task complexity. Prompt refinement gave limited improvements and sometimes made things worse by over-complicating the instructions.
Standalone AI couldn't deliver consistent results at the required scale. With both AST and pure LLM approaches falling short, the team reconsidered how humans actually do these conversions.
The Hybrid: Combining Code Structure with AI Context
Human engineers performing conversions draw on multiple sources simultaneously: the rendered component DOM, the React component code, AST insights, and years of frontend experience. The winning formula turned out to be feeding all of that context into an LLM.

The combined pipeline pushed conversion success to 80% on evaluation files—a 20-30% improvement over the LLM operating alone. Two changes drove most of the gains.
Collecting DOM Trees for Each Test Case
RTL fundamentally depends on rendered DOM structure, so the pipeline needed to capture it. The team built adaptors for Enzyme rendering methods that extracted the actual DOM produced during test execution. Because each test case can mount a component with different props and setups, DOM trees were collected per test case, then stored for LLM consumption.
// Import original methods
import enzyme, { mount as originalMount, shallow as originalShallow } from 'enzyme';
import fs from 'fs';
let currentTestCaseName: string | null = null;
beforeEach(() => {
// Set the current test case name before each test
const testName = expect.getState().currentTestName;
currentTestCaseName = testName ? testName.trim() : null;
});
afterEach(() => {
// Reset the current test case name after each test
currentTestCaseName = null;
});
// Override mount method
enzyme.mount = (node: React.ReactElement, options?: enzyme.MountRendererProps) => {
const wrapper = originalMount(node, options);
const htmlContent = wrapper.html();
if (process.env.DOM_TREE_FILE) {
fs.appendFileSync(
process.env.DOM_TREE_FILE,
`<test_case_title>${currentTestCaseName}</test_case_title> and <dom_tree>${htmlContent}</dom_tree>;\n`,
);
}
return wrapper;
};
...
Enzyme tests were run as part of the pipeline, with the rendering adaptors grabbing the DOM before the test proceeded.
Controlling the LLM Through Prompts and AST Suggestions
The second key move was imposing stricter control over hallucination and erratic responses. Two mechanisms did the heavy lifting.
Prompts were kept lean, split into three components rather than merged into one monstrosity. First, context setting established the translation task and general rules:
`I need assistance converting an Enzyme test case to the React Testing Library framework.
I will provide you with the Enzyme test file code inside <code></code> xml tags.
I will also give you the partially converted test file code inside <codemod></codemod> xml tags.
The rendered component DOM tree for each test case will be provided in <component></component> tags with this structure for one or more test cases "<test_case_title></test_case_title> and <dom_tree></dom_tree>".`
The main request section listed 10 required tasks and seven optional transformations in explicit detail:
`Please perform the following tasks:
1. Complete the conversion for the test file within <codemod></codemod> tags.
2. Convert all test cases and ensure the same number of tests in the file. ${numTestCasesString}
3. Replace Enzyme methods with the equivalent React Testing Library methods.
4. Update Enzyme imports to React Testing Library imports.
5. Adjust Jest matchers for React Testing Library.
6. Return the entire file with all converted test cases, enclosed in <code></code> tags.
7. Do not modify anything else, including imports for React components and helpers.
8. Preserve all abstracted functions as they are and use them in the converted file.
9. Maintain the original organization and naming of describe and it blocks.
10. Wrap component rendering into <Provider store={createTestStore()}><Component></Provider>. In order to do that you need to do two things
First, import these:
import { Provider } from '.../provider';
import createTestStore from '.../test-store';
Second, wrap component rendering in <Provider>, if it was not done before.
Example:
<Provider store={createTestStore()}>
<Component {...props} />
</Provider>
Ensure that all 10 conditions are met. The converted file should be runnable by Jest without any manual changes.
Other instructions section, use them when applicable:
1. "data-qa" attribute is configured to be used with "screen.getByTestId" queries.
2. Use these 4 augmented matchers that have "DOM" at the end to avoid conflicts with Enzyme
toBeCheckedDOM: toBeChecked,
toBeDisabledDOM: toBeDisabled,
toHaveStyleDOM: toHaveStyle,
toHaveValueDOM: toHaveValue
3. For user simulations use userEvent and import it with "import userEvent from '@testing-library/user-event';"
4. Prioritize queries in the following order getByRole, getByPlaceholderText, getByText, getByDisplayValue, getByAltText, getByTitle, then getByTestId.
5. Use query* variants only for non-existence checks: Example "expect(screen.query*('example')).not.toBeInTheDocument();"
6. Ensure all texts/strings are converted to lowercase regex expression. Example: screen.getByText(/your text here/i), screen.getByRole('button', {name: /your text here/i}).
7. When asserting that a DOM renders nothing, replace isEmptyRender()).toBe(true) with toBeEmptyDOMElement() by wrapping the component into a container. Example: expect(container).toBeEmptyDOMElement();`
Finally, instructions told the model how to evaluate its own output and present results:
`Now, please evaluate your output and make sure your converted code is between <code></code> tags.
If there are any deviations from the specified conditions, list them explicitly.
If the output adheres to all conditions and uses instructions section, you can simply state "The output meets all specified conditions."`
But the more effective control mechanism was AST-generated in-code guidance. Rather than sending raw test files to the LLM, the pipeline first ran the AST codemod to produce partially converted code. That output included comments inserted at every point requiring manual judgment, with suggestions and documentation links. Feeding this pre-annotated code to the LLM dramatically reduced hallucinations and nonsensical conversions.
The reasoning is straightforward: the AST codemod handles the simple, deterministic cases perfectly, while annotations steer the LLM away from making blind guesses on complex ones. Only an LLM can blend these disparate information sources—prompts, DOM trees, test file code, React components, test run logs, linter output, and AST annotations—into a coherent result.
The approach proved robust enough that Slack eventually open-sourced a version of the conversion tool as @slack/enzyme-to-rtl-codemod on npm. The migration path is still demanding, but the hybrid architecture turned a possibly year-long manual effort into a tractable engineering problem.
Measuring what the conversion actually saved
To quantify the success of the AI-powered conversion effort, the team tracked results through two execution channels: on-demand runs that produced output in 2-5 minutes, and nightly CI jobs capable of processing hundreds of files without straining infrastructure. Each nightly run categorized converted files by pass rate—fully converted, 50-99% passing, 20-49% passing, or under 20%—so developers could quickly spot and use the most reliable conversions. This arrangement let engineers skip script execution entirely for bulk work, while still offering the option to tweak original files locally for improved LLM performance on specific cases.
The adoption rate—files processed by the codemod divided by all files converted to React Testing Library—landed at roughly 64%. That figure reflects heavy usage by frontend developers and points to substantial time savings. Quality was assessed along two tracks. First, manual review of nine files of varying difficulty (three each of easy, medium, and complex) converted by both the LLM and human developers, using a rubric covering imports, rendering methods, JavaScript/TypeScript logic, and Jest assertions. The result: 80% of the code was accurate as-is, with the remaining 20% requiring human correction.
Second, the team examined pass rates across a broader sample—approximately 2,300 test cases in 338 files. Of those, roughly 500 test cases were successfully converted, executed, and passed, representing a 22% saving in developer time. That figure only counts cases where the test file actually ran; some conversions may have been valid but blocked by setup or import issues, so the true savings could be higher. Importantly, all generated code was human-verified before merging, keeping an expert in the loop throughout.

Lessons from the front lines
As the project wraps up in May 2024, the evidence shows that LLMs can meaningfully support developers and boost productivity. But the scarcity of published material on Enzyme-to-RTL conversion hints that this kind of migration is genuinely hard, and AI may not be the ultimate solution on its own. The team was fortunate that the chosen model handled JavaScript and TypeScript out of the box, with no extra training required—but custom implementations may be necessary to unlock an LLM's full potential in other contexts.
The custom codemod has proven reliable for large-scale migration work so far, earned positive feedback, and justified the investment in automation. The team is now exploring automated frontend unit test generation, an area developers view with optimism about what AI can do next.



