Skip to content

Commit cb7c4ab

Browse files
feat: upgrade React 16 to 18 and migrate Enzyme tests to RTL
Upgrade react/react-dom to ^18.3.1, react-mosaic-component to ^6.1.1, and @testing-library/react to ^16.1.0. Migrate ReactDOM.render() to createRoot(), add Blueprint v3 type augmentations for React 18 compatibility, remove Enzyme and migrate all 21 Enzyme test files to React Testing Library. Fix React 18 behavioral changes in controlled inputs and MobX observer re-rendering. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 82c0ce3 commit cb7c4ab

File tree

53 files changed

+1432
-5103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1432
-5103
lines changed

package.json

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@
6767
"package-json": "^7.0.0",
6868
"parse-env-string": "^1.0.1",
6969
"prettier": "^3.6.2",
70-
"react": "^16.14.0",
71-
"react-dom": "^16.14.0",
72-
"react-mosaic-component": "^4.1.1",
70+
"react": "^18.3.1",
71+
"react-dom": "^18.3.1",
72+
"react-mosaic-component": "^6.1.1",
7373
"react-window": "^1.8.10",
7474
"semver": "^7.3.4",
7575
"shell-env": "^3.0.1",
@@ -94,18 +94,16 @@
9494
"@reforged/maker-appimage": "^5.1.0",
9595
"@testing-library/dom": "^10.4.0",
9696
"@testing-library/jest-dom": "^6.6.3",
97-
"@testing-library/react": "^12.1.5",
97+
"@testing-library/react": "^16.1.0",
9898
"@testing-library/user-event": "^14.5.2",
9999
"@tsconfig/node22": "^22.0.2",
100100
"@types/classnames": "^2.2.11",
101-
"@types/enzyme": "^3.10.8",
102-
"@types/enzyme-adapter-react-16": "^1.0.6",
103101
"@types/fs-extra": "^9.0.7",
104102
"@types/getos": "^3.0.1",
105103
"@types/node": "^22.19.1",
106104
"@types/parse-env-string": "^1.0.2",
107-
"@types/react": "^16.14.0",
108-
"@types/react-dom": "^16.9.11",
105+
"@types/react": "^18.3.0",
106+
"@types/react-dom": "^18.3.0",
109107
"@types/react-window": "^1.8.8",
110108
"@types/semver": "^7.3.4",
111109
"@types/tmp": "0.2.0",
@@ -116,9 +114,6 @@
116114
"copy-webpack-plugin": "^11.0.0",
117115
"css-loader": "^6.7.1",
118116
"electron": "^40.1.0",
119-
"enzyme": "^3.11.0",
120-
"enzyme-adapter-react-16": "^1.15.7",
121-
"enzyme-to-json": "^3.6.1",
122117
"eslint": "^8.45.0",
123118
"eslint-config-prettier": "^8.8.0",
124119
"eslint-import-resolver-typescript": "^3.5.5",
@@ -167,7 +162,8 @@
167162
},
168163
"resolutions": {
169164
"@electron-forge/maker-base": "8.0.0-alpha.3",
170-
"@electron-forge/shared-types": "8.0.0-alpha.3"
165+
"@electron-forge/shared-types": "8.0.0-alpha.3",
166+
"@types/react": "^18.3.0"
171167
},
172168
"packageManager": "[email protected]+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
173169
"dependenciesMeta": {

rtl-spec/components/commands-address-bar.spec.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import { render } from '@testing-library/react';
3+
import { render, waitFor } from '@testing-library/react';
44
import { userEvent } from '@testing-library/user-event';
55
import { runInAction } from 'mobx';
66
import { beforeEach, describe, expect, it } from 'vitest';
@@ -76,7 +76,9 @@ describe('AddressBar component', () => {
7676
runInAction(() => {
7777
store.activeGistAction = action;
7878
});
79-
const btn = getByRole('button');
80-
expect(btn).toBeDisabled();
79+
await waitFor(() => {
80+
const btn = getByRole('button');
81+
expect(btn).toBeDisabled();
82+
});
8183
});
8284
});

rtl-spec/components/commands-publish-button.spec.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Octokit } from '@octokit/rest';
2+
import { act, waitFor } from '@testing-library/react';
23
import { beforeEach, describe, expect, it, vi } from 'vitest';
34

45
import {
@@ -273,7 +274,7 @@ describe('Action button component', () => {
273274
// create a button that's primed to update gistId
274275
state.gistId = gistId;
275276
({ instance } = createActionButton());
276-
instance.setState({ actionType: GistActionType.update });
277+
act(() => instance.setState({ actionType: GistActionType.update }));
277278

278279
mocktokit.gists.get.mockImplementation(() => {
279280
return {
@@ -316,7 +317,7 @@ describe('Action button component', () => {
316317

317318
// create a button primed to delete gistId
318319
({ instance } = createActionButton());
319-
instance.setState({ actionType: GistActionType.delete });
320+
act(() => instance.setState({ actionType: GistActionType.delete }));
320321
});
321322

322323
it('attempts to delete an existing Gist', async () => {
@@ -349,11 +350,19 @@ describe('Action button component', () => {
349350
} = createActionButton();
350351
expect(container.querySelector('fieldset')).not.toBeDisabled();
351352

352-
state.activeGistAction = gistActionState;
353-
expect(container.querySelector('fieldset')).toBeDisabled();
353+
act(() => {
354+
state.activeGistAction = gistActionState;
355+
});
356+
await waitFor(() => {
357+
expect(container.querySelector('fieldset')).toBeDisabled();
358+
});
354359

355-
state.activeGistAction = GistActionState.none;
356-
expect(container.querySelector('fieldset')).not.toBeDisabled();
360+
act(() => {
361+
state.activeGistAction = GistActionState.none;
362+
});
363+
await waitFor(() => {
364+
expect(container.querySelector('fieldset')).not.toBeDisabled();
365+
});
357366
}
358367

359368
it('while publishing', async () => {

rtl-spec/components/editors-toolbar-button.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe('Editor toolbar button component', () => {
4646
getRoot: vi.fn(),
4747
},
4848
mosaicId: 'test',
49+
blueprintNamespace: 'bp3',
4950
};
5051

5152
({ state: store } = window.app);

src/blueprint-react18.d.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Type augmentations for Blueprint.js v3 components to work with React 18.
3+
*
4+
* React 18 removed implicit `children` from React.Component props.
5+
* Blueprint v3 class components don't declare `children` in their prop types,
6+
* so they fail type-checking with \@types/react 18.
7+
*
8+
* This file adds `children` to the affected Blueprint component props.
9+
* It can be removed when Blueprint is replaced with shadcn/ui.
10+
*/
11+
12+
import { ReactNode } from 'react';
13+
14+
declare module '@blueprintjs/core' {
15+
interface IAlertProps {
16+
children?: ReactNode;
17+
}
18+
interface IDialogProps {
19+
children?: ReactNode;
20+
}
21+
interface IFormGroupProps {
22+
children?: ReactNode;
23+
}
24+
interface IRadioGroupProps {
25+
children?: ReactNode;
26+
}
27+
}
28+
29+
declare module '@blueprintjs/select' {
30+
interface ISelectProps<T> {
31+
children?: ReactNode;
32+
}
33+
}
34+
35+
declare module '@blueprintjs/popover2' {
36+
interface IPopover2Props<T> {
37+
children?: ReactNode;
38+
}
39+
interface ITooltip2Props<T> {
40+
children?: ReactNode;
41+
}
42+
}

src/renderer/app.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class App {
113113
* Initial setup call, loading Monaco and kicking off the React
114114
* render process.
115115
*/
116-
public async setup(): Promise<void | Element | React.Component> {
116+
public async setup(): Promise<void> {
117117
if (this.state.isUsingSystemTheme) {
118118
await this.loadTheme(getCurrentTheme().file);
119119
} else {
@@ -122,13 +122,13 @@ export class App {
122122

123123
const [
124124
{ default: React },
125-
{ render },
125+
{ createRoot },
126126
{ Dialogs },
127127
{ OutputEditorsWrapper },
128128
{ Header },
129129
] = await Promise.all([
130130
import('react'),
131-
import('react-dom'),
131+
import('react-dom/client'),
132132
import('./components/dialogs.js'),
133133
import('./components/output-editors-wrapper.js'),
134134
import('./components/header.js'),
@@ -146,7 +146,8 @@ export class App {
146146
</div>
147147
);
148148

149-
const rendered = render(app, document.getElementById('app'));
149+
const root = createRoot(document.getElementById('app')!);
150+
root.render(app);
150151

151152
this.setupResizeListener();
152153
this.setupOfflineListener();
@@ -160,8 +161,6 @@ export class App {
160161
window.ElectronFiddle.addEventListener('set-show-me-template', () => {
161162
window.ElectronFiddle.setShowMeTemplate(this.state.templateName);
162163
});
163-
164-
return rendered;
165164
}
166165

167166
private setupTypeListeners() {

src/renderer/components/output.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const Output = observer(
133133

134134
const { isConsoleShowing } = this.props.appState;
135135

136-
if (this.context.mosaicActions) {
136+
if (this.context?.mosaicActions) {
137137
const mosaicTree = this.context.mosaicActions.getRoot();
138138
if (isParentNode(mosaicTree)) {
139139
// splitPercentage defines the percentage of space the first panel takes

src/renderer/components/settings-electron.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,14 @@ export const ElectronSettings = observer(
204204
* Handles a change in which channels should be displayed.
205205
*/
206206
public handleChannelChange(event: React.FormEvent<HTMLInputElement>) {
207-
const { id, checked } = event.currentTarget;
207+
const { id } = event.currentTarget;
208208
const { appState } = this.props;
209+
const channel = id as ElectronReleaseChannel;
209210

210-
if (!checked) {
211-
appState.hideChannels([id as ElectronReleaseChannel]);
211+
if (appState.channelsToShow.includes(channel)) {
212+
appState.hideChannels([channel]);
212213
} else {
213-
appState.showChannels([id as ElectronReleaseChannel]);
214+
appState.showChannels([channel]);
214215
}
215216
}
216217

tests/renderer/app-spec.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { act } from '@testing-library/react';
12
import * as semver from 'semver';
23
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
34

@@ -59,10 +60,15 @@ describe('App component', () => {
5960
it('renders the app', async () => {
6061
vi.useFakeTimers();
6162

62-
const result = (await app.setup()) as HTMLDivElement;
63-
vi.runAllTimers();
63+
await act(async () => {
64+
await app.setup();
65+
await vi.advanceTimersByTimeAsync(100);
66+
});
6467

65-
expect(result.innerHTML).toBe('Header;OutputEditorsWrapper;Dialogs;');
68+
const appEl = document.getElementById('app')!;
69+
expect(appEl.innerHTML).toContain('Header;');
70+
expect(appEl.innerHTML).toContain('OutputEditorsWrapper;');
71+
expect(appEl.innerHTML).toContain('Dialogs;');
6672

6773
vi.useRealTimers();
6874
});

tests/renderer/components/__snapshots__/dialog-add-theme-spec.tsx.snap

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)