From dc74f5240a3f04d7df47d3366746db51560cd424 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 16 Jul 2026 14:55:12 -0400 Subject: [PATCH 1/3] Change Karma tests to run headlessly --- karma.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index f26089096a..65b9cbf1ad 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -68,7 +68,7 @@ module.exports = config => { } }, - browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome'], + browsers: env.BROWSER ? env.BROWSER.split(',') : ['ChromeHeadless'], singleRun: env.CONTINUOUS_INTEGRATION === 'true' }); From e909302dcc1c682eaf6c3cd4115a9fe5a632efad Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 17 Jul 2026 15:00:21 -0400 Subject: [PATCH 2/3] Replace remaining usage of Enzyme with RTL --- package.json | 2 - test/CheckboxSpec.js | 64 +++--- test/ControlLabelSpec.js | 34 ++-- test/FormControlFeedbackSpec.js | 29 ++- test/FormControlSpec.js | 70 +++---- test/FormControlStaticSpec.js | 37 ++-- test/FormSpec.js | 34 ++-- test/HelpBlockSpec.js | 18 +- test/InputGroupSpec.js | 27 +-- test/MenuItemSpec.js | 23 ++- test/NavSpec.js | 203 +++++++++---------- test/PanelGroupSpec.js | 48 ++--- test/PanelSpec.js | 155 ++++++++------ test/RadioSpec.js | 62 +++--- test/SafeAnchorSpec.js | 106 +++++----- test/TabContainerSpec.js | 216 ++++++++++---------- test/ToggleButtonGroupSpec.js | 52 +++-- test/helpers.js | 12 ++ test/index.js | 32 +-- yarn.lock | 347 +------------------------------- 20 files changed, 629 insertions(+), 942 deletions(-) diff --git a/package.json b/package.json index c2dc55bad4..5f7b46c629 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", - "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -82,7 +81,6 @@ "colors": "^1.2.1", "create-react-class": "^15.6.3", "cross-env": "^5.2.0", - "enzyme": "^3.1.0", "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", "eslint-config-prettier": "^2.9.0", diff --git a/test/CheckboxSpec.js b/test/CheckboxSpec.js index 9500622b44..dd08da409d 100644 --- a/test/CheckboxSpec.js +++ b/test/CheckboxSpec.js @@ -1,74 +1,60 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { mount, shallow } from 'enzyme'; import Checkbox from '../src/Checkbox'; -import { shouldWarn } from './helpers'; +import { assertNone, assertSingle, shouldWarn } from './helpers'; describe('', () => { it('should render correctly', () => { - const wrapper = shallow( - + const { container } = render( + My label ); - wrapper - .assertSingle('div.checkbox.my-checkbox') - .assertSingle('input[type="checkbox"][name="foo"][checked]'); + const div = assertSingle(container, 'div.checkbox.my-checkbox'); + const input = assertSingle(div, 'input[type="checkbox"][name="foo"]'); + expect(input.checked).to.equal(true); - wrapper - .assertSingle('label') - .text() - .should.equal('My label'); + assertSingle(container, 'label').textContent.should.equal('My label'); }); it('should support inline', () => { - const wrapper = shallow( + const { container } = render( My label ); - wrapper - .assertSingle('label.checkbox-inline.my-checkbox') - .assertSingle('input[type="checkbox"][name="foo"]'); + const label = assertSingle(container, 'label.checkbox-inline.my-checkbox'); + assertSingle(label, 'input[type="checkbox"][name="foo"]'); - wrapper - .assertSingle('label') - .text() - .should.equal('My label'); + assertSingle(container, 'label').textContent.should.equal('My label'); }); it('should support validation state', () => { - shallow().assertSingle( - '.has-success' - ); + const { container } = render(); + assertSingle(container, '.has-success'); }); it('should not support validation state when inline', () => { shouldWarn('ignored'); - shallow() - .find('.has-success') - .should.have.length(0); + const { container } = render(); + assertNone(container, '.has-success'); }); it('should support inputRef', () => { - class Container extends React.Component { - render() { - return ( - { - this.input = ref; - }} - /> - ); - } - } - - const instance = mount().instance(); + let input; + render( + { + input = ref; + }} + /> + ); - expect(instance.input.tagName).to.equal('INPUT'); + expect(input.tagName).to.equal('INPUT'); }); }); diff --git a/test/ControlLabelSpec.js b/test/ControlLabelSpec.js index f7bcd054eb..f1b517a84f 100644 --- a/test/ControlLabelSpec.js +++ b/test/ControlLabelSpec.js @@ -1,45 +1,47 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { mount, shallow } from 'enzyme'; import ControlLabel from '../src/ControlLabel'; import FormGroup from '../src/FormGroup'; -import { shouldWarn } from './helpers'; +import { assertSingle, shouldWarn } from './helpers'; describe('', () => { it('should render correctly', () => { + const { container } = render( + + Label + + ); + expect( - shallow( - - Label - - ) - .assertSingle('label.control-label.my-control-label[htmlFor="foo"]') - .text() + assertSingle(container, 'label.control-label.my-control-label[for="foo"]') + .textContent ).to.equal('Label'); }); it('should respect srOnly', () => { - shallow(Label).assertSingle( - 'label.control-label.sr-only' - ); + const { container } = render(Label); + assertSingle(container, 'label.control-label.sr-only'); }); it('should use controlId for htmlFor', () => { - mount( + const { container } = render( Label - ).assertSingle('label.control-label[htmlFor="foo"]'); + ); + assertSingle(container, 'label.control-label[for="foo"]'); }); it('should prefer explicit htmlFor', () => { shouldWarn('ignored'); - mount( + const { container } = render( Label - ).assertSingle('label.control-label[htmlFor="bar"]'); + ); + assertSingle(container, 'label.control-label[for="bar"]'); }); }); diff --git a/test/FormControlFeedbackSpec.js b/test/FormControlFeedbackSpec.js index 7a726a1b22..adb812260c 100644 --- a/test/FormControlFeedbackSpec.js +++ b/test/FormControlFeedbackSpec.js @@ -1,42 +1,48 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { mount } from 'enzyme'; import FormControl from '../src/FormControl'; import FormGroup from '../src/FormGroup'; +import { assertSingle } from './helpers'; + describe('', () => { it('should render default success', () => { - mount( + const { container } = render( - ).assertSingle('.form-control-feedback.glyphicon-ok'); + ); + assertSingle(container, '.form-control-feedback.glyphicon-ok'); }); it('should render default warning', () => { - mount( + const { container } = render( - ).assertSingle('.form-control-feedback.glyphicon-warning-sign'); + ); + assertSingle(container, '.form-control-feedback.glyphicon-warning-sign'); }); it('should render default error', () => { - mount( + const { container } = render( - ).assertSingle('.form-control-feedback.glyphicon-remove'); + ); + assertSingle(container, '.form-control-feedback.glyphicon-remove'); }); it('should render default validation state', () => { - mount( + const { container } = render(
- ).assertSingle('.form-control-feedback.glyphicon-ok'); + ); + assertSingle(container, '.form-control-feedback.glyphicon-ok'); }); it('should render custom component', () => { @@ -44,10 +50,11 @@ describe('', () => { return
; } - mount( + const { container } = render( - ).assertSingle('MyComponent.foo.form-control-feedback'); + ); + assertSingle(container, '.foo.form-control-feedback'); }); }); diff --git a/test/FormControlSpec.js b/test/FormControlSpec.js index 6632ab0e3a..e4e190509c 100644 --- a/test/FormControlSpec.js +++ b/test/FormControlSpec.js @@ -1,78 +1,72 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { mount, shallow } from 'enzyme'; import FormControl from '../src/FormControl'; import FormGroup from '../src/FormGroup'; -import { shouldWarn } from './helpers'; +import { assertNone, assertSingle, shouldWarn } from './helpers'; describe('', () => { it('should render correctly', () => { - shallow( + const { container } = render( - ).assertSingle('input#foo.form-control.my-control[name="bar"]'); + ); + assertSingle(container, 'input#foo.form-control.my-control[name="bar"]'); }); it('should support textarea', () => { - shallow().assertSingle( - 'textarea.form-control' - ); + const { container } = render(); + assertSingle(container, 'textarea.form-control'); }); it('should support select', () => { - shallow().assertSingle( - 'select.form-control' - ); + const { container } = render(); + assertSingle(container, 'select.form-control'); }); it('should not render .form-control for type="file"', () => { - shallow() - .assertSingle('input[type="file"]') - .find('.form-control') - .should.have.length(0); + const { container } = render(); + assertSingle(container, 'input[type="file"]'); + assertNone(container, '.form-control'); }); it('should use controlId for id', () => { - mount( + const { container } = render( - ).assertSingle('input#foo.form-control'); + ); + assertSingle(container, 'input#foo.form-control'); }); it('should prefer explicit id', () => { shouldWarn('ignored'); - mount( + const { container } = render( - ).assertSingle('input#bar.form-control'); + ); + assertSingle(container, 'input#bar.form-control'); }); it('should support inputRef', () => { - class Container extends React.Component { - render() { - return ( - - { - this.input = ref; - }} - /> - - ); - } - } - - const instance = mount().instance(); - expect(instance.input.tagName).to.equal('INPUT'); + let input; + render( + + { + input = ref; + }} + /> + + ); + expect(input.tagName).to.equal('INPUT'); }); it('should properly display size of FormControl', () => { - mount().assertSingle( - 'input.form-control.input-lg' - ); + const { container } = render(); + assertSingle(container, 'input.form-control.input-lg'); }); }); diff --git a/test/FormControlStaticSpec.js b/test/FormControlStaticSpec.js index 50047127b5..288a51a018 100644 --- a/test/FormControlStaticSpec.js +++ b/test/FormControlStaticSpec.js @@ -1,18 +1,21 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { shallow } from 'enzyme'; import FormControl from '../src/FormControl'; +import { assertSingle } from './helpers'; + describe('', () => { it('should render correctly', () => { + const { container } = render( + + Static text + + ); + expect( - shallow( - - Static text - - ) - .assertSingle('.form-control-static.my-form-control-static') - .text() + assertSingle(container, '.form-control-static.my-form-control-static') + .textContent ).to.equal('Static text'); }); @@ -21,14 +24,14 @@ describe('', () => { return
{children}
; } - expect( - shallow( - - Static text - - ) - .assertSingle('MyComponent.form-control-static') - .contains('Static text') - ).to.equal(true); + const { container } = render( + + Static text + + ); + + const node = assertSingle(container, '.form-control-static'); + assert.equal(node.nodeName, 'DIV'); + expect(node.textContent).to.contain('Static text'); }); }); diff --git a/test/FormSpec.js b/test/FormSpec.js index a53aad9224..8972ac912c 100644 --- a/test/FormSpec.js +++ b/test/FormSpec.js @@ -1,37 +1,45 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { shallow } from 'enzyme'; import Form from '../src/Form'; import FormGroup from '../src/FormGroup'; +import { assertSingle } from './helpers'; + describe('
', () => { it('should support horizontal', () => { - shallow( + const { container } = render( - ) - .assertSingle('form.form-horizontal.my-form') - .assertSingle(FormGroup); + ); + + const form = assertSingle(container, 'form.form-horizontal.my-form'); + assertSingle(form, '.form-group'); }); it('should support inline', () => { - shallow( + const { container } = render(
- ) - .assertSingle('form.form-inline.my-form') - .assertSingle(FormGroup); + ); + + const form = assertSingle(container, 'form.form-inline.my-form'); + assertSingle(form, '.form-group'); }); it('should support custom componentClass', () => { - shallow( + const { container } = render(
- ) - .assertSingle('fieldset.form-horizontal.my-form') - .assertSingle(FormGroup); + ); + + const fieldset = assertSingle( + container, + 'fieldset.form-horizontal.my-form' + ); + assertSingle(fieldset, '.form-group'); }); }); diff --git a/test/HelpBlockSpec.js b/test/HelpBlockSpec.js index 0a07d501d5..123bd51834 100644 --- a/test/HelpBlockSpec.js +++ b/test/HelpBlockSpec.js @@ -1,18 +1,20 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { shallow } from 'enzyme'; import HelpBlock from '../src/HelpBlock'; +import { assertSingle } from './helpers'; + describe('', () => { it('should render correctly', () => { + const { container } = render( + + Help contents + + ); + expect( - shallow( - - Help contents - - ) - .assertSingle('#foo.help-block.my-help-block') - .text() + assertSingle(container, '#foo.help-block.my-help-block').textContent ).to.equal('Help contents'); }); }); diff --git a/test/InputGroupSpec.js b/test/InputGroupSpec.js index 35f4568c89..ef02a37e61 100644 --- a/test/InputGroupSpec.js +++ b/test/InputGroupSpec.js @@ -1,13 +1,15 @@ +import { render } from '@testing-library/react'; import React from 'react'; -import { mount, shallow } from 'enzyme'; import Button from '../src/Button'; import FormControl from '../src/FormControl'; import InputGroup from '../src/InputGroup'; +import { assertSingle } from './helpers'; + describe('', () => { it('should render properly', () => { - const wrapper = mount( + const { container } = render( Foo @@ -17,21 +19,22 @@ describe('', () => { - ).assertSingle('.input-group.my-input-group'); + ); + const wrapper = assertSingle(container, '.input-group.my-input-group'); - wrapper - .assertSingle('.input-group-addon.my-addon') - .text() - .should.equal('Foo'); + assertSingle( + wrapper, + '.input-group-addon.my-addon' + ).textContent.should.equal('Foo'); - wrapper.assertSingle('input.form-control[type="text"]'); + assertSingle(wrapper, 'input.form-control[type="text"]'); - wrapper.assertSingle('.input-group-btn.my-button').assertSingle(Button); + const button = assertSingle(wrapper, '.input-group-btn.my-button'); + assertSingle(button, '.btn'); }); it('should support bsSize', () => { - shallow().assertSingle( - '.input-group.input-group-sm' - ); + const { container } = render(); + assertSingle(container, '.input-group.input-group-sm'); }); }); diff --git a/test/MenuItemSpec.js b/test/MenuItemSpec.js index 2493b3724b..1343d35df2 100644 --- a/test/MenuItemSpec.js +++ b/test/MenuItemSpec.js @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { shallow } from 'enzyme'; import MenuItem from '../src/MenuItem'; @@ -146,17 +145,23 @@ describe('', () => { }); it('does not pass onClick to DOM node', () => { - shallow( {}}>Item) - .children() - .props() - .should.not.have.property('onSelect'); + // `onSelect` is an internal prop and must not leak onto the rendered DOM + // node. There is no DOM attribute for `onSelect`, so the closest observable + // check is that the rendered anchor carries no `onselect` attribute (and no + // React unknown-prop warning is emitted, which the harness would surface). + const { container } = render( {}}>Item); + const anchor = container.querySelector('a'); + + assert.equal(anchor.getAttribute('onselect'), null); }); it('does not pass onClick to children', () => { - shallow( {}}>Item) - .find('SafeAnchor') - .props() - .should.not.have.property('onSelect'); + // Same intent as above: `onSelect` is omitted before being spread onto the + // child SafeAnchor, so it never reaches the rendered anchor element. + const { container } = render( {}}>Item); + const anchor = container.querySelector('a'); + + assert.equal(anchor.getAttribute('onselect'), null); }); it('disabled link', async () => { diff --git a/test/NavSpec.js b/test/NavSpec.js index 892090cccc..ff7aee81ff 100644 --- a/test/NavSpec.js +++ b/test/NavSpec.js @@ -1,7 +1,6 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { mount } from 'enzyme'; import Nav from '../src/Nav'; import NavItem from '../src/NavItem'; @@ -136,32 +135,41 @@ describe('